diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2803ecdc..e2a5ee32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' @@ -22,16 +24,16 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/stainless-v0-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: |- github.repository == 'stainless-sdks/stainless-v0-go' && - (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Get GitHub OIDC Token if: |- github.repository == 'stainless-sdks/stainless-v0-go' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -51,10 +53,10 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: ./go.mod @@ -66,10 +68,10 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/stainless-v0-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: ./go.mod diff --git a/.gitignore b/.gitignore index c6d05015..8554affe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log codegen.log Brewfile.lock.json .idea/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 554e34bb..f81bf992 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.30.0" + ".": "0.31.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 80762ab8..9003a691 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 22 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless%2Fstainless-v0-ef33241ec003fafbf4f2ffe434c3d8c2ac0ba929137942185663ff59974d2138.yml -openapi_spec_hash: 87bd0d9c684517522cbbbd48bbe8ad83 -config_hash: 4b44da9496c775d2294758cd233f4ecd +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless/stainless-v0-4ec3b12ce6b2e1e2eb800fab7dea5fc272c8c7aaf364a5305af5f4eda95e07b3.yml +openapi_spec_hash: 8e661d2611da4f7d874cafe22cda765d +config_hash: 63178ec4b1d2ea5636c8619cffcf129b diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a003391..0a495271 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +## 0.31.0 (2026-05-29) + +Full Changelog: [v0.30.0...v0.31.0](https://github.com/stainless-api/stainless-api-go/compare/v0.30.0...v0.31.0) + +### Features + +* allow direct upload of spec/config to v0 builds api when merging/rebasing ([b9b9fa1](https://github.com/stainless-api/stainless-api-go/commit/b9b9fa1d7d191af835ed7983808f331b130ecbee)) +* **go:** add default http client with timeout ([b0fcaa2](https://github.com/stainless-api/stainless-api-go/commit/b0fcaa21da3ce9784ab5df50f2ad53c1624eee63)) +* **internal:** support comma format in multipart form encoding ([fe14b5a](https://github.com/stainless-api/stainless-api-go/commit/fe14b5a12cc6921f134e3cd58f1ddbadbef687d8)) +* support setting headers via env ([49cf293](https://github.com/stainless-api/stainless-api-go/commit/49cf293f1cba3628ff6690f853178c01e2560c85)) + + +### Bug Fixes + +* fix for union type names ([199c263](https://github.com/stainless-api/stainless-api-go/commit/199c263e6b0ad82369d0a957ea3b06856f9cd8b3)) +* **go:** avoid panic when http.DefaultTransport is wrapped ([7eaefa2](https://github.com/stainless-api/stainless-api-go/commit/7eaefa2b69e4f6a98a5baa1dd6a78ce334a3394a)) +* prevent duplicate ? in query params ([75d6c76](https://github.com/stainless-api/stainless-api-go/commit/75d6c762a9ff3a61bd85ca3c6849beba49071487)) + + +### Chores + +* avoid embedding reflect.Type for dead code elimination ([ed95de5](https://github.com/stainless-api/stainless-api-go/commit/ed95de5b4950f55c49a33c9f7aa0762e5b9a7eca)) +* **ci:** skip lint on metadata-only changes ([9ff15a2](https://github.com/stainless-api/stainless-api-go/commit/9ff15a28b3fa87a039afbc0530fd1596a2c3628b)) +* **ci:** support opting out of skipping builds on metadata-only commits ([fc8ba99](https://github.com/stainless-api/stainless-api-go/commit/fc8ba99ac8e0b5911fb116128c95f3b834b6e0eb)) +* **client:** fix multipart serialisation of Default() fields ([3440645](https://github.com/stainless-api/stainless-api-go/commit/3440645718d76c103cc8bbc0581f6b6f0701a5da)) +* configure new SDK language ([e3cdd77](https://github.com/stainless-api/stainless-api-go/commit/e3cdd774fec0ca4730b49a29b9449e6486689538)) +* **internal:** codegen related update ([0db8f71](https://github.com/stainless-api/stainless-api-go/commit/0db8f716cdd728dd59b8bac086c65aa050e4d0b8)) +* **internal:** codegen related update ([0060599](https://github.com/stainless-api/stainless-api-go/commit/0060599fcb6cc027331f9b2cd5308c203d22b220)) +* **internal:** more robust bootstrap script ([7a3c829](https://github.com/stainless-api/stainless-api-go/commit/7a3c829bc1b041174a381b0123cb75a9eee24125)) +* **internal:** support default value struct tag ([51c93c2](https://github.com/stainless-api/stainless-api-go/commit/51c93c2ae106ad64285dea87f94f6240f7f56e8a)) +* **internal:** tweak CI branches ([be313f6](https://github.com/stainless-api/stainless-api-go/commit/be313f62c0c1ac7bf5351180152ba28b97bead3a)) +* **internal:** update gitignore ([7b22047](https://github.com/stainless-api/stainless-api-go/commit/7b2204784376dd5ce909c461dbc7a56e35671415)) +* redact api-key headers in debug logs ([27ed10e](https://github.com/stainless-api/stainless-api-go/commit/27ed10ebca2c7b1c7ac6d97997d0f829d3b77bad)) +* remove unnecessary error check for url parsing ([459c159](https://github.com/stainless-api/stainless-api-go/commit/459c159f2481411fb1295c14570d02a44eeacc66)) +* **tests:** bump steady to v0.19.4 ([d07d7c1](https://github.com/stainless-api/stainless-api-go/commit/d07d7c193749d6405dbb22b4e8aea5993efd07d0)) +* **tests:** bump steady to v0.19.5 ([1abf6bb](https://github.com/stainless-api/stainless-api-go/commit/1abf6bb3dd5a72b18337c2d7ad557c9dbc5b6240)) +* **tests:** bump steady to v0.19.6 ([c3b044e](https://github.com/stainless-api/stainless-api-go/commit/c3b044efa0112dee1aaddb23bae87622eff99ec8)) +* **tests:** bump steady to v0.19.7 ([9fbed10](https://github.com/stainless-api/stainless-api-go/commit/9fbed10600a9bf9655c8289b6f0eb18de8a982b6)) +* **tests:** bump steady to v0.20.1 ([34fa796](https://github.com/stainless-api/stainless-api-go/commit/34fa7961079423a9653316c1d2e68023fdf2b76e)) +* **tests:** bump steady to v0.20.2 ([5b8d8a7](https://github.com/stainless-api/stainless-api-go/commit/5b8d8a7f2074c8cbba1e057cba98c78553a16b48)) +* **tests:** bump steady to v0.22.1 ([a5e7563](https://github.com/stainless-api/stainless-api-go/commit/a5e75636ac7bf44dfb46792921e22e0f161f96f0)) +* update docs for api:"required" ([dff036b](https://github.com/stainless-api/stainless-api-go/commit/dff036b0396ec75667e1880662a6d7d43cade57d)) +* update probot ([940e928](https://github.com/stainless-api/stainless-api-go/commit/940e9283bcbf16347fd8cb8f9c6d83cfb08fb227)) + + +### Refactors + +* **tests:** switch from prism to steady ([ac75b85](https://github.com/stainless-api/stainless-api-go/commit/ac75b85a15fdf1969bc92c55698cfb4793316b84)) + ## 0.30.0 (2026-03-11) Full Changelog: [v0.29.0...v0.30.0](https://github.com/stainless-api/stainless-api-go/compare/v0.29.0...v0.30.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 07e20026..0d887231 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,7 +46,7 @@ $ go mod edit -replace github.com/stainless-api/stainless-api-go=/path/to/stainl ## 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. +Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests. ```sh $ ./scripts/mock diff --git a/README.md b/README.md index 2b480ee2..a0905e2b 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Stainless MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40stainless-api%2Fmcp&config=eyJuYW1lIjoiQHN0YWlubGVzcy1hcGkvbWNwIiwidHJhbnNwb3J0IjoiaHR0cCIsInVybCI6Imh0dHBzOi8vc3RhaW5sZXNzLXYwLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7Ingtc3RhaW5sZXNzLWFwaS1rZXkiOiJNeSBBUEkgS2V5In19) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40stainless-api%2Fmcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fstainless-v0.stlmcp.com%22%2C%22headers%22%3A%7B%22x-stainless-api-key%22%3A%22My%20API%20Key%22%7D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40stainless-api%2Fsdk-mcp&config=eyJuYW1lIjoiQHN0YWlubGVzcy1hcGkvc2RrLW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL3N0YWlubGVzcy12MC5zdGxtY3AuY29tIiwiaGVhZGVycyI6eyJ4LXN0YWlubGVzcy1hcGkta2V5IjoiTXkgQVBJIEtleSJ9fQ) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40stainless-api%2Fsdk-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fstainless-v0.stlmcp.com%22%2C%22headers%22%3A%7B%22x-stainless-api-key%22%3A%22My%20API%20Key%22%7D%7D) > Note: You may need to set environment variables in your MCP client. @@ -37,7 +37,7 @@ Or to pin the version: ```sh -go get -u 'github.com/stainless-api/stainless-api-go@v0.30.0' +go get -u 'github.com/stainless-api/stainless-api-go@v0.31.0' ``` @@ -85,7 +85,7 @@ func main() { The stainless library uses the [`omitzero`](https://tip.golang.org/doc/go1.24#encodingjsonpkgencodingjson) semantics from the Go 1.24+ `encoding/json` release for request fields. -Required primitive fields (`int64`, `string`, etc.) feature the tag \`json:"...,required"\`. These +Required primitive fields (`int64`, `string`, etc.) feature the tag \`api:"required"\`. These fields are always serialized, even their zero values. Optional primitive types are wrapped in a `param.Opt[T]`. These fields can be set with the provided constructors, `stainless.String(string)`, `stainless.Int(int64)`, etc. diff --git a/build.go b/build.go index 33e60ae5..a0ab3701 100644 --- a/build.go +++ b/build.go @@ -164,17 +164,16 @@ func (r *Build) UnmarshalJSON(data []byte) error { } // BuildDocumentedSpecUnion contains all possible properties and values from -// [BuildDocumentedSpecObject], [BuildDocumentedSpecObject]. +// [BuildDocumentedSpecObject], [BuildDocumentedSpecObject2]. // // Use the methods beginning with 'As' to cast the union to one of its variants. type BuildDocumentedSpecUnion struct { // This field is from variant [BuildDocumentedSpecObject]. Content string `json:"content"` - // This field is from variant [BuildDocumentedSpecObject]. - Type string `json:"type"` - // This field is from variant [BuildDocumentedSpecObject]. + Type string `json:"type"` + // This field is from variant [BuildDocumentedSpecObject2]. Expires time.Time `json:"expires"` - // This field is from variant [BuildDocumentedSpecObject]. + // This field is from variant [BuildDocumentedSpecObject2]. URL string `json:"url"` JSON struct { Content respjson.Field @@ -190,7 +189,7 @@ func (u BuildDocumentedSpecUnion) AsBuildDocumentedSpecObject() (v BuildDocument return } -func (u BuildDocumentedSpecUnion) AsVariant2() (v BuildDocumentedSpecObject) { +func (u BuildDocumentedSpecUnion) AsBuildDocumentedSpecObject2() (v BuildDocumentedSpecObject2) { apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) return } @@ -221,6 +220,27 @@ func (r *BuildDocumentedSpecObject) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type BuildDocumentedSpecObject2 struct { + Expires time.Time `json:"expires" api:"required" format:"date-time"` + // Any of "url". + Type string `json:"type" api:"required"` + URL string `json:"url" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Expires respjson.Field + Type respjson.Field + URL respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r BuildDocumentedSpecObject2) RawJSON() string { return r.JSON.raw } +func (r *BuildDocumentedSpecObject2) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type BuildObject string const ( @@ -298,14 +318,15 @@ func (r *BuildTarget) UnmarshalJSON(data []byte) error { } // BuildTargetCommitUnion contains all possible properties and values from -// [BuildTargetCommitNotStarted], [BuildTargetCommitQueued], -// [BuildTargetCommitInProgress], [BuildTargetCommitCompleted]. +// [BuildTargetCommitNotStarted], [BuildTargetCommitWaiting], +// [BuildTargetCommitQueued], [BuildTargetCommitInProgress], +// [BuildTargetCommitCompleted]. // // Use the [BuildTargetCommitUnion.AsAny] method to switch on the variant. // // Use the methods beginning with 'As' to cast the union to one of its variants. type BuildTargetCommitUnion struct { - // Any of "not_started", "queued", "in_progress", "completed". + // Any of "not_started", "waiting", "queued", "in_progress", "completed". Status string `json:"status"` // This field is from variant [BuildTargetCommitCompleted]. Commit shared.Commit `json:"commit"` @@ -335,6 +356,7 @@ type anyBuildTargetCommit interface { } func (BuildTargetCommitNotStarted) implBuildTargetCommitUnion() {} +func (BuildTargetCommitWaiting) implBuildTargetCommitUnion() {} func (BuildTargetCommitQueued) implBuildTargetCommitUnion() {} func (BuildTargetCommitInProgress) implBuildTargetCommitUnion() {} func (BuildTargetCommitCompleted) implBuildTargetCommitUnion() {} @@ -343,6 +365,7 @@ func (BuildTargetCommitCompleted) implBuildTargetCommitUnion() {} // // switch variant := BuildTargetCommitUnion.AsAny().(type) { // case stainless.BuildTargetCommitNotStarted: +// case stainless.BuildTargetCommitWaiting: // case stainless.BuildTargetCommitQueued: // case stainless.BuildTargetCommitInProgress: // case stainless.BuildTargetCommitCompleted: @@ -353,6 +376,8 @@ func (u BuildTargetCommitUnion) AsAny() anyBuildTargetCommit { switch u.Status { case "not_started": return u.AsNotStarted() + case "waiting": + return u.AsWaiting() case "queued": return u.AsQueued() case "in_progress": @@ -368,6 +393,11 @@ func (u BuildTargetCommitUnion) AsNotStarted() (v BuildTargetCommitNotStarted) { return } +func (u BuildTargetCommitUnion) AsWaiting() (v BuildTargetCommitWaiting) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + func (u BuildTargetCommitUnion) AsQueued() (v BuildTargetCommitQueued) { apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) return @@ -391,7 +421,7 @@ func (r *BuildTargetCommitUnion) UnmarshalJSON(data []byte) error { } type BuildTargetCommitNotStarted struct { - Status constant.NotStarted `json:"status" api:"required"` + Status constant.NotStarted `json:"status" default:"not_started"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Status respjson.Field @@ -406,8 +436,24 @@ func (r *BuildTargetCommitNotStarted) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type BuildTargetCommitWaiting struct { + Status constant.Waiting `json:"status" default:"waiting"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Status respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r BuildTargetCommitWaiting) RawJSON() string { return r.JSON.raw } +func (r *BuildTargetCommitWaiting) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type BuildTargetCommitQueued struct { - Status constant.Queued `json:"status" api:"required"` + Status constant.Queued `json:"status" default:"queued"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Status respjson.Field @@ -423,7 +469,7 @@ func (r *BuildTargetCommitQueued) UnmarshalJSON(data []byte) error { } type BuildTargetCommitInProgress struct { - Status constant.InProgress `json:"status" api:"required"` + Status constant.InProgress `json:"status" default:"in_progress"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Status respjson.Field @@ -448,7 +494,7 @@ type BuildTargetCommitCompleted struct { // "timed_out", "noop", "version_bump". Conclusion string `json:"conclusion" api:"required"` MergeConflictPr BuildTargetCommitCompletedMergeConflictPr `json:"merge_conflict_pr" api:"required"` - Status constant.Completed `json:"status" api:"required"` + Status constant.Completed `json:"status" default:"completed"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Commit respjson.Field @@ -586,14 +632,14 @@ const ( ) // CheckStepUnion contains all possible properties and values from -// [CheckStepNotStarted], [CheckStepQueued], [CheckStepInProgress], -// [CheckStepCompleted]. +// [CheckStepNotStarted], [CheckStepWaiting], [CheckStepQueued], +// [CheckStepInProgress], [CheckStepCompleted]. // // Use the [CheckStepUnion.AsAny] method to switch on the variant. // // Use the methods beginning with 'As' to cast the union to one of its variants. type CheckStepUnion struct { - // Any of "not_started", "queued", "in_progress", "completed". + // Any of "not_started", "waiting", "queued", "in_progress", "completed". Status string `json:"status"` URL string `json:"url"` // This field is from variant [CheckStepCompleted]. @@ -616,6 +662,7 @@ type anyCheckStep interface { } func (CheckStepNotStarted) implCheckStepUnion() {} +func (CheckStepWaiting) implCheckStepUnion() {} func (CheckStepQueued) implCheckStepUnion() {} func (CheckStepInProgress) implCheckStepUnion() {} func (CheckStepCompleted) implCheckStepUnion() {} @@ -624,6 +671,7 @@ func (CheckStepCompleted) implCheckStepUnion() {} // // switch variant := CheckStepUnion.AsAny().(type) { // case stainless.CheckStepNotStarted: +// case stainless.CheckStepWaiting: // case stainless.CheckStepQueued: // case stainless.CheckStepInProgress: // case stainless.CheckStepCompleted: @@ -634,6 +682,8 @@ func (u CheckStepUnion) AsAny() anyCheckStep { switch u.Status { case "not_started": return u.AsNotStarted() + case "waiting": + return u.AsWaiting() case "queued": return u.AsQueued() case "in_progress": @@ -649,6 +699,11 @@ func (u CheckStepUnion) AsNotStarted() (v CheckStepNotStarted) { return } +func (u CheckStepUnion) AsWaiting() (v CheckStepWaiting) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + func (u CheckStepUnion) AsQueued() (v CheckStepQueued) { apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) return @@ -672,7 +727,7 @@ func (r *CheckStepUnion) UnmarshalJSON(data []byte) error { } type CheckStepNotStarted struct { - Status constant.NotStarted `json:"status" api:"required"` + Status constant.NotStarted `json:"status" default:"not_started"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Status respjson.Field @@ -687,8 +742,26 @@ func (r *CheckStepNotStarted) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type CheckStepWaiting struct { + Status constant.Waiting `json:"status" default:"waiting"` + URL string `json:"url" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Status respjson.Field + URL respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r CheckStepWaiting) RawJSON() string { return r.JSON.raw } +func (r *CheckStepWaiting) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type CheckStepQueued struct { - Status constant.Queued `json:"status" api:"required"` + Status constant.Queued `json:"status" default:"queued"` URL string `json:"url" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -706,7 +779,7 @@ func (r *CheckStepQueued) UnmarshalJSON(data []byte) error { } type CheckStepInProgress struct { - Status constant.InProgress `json:"status" api:"required"` + Status constant.InProgress `json:"status" default:"in_progress"` URL string `json:"url" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -729,7 +802,7 @@ type CheckStepCompleted struct { // Any of "success", "failure", "skipped", "cancelled", "action_required", // "neutral", "timed_out". Conclusion string `json:"conclusion" api:"required"` - Status constant.Completed `json:"status" api:"required"` + Status constant.Completed `json:"status" default:"completed"` URL string `json:"url" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -825,20 +898,23 @@ func (r *BuildNewParams) UnmarshalJSON(data []byte) error { // // Use [param.IsOmitted] to confirm if a field is set. type BuildNewParamsRevisionUnion struct { - OfString param.Opt[string] `json:",omitzero,inline"` - OfFileInputMap map[string]shared.FileInputUnionParam `json:",omitzero,inline"` + OfBuildNewsRevisionObject *BuildNewParamsRevisionObject `json:",omitzero,inline"` + OfString param.Opt[string] `json:",omitzero,inline"` + OfFileInputMap map[string]shared.FileInputUnionParam `json:",omitzero,inline"` paramUnion } func (u BuildNewParamsRevisionUnion) MarshalJSON() ([]byte, error) { - return param.MarshalUnion(u, u.OfString, u.OfFileInputMap) + return param.MarshalUnion(u, u.OfBuildNewsRevisionObject, u.OfString, u.OfFileInputMap) } func (u *BuildNewParamsRevisionUnion) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, u) } func (u *BuildNewParamsRevisionUnion) asAny() any { - if !param.IsOmitted(u.OfString) { + if !param.IsOmitted(u.OfBuildNewsRevisionObject) { + return u.OfBuildNewsRevisionObject + } else if !param.IsOmitted(u.OfString) { return &u.OfString.Value } else if !param.IsOmitted(u.OfFileInputMap) { return &u.OfFileInputMap @@ -846,6 +922,26 @@ func (u *BuildNewParamsRevisionUnion) asAny() any { return nil } +// A merge command combined with explicit file contents. The files are committed to +// the merge target (`base`) without performing an auto-merge. +// +// The properties Files, Merge are required. +type BuildNewParamsRevisionObject struct { + // File contents to commit directly + Files map[string]shared.FileInputUnionParam `json:"files,omitzero" api:"required"` + // A merge command in the format "base..head" + Merge string `json:"merge" api:"required"` + paramObj +} + +func (r BuildNewParamsRevisionObject) MarshalJSON() (data []byte, err error) { + type shadow BuildNewParamsRevisionObject + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BuildNewParamsRevisionObject) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // Optional commit messages to use for each SDK when making a new commit. SDKs not // represented in this object will fallback to the optional `commit_message` // parameter, or will fallback further to the default commit message. @@ -979,20 +1075,23 @@ func (r *BuildCompareParamsBase) UnmarshalJSON(data []byte) error { // // Use [param.IsOmitted] to confirm if a field is set. type BuildCompareParamsBaseRevisionUnion struct { - OfString param.Opt[string] `json:",omitzero,inline"` - OfFileInputMap map[string]shared.FileInputUnionParam `json:",omitzero,inline"` + OfBuildComparesBaseRevisionObject *BuildCompareParamsBaseRevisionObject `json:",omitzero,inline"` + OfString param.Opt[string] `json:",omitzero,inline"` + OfFileInputMap map[string]shared.FileInputUnionParam `json:",omitzero,inline"` paramUnion } func (u BuildCompareParamsBaseRevisionUnion) MarshalJSON() ([]byte, error) { - return param.MarshalUnion(u, u.OfString, u.OfFileInputMap) + return param.MarshalUnion(u, u.OfBuildComparesBaseRevisionObject, u.OfString, u.OfFileInputMap) } func (u *BuildCompareParamsBaseRevisionUnion) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, u) } func (u *BuildCompareParamsBaseRevisionUnion) asAny() any { - if !param.IsOmitted(u.OfString) { + if !param.IsOmitted(u.OfBuildComparesBaseRevisionObject) { + return u.OfBuildComparesBaseRevisionObject + } else if !param.IsOmitted(u.OfString) { return &u.OfString.Value } else if !param.IsOmitted(u.OfFileInputMap) { return &u.OfFileInputMap @@ -1000,6 +1099,26 @@ func (u *BuildCompareParamsBaseRevisionUnion) asAny() any { return nil } +// A merge command combined with explicit file contents. The files are committed to +// the merge target (`base`) without performing an auto-merge. +// +// The properties Files, Merge are required. +type BuildCompareParamsBaseRevisionObject struct { + // File contents to commit directly + Files map[string]shared.FileInputUnionParam `json:"files,omitzero" api:"required"` + // A merge command in the format "base..head" + Merge string `json:"merge" api:"required"` + paramObj +} + +func (r BuildCompareParamsBaseRevisionObject) MarshalJSON() (data []byte, err error) { + type shadow BuildCompareParamsBaseRevisionObject + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BuildCompareParamsBaseRevisionObject) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // Parameters for the head build // // The properties Branch, Revision are required. @@ -1026,23 +1145,46 @@ func (r *BuildCompareParamsHead) UnmarshalJSON(data []byte) error { // // Use [param.IsOmitted] to confirm if a field is set. type BuildCompareParamsHeadRevisionUnion struct { - OfString param.Opt[string] `json:",omitzero,inline"` - OfFileInputMap map[string]shared.FileInputUnionParam `json:",omitzero,inline"` + OfBuildComparesHeadRevisionObject *BuildCompareParamsHeadRevisionObject `json:",omitzero,inline"` + OfString param.Opt[string] `json:",omitzero,inline"` + OfFileInputMap map[string]shared.FileInputUnionParam `json:",omitzero,inline"` paramUnion } func (u BuildCompareParamsHeadRevisionUnion) MarshalJSON() ([]byte, error) { - return param.MarshalUnion(u, u.OfString, u.OfFileInputMap) + return param.MarshalUnion(u, u.OfBuildComparesHeadRevisionObject, u.OfString, u.OfFileInputMap) } func (u *BuildCompareParamsHeadRevisionUnion) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, u) } func (u *BuildCompareParamsHeadRevisionUnion) asAny() any { - if !param.IsOmitted(u.OfString) { + if !param.IsOmitted(u.OfBuildComparesHeadRevisionObject) { + return u.OfBuildComparesHeadRevisionObject + } else if !param.IsOmitted(u.OfString) { return &u.OfString.Value } else if !param.IsOmitted(u.OfFileInputMap) { return &u.OfFileInputMap } return nil } + +// A merge command combined with explicit file contents. The files are committed to +// the merge target (`base`) without performing an auto-merge. +// +// The properties Files, Merge are required. +type BuildCompareParamsHeadRevisionObject struct { + // File contents to commit directly + Files map[string]shared.FileInputUnionParam `json:"files,omitzero" api:"required"` + // A merge command in the format "base..head" + Merge string `json:"merge" api:"required"` + paramObj +} + +func (r BuildCompareParamsHeadRevisionObject) MarshalJSON() (data []byte, err error) { + type shadow BuildCompareParamsHeadRevisionObject + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *BuildCompareParamsHeadRevisionObject) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/build_test.go b/build_test.go index 6b50fa10..ee2976e2 100644 --- a/build_test.go +++ b/build_test.go @@ -29,7 +29,16 @@ func TestBuildNewWithOptionalParams(t *testing.T) { _, err := client.Builds.New(context.TODO(), stainless.BuildNewParams{ Project: stainless.String("project"), Revision: stainless.BuildNewParamsRevisionUnion{ - OfString: stainless.String("string"), + OfBuildNewsRevisionObject: &stainless.BuildNewParamsRevisionObject{ + Files: map[string]shared.FileInputUnionParam{ + "foo": { + OfFileInputContent: &shared.FileInputContentParam{ + Content: "content", + }, + }, + }, + Merge: "merge", + }, }, AllowEmpty: stainless.Bool(true), Branch: stainless.String("branch"), @@ -129,14 +138,32 @@ func TestBuildCompareWithOptionalParams(t *testing.T) { Base: stainless.BuildCompareParamsBase{ Branch: "branch", Revision: stainless.BuildCompareParamsBaseRevisionUnion{ - OfString: stainless.String("string"), + OfBuildComparesBaseRevisionObject: &stainless.BuildCompareParamsBaseRevisionObject{ + Files: map[string]shared.FileInputUnionParam{ + "foo": { + OfFileInputContent: &shared.FileInputContentParam{ + Content: "content", + }, + }, + }, + Merge: "merge", + }, }, CommitMessage: stainless.String("commit_message"), }, Head: stainless.BuildCompareParamsHead{ Branch: "branch", Revision: stainless.BuildCompareParamsHeadRevisionUnion{ - OfString: stainless.String("string"), + OfBuildComparesHeadRevisionObject: &stainless.BuildCompareParamsHeadRevisionObject{ + Files: map[string]shared.FileInputUnionParam{ + "foo": { + OfFileInputContent: &shared.FileInputContentParam{ + Content: "content", + }, + }, + }, + Merge: "merge", + }, }, CommitMessage: stainless.String("commit_message"), }, diff --git a/builddiagnostic.go b/builddiagnostic.go index c8d2bec4..82da63f5 100644 --- a/builddiagnostic.go +++ b/builddiagnostic.go @@ -187,7 +187,7 @@ func (r *BuildDiagnosticMoreUnion) UnmarshalJSON(data []byte) error { type BuildDiagnosticMoreMarkdown struct { Markdown string `json:"markdown" api:"required"` - Type constant.Markdown `json:"type" api:"required"` + Type constant.Markdown `json:"type" default:"markdown"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Markdown respjson.Field @@ -205,7 +205,7 @@ func (r *BuildDiagnosticMoreMarkdown) UnmarshalJSON(data []byte) error { type BuildDiagnosticMoreRaw struct { Raw string `json:"raw" api:"required"` - Type constant.Raw `json:"type" api:"required"` + Type constant.Raw `json:"type" default:"raw"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { Raw respjson.Field diff --git a/buildtargetoutput.go b/buildtargetoutput.go index a735ca54..ea65f1c1 100644 --- a/buildtargetoutput.go +++ b/buildtargetoutput.go @@ -133,7 +133,7 @@ func (r *BuildTargetOutputGetResponseUnion) UnmarshalJSON(data []byte) error { } type BuildTargetOutputGetResponseURL struct { - Output constant.URL `json:"output" api:"required"` + Output constant.URL `json:"output" default:"url"` // Any of "node", "typescript", "python", "go", "java", "kotlin", "ruby", // "terraform", "cli", "php", "csharp", "sql", "openapi". Target shared.Target `json:"target" api:"required"` @@ -178,7 +178,7 @@ const ( type BuildTargetOutputGetResponseGit struct { // Temporary GitHub access token Token string `json:"token" api:"required"` - Output constant.Git `json:"output" api:"required"` + Output constant.Git `json:"output" default:"git"` // Git reference (commit SHA, branch, or tag) Ref string `json:"ref" api:"required"` // Any of "node", "typescript", "python", "go", "java", "kotlin", "ruby", diff --git a/client.go b/client.go index 98ecebda..766c7b23 100644 --- a/client.go +++ b/client.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "slices" + "strings" "github.com/stainless-api/stainless-api-go/internal/requestconfig" "github.com/stainless-api/stainless-api-go/option" @@ -26,13 +27,21 @@ type Client struct { // DefaultClientOptions read from the environment (STAINLESS_API_KEY, // STAINLESS_BASE_URL). This should be used to initialize new clients. func DefaultClientOptions() []option.RequestOption { - defaults := []option.RequestOption{option.WithEnvironmentProduction()} + defaults := []option.RequestOption{option.WithHTTPClient(defaultHTTPClient()), option.WithEnvironmentProduction()} if o, ok := os.LookupEnv("STAINLESS_BASE_URL"); ok { defaults = append(defaults, option.WithBaseURL(o)) } if o, ok := os.LookupEnv("STAINLESS_API_KEY"); ok { defaults = append(defaults, option.WithAPIKey(o)) } + if o, ok := os.LookupEnv("STAINLESS_CUSTOM_HEADERS"); ok { + for _, line := range strings.Split(o, "\n") { + colon := strings.Index(line, ":") + if colon >= 0 { + defaults = append(defaults, option.WithHeader(strings.TrimSpace(line[:colon]), strings.TrimSpace(line[colon+1:]))) + } + } + } return defaults } diff --git a/default_http_client.go b/default_http_client.go new file mode 100644 index 00000000..436bf5a6 --- /dev/null +++ b/default_http_client.go @@ -0,0 +1,30 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package stainless + +import ( + "net/http" + "time" +) + +// defaultResponseHeaderTimeout bounds the time between a fully written request +// and the server's response headers. It does not apply to the response body, +// so long-running streams are unaffected. Without this, a server that accepts +// the connection but never responds would hang the request indefinitely. +const defaultResponseHeaderTimeout = 10 * time.Minute + +// defaultHTTPClient returns an [*http.Client] used when the caller does not +// supply one via [option.WithHTTPClient]. When [http.DefaultTransport] is the +// stdlib [*http.Transport], it is cloned and a [http.Transport.ResponseHeaderTimeout] +// is set so stuck connections fail fast instead of compounding across retries. +// If [http.DefaultTransport] has been wrapped (for example by otelhttp for +// distributed tracing), the wrapping is preserved and the header timeout is +// skipped. +func defaultHTTPClient() *http.Client { + if t, ok := http.DefaultTransport.(*http.Transport); ok { + t = t.Clone() + t.ResponseHeaderTimeout = defaultResponseHeaderTimeout + return &http.Client{Transport: t} + } + return &http.Client{Transport: http.DefaultTransport} +} diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index 40916adb..f8bbc166 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -58,7 +58,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string arrayFmt string root bool @@ -76,7 +76,7 @@ func (e *encoder) marshal(value any, writer *multipart.Writer) error { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, arrayFmt: e.arrayFmt, root: e.root, @@ -183,6 +183,18 @@ func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { itemEncoder := e.typeEncoder(t.Elem()) keyFn := e.arrayKeyEncoder() + if e.arrayFmt == "comma" { + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if v.Len() == 0 { + return nil + } + elements := make([]string, v.Len()) + for i := 0; i < v.Len(); i++ { + elements[i] = fmt.Sprint(v.Index(i).Interface()) + } + return writer.WriteField(key, strings.Join(elements, ",")) + } + } return func(key string, v reflect.Value, writer *multipart.Writer) error { if keyFn == nil { return fmt.Errorf("apiform: unsupported array format") @@ -265,6 +277,14 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { } return typeEncoderFn(key, value, writer) } + } else if ptag.defaultValue != nil { + typeEncoderFn := e.typeEncoder(field.Type) + encoderFn = func(key string, value reflect.Value, writer *multipart.Writer) error { + if value.IsZero() { + return typeEncoderFn(key, reflect.ValueOf(ptag.defaultValue), writer) + } + return typeEncoderFn(key, value, writer) + } } else { encoderFn = e.typeEncoder(field.Type) } diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index a2290b09..3f1e942a 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -123,6 +123,11 @@ type StructUnion struct { param.APIUnion } +type ConstantStruct struct { + Anchor string `form:"anchor" default:"created_at"` + Seconds int `form:"seconds"` +} + type MultipartMarshalerParent struct { Middle MultipartMarshalerMiddleNext `form:"middle"` } @@ -554,6 +559,37 @@ Content-Disposition: form-data; name="union" Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), }, }, + "constant_zero_value": { + `--xxx +Content-Disposition: form-data; name="anchor" + +created_at +--xxx +Content-Disposition: form-data; name="seconds" + +3600 +--xxx-- +`, + ConstantStruct{ + Seconds: 3600, + }, + }, + "constant_explicit_value": { + `--xxx +Content-Disposition: form-data; name="anchor" + +created_at_override +--xxx +Content-Disposition: form-data; name="seconds" + +3600 +--xxx-- +`, + ConstantStruct{ + Anchor: "created_at_override", + Seconds: 3600, + }, + }, "deeply-nested-struct,brackets": { `--xxx Content-Disposition: form-data; name="middle[middleNext][child]" diff --git a/internal/apiform/tag.go b/internal/apiform/tag.go index d9915d4a..f0c9d14c 100644 --- a/internal/apiform/tag.go +++ b/internal/apiform/tag.go @@ -9,13 +9,15 @@ const apiStructTag = "api" const jsonStructTag = "json" const formStructTag = "form" const formatStructTag = "format" +const defaultStructTag = "default" type parsedStructTag struct { - name string - required bool - extras bool - metadata bool - omitzero bool + name string + required bool + extras bool + metadata bool + omitzero bool + defaultValue any } func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { @@ -45,9 +47,23 @@ func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool } parseApiStructTag(field, &tag) + parseDefaultStructTag(field, &tag) return tag, ok } +func parseDefaultStructTag(field reflect.StructField, tag *parsedStructTag) { + if field.Type.Kind() != reflect.String { + // Only strings are currently supported + return + } + + raw, ok := field.Tag.Lookup(defaultStructTag) + if !ok { + return + } + tag.defaultValue = raw +} + func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { raw, ok := field.Tag.Lookup(apiStructTag) if !ok { diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go index a96464a2..465a3ccf 100644 --- a/internal/apijson/decoder.go +++ b/internal/apijson/decoder.go @@ -80,7 +80,7 @@ type decoderField struct { } type decoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool } @@ -108,7 +108,7 @@ func (d *decoderBuilder) unmarshalWithExactness(raw []byte, to any) (exactness, func (d *decoderBuilder) typeDecoder(t reflect.Type) decoderFunc { entry := decoderEntry{ - Type: t, + typ: t, dateFormat: d.dateFormat, root: d.root, } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index 0decb733..ed0b4f32 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -12,6 +12,8 @@ import ( "time" "github.com/tidwall/sjson" + + shimjson "github.com/stainless-api/stainless-api-go/internal/encoding/json" ) var encoders sync.Map // map[encoderEntry]encoderFunc @@ -44,7 +46,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool } @@ -61,7 +63,7 @@ func (e *encoder) marshal(value any) ([]byte, error) { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, root: e.root, } @@ -271,6 +273,12 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { if err != nil { return nil, err } + if ef.tag.defaultValue != nil && (!field.IsValid() || field.IsZero()) { + encoded, err = shimjson.Marshal(ef.tag.defaultValue) + if err != nil { + return nil, err + } + } if encoded == nil { continue } diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go index 19b36146..2853bf94 100644 --- a/internal/apijson/json_test.go +++ b/internal/apijson/json_test.go @@ -614,3 +614,20 @@ func TestEncode(t *testing.T) { }) } } + +type StructWithDefault struct { + Type string `json:"type" default:"foo"` +} + +func TestDefault(t *testing.T) { + value := StructWithDefault{} + expected := `{"type":"foo"}` + + raw, err := Marshal(value) + if err != nil { + t.Fatalf("serialization of %v failed with error %v", value, err) + } + if string(raw) != expected { + t.Fatalf("expected %+#v to serialize to %s but got %s", value, expected, string(raw)) + } +} diff --git a/internal/apijson/tag.go b/internal/apijson/tag.go index 17b21302..efcaf8ca 100644 --- a/internal/apijson/tag.go +++ b/internal/apijson/tag.go @@ -8,13 +8,15 @@ import ( const apiStructTag = "api" const jsonStructTag = "json" const formatStructTag = "format" +const defaultStructTag = "default" type parsedStructTag struct { - name string - required bool - extras bool - metadata bool - inline bool + name string + required bool + extras bool + metadata bool + inline bool + defaultValue any } func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { @@ -42,9 +44,23 @@ func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool // the `api` struct tag is only used alongside `json` for custom behaviour parseApiStructTag(field, &tag) + parseDefaultStructTag(field, &tag) return tag, ok } +func parseDefaultStructTag(field reflect.StructField, tag *parsedStructTag) { + if field.Type.Kind() != reflect.String { + // Only strings are currently supported + return + } + + raw, ok := field.Tag.Lookup(defaultStructTag) + if !ok { + return + } + tag.defaultValue = raw +} + func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { raw, ok := field.Tag.Lookup(apiStructTag) if !ok { diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index a843d580..ebd476cc 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -29,7 +29,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool settings QuerySettings @@ -42,7 +42,7 @@ type Pair struct { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, root: e.root, settings: e.settings, diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go index b9a9d4bb..2da9f4ea 100644 --- a/internal/encoding/json/encode.go +++ b/internal/encoding/json/encode.go @@ -173,15 +173,21 @@ import ( // JSON cannot represent cyclic data structures and Marshal does not // handle them. Passing cyclic structures to Marshal will result in // an error. -func Marshal(v any) ([]byte, error) { +// EDIT(begin): add optimization options +func Marshal(v any, opts ...Option) ([]byte, error) { + // EDIT(end): add optimization options e := newEncodeState() defer encodeStatePool.Put(e) - // SHIM(begin): don't escape HTML by default - err := e.marshal(v, encOpts{escapeHTML: shims.EscapeHTMLByDefault}) + // EDIT(begin): don't escape HTML by default, and apply options + encOpts := encOpts{escapeHTML: shims.EscapeHTMLByDefault} + if opts != nil { + encOpts = encOpts.apply(opts...) + } + err := e.marshal(v, encOpts) // ORIGINAL: // err := e.marshal(v, encOpts{escapeHTML: true}) - // SHIM(end) + // EDIT(end) if err != nil { return nil, err } @@ -352,6 +358,9 @@ type encOpts struct { // EDIT(begin): save the timefmt timefmt string // EDIT(end) + // EDIT(begin): add optimization to skip compaction + skipCompaction bool + // EDIT(end) } type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts) @@ -483,7 +492,7 @@ func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { if err == nil { e.Grow(len(b)) out := e.AvailableBuffer() - out, err = appendCompact(out, b, opts.escapeHTML) + out, err = appendCompact(out, b, opts) e.Buffer.Write(out) } if err != nil { @@ -509,7 +518,7 @@ func addrMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { if err == nil { e.Grow(len(b)) out := e.AvailableBuffer() - out, err = appendCompact(out, b, opts.escapeHTML) + out, err = appendCompact(out, b, opts) e.Buffer.Write(out) } if err != nil { diff --git a/internal/encoding/json/indent.go b/internal/encoding/json/indent.go index 01bfdf65..c9d6ca5b 100644 --- a/internal/encoding/json/indent.go +++ b/internal/encoding/json/indent.go @@ -4,7 +4,9 @@ package json -import "bytes" +import ( + "bytes" +) // HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 // characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 @@ -41,12 +43,21 @@ func appendHTMLEscape(dst, src []byte) []byte { func Compact(dst *bytes.Buffer, src []byte) error { dst.Grow(len(src)) b := dst.AvailableBuffer() - b, err := appendCompact(b, src, false) + b, err := appendCompact(b, src, encOpts{}) dst.Write(b) return err } -func appendCompact(dst, src []byte, escape bool) ([]byte, error) { +func appendCompact(dst, src []byte, opts encOpts) ([]byte, error) { + // EDIT(begin): optimize for skipCompaction + if opts.skipCompaction { + dst = append(dst, src...) + return dst, nil + } + + escape := opts.escapeHTML + // EDIT(end) + origLen := len(dst) scan := newScanner() defer freeScanner(scan) diff --git a/internal/encoding/json/opt.go b/internal/encoding/json/opt.go new file mode 100644 index 00000000..fd6f8d2f --- /dev/null +++ b/internal/encoding/json/opt.go @@ -0,0 +1,24 @@ +// EDIT(begin): add custom options for JSON encoding +package json + +type Option func(*encOpts) + +// Every time a sub-type of [json.Marshaler] is encountered, +// skip a redundant and costly compaction step, trust it to self-compact. +// +// This is a divergence from the standard library behavior, and is only guaranteed +// safe with SDK types. +func WithSkipCompaction(b bool) Option { + return func(eos *encOpts) { + eos.skipCompaction = true + } +} + +func (eos encOpts) apply(opts ...Option) encOpts { + for _, opt := range opts { + opt(&eos) + } + return eos +} + +// EDIT(end) diff --git a/internal/encoding/json/stream.go b/internal/encoding/json/stream.go index e2d9470b..652522c3 100644 --- a/internal/encoding/json/stream.go +++ b/internal/encoding/json/stream.go @@ -6,7 +6,6 @@ package json import ( "bytes" - "errors" "io" ) @@ -253,30 +252,34 @@ func (enc *Encoder) SetEscapeHTML(on bool) { enc.escapeHTML = on } -// RawMessage is a raw encoded JSON value. -// It implements [Marshaler] and [Unmarshaler] and can -// be used to delay JSON decoding or precompute a JSON encoding. -type RawMessage []byte - -// MarshalJSON returns m as the JSON encoding of m. -func (m RawMessage) MarshalJSON() ([]byte, error) { - if m == nil { - return []byte("null"), nil - } - return m, nil -} - -// UnmarshalJSON sets *m to a copy of data. -func (m *RawMessage) UnmarshalJSON(data []byte) error { - if m == nil { - return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") - } - *m = append((*m)[0:0], data...) - return nil -} - -var _ Marshaler = (*RawMessage)(nil) -var _ Unmarshaler = (*RawMessage)(nil) +// EDIT(begin): remove RawMessage +// +// // RawMessage is a raw encoded JSON value. +// // It implements [Marshaler] and [Unmarshaler] and can +// // be used to delay JSON decoding or precompute a JSON encoding. +// type RawMessage []byte +// +// // MarshalJSON returns m as the JSON encoding of m. +// func (m RawMessage) MarshalJSON() ([]byte, error) { +// if m == nil { +// return []byte("null"), nil +// } +// return m, nil +// } +// +// // UnmarshalJSON sets *m to a copy of data. +// func (m *RawMessage) UnmarshalJSON(data []byte) error { +// if m == nil { +// return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") +// } +// *m = append((*m)[0:0], data...) +// return nil +// } +// +// var _ Marshaler = (*RawMessage)(nil) +// var _ Unmarshaler = (*RawMessage)(nil) +// +// EDIT(end) // A Token holds a value of one of these types: // diff --git a/internal/encoding/json/time.go b/internal/encoding/json/time.go index 601f941d..8e8e3b6a 100644 --- a/internal/encoding/json/time.go +++ b/internal/encoding/json/time.go @@ -50,7 +50,7 @@ func timeMarshalEncoder(e *encodeState, v reflect.Value, opts encOpts) bool { if b != nil { e.Grow(len(b)) out := e.AvailableBuffer() - out, _ = appendCompact(out, b, opts.escapeHTML) + out, _ = appendCompact(out, b, opts) e.Buffer.Write(out) return true } diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index 962432c6..b2f9f0f6 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -122,7 +122,13 @@ func NewRequestConfig(ctx context.Context, method string, u string, body any, ds } params := q.Encode() if params != "" { - u = u + "?" + params + parsed, _ := url.Parse(u) + if parsed.RawQuery != "" { + parsed.RawQuery = parsed.RawQuery + "&" + params + u = parsed.String() + } else { + u = u + "?" + params + } } } if body, ok := body.([]byte); ok { diff --git a/internal/version.go b/internal/version.go index 7d5879c5..d26ea0ab 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.30.0" // x-release-please-version +const PackageVersion = "0.31.0" // x-release-please-version diff --git a/option/middleware.go b/option/middleware.go index 8ec9dd60..4be09875 100644 --- a/option/middleware.go +++ b/option/middleware.go @@ -8,6 +8,10 @@ import ( "net/http/httputil" ) +// sensitiveLogHeaders are redacted before request and response content is +// written to the debug logger. +var sensitiveLogHeaders = []string{"authorization", "api-key", "x-api-key", "cookie", "set-cookie"} + // WithDebugLog logs the HTTP request and response content. // If the logger parameter is nil, it uses the default logger. // @@ -20,7 +24,7 @@ func WithDebugLog(logger *log.Logger) RequestOption { logger = log.Default() } - if reqBytes, err := httputil.DumpRequest(req, true); err == nil { + if reqBytes, err := dumpRedactedRequest(req); err == nil { logger.Printf("Request Content:\n%s\n", reqBytes) } @@ -29,10 +33,48 @@ func WithDebugLog(logger *log.Logger) RequestOption { return resp, err } - if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + if respBytes, err := dumpRedactedResponse(resp); err == nil { logger.Printf("Response Content:\n%s\n", respBytes) } return resp, err }) } + +// dumpRedactedRequest dumps req with sensitive headers replaced. The +// original headers are restored via defer so a panic in DumpRequest cannot +// leak the placeholder map into the live request sent downstream. +func dumpRedactedRequest(req *http.Request) ([]byte, error) { + origHeaders := req.Header + req.Header = redactDebugHeaders(origHeaders) + defer func() { req.Header = origHeaders }() + return httputil.DumpRequest(req, true) +} + +func dumpRedactedResponse(resp *http.Response) ([]byte, error) { + origHeaders := resp.Header + resp.Header = redactDebugHeaders(origHeaders) + defer func() { resp.Header = origHeaders }() + return httputil.DumpResponse(resp, true) +} + +func redactDebugHeaders(headers http.Header) http.Header { + var redacted http.Header + for _, name := range sensitiveLogHeaders { + values := headers.Values(name) + if len(values) == 0 { + continue + } + if redacted == nil { + redacted = headers.Clone() + } + redacted.Del(name) + for range values { + redacted.Add(name, "***") + } + } + if redacted == nil { + return headers + } + return redacted +} diff --git a/packages/param/encoder.go b/packages/param/encoder.go index 7a404515..47b98e4a 100644 --- a/packages/param/encoder.go +++ b/packages/param/encoder.go @@ -66,7 +66,7 @@ func MarshalWithExtras[T ParamStruct, R any](f T, underlying any, extras map[str } else if ovr, ok := f.Overrides(); ok { return shimjson.Marshal(ovr) } else { - return shimjson.Marshal(underlying) + return shimjson.Marshal(underlying, shimjson.WithSkipCompaction(true)) } } @@ -96,7 +96,7 @@ func MarshalUnion[T ParamStruct](metadata T, variants ...any) ([]byte, error) { Err: fmt.Errorf("expected union to have only one present variant, got %d", nPresent), } } - return shimjson.Marshal(variants[presentIdx]) + return shimjson.Marshal(variants[presentIdx], shimjson.WithSkipCompaction(true)) } // typeFor is shimmed from Go 1.23 "reflect" package diff --git a/packages/param/encoder_test.go b/packages/param/encoder_test.go index e311890a..ef3dcc23 100644 --- a/packages/param/encoder_test.go +++ b/packages/param/encoder_test.go @@ -1,10 +1,13 @@ package param_test import ( + "bytes" "encoding/json" + "reflect" "testing" "time" + shimjson "github.com/stainless-api/stainless-api-go/internal/encoding/json" "github.com/stainless-api/stainless-api-go/packages/param" ) @@ -375,3 +378,176 @@ func TestNullStructUnion(t *testing.T) { t.Fatalf("expected null, received %s", string(b)) } } + +// +// Compaction optimization +// + +type NonCompactedDoubleParent struct { + Prop string `json:"prop"` + Parent NonCompactedParent `json:"parent"` + + param.APIObject +} + +type NonCompactedParent struct { + BadChild NonCompacted `json:"bad_child"` + + param.APIObject +} + +type NonCompacted struct { + Raw string + + param.APIObject +} + +func (a NonCompactedDoubleParent) MarshalJSON() ([]byte, error) { + type shadow NonCompactedDoubleParent + return param.MarshalObject(a, (*shadow)(&a)) +} + +func (a NonCompactedParent) MarshalJSON() ([]byte, error) { + type shadow NonCompactedParent + return param.MarshalObject(a, (*shadow)(&a)) +} + +func (a NonCompacted) MarshalJSON() ([]byte, error) { + if a.Raw == "" { + a.Raw = nonCompactedRaw + } + return []byte(a.Raw), nil +} + +var nonCompactedRaw string = ` { "foo": "bar" } ` + +func TestAppendCompactBroken(t *testing.T) { + tests := map[string]struct { + value json.Marshaler + }{ + "red/illegal-json": { + NonCompacted{Raw: `{ "broken": "json" `}, + }, + "red/nested-with-illegal-json": { + NonCompactedParent{BadChild: NonCompacted{ + Raw: `{ "broken": "json" `, + }}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + v, err := json.Marshal(test.value) + if err == nil { + t.Fatal("expected error got", v) + } + }) + } +} + +// TestAppendCompact validates an optimization for internal SDK types to +// avoid O(keys^2) iteration over each JSON object. +// +// It's possible to intentionally trigger this behavior as both a user and +// SDK developer. However, the edge case is quite pathological and requires +// calling [json.Marshaler.MarshalJSON] rather than [json.Marshal]. +func TestAppendCompact(t *testing.T) { + + tests := map[string]struct { + value json.Marshaler + expected string + }{ + // + // Non-compacted cases + // + // Note this is how to exploit the compacter to fail, you must call [json.Marshaler.MarshalJSON] rather than [json.Marshal]. + // The type must also embed [param.APIObject] and return non-compacted JSON. + // + + "no-compact/fails-compaction": { + NonCompacted{Raw: nonCompactedRaw}, + nonCompactedRaw, + }, + "no-compact/nested-with-bad-child": { + NonCompactedParent{BadChild: NonCompacted{ + Raw: nonCompactedRaw, + }}, + `{"bad_child":` + nonCompactedRaw + `}`, + }, + "no-compact/double-nested-with-bad-child": { + NonCompactedDoubleParent{Prop: "1", Parent: NonCompactedParent{BadChild: NonCompacted{ + Raw: nonCompactedRaw, + }}}, + `{"prop":"1","parent":{"bad_child":` + nonCompactedRaw + `}}`, + }, + + // + // Compacted cases + // + + "override/spaces-within": { + param.Override[NonCompactedDoubleParent](json.RawMessage(`{"com": "pact"}`)), + `{"com":"pact"}`, + }, + "override/spaces-after": { + param.Override[NonCompactedDoubleParent](json.RawMessage(`{"com":"pact"} `)), + `{"com":"pact"}`, + }, + "override/spaces-before": { + param.Override[NonCompactedDoubleParent](json.RawMessage(` {"com":"pact"}`)), + `{"com":"pact"}`, + }, + "override/spaces-around": { + param.Override[NonCompactedDoubleParent](json.RawMessage(` { "com": "pact" }`)), + `{"com":"pact"}`, + }, + "override/override-with-nested": { + param.Override[NonCompactedDoubleParent](NonCompactedParent{}), + `{"bad_child":{"foo":"bar"}}`, + }, + "override/override-with-non-compacted": { + param.Override[NonCompactedDoubleParent](NonCompacted{}), + `{"foo":"bar"}`, + }, + } + + for name, test := range tests { + t.Run(name+"/marshal-json", func(t *testing.T) { + b, err := test.value.MarshalJSON() + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + if string(b) != test.expected { + t.Fatalf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) + } + }) + + t.Run(name+"/json-marshal", func(t *testing.T) { + b, err := json.Marshal(test.value) + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + + // expected output of JSON Marshal should always be compacted + var compactedExpected bytes.Buffer + err = json.Compact(&compactedExpected, []byte(test.expected)) + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + + if string(b) != compactedExpected.String() { + t.Fatalf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) + } + }) + + t.Run(name+"/shimjson-marshal", func(t *testing.T) { + b, err := shimjson.Marshal(test.value) + if err != nil { + t.Fatalf("didn't expect error %v, expected %s", err, test.expected) + } + if string(b) != test.expected { + t.Logf("expected %s (%s), received %s", test.expected, reflect.TypeOf(test.value), string(b)) + } + }) + } +} diff --git a/projectbranch.go b/projectbranch.go index 1ea1563a..7e1c4f81 100644 --- a/projectbranch.go +++ b/projectbranch.go @@ -17,6 +17,7 @@ import ( "github.com/stainless-api/stainless-api-go/packages/pagination" "github.com/stainless-api/stainless-api-go/packages/param" "github.com/stainless-api/stainless-api-go/packages/respjson" + "github.com/stainless-api/stainless-api-go/shared" ) // ProjectBranchService contains methods and other services that help with @@ -137,6 +138,9 @@ func (r *ProjectBranchService) Delete(ctx context.Context, branch string, body P // // The branch is rebased onto the `base` branch or commit SHA, inheriting any // config and custom code changes. +// +// If `files` is provided, the auto-rebase is skipped: the branch is hard-reset to +// `base` and the provided files are committed on top. func (r *ProjectBranchService) Rebase(ctx context.Context, branch string, params ProjectBranchRebaseParams, opts ...option.RequestOption) (res *ProjectBranch, err error) { opts = slices.Concat(r.Options, opts) precfg, err := requestconfig.PreRequestOptions(opts...) @@ -449,9 +453,23 @@ type ProjectBranchRebaseParams struct { Project param.Opt[string] `path:"project,omitzero" api:"required" json:"-"` // The branch or commit SHA to rebase onto. Defaults to "main". Base param.Opt[string] `query:"base,omitzero" json:"-"` + // Optional commit message to use when `files` is provided. + CommitMessage param.Opt[string] `json:"commit_message,omitzero"` + // File contents to commit directly on top of `base`. When provided, the + // auto-rebase is skipped and the branch is hard-reset to `base` before the files + // are committed. + Files map[string]shared.FileInputUnionParam `json:"files,omitzero"` paramObj } +func (r ProjectBranchRebaseParams) MarshalJSON() (data []byte, err error) { + type shadow ProjectBranchRebaseParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ProjectBranchRebaseParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // URLQuery serializes [ProjectBranchRebaseParams]'s query parameters as // `url.Values`. func (r ProjectBranchRebaseParams) URLQuery() (v url.Values, err error) { diff --git a/projectbranch_test.go b/projectbranch_test.go index 07139654..2fe6ad77 100644 --- a/projectbranch_test.go +++ b/projectbranch_test.go @@ -11,6 +11,7 @@ import ( "github.com/stainless-api/stainless-api-go" "github.com/stainless-api/stainless-api-go/internal/testutil" "github.com/stainless-api/stainless-api-go/option" + "github.com/stainless-api/stainless-api-go/shared" ) func TestProjectBranchNewWithOptionalParams(t *testing.T) { @@ -138,8 +139,16 @@ func TestProjectBranchRebaseWithOptionalParams(t *testing.T) { context.TODO(), "branch", stainless.ProjectBranchRebaseParams{ - Project: stainless.String("project"), - Base: stainless.String("base"), + Project: stainless.String("project"), + Base: stainless.String("base"), + CommitMessage: stainless.String("commit_message"), + Files: map[string]shared.FileInputUnionParam{ + "foo": { + OfFileInputContent: &shared.FileInputContentParam{ + Content: "content", + }, + }, + }, }, ) if err != nil { diff --git a/scripts/bootstrap b/scripts/bootstrap index 5ab30665..46547f18 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response diff --git a/scripts/mock b/scripts/mock index bcf3b392..feebe5ed 100755 --- a/scripts/mock +++ b/scripts/mock @@ -19,34 +19,34 @@ fi echo "==> Starting mock server with URL ${URL}" -# Run prism mock on the given spec +# Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stdy/cli@0.22.1 -- steady --version - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & - # Wait for server to come online (max 30s) + # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" attempts=0 - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi attempts=$((attempts + 1)) if [ "$attempts" -ge 300 ]; then echo - echo "Timed out waiting for Prism server to start" - cat .prism.log + echo "Timed out waiting for Steady server to start" + cat .stdy.log exit 1 fi 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" + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index c26b1222..14cb1afc 100755 --- a/scripts/test +++ b/scripts/test @@ -9,8 +9,8 @@ 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 +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 } kill_server_on_port() { @@ -25,7 +25,7 @@ function is_overriding_api_base_url() { [ -n "$TEST_API_BASE_URL" ] } -if ! is_overriding_api_base_url && ! prism_is_running ; then +if ! is_overriding_api_base_url && ! steady_is_running ; then # When we exit this script, make sure to kill the background mock server process trap 'kill_server_on_port 4010' EXIT @@ -36,19 +36,19 @@ 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" +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady 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 -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=comma --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" echo fi diff --git a/shared/constant/constants.go b/shared/constant/constants.go index 7aa9ba13..416006b4 100644 --- a/shared/constant/constants.go +++ b/shared/constant/constants.go @@ -26,6 +26,7 @@ type NotStarted string // Always "not_started" type Queued string // Always "queued" type Raw string // Always "raw" type URL string // Always "url" +type Waiting string // Always "waiting" func (c Completed) Default() Completed { return "completed" } func (c Git) Default() Git { return "git" } @@ -35,6 +36,7 @@ func (c NotStarted) Default() NotStarted { return "not_started" } func (c Queued) Default() Queued { return "queued" } func (c Raw) Default() Raw { return "raw" } func (c URL) Default() URL { return "url" } +func (c Waiting) Default() Waiting { return "waiting" } func (c Completed) MarshalJSON() ([]byte, error) { return marshalString(c) } func (c Git) MarshalJSON() ([]byte, error) { return marshalString(c) } @@ -44,6 +46,7 @@ func (c NotStarted) MarshalJSON() ([]byte, error) { return marshalString(c) } func (c Queued) MarshalJSON() ([]byte, error) { return marshalString(c) } func (c Raw) MarshalJSON() ([]byte, error) { return marshalString(c) } func (c URL) MarshalJSON() ([]byte, error) { return marshalString(c) } +func (c Waiting) MarshalJSON() ([]byte, error) { return marshalString(c) } type constant[T any] interface { Constant[T]