Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.1.0-alpha.4"
".": "0.1.0-alpha.5"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company%2Fsfc-nodes-c9d6d56eabd56a40a29dc2639a77d22dd5394ecd3ec9aeaebb3a3977811571da.yml
openapi_spec_hash: beda3f45c48679e14d6fe8bbe7003d51
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/the-san-francisco-compute-company%2Fsfc-nodes-7686e4482b4198d5272206797ea47239ffa1beba6e49f3db5a257e4c7d3f832d.yml
openapi_spec_hash: 19955ca78c8b6ebca90ab66394aca311
config_hash: a187153315a646ecf95709ee4a223df5
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## 0.1.0-alpha.5 (2025-12-19)

Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sfcompute/nodes-go/compare/v0.1.0-alpha.4...v0.1.0-alpha.5)

### Features

* **api:** api update ([dd70400](https://github.com/sfcompute/nodes-go/commit/dd70400abff7e7332cf7a7f3dee59e9e2e4b61b5))
* **encoder:** support bracket encoding form-data object members ([bf75f99](https://github.com/sfcompute/nodes-go/commit/bf75f99a26c353742229dabb561684545d0835c7))


### Bug Fixes

* **mcp:** correct code tool API endpoint ([2eeb64b](https://github.com/sfcompute/nodes-go/commit/2eeb64be8909979b8cb3524a077cb764c85a1f21))
* rename param to avoid collision ([f221c75](https://github.com/sfcompute/nodes-go/commit/f221c7569b746c43bd46c4ab3c1615b30fc0c05c))
* skip usage tests that don't work with Prism ([61d03bd](https://github.com/sfcompute/nodes-go/commit/61d03bd3c1f7772a74bef48676020df27c371ab1))


### Chores

* add float64 to valid types for RegisterFieldValidator ([499e663](https://github.com/sfcompute/nodes-go/commit/499e663b659fa00c013ebe3db1e0622b5e2a6a51))
* elide duplicate aliases ([9e83189](https://github.com/sfcompute/nodes-go/commit/9e83189fd37d851e8441c313c738656a72760483))
* **internal:** codegen related update ([e40a3de](https://github.com/sfcompute/nodes-go/commit/e40a3debee22efa7015e5230d14374245af79b1f))

## 0.1.0-alpha.4 (2025-12-01)

Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sfcompute/nodes-go/compare/v0.1.0-alpha.3...v0.1.0-alpha.4)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->

```sh
go get -u 'github.com/sfcompute/nodes-go@v0.1.0-alpha.4'
go get -u 'github.com/sfcompute/nodes-go@v0.1.0-alpha.5'
```

<!-- x-release-please-end -->
Expand Down
80 changes: 44 additions & 36 deletions internal/apiform/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type encoderField struct {
type encoderEntry struct {
reflect.Type
dateFormat string
arrayFmt string
root bool
}

Expand All @@ -77,6 +78,7 @@ func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
entry := encoderEntry{
Type: t,
dateFormat: e.dateFormat,
arrayFmt: e.arrayFmt,
root: e.root,
}

Expand Down Expand Up @@ -178,34 +180,9 @@ func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
}
}

func arrayKeyEncoder(arrayFmt string) func(string, int) string {
var keyFn func(string, int) string
switch arrayFmt {
case "comma", "repeat":
keyFn = func(k string, _ int) string { return k }
case "brackets":
keyFn = func(key string, _ int) string { return key + "[]" }
case "indices:dots":
keyFn = func(k string, i int) string {
if k == "" {
return strconv.Itoa(i)
}
return k + "." + strconv.Itoa(i)
}
case "indices:brackets":
keyFn = func(k string, i int) string {
if k == "" {
return strconv.Itoa(i)
}
return k + "[" + strconv.Itoa(i) + "]"
}
}
return keyFn
}

func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
itemEncoder := e.typeEncoder(t.Elem())
keyFn := arrayKeyEncoder(e.arrayFmt)
keyFn := e.arrayKeyEncoder()
return func(key string, v reflect.Value, writer *multipart.Writer) error {
if keyFn == nil {
return fmt.Errorf("apiform: unsupported array format")
Expand Down Expand Up @@ -303,13 +280,10 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
})

return func(key string, value reflect.Value, writer *multipart.Writer) error {
if key != "" {
key = key + "."
}

keyFn := e.objKeyEncoder(key)
for _, ef := range encoderFields {
field := value.FieldByIndex(ef.idx)
err := ef.fn(key+ef.tag.name, field, writer)
err := ef.fn(keyFn(ef.tag.name), field, writer)
if err != nil {
return err
}
Expand Down Expand Up @@ -405,6 +379,43 @@ func (e *encoder) newReaderTypeEncoder() encoderFunc {
}
}

func (e encoder) arrayKeyEncoder() func(string, int) string {
var keyFn func(string, int) string
switch e.arrayFmt {
case "comma", "repeat":
keyFn = func(k string, _ int) string { return k }
case "brackets":
keyFn = func(key string, _ int) string { return key + "[]" }
case "indices:dots":
keyFn = func(k string, i int) string {
if k == "" {
return strconv.Itoa(i)
}
return k + "." + strconv.Itoa(i)
}
case "indices:brackets":
keyFn = func(k string, i int) string {
if k == "" {
return strconv.Itoa(i)
}
return k + "[" + strconv.Itoa(i) + "]"
}
}
return keyFn
}

func (e encoder) objKeyEncoder(parent string) func(string) string {
if parent == "" {
return func(child string) string { return child }
}
switch e.arrayFmt {
case "brackets":
return func(child string) string { return parent + "[" + child + "]" }
default:
return func(child string) string { return parent + "." + child }
}
}

// Given a []byte of json (may either be an empty object or an object that already contains entries)
// encode all of the entries in the map to the json byte array.
func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipart.Writer) error {
Expand All @@ -413,10 +424,6 @@ func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipar
value reflect.Value
}

if key != "" {
key = key + "."
}

pairs := []mapPair{}

iter := v.MapRange()
Expand All @@ -434,8 +441,9 @@ func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipar
})

elementEncoder := e.typeEncoder(v.Type().Elem())
keyFn := e.objKeyEncoder(key)
for _, p := range pairs {
err := elementEncoder(key+string(p.key), p.value, writer)
err := elementEncoder(keyFn(p.key), p.value, writer)
if err != nil {
return err
}
Expand Down
51 changes: 50 additions & 1 deletion internal/apiform/form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ type StructUnion struct {
param.APIUnion
}

type MultipartMarshalerParent struct {
Middle MultipartMarshalerMiddleNext `form:"middle"`
}

type MultipartMarshalerMiddleNext struct {
MiddleNext MultipartMarshalerMiddle `form:"middleNext"`
}

type MultipartMarshalerMiddle struct {
Child int `form:"child"`
}

var tests = map[string]struct {
buf string
val any
Expand Down Expand Up @@ -366,6 +378,19 @@ true
},
},
},
"recursive_struct,brackets": {
`--xxx
Content-Disposition: form-data; name="child[name]"

Alex
--xxx
Content-Disposition: form-data; name="name"

Robert
--xxx--
`,
Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
},

"recursive_struct": {
`--xxx
Expand Down Expand Up @@ -529,6 +554,30 @@ Content-Disposition: form-data; name="union"
Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
},
},
"deeply-nested-struct,brackets": {
`--xxx
Content-Disposition: form-data; name="middle[middleNext][child]"

10
--xxx--
`,
MultipartMarshalerParent{
Middle: MultipartMarshalerMiddleNext{
MiddleNext: MultipartMarshalerMiddle{
Child: 10,
},
},
},
},
"deeply-nested-map,brackets": {
`--xxx
Content-Disposition: form-data; name="middle[middleNext][child]"

10
--xxx--
`,
map[string]any{"middle": map[string]any{"middleNext": map[string]any{"child": 10}}},
},
}

func TestEncode(t *testing.T) {
Expand All @@ -553,7 +602,7 @@ func TestEncode(t *testing.T) {
}
raw := buf.Bytes()
if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") {
t.Errorf("expected %+#v to serialize to '%s' but got '%s'", test.val, test.buf, string(raw))
t.Errorf("expected %+#v to serialize to '%s' but got '%s' (with format %s)", test.val, test.buf, string(raw), arrayFmt)
}
})
}
Expand Down
2 changes: 1 addition & 1 deletion internal/apijson/enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type validatorFunc func(reflect.Value) exactness
var validators sync.Map
var validationRegistry = map[reflect.Type][]validationEntry{}

func RegisterFieldValidator[T any, V string | bool | int](fieldName string, values ...V) {
func RegisterFieldValidator[T any, V string | bool | int | float64](fieldName string, values ...V) {
var t T
parentType := reflect.TypeOf(t)

Expand Down
2 changes: 1 addition & 1 deletion internal/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

package internal

const PackageVersion = "0.1.0-alpha.4" // x-release-please-version
const PackageVersion = "0.1.0-alpha.5" // x-release-please-version
17 changes: 14 additions & 3 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,16 @@ const (
AcceleratorTypeH200 AcceleratorType = "H200"
)

// The properties DesiredCount, MaxPricePerNodeHour, Zone are required.
// The properties DesiredCount, MaxPricePerNodeHour are required.
type CreateNodesRequestParam struct {
DesiredCount int64 `json:"desired_count,required"`
// Max price per hour for a node in cents
MaxPricePerNodeHour int64 `json:"max_price_per_node_hour,required"`
// Zone to create the nodes in
Zone string `json:"zone,required"`
// End time as Unix timestamp in seconds If provided, end time must be aligned to
// the hour If not provided, the node will be created as an autoreserved node
EndAt param.Opt[int64] `json:"end_at,omitzero"`
// Allow auto reserved nodes to be created in any zone that meets the requirements
AnyZone param.Opt[bool] `json:"any_zone,omitzero"`
// User script to be executed during the VM's boot process Data should be base64
// encoded
CloudInitUserData param.Opt[string] `json:"cloud_init_user_data,omitzero" format:"byte"`
Expand All @@ -143,6 +143,9 @@ type CreateNodesRequestParam struct {
// Start time as Unix timestamp in seconds Optional for reserved nodes. If not
// provided, defaults to now
StartAt param.Opt[int64] `json:"start_at,omitzero"`
// Zone to create the nodes in. Required for auto reserved nodes if any_zone is
// false.
Zone param.Opt[string] `json:"zone,omitzero"`
// Custom node names Names cannot begin with 'vm*' or 'n*' as this is reserved for
// system-generated IDs Names cannot be numeric strings Names cannot exceed 128
// characters
Expand Down Expand Up @@ -265,6 +268,7 @@ type ListResponseNodeDataCurrentVM struct {
// Any of "Pending", "Running", "Destroyed", "NodeFailure", "Unspecified".
Status string `json:"status,required"`
UpdatedAt int64 `json:"updated_at,required"`
Zone string `json:"zone,required"`
ImageID string `json:"image_id,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
Expand All @@ -275,6 +279,7 @@ type ListResponseNodeDataCurrentVM struct {
StartAt respjson.Field
Status respjson.Field
UpdatedAt respjson.Field
Zone respjson.Field
ImageID respjson.Field
ExtraFields map[string]respjson.Field
raw string
Expand Down Expand Up @@ -314,6 +319,7 @@ type ListResponseNodeDataVMsData struct {
// Any of "Pending", "Running", "Destroyed", "NodeFailure", "Unspecified".
Status string `json:"status,required"`
UpdatedAt int64 `json:"updated_at,required"`
Zone string `json:"zone,required"`
ImageID string `json:"image_id,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
Expand All @@ -324,6 +330,7 @@ type ListResponseNodeDataVMsData struct {
StartAt respjson.Field
Status respjson.Field
UpdatedAt respjson.Field
Zone respjson.Field
ImageID respjson.Field
ExtraFields map[string]respjson.Field
raw string
Expand Down Expand Up @@ -405,6 +412,7 @@ type NodeCurrentVM struct {
// Any of "Pending", "Running", "Destroyed", "NodeFailure", "Unspecified".
Status string `json:"status,required"`
UpdatedAt int64 `json:"updated_at,required"`
Zone string `json:"zone,required"`
ImageID string `json:"image_id,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
Expand All @@ -415,6 +423,7 @@ type NodeCurrentVM struct {
StartAt respjson.Field
Status respjson.Field
UpdatedAt respjson.Field
Zone respjson.Field
ImageID respjson.Field
ExtraFields map[string]respjson.Field
raw string
Expand Down Expand Up @@ -454,6 +463,7 @@ type NodeVMsData struct {
// Any of "Pending", "Running", "Destroyed", "NodeFailure", "Unspecified".
Status string `json:"status,required"`
UpdatedAt int64 `json:"updated_at,required"`
Zone string `json:"zone,required"`
ImageID string `json:"image_id,nullable"`
// JSON contains metadata for fields, check presence with [respjson.Field.Valid].
JSON struct {
Expand All @@ -464,6 +474,7 @@ type NodeVMsData struct {
StartAt respjson.Field
Status respjson.Field
UpdatedAt respjson.Field
Zone respjson.Field
ImageID respjson.Field
ExtraFields map[string]respjson.Field
raw string
Expand Down
3 changes: 2 additions & 1 deletion node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ func TestNodeNewWithOptionalParams(t *testing.T) {
CreateNodesRequest: sfcnodes.CreateNodesRequestParam{
DesiredCount: 1,
MaxPricePerNodeHour: 1000,
Zone: "hayesvalley",
AnyZone: sfcnodes.Bool(false),
CloudInitUserData: sfcnodes.String("aGVsbG8gd29ybGQ="),
EndAt: sfcnodes.Int(0),
ImageID: sfcnodes.String("vmi_1234567890abcdef"),
Names: []string{"cuda-crunch"},
NodeType: sfcnodes.NodeTypeAutoreserved,
StartAt: sfcnodes.Int(1640995200),
Zone: sfcnodes.String("hayesvalley"),
},
})
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func TestUsage(t *testing.T) {
option.WithBaseURL(baseURL),
option.WithBearerToken("My Bearer Token"),
)
t.Skip("Prism tests are disabled")
listResponseNode, err := client.Nodes.List(context.TODO(), sfcnodes.NodeListParams{})
if err != nil {
t.Fatalf("err should be nil: %s", err.Error())
Expand Down