diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70f7f3f..d36dd9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup go uses: actions/setup-go@v5 @@ -35,7 +35,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/sfc-nodes-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup go uses: actions/setup-go@v5 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b56c3d0..e8285b7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.4" + ".": "0.1.0-alpha.5" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 649bd1e..0c741d5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -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-d786973209f42e6ca35f318f4d5bf01e4abd77205e210409c6a3fb371a99a4c5.yml +openapi_spec_hash: 03857ab189ed9fcd889e7b3fe1cc2f2f config_hash: a187153315a646ecf95709ee4a223df5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 074875f..3eaaa82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # Changelog +## 0.1.0-alpha.5 (2026-02-20) + +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 ([a0a7fa2](https://github.com/sfcompute/nodes-go/commit/a0a7fa24667407359b24deb9a6391599873cfc4e)) +* **api:** api update ([531d77e](https://github.com/sfcompute/nodes-go/commit/531d77efc30775a747cf3a96320e1a85c172fdb2)) +* **api:** api update ([dff5f7f](https://github.com/sfcompute/nodes-go/commit/dff5f7f8daf4d7f353962442bf41e2d55ff028a1)) +* **api:** api update ([ca18f27](https://github.com/sfcompute/nodes-go/commit/ca18f27daf6688518636b18a4851308dcdae0db6)) +* **api:** api update ([894995d](https://github.com/sfcompute/nodes-go/commit/894995db7b4a6347ef91fed5db242fbb14df34ef)) +* **api:** api update ([afa8e35](https://github.com/sfcompute/nodes-go/commit/afa8e35b48706ac111a768c9a7a81c59aa1602b0)) +* **api:** api update ([f77e6d0](https://github.com/sfcompute/nodes-go/commit/f77e6d07e261819f07b68b9241218f42a0ab36a1)) +* **api:** api update ([3068ae6](https://github.com/sfcompute/nodes-go/commit/3068ae6937f3d6f6f5b36f8ac7c7e318866c3ce1)) +* **api:** api update ([a4e5181](https://github.com/sfcompute/nodes-go/commit/a4e5181e44815a66b891ecd41790c9dab76c0375)) +* **api:** api update ([6a064fd](https://github.com/sfcompute/nodes-go/commit/6a064fdc947dd668159e8643e7cab0689254d4c6)) +* **api:** api update ([dd70400](https://github.com/sfcompute/nodes-go/commit/dd70400abff7e7332cf7a7f3dee59e9e2e4b61b5)) +* **client:** add a convenient param.SetJSON helper ([a7293f3](https://github.com/sfcompute/nodes-go/commit/a7293f32d7d9f676d53e6200ca7bd82f603029bb)) +* **encoder:** support bracket encoding form-data object members ([bf75f99](https://github.com/sfcompute/nodes-go/commit/bf75f99a26c353742229dabb561684545d0835c7)) + + +### Bug Fixes + +* allow canceling a request while it is waiting to retry ([b16eec0](https://github.com/sfcompute/nodes-go/commit/b16eec05809da7c51496804b92be7f79a664860a)) +* **docs:** add missing pointer prefix to api.md return types ([6dd1ead](https://github.com/sfcompute/nodes-go/commit/6dd1eadf389b5a9adf98195f624f0bba54f0b686)) +* **encoder:** correctly serialize NullStruct ([ed7b95d](https://github.com/sfcompute/nodes-go/commit/ed7b95dc7003020ff3f2738321d023ae3599e9e0)) +* **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 ([44eb5d0](https://github.com/sfcompute/nodes-go/commit/44eb5d0a3f745631aebb3b0d2986a049302131fe)) +* **internal:** codegen related update ([e40a3de](https://github.com/sfcompute/nodes-go/commit/e40a3debee22efa7015e5230d14374245af79b1f)) +* **internal:** remove mock server code ([f28644a](https://github.com/sfcompute/nodes-go/commit/f28644aaee0119dccf2223264f06703f00567327)) +* **internal:** update `actions/checkout` version ([c853455](https://github.com/sfcompute/nodes-go/commit/c853455bd8e0c977606bc29049cfc84b2caea449)) +* update mock server docs ([588eb6a](https://github.com/sfcompute/nodes-go/commit/588eb6aca1879b14a61976f013b0155140dc1971)) + ## 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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 090e94c..796313f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,13 +46,6 @@ $ go mod edit -replace github.com/sfcompute/nodes-go=/path/to/nodes-go ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. - -```sh -# you will need npm installed -$ npx prism mock path/to/your/openapi.yml -``` - ```sh $ ./scripts/test ``` diff --git a/LICENSE b/LICENSE index 30db98a..46b2b7e 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 SFC Nodes + Copyright 2026 SFC Nodes Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 43ac98e..0d65833 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Or to pin the 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' ``` diff --git a/api.md b/api.md index a45a8de..1348708 100644 --- a/api.md +++ b/api.md @@ -7,8 +7,8 @@ Response Types: Methods: -- client.VMs.Logs(ctx context.Context, query sfcnodes.VMLogsParams) (sfcnodes.VMLogsResponse, error) -- client.VMs.SSH(ctx context.Context, query sfcnodes.VMSSHParams) (sfcnodes.VmsshResponse, error) +- client.VMs.Logs(ctx context.Context, query sfcnodes.VMLogsParams) (\*sfcnodes.VMLogsResponse, error) +- client.VMs.SSH(ctx context.Context, query sfcnodes.VMSSHParams) (\*sfcnodes.VmsshResponse, error) ## Script @@ -24,8 +24,8 @@ Response Types: Methods: -- client.VMs.Script.New(ctx context.Context, body sfcnodes.VMScriptNewParams) (sfcnodes.VMScriptNewResponse, error) -- client.VMs.Script.Get(ctx context.Context) (sfcnodes.VMScriptGetResponse, error) +- client.VMs.Script.New(ctx context.Context, body sfcnodes.VMScriptNewParams) (\*sfcnodes.VMScriptNewResponse, error) +- client.VMs.Script.Get(ctx context.Context) (\*sfcnodes.VMScriptGetResponse, error) ## Images @@ -36,8 +36,8 @@ Response Types: Methods: -- client.VMs.Images.List(ctx context.Context) (sfcnodes.VMImageListResponse, error) -- client.VMs.Images.Get(ctx context.Context, imageID string) (sfcnodes.VMImageGetResponse, error) +- client.VMs.Images.List(ctx context.Context) (\*sfcnodes.VMImageListResponse, error) +- client.VMs.Images.Get(ctx context.Context, imageID string) (\*sfcnodes.VMImageGetResponse, error) # Nodes @@ -57,13 +57,13 @@ Response Types: Methods: -- client.Nodes.New(ctx context.Context, body sfcnodes.NodeNewParams) (sfcnodes.ListResponseNode, error) -- client.Nodes.List(ctx context.Context, query sfcnodes.NodeListParams) (sfcnodes.ListResponseNode, error) +- client.Nodes.New(ctx context.Context, body sfcnodes.NodeNewParams) (\*sfcnodes.ListResponseNode, error) +- client.Nodes.List(ctx context.Context, query sfcnodes.NodeListParams) (\*sfcnodes.ListResponseNode, error) - client.Nodes.Delete(ctx context.Context, id string) error -- client.Nodes.Extend(ctx context.Context, id string, body sfcnodes.NodeExtendParams) (sfcnodes.Node, error) -- client.Nodes.Get(ctx context.Context, id string) (sfcnodes.Node, error) -- client.Nodes.Redeploy(ctx context.Context, id string, body sfcnodes.NodeRedeployParams) (sfcnodes.Node, error) -- client.Nodes.Release(ctx context.Context, id string) (sfcnodes.Node, error) +- client.Nodes.Extend(ctx context.Context, id string, body sfcnodes.NodeExtendParams) (\*sfcnodes.Node, error) +- client.Nodes.Get(ctx context.Context, id string) (\*sfcnodes.Node, error) +- client.Nodes.Redeploy(ctx context.Context, id string, body sfcnodes.NodeRedeployParams) (\*sfcnodes.Node, error) +- client.Nodes.Release(ctx context.Context, id string) (\*sfcnodes.Node, error) # Zones @@ -74,5 +74,5 @@ Response Types: Methods: -- client.Zones.List(ctx context.Context) (sfcnodes.ZoneListResponse, error) -- client.Zones.Get(ctx context.Context, id string) (sfcnodes.ZoneGetResponse, error) +- client.Zones.List(ctx context.Context) (\*sfcnodes.ZoneListResponse, error) +- client.Zones.Get(ctx context.Context, id string) (\*sfcnodes.ZoneGetResponse, error) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index 8fd59d9..cd7b726 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -60,6 +60,7 @@ type encoderField struct { type encoderEntry struct { reflect.Type dateFormat string + arrayFmt string root bool } @@ -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, } @@ -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") @@ -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 } @@ -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 { @@ -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() @@ -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 } diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index 0d05c4a..1874257 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -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 @@ -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 @@ -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) { @@ -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) } }) } diff --git a/internal/apijson/enum.go b/internal/apijson/enum.go index 18b218a..5bef11c 100644 --- a/internal/apijson/enum.go +++ b/internal/apijson/enum.go @@ -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) diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index f36e5e7..d0ee6e7 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -466,7 +466,11 @@ func (cfg *RequestConfig) Execute() (err error) { res.Body.Close() } - time.Sleep(retryDelay(res, retryCount)) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(retryDelay(res, retryCount)): + } } // Save *http.Response if it is requested to, even if there was an error making the request. This is diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 826d266..31103e9 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -16,10 +16,10 @@ func CheckTestServer(t *testing.T, url string) bool { t.Fatalf("strconv.ParseBool(os.LookupEnv(%s)) failed: %s", SKIP_MOCK_TESTS, err) } if skip { - t.Skip("The test will not run without a mock Prism server running against your OpenAPI spec") + t.Skip("The test will not run without a mock server running against your OpenAPI spec") return false } - t.Errorf("The test will not run without a mock Prism server running against your OpenAPI spec. You can set the environment variable %s to true to skip running any tests that require the mock server", SKIP_MOCK_TESTS) + t.Errorf("The test will not run without a mock server running against your OpenAPI spec. You can set the environment variable %s to true to skip running any tests that require the mock server", SKIP_MOCK_TESTS) return false } } diff --git a/internal/version.go b/internal/version.go index 5469df6..2aad167 100644 --- a/internal/version.go +++ b/internal/version.go @@ -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 diff --git a/node.go b/node.go index b15c3ad..606db63 100644 --- a/node.go +++ b/node.go @@ -125,24 +125,29 @@ 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"` + // (Optional) If set, enables forwarding to the VM on port 443. + Forward443 param.Opt[bool] `json:"forward_443,omitzero"` // Custom image ID to use for the VM instances ImageID param.Opt[string] `json:"image_id,omitzero"` // 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 @@ -265,6 +270,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 { @@ -275,6 +281,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 @@ -314,6 +321,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 { @@ -324,6 +332,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 @@ -405,6 +414,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 { @@ -415,6 +425,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 @@ -454,6 +465,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 { @@ -464,6 +476,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 diff --git a/node_test.go b/node_test.go index 88ca793..85cf86f 100644 --- a/node_test.go +++ b/node_test.go @@ -14,7 +14,7 @@ import ( ) func TestNodeNewWithOptionalParams(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -29,14 +29,16 @@ func TestNodeNewWithOptionalParams(t *testing.T) { _, err := client.Nodes.New(context.TODO(), sfcnodes.NodeNewParams{ CreateNodesRequest: sfcnodes.CreateNodesRequestParam{ DesiredCount: 1, - MaxPricePerNodeHour: 1000, - Zone: "hayesvalley", + MaxPricePerNodeHour: 1600, + AnyZone: sfcnodes.Bool(false), CloudInitUserData: sfcnodes.String("aGVsbG8gd29ybGQ="), EndAt: sfcnodes.Int(0), + Forward443: sfcnodes.Bool(false), ImageID: sfcnodes.String("vmi_1234567890abcdef"), Names: []string{"cuda-crunch"}, NodeType: sfcnodes.NodeTypeAutoreserved, StartAt: sfcnodes.Int(1640995200), + Zone: sfcnodes.String("hayesvalley"), }, }) if err != nil { @@ -49,7 +51,7 @@ func TestNodeNewWithOptionalParams(t *testing.T) { } func TestNodeListWithOptionalParams(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -76,7 +78,7 @@ func TestNodeListWithOptionalParams(t *testing.T) { } func TestNodeDelete(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -99,7 +101,7 @@ func TestNodeDelete(t *testing.T) { } func TestNodeExtend(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -131,7 +133,7 @@ func TestNodeExtend(t *testing.T) { } func TestNodeGet(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -154,7 +156,7 @@ func TestNodeGet(t *testing.T) { } func TestNodeRedeployWithOptionalParams(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -185,7 +187,7 @@ func TestNodeRedeployWithOptionalParams(t *testing.T) { } func TestNodeRelease(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL diff --git a/packages/param/encoder.go b/packages/param/encoder.go index 7c3a221..01fe75f 100644 --- a/packages/param/encoder.go +++ b/packages/param/encoder.go @@ -83,6 +83,9 @@ func MarshalUnion[T ParamStruct](metadata T, variants ...any) ([]byte, error) { } } if nPresent == 0 || presentIdx == -1 { + if metadata.null() { + return []byte("null"), nil + } if ovr, ok := metadata.Overrides(); ok { return shimjson.Marshal(ovr) } diff --git a/packages/param/encoder_test.go b/packages/param/encoder_test.go index f64ff67..e72e18d 100644 --- a/packages/param/encoder_test.go +++ b/packages/param/encoder_test.go @@ -363,3 +363,15 @@ func TestOverriddenUnion(t *testing.T) { }) } } + +func TestNullStructUnion(t *testing.T) { + nullUnion := param.NullStruct[PrimitiveUnion]() + + b, err := json.Marshal(nullUnion) + if err != nil { + t.Fatalf("didn't expect error %v", err) + } + if string(b) != "null" { + t.Fatalf("expected null, received %s", string(b)) + } +} diff --git a/packages/param/param.go b/packages/param/param.go index 500a3f5..684d7e9 100644 --- a/packages/param/param.go +++ b/packages/param/param.go @@ -41,6 +41,19 @@ func Override[T ParamStruct, PtrT InferPtr[T]](v any) T { return *pt } +// SetJSON configures a param struct to serialize with the provided raw JSON data. +// Use this when you have existing JSON that you want to send as request parameters. +// +// var req example.NewUserParams +// var rawJSON = []byte(`{"name": "...", "age": 40}`) +// param.SetJSON(rawJSON, &req) +// res, err := client.Users.New(ctx, req) +// +// Note: The struct's existing fields will be ignored; only the provided JSON is serialized. +func SetJSON(rawJSON []byte, ptr anyParamStruct) { + ptr.setMetadata(json.RawMessage(rawJSON)) +} + // IsOmitted returns true if v is the zero value of its type. // // If IsOmitted is true, and the field uses a `json:"...,omitzero"` tag, @@ -91,6 +104,11 @@ type ParamStruct interface { extraFields() map[string]any } +// A pointer to ParamStruct +type anyParamStruct interface { + setMetadata(any) +} + // This is an implementation detail and should never be explicitly set. type InferPtr[T ParamStruct] interface { setMetadata(any) diff --git a/scripts/mock b/scripts/mock deleted file mode 100755 index 0b28f6e..0000000 --- a/scripts/mock +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [[ -n "$1" && "$1" != '--'* ]]; then - URL="$1" - shift -else - URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" -fi - -# Check if the URL is empty -if [ -z "$URL" ]; then - echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" - exit 1 -fi - -echo "==> Starting mock server with URL ${URL}" - -# Run prism mock on the given spec -if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - - # Wait for server to come online - echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do - echo -n "." - sleep 0.1 - done - - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - - echo -else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" -fi diff --git a/scripts/test b/scripts/test index c26b122..8704b64 100755 --- a/scripts/test +++ b/scripts/test @@ -4,53 +4,7 @@ set -e cd "$(dirname "$0")/.." -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 -} - -kill_server_on_port() { - pids=$(lsof -t -i tcp:"$1" || echo "") - if [ "$pids" != "" ]; then - kill "$pids" - echo "Stopped $pids." - fi -} - -function is_overriding_api_base_url() { - [ -n "$TEST_API_BASE_URL" ] -} - -if ! is_overriding_api_base_url && ! prism_is_running ; then - # When we exit this script, make sure to kill the background mock server process - trap 'kill_server_on_port 4010' EXIT - - # Start the dev server - ./scripts/mock --daemon -fi - -if is_overriding_api_base_url ; then - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" - echo -e "running against your OpenAPI spec." - echo - echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" - echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" - echo - - exit 1 -else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" - echo -fi echo "==> Running tests" go test ./... "$@" diff --git a/usage_test.go b/usage_test.go index 1117ee4..a25bf6d 100644 --- a/usage_test.go +++ b/usage_test.go @@ -24,6 +24,7 @@ func TestUsage(t *testing.T) { option.WithBaseURL(baseURL), option.WithBearerToken("My Bearer Token"), ) + t.Skip("Mock server tests are disabled") listResponseNode, err := client.Nodes.List(context.TODO(), sfcnodes.NodeListParams{}) if err != nil { t.Fatalf("err should be nil: %s", err.Error()) diff --git a/vm.go b/vm.go index ca30cf2..5cc5d4d 100644 --- a/vm.go +++ b/vm.go @@ -99,9 +99,9 @@ func (r *VMLogsResponseData) UnmarshalJSON(data []byte) error { type VmsshResponse struct { SSHHostname string `json:"ssh_hostname,required"` SSHPort int64 `json:"ssh_port,required"` - // Unix timestamp in seconds since epoch + // Unix timestamp. LastAttemptedKeyUpdate int64 `json:"last_attempted_key_update,nullable"` - // Unix timestamp in seconds since epoch + // Unix timestamp. LastSuccessfulKeyUpdate int64 `json:"last_successful_key_update,nullable"` SSHHostKeys []VmsshResponseSSHHostKey `json:"ssh_host_keys,nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. diff --git a/vm_test.go b/vm_test.go index 2adcbbd..74bf38f 100644 --- a/vm_test.go +++ b/vm_test.go @@ -14,7 +14,7 @@ import ( ) func TestVMLogsWithOptionalParams(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -45,7 +45,7 @@ func TestVMLogsWithOptionalParams(t *testing.T) { } func TestVMSSH(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL diff --git a/vmimage_test.go b/vmimage_test.go index d27aa39..85daf31 100644 --- a/vmimage_test.go +++ b/vmimage_test.go @@ -14,7 +14,7 @@ import ( ) func TestVMImageList(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -37,7 +37,7 @@ func TestVMImageList(t *testing.T) { } func TestVMImageGet(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL diff --git a/vmscript_test.go b/vmscript_test.go index 78af7cd..398e4b0 100644 --- a/vmscript_test.go +++ b/vmscript_test.go @@ -14,7 +14,7 @@ import ( ) func TestVMScriptNew(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -41,7 +41,7 @@ func TestVMScriptNew(t *testing.T) { } func TestVMScriptGet(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL diff --git a/zone.go b/zone.go index b294733..dc78902 100644 --- a/zone.go +++ b/zone.go @@ -86,6 +86,8 @@ type ZoneListResponseData struct { Object string `json:"object,required"` // Any of "NorthAmerica", "AsiaPacific", "EuropeMiddleEastAfrica". Region string `json:"region,required"` + // User-facing zone name (e.g., "Hayes Valley", "Land's End") + DisplayName string `json:"display_name,nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { AvailableCapacity respjson.Field @@ -95,6 +97,7 @@ type ZoneListResponseData struct { Name respjson.Field Object respjson.Field Region respjson.Field + DisplayName respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -107,11 +110,11 @@ func (r *ZoneListResponseData) UnmarshalJSON(data []byte) error { } type ZoneListResponseDataAvailableCapacity struct { - // Unix timestamp in seconds since epoch + // Unix timestamp. EndTimestamp int64 `json:"end_timestamp,required"` // The number of nodes available during this time period Quantity int64 `json:"quantity,required"` - // Unix timestamp in seconds since epoch + // Unix timestamp. StartTimestamp int64 `json:"start_timestamp,required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -143,6 +146,8 @@ type ZoneGetResponse struct { Object string `json:"object,required"` // Any of "NorthAmerica", "AsiaPacific", "EuropeMiddleEastAfrica". Region ZoneGetResponseRegion `json:"region,required"` + // User-facing zone name (e.g., "Hayes Valley", "Land's End") + DisplayName string `json:"display_name,nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { AvailableCapacity respjson.Field @@ -152,6 +157,7 @@ type ZoneGetResponse struct { Name respjson.Field Object respjson.Field Region respjson.Field + DisplayName respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -164,11 +170,11 @@ func (r *ZoneGetResponse) UnmarshalJSON(data []byte) error { } type ZoneGetResponseAvailableCapacity struct { - // Unix timestamp in seconds since epoch + // Unix timestamp. EndTimestamp int64 `json:"end_timestamp,required"` // The number of nodes available during this time period Quantity int64 `json:"quantity,required"` - // Unix timestamp in seconds since epoch + // Unix timestamp. StartTimestamp int64 `json:"start_timestamp,required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { diff --git a/zone_test.go b/zone_test.go index 061d9ba..11ad8c7 100644 --- a/zone_test.go +++ b/zone_test.go @@ -14,7 +14,7 @@ import ( ) func TestZoneList(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -37,7 +37,7 @@ func TestZoneList(t *testing.T) { } func TestZoneGet(t *testing.T) { - t.Skip("Prism tests are disabled") + t.Skip("Mock server tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL