diff --git a/.gitignore b/.gitignore index a07a7493..27316da6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules .env .idea .vscode +openapi.json diff --git a/package.json b/package.json index ed4fcded..55242761 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "packageManager": "bun@1.2.14", "scripts": { "typecheck": "bun run --filter='*' typecheck", + "stainless": "bun run ./packages/opencode/src/index.ts serve ", "postinstall": "./scripts/hooks" }, "workspaces": { diff --git a/packages/tui/go.mod b/packages/tui/go.mod index 6cd1bae6..043d9fcd 100644 --- a/packages/tui/go.mod +++ b/packages/tui/go.mod @@ -20,6 +20,8 @@ require ( rsc.io/qr v0.2.0 ) +replace github.com/sst/opencode-sdk-go => ./sdk + require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect require ( diff --git a/packages/tui/go.sum b/packages/tui/go.sum index ac6981f2..29548273 100644 --- a/packages/tui/go.sum +++ b/packages/tui/go.sum @@ -181,8 +181,6 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/sst/opencode-sdk-go v0.1.0-alpha.8 h1:Tp7nbckbMCwAA/ieVZeeZCp79xXtrPMaWLRk5mhNwrw= -github.com/sst/opencode-sdk-go v0.1.0-alpha.8/go.mod h1:uagorfAHZsVy6vf0xY6TlQraM4uCILdZ5tKKhl1oToM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/packages/tui/sdk/.devcontainer/devcontainer.json b/packages/tui/sdk/.devcontainer/devcontainer.json new file mode 100644 index 00000000..889ae347 --- /dev/null +++ b/packages/tui/sdk/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Development", + "image": "mcr.microsoft.com/devcontainers/go:1.23-bookworm", + "postCreateCommand": "go mod tidy" +} diff --git a/packages/tui/sdk/.github/workflows/ci.yml b/packages/tui/sdk/.github/workflows/ci.yml new file mode 100644 index 00000000..4bf1e907 --- /dev/null +++ b/packages/tui/sdk/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI +on: + push: + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'stl-preview-head/**' + - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' + +jobs: + lint: + timeout-minutes: 10 + name: lint + runs-on: ${{ github.repository == 'stainless-sdks/opencode-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + + steps: + - uses: actions/checkout@v4 + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Run lints + run: ./scripts/lint + test: + timeout-minutes: 10 + name: test + runs-on: ${{ github.repository == 'stainless-sdks/opencode-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + steps: + - uses: actions/checkout@v4 + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test diff --git a/packages/tui/sdk/.gitignore b/packages/tui/sdk/.gitignore new file mode 100644 index 00000000..c6d05015 --- /dev/null +++ b/packages/tui/sdk/.gitignore @@ -0,0 +1,4 @@ +.prism.log +codegen.log +Brewfile.lock.json +.idea/ diff --git a/packages/tui/sdk/.release-please-manifest.json b/packages/tui/sdk/.release-please-manifest.json new file mode 100644 index 00000000..c373724d --- /dev/null +++ b/packages/tui/sdk/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0-alpha.8" +} \ No newline at end of file diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml new file mode 100644 index 00000000..ab6c4a20 --- /dev/null +++ b/packages/tui/sdk/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 20 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-945f9da9e9a4c4008834deef63e4346c0076e020eed3d3c98c249095033c1ac5.yml +openapi_spec_hash: 522a44f6cb0677435fe2ac7693848ad7 +config_hash: 6c8822d278ba83456e5eed6d774ca230 diff --git a/packages/tui/sdk/Brewfile b/packages/tui/sdk/Brewfile new file mode 100644 index 00000000..577e34a4 --- /dev/null +++ b/packages/tui/sdk/Brewfile @@ -0,0 +1 @@ +brew "go" diff --git a/packages/tui/sdk/CHANGELOG.md b/packages/tui/sdk/CHANGELOG.md new file mode 100644 index 00000000..bc407fad --- /dev/null +++ b/packages/tui/sdk/CHANGELOG.md @@ -0,0 +1,73 @@ +# Changelog + +## 0.1.0-alpha.8 (2025-07-02) + +Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.7...v0.1.0-alpha.8) + +### Features + +* **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519)) + +## 0.1.0-alpha.7 (2025-06-30) + +Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.6...v0.1.0-alpha.7) + +### Features + +* **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775)) +* **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf)) + + +### Chores + +* **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d)) + +## 0.1.0-alpha.6 (2025-06-28) + +Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.5...v0.1.0-alpha.6) + +### Bug Fixes + +* don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d)) + +## 0.1.0-alpha.5 (2025-06-27) + +Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.4...v0.1.0-alpha.5) + +### Features + +* **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e)) + +## 0.1.0-alpha.4 (2025-06-27) + +Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.3...v0.1.0-alpha.4) + +### Features + +* **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52)) + +## 0.1.0-alpha.3 (2025-06-27) + +Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.2...v0.1.0-alpha.3) + +### Features + +* **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61)) + +## 0.1.0-alpha.2 (2025-06-27) + +Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.1...v0.1.0-alpha.2) + +### Features + +* **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6)) + +## 0.1.0-alpha.1 (2025-06-27) + +Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencode-sdk-go/compare/v0.0.1-alpha.0...v0.1.0-alpha.1) + +### Features + +* **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a)) +* **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397)) +* **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844)) diff --git a/packages/tui/sdk/CONTRIBUTING.md b/packages/tui/sdk/CONTRIBUTING.md new file mode 100644 index 00000000..34620a3c --- /dev/null +++ b/packages/tui/sdk/CONTRIBUTING.md @@ -0,0 +1,66 @@ +## Setting up the environment + +To set up the repository, run: + +```sh +$ ./scripts/bootstrap +$ ./scripts/build +``` + +This will install all the required dependencies and build the SDK. + +You can also [install go 1.18+ manually](https://go.dev/doc/install). + +## Modifying/Adding code + +Most of the SDK is generated code. Modifications to code will be persisted between generations, but may +result in merge conflicts between manual patches and changes from the generator. The generator will never +modify the contents of the `lib/` and `examples/` directories. + +## Adding and running examples + +All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. + +```go +# add an example to examples//main.go + +package main + +func main() { + // ... +} +``` + +```sh +$ go run ./examples/ +``` + +## Using the repository from source + +To use a local version of this library from source in another project, edit the `go.mod` with a replace +directive. This can be done through the CLI with the following: + +```sh +$ go mod edit -replace github.com/sst/opencode-sdk-go=/path/to/opencode-sdk-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 +``` + +## Formatting + +This library uses the standard gofmt code formatter: + +```sh +$ ./scripts/format +``` diff --git a/packages/tui/sdk/LICENSE b/packages/tui/sdk/LICENSE new file mode 100644 index 00000000..a56ceacd --- /dev/null +++ b/packages/tui/sdk/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Opencode + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/tui/sdk/README.md b/packages/tui/sdk/README.md new file mode 100644 index 00000000..2b578234 --- /dev/null +++ b/packages/tui/sdk/README.md @@ -0,0 +1,354 @@ +# Opencode Go API Library + +Go Reference + +The Opencode Go library provides convenient access to the [Opencode REST API](https://opencode.ai/docs) +from applications written in Go. + +It is generated with [Stainless](https://www.stainless.com/). + +## Installation + + + +```go +import ( + "github.com/sst/opencode-sdk-go" // imported as opencode +) +``` + + + +Or to pin the version: + + + +```sh +go get -u 'github.com/sst/opencode-sdk-go@v0.1.0-alpha.8' +``` + + + +## Requirements + +This library requires Go 1.18+. + +## Usage + +The full API of this library can be found in [api.md](api.md). + +```go +package main + +import ( + "context" + "fmt" + + "github.com/sst/opencode-sdk-go" +) + +func main() { + client := opencode.NewClient() + events, err := client.Event.List(context.TODO()) + if err != nil { + panic(err.Error()) + } + fmt.Printf("%+v\n", events) +} + +``` + +### Request fields + +All request parameters are wrapped in a generic `Field` type, +which we use to distinguish zero values from null or omitted fields. + +This prevents accidentally sending a zero value if you forget a required parameter, +and enables explicitly sending `null`, `false`, `''`, or `0` on optional parameters. +Any field not specified is not sent. + +To construct fields with values, use the helpers `String()`, `Int()`, `Float()`, or most commonly, the generic `F[T]()`. +To send a null, use `Null[T]()`, and to send a nonconforming value, use `Raw[T](any)`. For example: + +```go +params := FooParams{ + Name: opencode.F("hello"), + + // Explicitly send `"description": null` + Description: opencode.Null[string](), + + Point: opencode.F(opencode.Point{ + X: opencode.Int(0), + Y: opencode.Int(1), + + // In cases where the API specifies a given type, + // but you want to send something else, use `Raw`: + Z: opencode.Raw[int64](0.01), // sends a float + }), +} +``` + +### Response objects + +All fields in response structs are value types (not pointers or wrappers). + +If a given field is `null`, not present, or invalid, the corresponding field +will simply be its zero value. + +All response structs also include a special `JSON` field, containing more detailed +information about each property, which you can use like so: + +```go +if res.Name == "" { + // true if `"name"` is either not present or explicitly null + res.JSON.Name.IsNull() + + // true if the `"name"` key was not present in the response JSON at all + res.JSON.Name.IsMissing() + + // When the API returns data that cannot be coerced to the expected type: + if res.JSON.Name.IsInvalid() { + raw := res.JSON.Name.Raw() + + legacyName := struct{ + First string `json:"first"` + Last string `json:"last"` + }{} + json.Unmarshal([]byte(raw), &legacyName) + name = legacyName.First + " " + legacyName.Last + } +} +``` + +These `.JSON` structs also include an `Extras` map containing +any properties in the json response that were not specified +in the struct. This can be useful for API features not yet +present in the SDK. + +```go +body := res.JSON.ExtraFields["my_unexpected_field"].Raw() +``` + +### RequestOptions + +This library uses the functional options pattern. Functions defined in the +`option` package return a `RequestOption`, which is a closure that mutates a +`RequestConfig`. These options can be supplied to the client or at individual +requests. For example: + +```go +client := opencode.NewClient( + // Adds a header to every request made by the client + option.WithHeader("X-Some-Header", "custom_header_info"), +) + +client.Event.List(context.TODO(), ..., + // Override the header + option.WithHeader("X-Some-Header", "some_other_custom_header_info"), + // Add an undocumented field to the request body, using sjson syntax + option.WithJSONSet("some.json.path", map[string]string{"my": "object"}), +) +``` + +See the [full list of request options](https://pkg.go.dev/github.com/sst/opencode-sdk-go/option). + +### Pagination + +This library provides some conveniences for working with paginated list endpoints. + +You can use `.ListAutoPaging()` methods to iterate through items across all pages: + +Or you can use simple `.List()` methods to fetch a single page and receive a standard response object +with additional helper methods like `.GetNextPage()`, e.g.: + +### Errors + +When the API returns a non-success status code, we return an error with type +`*opencode.Error`. This contains the `StatusCode`, `*http.Request`, and +`*http.Response` values of the request, as well as the JSON of the error body +(much like other response objects in the SDK). + +To handle errors, we recommend that you use the `errors.As` pattern: + +```go +_, err := client.Event.List(context.TODO()) +if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request + println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response + } + panic(err.Error()) // GET "/event": 400 Bad Request { ... } +} +``` + +When other errors occur, they are returned unwrapped; for example, +if HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`. + +### Timeouts + +Requests do not time out by default; use context to configure a timeout for a request lifecycle. + +Note that if a request is [retried](#retries), the context timeout does not start over. +To set a per-retry timeout, use `option.WithRequestTimeout()`. + +```go +// This sets the timeout for the request, including all the retries. +ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) +defer cancel() +client.Event.List( + ctx, + // This sets the per-retry timeout + option.WithRequestTimeout(20*time.Second), +) +``` + +### File uploads + +Request parameters that correspond to file uploads in multipart requests are typed as +`param.Field[io.Reader]`. The contents of the `io.Reader` will by default be sent as a multipart form +part with the file name of "anonymous_file" and content-type of "application/octet-stream". + +The file name and content-type can be customized by implementing `Name() string` or `ContentType() +string` on the run-time type of `io.Reader`. Note that `os.File` implements `Name() string`, so a +file returned by `os.Open` will be sent with the file name on disk. + +We also provide a helper `opencode.FileParam(reader io.Reader, filename string, contentType string)` +which can be used to wrap any `io.Reader` with the appropriate file name and content type. + +### Retries + +Certain errors will be automatically retried 2 times by default, with a short exponential backoff. +We retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit, +and >=500 Internal errors. + +You can use the `WithMaxRetries` option to configure or disable this: + +```go +// Configure the default for all requests: +client := opencode.NewClient( + option.WithMaxRetries(0), // default is 2 +) + +// Override per-request: +client.Event.List(context.TODO(), option.WithMaxRetries(5)) +``` + +### Accessing raw response data (e.g. response headers) + +You can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when +you need to examine response headers, status codes, or other details. + +```go +// Create a variable to store the HTTP response +var response *http.Response +events, err := client.Event.List(context.TODO(), option.WithResponseInto(&response)) +if err != nil { + // handle error +} +fmt.Printf("%+v\n", events) + +fmt.Printf("Status Code: %d\n", response.StatusCode) +fmt.Printf("Headers: %+#v\n", response.Header) +``` + +### Making custom/undocumented requests + +This library is typed for convenient access to the documented API. If you need to access undocumented +endpoints, params, or response properties, the library can still be used. + +#### Undocumented endpoints + +To make requests to undocumented endpoints, you can use `client.Get`, `client.Post`, and other HTTP verbs. +`RequestOptions` on the client, such as retries, will be respected when making these requests. + +```go +var ( + // params can be an io.Reader, a []byte, an encoding/json serializable object, + // or a "…Params" struct defined in this library. + params map[string]interface{} + + // result can be an []byte, *http.Response, a encoding/json deserializable object, + // or a model defined in this library. + result *http.Response +) +err := client.Post(context.Background(), "/unspecified", params, &result) +if err != nil { + … +} +``` + +#### Undocumented request params + +To make requests using undocumented parameters, you may use either the `option.WithQuerySet()` +or the `option.WithJSONSet()` methods. + +```go +params := FooNewParams{ + ID: opencode.F("id_xxxx"), + Data: opencode.F(FooNewParamsData{ + FirstName: opencode.F("John"), + }), +} +client.Foo.New(context.Background(), params, option.WithJSONSet("data.last_name", "Doe")) +``` + +#### Undocumented response properties + +To access undocumented response properties, you may either access the raw JSON of the response as a string +with `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with +`result.JSON.Foo.Raw()`. + +Any fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`. + +### Middleware + +We provide `option.WithMiddleware` which applies the given +middleware to requests. + +```go +func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { + // Before the request + start := time.Now() + LogReq(req) + + // Forward the request to the next handler + res, err = next(req) + + // Handle stuff after the request + end := time.Now() + LogRes(res, err, start - end) + + return res, err +} + +client := opencode.NewClient( + option.WithMiddleware(Logger), +) +``` + +When multiple middlewares are provided as variadic arguments, the middlewares +are applied left to right. If `option.WithMiddleware` is given +multiple times, for example first in the client then the method, the +middleware in the client will run first and the middleware given in the method +will run next. + +You may also replace the default `http.Client` with +`option.WithHTTPClient(client)`. Only one http client is +accepted (this overwrites any previous client) and receives requests after any +middleware has been applied. + +## Semantic versioning + +This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: + +1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ +2. Changes that we do not expect to impact the vast majority of users in practice. + +We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. + +We are keen for your feedback; please open an [issue](https://www.github.com/sst/opencode-sdk-go/issues) with questions, bugs, or suggestions. + +## Contributing + +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/packages/tui/sdk/SECURITY.md b/packages/tui/sdk/SECURITY.md new file mode 100644 index 00000000..6912e12b --- /dev/null +++ b/packages/tui/sdk/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. + +To report a security issue, please contact the Stainless team at security@stainless.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Opencode, please follow the respective company's security reporting guidelines. + +### Opencode Terms and Policies + +Please contact support@sst.dev for any questions or concerns regarding the security of our services. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/packages/tui/sdk/aliases.go b/packages/tui/sdk/aliases.go new file mode 100644 index 00000000..84dd614a --- /dev/null +++ b/packages/tui/sdk/aliases.go @@ -0,0 +1,34 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "github.com/sst/opencode-sdk-go/internal/apierror" + "github.com/sst/opencode-sdk-go/shared" +) + +type Error = apierror.Error + +// This is an alias to an internal type. +type ProviderAuthError = shared.ProviderAuthError + +// This is an alias to an internal type. +type ProviderAuthErrorData = shared.ProviderAuthErrorData + +// This is an alias to an internal type. +type ProviderAuthErrorName = shared.ProviderAuthErrorName + +// This is an alias to an internal value. +const ProviderAuthErrorNameProviderAuthError = shared.ProviderAuthErrorNameProviderAuthError + +// This is an alias to an internal type. +type UnknownError = shared.UnknownError + +// This is an alias to an internal type. +type UnknownErrorData = shared.UnknownErrorData + +// This is an alias to an internal type. +type UnknownErrorName = shared.UnknownErrorName + +// This is an alias to an internal value. +const UnknownErrorNameUnknownError = shared.UnknownErrorNameUnknownError diff --git a/packages/tui/sdk/api.md b/packages/tui/sdk/api.md new file mode 100644 index 00000000..4ac9edcf --- /dev/null +++ b/packages/tui/sdk/api.md @@ -0,0 +1,110 @@ +# Shared Response Types + +- shared.ProviderAuthError +- shared.UnknownError + +# Event + +Response Types: + +- opencode.EventListResponse + +Methods: + +- client.Event.List(ctx context.Context) (opencode.EventListResponse, error) + +# App + +Response Types: + +- opencode.App + +Methods: + +- client.App.Get(ctx context.Context) (opencode.App, error) +- client.App.Init(ctx context.Context) (bool, error) + +# Find + +Response Types: + +- opencode.FindSymbolsResponse +- opencode.FindTextResponse + +Methods: + +- client.Find.Files(ctx context.Context, query opencode.FindFilesParams) ([]string, error) +- client.Find.Symbols(ctx context.Context, query opencode.FindSymbolsParams) ([]opencode.FindSymbolsResponse, error) +- client.Find.Text(ctx context.Context, query opencode.FindTextParams) ([]opencode.FindTextResponse, error) + +# File + +Response Types: + +- opencode.FileReadResponse +- opencode.FileStatusResponse + +Methods: + +- client.File.Read(ctx context.Context, query opencode.FileReadParams) (opencode.FileReadResponse, error) +- client.File.Status(ctx context.Context) ([]opencode.FileStatusResponse, error) + +# Config + +Response Types: + +- opencode.Config +- opencode.Keybinds +- opencode.McpLocal +- opencode.McpRemote +- opencode.Model +- opencode.Provider +- opencode.ConfigProvidersResponse + +Methods: + +- client.Config.Get(ctx context.Context) (opencode.Config, error) +- client.Config.Providers(ctx context.Context) (opencode.ConfigProvidersResponse, error) + +# Session + +Params Types: + +- opencode.FilePartParam +- opencode.MessagePartUnionParam +- opencode.ReasoningPartParam +- opencode.SourceURLPartParam +- opencode.StepStartPartParam +- opencode.TextPartParam +- opencode.ToolCallParam +- opencode.ToolInvocationPartParam +- opencode.ToolPartialCallParam +- opencode.ToolResultParam + +Response Types: + +- opencode.FilePart +- opencode.Message +- opencode.MessagePart +- opencode.ReasoningPart +- opencode.Session +- opencode.SourceURLPart +- opencode.StepStartPart +- opencode.TextPart +- opencode.ToolCall +- opencode.ToolInvocationPart +- opencode.ToolPartialCall +- opencode.ToolResult + +Methods: + +- client.Session.New(ctx context.Context) (opencode.Session, error) +- client.Session.List(ctx context.Context) ([]opencode.Session, error) +- client.Session.Delete(ctx context.Context, id string) (bool, error) +- client.Session.Abort(ctx context.Context, id string) (bool, error) +- client.Session.Chat(ctx context.Context, id string, body opencode.SessionChatParams) (opencode.Message, error) +- client.Session.Init(ctx context.Context, id string, body opencode.SessionInitParams) (bool, error) +- client.Session.Messages(ctx context.Context, id string) ([]opencode.Message, error) +- client.Session.Share(ctx context.Context, id string) (opencode.Session, error) +- client.Session.Summarize(ctx context.Context, id string, body opencode.SessionSummarizeParams) (bool, error) +- client.Session.Unshare(ctx context.Context, id string) (opencode.Session, error) diff --git a/packages/tui/sdk/app.go b/packages/tui/sdk/app.go new file mode 100644 index 00000000..dc44a74b --- /dev/null +++ b/packages/tui/sdk/app.go @@ -0,0 +1,123 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" +) + +// AppService contains methods and other services that help with interacting with +// the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewAppService] method instead. +type AppService struct { + Options []option.RequestOption +} + +// NewAppService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewAppService(opts ...option.RequestOption) (r *AppService) { + r = &AppService{} + r.Options = opts + return +} + +// Get app info +func (r *AppService) Get(ctx context.Context, opts ...option.RequestOption) (res *App, err error) { + opts = append(r.Options[:], opts...) + path := "app" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// Initialize the app +func (r *AppService) Init(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + path := "app/init" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + return +} + +type App struct { + Git bool `json:"git,required"` + Hostname string `json:"hostname,required"` + Path AppPath `json:"path,required"` + Time AppTime `json:"time,required"` + User string `json:"user,required"` + JSON appJSON `json:"-"` +} + +// appJSON contains the JSON metadata for the struct [App] +type appJSON struct { + Git apijson.Field + Hostname apijson.Field + Path apijson.Field + Time apijson.Field + User apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *App) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r appJSON) RawJSON() string { + return r.raw +} + +type AppPath struct { + Config string `json:"config,required"` + Cwd string `json:"cwd,required"` + Data string `json:"data,required"` + Root string `json:"root,required"` + State string `json:"state,required"` + JSON appPathJSON `json:"-"` +} + +// appPathJSON contains the JSON metadata for the struct [AppPath] +type appPathJSON struct { + Config apijson.Field + Cwd apijson.Field + Data apijson.Field + Root apijson.Field + State apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *AppPath) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r appPathJSON) RawJSON() string { + return r.raw +} + +type AppTime struct { + Initialized float64 `json:"initialized"` + JSON appTimeJSON `json:"-"` +} + +// appTimeJSON contains the JSON metadata for the struct [AppTime] +type appTimeJSON struct { + Initialized apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *AppTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r appTimeJSON) RawJSON() string { + return r.raw +} diff --git a/packages/tui/sdk/app_test.go b/packages/tui/sdk/app_test.go new file mode 100644 index 00000000..f96495f1 --- /dev/null +++ b/packages/tui/sdk/app_test.go @@ -0,0 +1,58 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestAppGet(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.App.Get(context.TODO()) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestAppInit(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.App.Init(context.TODO()) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/packages/tui/sdk/client.go b/packages/tui/sdk/client.go new file mode 100644 index 00000000..955eb7d6 --- /dev/null +++ b/packages/tui/sdk/client.go @@ -0,0 +1,123 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + "os" + + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" +) + +// Client creates a struct with services and top level methods that help with +// interacting with the opencode API. You should not instantiate this client +// directly, and instead use the [NewClient] method instead. +type Client struct { + Options []option.RequestOption + Event *EventService + App *AppService + Find *FindService + File *FileService + Config *ConfigService + Session *SessionService +} + +// DefaultClientOptions read from the environment (OPENCODE_BASE_URL). This should +// be used to initialize new clients. +func DefaultClientOptions() []option.RequestOption { + defaults := []option.RequestOption{option.WithEnvironmentProduction()} + if o, ok := os.LookupEnv("OPENCODE_BASE_URL"); ok { + defaults = append(defaults, option.WithBaseURL(o)) + } + return defaults +} + +// NewClient generates a new client with the default option read from the +// environment (OPENCODE_BASE_URL). The option passed in as arguments are applied +// after these default arguments, and all option will be passed down to the +// services and requests that this client makes. +func NewClient(opts ...option.RequestOption) (r *Client) { + opts = append(DefaultClientOptions(), opts...) + + r = &Client{Options: opts} + + r.Event = NewEventService(opts...) + r.App = NewAppService(opts...) + r.Find = NewFindService(opts...) + r.File = NewFileService(opts...) + r.Config = NewConfigService(opts...) + r.Session = NewSessionService(opts...) + + return +} + +// Execute makes a request with the given context, method, URL, request params, +// response, and request options. This is useful for hitting undocumented endpoints +// while retaining the base URL, auth, retries, and other options from the client. +// +// If a byte slice or an [io.Reader] is supplied to params, it will be used as-is +// for the request body. +// +// The params is by default serialized into the body using [encoding/json]. If your +// type implements a MarshalJSON function, it will be used instead to serialize the +// request. If a URLQuery method is implemented, the returned [url.Values] will be +// used as query strings to the url. +// +// If your params struct uses [param.Field], you must provide either [MarshalJSON], +// [URLQuery], and/or [MarshalForm] functions. It is undefined behavior to use a +// struct uses [param.Field] without specifying how it is serialized. +// +// Any "…Params" object defined in this library can be used as the request +// argument. Note that 'path' arguments will not be forwarded into the url. +// +// The response body will be deserialized into the res variable, depending on its +// type: +// +// - A pointer to a [*http.Response] is populated by the raw response. +// - A pointer to a byte array will be populated with the contents of the request +// body. +// - A pointer to any other type uses this library's default JSON decoding, which +// respects UnmarshalJSON if it is defined on the type. +// - A nil value will not read the response body. +// +// For even greater flexibility, see [option.WithResponseInto] and +// [option.WithResponseBodyInto]. +func (r *Client) Execute(ctx context.Context, method string, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { + opts = append(r.Options, opts...) + return requestconfig.ExecuteNewRequest(ctx, method, path, params, res, opts...) +} + +// Get makes a GET request with the given URL, params, and optionally deserializes +// to a response. See [Execute] documentation on the params and response. +func (r *Client) Get(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodGet, path, params, res, opts...) +} + +// Post makes a POST request with the given URL, params, and optionally +// deserializes to a response. See [Execute] documentation on the params and +// response. +func (r *Client) Post(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodPost, path, params, res, opts...) +} + +// Put makes a PUT request with the given URL, params, and optionally deserializes +// to a response. See [Execute] documentation on the params and response. +func (r *Client) Put(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodPut, path, params, res, opts...) +} + +// Patch makes a PATCH request with the given URL, params, and optionally +// deserializes to a response. See [Execute] documentation on the params and +// response. +func (r *Client) Patch(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodPatch, path, params, res, opts...) +} + +// Delete makes a DELETE request with the given URL, params, and optionally +// deserializes to a response. See [Execute] documentation on the params and +// response. +func (r *Client) Delete(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error { + return r.Execute(ctx, http.MethodDelete, path, params, res, opts...) +} diff --git a/packages/tui/sdk/client_test.go b/packages/tui/sdk/client_test.go new file mode 100644 index 00000000..e75d6492 --- /dev/null +++ b/packages/tui/sdk/client_test.go @@ -0,0 +1,332 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "fmt" + "io" + "net/http" + "reflect" + "testing" + "time" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal" + "github.com/sst/opencode-sdk-go/option" +) + +type closureTransport struct { + fn func(req *http.Request) (*http.Response, error) +} + +func (t *closureTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return t.fn(req) +} + +func TestUserAgentHeader(t *testing.T) { + var userAgent string + client := opencode.NewClient( + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + userAgent = req.Header.Get("User-Agent") + return &http.Response{ + StatusCode: http.StatusOK, + }, nil + }, + }, + }), + ) + client.Event.List(context.Background()) + if userAgent != fmt.Sprintf("Opencode/Go %s", internal.PackageVersion) { + t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent) + } +} + +func TestRetryAfter(t *testing.T) { + retryCountHeaders := make([]string, 0) + client := opencode.NewClient( + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count")) + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + http.CanonicalHeaderKey("Retry-After"): []string{"0.1"}, + }, + }, nil + }, + }, + }), + ) + _, err := client.Event.List(context.Background()) + if err == nil { + t.Error("Expected there to be a cancel error") + } + + attempts := len(retryCountHeaders) + if attempts != 3 { + t.Errorf("Expected %d attempts, got %d", 3, attempts) + } + + expectedRetryCountHeaders := []string{"0", "1", "2"} + if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) { + t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders) + } +} + +func TestDeleteRetryCountHeader(t *testing.T) { + retryCountHeaders := make([]string, 0) + client := opencode.NewClient( + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count")) + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + http.CanonicalHeaderKey("Retry-After"): []string{"0.1"}, + }, + }, nil + }, + }, + }), + option.WithHeaderDel("X-Stainless-Retry-Count"), + ) + _, err := client.Event.List(context.Background()) + if err == nil { + t.Error("Expected there to be a cancel error") + } + + expectedRetryCountHeaders := []string{"", "", ""} + if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) { + t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders) + } +} + +func TestOverwriteRetryCountHeader(t *testing.T) { + retryCountHeaders := make([]string, 0) + client := opencode.NewClient( + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count")) + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + http.CanonicalHeaderKey("Retry-After"): []string{"0.1"}, + }, + }, nil + }, + }, + }), + option.WithHeader("X-Stainless-Retry-Count", "42"), + ) + _, err := client.Event.List(context.Background()) + if err == nil { + t.Error("Expected there to be a cancel error") + } + + expectedRetryCountHeaders := []string{"42", "42", "42"} + if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) { + t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders) + } +} + +func TestRetryAfterMs(t *testing.T) { + attempts := 0 + client := opencode.NewClient( + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + attempts++ + return &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + http.CanonicalHeaderKey("Retry-After-Ms"): []string{"100"}, + }, + }, nil + }, + }, + }), + ) + _, err := client.Event.List(context.Background()) + if err == nil { + t.Error("Expected there to be a cancel error") + } + if want := 3; attempts != want { + t.Errorf("Expected %d attempts, got %d", want, attempts) + } +} + +func TestContextCancel(t *testing.T) { + client := opencode.NewClient( + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + }, + }, + }), + ) + cancelCtx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := client.Event.List(cancelCtx) + if err == nil { + t.Error("Expected there to be a cancel error") + } +} + +func TestContextCancelDelay(t *testing.T) { + client := opencode.NewClient( + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + }, + }, + }), + ) + cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond) + defer cancel() + _, err := client.Event.List(cancelCtx) + if err == nil { + t.Error("expected there to be a cancel error") + } +} + +func TestContextDeadline(t *testing.T) { + testTimeout := time.After(3 * time.Second) + testDone := make(chan struct{}) + + deadline := time.Now().Add(100 * time.Millisecond) + deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline) + defer cancel() + + go func() { + client := opencode.NewClient( + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + }, + }, + }), + ) + _, err := client.Event.List(deadlineCtx) + if err == nil { + t.Error("expected there to be a deadline error") + } + close(testDone) + }() + + select { + case <-testTimeout: + t.Fatal("client didn't finish in time") + case <-testDone: + if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff { + t.Fatalf("client did not return within 30ms of context deadline, got %s", diff) + } + } +} + +func TestContextDeadlineStreaming(t *testing.T) { + testTimeout := time.After(3 * time.Second) + testDone := make(chan struct{}) + + deadline := time.Now().Add(100 * time.Millisecond) + deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline) + defer cancel() + + go func() { + client := opencode.NewClient( + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Status: "200 OK", + Body: io.NopCloser( + io.Reader(readerFunc(func([]byte) (int, error) { + <-req.Context().Done() + return 0, req.Context().Err() + })), + ), + }, nil + }, + }, + }), + ) + stream := client.Event.ListStreaming(deadlineCtx) + for stream.Next() { + _ = stream.Current() + } + if stream.Err() == nil { + t.Error("expected there to be a deadline error") + } + close(testDone) + }() + + select { + case <-testTimeout: + t.Fatal("client didn't finish in time") + case <-testDone: + if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff { + t.Fatalf("client did not return within 30ms of context deadline, got %s", diff) + } + } +} + +func TestContextDeadlineStreamingWithRequestTimeout(t *testing.T) { + testTimeout := time.After(3 * time.Second) + testDone := make(chan struct{}) + deadline := time.Now().Add(100 * time.Millisecond) + + go func() { + client := opencode.NewClient( + option.WithHTTPClient(&http.Client{ + Transport: &closureTransport{ + fn: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Status: "200 OK", + Body: io.NopCloser( + io.Reader(readerFunc(func([]byte) (int, error) { + <-req.Context().Done() + return 0, req.Context().Err() + })), + ), + }, nil + }, + }, + }), + ) + stream := client.Event.ListStreaming(context.Background(), option.WithRequestTimeout((100 * time.Millisecond))) + for stream.Next() { + _ = stream.Current() + } + if stream.Err() == nil { + t.Error("expected there to be a deadline error") + } + close(testDone) + }() + + select { + case <-testTimeout: + t.Fatal("client didn't finish in time") + case <-testDone: + if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff { + t.Fatalf("client did not return within 30ms of context deadline, got %s", diff) + } + } +} + +type readerFunc func([]byte) (int, error) + +func (f readerFunc) Read(p []byte) (int, error) { return f(p) } +func (f readerFunc) Close() error { return nil } diff --git a/packages/tui/sdk/config.go b/packages/tui/sdk/config.go new file mode 100644 index 00000000..39da2f94 --- /dev/null +++ b/packages/tui/sdk/config.go @@ -0,0 +1,724 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + "reflect" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" + "github.com/tidwall/gjson" +) + +// ConfigService contains methods and other services that help with interacting +// with the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewConfigService] method instead. +type ConfigService struct { + Options []option.RequestOption +} + +// NewConfigService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewConfigService(opts ...option.RequestOption) (r *ConfigService) { + r = &ConfigService{} + r.Options = opts + return +} + +// Get config info +func (r *ConfigService) Get(ctx context.Context, opts ...option.RequestOption) (res *Config, err error) { + opts = append(r.Options[:], opts...) + path := "config" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// List all providers +func (r *ConfigService) Providers(ctx context.Context, opts ...option.RequestOption) (res *ConfigProvidersResponse, err error) { + opts = append(r.Options[:], opts...) + path := "config/providers" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +type Config struct { + // JSON schema reference for configuration validation + Schema string `json:"$schema"` + // Share newly created sessions automatically + Autoshare bool `json:"autoshare"` + // Automatically update to the latest version + Autoupdate bool `json:"autoupdate"` + // Disable providers that are loaded automatically + DisabledProviders []string `json:"disabled_providers"` + Experimental ConfigExperimental `json:"experimental"` + // Additional instruction files or patterns to include + Instructions []string `json:"instructions"` + // Custom keybind configurations + Keybinds Keybinds `json:"keybinds"` + // MCP (Model Context Protocol) server configurations + Mcp map[string]ConfigMcp `json:"mcp"` + // Model to use in the format of provider/model, eg anthropic/claude-2 + Model string `json:"model"` + // Custom provider configurations and model overrides + Provider map[string]ConfigProvider `json:"provider"` + // Theme name to use for the interface + Theme string `json:"theme"` + JSON configJSON `json:"-"` +} + +// configJSON contains the JSON metadata for the struct [Config] +type configJSON struct { + Schema apijson.Field + Autoshare apijson.Field + Autoupdate apijson.Field + DisabledProviders apijson.Field + Experimental apijson.Field + Instructions apijson.Field + Keybinds apijson.Field + Mcp apijson.Field + Model apijson.Field + Provider apijson.Field + Theme apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Config) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configJSON) RawJSON() string { + return r.raw +} + +type ConfigExperimental struct { + Hook ConfigExperimentalHook `json:"hook"` + JSON configExperimentalJSON `json:"-"` +} + +// configExperimentalJSON contains the JSON metadata for the struct +// [ConfigExperimental] +type configExperimentalJSON struct { + Hook apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigExperimental) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configExperimentalJSON) RawJSON() string { + return r.raw +} + +type ConfigExperimentalHook struct { + FileEdited map[string][]ConfigExperimentalHookFileEdited `json:"file_edited"` + SessionCompleted []ConfigExperimentalHookSessionCompleted `json:"session_completed"` + JSON configExperimentalHookJSON `json:"-"` +} + +// configExperimentalHookJSON contains the JSON metadata for the struct +// [ConfigExperimentalHook] +type configExperimentalHookJSON struct { + FileEdited apijson.Field + SessionCompleted apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigExperimentalHook) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configExperimentalHookJSON) RawJSON() string { + return r.raw +} + +type ConfigExperimentalHookFileEdited struct { + Command []string `json:"command,required"` + Environment map[string]string `json:"environment"` + JSON configExperimentalHookFileEditedJSON `json:"-"` +} + +// configExperimentalHookFileEditedJSON contains the JSON metadata for the struct +// [ConfigExperimentalHookFileEdited] +type configExperimentalHookFileEditedJSON struct { + Command apijson.Field + Environment apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigExperimentalHookFileEdited) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configExperimentalHookFileEditedJSON) RawJSON() string { + return r.raw +} + +type ConfigExperimentalHookSessionCompleted struct { + Command []string `json:"command,required"` + Environment map[string]string `json:"environment"` + JSON configExperimentalHookSessionCompletedJSON `json:"-"` +} + +// configExperimentalHookSessionCompletedJSON contains the JSON metadata for the +// struct [ConfigExperimentalHookSessionCompleted] +type configExperimentalHookSessionCompletedJSON struct { + Command apijson.Field + Environment apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigExperimentalHookSessionCompleted) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configExperimentalHookSessionCompletedJSON) RawJSON() string { + return r.raw +} + +type ConfigMcp struct { + // Type of MCP server connection + Type ConfigMcpType `json:"type,required"` + // This field can have the runtime type of [[]string]. + Command interface{} `json:"command"` + // Enable or disable the MCP server on startup + Enabled bool `json:"enabled"` + // This field can have the runtime type of [map[string]string]. + Environment interface{} `json:"environment"` + // URL of the remote MCP server + URL string `json:"url"` + JSON configMcpJSON `json:"-"` + union ConfigMcpUnion +} + +// configMcpJSON contains the JSON metadata for the struct [ConfigMcp] +type configMcpJSON struct { + Type apijson.Field + Command apijson.Field + Enabled apijson.Field + Environment apijson.Field + URL apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r configMcpJSON) RawJSON() string { + return r.raw +} + +func (r *ConfigMcp) UnmarshalJSON(data []byte) (err error) { + *r = ConfigMcp{} + err = apijson.UnmarshalRoot(data, &r.union) + if err != nil { + return err + } + return apijson.Port(r.union, &r) +} + +// AsUnion returns a [ConfigMcpUnion] interface which you can cast to the specific +// types for more type safety. +// +// Possible runtime types of the union are [McpLocal], [McpRemote]. +func (r ConfigMcp) AsUnion() ConfigMcpUnion { + return r.union +} + +// Union satisfied by [McpLocal] or [McpRemote]. +type ConfigMcpUnion interface { + implementsConfigMcp() +} + +func init() { + apijson.RegisterUnion( + reflect.TypeOf((*ConfigMcpUnion)(nil)).Elem(), + "type", + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(McpLocal{}), + DiscriminatorValue: "local", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(McpRemote{}), + DiscriminatorValue: "remote", + }, + ) +} + +// Type of MCP server connection +type ConfigMcpType string + +const ( + ConfigMcpTypeLocal ConfigMcpType = "local" + ConfigMcpTypeRemote ConfigMcpType = "remote" +) + +func (r ConfigMcpType) IsKnown() bool { + switch r { + case ConfigMcpTypeLocal, ConfigMcpTypeRemote: + return true + } + return false +} + +type ConfigProvider struct { + Models map[string]ConfigProviderModel `json:"models,required"` + ID string `json:"id"` + API string `json:"api"` + Env []string `json:"env"` + Name string `json:"name"` + Npm string `json:"npm"` + Options map[string]interface{} `json:"options"` + JSON configProviderJSON `json:"-"` +} + +// configProviderJSON contains the JSON metadata for the struct [ConfigProvider] +type configProviderJSON struct { + Models apijson.Field + ID apijson.Field + API apijson.Field + Env apijson.Field + Name apijson.Field + Npm apijson.Field + Options apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigProvider) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configProviderJSON) RawJSON() string { + return r.raw +} + +type ConfigProviderModel struct { + ID string `json:"id"` + Attachment bool `json:"attachment"` + Cost ConfigProviderModelsCost `json:"cost"` + Limit ConfigProviderModelsLimit `json:"limit"` + Name string `json:"name"` + Options map[string]interface{} `json:"options"` + Reasoning bool `json:"reasoning"` + ReleaseDate string `json:"release_date"` + Temperature bool `json:"temperature"` + ToolCall bool `json:"tool_call"` + JSON configProviderModelJSON `json:"-"` +} + +// configProviderModelJSON contains the JSON metadata for the struct +// [ConfigProviderModel] +type configProviderModelJSON struct { + ID apijson.Field + Attachment apijson.Field + Cost apijson.Field + Limit apijson.Field + Name apijson.Field + Options apijson.Field + Reasoning apijson.Field + ReleaseDate apijson.Field + Temperature apijson.Field + ToolCall apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigProviderModel) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configProviderModelJSON) RawJSON() string { + return r.raw +} + +type ConfigProviderModelsCost struct { + Input float64 `json:"input,required"` + Output float64 `json:"output,required"` + CacheRead float64 `json:"cache_read"` + CacheWrite float64 `json:"cache_write"` + JSON configProviderModelsCostJSON `json:"-"` +} + +// configProviderModelsCostJSON contains the JSON metadata for the struct +// [ConfigProviderModelsCost] +type configProviderModelsCostJSON struct { + Input apijson.Field + Output apijson.Field + CacheRead apijson.Field + CacheWrite apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigProviderModelsCost) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configProviderModelsCostJSON) RawJSON() string { + return r.raw +} + +type ConfigProviderModelsLimit struct { + Context float64 `json:"context,required"` + Output float64 `json:"output,required"` + JSON configProviderModelsLimitJSON `json:"-"` +} + +// configProviderModelsLimitJSON contains the JSON metadata for the struct +// [ConfigProviderModelsLimit] +type configProviderModelsLimitJSON struct { + Context apijson.Field + Output apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigProviderModelsLimit) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configProviderModelsLimitJSON) RawJSON() string { + return r.raw +} + +type Keybinds struct { + // Exit the application + AppExit string `json:"app_exit"` + // Open external editor + EditorOpen string `json:"editor_open"` + // Show help dialog + Help string `json:"help"` + // Navigate to next history item + HistoryNext string `json:"history_next"` + // Navigate to previous history item + HistoryPrevious string `json:"history_previous"` + // Clear input field + InputClear string `json:"input_clear"` + // Insert newline in input + InputNewline string `json:"input_newline"` + // Paste from clipboard + InputPaste string `json:"input_paste"` + // Submit input + InputSubmit string `json:"input_submit"` + // Leader key for keybind combinations + Leader string `json:"leader"` + // Navigate to first message + MessagesFirst string `json:"messages_first"` + // Scroll messages down by half page + MessagesHalfPageDown string `json:"messages_half_page_down"` + // Scroll messages up by half page + MessagesHalfPageUp string `json:"messages_half_page_up"` + // Navigate to last message + MessagesLast string `json:"messages_last"` + // Navigate to next message + MessagesNext string `json:"messages_next"` + // Scroll messages down by one page + MessagesPageDown string `json:"messages_page_down"` + // Scroll messages up by one page + MessagesPageUp string `json:"messages_page_up"` + // Navigate to previous message + MessagesPrevious string `json:"messages_previous"` + // List available models + ModelList string `json:"model_list"` + // Initialize project configuration + ProjectInit string `json:"project_init"` + // Toggle compact mode for session + SessionCompact string `json:"session_compact"` + // Interrupt current session + SessionInterrupt string `json:"session_interrupt"` + // List all sessions + SessionList string `json:"session_list"` + // Create a new session + SessionNew string `json:"session_new"` + // Share current session + SessionShare string `json:"session_share"` + // List available themes + ThemeList string `json:"theme_list"` + // Show tool details + ToolDetails string `json:"tool_details"` + JSON keybindsJSON `json:"-"` +} + +// keybindsJSON contains the JSON metadata for the struct [Keybinds] +type keybindsJSON struct { + AppExit apijson.Field + EditorOpen apijson.Field + Help apijson.Field + HistoryNext apijson.Field + HistoryPrevious apijson.Field + InputClear apijson.Field + InputNewline apijson.Field + InputPaste apijson.Field + InputSubmit apijson.Field + Leader apijson.Field + MessagesFirst apijson.Field + MessagesHalfPageDown apijson.Field + MessagesHalfPageUp apijson.Field + MessagesLast apijson.Field + MessagesNext apijson.Field + MessagesPageDown apijson.Field + MessagesPageUp apijson.Field + MessagesPrevious apijson.Field + ModelList apijson.Field + ProjectInit apijson.Field + SessionCompact apijson.Field + SessionInterrupt apijson.Field + SessionList apijson.Field + SessionNew apijson.Field + SessionShare apijson.Field + ThemeList apijson.Field + ToolDetails apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Keybinds) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r keybindsJSON) RawJSON() string { + return r.raw +} + +type McpLocal struct { + // Command and arguments to run the MCP server + Command []string `json:"command,required"` + // Type of MCP server connection + Type McpLocalType `json:"type,required"` + // Enable or disable the MCP server on startup + Enabled bool `json:"enabled"` + // Environment variables to set when running the MCP server + Environment map[string]string `json:"environment"` + JSON mcpLocalJSON `json:"-"` +} + +// mcpLocalJSON contains the JSON metadata for the struct [McpLocal] +type mcpLocalJSON struct { + Command apijson.Field + Type apijson.Field + Enabled apijson.Field + Environment apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *McpLocal) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r mcpLocalJSON) RawJSON() string { + return r.raw +} + +func (r McpLocal) implementsConfigMcp() {} + +// Type of MCP server connection +type McpLocalType string + +const ( + McpLocalTypeLocal McpLocalType = "local" +) + +func (r McpLocalType) IsKnown() bool { + switch r { + case McpLocalTypeLocal: + return true + } + return false +} + +type McpRemote struct { + // Type of MCP server connection + Type McpRemoteType `json:"type,required"` + // URL of the remote MCP server + URL string `json:"url,required"` + // Enable or disable the MCP server on startup + Enabled bool `json:"enabled"` + JSON mcpRemoteJSON `json:"-"` +} + +// mcpRemoteJSON contains the JSON metadata for the struct [McpRemote] +type mcpRemoteJSON struct { + Type apijson.Field + URL apijson.Field + Enabled apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *McpRemote) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r mcpRemoteJSON) RawJSON() string { + return r.raw +} + +func (r McpRemote) implementsConfigMcp() {} + +// Type of MCP server connection +type McpRemoteType string + +const ( + McpRemoteTypeRemote McpRemoteType = "remote" +) + +func (r McpRemoteType) IsKnown() bool { + switch r { + case McpRemoteTypeRemote: + return true + } + return false +} + +type Model struct { + ID string `json:"id,required"` + Attachment bool `json:"attachment,required"` + Cost ModelCost `json:"cost,required"` + Limit ModelLimit `json:"limit,required"` + Name string `json:"name,required"` + Options map[string]interface{} `json:"options,required"` + Reasoning bool `json:"reasoning,required"` + ReleaseDate string `json:"release_date,required"` + Temperature bool `json:"temperature,required"` + ToolCall bool `json:"tool_call,required"` + JSON modelJSON `json:"-"` +} + +// modelJSON contains the JSON metadata for the struct [Model] +type modelJSON struct { + ID apijson.Field + Attachment apijson.Field + Cost apijson.Field + Limit apijson.Field + Name apijson.Field + Options apijson.Field + Reasoning apijson.Field + ReleaseDate apijson.Field + Temperature apijson.Field + ToolCall apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Model) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r modelJSON) RawJSON() string { + return r.raw +} + +type ModelCost struct { + Input float64 `json:"input,required"` + Output float64 `json:"output,required"` + CacheRead float64 `json:"cache_read"` + CacheWrite float64 `json:"cache_write"` + JSON modelCostJSON `json:"-"` +} + +// modelCostJSON contains the JSON metadata for the struct [ModelCost] +type modelCostJSON struct { + Input apijson.Field + Output apijson.Field + CacheRead apijson.Field + CacheWrite apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ModelCost) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r modelCostJSON) RawJSON() string { + return r.raw +} + +type ModelLimit struct { + Context float64 `json:"context,required"` + Output float64 `json:"output,required"` + JSON modelLimitJSON `json:"-"` +} + +// modelLimitJSON contains the JSON metadata for the struct [ModelLimit] +type modelLimitJSON struct { + Context apijson.Field + Output apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ModelLimit) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r modelLimitJSON) RawJSON() string { + return r.raw +} + +type Provider struct { + ID string `json:"id,required"` + Env []string `json:"env,required"` + Models map[string]Model `json:"models,required"` + Name string `json:"name,required"` + API string `json:"api"` + Npm string `json:"npm"` + JSON providerJSON `json:"-"` +} + +// providerJSON contains the JSON metadata for the struct [Provider] +type providerJSON struct { + ID apijson.Field + Env apijson.Field + Models apijson.Field + Name apijson.Field + API apijson.Field + Npm apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Provider) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r providerJSON) RawJSON() string { + return r.raw +} + +type ConfigProvidersResponse struct { + Default map[string]string `json:"default,required"` + Providers []Provider `json:"providers,required"` + JSON configProvidersResponseJSON `json:"-"` +} + +// configProvidersResponseJSON contains the JSON metadata for the struct +// [ConfigProvidersResponse] +type configProvidersResponseJSON struct { + Default apijson.Field + Providers apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigProvidersResponse) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configProvidersResponseJSON) RawJSON() string { + return r.raw +} diff --git a/packages/tui/sdk/config_test.go b/packages/tui/sdk/config_test.go new file mode 100644 index 00000000..57a1d158 --- /dev/null +++ b/packages/tui/sdk/config_test.go @@ -0,0 +1,58 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestConfigGet(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Config.Get(context.TODO()) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestConfigProviders(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Config.Providers(context.TODO()) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/packages/tui/sdk/event.go b/packages/tui/sdk/event.go new file mode 100644 index 00000000..ed92b2ae --- /dev/null +++ b/packages/tui/sdk/event.go @@ -0,0 +1,1180 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + "reflect" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" + "github.com/sst/opencode-sdk-go/packages/ssestream" + "github.com/sst/opencode-sdk-go/shared" + "github.com/tidwall/gjson" +) + +// EventService contains methods and other services that help with interacting with +// the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewEventService] method instead. +type EventService struct { + Options []option.RequestOption +} + +// NewEventService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewEventService(opts ...option.RequestOption) (r *EventService) { + r = &EventService{} + r.Options = opts + return +} + +// Get events +func (r *EventService) ListStreaming(ctx context.Context, opts ...option.RequestOption) (stream *ssestream.Stream[EventListResponse]) { + var ( + raw *http.Response + err error + ) + opts = append(r.Options[:], opts...) + path := "event" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &raw, opts...) + return ssestream.NewStream[EventListResponse](ssestream.NewDecoder(raw), err) +} + +type EventListResponse struct { + // This field can have the runtime type of + // [EventListResponseEventLspClientDiagnosticsProperties], + // [EventListResponseEventPermissionUpdatedProperties], + // [EventListResponseEventFileEditedProperties], + // [EventListResponseEventStorageWriteProperties], + // [EventListResponseEventInstallationUpdatedProperties], + // [EventListResponseEventMessageUpdatedProperties], + // [EventListResponseEventMessageRemovedProperties], + // [EventListResponseEventMessagePartUpdatedProperties], + // [EventListResponseEventSessionUpdatedProperties], + // [EventListResponseEventSessionDeletedProperties], + // [EventListResponseEventSessionIdleProperties], + // [EventListResponseEventSessionErrorProperties], + // [EventListResponseEventFileWatcherUpdatedProperties]. + Properties interface{} `json:"properties,required"` + Type EventListResponseType `json:"type,required"` + JSON eventListResponseJSON `json:"-"` + union EventListResponseUnion +} + +// eventListResponseJSON contains the JSON metadata for the struct +// [EventListResponse] +type eventListResponseJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r eventListResponseJSON) RawJSON() string { + return r.raw +} + +func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) { + *r = EventListResponse{} + err = apijson.UnmarshalRoot(data, &r.union) + if err != nil { + return err + } + return apijson.Port(r.union, &r) +} + +// AsUnion returns a [EventListResponseUnion] interface which you can cast to the +// specific types for more type safety. +// +// Possible runtime types of the union are +// [EventListResponseEventLspClientDiagnostics], +// [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited], +// [EventListResponseEventStorageWrite], +// [EventListResponseEventInstallationUpdated], +// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved], +// [EventListResponseEventMessagePartUpdated], +// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], +// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], +// [EventListResponseEventFileWatcherUpdated]. +func (r EventListResponse) AsUnion() EventListResponseUnion { + return r.union +} + +// Union satisfied by [EventListResponseEventLspClientDiagnostics], +// [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited], +// [EventListResponseEventStorageWrite], +// [EventListResponseEventInstallationUpdated], +// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved], +// [EventListResponseEventMessagePartUpdated], +// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], +// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or +// [EventListResponseEventFileWatcherUpdated]. +type EventListResponseUnion interface { + implementsEventListResponse() +} + +func init() { + apijson.RegisterUnion( + reflect.TypeOf((*EventListResponseUnion)(nil)).Elem(), + "type", + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventLspClientDiagnostics{}), + DiscriminatorValue: "lsp.client.diagnostics", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventPermissionUpdated{}), + DiscriminatorValue: "permission.updated", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventFileEdited{}), + DiscriminatorValue: "file.edited", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventStorageWrite{}), + DiscriminatorValue: "storage.write", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventInstallationUpdated{}), + DiscriminatorValue: "installation.updated", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventMessageUpdated{}), + DiscriminatorValue: "message.updated", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventMessageRemoved{}), + DiscriminatorValue: "message.removed", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventMessagePartUpdated{}), + DiscriminatorValue: "message.part.updated", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventSessionUpdated{}), + DiscriminatorValue: "session.updated", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventSessionDeleted{}), + DiscriminatorValue: "session.deleted", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventSessionIdle{}), + DiscriminatorValue: "session.idle", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventSessionError{}), + DiscriminatorValue: "session.error", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventFileWatcherUpdated{}), + DiscriminatorValue: "file.watcher.updated", + }, + ) +} + +type EventListResponseEventLspClientDiagnostics struct { + Properties EventListResponseEventLspClientDiagnosticsProperties `json:"properties,required"` + Type EventListResponseEventLspClientDiagnosticsType `json:"type,required"` + JSON eventListResponseEventLspClientDiagnosticsJSON `json:"-"` +} + +// eventListResponseEventLspClientDiagnosticsJSON contains the JSON metadata for +// the struct [EventListResponseEventLspClientDiagnostics] +type eventListResponseEventLspClientDiagnosticsJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventLspClientDiagnostics) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventLspClientDiagnosticsJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventLspClientDiagnostics) implementsEventListResponse() {} + +type EventListResponseEventLspClientDiagnosticsProperties struct { + Path string `json:"path,required"` + ServerID string `json:"serverID,required"` + JSON eventListResponseEventLspClientDiagnosticsPropertiesJSON `json:"-"` +} + +// eventListResponseEventLspClientDiagnosticsPropertiesJSON contains the JSON +// metadata for the struct [EventListResponseEventLspClientDiagnosticsProperties] +type eventListResponseEventLspClientDiagnosticsPropertiesJSON struct { + Path apijson.Field + ServerID apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventLspClientDiagnosticsProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventLspClientDiagnosticsPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventLspClientDiagnosticsType string + +const ( + EventListResponseEventLspClientDiagnosticsTypeLspClientDiagnostics EventListResponseEventLspClientDiagnosticsType = "lsp.client.diagnostics" +) + +func (r EventListResponseEventLspClientDiagnosticsType) IsKnown() bool { + switch r { + case EventListResponseEventLspClientDiagnosticsTypeLspClientDiagnostics: + return true + } + return false +} + +type EventListResponseEventPermissionUpdated struct { + Properties EventListResponseEventPermissionUpdatedProperties `json:"properties,required"` + Type EventListResponseEventPermissionUpdatedType `json:"type,required"` + JSON eventListResponseEventPermissionUpdatedJSON `json:"-"` +} + +// eventListResponseEventPermissionUpdatedJSON contains the JSON metadata for the +// struct [EventListResponseEventPermissionUpdated] +type eventListResponseEventPermissionUpdatedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventPermissionUpdated) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventPermissionUpdatedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventPermissionUpdated) implementsEventListResponse() {} + +type EventListResponseEventPermissionUpdatedProperties struct { + ID string `json:"id,required"` + Metadata map[string]interface{} `json:"metadata,required"` + SessionID string `json:"sessionID,required"` + Time EventListResponseEventPermissionUpdatedPropertiesTime `json:"time,required"` + Title string `json:"title,required"` + JSON eventListResponseEventPermissionUpdatedPropertiesJSON `json:"-"` +} + +// eventListResponseEventPermissionUpdatedPropertiesJSON contains the JSON metadata +// for the struct [EventListResponseEventPermissionUpdatedProperties] +type eventListResponseEventPermissionUpdatedPropertiesJSON struct { + ID apijson.Field + Metadata apijson.Field + SessionID apijson.Field + Time apijson.Field + Title apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventPermissionUpdatedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventPermissionUpdatedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventPermissionUpdatedPropertiesTime struct { + Created float64 `json:"created,required"` + JSON eventListResponseEventPermissionUpdatedPropertiesTimeJSON `json:"-"` +} + +// eventListResponseEventPermissionUpdatedPropertiesTimeJSON contains the JSON +// metadata for the struct [EventListResponseEventPermissionUpdatedPropertiesTime] +type eventListResponseEventPermissionUpdatedPropertiesTimeJSON struct { + Created apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventPermissionUpdatedPropertiesTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventPermissionUpdatedPropertiesTimeJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventPermissionUpdatedType string + +const ( + EventListResponseEventPermissionUpdatedTypePermissionUpdated EventListResponseEventPermissionUpdatedType = "permission.updated" +) + +func (r EventListResponseEventPermissionUpdatedType) IsKnown() bool { + switch r { + case EventListResponseEventPermissionUpdatedTypePermissionUpdated: + return true + } + return false +} + +type EventListResponseEventFileEdited struct { + Properties EventListResponseEventFileEditedProperties `json:"properties,required"` + Type EventListResponseEventFileEditedType `json:"type,required"` + JSON eventListResponseEventFileEditedJSON `json:"-"` +} + +// eventListResponseEventFileEditedJSON contains the JSON metadata for the struct +// [EventListResponseEventFileEdited] +type eventListResponseEventFileEditedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventFileEdited) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventFileEditedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventFileEdited) implementsEventListResponse() {} + +type EventListResponseEventFileEditedProperties struct { + File string `json:"file,required"` + JSON eventListResponseEventFileEditedPropertiesJSON `json:"-"` +} + +// eventListResponseEventFileEditedPropertiesJSON contains the JSON metadata for +// the struct [EventListResponseEventFileEditedProperties] +type eventListResponseEventFileEditedPropertiesJSON struct { + File apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventFileEditedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventFileEditedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventFileEditedType string + +const ( + EventListResponseEventFileEditedTypeFileEdited EventListResponseEventFileEditedType = "file.edited" +) + +func (r EventListResponseEventFileEditedType) IsKnown() bool { + switch r { + case EventListResponseEventFileEditedTypeFileEdited: + return true + } + return false +} + +type EventListResponseEventStorageWrite struct { + Properties EventListResponseEventStorageWriteProperties `json:"properties,required"` + Type EventListResponseEventStorageWriteType `json:"type,required"` + JSON eventListResponseEventStorageWriteJSON `json:"-"` +} + +// eventListResponseEventStorageWriteJSON contains the JSON metadata for the struct +// [EventListResponseEventStorageWrite] +type eventListResponseEventStorageWriteJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventStorageWrite) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventStorageWriteJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventStorageWrite) implementsEventListResponse() {} + +type EventListResponseEventStorageWriteProperties struct { + Key string `json:"key,required"` + Content interface{} `json:"content"` + JSON eventListResponseEventStorageWritePropertiesJSON `json:"-"` +} + +// eventListResponseEventStorageWritePropertiesJSON contains the JSON metadata for +// the struct [EventListResponseEventStorageWriteProperties] +type eventListResponseEventStorageWritePropertiesJSON struct { + Key apijson.Field + Content apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventStorageWriteProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventStorageWritePropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventStorageWriteType string + +const ( + EventListResponseEventStorageWriteTypeStorageWrite EventListResponseEventStorageWriteType = "storage.write" +) + +func (r EventListResponseEventStorageWriteType) IsKnown() bool { + switch r { + case EventListResponseEventStorageWriteTypeStorageWrite: + return true + } + return false +} + +type EventListResponseEventInstallationUpdated struct { + Properties EventListResponseEventInstallationUpdatedProperties `json:"properties,required"` + Type EventListResponseEventInstallationUpdatedType `json:"type,required"` + JSON eventListResponseEventInstallationUpdatedJSON `json:"-"` +} + +// eventListResponseEventInstallationUpdatedJSON contains the JSON metadata for the +// struct [EventListResponseEventInstallationUpdated] +type eventListResponseEventInstallationUpdatedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventInstallationUpdated) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventInstallationUpdatedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventInstallationUpdated) implementsEventListResponse() {} + +type EventListResponseEventInstallationUpdatedProperties struct { + Version string `json:"version,required"` + JSON eventListResponseEventInstallationUpdatedPropertiesJSON `json:"-"` +} + +// eventListResponseEventInstallationUpdatedPropertiesJSON contains the JSON +// metadata for the struct [EventListResponseEventInstallationUpdatedProperties] +type eventListResponseEventInstallationUpdatedPropertiesJSON struct { + Version apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventInstallationUpdatedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventInstallationUpdatedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventInstallationUpdatedType string + +const ( + EventListResponseEventInstallationUpdatedTypeInstallationUpdated EventListResponseEventInstallationUpdatedType = "installation.updated" +) + +func (r EventListResponseEventInstallationUpdatedType) IsKnown() bool { + switch r { + case EventListResponseEventInstallationUpdatedTypeInstallationUpdated: + return true + } + return false +} + +type EventListResponseEventMessageUpdated struct { + Properties EventListResponseEventMessageUpdatedProperties `json:"properties,required"` + Type EventListResponseEventMessageUpdatedType `json:"type,required"` + JSON eventListResponseEventMessageUpdatedJSON `json:"-"` +} + +// eventListResponseEventMessageUpdatedJSON contains the JSON metadata for the +// struct [EventListResponseEventMessageUpdated] +type eventListResponseEventMessageUpdatedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventMessageUpdated) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventMessageUpdatedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventMessageUpdated) implementsEventListResponse() {} + +type EventListResponseEventMessageUpdatedProperties struct { + Info Message `json:"info,required"` + JSON eventListResponseEventMessageUpdatedPropertiesJSON `json:"-"` +} + +// eventListResponseEventMessageUpdatedPropertiesJSON contains the JSON metadata +// for the struct [EventListResponseEventMessageUpdatedProperties] +type eventListResponseEventMessageUpdatedPropertiesJSON struct { + Info apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventMessageUpdatedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventMessageUpdatedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventMessageUpdatedType string + +const ( + EventListResponseEventMessageUpdatedTypeMessageUpdated EventListResponseEventMessageUpdatedType = "message.updated" +) + +func (r EventListResponseEventMessageUpdatedType) IsKnown() bool { + switch r { + case EventListResponseEventMessageUpdatedTypeMessageUpdated: + return true + } + return false +} + +type EventListResponseEventMessageRemoved struct { + Properties EventListResponseEventMessageRemovedProperties `json:"properties,required"` + Type EventListResponseEventMessageRemovedType `json:"type,required"` + JSON eventListResponseEventMessageRemovedJSON `json:"-"` +} + +// eventListResponseEventMessageRemovedJSON contains the JSON metadata for the +// struct [EventListResponseEventMessageRemoved] +type eventListResponseEventMessageRemovedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventMessageRemoved) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventMessageRemovedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventMessageRemoved) implementsEventListResponse() {} + +type EventListResponseEventMessageRemovedProperties struct { + MessageID string `json:"messageID,required"` + SessionID string `json:"sessionID,required"` + JSON eventListResponseEventMessageRemovedPropertiesJSON `json:"-"` +} + +// eventListResponseEventMessageRemovedPropertiesJSON contains the JSON metadata +// for the struct [EventListResponseEventMessageRemovedProperties] +type eventListResponseEventMessageRemovedPropertiesJSON struct { + MessageID apijson.Field + SessionID apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventMessageRemovedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventMessageRemovedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventMessageRemovedType string + +const ( + EventListResponseEventMessageRemovedTypeMessageRemoved EventListResponseEventMessageRemovedType = "message.removed" +) + +func (r EventListResponseEventMessageRemovedType) IsKnown() bool { + switch r { + case EventListResponseEventMessageRemovedTypeMessageRemoved: + return true + } + return false +} + +type EventListResponseEventMessagePartUpdated struct { + Properties EventListResponseEventMessagePartUpdatedProperties `json:"properties,required"` + Type EventListResponseEventMessagePartUpdatedType `json:"type,required"` + JSON eventListResponseEventMessagePartUpdatedJSON `json:"-"` +} + +// eventListResponseEventMessagePartUpdatedJSON contains the JSON metadata for the +// struct [EventListResponseEventMessagePartUpdated] +type eventListResponseEventMessagePartUpdatedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventMessagePartUpdated) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventMessagePartUpdatedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventMessagePartUpdated) implementsEventListResponse() {} + +type EventListResponseEventMessagePartUpdatedProperties struct { + MessageID string `json:"messageID,required"` + Part MessagePart `json:"part,required"` + SessionID string `json:"sessionID,required"` + JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"` +} + +// eventListResponseEventMessagePartUpdatedPropertiesJSON contains the JSON +// metadata for the struct [EventListResponseEventMessagePartUpdatedProperties] +type eventListResponseEventMessagePartUpdatedPropertiesJSON struct { + MessageID apijson.Field + Part apijson.Field + SessionID apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventMessagePartUpdatedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventMessagePartUpdatedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventMessagePartUpdatedType string + +const ( + EventListResponseEventMessagePartUpdatedTypeMessagePartUpdated EventListResponseEventMessagePartUpdatedType = "message.part.updated" +) + +func (r EventListResponseEventMessagePartUpdatedType) IsKnown() bool { + switch r { + case EventListResponseEventMessagePartUpdatedTypeMessagePartUpdated: + return true + } + return false +} + +type EventListResponseEventSessionUpdated struct { + Properties EventListResponseEventSessionUpdatedProperties `json:"properties,required"` + Type EventListResponseEventSessionUpdatedType `json:"type,required"` + JSON eventListResponseEventSessionUpdatedJSON `json:"-"` +} + +// eventListResponseEventSessionUpdatedJSON contains the JSON metadata for the +// struct [EventListResponseEventSessionUpdated] +type eventListResponseEventSessionUpdatedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionUpdated) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionUpdatedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventSessionUpdated) implementsEventListResponse() {} + +type EventListResponseEventSessionUpdatedProperties struct { + Info Session `json:"info,required"` + JSON eventListResponseEventSessionUpdatedPropertiesJSON `json:"-"` +} + +// eventListResponseEventSessionUpdatedPropertiesJSON contains the JSON metadata +// for the struct [EventListResponseEventSessionUpdatedProperties] +type eventListResponseEventSessionUpdatedPropertiesJSON struct { + Info apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionUpdatedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionUpdatedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventSessionUpdatedType string + +const ( + EventListResponseEventSessionUpdatedTypeSessionUpdated EventListResponseEventSessionUpdatedType = "session.updated" +) + +func (r EventListResponseEventSessionUpdatedType) IsKnown() bool { + switch r { + case EventListResponseEventSessionUpdatedTypeSessionUpdated: + return true + } + return false +} + +type EventListResponseEventSessionDeleted struct { + Properties EventListResponseEventSessionDeletedProperties `json:"properties,required"` + Type EventListResponseEventSessionDeletedType `json:"type,required"` + JSON eventListResponseEventSessionDeletedJSON `json:"-"` +} + +// eventListResponseEventSessionDeletedJSON contains the JSON metadata for the +// struct [EventListResponseEventSessionDeleted] +type eventListResponseEventSessionDeletedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionDeleted) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionDeletedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventSessionDeleted) implementsEventListResponse() {} + +type EventListResponseEventSessionDeletedProperties struct { + Info Session `json:"info,required"` + JSON eventListResponseEventSessionDeletedPropertiesJSON `json:"-"` +} + +// eventListResponseEventSessionDeletedPropertiesJSON contains the JSON metadata +// for the struct [EventListResponseEventSessionDeletedProperties] +type eventListResponseEventSessionDeletedPropertiesJSON struct { + Info apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionDeletedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionDeletedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventSessionDeletedType string + +const ( + EventListResponseEventSessionDeletedTypeSessionDeleted EventListResponseEventSessionDeletedType = "session.deleted" +) + +func (r EventListResponseEventSessionDeletedType) IsKnown() bool { + switch r { + case EventListResponseEventSessionDeletedTypeSessionDeleted: + return true + } + return false +} + +type EventListResponseEventSessionIdle struct { + Properties EventListResponseEventSessionIdleProperties `json:"properties,required"` + Type EventListResponseEventSessionIdleType `json:"type,required"` + JSON eventListResponseEventSessionIdleJSON `json:"-"` +} + +// eventListResponseEventSessionIdleJSON contains the JSON metadata for the struct +// [EventListResponseEventSessionIdle] +type eventListResponseEventSessionIdleJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionIdle) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionIdleJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventSessionIdle) implementsEventListResponse() {} + +type EventListResponseEventSessionIdleProperties struct { + SessionID string `json:"sessionID,required"` + JSON eventListResponseEventSessionIdlePropertiesJSON `json:"-"` +} + +// eventListResponseEventSessionIdlePropertiesJSON contains the JSON metadata for +// the struct [EventListResponseEventSessionIdleProperties] +type eventListResponseEventSessionIdlePropertiesJSON struct { + SessionID apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionIdleProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionIdlePropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventSessionIdleType string + +const ( + EventListResponseEventSessionIdleTypeSessionIdle EventListResponseEventSessionIdleType = "session.idle" +) + +func (r EventListResponseEventSessionIdleType) IsKnown() bool { + switch r { + case EventListResponseEventSessionIdleTypeSessionIdle: + return true + } + return false +} + +type EventListResponseEventSessionError struct { + Properties EventListResponseEventSessionErrorProperties `json:"properties,required"` + Type EventListResponseEventSessionErrorType `json:"type,required"` + JSON eventListResponseEventSessionErrorJSON `json:"-"` +} + +// eventListResponseEventSessionErrorJSON contains the JSON metadata for the struct +// [EventListResponseEventSessionError] +type eventListResponseEventSessionErrorJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionError) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionErrorJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventSessionError) implementsEventListResponse() {} + +type EventListResponseEventSessionErrorProperties struct { + Error EventListResponseEventSessionErrorPropertiesError `json:"error"` + JSON eventListResponseEventSessionErrorPropertiesJSON `json:"-"` +} + +// eventListResponseEventSessionErrorPropertiesJSON contains the JSON metadata for +// the struct [EventListResponseEventSessionErrorProperties] +type eventListResponseEventSessionErrorPropertiesJSON struct { + Error apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionErrorProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionErrorPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventSessionErrorPropertiesError struct { + // This field can have the runtime type of [shared.ProviderAuthErrorData], + // [shared.UnknownErrorData], [interface{}]. + Data interface{} `json:"data,required"` + Name EventListResponseEventSessionErrorPropertiesErrorName `json:"name,required"` + JSON eventListResponseEventSessionErrorPropertiesErrorJSON `json:"-"` + union EventListResponseEventSessionErrorPropertiesErrorUnion +} + +// eventListResponseEventSessionErrorPropertiesErrorJSON contains the JSON metadata +// for the struct [EventListResponseEventSessionErrorPropertiesError] +type eventListResponseEventSessionErrorPropertiesErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r eventListResponseEventSessionErrorPropertiesErrorJSON) RawJSON() string { + return r.raw +} + +func (r *EventListResponseEventSessionErrorPropertiesError) UnmarshalJSON(data []byte) (err error) { + *r = EventListResponseEventSessionErrorPropertiesError{} + err = apijson.UnmarshalRoot(data, &r.union) + if err != nil { + return err + } + return apijson.Port(r.union, &r) +} + +// AsUnion returns a [EventListResponseEventSessionErrorPropertiesErrorUnion] +// interface which you can cast to the specific types for more type safety. +// +// Possible runtime types of the union are [shared.ProviderAuthError], +// [shared.UnknownError], +// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError]. +func (r EventListResponseEventSessionErrorPropertiesError) AsUnion() EventListResponseEventSessionErrorPropertiesErrorUnion { + return r.union +} + +// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError] or +// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError]. +type EventListResponseEventSessionErrorPropertiesErrorUnion interface { + ImplementsEventListResponseEventSessionErrorPropertiesError() +} + +func init() { + apijson.RegisterUnion( + reflect.TypeOf((*EventListResponseEventSessionErrorPropertiesErrorUnion)(nil)).Elem(), + "name", + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(shared.ProviderAuthError{}), + DiscriminatorValue: "ProviderAuthError", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(shared.UnknownError{}), + DiscriminatorValue: "UnknownError", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError{}), + DiscriminatorValue: "MessageOutputLengthError", + }, + ) +} + +type EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError struct { + Data interface{} `json:"data,required"` + Name EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorName `json:"name,required"` + JSON eventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorJSON `json:"-"` +} + +// eventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorJSON +// contains the JSON metadata for the struct +// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError] +type eventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError) ImplementsEventListResponseEventSessionErrorPropertiesError() { +} + +type EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorName string + +const ( + EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorNameMessageOutputLengthError EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorName = "MessageOutputLengthError" +) + +func (r EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorName) IsKnown() bool { + switch r { + case EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorNameMessageOutputLengthError: + return true + } + return false +} + +type EventListResponseEventSessionErrorPropertiesErrorName string + +const ( + EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError EventListResponseEventSessionErrorPropertiesErrorName = "ProviderAuthError" + EventListResponseEventSessionErrorPropertiesErrorNameUnknownError EventListResponseEventSessionErrorPropertiesErrorName = "UnknownError" + EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError EventListResponseEventSessionErrorPropertiesErrorName = "MessageOutputLengthError" +) + +func (r EventListResponseEventSessionErrorPropertiesErrorName) IsKnown() bool { + switch r { + case EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError, EventListResponseEventSessionErrorPropertiesErrorNameUnknownError, EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError: + return true + } + return false +} + +type EventListResponseEventSessionErrorType string + +const ( + EventListResponseEventSessionErrorTypeSessionError EventListResponseEventSessionErrorType = "session.error" +) + +func (r EventListResponseEventSessionErrorType) IsKnown() bool { + switch r { + case EventListResponseEventSessionErrorTypeSessionError: + return true + } + return false +} + +type EventListResponseEventFileWatcherUpdated struct { + Properties EventListResponseEventFileWatcherUpdatedProperties `json:"properties,required"` + Type EventListResponseEventFileWatcherUpdatedType `json:"type,required"` + JSON eventListResponseEventFileWatcherUpdatedJSON `json:"-"` +} + +// eventListResponseEventFileWatcherUpdatedJSON contains the JSON metadata for the +// struct [EventListResponseEventFileWatcherUpdated] +type eventListResponseEventFileWatcherUpdatedJSON struct { + Properties apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventFileWatcherUpdated) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventFileWatcherUpdatedJSON) RawJSON() string { + return r.raw +} + +func (r EventListResponseEventFileWatcherUpdated) implementsEventListResponse() {} + +type EventListResponseEventFileWatcherUpdatedProperties struct { + Event EventListResponseEventFileWatcherUpdatedPropertiesEvent `json:"event,required"` + File string `json:"file,required"` + JSON eventListResponseEventFileWatcherUpdatedPropertiesJSON `json:"-"` +} + +// eventListResponseEventFileWatcherUpdatedPropertiesJSON contains the JSON +// metadata for the struct [EventListResponseEventFileWatcherUpdatedProperties] +type eventListResponseEventFileWatcherUpdatedPropertiesJSON struct { + Event apijson.Field + File apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *EventListResponseEventFileWatcherUpdatedProperties) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r eventListResponseEventFileWatcherUpdatedPropertiesJSON) RawJSON() string { + return r.raw +} + +type EventListResponseEventFileWatcherUpdatedPropertiesEvent string + +const ( + EventListResponseEventFileWatcherUpdatedPropertiesEventRename EventListResponseEventFileWatcherUpdatedPropertiesEvent = "rename" + EventListResponseEventFileWatcherUpdatedPropertiesEventChange EventListResponseEventFileWatcherUpdatedPropertiesEvent = "change" +) + +func (r EventListResponseEventFileWatcherUpdatedPropertiesEvent) IsKnown() bool { + switch r { + case EventListResponseEventFileWatcherUpdatedPropertiesEventRename, EventListResponseEventFileWatcherUpdatedPropertiesEventChange: + return true + } + return false +} + +type EventListResponseEventFileWatcherUpdatedType string + +const ( + EventListResponseEventFileWatcherUpdatedTypeFileWatcherUpdated EventListResponseEventFileWatcherUpdatedType = "file.watcher.updated" +) + +func (r EventListResponseEventFileWatcherUpdatedType) IsKnown() bool { + switch r { + case EventListResponseEventFileWatcherUpdatedTypeFileWatcherUpdated: + return true + } + return false +} + +type EventListResponseType string + +const ( + EventListResponseTypeLspClientDiagnostics EventListResponseType = "lsp.client.diagnostics" + EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated" + EventListResponseTypeFileEdited EventListResponseType = "file.edited" + EventListResponseTypeStorageWrite EventListResponseType = "storage.write" + EventListResponseTypeInstallationUpdated EventListResponseType = "installation.updated" + EventListResponseTypeMessageUpdated EventListResponseType = "message.updated" + EventListResponseTypeMessageRemoved EventListResponseType = "message.removed" + EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated" + EventListResponseTypeSessionUpdated EventListResponseType = "session.updated" + EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted" + EventListResponseTypeSessionIdle EventListResponseType = "session.idle" + EventListResponseTypeSessionError EventListResponseType = "session.error" + EventListResponseTypeFileWatcherUpdated EventListResponseType = "file.watcher.updated" +) + +func (r EventListResponseType) IsKnown() bool { + switch r { + case EventListResponseTypeLspClientDiagnostics, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeStorageWrite, EventListResponseTypeInstallationUpdated, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated: + return true + } + return false +} diff --git a/packages/tui/sdk/examples/.keep b/packages/tui/sdk/examples/.keep new file mode 100644 index 00000000..d8c73e93 --- /dev/null +++ b/packages/tui/sdk/examples/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store example files demonstrating usage of this SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/packages/tui/sdk/field.go b/packages/tui/sdk/field.go new file mode 100644 index 00000000..56d2f890 --- /dev/null +++ b/packages/tui/sdk/field.go @@ -0,0 +1,50 @@ +package opencode + +import ( + "github.com/sst/opencode-sdk-go/internal/param" + "io" +) + +// F is a param field helper used to initialize a [param.Field] generic struct. +// This helps specify null, zero values, and overrides, as well as normal values. +// You can read more about this in our [README]. +// +// [README]: https://pkg.go.dev/github.com/sst/opencode-sdk-go#readme-request-fields +func F[T any](value T) param.Field[T] { return param.Field[T]{Value: value, Present: true} } + +// Null is a param field helper which explicitly sends null to the API. +func Null[T any]() param.Field[T] { return param.Field[T]{Null: true, Present: true} } + +// Raw is a param field helper for specifying values for fields when the +// type you are looking to send is different from the type that is specified in +// the SDK. For example, if the type of the field is an integer, but you want +// to send a float, you could do that by setting the corresponding field with +// Raw[int](0.5). +func Raw[T any](value any) param.Field[T] { return param.Field[T]{Raw: value, Present: true} } + +// Int is a param field helper which helps specify integers. This is +// particularly helpful when specifying integer constants for fields. +func Int(value int64) param.Field[int64] { return F(value) } + +// String is a param field helper which helps specify strings. +func String(value string) param.Field[string] { return F(value) } + +// Float is a param field helper which helps specify floats. +func Float(value float64) param.Field[float64] { return F(value) } + +// Bool is a param field helper which helps specify bools. +func Bool(value bool) param.Field[bool] { return F(value) } + +// FileParam is a param field helper which helps files with a mime content-type. +func FileParam(reader io.Reader, filename string, contentType string) param.Field[io.Reader] { + return F[io.Reader](&file{reader, filename, contentType}) +} + +type file struct { + io.Reader + name string + contentType string +} + +func (f *file) ContentType() string { return f.contentType } +func (f *file) Filename() string { return f.name } diff --git a/packages/tui/sdk/file.go b/packages/tui/sdk/file.go new file mode 100644 index 00000000..a9d6f018 --- /dev/null +++ b/packages/tui/sdk/file.go @@ -0,0 +1,143 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + "net/url" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" + "github.com/sst/opencode-sdk-go/internal/param" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" +) + +// FileService contains methods and other services that help with interacting with +// the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewFileService] method instead. +type FileService struct { + Options []option.RequestOption +} + +// NewFileService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewFileService(opts ...option.RequestOption) (r *FileService) { + r = &FileService{} + r.Options = opts + return +} + +// Read a file +func (r *FileService) Read(ctx context.Context, query FileReadParams, opts ...option.RequestOption) (res *FileReadResponse, err error) { + opts = append(r.Options[:], opts...) + path := "file" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +// Get file status +func (r *FileService) Status(ctx context.Context, opts ...option.RequestOption) (res *[]FileStatusResponse, err error) { + opts = append(r.Options[:], opts...) + path := "file/status" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +type FileReadResponse struct { + Content string `json:"content,required"` + Type FileReadResponseType `json:"type,required"` + JSON fileReadResponseJSON `json:"-"` +} + +// fileReadResponseJSON contains the JSON metadata for the struct +// [FileReadResponse] +type fileReadResponseJSON struct { + Content apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FileReadResponse) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r fileReadResponseJSON) RawJSON() string { + return r.raw +} + +type FileReadResponseType string + +const ( + FileReadResponseTypeRaw FileReadResponseType = "raw" + FileReadResponseTypePatch FileReadResponseType = "patch" +) + +func (r FileReadResponseType) IsKnown() bool { + switch r { + case FileReadResponseTypeRaw, FileReadResponseTypePatch: + return true + } + return false +} + +type FileStatusResponse struct { + Added int64 `json:"added,required"` + File string `json:"file,required"` + Removed int64 `json:"removed,required"` + Status FileStatusResponseStatus `json:"status,required"` + JSON fileStatusResponseJSON `json:"-"` +} + +// fileStatusResponseJSON contains the JSON metadata for the struct +// [FileStatusResponse] +type fileStatusResponseJSON struct { + Added apijson.Field + File apijson.Field + Removed apijson.Field + Status apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FileStatusResponse) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r fileStatusResponseJSON) RawJSON() string { + return r.raw +} + +type FileStatusResponseStatus string + +const ( + FileStatusResponseStatusAdded FileStatusResponseStatus = "added" + FileStatusResponseStatusDeleted FileStatusResponseStatus = "deleted" + FileStatusResponseStatusModified FileStatusResponseStatus = "modified" +) + +func (r FileStatusResponseStatus) IsKnown() bool { + switch r { + case FileStatusResponseStatusAdded, FileStatusResponseStatusDeleted, FileStatusResponseStatusModified: + return true + } + return false +} + +type FileReadParams struct { + Path param.Field[string] `query:"path,required"` +} + +// URLQuery serializes [FileReadParams]'s query parameters as `url.Values`. +func (r FileReadParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/tui/sdk/file_test.go b/packages/tui/sdk/file_test.go new file mode 100644 index 00000000..60212ea2 --- /dev/null +++ b/packages/tui/sdk/file_test.go @@ -0,0 +1,60 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestFileRead(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.File.Read(context.TODO(), opencode.FileReadParams{ + Path: opencode.F("path"), + }) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFileStatus(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.File.Status(context.TODO()) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/packages/tui/sdk/find.go b/packages/tui/sdk/find.go new file mode 100644 index 00000000..bbd6b680 --- /dev/null +++ b/packages/tui/sdk/find.go @@ -0,0 +1,213 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + "net/url" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/apiquery" + "github.com/sst/opencode-sdk-go/internal/param" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" +) + +// FindService contains methods and other services that help with interacting with +// the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewFindService] method instead. +type FindService struct { + Options []option.RequestOption +} + +// NewFindService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewFindService(opts ...option.RequestOption) (r *FindService) { + r = &FindService{} + r.Options = opts + return +} + +// Find files +func (r *FindService) Files(ctx context.Context, query FindFilesParams, opts ...option.RequestOption) (res *[]string, err error) { + opts = append(r.Options[:], opts...) + path := "find/file" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +// Find workspace symbols +func (r *FindService) Symbols(ctx context.Context, query FindSymbolsParams, opts ...option.RequestOption) (res *[]FindSymbolsResponse, err error) { + opts = append(r.Options[:], opts...) + path := "find/symbol" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +// Find text in files +func (r *FindService) Text(ctx context.Context, query FindTextParams, opts ...option.RequestOption) (res *[]FindTextResponse, err error) { + opts = append(r.Options[:], opts...) + path := "find" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return +} + +type FindSymbolsResponse = interface{} + +type FindTextResponse struct { + AbsoluteOffset float64 `json:"absolute_offset,required"` + LineNumber float64 `json:"line_number,required"` + Lines FindTextResponseLines `json:"lines,required"` + Path FindTextResponsePath `json:"path,required"` + Submatches []FindTextResponseSubmatch `json:"submatches,required"` + JSON findTextResponseJSON `json:"-"` +} + +// findTextResponseJSON contains the JSON metadata for the struct +// [FindTextResponse] +type findTextResponseJSON struct { + AbsoluteOffset apijson.Field + LineNumber apijson.Field + Lines apijson.Field + Path apijson.Field + Submatches apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FindTextResponse) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r findTextResponseJSON) RawJSON() string { + return r.raw +} + +type FindTextResponseLines struct { + Text string `json:"text,required"` + JSON findTextResponseLinesJSON `json:"-"` +} + +// findTextResponseLinesJSON contains the JSON metadata for the struct +// [FindTextResponseLines] +type findTextResponseLinesJSON struct { + Text apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FindTextResponseLines) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r findTextResponseLinesJSON) RawJSON() string { + return r.raw +} + +type FindTextResponsePath struct { + Text string `json:"text,required"` + JSON findTextResponsePathJSON `json:"-"` +} + +// findTextResponsePathJSON contains the JSON metadata for the struct +// [FindTextResponsePath] +type findTextResponsePathJSON struct { + Text apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FindTextResponsePath) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r findTextResponsePathJSON) RawJSON() string { + return r.raw +} + +type FindTextResponseSubmatch struct { + End float64 `json:"end,required"` + Match FindTextResponseSubmatchesMatch `json:"match,required"` + Start float64 `json:"start,required"` + JSON findTextResponseSubmatchJSON `json:"-"` +} + +// findTextResponseSubmatchJSON contains the JSON metadata for the struct +// [FindTextResponseSubmatch] +type findTextResponseSubmatchJSON struct { + End apijson.Field + Match apijson.Field + Start apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FindTextResponseSubmatch) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r findTextResponseSubmatchJSON) RawJSON() string { + return r.raw +} + +type FindTextResponseSubmatchesMatch struct { + Text string `json:"text,required"` + JSON findTextResponseSubmatchesMatchJSON `json:"-"` +} + +// findTextResponseSubmatchesMatchJSON contains the JSON metadata for the struct +// [FindTextResponseSubmatchesMatch] +type findTextResponseSubmatchesMatchJSON struct { + Text apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FindTextResponseSubmatchesMatch) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r findTextResponseSubmatchesMatchJSON) RawJSON() string { + return r.raw +} + +type FindFilesParams struct { + Query param.Field[string] `query:"query,required"` +} + +// URLQuery serializes [FindFilesParams]'s query parameters as `url.Values`. +func (r FindFilesParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type FindSymbolsParams struct { + Query param.Field[string] `query:"query,required"` +} + +// URLQuery serializes [FindSymbolsParams]'s query parameters as `url.Values`. +func (r FindSymbolsParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + +type FindTextParams struct { + Pattern param.Field[string] `query:"pattern,required"` +} + +// URLQuery serializes [FindTextParams]'s query parameters as `url.Values`. +func (r FindTextParams) URLQuery() (v url.Values) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/packages/tui/sdk/find_test.go b/packages/tui/sdk/find_test.go new file mode 100644 index 00000000..e2f1caa1 --- /dev/null +++ b/packages/tui/sdk/find_test.go @@ -0,0 +1,86 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestFindFiles(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Find.Files(context.TODO(), opencode.FindFilesParams{ + Query: opencode.F("query"), + }) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFindSymbols(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Find.Symbols(context.TODO(), opencode.FindSymbolsParams{ + Query: opencode.F("query"), + }) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestFindText(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Find.Text(context.TODO(), opencode.FindTextParams{ + Pattern: opencode.F("pattern"), + }) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/packages/tui/sdk/go.mod b/packages/tui/sdk/go.mod new file mode 100644 index 00000000..2817d301 --- /dev/null +++ b/packages/tui/sdk/go.mod @@ -0,0 +1,13 @@ +module github.com/sst/opencode-sdk-go + +go 1.21 + +require ( + github.com/tidwall/gjson v1.14.4 + github.com/tidwall/sjson v1.2.5 +) + +require ( + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect +) diff --git a/packages/tui/sdk/go.sum b/packages/tui/sdk/go.sum new file mode 100644 index 00000000..a70a5e0a --- /dev/null +++ b/packages/tui/sdk/go.sum @@ -0,0 +1,10 @@ +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= diff --git a/packages/tui/sdk/internal/apierror/apierror.go b/packages/tui/sdk/internal/apierror/apierror.go new file mode 100644 index 00000000..24307fc3 --- /dev/null +++ b/packages/tui/sdk/internal/apierror/apierror.go @@ -0,0 +1,53 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package apierror + +import ( + "fmt" + "net/http" + "net/http/httputil" + + "github.com/sst/opencode-sdk-go/internal/apijson" +) + +// Error represents an error that originates from the API, i.e. when a request is +// made and the API returns a response with a HTTP status code. Other errors are +// not wrapped by this SDK. +type Error struct { + JSON errorJSON `json:"-"` + StatusCode int + Request *http.Request + Response *http.Response +} + +// errorJSON contains the JSON metadata for the struct [Error] +type errorJSON struct { + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Error) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r errorJSON) RawJSON() string { + return r.raw +} + +func (r *Error) Error() string { + // Attempt to re-populate the response body + return fmt.Sprintf("%s \"%s\": %d %s %s", r.Request.Method, r.Request.URL, r.Response.StatusCode, http.StatusText(r.Response.StatusCode), r.JSON.RawJSON()) +} + +func (r *Error) DumpRequest(body bool) []byte { + if r.Request.GetBody != nil { + r.Request.Body, _ = r.Request.GetBody() + } + out, _ := httputil.DumpRequestOut(r.Request, body) + return out +} + +func (r *Error) DumpResponse(body bool) []byte { + out, _ := httputil.DumpResponse(r.Response, body) + return out +} diff --git a/packages/tui/sdk/internal/apiform/encoder.go b/packages/tui/sdk/internal/apiform/encoder.go new file mode 100644 index 00000000..243a1a12 --- /dev/null +++ b/packages/tui/sdk/internal/apiform/encoder.go @@ -0,0 +1,383 @@ +package apiform + +import ( + "fmt" + "io" + "mime/multipart" + "net/textproto" + "path" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/sst/opencode-sdk-go/internal/param" +) + +var encoders sync.Map // map[encoderEntry]encoderFunc + +func Marshal(value interface{}, writer *multipart.Writer) error { + e := &encoder{dateFormat: time.RFC3339} + return e.marshal(value, writer) +} + +func MarshalRoot(value interface{}, writer *multipart.Writer) error { + e := &encoder{root: true, dateFormat: time.RFC3339} + return e.marshal(value, writer) +} + +type encoder struct { + dateFormat string + root bool +} + +type encoderFunc func(key string, value reflect.Value, writer *multipart.Writer) error + +type encoderField struct { + tag parsedStructTag + fn encoderFunc + idx []int +} + +type encoderEntry struct { + reflect.Type + dateFormat string + root bool +} + +func (e *encoder) marshal(value interface{}, writer *multipart.Writer) error { + val := reflect.ValueOf(value) + if !val.IsValid() { + return nil + } + typ := val.Type() + enc := e.typeEncoder(typ) + return enc("", val, writer) +} + +func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { + entry := encoderEntry{ + Type: t, + dateFormat: e.dateFormat, + root: e.root, + } + + if fi, ok := encoders.Load(entry); ok { + return fi.(encoderFunc) + } + + // To deal with recursive types, populate the map with an + // indirect func before we build it. This type waits on the + // real func (f) to be ready and then calls it. This indirect + // func is only used for recursive types. + var ( + wg sync.WaitGroup + f encoderFunc + ) + wg.Add(1) + fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value, writer *multipart.Writer) error { + wg.Wait() + return f(key, v, writer) + })) + if loaded { + return fi.(encoderFunc) + } + + // Compute the real encoder and replace the indirect func with it. + f = e.newTypeEncoder(t) + wg.Done() + encoders.Store(entry, f) + return f +} + +func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return e.newTimeTypeEncoder() + } + if t.ConvertibleTo(reflect.TypeOf((*io.Reader)(nil)).Elem()) { + return e.newReaderTypeEncoder() + } + e.root = false + switch t.Kind() { + case reflect.Pointer: + inner := t.Elem() + + innerEncoder := e.typeEncoder(inner) + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if !v.IsValid() || v.IsNil() { + return nil + } + return innerEncoder(key, v.Elem(), writer) + } + case reflect.Struct: + return e.newStructTypeEncoder(t) + case reflect.Slice, reflect.Array: + return e.newArrayTypeEncoder(t) + case reflect.Map: + return e.newMapEncoder(t) + case reflect.Interface: + return e.newInterfaceEncoder() + default: + return e.newPrimitiveTypeEncoder(t) + } +} + +func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { + switch t.Kind() { + // Note that we could use `gjson` to encode these types but it would complicate our + // code more and this current code shouldn't cause any issues + case reflect.String: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, v.String()) + } + case reflect.Bool: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if v.Bool() { + return writer.WriteField(key, "true") + } + return writer.WriteField(key, "false") + } + case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatInt(v.Int(), 10)) + } + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatUint(v.Uint(), 10)) + } + case reflect.Float32: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 32)) + } + case reflect.Float64: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 64)) + } + default: + return func(key string, v reflect.Value, writer *multipart.Writer) error { + return fmt.Errorf("unknown type received at primitive encoder: %s", t.String()) + } + } +} + +func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { + itemEncoder := e.typeEncoder(t.Elem()) + + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if key != "" { + key = key + "." + } + for i := 0; i < v.Len(); i++ { + err := itemEncoder(key+strconv.Itoa(i), v.Index(i), writer) + if err != nil { + return err + } + } + return nil + } +} + +func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { + if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) { + return e.newFieldTypeEncoder(t) + } + + encoderFields := []encoderField{} + extraEncoder := (*encoderField)(nil) + + // This helper allows us to recursively collect field encoders into a flat + // array. The parameter `index` keeps track of the access patterns necessary + // to get to some field. + var collectEncoderFields func(r reflect.Type, index []int) + collectEncoderFields = func(r reflect.Type, index []int) { + for i := 0; i < r.NumField(); i++ { + idx := append(index, i) + field := t.FieldByIndex(idx) + if !field.IsExported() { + continue + } + // If this is an embedded struct, traverse one level deeper to extract + // the field and get their encoders as well. + if field.Anonymous { + collectEncoderFields(field.Type, idx) + continue + } + // If json tag is not present, then we skip, which is intentionally + // different behavior from the stdlib. + ptag, ok := parseFormStructTag(field) + if !ok { + continue + } + // We only want to support unexported field if they're tagged with + // `extras` because that field shouldn't be part of the public API. We + // also want to only keep the top level extras + if ptag.extras && len(index) == 0 { + extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx} + continue + } + if ptag.name == "-" { + continue + } + + dateFormat, ok := parseFormatStructTag(field) + oldFormat := e.dateFormat + if ok { + switch dateFormat { + case "date-time": + e.dateFormat = time.RFC3339 + case "date": + e.dateFormat = "2006-01-02" + } + } + encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx}) + e.dateFormat = oldFormat + } + } + collectEncoderFields(t, []int{}) + + // Ensure deterministic output by sorting by lexicographic order + sort.Slice(encoderFields, func(i, j int) bool { + return encoderFields[i].tag.name < encoderFields[j].tag.name + }) + + return func(key string, value reflect.Value, writer *multipart.Writer) error { + if key != "" { + key = key + "." + } + + for _, ef := range encoderFields { + field := value.FieldByIndex(ef.idx) + err := ef.fn(key+ef.tag.name, field, writer) + if err != nil { + return err + } + } + + if extraEncoder != nil { + err := e.encodeMapEntries(key, value.FieldByIndex(extraEncoder.idx), writer) + if err != nil { + return err + } + } + + return nil + } +} + +func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { + f, _ := t.FieldByName("Value") + enc := e.typeEncoder(f.Type) + + return func(key string, value reflect.Value, writer *multipart.Writer) error { + present := value.FieldByName("Present") + if !present.Bool() { + return nil + } + null := value.FieldByName("Null") + if null.Bool() { + return nil + } + raw := value.FieldByName("Raw") + if !raw.IsNil() { + return e.typeEncoder(raw.Type())(key, raw, writer) + } + return enc(key, value.FieldByName("Value"), writer) + } +} + +func (e *encoder) newTimeTypeEncoder() encoderFunc { + format := e.dateFormat + return func(key string, value reflect.Value, writer *multipart.Writer) error { + return writer.WriteField(key, value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format)) + } +} + +func (e encoder) newInterfaceEncoder() encoderFunc { + return func(key string, value reflect.Value, writer *multipart.Writer) error { + value = value.Elem() + if !value.IsValid() { + return nil + } + return e.typeEncoder(value.Type())(key, value, writer) + } +} + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(s) +} + +func (e *encoder) newReaderTypeEncoder() encoderFunc { + return func(key string, value reflect.Value, writer *multipart.Writer) error { + reader := value.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader) + filename := "anonymous_file" + contentType := "application/octet-stream" + if named, ok := reader.(interface{ Filename() string }); ok { + filename = named.Filename() + } else if named, ok := reader.(interface{ Name() string }); ok { + filename = path.Base(named.Name()) + } + if typed, ok := reader.(interface{ ContentType() string }); ok { + contentType = typed.ContentType() + } + + // Below is taken almost 1-for-1 from [multipart.CreateFormFile] + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(key), escapeQuotes(filename))) + h.Set("Content-Type", contentType) + filewriter, err := writer.CreatePart(h) + if err != nil { + return err + } + _, err = io.Copy(filewriter, reader) + return err + } +} + +// 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 { + type mapPair struct { + key string + value reflect.Value + } + + if key != "" { + key = key + "." + } + + pairs := []mapPair{} + + iter := v.MapRange() + for iter.Next() { + if iter.Key().Type().Kind() == reflect.String { + pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()}) + } else { + return fmt.Errorf("cannot encode a map with a non string key") + } + } + + // Ensure deterministic output + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].key < pairs[j].key + }) + + elementEncoder := e.typeEncoder(v.Type().Elem()) + for _, p := range pairs { + err := elementEncoder(key+string(p.key), p.value, writer) + if err != nil { + return err + } + } + + return nil +} + +func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc { + return func(key string, value reflect.Value, writer *multipart.Writer) error { + return e.encodeMapEntries(key, value, writer) + } +} diff --git a/packages/tui/sdk/internal/apiform/form.go b/packages/tui/sdk/internal/apiform/form.go new file mode 100644 index 00000000..5445116e --- /dev/null +++ b/packages/tui/sdk/internal/apiform/form.go @@ -0,0 +1,5 @@ +package apiform + +type Marshaler interface { + MarshalMultipart() ([]byte, string, error) +} diff --git a/packages/tui/sdk/internal/apiform/form_test.go b/packages/tui/sdk/internal/apiform/form_test.go new file mode 100644 index 00000000..39d1460c --- /dev/null +++ b/packages/tui/sdk/internal/apiform/form_test.go @@ -0,0 +1,440 @@ +package apiform + +import ( + "bytes" + "mime/multipart" + "strings" + "testing" + "time" +) + +func P[T any](v T) *T { return &v } + +type Primitives struct { + A bool `form:"a"` + B int `form:"b"` + C uint `form:"c"` + D float64 `form:"d"` + E float32 `form:"e"` + F []int `form:"f"` +} + +type PrimitivePointers struct { + A *bool `form:"a"` + B *int `form:"b"` + C *uint `form:"c"` + D *float64 `form:"d"` + E *float32 `form:"e"` + F *[]int `form:"f"` +} + +type Slices struct { + Slice []Primitives `form:"slices"` +} + +type DateTime struct { + Date time.Time `form:"date" format:"date"` + DateTime time.Time `form:"date-time" format:"date-time"` +} + +type AdditionalProperties struct { + A bool `form:"a"` + Extras map[string]interface{} `form:"-,extras"` +} + +type TypedAdditionalProperties struct { + A bool `form:"a"` + Extras map[string]int `form:"-,extras"` +} + +type EmbeddedStructs struct { + AdditionalProperties + A *int `form:"number2"` + Extras map[string]interface{} `form:"-,extras"` +} + +type Recursive struct { + Name string `form:"name"` + Child *Recursive `form:"child"` +} + +type UnknownStruct struct { + Unknown interface{} `form:"unknown"` +} + +type UnionStruct struct { + Union Union `form:"union" format:"date"` +} + +type Union interface { + union() +} + +type UnionInteger int64 + +func (UnionInteger) union() {} + +type UnionStructA struct { + Type string `form:"type"` + A string `form:"a"` + B string `form:"b"` +} + +func (UnionStructA) union() {} + +type UnionStructB struct { + Type string `form:"type"` + A string `form:"a"` +} + +func (UnionStructB) union() {} + +type UnionTime time.Time + +func (UnionTime) union() {} + +type ReaderStruct struct { +} + +var tests = map[string]struct { + buf string + val interface{} +}{ + "map_string": { + `--xxx +Content-Disposition: form-data; name="foo" + +bar +--xxx-- +`, + map[string]string{"foo": "bar"}, + }, + + "map_interface": { + `--xxx +Content-Disposition: form-data; name="a" + +1 +--xxx +Content-Disposition: form-data; name="b" + +str +--xxx +Content-Disposition: form-data; name="c" + +false +--xxx-- +`, + map[string]interface{}{"a": float64(1), "b": "str", "c": false}, + }, + + "primitive_struct": { + `--xxx +Content-Disposition: form-data; name="a" + +false +--xxx +Content-Disposition: form-data; name="b" + +237628372683 +--xxx +Content-Disposition: form-data; name="c" + +654 +--xxx +Content-Disposition: form-data; name="d" + +9999.43 +--xxx +Content-Disposition: form-data; name="e" + +43.76 +--xxx +Content-Disposition: form-data; name="f.0" + +1 +--xxx +Content-Disposition: form-data; name="f.1" + +2 +--xxx +Content-Disposition: form-data; name="f.2" + +3 +--xxx +Content-Disposition: form-data; name="f.3" + +4 +--xxx-- +`, + Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + }, + + "slices": { + `--xxx +Content-Disposition: form-data; name="slices.0.a" + +false +--xxx +Content-Disposition: form-data; name="slices.0.b" + +237628372683 +--xxx +Content-Disposition: form-data; name="slices.0.c" + +654 +--xxx +Content-Disposition: form-data; name="slices.0.d" + +9999.43 +--xxx +Content-Disposition: form-data; name="slices.0.e" + +43.76 +--xxx +Content-Disposition: form-data; name="slices.0.f.0" + +1 +--xxx +Content-Disposition: form-data; name="slices.0.f.1" + +2 +--xxx +Content-Disposition: form-data; name="slices.0.f.2" + +3 +--xxx +Content-Disposition: form-data; name="slices.0.f.3" + +4 +--xxx-- +`, + Slices{ + Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}}, + }, + }, + + "primitive_pointer_struct": { + `--xxx +Content-Disposition: form-data; name="a" + +false +--xxx +Content-Disposition: form-data; name="b" + +237628372683 +--xxx +Content-Disposition: form-data; name="c" + +654 +--xxx +Content-Disposition: form-data; name="d" + +9999.43 +--xxx +Content-Disposition: form-data; name="e" + +43.76 +--xxx +Content-Disposition: form-data; name="f.0" + +1 +--xxx +Content-Disposition: form-data; name="f.1" + +2 +--xxx +Content-Disposition: form-data; name="f.2" + +3 +--xxx +Content-Disposition: form-data; name="f.3" + +4 +--xxx +Content-Disposition: form-data; name="f.4" + +5 +--xxx-- +`, + PrimitivePointers{ + A: P(false), + B: P(237628372683), + C: P(uint(654)), + D: P(9999.43), + E: P(float32(43.76)), + F: &[]int{1, 2, 3, 4, 5}, + }, + }, + + "datetime_struct": { + `--xxx +Content-Disposition: form-data; name="date" + +2006-01-02 +--xxx +Content-Disposition: form-data; name="date-time" + +2006-01-02T15:04:05Z +--xxx-- +`, + DateTime{ + Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC), + DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), + }, + }, + + "additional_properties": { + `--xxx +Content-Disposition: form-data; name="a" + +true +--xxx +Content-Disposition: form-data; name="bar" + +value +--xxx +Content-Disposition: form-data; name="foo" + +true +--xxx-- +`, + AdditionalProperties{ + A: true, + Extras: map[string]interface{}{ + "bar": "value", + "foo": true, + }, + }, + }, + + "recursive_struct": { + `--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"}}, + }, + + "unknown_struct_number": { + `--xxx +Content-Disposition: form-data; name="unknown" + +12 +--xxx-- +`, + UnknownStruct{ + Unknown: 12., + }, + }, + + "unknown_struct_map": { + `--xxx +Content-Disposition: form-data; name="unknown.foo" + +bar +--xxx-- +`, + UnknownStruct{ + Unknown: map[string]interface{}{ + "foo": "bar", + }, + }, + }, + + "union_integer": { + `--xxx +Content-Disposition: form-data; name="union" + +12 +--xxx-- +`, + UnionStruct{ + Union: UnionInteger(12), + }, + }, + + "union_struct_discriminated_a": { + `--xxx +Content-Disposition: form-data; name="union.a" + +foo +--xxx +Content-Disposition: form-data; name="union.b" + +bar +--xxx +Content-Disposition: form-data; name="union.type" + +typeA +--xxx-- +`, + + UnionStruct{ + Union: UnionStructA{ + Type: "typeA", + A: "foo", + B: "bar", + }, + }, + }, + + "union_struct_discriminated_b": { + `--xxx +Content-Disposition: form-data; name="union.a" + +foo +--xxx +Content-Disposition: form-data; name="union.type" + +typeB +--xxx-- +`, + UnionStruct{ + Union: UnionStructB{ + Type: "typeB", + A: "foo", + }, + }, + }, + + "union_struct_time": { + `--xxx +Content-Disposition: form-data; name="union" + +2010-05-23 +--xxx-- +`, + UnionStruct{ + Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), + }, + }, +} + +func TestEncode(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + writer.SetBoundary("xxx") + err := Marshal(test.val, writer) + if err != nil { + t.Errorf("serialization of %v failed with error %v", test.val, err) + } + err = writer.Close() + if err != nil { + t.Errorf("serialization of %v failed with error %v", test.val, err) + } + 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)) + } + }) + } +} diff --git a/packages/tui/sdk/internal/apiform/tag.go b/packages/tui/sdk/internal/apiform/tag.go new file mode 100644 index 00000000..b22e054f --- /dev/null +++ b/packages/tui/sdk/internal/apiform/tag.go @@ -0,0 +1,48 @@ +package apiform + +import ( + "reflect" + "strings" +) + +const jsonStructTag = "json" +const formStructTag = "form" +const formatStructTag = "format" + +type parsedStructTag struct { + name string + required bool + extras bool + metadata bool +} + +func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { + raw, ok := field.Tag.Lookup(formStructTag) + if !ok { + raw, ok = field.Tag.Lookup(jsonStructTag) + } + if !ok { + return + } + parts := strings.Split(raw, ",") + if len(parts) == 0 { + return tag, false + } + tag.name = parts[0] + for _, part := range parts[1:] { + switch part { + case "required": + tag.required = true + case "extras": + tag.extras = true + case "metadata": + tag.metadata = true + } + } + return +} + +func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { + format, ok = field.Tag.Lookup(formatStructTag) + return +} diff --git a/packages/tui/sdk/internal/apijson/decoder.go b/packages/tui/sdk/internal/apijson/decoder.go new file mode 100644 index 00000000..68b7ed6b --- /dev/null +++ b/packages/tui/sdk/internal/apijson/decoder.go @@ -0,0 +1,670 @@ +package apijson + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strconv" + "sync" + "time" + "unsafe" + + "github.com/tidwall/gjson" +) + +// decoders is a synchronized map with roughly the following type: +// map[reflect.Type]decoderFunc +var decoders sync.Map + +// Unmarshal is similar to [encoding/json.Unmarshal] and parses the JSON-encoded +// data and stores it in the given pointer. +func Unmarshal(raw []byte, to any) error { + d := &decoderBuilder{dateFormat: time.RFC3339} + return d.unmarshal(raw, to) +} + +// UnmarshalRoot is like Unmarshal, but doesn't try to call MarshalJSON on the +// root element. Useful if a struct's UnmarshalJSON is overrode to use the +// behavior of this encoder versus the standard library. +func UnmarshalRoot(raw []byte, to any) error { + d := &decoderBuilder{dateFormat: time.RFC3339, root: true} + return d.unmarshal(raw, to) +} + +// decoderBuilder contains the 'compile-time' state of the decoder. +type decoderBuilder struct { + // Whether or not this is the first element and called by [UnmarshalRoot], see + // the documentation there to see why this is necessary. + root bool + // The dateFormat (a format string for [time.Format]) which is chosen by the + // last struct tag that was seen. + dateFormat string +} + +// decoderState contains the 'run-time' state of the decoder. +type decoderState struct { + strict bool + exactness exactness +} + +// Exactness refers to how close to the type the result was if deserialization +// was successful. This is useful in deserializing unions, where you want to try +// each entry, first with strict, then with looser validation, without actually +// having to do a lot of redundant work by marshalling twice (or maybe even more +// times). +type exactness int8 + +const ( + // Some values had to fudged a bit, for example by converting a string to an + // int, or an enum with extra values. + loose exactness = iota + // There are some extra arguments, but other wise it matches the union. + extras + // Exactly right. + exact +) + +type decoderFunc func(node gjson.Result, value reflect.Value, state *decoderState) error + +type decoderField struct { + tag parsedStructTag + fn decoderFunc + idx []int + goname string +} + +type decoderEntry struct { + reflect.Type + dateFormat string + root bool +} + +func (d *decoderBuilder) unmarshal(raw []byte, to any) error { + value := reflect.ValueOf(to).Elem() + result := gjson.ParseBytes(raw) + if !value.IsValid() { + return fmt.Errorf("apijson: cannot marshal into invalid value") + } + return d.typeDecoder(value.Type())(result, value, &decoderState{strict: false, exactness: exact}) +} + +func (d *decoderBuilder) typeDecoder(t reflect.Type) decoderFunc { + entry := decoderEntry{ + Type: t, + dateFormat: d.dateFormat, + root: d.root, + } + + if fi, ok := decoders.Load(entry); ok { + return fi.(decoderFunc) + } + + // To deal with recursive types, populate the map with an + // indirect func before we build it. This type waits on the + // real func (f) to be ready and then calls it. This indirect + // func is only used for recursive types. + var ( + wg sync.WaitGroup + f decoderFunc + ) + wg.Add(1) + fi, loaded := decoders.LoadOrStore(entry, decoderFunc(func(node gjson.Result, v reflect.Value, state *decoderState) error { + wg.Wait() + return f(node, v, state) + })) + if loaded { + return fi.(decoderFunc) + } + + // Compute the real decoder and replace the indirect func with it. + f = d.newTypeDecoder(t) + wg.Done() + decoders.Store(entry, f) + return f +} + +func indirectUnmarshalerDecoder(n gjson.Result, v reflect.Value, state *decoderState) error { + return v.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw)) +} + +func unmarshalerDecoder(n gjson.Result, v reflect.Value, state *decoderState) error { + if v.Kind() == reflect.Pointer && v.CanSet() { + v.Set(reflect.New(v.Type().Elem())) + } + return v.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw)) +} + +func (d *decoderBuilder) newTypeDecoder(t reflect.Type) decoderFunc { + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return d.newTimeTypeDecoder(t) + } + if !d.root && t.Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) { + return unmarshalerDecoder + } + if !d.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) { + if _, ok := unionVariants[t]; !ok { + return indirectUnmarshalerDecoder + } + } + d.root = false + + if _, ok := unionRegistry[t]; ok { + return d.newUnionDecoder(t) + } + + switch t.Kind() { + case reflect.Pointer: + inner := t.Elem() + innerDecoder := d.typeDecoder(inner) + + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + if !v.IsValid() { + return fmt.Errorf("apijson: unexpected invalid reflection value %+#v", v) + } + + newValue := reflect.New(inner).Elem() + err := innerDecoder(n, newValue, state) + if err != nil { + return err + } + + v.Set(newValue.Addr()) + return nil + } + case reflect.Struct: + return d.newStructTypeDecoder(t) + case reflect.Array: + fallthrough + case reflect.Slice: + return d.newArrayTypeDecoder(t) + case reflect.Map: + return d.newMapDecoder(t) + case reflect.Interface: + return func(node gjson.Result, value reflect.Value, state *decoderState) error { + if !value.IsValid() { + return fmt.Errorf("apijson: unexpected invalid value %+#v", value) + } + if node.Value() != nil && value.CanSet() { + value.Set(reflect.ValueOf(node.Value())) + } + return nil + } + default: + return d.newPrimitiveTypeDecoder(t) + } +} + +// newUnionDecoder returns a decoderFunc that deserializes into a union using an +// algorithm roughly similar to Pydantic's [smart algorithm]. +// +// Conceptually this is equivalent to choosing the best schema based on how 'exact' +// the deserialization is for each of the schemas. +// +// If there is a tie in the level of exactness, then the tie is broken +// left-to-right. +// +// [smart algorithm]: https://docs.pydantic.dev/latest/concepts/unions/#smart-mode +func (d *decoderBuilder) newUnionDecoder(t reflect.Type) decoderFunc { + unionEntry, ok := unionRegistry[t] + if !ok { + panic("apijson: couldn't find union of type " + t.String() + " in union registry") + } + decoders := []decoderFunc{} + for _, variant := range unionEntry.variants { + decoder := d.typeDecoder(variant.Type) + decoders = append(decoders, decoder) + } + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + // If there is a discriminator match, circumvent the exactness logic entirely + for idx, variant := range unionEntry.variants { + decoder := decoders[idx] + if variant.TypeFilter != n.Type { + continue + } + + if len(unionEntry.discriminatorKey) != 0 { + discriminatorValue := n.Get(unionEntry.discriminatorKey).Value() + if discriminatorValue == variant.DiscriminatorValue { + inner := reflect.New(variant.Type).Elem() + err := decoder(n, inner, state) + v.Set(inner) + return err + } + } + } + + // Set bestExactness to worse than loose + bestExactness := loose - 1 + for idx, variant := range unionEntry.variants { + decoder := decoders[idx] + if variant.TypeFilter != n.Type { + continue + } + sub := decoderState{strict: state.strict, exactness: exact} + inner := reflect.New(variant.Type).Elem() + err := decoder(n, inner, &sub) + if err != nil { + continue + } + if sub.exactness == exact { + v.Set(inner) + return nil + } + if sub.exactness > bestExactness { + v.Set(inner) + bestExactness = sub.exactness + } + } + + if bestExactness < loose { + return errors.New("apijson: was not able to coerce type as union") + } + + if guardStrict(state, bestExactness != exact) { + return errors.New("apijson: was not able to coerce type as union strictly") + } + + return nil + } +} + +func (d *decoderBuilder) newMapDecoder(t reflect.Type) decoderFunc { + keyType := t.Key() + itemType := t.Elem() + itemDecoder := d.typeDecoder(itemType) + + return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) { + mapValue := reflect.MakeMapWithSize(t, len(node.Map())) + + node.ForEach(func(key, value gjson.Result) bool { + // It's fine for us to just use `ValueOf` here because the key types will + // always be primitive types so we don't need to decode it using the standard pattern + keyValue := reflect.ValueOf(key.Value()) + if !keyValue.IsValid() { + if err == nil { + err = fmt.Errorf("apijson: received invalid key type %v", keyValue.String()) + } + return false + } + if keyValue.Type() != keyType { + if err == nil { + err = fmt.Errorf("apijson: expected key type %v but got %v", keyType, keyValue.Type()) + } + return false + } + + itemValue := reflect.New(itemType).Elem() + itemerr := itemDecoder(value, itemValue, state) + if itemerr != nil { + if err == nil { + err = itemerr + } + return false + } + + mapValue.SetMapIndex(keyValue, itemValue) + return true + }) + + if err != nil { + return err + } + value.Set(mapValue) + return nil + } +} + +func (d *decoderBuilder) newArrayTypeDecoder(t reflect.Type) decoderFunc { + itemDecoder := d.typeDecoder(t.Elem()) + + return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) { + if !node.IsArray() { + return fmt.Errorf("apijson: could not deserialize to an array") + } + + arrayNode := node.Array() + + arrayValue := reflect.MakeSlice(reflect.SliceOf(t.Elem()), len(arrayNode), len(arrayNode)) + for i, itemNode := range arrayNode { + err = itemDecoder(itemNode, arrayValue.Index(i), state) + if err != nil { + return err + } + } + + value.Set(arrayValue) + return nil + } +} + +func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc { + // map of json field name to struct field decoders + decoderFields := map[string]decoderField{} + anonymousDecoders := []decoderField{} + extraDecoder := (*decoderField)(nil) + inlineDecoder := (*decoderField)(nil) + + for i := 0; i < t.NumField(); i++ { + idx := []int{i} + field := t.FieldByIndex(idx) + if !field.IsExported() { + continue + } + // If this is an embedded struct, traverse one level deeper to extract + // the fields and get their encoders as well. + if field.Anonymous { + anonymousDecoders = append(anonymousDecoders, decoderField{ + fn: d.typeDecoder(field.Type), + idx: idx[:], + }) + continue + } + // If json tag is not present, then we skip, which is intentionally + // different behavior from the stdlib. + ptag, ok := parseJSONStructTag(field) + if !ok { + continue + } + // We only want to support unexported fields if they're tagged with + // `extras` because that field shouldn't be part of the public API. + if ptag.extras { + extraDecoder = &decoderField{ptag, d.typeDecoder(field.Type.Elem()), idx, field.Name} + continue + } + if ptag.inline { + inlineDecoder = &decoderField{ptag, d.typeDecoder(field.Type), idx, field.Name} + continue + } + if ptag.metadata { + continue + } + + oldFormat := d.dateFormat + dateFormat, ok := parseFormatStructTag(field) + if ok { + switch dateFormat { + case "date-time": + d.dateFormat = time.RFC3339 + case "date": + d.dateFormat = "2006-01-02" + } + } + decoderFields[ptag.name] = decoderField{ptag, d.typeDecoder(field.Type), idx, field.Name} + d.dateFormat = oldFormat + } + + return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) { + if field := value.FieldByName("JSON"); field.IsValid() { + if raw := field.FieldByName("raw"); raw.IsValid() { + setUnexportedField(raw, node.Raw) + } + } + + for _, decoder := range anonymousDecoders { + // ignore errors + decoder.fn(node, value.FieldByIndex(decoder.idx), state) + } + + if inlineDecoder != nil { + var meta Field + dest := value.FieldByIndex(inlineDecoder.idx) + isValid := false + if dest.IsValid() && node.Type != gjson.Null { + err = inlineDecoder.fn(node, dest, state) + if err == nil { + isValid = true + } + } + + if node.Type == gjson.Null { + meta = Field{ + raw: node.Raw, + status: null, + } + } else if !isValid { + meta = Field{ + raw: node.Raw, + status: invalid, + } + } else if isValid { + meta = Field{ + raw: node.Raw, + status: valid, + } + } + if metadata := getSubField(value, inlineDecoder.idx, inlineDecoder.goname); metadata.IsValid() { + metadata.Set(reflect.ValueOf(meta)) + } + return err + } + + typedExtraType := reflect.Type(nil) + typedExtraFields := reflect.Value{} + if extraDecoder != nil { + typedExtraType = value.FieldByIndex(extraDecoder.idx).Type() + typedExtraFields = reflect.MakeMap(typedExtraType) + } + untypedExtraFields := map[string]Field{} + + for fieldName, itemNode := range node.Map() { + df, explicit := decoderFields[fieldName] + var ( + dest reflect.Value + fn decoderFunc + meta Field + ) + if explicit { + fn = df.fn + dest = value.FieldByIndex(df.idx) + } + if !explicit && extraDecoder != nil { + dest = reflect.New(typedExtraType.Elem()).Elem() + fn = extraDecoder.fn + } + + isValid := false + if dest.IsValid() && itemNode.Type != gjson.Null { + err = fn(itemNode, dest, state) + if err == nil { + isValid = true + } + } + + if itemNode.Type == gjson.Null { + meta = Field{ + raw: itemNode.Raw, + status: null, + } + } else if !isValid { + meta = Field{ + raw: itemNode.Raw, + status: invalid, + } + } else if isValid { + meta = Field{ + raw: itemNode.Raw, + status: valid, + } + } + + if explicit { + if metadata := getSubField(value, df.idx, df.goname); metadata.IsValid() { + metadata.Set(reflect.ValueOf(meta)) + } + } + if !explicit { + untypedExtraFields[fieldName] = meta + } + if !explicit && extraDecoder != nil { + typedExtraFields.SetMapIndex(reflect.ValueOf(fieldName), dest) + } + } + + if extraDecoder != nil && typedExtraFields.Len() > 0 { + value.FieldByIndex(extraDecoder.idx).Set(typedExtraFields) + } + + // Set exactness to 'extras' if there are untyped, extra fields. + if len(untypedExtraFields) > 0 && state.exactness > extras { + state.exactness = extras + } + + if metadata := getSubField(value, []int{-1}, "ExtraFields"); metadata.IsValid() && len(untypedExtraFields) > 0 { + metadata.Set(reflect.ValueOf(untypedExtraFields)) + } + return nil + } +} + +func (d *decoderBuilder) newPrimitiveTypeDecoder(t reflect.Type) decoderFunc { + switch t.Kind() { + case reflect.String: + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + v.SetString(n.String()) + if guardStrict(state, n.Type != gjson.String) { + return fmt.Errorf("apijson: failed to parse string strictly") + } + // Everything that is not an object can be loosely stringified. + if n.Type == gjson.JSON { + return fmt.Errorf("apijson: failed to parse string") + } + if guardUnknown(state, v) { + return fmt.Errorf("apijson: failed string enum validation") + } + return nil + } + case reflect.Bool: + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + v.SetBool(n.Bool()) + if guardStrict(state, n.Type != gjson.True && n.Type != gjson.False) { + return fmt.Errorf("apijson: failed to parse bool strictly") + } + // Numbers and strings that are either 'true' or 'false' can be loosely + // deserialized as bool. + if n.Type == gjson.String && (n.Raw != "true" && n.Raw != "false") || n.Type == gjson.JSON { + return fmt.Errorf("apijson: failed to parse bool") + } + if guardUnknown(state, v) { + return fmt.Errorf("apijson: failed bool enum validation") + } + return nil + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + v.SetInt(n.Int()) + if guardStrict(state, n.Type != gjson.Number || n.Num != float64(int(n.Num))) { + return fmt.Errorf("apijson: failed to parse int strictly") + } + // Numbers, booleans, and strings that maybe look like numbers can be + // loosely deserialized as numbers. + if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) { + return fmt.Errorf("apijson: failed to parse int") + } + if guardUnknown(state, v) { + return fmt.Errorf("apijson: failed int enum validation") + } + return nil + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + v.SetUint(n.Uint()) + if guardStrict(state, n.Type != gjson.Number || n.Num != float64(int(n.Num)) || n.Num < 0) { + return fmt.Errorf("apijson: failed to parse uint strictly") + } + // Numbers, booleans, and strings that maybe look like numbers can be + // loosely deserialized as uint. + if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) { + return fmt.Errorf("apijson: failed to parse uint") + } + if guardUnknown(state, v) { + return fmt.Errorf("apijson: failed uint enum validation") + } + return nil + } + case reflect.Float32, reflect.Float64: + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + v.SetFloat(n.Float()) + if guardStrict(state, n.Type != gjson.Number) { + return fmt.Errorf("apijson: failed to parse float strictly") + } + // Numbers, booleans, and strings that maybe look like numbers can be + // loosely deserialized as floats. + if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) { + return fmt.Errorf("apijson: failed to parse float") + } + if guardUnknown(state, v) { + return fmt.Errorf("apijson: failed float enum validation") + } + return nil + } + default: + return func(node gjson.Result, v reflect.Value, state *decoderState) error { + return fmt.Errorf("unknown type received at primitive decoder: %s", t.String()) + } + } +} + +func (d *decoderBuilder) newTimeTypeDecoder(t reflect.Type) decoderFunc { + format := d.dateFormat + return func(n gjson.Result, v reflect.Value, state *decoderState) error { + parsed, err := time.Parse(format, n.Str) + if err == nil { + v.Set(reflect.ValueOf(parsed).Convert(t)) + return nil + } + + if guardStrict(state, true) { + return err + } + + layouts := []string{ + "2006-01-02", + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05Z0700", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05Z07:00", + "2006-01-02 15:04:05Z0700", + "2006-01-02 15:04:05", + } + + for _, layout := range layouts { + parsed, err := time.Parse(layout, n.Str) + if err == nil { + v.Set(reflect.ValueOf(parsed).Convert(t)) + return nil + } + } + + return fmt.Errorf("unable to leniently parse date-time string: %s", n.Str) + } +} + +func setUnexportedField(field reflect.Value, value interface{}) { + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(value)) +} + +func guardStrict(state *decoderState, cond bool) bool { + if !cond { + return false + } + + if state.strict { + return true + } + + state.exactness = loose + return false +} + +func canParseAsNumber(str string) bool { + _, err := strconv.ParseFloat(str, 64) + return err == nil +} + +func guardUnknown(state *decoderState, v reflect.Value) bool { + if have, ok := v.Interface().(interface{ IsKnown() bool }); guardStrict(state, ok && !have.IsKnown()) { + return true + } + return false +} diff --git a/packages/tui/sdk/internal/apijson/encoder.go b/packages/tui/sdk/internal/apijson/encoder.go new file mode 100644 index 00000000..0e5f89e1 --- /dev/null +++ b/packages/tui/sdk/internal/apijson/encoder.go @@ -0,0 +1,398 @@ +package apijson + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/tidwall/sjson" + + "github.com/sst/opencode-sdk-go/internal/param" +) + +var encoders sync.Map // map[encoderEntry]encoderFunc + +func Marshal(value interface{}) ([]byte, error) { + e := &encoder{dateFormat: time.RFC3339} + return e.marshal(value) +} + +func MarshalRoot(value interface{}) ([]byte, error) { + e := &encoder{root: true, dateFormat: time.RFC3339} + return e.marshal(value) +} + +type encoder struct { + dateFormat string + root bool +} + +type encoderFunc func(value reflect.Value) ([]byte, error) + +type encoderField struct { + tag parsedStructTag + fn encoderFunc + idx []int +} + +type encoderEntry struct { + reflect.Type + dateFormat string + root bool +} + +func (e *encoder) marshal(value interface{}) ([]byte, error) { + val := reflect.ValueOf(value) + if !val.IsValid() { + return nil, nil + } + typ := val.Type() + enc := e.typeEncoder(typ) + return enc(val) +} + +func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { + entry := encoderEntry{ + Type: t, + dateFormat: e.dateFormat, + root: e.root, + } + + if fi, ok := encoders.Load(entry); ok { + return fi.(encoderFunc) + } + + // To deal with recursive types, populate the map with an + // indirect func before we build it. This type waits on the + // real func (f) to be ready and then calls it. This indirect + // func is only used for recursive types. + var ( + wg sync.WaitGroup + f encoderFunc + ) + wg.Add(1) + fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(v reflect.Value) ([]byte, error) { + wg.Wait() + return f(v) + })) + if loaded { + return fi.(encoderFunc) + } + + // Compute the real encoder and replace the indirect func with it. + f = e.newTypeEncoder(t) + wg.Done() + encoders.Store(entry, f) + return f +} + +func marshalerEncoder(v reflect.Value) ([]byte, error) { + return v.Interface().(json.Marshaler).MarshalJSON() +} + +func indirectMarshalerEncoder(v reflect.Value) ([]byte, error) { + return v.Addr().Interface().(json.Marshaler).MarshalJSON() +} + +func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return e.newTimeTypeEncoder() + } + if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) { + return marshalerEncoder + } + if !e.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) { + return indirectMarshalerEncoder + } + e.root = false + switch t.Kind() { + case reflect.Pointer: + inner := t.Elem() + + innerEncoder := e.typeEncoder(inner) + return func(v reflect.Value) ([]byte, error) { + if !v.IsValid() || v.IsNil() { + return nil, nil + } + return innerEncoder(v.Elem()) + } + case reflect.Struct: + return e.newStructTypeEncoder(t) + case reflect.Array: + fallthrough + case reflect.Slice: + return e.newArrayTypeEncoder(t) + case reflect.Map: + return e.newMapEncoder(t) + case reflect.Interface: + return e.newInterfaceEncoder() + default: + return e.newPrimitiveTypeEncoder(t) + } +} + +func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { + switch t.Kind() { + // Note that we could use `gjson` to encode these types but it would complicate our + // code more and this current code shouldn't cause any issues + case reflect.String: + return func(v reflect.Value) ([]byte, error) { + return json.Marshal(v.Interface()) + } + case reflect.Bool: + return func(v reflect.Value) ([]byte, error) { + if v.Bool() { + return []byte("true"), nil + } + return []byte("false"), nil + } + case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: + return func(v reflect.Value) ([]byte, error) { + return []byte(strconv.FormatInt(v.Int(), 10)), nil + } + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return func(v reflect.Value) ([]byte, error) { + return []byte(strconv.FormatUint(v.Uint(), 10)), nil + } + case reflect.Float32: + return func(v reflect.Value) ([]byte, error) { + return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 32)), nil + } + case reflect.Float64: + return func(v reflect.Value) ([]byte, error) { + return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 64)), nil + } + default: + return func(v reflect.Value) ([]byte, error) { + return nil, fmt.Errorf("unknown type received at primitive encoder: %s", t.String()) + } + } +} + +func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { + itemEncoder := e.typeEncoder(t.Elem()) + + return func(value reflect.Value) ([]byte, error) { + json := []byte("[]") + for i := 0; i < value.Len(); i++ { + var value, err = itemEncoder(value.Index(i)) + if err != nil { + return nil, err + } + if value == nil { + // Assume that empty items should be inserted as `null` so that the output array + // will be the same length as the input array + value = []byte("null") + } + + json, err = sjson.SetRawBytes(json, "-1", value) + if err != nil { + return nil, err + } + } + + return json, nil + } +} + +func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { + if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) { + return e.newFieldTypeEncoder(t) + } + + encoderFields := []encoderField{} + extraEncoder := (*encoderField)(nil) + + // This helper allows us to recursively collect field encoders into a flat + // array. The parameter `index` keeps track of the access patterns necessary + // to get to some field. + var collectEncoderFields func(r reflect.Type, index []int) + collectEncoderFields = func(r reflect.Type, index []int) { + for i := 0; i < r.NumField(); i++ { + idx := append(index, i) + field := t.FieldByIndex(idx) + if !field.IsExported() { + continue + } + // If this is an embedded struct, traverse one level deeper to extract + // the field and get their encoders as well. + if field.Anonymous { + collectEncoderFields(field.Type, idx) + continue + } + // If json tag is not present, then we skip, which is intentionally + // different behavior from the stdlib. + ptag, ok := parseJSONStructTag(field) + if !ok { + continue + } + // We only want to support unexported field if they're tagged with + // `extras` because that field shouldn't be part of the public API. We + // also want to only keep the top level extras + if ptag.extras && len(index) == 0 { + extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx} + continue + } + if ptag.name == "-" { + continue + } + + dateFormat, ok := parseFormatStructTag(field) + oldFormat := e.dateFormat + if ok { + switch dateFormat { + case "date-time": + e.dateFormat = time.RFC3339 + case "date": + e.dateFormat = "2006-01-02" + } + } + encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx}) + e.dateFormat = oldFormat + } + } + collectEncoderFields(t, []int{}) + + // Ensure deterministic output by sorting by lexicographic order + sort.Slice(encoderFields, func(i, j int) bool { + return encoderFields[i].tag.name < encoderFields[j].tag.name + }) + + return func(value reflect.Value) (json []byte, err error) { + json = []byte("{}") + + for _, ef := range encoderFields { + field := value.FieldByIndex(ef.idx) + encoded, err := ef.fn(field) + if err != nil { + return nil, err + } + if encoded == nil { + continue + } + json, err = sjson.SetRawBytes(json, ef.tag.name, encoded) + if err != nil { + return nil, err + } + } + + if extraEncoder != nil { + json, err = e.encodeMapEntries(json, value.FieldByIndex(extraEncoder.idx)) + if err != nil { + return nil, err + } + } + return + } +} + +func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { + f, _ := t.FieldByName("Value") + enc := e.typeEncoder(f.Type) + + return func(value reflect.Value) (json []byte, err error) { + present := value.FieldByName("Present") + if !present.Bool() { + return nil, nil + } + null := value.FieldByName("Null") + if null.Bool() { + return []byte("null"), nil + } + raw := value.FieldByName("Raw") + if !raw.IsNil() { + return e.typeEncoder(raw.Type())(raw) + } + return enc(value.FieldByName("Value")) + } +} + +func (e *encoder) newTimeTypeEncoder() encoderFunc { + format := e.dateFormat + return func(value reflect.Value) (json []byte, err error) { + return []byte(`"` + value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format) + `"`), nil + } +} + +func (e encoder) newInterfaceEncoder() encoderFunc { + return func(value reflect.Value) ([]byte, error) { + value = value.Elem() + if !value.IsValid() { + return nil, nil + } + return e.typeEncoder(value.Type())(value) + } +} + +// 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(json []byte, v reflect.Value) ([]byte, error) { + type mapPair struct { + key []byte + value reflect.Value + } + + pairs := []mapPair{} + keyEncoder := e.typeEncoder(v.Type().Key()) + + iter := v.MapRange() + for iter.Next() { + var encodedKeyString string + if iter.Key().Type().Kind() == reflect.String { + encodedKeyString = iter.Key().String() + } else { + var err error + encodedKeyBytes, err := keyEncoder(iter.Key()) + if err != nil { + return nil, err + } + encodedKeyString = string(encodedKeyBytes) + } + encodedKey := []byte(sjsonReplacer.Replace(encodedKeyString)) + pairs = append(pairs, mapPair{key: encodedKey, value: iter.Value()}) + } + + // Ensure deterministic output + sort.Slice(pairs, func(i, j int) bool { + return bytes.Compare(pairs[i].key, pairs[j].key) < 0 + }) + + elementEncoder := e.typeEncoder(v.Type().Elem()) + for _, p := range pairs { + encodedValue, err := elementEncoder(p.value) + if err != nil { + return nil, err + } + if len(encodedValue) == 0 { + continue + } + json, err = sjson.SetRawBytes(json, string(p.key), encodedValue) + if err != nil { + return nil, err + } + } + + return json, nil +} + +func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc { + return func(value reflect.Value) ([]byte, error) { + json := []byte("{}") + var err error + json, err = e.encodeMapEntries(json, value) + if err != nil { + return nil, err + } + return json, nil + } +} + +// If we want to set a literal key value into JSON using sjson, we need to make sure it doesn't have +// special characters that sjson interprets as a path. +var sjsonReplacer *strings.Replacer = strings.NewReplacer(".", "\\.", ":", "\\:", "*", "\\*") diff --git a/packages/tui/sdk/internal/apijson/field.go b/packages/tui/sdk/internal/apijson/field.go new file mode 100644 index 00000000..3ef207c5 --- /dev/null +++ b/packages/tui/sdk/internal/apijson/field.go @@ -0,0 +1,41 @@ +package apijson + +import "reflect" + +type status uint8 + +const ( + missing status = iota + null + invalid + valid +) + +type Field struct { + raw string + status status +} + +// Returns true if the field is explicitly `null` _or_ if it is not present at all (ie, missing). +// To check if the field's key is present in the JSON with an explicit null value, +// you must check `f.IsNull() && !f.IsMissing()`. +func (j Field) IsNull() bool { return j.status <= null } +func (j Field) IsMissing() bool { return j.status == missing } +func (j Field) IsInvalid() bool { return j.status == invalid } +func (j Field) Raw() string { return j.raw } + +func getSubField(root reflect.Value, index []int, name string) reflect.Value { + strct := root.FieldByIndex(index[:len(index)-1]) + if !strct.IsValid() { + panic("couldn't find encapsulating struct for field " + name) + } + meta := strct.FieldByName("JSON") + if !meta.IsValid() { + return reflect.Value{} + } + field := meta.FieldByName(name) + if !field.IsValid() { + return reflect.Value{} + } + return field +} diff --git a/packages/tui/sdk/internal/apijson/field_test.go b/packages/tui/sdk/internal/apijson/field_test.go new file mode 100644 index 00000000..2e170c76 --- /dev/null +++ b/packages/tui/sdk/internal/apijson/field_test.go @@ -0,0 +1,66 @@ +package apijson + +import ( + "testing" + "time" + + "github.com/sst/opencode-sdk-go/internal/param" +) + +type Struct struct { + A string `json:"a"` + B int64 `json:"b"` +} + +type FieldStruct struct { + A param.Field[string] `json:"a"` + B param.Field[int64] `json:"b"` + C param.Field[Struct] `json:"c"` + D param.Field[time.Time] `json:"d" format:"date"` + E param.Field[time.Time] `json:"e" format:"date-time"` + F param.Field[int64] `json:"f"` +} + +func TestFieldMarshal(t *testing.T) { + tests := map[string]struct { + value interface{} + expected string + }{ + "null_string": {param.Field[string]{Present: true, Null: true}, "null"}, + "null_int": {param.Field[int]{Present: true, Null: true}, "null"}, + "null_int64": {param.Field[int64]{Present: true, Null: true}, "null"}, + "null_struct": {param.Field[Struct]{Present: true, Null: true}, "null"}, + + "string": {param.Field[string]{Present: true, Value: "string"}, `"string"`}, + "int": {param.Field[int]{Present: true, Value: 123}, "123"}, + "int64": {param.Field[int64]{Present: true, Value: int64(123456789123456789)}, "123456789123456789"}, + "struct": {param.Field[Struct]{Present: true, Value: Struct{A: "yo", B: 123}}, `{"a":"yo","b":123}`}, + + "string_raw": {param.Field[int]{Present: true, Raw: "string"}, `"string"`}, + "int_raw": {param.Field[int]{Present: true, Raw: 123}, "123"}, + "int64_raw": {param.Field[int]{Present: true, Raw: int64(123456789123456789)}, "123456789123456789"}, + "struct_raw": {param.Field[int]{Present: true, Raw: Struct{A: "yo", B: 123}}, `{"a":"yo","b":123}`}, + + "param_struct": { + FieldStruct{ + A: param.Field[string]{Present: true, Value: "hello"}, + B: param.Field[int64]{Present: true, Value: int64(12)}, + D: param.Field[time.Time]{Present: true, Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, + E: param.Field[time.Time]{Present: true, Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)}, + }, + `{"a":"hello","b":12,"d":"2023-03-18","e":"2023-03-18T14:47:38Z"}`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + b, err := Marshal(test.value) + if err != nil { + t.Fatalf("didn't expect error %v", err) + } + if string(b) != test.expected { + t.Fatalf("expected %s, received %s", test.expected, string(b)) + } + }) + } +} diff --git a/packages/tui/sdk/internal/apijson/json_test.go b/packages/tui/sdk/internal/apijson/json_test.go new file mode 100644 index 00000000..e6563448 --- /dev/null +++ b/packages/tui/sdk/internal/apijson/json_test.go @@ -0,0 +1,617 @@ +package apijson + +import ( + "reflect" + "strings" + "testing" + "time" + + "github.com/tidwall/gjson" +) + +func P[T any](v T) *T { return &v } + +type Primitives struct { + A bool `json:"a"` + B int `json:"b"` + C uint `json:"c"` + D float64 `json:"d"` + E float32 `json:"e"` + F []int `json:"f"` +} + +type PrimitivePointers struct { + A *bool `json:"a"` + B *int `json:"b"` + C *uint `json:"c"` + D *float64 `json:"d"` + E *float32 `json:"e"` + F *[]int `json:"f"` +} + +type Slices struct { + Slice []Primitives `json:"slices"` +} + +type DateTime struct { + Date time.Time `json:"date" format:"date"` + DateTime time.Time `json:"date-time" format:"date-time"` +} + +type AdditionalProperties struct { + A bool `json:"a"` + ExtraFields map[string]interface{} `json:"-,extras"` +} + +type TypedAdditionalProperties struct { + A bool `json:"a"` + ExtraFields map[string]int `json:"-,extras"` +} + +type EmbeddedStruct struct { + A bool `json:"a"` + B string `json:"b"` + + JSON EmbeddedStructJSON +} + +type EmbeddedStructJSON struct { + A Field + B Field + ExtraFields map[string]Field + raw string +} + +type EmbeddedStructs struct { + EmbeddedStruct + A *int `json:"a"` + ExtraFields map[string]interface{} `json:"-,extras"` + + JSON EmbeddedStructsJSON +} + +type EmbeddedStructsJSON struct { + A Field + ExtraFields map[string]Field + raw string +} + +type Recursive struct { + Name string `json:"name"` + Child *Recursive `json:"child"` +} + +type JSONFieldStruct struct { + A bool `json:"a"` + B int64 `json:"b"` + C string `json:"c"` + D string `json:"d"` + ExtraFields map[string]int64 `json:"-,extras"` + JSON JSONFieldStructJSON `json:"-,metadata"` +} + +type JSONFieldStructJSON struct { + A Field + B Field + C Field + D Field + ExtraFields map[string]Field + raw string +} + +type UnknownStruct struct { + Unknown interface{} `json:"unknown"` +} + +type UnionStruct struct { + Union Union `json:"union" format:"date"` +} + +type Union interface { + union() +} + +type Inline struct { + InlineField Primitives `json:"-,inline"` + JSON InlineJSON `json:"-,metadata"` +} + +type InlineArray struct { + InlineField []string `json:"-,inline"` + JSON InlineJSON `json:"-,metadata"` +} + +type InlineJSON struct { + InlineField Field + raw string +} + +type UnionInteger int64 + +func (UnionInteger) union() {} + +type UnionStructA struct { + Type string `json:"type"` + A string `json:"a"` + B string `json:"b"` +} + +func (UnionStructA) union() {} + +type UnionStructB struct { + Type string `json:"type"` + A string `json:"a"` +} + +func (UnionStructB) union() {} + +type UnionTime time.Time + +func (UnionTime) union() {} + +func init() { + RegisterUnion(reflect.TypeOf((*Union)(nil)).Elem(), "type", + UnionVariant{ + TypeFilter: gjson.String, + Type: reflect.TypeOf(UnionTime{}), + }, + UnionVariant{ + TypeFilter: gjson.Number, + Type: reflect.TypeOf(UnionInteger(0)), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + DiscriminatorValue: "typeA", + Type: reflect.TypeOf(UnionStructA{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + DiscriminatorValue: "typeB", + Type: reflect.TypeOf(UnionStructB{}), + }, + ) +} + +type ComplexUnionStruct struct { + Union ComplexUnion `json:"union"` +} + +type ComplexUnion interface { + complexUnion() +} + +type ComplexUnionA struct { + Boo string `json:"boo"` + Foo bool `json:"foo"` +} + +func (ComplexUnionA) complexUnion() {} + +type ComplexUnionB struct { + Boo bool `json:"boo"` + Foo string `json:"foo"` +} + +func (ComplexUnionB) complexUnion() {} + +type ComplexUnionC struct { + Boo int64 `json:"boo"` +} + +func (ComplexUnionC) complexUnion() {} + +type ComplexUnionTypeA struct { + Baz int64 `json:"baz"` + Type TypeA `json:"type"` +} + +func (ComplexUnionTypeA) complexUnion() {} + +type TypeA string + +func (t TypeA) IsKnown() bool { + return t == "a" +} + +type ComplexUnionTypeB struct { + Baz int64 `json:"baz"` + Type TypeB `json:"type"` +} + +type TypeB string + +func (t TypeB) IsKnown() bool { + return t == "b" +} + +type UnmarshalStruct struct { + Foo string `json:"foo"` + prop bool `json:"-"` +} + +func (r *UnmarshalStruct) UnmarshalJSON(json []byte) error { + r.prop = true + return UnmarshalRoot(json, r) +} + +func (ComplexUnionTypeB) complexUnion() {} + +func init() { + RegisterUnion(reflect.TypeOf((*ComplexUnion)(nil)).Elem(), "", + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ComplexUnionA{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ComplexUnionB{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ComplexUnionC{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ComplexUnionTypeA{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ComplexUnionTypeB{}), + }, + ) +} + +type MarshallingUnionStruct struct { + Union MarshallingUnion +} + +func (r *MarshallingUnionStruct) UnmarshalJSON(data []byte) (err error) { + *r = MarshallingUnionStruct{} + err = UnmarshalRoot(data, &r.Union) + return +} + +func (r MarshallingUnionStruct) MarshalJSON() (data []byte, err error) { + return MarshalRoot(r.Union) +} + +type MarshallingUnion interface { + marshallingUnion() +} + +type MarshallingUnionA struct { + Boo string `json:"boo"` +} + +func (MarshallingUnionA) marshallingUnion() {} + +func (r *MarshallingUnionA) UnmarshalJSON(data []byte) (err error) { + return UnmarshalRoot(data, r) +} + +type MarshallingUnionB struct { + Foo string `json:"foo"` +} + +func (MarshallingUnionB) marshallingUnion() {} + +func (r *MarshallingUnionB) UnmarshalJSON(data []byte) (err error) { + return UnmarshalRoot(data, r) +} + +func init() { + RegisterUnion( + reflect.TypeOf((*MarshallingUnion)(nil)).Elem(), + "", + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(MarshallingUnionA{}), + }, + UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(MarshallingUnionB{}), + }, + ) +} + +var tests = map[string]struct { + buf string + val interface{} +}{ + "true": {"true", true}, + "false": {"false", false}, + "int": {"1", 1}, + "int_bigger": {"12324", 12324}, + "int_string_coerce": {`"65"`, 65}, + "int_boolean_coerce": {"true", 1}, + "int64": {"1", int64(1)}, + "int64_huge": {"123456789123456789", int64(123456789123456789)}, + "uint": {"1", uint(1)}, + "uint_bigger": {"12324", uint(12324)}, + "uint_coerce": {`"65"`, uint(65)}, + "float_1.54": {"1.54", float32(1.54)}, + "float_1.89": {"1.89", float64(1.89)}, + "string": {`"str"`, "str"}, + "string_int_coerce": {`12`, "12"}, + "array_string": {`["foo","bar"]`, []string{"foo", "bar"}}, + "array_int": {`[1,2]`, []int{1, 2}}, + "array_int_coerce": {`["1",2]`, []int{1, 2}}, + + "ptr_true": {"true", P(true)}, + "ptr_false": {"false", P(false)}, + "ptr_int": {"1", P(1)}, + "ptr_int_bigger": {"12324", P(12324)}, + "ptr_int_string_coerce": {`"65"`, P(65)}, + "ptr_int_boolean_coerce": {"true", P(1)}, + "ptr_int64": {"1", P(int64(1))}, + "ptr_int64_huge": {"123456789123456789", P(int64(123456789123456789))}, + "ptr_uint": {"1", P(uint(1))}, + "ptr_uint_bigger": {"12324", P(uint(12324))}, + "ptr_uint_coerce": {`"65"`, P(uint(65))}, + "ptr_float_1.54": {"1.54", P(float32(1.54))}, + "ptr_float_1.89": {"1.89", P(float64(1.89))}, + + "date_time": {`"2007-03-01T13:00:00Z"`, time.Date(2007, time.March, 1, 13, 0, 0, 0, time.UTC)}, + "date_time_nano_coerce": {`"2007-03-01T13:03:05.123456789Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 123456789, time.UTC)}, + + "date_time_missing_t_coerce": {`"2007-03-01 13:03:05Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.UTC)}, + "date_time_missing_timezone_coerce": {`"2007-03-01T13:03:05"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.UTC)}, + // note: using -1200 to minimize probability of conflicting with the local timezone of the test runner + // see https://en.wikipedia.org/wiki/UTC%E2%88%9212:00 + "date_time_missing_timezone_colon_coerce": {`"2007-03-01T13:03:05-1200"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.FixedZone("", -12*60*60))}, + "date_time_nano_missing_t_coerce": {`"2007-03-01 13:03:05.123456789Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 123456789, time.UTC)}, + + "map_string": {`{"foo":"bar"}`, map[string]string{"foo": "bar"}}, + "map_string_with_sjson_path_chars": {`{":a.b.c*:d*-1e.f":"bar"}`, map[string]string{":a.b.c*:d*-1e.f": "bar"}}, + "map_interface": {`{"a":1,"b":"str","c":false}`, map[string]interface{}{"a": float64(1), "b": "str", "c": false}}, + + "primitive_struct": { + `{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}`, + Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + }, + + "slices": { + `{"slices":[{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}]}`, + Slices{ + Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}}, + }, + }, + + "primitive_pointer_struct": { + `{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4,5]}`, + PrimitivePointers{ + A: P(false), + B: P(237628372683), + C: P(uint(654)), + D: P(9999.43), + E: P(float32(43.76)), + F: &[]int{1, 2, 3, 4, 5}, + }, + }, + + "datetime_struct": { + `{"date":"2006-01-02","date-time":"2006-01-02T15:04:05Z"}`, + DateTime{ + Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC), + DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), + }, + }, + + "additional_properties": { + `{"a":true,"bar":"value","foo":true}`, + AdditionalProperties{ + A: true, + ExtraFields: map[string]interface{}{ + "bar": "value", + "foo": true, + }, + }, + }, + + "embedded_struct": { + `{"a":1,"b":"bar"}`, + EmbeddedStructs{ + EmbeddedStruct: EmbeddedStruct{ + A: true, + B: "bar", + JSON: EmbeddedStructJSON{ + A: Field{raw: `1`, status: valid}, + B: Field{raw: `"bar"`, status: valid}, + raw: `{"a":1,"b":"bar"}`, + }, + }, + A: P(1), + ExtraFields: map[string]interface{}{"b": "bar"}, + JSON: EmbeddedStructsJSON{ + A: Field{raw: `1`, status: valid}, + ExtraFields: map[string]Field{ + "b": {raw: `"bar"`, status: valid}, + }, + raw: `{"a":1,"b":"bar"}`, + }, + }, + }, + + "recursive_struct": { + `{"child":{"name":"Alex"},"name":"Robert"}`, + Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, + }, + + "metadata_coerce": { + `{"a":"12","b":"12","c":null,"extra_typed":12,"extra_untyped":{"foo":"bar"}}`, + JSONFieldStruct{ + A: false, + B: 12, + C: "", + JSON: JSONFieldStructJSON{ + raw: `{"a":"12","b":"12","c":null,"extra_typed":12,"extra_untyped":{"foo":"bar"}}`, + A: Field{raw: `"12"`, status: invalid}, + B: Field{raw: `"12"`, status: valid}, + C: Field{raw: "null", status: null}, + D: Field{raw: "", status: missing}, + ExtraFields: map[string]Field{ + "extra_typed": { + raw: "12", + status: valid, + }, + "extra_untyped": { + raw: `{"foo":"bar"}`, + status: invalid, + }, + }, + }, + ExtraFields: map[string]int64{ + "extra_typed": 12, + "extra_untyped": 0, + }, + }, + }, + + "unknown_struct_number": { + `{"unknown":12}`, + UnknownStruct{ + Unknown: 12., + }, + }, + + "unknown_struct_map": { + `{"unknown":{"foo":"bar"}}`, + UnknownStruct{ + Unknown: map[string]interface{}{ + "foo": "bar", + }, + }, + }, + + "union_integer": { + `{"union":12}`, + UnionStruct{ + Union: UnionInteger(12), + }, + }, + + "union_struct_discriminated_a": { + `{"union":{"a":"foo","b":"bar","type":"typeA"}}`, + UnionStruct{ + Union: UnionStructA{ + Type: "typeA", + A: "foo", + B: "bar", + }, + }, + }, + + "union_struct_discriminated_b": { + `{"union":{"a":"foo","type":"typeB"}}`, + UnionStruct{ + Union: UnionStructB{ + Type: "typeB", + A: "foo", + }, + }, + }, + + "union_struct_time": { + `{"union":"2010-05-23"}`, + UnionStruct{ + Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), + }, + }, + + "complex_union_a": { + `{"union":{"boo":"12","foo":true}}`, + ComplexUnionStruct{Union: ComplexUnionA{Boo: "12", Foo: true}}, + }, + + "complex_union_b": { + `{"union":{"boo":true,"foo":"12"}}`, + ComplexUnionStruct{Union: ComplexUnionB{Boo: true, Foo: "12"}}, + }, + + "complex_union_c": { + `{"union":{"boo":12}}`, + ComplexUnionStruct{Union: ComplexUnionC{Boo: 12}}, + }, + + "complex_union_type_a": { + `{"union":{"baz":12,"type":"a"}}`, + ComplexUnionStruct{Union: ComplexUnionTypeA{Baz: 12, Type: TypeA("a")}}, + }, + + "complex_union_type_b": { + `{"union":{"baz":12,"type":"b"}}`, + ComplexUnionStruct{Union: ComplexUnionTypeB{Baz: 12, Type: TypeB("b")}}, + }, + + "marshalling_union_a": { + `{"boo":"hello"}`, + MarshallingUnionStruct{Union: MarshallingUnionA{Boo: "hello"}}, + }, + "marshalling_union_b": { + `{"foo":"hi"}`, + MarshallingUnionStruct{Union: MarshallingUnionB{Foo: "hi"}}, + }, + + "unmarshal": { + `{"foo":"hello"}`, + &UnmarshalStruct{Foo: "hello", prop: true}, + }, + + "array_of_unmarshal": { + `[{"foo":"hello"}]`, + []UnmarshalStruct{{Foo: "hello", prop: true}}, + }, + + "inline_coerce": { + `{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}`, + Inline{ + InlineField: Primitives{A: false, B: 237628372683, C: 0x28e, D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + JSON: InlineJSON{ + InlineField: Field{raw: "{\"a\":false,\"b\":237628372683,\"c\":654,\"d\":9999.43,\"e\":43.76,\"f\":[1,2,3,4]}", status: 3}, + raw: "{\"a\":false,\"b\":237628372683,\"c\":654,\"d\":9999.43,\"e\":43.76,\"f\":[1,2,3,4]}", + }, + }, + }, + + "inline_array_coerce": { + `["Hello","foo","bar"]`, + InlineArray{ + InlineField: []string{"Hello", "foo", "bar"}, + JSON: InlineJSON{ + InlineField: Field{raw: `["Hello","foo","bar"]`, status: 3}, + raw: `["Hello","foo","bar"]`, + }, + }, + }, +} + +func TestDecode(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { + result := reflect.New(reflect.TypeOf(test.val)) + if err := Unmarshal([]byte(test.buf), result.Interface()); err != nil { + t.Fatalf("deserialization of %v failed with error %v", result, err) + } + if !reflect.DeepEqual(result.Elem().Interface(), test.val) { + t.Fatalf("expected '%s' to deserialize to \n%#v\nbut got\n%#v", test.buf, test.val, result.Elem().Interface()) + } + }) + } +} + +func TestEncode(t *testing.T) { + for name, test := range tests { + if strings.HasSuffix(name, "_coerce") { + continue + } + t.Run(name, func(t *testing.T) { + raw, err := Marshal(test.val) + if err != nil { + t.Fatalf("serialization of %v failed with error %v", test.val, err) + } + if string(raw) != test.buf { + t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.buf, string(raw)) + } + }) + } +} diff --git a/packages/tui/sdk/internal/apijson/port.go b/packages/tui/sdk/internal/apijson/port.go new file mode 100644 index 00000000..502ab778 --- /dev/null +++ b/packages/tui/sdk/internal/apijson/port.go @@ -0,0 +1,120 @@ +package apijson + +import ( + "fmt" + "reflect" +) + +// Port copies over values from one struct to another struct. +func Port(from any, to any) error { + toVal := reflect.ValueOf(to) + fromVal := reflect.ValueOf(from) + + if toVal.Kind() != reflect.Ptr || toVal.IsNil() { + return fmt.Errorf("destination must be a non-nil pointer") + } + + for toVal.Kind() == reflect.Ptr { + toVal = toVal.Elem() + } + toType := toVal.Type() + + for fromVal.Kind() == reflect.Ptr { + fromVal = fromVal.Elem() + } + fromType := fromVal.Type() + + if toType.Kind() != reflect.Struct { + return fmt.Errorf("destination must be a non-nil pointer to a struct (%v %v)", toType, toType.Kind()) + } + + values := map[string]reflect.Value{} + fields := map[string]reflect.Value{} + + fromJSON := fromVal.FieldByName("JSON") + toJSON := toVal.FieldByName("JSON") + + // Iterate through the fields of v and load all the "normal" fields in the struct to the map of + // string to reflect.Value, as well as their raw .JSON.Foo counterpart indicated by j. + var getFields func(t reflect.Type, v reflect.Value) + getFields = func(t reflect.Type, v reflect.Value) { + j := v.FieldByName("JSON") + + // Recurse into anonymous fields first, since the fields on the object should win over the fields in the + // embedded object. + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.Anonymous { + getFields(field.Type, v.Field(i)) + continue + } + } + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + ptag, ok := parseJSONStructTag(field) + if !ok || ptag.name == "-" { + continue + } + values[ptag.name] = v.Field(i) + if j.IsValid() { + fields[ptag.name] = j.FieldByName(field.Name) + } + } + } + getFields(fromType, fromVal) + + // Use the values from the previous step to populate the 'to' struct. + for i := 0; i < toType.NumField(); i++ { + field := toType.Field(i) + ptag, ok := parseJSONStructTag(field) + if !ok { + continue + } + if ptag.name == "-" { + continue + } + if value, ok := values[ptag.name]; ok { + delete(values, ptag.name) + if field.Type.Kind() == reflect.Interface { + toVal.Field(i).Set(value) + } else { + switch value.Kind() { + case reflect.String: + toVal.Field(i).SetString(value.String()) + case reflect.Bool: + toVal.Field(i).SetBool(value.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + toVal.Field(i).SetInt(value.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + toVal.Field(i).SetUint(value.Uint()) + case reflect.Float32, reflect.Float64: + toVal.Field(i).SetFloat(value.Float()) + default: + toVal.Field(i).Set(value) + } + } + } + + if fromJSONField, ok := fields[ptag.name]; ok { + if toJSONField := toJSON.FieldByName(field.Name); toJSONField.IsValid() { + toJSONField.Set(fromJSONField) + } + } + } + + // Finally, copy over the .JSON.raw and .JSON.ExtraFields + if toJSON.IsValid() { + if raw := toJSON.FieldByName("raw"); raw.IsValid() { + setUnexportedField(raw, fromJSON.Interface().(interface{ RawJSON() string }).RawJSON()) + } + + if toExtraFields := toJSON.FieldByName("ExtraFields"); toExtraFields.IsValid() { + if fromExtraFields := fromJSON.FieldByName("ExtraFields"); fromExtraFields.IsValid() { + setUnexportedField(toExtraFields, fromExtraFields.Interface()) + } + } + } + + return nil +} diff --git a/packages/tui/sdk/internal/apijson/port_test.go b/packages/tui/sdk/internal/apijson/port_test.go new file mode 100644 index 00000000..11540533 --- /dev/null +++ b/packages/tui/sdk/internal/apijson/port_test.go @@ -0,0 +1,257 @@ +package apijson + +import ( + "reflect" + "testing" +) + +type Metadata struct { + CreatedAt string `json:"created_at"` +} + +// Card is the "combined" type of CardVisa and CardMastercard +type Card struct { + Processor CardProcessor `json:"processor"` + Data any `json:"data"` + IsFoo bool `json:"is_foo"` + IsBar bool `json:"is_bar"` + Metadata Metadata `json:"metadata"` + Value interface{} `json:"value"` + + JSON cardJSON +} + +type cardJSON struct { + Processor Field + Data Field + IsFoo Field + IsBar Field + Metadata Field + Value Field + ExtraFields map[string]Field + raw string +} + +func (r cardJSON) RawJSON() string { return r.raw } + +type CardProcessor string + +// CardVisa +type CardVisa struct { + Processor CardVisaProcessor `json:"processor"` + Data CardVisaData `json:"data"` + IsFoo bool `json:"is_foo"` + Metadata Metadata `json:"metadata"` + Value string `json:"value"` + + JSON cardVisaJSON +} + +type cardVisaJSON struct { + Processor Field + Data Field + IsFoo Field + Metadata Field + Value Field + ExtraFields map[string]Field + raw string +} + +func (r cardVisaJSON) RawJSON() string { return r.raw } + +type CardVisaProcessor string + +type CardVisaData struct { + Foo string `json:"foo"` +} + +// CardMastercard +type CardMastercard struct { + Processor CardMastercardProcessor `json:"processor"` + Data CardMastercardData `json:"data"` + IsBar bool `json:"is_bar"` + Metadata Metadata `json:"metadata"` + Value bool `json:"value"` + + JSON cardMastercardJSON +} + +type cardMastercardJSON struct { + Processor Field + Data Field + IsBar Field + Metadata Field + Value Field + ExtraFields map[string]Field + raw string +} + +func (r cardMastercardJSON) RawJSON() string { return r.raw } + +type CardMastercardProcessor string + +type CardMastercardData struct { + Bar int64 `json:"bar"` +} + +type CommonFields struct { + Metadata Metadata `json:"metadata"` + Value string `json:"value"` + + JSON commonFieldsJSON +} + +type commonFieldsJSON struct { + Metadata Field + Value Field + ExtraFields map[string]Field + raw string +} + +type CardEmbedded struct { + CommonFields + Processor CardVisaProcessor `json:"processor"` + Data CardVisaData `json:"data"` + IsFoo bool `json:"is_foo"` + + JSON cardEmbeddedJSON +} + +type cardEmbeddedJSON struct { + Processor Field + Data Field + IsFoo Field + ExtraFields map[string]Field + raw string +} + +func (r cardEmbeddedJSON) RawJSON() string { return r.raw } + +var portTests = map[string]struct { + from any + to any +}{ + "visa to card": { + CardVisa{ + Processor: "visa", + IsFoo: true, + Data: CardVisaData{ + Foo: "foo", + }, + Metadata: Metadata{ + CreatedAt: "Mar 29 2024", + }, + Value: "value", + JSON: cardVisaJSON{ + raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`, + Processor: Field{raw: `"visa"`, status: valid}, + IsFoo: Field{raw: `true`, status: valid}, + Data: Field{raw: `{"foo":"foo"}`, status: valid}, + Value: Field{raw: `"value"`, status: valid}, + ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}}, + }, + }, + Card{ + Processor: "visa", + IsFoo: true, + IsBar: false, + Data: CardVisaData{ + Foo: "foo", + }, + Metadata: Metadata{ + CreatedAt: "Mar 29 2024", + }, + Value: "value", + JSON: cardJSON{ + raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`, + Processor: Field{raw: `"visa"`, status: valid}, + IsFoo: Field{raw: `true`, status: valid}, + Data: Field{raw: `{"foo":"foo"}`, status: valid}, + Value: Field{raw: `"value"`, status: valid}, + ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}}, + }, + }, + }, + "mastercard to card": { + CardMastercard{ + Processor: "mastercard", + IsBar: true, + Data: CardMastercardData{ + Bar: 13, + }, + Value: false, + }, + Card{ + Processor: "mastercard", + IsFoo: false, + IsBar: true, + Data: CardMastercardData{ + Bar: 13, + }, + Value: false, + }, + }, + "embedded to card": { + CardEmbedded{ + CommonFields: CommonFields{ + Metadata: Metadata{ + CreatedAt: "Mar 29 2024", + }, + Value: "embedded_value", + JSON: commonFieldsJSON{ + Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: valid}, + Value: Field{raw: `"embedded_value"`, status: valid}, + raw: `should not matter`, + }, + }, + Processor: "visa", + IsFoo: true, + Data: CardVisaData{ + Foo: "embedded_foo", + }, + JSON: cardEmbeddedJSON{ + raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`, + Processor: Field{raw: `"visa"`, status: valid}, + IsFoo: Field{raw: `true`, status: valid}, + Data: Field{raw: `{"foo":"embedded_foo"}`, status: valid}, + }, + }, + Card{ + Processor: "visa", + IsFoo: true, + IsBar: false, + Data: CardVisaData{ + Foo: "embedded_foo", + }, + Metadata: Metadata{ + CreatedAt: "Mar 29 2024", + }, + Value: "embedded_value", + JSON: cardJSON{ + raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`, + Processor: Field{raw: `"visa"`, status: 0x3}, + IsFoo: Field{raw: "true", status: 0x3}, + Data: Field{raw: `{"foo":"embedded_foo"}`, status: 0x3}, + Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: 0x3}, + Value: Field{raw: `"embedded_value"`, status: 0x3}, + }, + }, + }, +} + +func TestPort(t *testing.T) { + for name, test := range portTests { + t.Run(name, func(t *testing.T) { + toVal := reflect.New(reflect.TypeOf(test.to)) + + err := Port(test.from, toVal.Interface()) + if err != nil { + t.Fatalf("port of %v failed with error %v", test.from, err) + } + + if !reflect.DeepEqual(toVal.Elem().Interface(), test.to) { + t.Fatalf("expected:\n%+#v\n\nto port to:\n%+#v\n\nbut got:\n%+#v", test.from, test.to, toVal.Elem().Interface()) + } + }) + } +} diff --git a/packages/tui/sdk/internal/apijson/registry.go b/packages/tui/sdk/internal/apijson/registry.go new file mode 100644 index 00000000..119cc5ff --- /dev/null +++ b/packages/tui/sdk/internal/apijson/registry.go @@ -0,0 +1,41 @@ +package apijson + +import ( + "reflect" + + "github.com/tidwall/gjson" +) + +type UnionVariant struct { + TypeFilter gjson.Type + DiscriminatorValue interface{} + Type reflect.Type +} + +var unionRegistry = map[reflect.Type]unionEntry{} +var unionVariants = map[reflect.Type]interface{}{} + +type unionEntry struct { + discriminatorKey string + variants []UnionVariant +} + +func RegisterUnion(typ reflect.Type, discriminator string, variants ...UnionVariant) { + unionRegistry[typ] = unionEntry{ + discriminatorKey: discriminator, + variants: variants, + } + for _, variant := range variants { + unionVariants[variant.Type] = typ + } +} + +// Useful to wrap a union type to force it to use [apijson.UnmarshalJSON] since you cannot define an +// UnmarshalJSON function on the interface itself. +type UnionUnmarshaler[T any] struct { + Value T +} + +func (c *UnionUnmarshaler[T]) UnmarshalJSON(buf []byte) error { + return UnmarshalRoot(buf, &c.Value) +} diff --git a/packages/tui/sdk/internal/apijson/tag.go b/packages/tui/sdk/internal/apijson/tag.go new file mode 100644 index 00000000..812fb3ca --- /dev/null +++ b/packages/tui/sdk/internal/apijson/tag.go @@ -0,0 +1,47 @@ +package apijson + +import ( + "reflect" + "strings" +) + +const jsonStructTag = "json" +const formatStructTag = "format" + +type parsedStructTag struct { + name string + required bool + extras bool + metadata bool + inline bool +} + +func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { + raw, ok := field.Tag.Lookup(jsonStructTag) + if !ok { + return + } + parts := strings.Split(raw, ",") + if len(parts) == 0 { + return tag, false + } + tag.name = parts[0] + for _, part := range parts[1:] { + switch part { + case "required": + tag.required = true + case "extras": + tag.extras = true + case "metadata": + tag.metadata = true + case "inline": + tag.inline = true + } + } + return +} + +func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { + format, ok = field.Tag.Lookup(formatStructTag) + return +} diff --git a/packages/tui/sdk/internal/apiquery/encoder.go b/packages/tui/sdk/internal/apiquery/encoder.go new file mode 100644 index 00000000..0922c231 --- /dev/null +++ b/packages/tui/sdk/internal/apiquery/encoder.go @@ -0,0 +1,341 @@ +package apiquery + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "github.com/sst/opencode-sdk-go/internal/param" +) + +var encoders sync.Map // map[reflect.Type]encoderFunc + +type encoder struct { + dateFormat string + root bool + settings QuerySettings +} + +type encoderFunc func(key string, value reflect.Value) []Pair + +type encoderField struct { + tag parsedStructTag + fn encoderFunc + idx []int +} + +type encoderEntry struct { + reflect.Type + dateFormat string + root bool + settings QuerySettings +} + +type Pair struct { + key string + value string +} + +func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { + entry := encoderEntry{ + Type: t, + dateFormat: e.dateFormat, + root: e.root, + settings: e.settings, + } + + if fi, ok := encoders.Load(entry); ok { + return fi.(encoderFunc) + } + + // To deal with recursive types, populate the map with an + // indirect func before we build it. This type waits on the + // real func (f) to be ready and then calls it. This indirect + // func is only used for recursive types. + var ( + wg sync.WaitGroup + f encoderFunc + ) + wg.Add(1) + fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value) []Pair { + wg.Wait() + return f(key, v) + })) + if loaded { + return fi.(encoderFunc) + } + + // Compute the real encoder and replace the indirect func with it. + f = e.newTypeEncoder(t) + wg.Done() + encoders.Store(entry, f) + return f +} + +func marshalerEncoder(key string, value reflect.Value) []Pair { + s, _ := value.Interface().(json.Marshaler).MarshalJSON() + return []Pair{{key, string(s)}} +} + +func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { + if t.ConvertibleTo(reflect.TypeOf(time.Time{})) { + return e.newTimeTypeEncoder(t) + } + if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) { + return marshalerEncoder + } + e.root = false + switch t.Kind() { + case reflect.Pointer: + encoder := e.typeEncoder(t.Elem()) + return func(key string, value reflect.Value) (pairs []Pair) { + if !value.IsValid() || value.IsNil() { + return + } + pairs = encoder(key, value.Elem()) + return + } + case reflect.Struct: + return e.newStructTypeEncoder(t) + case reflect.Array: + fallthrough + case reflect.Slice: + return e.newArrayTypeEncoder(t) + case reflect.Map: + return e.newMapEncoder(t) + case reflect.Interface: + return e.newInterfaceEncoder() + default: + return e.newPrimitiveTypeEncoder(t) + } +} + +func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { + if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) { + return e.newFieldTypeEncoder(t) + } + + encoderFields := []encoderField{} + + // This helper allows us to recursively collect field encoders into a flat + // array. The parameter `index` keeps track of the access patterns necessary + // to get to some field. + var collectEncoderFields func(r reflect.Type, index []int) + collectEncoderFields = func(r reflect.Type, index []int) { + for i := 0; i < r.NumField(); i++ { + idx := append(index, i) + field := t.FieldByIndex(idx) + if !field.IsExported() { + continue + } + // If this is an embedded struct, traverse one level deeper to extract + // the field and get their encoders as well. + if field.Anonymous { + collectEncoderFields(field.Type, idx) + continue + } + // If query tag is not present, then we skip, which is intentionally + // different behavior from the stdlib. + ptag, ok := parseQueryStructTag(field) + if !ok { + continue + } + + if ptag.name == "-" && !ptag.inline { + continue + } + + dateFormat, ok := parseFormatStructTag(field) + oldFormat := e.dateFormat + if ok { + switch dateFormat { + case "date-time": + e.dateFormat = time.RFC3339 + case "date": + e.dateFormat = "2006-01-02" + } + } + encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx}) + e.dateFormat = oldFormat + } + } + collectEncoderFields(t, []int{}) + + return func(key string, value reflect.Value) (pairs []Pair) { + for _, ef := range encoderFields { + var subkey string = e.renderKeyPath(key, ef.tag.name) + if ef.tag.inline { + subkey = key + } + + field := value.FieldByIndex(ef.idx) + pairs = append(pairs, ef.fn(subkey, field)...) + } + return + } +} + +func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc { + keyEncoder := e.typeEncoder(t.Key()) + elementEncoder := e.typeEncoder(t.Elem()) + return func(key string, value reflect.Value) (pairs []Pair) { + iter := value.MapRange() + for iter.Next() { + encodedKey := keyEncoder("", iter.Key()) + if len(encodedKey) != 1 { + panic("Unexpected number of parts for encoded map key. Are you using a non-primitive for this map?") + } + subkey := encodedKey[0].value + keyPath := e.renderKeyPath(key, subkey) + pairs = append(pairs, elementEncoder(keyPath, iter.Value())...) + } + return + } +} + +func (e *encoder) renderKeyPath(key string, subkey string) string { + if len(key) == 0 { + return subkey + } + if e.settings.NestedFormat == NestedQueryFormatDots { + return fmt.Sprintf("%s.%s", key, subkey) + } + return fmt.Sprintf("%s[%s]", key, subkey) +} + +func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { + switch e.settings.ArrayFormat { + case ArrayQueryFormatComma: + innerEncoder := e.typeEncoder(t.Elem()) + return func(key string, v reflect.Value) []Pair { + elements := []string{} + for i := 0; i < v.Len(); i++ { + for _, pair := range innerEncoder("", v.Index(i)) { + elements = append(elements, pair.value) + } + } + if len(elements) == 0 { + return []Pair{} + } + return []Pair{{key, strings.Join(elements, ",")}} + } + case ArrayQueryFormatRepeat: + innerEncoder := e.typeEncoder(t.Elem()) + return func(key string, value reflect.Value) (pairs []Pair) { + for i := 0; i < value.Len(); i++ { + pairs = append(pairs, innerEncoder(key, value.Index(i))...) + } + return pairs + } + case ArrayQueryFormatIndices: + panic("The array indices format is not supported yet") + case ArrayQueryFormatBrackets: + innerEncoder := e.typeEncoder(t.Elem()) + return func(key string, value reflect.Value) []Pair { + pairs := []Pair{} + for i := 0; i < value.Len(); i++ { + pairs = append(pairs, innerEncoder(key+"[]", value.Index(i))...) + } + return pairs + } + default: + panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat)) + } +} + +func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { + switch t.Kind() { + case reflect.Pointer: + inner := t.Elem() + + innerEncoder := e.newPrimitiveTypeEncoder(inner) + return func(key string, v reflect.Value) []Pair { + if !v.IsValid() || v.IsNil() { + return nil + } + return innerEncoder(key, v.Elem()) + } + case reflect.String: + return func(key string, v reflect.Value) []Pair { + return []Pair{{key, v.String()}} + } + case reflect.Bool: + return func(key string, v reflect.Value) []Pair { + if v.Bool() { + return []Pair{{key, "true"}} + } + return []Pair{{key, "false"}} + } + case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: + return func(key string, v reflect.Value) []Pair { + return []Pair{{key, strconv.FormatInt(v.Int(), 10)}} + } + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return func(key string, v reflect.Value) []Pair { + return []Pair{{key, strconv.FormatUint(v.Uint(), 10)}} + } + case reflect.Float32, reflect.Float64: + return func(key string, v reflect.Value) []Pair { + return []Pair{{key, strconv.FormatFloat(v.Float(), 'f', -1, 64)}} + } + case reflect.Complex64, reflect.Complex128: + bitSize := 64 + if t.Kind() == reflect.Complex128 { + bitSize = 128 + } + return func(key string, v reflect.Value) []Pair { + return []Pair{{key, strconv.FormatComplex(v.Complex(), 'f', -1, bitSize)}} + } + default: + return func(key string, v reflect.Value) []Pair { + return nil + } + } +} + +func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { + f, _ := t.FieldByName("Value") + enc := e.typeEncoder(f.Type) + + return func(key string, value reflect.Value) []Pair { + present := value.FieldByName("Present") + if !present.Bool() { + return nil + } + null := value.FieldByName("Null") + if null.Bool() { + // TODO: Error? + return nil + } + raw := value.FieldByName("Raw") + if !raw.IsNil() { + return e.typeEncoder(raw.Type())(key, raw) + } + return enc(key, value.FieldByName("Value")) + } +} + +func (e *encoder) newTimeTypeEncoder(t reflect.Type) encoderFunc { + format := e.dateFormat + return func(key string, value reflect.Value) []Pair { + return []Pair{{ + key, + value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format), + }} + } +} + +func (e encoder) newInterfaceEncoder() encoderFunc { + return func(key string, value reflect.Value) []Pair { + value = value.Elem() + if !value.IsValid() { + return nil + } + return e.typeEncoder(value.Type())(key, value) + } + +} diff --git a/packages/tui/sdk/internal/apiquery/query.go b/packages/tui/sdk/internal/apiquery/query.go new file mode 100644 index 00000000..6f90e993 --- /dev/null +++ b/packages/tui/sdk/internal/apiquery/query.go @@ -0,0 +1,50 @@ +package apiquery + +import ( + "net/url" + "reflect" + "time" +) + +func MarshalWithSettings(value interface{}, settings QuerySettings) url.Values { + e := encoder{time.RFC3339, true, settings} + kv := url.Values{} + val := reflect.ValueOf(value) + if !val.IsValid() { + return nil + } + typ := val.Type() + for _, pair := range e.typeEncoder(typ)("", val) { + kv.Add(pair.key, pair.value) + } + return kv +} + +func Marshal(value interface{}) url.Values { + return MarshalWithSettings(value, QuerySettings{}) +} + +type Queryer interface { + URLQuery() url.Values +} + +type QuerySettings struct { + NestedFormat NestedQueryFormat + ArrayFormat ArrayQueryFormat +} + +type NestedQueryFormat int + +const ( + NestedQueryFormatBrackets NestedQueryFormat = iota + NestedQueryFormatDots +) + +type ArrayQueryFormat int + +const ( + ArrayQueryFormatComma ArrayQueryFormat = iota + ArrayQueryFormatRepeat + ArrayQueryFormatIndices + ArrayQueryFormatBrackets +) diff --git a/packages/tui/sdk/internal/apiquery/query_test.go b/packages/tui/sdk/internal/apiquery/query_test.go new file mode 100644 index 00000000..1e740d6a --- /dev/null +++ b/packages/tui/sdk/internal/apiquery/query_test.go @@ -0,0 +1,335 @@ +package apiquery + +import ( + "net/url" + "testing" + "time" +) + +func P[T any](v T) *T { return &v } + +type Primitives struct { + A bool `query:"a"` + B int `query:"b"` + C uint `query:"c"` + D float64 `query:"d"` + E float32 `query:"e"` + F []int `query:"f"` +} + +type PrimitivePointers struct { + A *bool `query:"a"` + B *int `query:"b"` + C *uint `query:"c"` + D *float64 `query:"d"` + E *float32 `query:"e"` + F *[]int `query:"f"` +} + +type Slices struct { + Slice []Primitives `query:"slices"` + Mixed []interface{} `query:"mixed"` +} + +type DateTime struct { + Date time.Time `query:"date" format:"date"` + DateTime time.Time `query:"date-time" format:"date-time"` +} + +type AdditionalProperties struct { + A bool `query:"a"` + Extras map[string]interface{} `query:"-,inline"` +} + +type Recursive struct { + Name string `query:"name"` + Child *Recursive `query:"child"` +} + +type UnknownStruct struct { + Unknown interface{} `query:"unknown"` +} + +type UnionStruct struct { + Union Union `query:"union" format:"date"` +} + +type Union interface { + union() +} + +type UnionInteger int64 + +func (UnionInteger) union() {} + +type UnionString string + +func (UnionString) union() {} + +type UnionStructA struct { + Type string `query:"type"` + A string `query:"a"` + B string `query:"b"` +} + +func (UnionStructA) union() {} + +type UnionStructB struct { + Type string `query:"type"` + A string `query:"a"` +} + +func (UnionStructB) union() {} + +type UnionTime time.Time + +func (UnionTime) union() {} + +type DeeplyNested struct { + A DeeplyNested1 `query:"a"` +} + +type DeeplyNested1 struct { + B DeeplyNested2 `query:"b"` +} + +type DeeplyNested2 struct { + C DeeplyNested3 `query:"c"` +} + +type DeeplyNested3 struct { + D *string `query:"d"` +} + +var tests = map[string]struct { + enc string + val interface{} + settings QuerySettings +}{ + "primitives": { + "a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4", + Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + QuerySettings{}, + }, + + "slices_brackets": { + `mixed[]=1&mixed[]=2.3&mixed[]=hello&slices[][a]=false&slices[][a]=false&slices[][b]=237628372683&slices[][b]=237628372683&slices[][c]=654&slices[][c]=654&slices[][d]=9999.43&slices[][d]=9999.43&slices[][e]=43.7599983215332&slices[][e]=43.7599983215332&slices[][f][]=1&slices[][f][]=2&slices[][f][]=3&slices[][f][]=4&slices[][f][]=1&slices[][f][]=2&slices[][f][]=3&slices[][f][]=4`, + Slices{ + Slice: []Primitives{ + {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + }, + Mixed: []interface{}{1, 2.3, "hello"}, + }, + QuerySettings{ArrayFormat: ArrayQueryFormatBrackets}, + }, + + "slices_comma": { + `mixed=1,2.3,hello`, + Slices{ + Mixed: []interface{}{1, 2.3, "hello"}, + }, + QuerySettings{ArrayFormat: ArrayQueryFormatComma}, + }, + + "slices_repeat": { + `mixed=1&mixed=2.3&mixed=hello&slices[a]=false&slices[a]=false&slices[b]=237628372683&slices[b]=237628372683&slices[c]=654&slices[c]=654&slices[d]=9999.43&slices[d]=9999.43&slices[e]=43.7599983215332&slices[e]=43.7599983215332&slices[f]=1&slices[f]=2&slices[f]=3&slices[f]=4&slices[f]=1&slices[f]=2&slices[f]=3&slices[f]=4`, + Slices{ + Slice: []Primitives{ + {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}, + }, + Mixed: []interface{}{1, 2.3, "hello"}, + }, + QuerySettings{ArrayFormat: ArrayQueryFormatRepeat}, + }, + + "primitive_pointer_struct": { + "a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4,5", + PrimitivePointers{ + A: P(false), + B: P(237628372683), + C: P(uint(654)), + D: P(9999.43), + E: P(float32(43.76)), + F: &[]int{1, 2, 3, 4, 5}, + }, + QuerySettings{}, + }, + + "datetime_struct": { + `date=2006-01-02&date-time=2006-01-02T15:04:05Z`, + DateTime{ + Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC), + DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), + }, + QuerySettings{}, + }, + + "additional_properties": { + `a=true&bar=value&foo=true`, + AdditionalProperties{ + A: true, + Extras: map[string]interface{}{ + "bar": "value", + "foo": true, + }, + }, + QuerySettings{}, + }, + + "recursive_struct_brackets": { + `child[name]=Alex&name=Robert`, + Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, + QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + }, + + "recursive_struct_dots": { + `child.name=Alex&name=Robert`, + Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, + QuerySettings{NestedFormat: NestedQueryFormatDots}, + }, + + "unknown_struct_number": { + `unknown=12`, + UnknownStruct{ + Unknown: 12., + }, + QuerySettings{}, + }, + + "unknown_struct_map_brackets": { + `unknown[foo]=bar`, + UnknownStruct{ + Unknown: map[string]interface{}{ + "foo": "bar", + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + }, + + "unknown_struct_map_dots": { + `unknown.foo=bar`, + UnknownStruct{ + Unknown: map[string]interface{}{ + "foo": "bar", + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatDots}, + }, + + "union_string": { + `union=hello`, + UnionStruct{ + Union: UnionString("hello"), + }, + QuerySettings{}, + }, + + "union_integer": { + `union=12`, + UnionStruct{ + Union: UnionInteger(12), + }, + QuerySettings{}, + }, + + "union_struct_discriminated_a": { + `union[a]=foo&union[b]=bar&union[type]=typeA`, + UnionStruct{ + Union: UnionStructA{ + Type: "typeA", + A: "foo", + B: "bar", + }, + }, + QuerySettings{}, + }, + + "union_struct_discriminated_b": { + `union[a]=foo&union[type]=typeB`, + UnionStruct{ + Union: UnionStructB{ + Type: "typeB", + A: "foo", + }, + }, + QuerySettings{}, + }, + + "union_struct_time": { + `union=2010-05-23`, + UnionStruct{ + Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), + }, + QuerySettings{}, + }, + + "deeply_nested_brackets": { + `a[b][c][d]=hello`, + DeeplyNested{ + A: DeeplyNested1{ + B: DeeplyNested2{ + C: DeeplyNested3{ + D: P("hello"), + }, + }, + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + }, + + "deeply_nested_dots": { + `a.b.c.d=hello`, + DeeplyNested{ + A: DeeplyNested1{ + B: DeeplyNested2{ + C: DeeplyNested3{ + D: P("hello"), + }, + }, + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatDots}, + }, + + "deeply_nested_brackets_empty": { + ``, + DeeplyNested{ + A: DeeplyNested1{ + B: DeeplyNested2{ + C: DeeplyNested3{ + D: nil, + }, + }, + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatBrackets}, + }, + + "deeply_nested_dots_empty": { + ``, + DeeplyNested{ + A: DeeplyNested1{ + B: DeeplyNested2{ + C: DeeplyNested3{ + D: nil, + }, + }, + }, + }, + QuerySettings{NestedFormat: NestedQueryFormatDots}, + }, +} + +func TestEncode(t *testing.T) { + for name, test := range tests { + t.Run(name, func(t *testing.T) { + values := MarshalWithSettings(test.val, test.settings) + str, _ := url.QueryUnescape(values.Encode()) + if str != test.enc { + t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.enc, str) + } + }) + } +} diff --git a/packages/tui/sdk/internal/apiquery/tag.go b/packages/tui/sdk/internal/apiquery/tag.go new file mode 100644 index 00000000..7ccd739c --- /dev/null +++ b/packages/tui/sdk/internal/apiquery/tag.go @@ -0,0 +1,41 @@ +package apiquery + +import ( + "reflect" + "strings" +) + +const queryStructTag = "query" +const formatStructTag = "format" + +type parsedStructTag struct { + name string + omitempty bool + inline bool +} + +func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { + raw, ok := field.Tag.Lookup(queryStructTag) + if !ok { + return + } + parts := strings.Split(raw, ",") + if len(parts) == 0 { + return tag, false + } + tag.name = parts[0] + for _, part := range parts[1:] { + switch part { + case "omitempty": + tag.omitempty = true + case "inline": + tag.inline = true + } + } + return +} + +func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { + format, ok = field.Tag.Lookup(formatStructTag) + return +} diff --git a/packages/tui/sdk/internal/param/field.go b/packages/tui/sdk/internal/param/field.go new file mode 100644 index 00000000..4d0fd9c6 --- /dev/null +++ b/packages/tui/sdk/internal/param/field.go @@ -0,0 +1,29 @@ +package param + +import ( + "fmt" +) + +type FieldLike interface{ field() } + +// Field is a wrapper used for all values sent to the API, +// to distinguish zero values from null or omitted fields. +// +// It also allows sending arbitrary deserializable values. +// +// To instantiate a Field, use the helpers exported from +// the package root: `F()`, `Null()`, `Raw()`, etc. +type Field[T any] struct { + FieldLike + Value T + Null bool + Present bool + Raw any +} + +func (f Field[T]) String() string { + if s, ok := any(f.Value).(fmt.Stringer); ok { + return s.String() + } + return fmt.Sprintf("%v", f.Value) +} diff --git a/packages/tui/sdk/internal/requestconfig/requestconfig.go b/packages/tui/sdk/internal/requestconfig/requestconfig.go new file mode 100644 index 00000000..91b70cca --- /dev/null +++ b/packages/tui/sdk/internal/requestconfig/requestconfig.go @@ -0,0 +1,629 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package requestconfig + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "math" + "math/rand" + "mime" + "net/http" + "net/url" + "runtime" + "strconv" + "strings" + "time" + + "github.com/sst/opencode-sdk-go/internal" + "github.com/sst/opencode-sdk-go/internal/apierror" + "github.com/sst/opencode-sdk-go/internal/apiform" + "github.com/sst/opencode-sdk-go/internal/apiquery" + "github.com/sst/opencode-sdk-go/internal/param" +) + +func getDefaultHeaders() map[string]string { + return map[string]string{ + "User-Agent": fmt.Sprintf("Opencode/Go %s", internal.PackageVersion), + } +} + +func getNormalizedOS() string { + switch runtime.GOOS { + case "ios": + return "iOS" + case "android": + return "Android" + case "darwin": + return "MacOS" + case "window": + return "Windows" + case "freebsd": + return "FreeBSD" + case "openbsd": + return "OpenBSD" + case "linux": + return "Linux" + default: + return fmt.Sprintf("Other:%s", runtime.GOOS) + } +} + +func getNormalizedArchitecture() string { + switch runtime.GOARCH { + case "386": + return "x32" + case "amd64": + return "x64" + case "arm": + return "arm" + case "arm64": + return "arm64" + default: + return fmt.Sprintf("other:%s", runtime.GOARCH) + } +} + +func getPlatformProperties() map[string]string { + return map[string]string{ + "X-Stainless-Lang": "go", + "X-Stainless-Package-Version": internal.PackageVersion, + "X-Stainless-OS": getNormalizedOS(), + "X-Stainless-Arch": getNormalizedArchitecture(), + "X-Stainless-Runtime": "go", + "X-Stainless-Runtime-Version": runtime.Version(), + } +} + +type RequestOption interface { + Apply(*RequestConfig) error +} + +type RequestOptionFunc func(*RequestConfig) error +type PreRequestOptionFunc func(*RequestConfig) error + +func (s RequestOptionFunc) Apply(r *RequestConfig) error { return s(r) } +func (s PreRequestOptionFunc) Apply(r *RequestConfig) error { return s(r) } + +func NewRequestConfig(ctx context.Context, method string, u string, body interface{}, dst interface{}, opts ...RequestOption) (*RequestConfig, error) { + var reader io.Reader + + contentType := "application/json" + hasSerializationFunc := false + + if body, ok := body.(json.Marshaler); ok { + content, err := body.MarshalJSON() + if err != nil { + return nil, err + } + reader = bytes.NewBuffer(content) + hasSerializationFunc = true + } + if body, ok := body.(apiform.Marshaler); ok { + var ( + content []byte + err error + ) + content, contentType, err = body.MarshalMultipart() + if err != nil { + return nil, err + } + reader = bytes.NewBuffer(content) + hasSerializationFunc = true + } + if body, ok := body.(apiquery.Queryer); ok { + hasSerializationFunc = true + params := body.URLQuery().Encode() + if params != "" { + u = u + "?" + params + } + } + if body, ok := body.([]byte); ok { + reader = bytes.NewBuffer(body) + hasSerializationFunc = true + } + if body, ok := body.(io.Reader); ok { + reader = body + hasSerializationFunc = true + } + + // Fallback to json serialization if none of the serialization functions that we expect + // to see is present. + if body != nil && !hasSerializationFunc { + content, err := json.Marshal(body) + if err != nil { + return nil, err + } + reader = bytes.NewBuffer(content) + } + + req, err := http.NewRequestWithContext(ctx, method, u, nil) + if err != nil { + return nil, err + } + if reader != nil { + req.Header.Set("Content-Type", contentType) + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("X-Stainless-Retry-Count", "0") + req.Header.Set("X-Stainless-Timeout", "0") + for k, v := range getDefaultHeaders() { + req.Header.Add(k, v) + } + + for k, v := range getPlatformProperties() { + req.Header.Add(k, v) + } + cfg := RequestConfig{ + MaxRetries: 2, + Context: ctx, + Request: req, + HTTPClient: http.DefaultClient, + Body: reader, + } + cfg.ResponseBodyInto = dst + err = cfg.Apply(opts...) + if err != nil { + return nil, err + } + + // This must run after `cfg.Apply(...)` above in case the request timeout gets modified. We also only + // apply our own logic for it if it's still "0" from above. If it's not, then it was deleted or modified + // by the user and we should respect that. + if req.Header.Get("X-Stainless-Timeout") == "0" { + if cfg.RequestTimeout == time.Duration(0) { + req.Header.Del("X-Stainless-Timeout") + } else { + req.Header.Set("X-Stainless-Timeout", strconv.Itoa(int(cfg.RequestTimeout.Seconds()))) + } + } + + return &cfg, nil +} + +func UseDefaultParam[T any](dst *param.Field[T], src *T) { + if !dst.Present && src != nil { + dst.Value = *src + dst.Present = true + } +} + +// This interface is primarily used to describe an [*http.Client], but also +// supports custom HTTP implementations. +type HTTPDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// RequestConfig represents all the state related to one request. +// +// Editing the variables inside RequestConfig directly is unstable api. Prefer +// composing the RequestOption instead if possible. +type RequestConfig struct { + MaxRetries int + RequestTimeout time.Duration + Context context.Context + Request *http.Request + BaseURL *url.URL + // DefaultBaseURL will be used if BaseURL is not explicitly overridden using + // WithBaseURL. + DefaultBaseURL *url.URL + CustomHTTPDoer HTTPDoer + HTTPClient *http.Client + Middlewares []middleware + // If ResponseBodyInto not nil, then we will attempt to deserialize into + // ResponseBodyInto. If Destination is a []byte, then it will return the body as + // is. + ResponseBodyInto interface{} + // ResponseInto copies the \*http.Response of the corresponding request into the + // given address + ResponseInto **http.Response + Body io.Reader +} + +// middleware is exactly the same type as the Middleware type found in the [option] package, +// but it is redeclared here for circular dependency issues. +type middleware = func(*http.Request, middlewareNext) (*http.Response, error) + +// middlewareNext is exactly the same type as the MiddlewareNext type found in the [option] package, +// but it is redeclared here for circular dependency issues. +type middlewareNext = func(*http.Request) (*http.Response, error) + +func applyMiddleware(middleware middleware, next middlewareNext) middlewareNext { + return func(req *http.Request) (res *http.Response, err error) { + return middleware(req, next) + } +} + +func shouldRetry(req *http.Request, res *http.Response) bool { + // If there is no way to recover the Body, then we shouldn't retry. + if req.Body != nil && req.GetBody == nil { + return false + } + + // If there is no response, that indicates that there is a connection error + // so we retry the request. + if res == nil { + return true + } + + // If the header explicitly wants a retry behavior, respect that over the + // http status code. + if res.Header.Get("x-should-retry") == "true" { + return true + } + if res.Header.Get("x-should-retry") == "false" { + return false + } + + return res.StatusCode == http.StatusRequestTimeout || + res.StatusCode == http.StatusConflict || + res.StatusCode == http.StatusTooManyRequests || + res.StatusCode >= http.StatusInternalServerError +} + +func parseRetryAfterHeader(resp *http.Response) (time.Duration, bool) { + if resp == nil { + return 0, false + } + + type retryData struct { + header string + units time.Duration + + // custom is used when the regular algorithm failed and is optional. + // the returned duration is used verbatim (units is not applied). + custom func(string) (time.Duration, bool) + } + + nop := func(string) (time.Duration, bool) { return 0, false } + + // the headers are listed in order of preference + retries := []retryData{ + { + header: "Retry-After-Ms", + units: time.Millisecond, + custom: nop, + }, + { + header: "Retry-After", + units: time.Second, + + // retry-after values are expressed in either number of + // seconds or an HTTP-date indicating when to try again + custom: func(ra string) (time.Duration, bool) { + t, err := time.Parse(time.RFC1123, ra) + if err != nil { + return 0, false + } + return time.Until(t), true + }, + }, + } + + for _, retry := range retries { + v := resp.Header.Get(retry.header) + if v == "" { + continue + } + if retryAfter, err := strconv.ParseFloat(v, 64); err == nil { + return time.Duration(retryAfter * float64(retry.units)), true + } + if d, ok := retry.custom(v); ok { + return d, true + } + } + + return 0, false +} + +// isBeforeContextDeadline reports whether the non-zero Time t is +// before ctx's deadline. If ctx does not have a deadline, it +// always reports true (the deadline is considered infinite). +func isBeforeContextDeadline(t time.Time, ctx context.Context) bool { + d, ok := ctx.Deadline() + if !ok { + return true + } + return t.Before(d) +} + +// bodyWithTimeout is an io.ReadCloser which can observe a context's cancel func +// to handle timeouts etc. It wraps an existing io.ReadCloser. +type bodyWithTimeout struct { + stop func() // stops the time.Timer waiting to cancel the request + rc io.ReadCloser +} + +func (b *bodyWithTimeout) Read(p []byte) (n int, err error) { + n, err = b.rc.Read(p) + if err == nil { + return n, nil + } + if err == io.EOF { + return n, err + } + return n, err +} + +func (b *bodyWithTimeout) Close() error { + err := b.rc.Close() + b.stop() + return err +} + +func retryDelay(res *http.Response, retryCount int) time.Duration { + // If the API asks us to wait a certain amount of time (and it's a reasonable amount), + // just do what it says. + + if retryAfterDelay, ok := parseRetryAfterHeader(res); ok && 0 <= retryAfterDelay && retryAfterDelay < time.Minute { + return retryAfterDelay + } + + maxDelay := 8 * time.Second + delay := time.Duration(0.5 * float64(time.Second) * math.Pow(2, float64(retryCount))) + if delay > maxDelay { + delay = maxDelay + } + + jitter := rand.Int63n(int64(delay / 4)) + delay -= time.Duration(jitter) + return delay +} + +func (cfg *RequestConfig) Execute() (err error) { + if cfg.BaseURL == nil { + if cfg.DefaultBaseURL != nil { + cfg.BaseURL = cfg.DefaultBaseURL + } else { + return fmt.Errorf("requestconfig: base url is not set") + } + } + + cfg.Request.URL, err = cfg.BaseURL.Parse(strings.TrimLeft(cfg.Request.URL.String(), "/")) + if err != nil { + return err + } + + if cfg.Body != nil && cfg.Request.Body == nil { + switch body := cfg.Body.(type) { + case *bytes.Buffer: + b := body.Bytes() + cfg.Request.ContentLength = int64(body.Len()) + cfg.Request.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil } + cfg.Request.Body, _ = cfg.Request.GetBody() + case *bytes.Reader: + cfg.Request.ContentLength = int64(body.Len()) + cfg.Request.GetBody = func() (io.ReadCloser, error) { + _, err := body.Seek(0, 0) + return io.NopCloser(body), err + } + cfg.Request.Body, _ = cfg.Request.GetBody() + default: + if rc, ok := body.(io.ReadCloser); ok { + cfg.Request.Body = rc + } else { + cfg.Request.Body = io.NopCloser(body) + } + } + } + + handler := cfg.HTTPClient.Do + if cfg.CustomHTTPDoer != nil { + handler = cfg.CustomHTTPDoer.Do + } + for i := len(cfg.Middlewares) - 1; i >= 0; i -= 1 { + handler = applyMiddleware(cfg.Middlewares[i], handler) + } + + // Don't send the current retry count in the headers if the caller modified the header defaults. + shouldSendRetryCount := cfg.Request.Header.Get("X-Stainless-Retry-Count") == "0" + + var res *http.Response + var cancel context.CancelFunc + for retryCount := 0; retryCount <= cfg.MaxRetries; retryCount += 1 { + ctx := cfg.Request.Context() + if cfg.RequestTimeout != time.Duration(0) && isBeforeContextDeadline(time.Now().Add(cfg.RequestTimeout), ctx) { + ctx, cancel = context.WithTimeout(ctx, cfg.RequestTimeout) + defer func() { + // The cancel function is nil if it was handed off to be handled in a different scope. + if cancel != nil { + cancel() + } + }() + } + + req := cfg.Request.Clone(ctx) + if shouldSendRetryCount { + req.Header.Set("X-Stainless-Retry-Count", strconv.Itoa(retryCount)) + } + + res, err = handler(req) + if ctx != nil && ctx.Err() != nil { + return ctx.Err() + } + if !shouldRetry(cfg.Request, res) || retryCount >= cfg.MaxRetries { + break + } + + // Prepare next request and wait for the retry delay + if cfg.Request.GetBody != nil { + cfg.Request.Body, err = cfg.Request.GetBody() + if err != nil { + return err + } + } + + // Can't actually refresh the body, so we don't attempt to retry here + if cfg.Request.GetBody == nil && cfg.Request.Body != nil { + break + } + + time.Sleep(retryDelay(res, retryCount)) + } + + // Save *http.Response if it is requested to, even if there was an error making the request. This is + // useful in cases where you might want to debug by inspecting the response. Note that if err != nil, + // the response should be generally be empty, but there are edge cases. + if cfg.ResponseInto != nil { + *cfg.ResponseInto = res + } + if responseBodyInto, ok := cfg.ResponseBodyInto.(**http.Response); ok { + *responseBodyInto = res + } + + // If there was a connection error in the final request or any other transport error, + // return that early without trying to coerce into an APIError. + if err != nil { + return err + } + + if res.StatusCode >= 400 { + contents, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + return err + } + + // If there is an APIError, re-populate the response body so that debugging + // utilities can conveniently dump the response without issue. + res.Body = io.NopCloser(bytes.NewBuffer(contents)) + + // Load the contents into the error format if it is provided. + aerr := apierror.Error{Request: cfg.Request, Response: res, StatusCode: res.StatusCode} + err = aerr.UnmarshalJSON(contents) + if err != nil { + return err + } + return &aerr + } + + _, intoCustomResponseBody := cfg.ResponseBodyInto.(**http.Response) + if cfg.ResponseBodyInto == nil || intoCustomResponseBody { + // We aren't reading the response body in this scope, but whoever is will need the + // cancel func from the context to observe request timeouts. + // Put the cancel function in the response body so it can be handled elsewhere. + if cancel != nil { + res.Body = &bodyWithTimeout{rc: res.Body, stop: cancel} + cancel = nil + } + return nil + } + + contents, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + return fmt.Errorf("error reading response body: %w", err) + } + + // If we are not json, return plaintext + contentType := res.Header.Get("content-type") + mediaType, _, _ := mime.ParseMediaType(contentType) + isJSON := strings.Contains(mediaType, "application/json") || strings.HasSuffix(mediaType, "+json") + if !isJSON { + switch dst := cfg.ResponseBodyInto.(type) { + case *string: + *dst = string(contents) + case **string: + tmp := string(contents) + *dst = &tmp + case *[]byte: + *dst = contents + default: + return fmt.Errorf("expected destination type of 'string' or '[]byte' for responses with content-type '%s' that is not 'application/json'", contentType) + } + return nil + } + + switch dst := cfg.ResponseBodyInto.(type) { + // If the response happens to be a byte array, deserialize the body as-is. + case *[]byte: + *dst = contents + default: + err = json.NewDecoder(bytes.NewReader(contents)).Decode(cfg.ResponseBodyInto) + if err != nil { + return fmt.Errorf("error parsing response json: %w", err) + } + } + + return nil +} + +func ExecuteNewRequest(ctx context.Context, method string, u string, body interface{}, dst interface{}, opts ...RequestOption) error { + cfg, err := NewRequestConfig(ctx, method, u, body, dst, opts...) + if err != nil { + return err + } + return cfg.Execute() +} + +func (cfg *RequestConfig) Clone(ctx context.Context) *RequestConfig { + if cfg == nil { + return nil + } + req := cfg.Request.Clone(ctx) + var err error + if req.Body != nil { + req.Body, err = req.GetBody() + } + if err != nil { + return nil + } + new := &RequestConfig{ + MaxRetries: cfg.MaxRetries, + RequestTimeout: cfg.RequestTimeout, + Context: ctx, + Request: req, + BaseURL: cfg.BaseURL, + HTTPClient: cfg.HTTPClient, + Middlewares: cfg.Middlewares, + } + + return new +} + +func (cfg *RequestConfig) Apply(opts ...RequestOption) error { + for _, opt := range opts { + err := opt.Apply(cfg) + if err != nil { + return err + } + } + return nil +} + +// PreRequestOptions is used to collect all the options which need to be known before +// a call to [RequestConfig.ExecuteNewRequest], such as path parameters +// or global defaults. +// PreRequestOptions will return a [RequestConfig] with the options applied. +// +// Only request option functions of type [PreRequestOptionFunc] are applied. +func PreRequestOptions(opts ...RequestOption) (RequestConfig, error) { + cfg := RequestConfig{} + for _, opt := range opts { + if opt, ok := opt.(PreRequestOptionFunc); ok { + err := opt.Apply(&cfg) + if err != nil { + return cfg, err + } + } + } + return cfg, nil +} + +// WithDefaultBaseURL returns a RequestOption that sets the client's default Base URL. +// This is always overridden by setting a base URL with WithBaseURL. +// WithBaseURL should be used instead of WithDefaultBaseURL except in internal code. +func WithDefaultBaseURL(baseURL string) RequestOption { + u, err := url.Parse(baseURL) + return RequestOptionFunc(func(r *RequestConfig) error { + if err != nil { + return err + } + r.DefaultBaseURL = u + return nil + }) +} diff --git a/packages/tui/sdk/internal/testutil/testutil.go b/packages/tui/sdk/internal/testutil/testutil.go new file mode 100644 index 00000000..826d266f --- /dev/null +++ b/packages/tui/sdk/internal/testutil/testutil.go @@ -0,0 +1,27 @@ +package testutil + +import ( + "net/http" + "os" + "strconv" + "testing" +) + +func CheckTestServer(t *testing.T, url string) bool { + if _, err := http.Get(url); err != nil { + const SKIP_MOCK_TESTS = "SKIP_MOCK_TESTS" + if str, ok := os.LookupEnv(SKIP_MOCK_TESTS); ok { + skip, err := strconv.ParseBool(str) + if err != nil { + 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") + 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) + return false + } + } + return true +} diff --git a/packages/tui/sdk/internal/version.go b/packages/tui/sdk/internal/version.go new file mode 100644 index 00000000..64dcebbb --- /dev/null +++ b/packages/tui/sdk/internal/version.go @@ -0,0 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package internal + +const PackageVersion = "0.1.0-alpha.8" // x-release-please-version diff --git a/packages/tui/sdk/lib/.keep b/packages/tui/sdk/lib/.keep new file mode 100644 index 00000000..5e2c99fd --- /dev/null +++ b/packages/tui/sdk/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/packages/tui/sdk/option/middleware.go b/packages/tui/sdk/option/middleware.go new file mode 100644 index 00000000..8ec9dd60 --- /dev/null +++ b/packages/tui/sdk/option/middleware.go @@ -0,0 +1,38 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package option + +import ( + "log" + "net/http" + "net/http/httputil" +) + +// WithDebugLog logs the HTTP request and response content. +// If the logger parameter is nil, it uses the default logger. +// +// WithDebugLog is for debugging and development purposes only. +// It should not be used in production code. The behavior and interface +// of WithDebugLog is not guaranteed to be stable. +func WithDebugLog(logger *log.Logger) RequestOption { + return WithMiddleware(func(req *http.Request, nxt MiddlewareNext) (*http.Response, error) { + if logger == nil { + logger = log.Default() + } + + if reqBytes, err := httputil.DumpRequest(req, true); err == nil { + logger.Printf("Request Content:\n%s\n", reqBytes) + } + + resp, err := nxt(req) + if err != nil { + return resp, err + } + + if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + logger.Printf("Response Content:\n%s\n", respBytes) + } + + return resp, err + }) +} diff --git a/packages/tui/sdk/option/requestoption.go b/packages/tui/sdk/option/requestoption.go new file mode 100644 index 00000000..313552e9 --- /dev/null +++ b/packages/tui/sdk/option/requestoption.go @@ -0,0 +1,266 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package option + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/tidwall/sjson" +) + +// RequestOption is an option for the requests made by the opencode API Client +// which can be supplied to clients, services, and methods. You can read more about this functional +// options pattern in our [README]. +// +// [README]: https://pkg.go.dev/github.com/sst/opencode-sdk-go#readme-requestoptions +type RequestOption = requestconfig.RequestOption + +// WithBaseURL returns a RequestOption that sets the BaseURL for the client. +// +// For security reasons, ensure that the base URL is trusted. +func WithBaseURL(base string) RequestOption { + u, err := url.Parse(base) + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + if err != nil { + return fmt.Errorf("requestoption: WithBaseURL failed to parse url %s\n", err) + } + + if u.Path != "" && !strings.HasSuffix(u.Path, "/") { + u.Path += "/" + } + r.BaseURL = u + return nil + }) +} + +// HTTPClient is primarily used to describe an [*http.Client], but also +// supports custom implementations. +// +// For bespoke implementations, prefer using an [*http.Client] with a +// custom transport. See [http.RoundTripper] for further information. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// WithHTTPClient returns a RequestOption that changes the underlying http client used to make this +// request, which by default is [http.DefaultClient]. +// +// For custom uses cases, it is recommended to provide an [*http.Client] with a custom +// [http.RoundTripper] as its transport, rather than directly implementing [HTTPClient]. +func WithHTTPClient(client HTTPClient) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + if client == nil { + return fmt.Errorf("requestoption: custom http client cannot be nil") + } + + if c, ok := client.(*http.Client); ok { + // Prefer the native client if possible. + r.HTTPClient = c + r.CustomHTTPDoer = nil + } else { + r.CustomHTTPDoer = client + } + + return nil + }) +} + +// MiddlewareNext is a function which is called by a middleware to pass an HTTP request +// to the next stage in the middleware chain. +type MiddlewareNext = func(*http.Request) (*http.Response, error) + +// Middleware is a function which intercepts HTTP requests, processing or modifying +// them, and then passing the request to the next middleware or handler +// in the chain by calling the provided MiddlewareNext function. +type Middleware = func(*http.Request, MiddlewareNext) (*http.Response, error) + +// WithMiddleware returns a RequestOption that applies the given middleware +// to the requests made. Each middleware will execute in the order they were given. +func WithMiddleware(middlewares ...Middleware) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + r.Middlewares = append(r.Middlewares, middlewares...) + return nil + }) +} + +// WithMaxRetries returns a RequestOption that sets the maximum number of retries that the client +// attempts to make. When given 0, the client only makes one request. By +// default, the client retries two times. +// +// WithMaxRetries panics when retries is negative. +func WithMaxRetries(retries int) RequestOption { + if retries < 0 { + panic("option: cannot have fewer than 0 retries") + } + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + r.MaxRetries = retries + return nil + }) +} + +// WithHeader returns a RequestOption that sets the header value to the associated key. It overwrites +// any value if there was one already present. +func WithHeader(key, value string) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + r.Request.Header.Set(key, value) + return nil + }) +} + +// WithHeaderAdd returns a RequestOption that adds the header value to the associated key. It appends +// onto any existing values. +func WithHeaderAdd(key, value string) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + r.Request.Header.Add(key, value) + return nil + }) +} + +// WithHeaderDel returns a RequestOption that deletes the header value(s) associated with the given key. +func WithHeaderDel(key string) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + r.Request.Header.Del(key) + return nil + }) +} + +// WithQuery returns a RequestOption that sets the query value to the associated key. It overwrites +// any value if there was one already present. +func WithQuery(key, value string) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + query := r.Request.URL.Query() + query.Set(key, value) + r.Request.URL.RawQuery = query.Encode() + return nil + }) +} + +// WithQueryAdd returns a RequestOption that adds the query value to the associated key. It appends +// onto any existing values. +func WithQueryAdd(key, value string) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + query := r.Request.URL.Query() + query.Add(key, value) + r.Request.URL.RawQuery = query.Encode() + return nil + }) +} + +// WithQueryDel returns a RequestOption that deletes the query value(s) associated with the key. +func WithQueryDel(key string) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + query := r.Request.URL.Query() + query.Del(key) + r.Request.URL.RawQuery = query.Encode() + return nil + }) +} + +// WithJSONSet returns a RequestOption that sets the body's JSON value associated with the key. +// The key accepts a string as defined by the [sjson format]. +// +// [sjson format]: https://github.com/tidwall/sjson +func WithJSONSet(key string, value interface{}) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) (err error) { + var b []byte + + if r.Body == nil { + b, err = sjson.SetBytes(nil, key, value) + if err != nil { + return err + } + } else if buffer, ok := r.Body.(*bytes.Buffer); ok { + b = buffer.Bytes() + b, err = sjson.SetBytes(b, key, value) + if err != nil { + return err + } + } else { + return fmt.Errorf("cannot use WithJSONSet on a body that is not serialized as *bytes.Buffer") + } + + r.Body = bytes.NewBuffer(b) + return nil + }) +} + +// WithJSONDel returns a RequestOption that deletes the body's JSON value associated with the key. +// The key accepts a string as defined by the [sjson format]. +// +// [sjson format]: https://github.com/tidwall/sjson +func WithJSONDel(key string) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) (err error) { + if buffer, ok := r.Body.(*bytes.Buffer); ok { + b := buffer.Bytes() + b, err = sjson.DeleteBytes(b, key) + if err != nil { + return err + } + r.Body = bytes.NewBuffer(b) + return nil + } + + return fmt.Errorf("cannot use WithJSONDel on a body that is not serialized as *bytes.Buffer") + }) +} + +// WithResponseBodyInto returns a RequestOption that overwrites the deserialization target with +// the given destination. If provided, we don't deserialize into the default struct. +func WithResponseBodyInto(dst any) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + r.ResponseBodyInto = dst + return nil + }) +} + +// WithResponseInto returns a RequestOption that copies the [*http.Response] into the given address. +func WithResponseInto(dst **http.Response) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + r.ResponseInto = dst + return nil + }) +} + +// WithRequestBody returns a RequestOption that provides a custom serialized body with the given +// content type. +// +// body accepts an io.Reader or raw []bytes. +func WithRequestBody(contentType string, body any) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + if reader, ok := body.(io.Reader); ok { + r.Body = reader + return r.Apply(WithHeader("Content-Type", contentType)) + } + + if b, ok := body.([]byte); ok { + r.Body = bytes.NewBuffer(b) + return r.Apply(WithHeader("Content-Type", contentType)) + } + + return fmt.Errorf("body must be a byte slice or implement io.Reader") + }) +} + +// WithRequestTimeout returns a RequestOption that sets the timeout for +// each request attempt. This should be smaller than the timeout defined in +// the context, which spans all retries. +func WithRequestTimeout(dur time.Duration) RequestOption { + return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { + r.RequestTimeout = dur + return nil + }) +} + +// WithEnvironmentProduction returns a RequestOption that sets the current +// environment to be the "production" environment. An environment specifies which base URL +// to use by default. +func WithEnvironmentProduction() RequestOption { + return requestconfig.WithDefaultBaseURL("http://localhost:54321/") +} diff --git a/packages/tui/sdk/packages/ssestream/ssestream.go b/packages/tui/sdk/packages/ssestream/ssestream.go new file mode 100644 index 00000000..81adbd69 --- /dev/null +++ b/packages/tui/sdk/packages/ssestream/ssestream.go @@ -0,0 +1,181 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package ssestream + +import ( + "bufio" + "bytes" + "encoding/json" + "io" + "net/http" + "strings" +) + +type Decoder interface { + Event() Event + Next() bool + Close() error + Err() error +} + +func NewDecoder(res *http.Response) Decoder { + if res == nil || res.Body == nil { + return nil + } + + var decoder Decoder + contentType := res.Header.Get("content-type") + if t, ok := decoderTypes[contentType]; ok { + decoder = t(res.Body) + } else { + scn := bufio.NewScanner(res.Body) + scn.Buffer(nil, bufio.MaxScanTokenSize<<4) + decoder = &eventStreamDecoder{rc: res.Body, scn: scn} + } + return decoder +} + +var decoderTypes = map[string](func(io.ReadCloser) Decoder){} + +func RegisterDecoder(contentType string, decoder func(io.ReadCloser) Decoder) { + decoderTypes[strings.ToLower(contentType)] = decoder +} + +type Event struct { + Type string + Data []byte +} + +// A base implementation of a Decoder for text/event-stream. +type eventStreamDecoder struct { + evt Event + rc io.ReadCloser + scn *bufio.Scanner + err error +} + +func (s *eventStreamDecoder) Next() bool { + if s.err != nil { + return false + } + + event := "" + data := bytes.NewBuffer(nil) + + for s.scn.Scan() { + txt := s.scn.Bytes() + + // Dispatch event on an empty line + if len(txt) == 0 { + s.evt = Event{ + Type: event, + Data: data.Bytes(), + } + return true + } + + // Split a string like "event: bar" into name="event" and value=" bar". + name, value, _ := bytes.Cut(txt, []byte(":")) + + // Consume an optional space after the colon if it exists. + if len(value) > 0 && value[0] == ' ' { + value = value[1:] + } + + switch string(name) { + case "": + // An empty line in the for ": something" is a comment and should be ignored. + continue + case "event": + event = string(value) + case "data": + _, s.err = data.Write(value) + if s.err != nil { + break + } + _, s.err = data.WriteRune('\n') + if s.err != nil { + break + } + } + } + + if s.scn.Err() != nil { + s.err = s.scn.Err() + } + + return false +} + +func (s *eventStreamDecoder) Event() Event { + return s.evt +} + +func (s *eventStreamDecoder) Close() error { + return s.rc.Close() +} + +func (s *eventStreamDecoder) Err() error { + return s.err +} + +type Stream[T any] struct { + decoder Decoder + cur T + err error +} + +func NewStream[T any](decoder Decoder, err error) *Stream[T] { + return &Stream[T]{ + decoder: decoder, + err: err, + } +} + +// Next returns false if the stream has ended or an error occurred. +// Call Stream.Current() to get the current value. +// Call Stream.Err() to get the error. +// +// for stream.Next() { +// data := stream.Current() +// } +// +// if stream.Err() != nil { +// ... +// } +func (s *Stream[T]) Next() bool { + if s.err != nil { + return false + } + + for s.decoder.Next() { + var nxt T + s.err = json.Unmarshal(s.decoder.Event().Data, &nxt) + if s.err != nil { + return false + } + s.cur = nxt + return true + } + + // decoder.Next() may be false because of an error + s.err = s.decoder.Err() + + return false +} + +func (s *Stream[T]) Current() T { + return s.cur +} + +func (s *Stream[T]) Err() error { + return s.err +} + +func (s *Stream[T]) Close() error { + if s.decoder == nil { + // already closed + return nil + } + return s.decoder.Close() +} diff --git a/packages/tui/sdk/release-please-config.json b/packages/tui/sdk/release-please-config.json new file mode 100644 index 00000000..a38198ec --- /dev/null +++ b/packages/tui/sdk/release-please-config.json @@ -0,0 +1,67 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "go", + "extra-files": [ + "internal/version.go", + "README.md" + ] +} \ No newline at end of file diff --git a/packages/tui/sdk/scripts/bootstrap b/packages/tui/sdk/scripts/bootstrap new file mode 100755 index 00000000..d6ac1654 --- /dev/null +++ b/packages/tui/sdk/scripts/bootstrap @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ]; then + brew bundle check >/dev/null 2>&1 || { + echo "==> Installing Homebrew dependencies…" + brew bundle + } +fi + +echo "==> Installing Go dependencies…" + +go mod tidy -e diff --git a/packages/tui/sdk/scripts/format b/packages/tui/sdk/scripts/format new file mode 100755 index 00000000..db2a3fa2 --- /dev/null +++ b/packages/tui/sdk/scripts/format @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running gofmt -s -w" +gofmt -s -w . diff --git a/packages/tui/sdk/scripts/lint b/packages/tui/sdk/scripts/lint new file mode 100755 index 00000000..fa7ba1f6 --- /dev/null +++ b/packages/tui/sdk/scripts/lint @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running Go build" +go build ./... diff --git a/packages/tui/sdk/scripts/mock b/packages/tui/sdk/scripts/mock new file mode 100755 index 00000000..d2814ae6 --- /dev/null +++ b/packages/tui/sdk/scripts/mock @@ -0,0 +1,41 @@ +#!/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.8.5 -- 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.8.5 -- prism mock "$URL" +fi diff --git a/packages/tui/sdk/scripts/test b/packages/tui/sdk/scripts/test new file mode 100755 index 00000000..efebceae --- /dev/null +++ b/packages/tui/sdk/scripts/test @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +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=@stoplight/prism-cli@~5.3.2 -- 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/packages/tui/sdk/session.go b/packages/tui/sdk/session.go new file mode 100644 index 00000000..e8216a1f --- /dev/null +++ b/packages/tui/sdk/session.go @@ -0,0 +1,1385 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "errors" + "fmt" + "net/http" + "reflect" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/param" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" + "github.com/sst/opencode-sdk-go/shared" + "github.com/tidwall/gjson" +) + +// SessionService contains methods and other services that help with interacting +// with the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewSessionService] method instead. +type SessionService struct { + Options []option.RequestOption +} + +// NewSessionService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewSessionService(opts ...option.RequestOption) (r *SessionService) { + r = &SessionService{} + r.Options = opts + return +} + +// Create a new session +func (r *SessionService) New(ctx context.Context, opts ...option.RequestOption) (res *Session, err error) { + opts = append(r.Options[:], opts...) + path := "session" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + return +} + +// List all sessions +func (r *SessionService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Session, err error) { + opts = append(r.Options[:], opts...) + path := "session" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// Delete a session and all its data +func (r *SessionService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) + return +} + +// Abort a session +func (r *SessionService) Abort(ctx context.Context, id string, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s/abort", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + return +} + +// Create and send a new message to a session +func (r *SessionService) Chat(ctx context.Context, id string, body SessionChatParams, opts ...option.RequestOption) (res *Message, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s/message", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// Analyze the app and create an AGENTS.md file +func (r *SessionService) Init(ctx context.Context, id string, body SessionInitParams, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s/init", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// List messages for a session +func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]Message, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s/message", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +// Share a session +func (r *SessionService) Share(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s/share", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + return +} + +// Summarize the session +func (r *SessionService) Summarize(ctx context.Context, id string, body SessionSummarizeParams, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s/summarize", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + +// Unshare the session +func (r *SessionService) Unshare(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s/share", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) + return +} + +type FilePart struct { + MediaType string `json:"mediaType,required"` + Type FilePartType `json:"type,required"` + URL string `json:"url,required"` + Filename string `json:"filename"` + JSON filePartJSON `json:"-"` +} + +// filePartJSON contains the JSON metadata for the struct [FilePart] +type filePartJSON struct { + MediaType apijson.Field + Type apijson.Field + URL apijson.Field + Filename apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *FilePart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r filePartJSON) RawJSON() string { + return r.raw +} + +func (r FilePart) implementsMessagePart() {} + +type FilePartType string + +const ( + FilePartTypeFile FilePartType = "file" +) + +func (r FilePartType) IsKnown() bool { + switch r { + case FilePartTypeFile: + return true + } + return false +} + +type FilePartParam struct { + MediaType param.Field[string] `json:"mediaType,required"` + Type param.Field[FilePartType] `json:"type,required"` + URL param.Field[string] `json:"url,required"` + Filename param.Field[string] `json:"filename"` +} + +func (r FilePartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r FilePartParam) implementsMessagePartUnionParam() {} + +type Message struct { + ID string `json:"id,required"` + Metadata MessageMetadata `json:"metadata,required"` + Parts []MessagePart `json:"parts,required"` + Role MessageRole `json:"role,required"` + JSON messageJSON `json:"-"` +} + +// messageJSON contains the JSON metadata for the struct [Message] +type messageJSON struct { + ID apijson.Field + Metadata apijson.Field + Parts apijson.Field + Role apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Message) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageJSON) RawJSON() string { + return r.raw +} + +type MessageMetadata struct { + SessionID string `json:"sessionID,required"` + Time MessageMetadataTime `json:"time,required"` + Tool map[string]MessageMetadataTool `json:"tool,required"` + Assistant MessageMetadataAssistant `json:"assistant"` + Error MessageMetadataError `json:"error"` + Snapshot string `json:"snapshot"` + JSON messageMetadataJSON `json:"-"` +} + +// messageMetadataJSON contains the JSON metadata for the struct [MessageMetadata] +type messageMetadataJSON struct { + SessionID apijson.Field + Time apijson.Field + Tool apijson.Field + Assistant apijson.Field + Error apijson.Field + Snapshot apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadata) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataTime struct { + Created float64 `json:"created,required"` + Completed float64 `json:"completed"` + JSON messageMetadataTimeJSON `json:"-"` +} + +// messageMetadataTimeJSON contains the JSON metadata for the struct +// [MessageMetadataTime] +type messageMetadataTimeJSON struct { + Created apijson.Field + Completed apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataTimeJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataTool struct { + Time MessageMetadataToolTime `json:"time,required"` + Title string `json:"title,required"` + Snapshot string `json:"snapshot"` + ExtraFields map[string]interface{} `json:"-,extras"` + JSON messageMetadataToolJSON `json:"-"` +} + +// messageMetadataToolJSON contains the JSON metadata for the struct +// [MessageMetadataTool] +type messageMetadataToolJSON struct { + Time apijson.Field + Title apijson.Field + Snapshot apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataTool) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataToolJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataToolTime struct { + End float64 `json:"end,required"` + Start float64 `json:"start,required"` + JSON messageMetadataToolTimeJSON `json:"-"` +} + +// messageMetadataToolTimeJSON contains the JSON metadata for the struct +// [MessageMetadataToolTime] +type messageMetadataToolTimeJSON struct { + End apijson.Field + Start apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataToolTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataToolTimeJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataAssistant struct { + Cost float64 `json:"cost,required"` + ModelID string `json:"modelID,required"` + Path MessageMetadataAssistantPath `json:"path,required"` + ProviderID string `json:"providerID,required"` + System []string `json:"system,required"` + Tokens MessageMetadataAssistantTokens `json:"tokens,required"` + Summary bool `json:"summary"` + JSON messageMetadataAssistantJSON `json:"-"` +} + +// messageMetadataAssistantJSON contains the JSON metadata for the struct +// [MessageMetadataAssistant] +type messageMetadataAssistantJSON struct { + Cost apijson.Field + ModelID apijson.Field + Path apijson.Field + ProviderID apijson.Field + System apijson.Field + Tokens apijson.Field + Summary apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataAssistant) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataAssistantJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataAssistantPath struct { + Cwd string `json:"cwd,required"` + Root string `json:"root,required"` + JSON messageMetadataAssistantPathJSON `json:"-"` +} + +// messageMetadataAssistantPathJSON contains the JSON metadata for the struct +// [MessageMetadataAssistantPath] +type messageMetadataAssistantPathJSON struct { + Cwd apijson.Field + Root apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataAssistantPath) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataAssistantPathJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataAssistantTokens struct { + Cache MessageMetadataAssistantTokensCache `json:"cache,required"` + Input float64 `json:"input,required"` + Output float64 `json:"output,required"` + Reasoning float64 `json:"reasoning,required"` + JSON messageMetadataAssistantTokensJSON `json:"-"` +} + +// messageMetadataAssistantTokensJSON contains the JSON metadata for the struct +// [MessageMetadataAssistantTokens] +type messageMetadataAssistantTokensJSON struct { + Cache apijson.Field + Input apijson.Field + Output apijson.Field + Reasoning apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataAssistantTokens) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataAssistantTokensJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataAssistantTokensCache struct { + Read float64 `json:"read,required"` + Write float64 `json:"write,required"` + JSON messageMetadataAssistantTokensCacheJSON `json:"-"` +} + +// messageMetadataAssistantTokensCacheJSON contains the JSON metadata for the +// struct [MessageMetadataAssistantTokensCache] +type messageMetadataAssistantTokensCacheJSON struct { + Read apijson.Field + Write apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataAssistantTokensCache) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataAssistantTokensCacheJSON) RawJSON() string { + return r.raw +} + +type MessageMetadataError struct { + // This field can have the runtime type of [shared.ProviderAuthErrorData], + // [shared.UnknownErrorData], [interface{}]. + Data interface{} `json:"data,required"` + Name MessageMetadataErrorName `json:"name,required"` + JSON messageMetadataErrorJSON `json:"-"` + union MessageMetadataErrorUnion +} + +// messageMetadataErrorJSON contains the JSON metadata for the struct +// [MessageMetadataError] +type messageMetadataErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r messageMetadataErrorJSON) RawJSON() string { + return r.raw +} + +func (r *MessageMetadataError) UnmarshalJSON(data []byte) (err error) { + *r = MessageMetadataError{} + err = apijson.UnmarshalRoot(data, &r.union) + if err != nil { + return err + } + return apijson.Port(r.union, &r) +} + +// AsUnion returns a [MessageMetadataErrorUnion] interface which you can cast to +// the specific types for more type safety. +// +// Possible runtime types of the union are [shared.ProviderAuthError], +// [shared.UnknownError], [MessageMetadataErrorMessageOutputLengthError]. +func (r MessageMetadataError) AsUnion() MessageMetadataErrorUnion { + return r.union +} + +// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError] or +// [MessageMetadataErrorMessageOutputLengthError]. +type MessageMetadataErrorUnion interface { + ImplementsMessageMetadataError() +} + +func init() { + apijson.RegisterUnion( + reflect.TypeOf((*MessageMetadataErrorUnion)(nil)).Elem(), + "name", + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(shared.ProviderAuthError{}), + DiscriminatorValue: "ProviderAuthError", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(shared.UnknownError{}), + DiscriminatorValue: "UnknownError", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(MessageMetadataErrorMessageOutputLengthError{}), + DiscriminatorValue: "MessageOutputLengthError", + }, + ) +} + +type MessageMetadataErrorMessageOutputLengthError struct { + Data interface{} `json:"data,required"` + Name MessageMetadataErrorMessageOutputLengthErrorName `json:"name,required"` + JSON messageMetadataErrorMessageOutputLengthErrorJSON `json:"-"` +} + +// messageMetadataErrorMessageOutputLengthErrorJSON contains the JSON metadata for +// the struct [MessageMetadataErrorMessageOutputLengthError] +type messageMetadataErrorMessageOutputLengthErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *MessageMetadataErrorMessageOutputLengthError) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r messageMetadataErrorMessageOutputLengthErrorJSON) RawJSON() string { + return r.raw +} + +func (r MessageMetadataErrorMessageOutputLengthError) ImplementsMessageMetadataError() {} + +type MessageMetadataErrorMessageOutputLengthErrorName string + +const ( + MessageMetadataErrorMessageOutputLengthErrorNameMessageOutputLengthError MessageMetadataErrorMessageOutputLengthErrorName = "MessageOutputLengthError" +) + +func (r MessageMetadataErrorMessageOutputLengthErrorName) IsKnown() bool { + switch r { + case MessageMetadataErrorMessageOutputLengthErrorNameMessageOutputLengthError: + return true + } + return false +} + +type MessageMetadataErrorName string + +const ( + MessageMetadataErrorNameProviderAuthError MessageMetadataErrorName = "ProviderAuthError" + MessageMetadataErrorNameUnknownError MessageMetadataErrorName = "UnknownError" + MessageMetadataErrorNameMessageOutputLengthError MessageMetadataErrorName = "MessageOutputLengthError" +) + +func (r MessageMetadataErrorName) IsKnown() bool { + switch r { + case MessageMetadataErrorNameProviderAuthError, MessageMetadataErrorNameUnknownError, MessageMetadataErrorNameMessageOutputLengthError: + return true + } + return false +} + +type MessageRole string + +const ( + MessageRoleUser MessageRole = "user" + MessageRoleAssistant MessageRole = "assistant" +) + +func (r MessageRole) IsKnown() bool { + switch r { + case MessageRoleUser, MessageRoleAssistant: + return true + } + return false +} + +type MessagePart struct { + Type MessagePartType `json:"type,required"` + Filename string `json:"filename"` + MediaType string `json:"mediaType"` + // This field can have the runtime type of [map[string]interface{}]. + ProviderMetadata interface{} `json:"providerMetadata"` + SourceID string `json:"sourceId"` + Text string `json:"text"` + Title string `json:"title"` + // This field can have the runtime type of [ToolInvocationPartToolInvocation]. + ToolInvocation interface{} `json:"toolInvocation"` + URL string `json:"url"` + JSON messagePartJSON `json:"-"` + union MessagePartUnion +} + +// messagePartJSON contains the JSON metadata for the struct [MessagePart] +type messagePartJSON struct { + Type apijson.Field + Filename apijson.Field + MediaType apijson.Field + ProviderMetadata apijson.Field + SourceID apijson.Field + Text apijson.Field + Title apijson.Field + ToolInvocation apijson.Field + URL apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r messagePartJSON) RawJSON() string { + return r.raw +} + +func (r *MessagePart) UnmarshalJSON(data []byte) (err error) { + *r = MessagePart{} + err = apijson.UnmarshalRoot(data, &r.union) + if err != nil { + return err + } + return apijson.Port(r.union, &r) +} + +// AsUnion returns a [MessagePartUnion] interface which you can cast to the +// specific types for more type safety. +// +// Possible runtime types of the union are [TextPart], [ReasoningPart], +// [ToolInvocationPart], [SourceURLPart], [FilePart], [StepStartPart]. +func (r MessagePart) AsUnion() MessagePartUnion { + return r.union +} + +// Union satisfied by [TextPart], [ReasoningPart], [ToolInvocationPart], +// [SourceURLPart], [FilePart] or [StepStartPart]. +type MessagePartUnion interface { + implementsMessagePart() +} + +func init() { + apijson.RegisterUnion( + reflect.TypeOf((*MessagePartUnion)(nil)).Elem(), + "type", + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(TextPart{}), + DiscriminatorValue: "text", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ReasoningPart{}), + DiscriminatorValue: "reasoning", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ToolInvocationPart{}), + DiscriminatorValue: "tool-invocation", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(SourceURLPart{}), + DiscriminatorValue: "source-url", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(FilePart{}), + DiscriminatorValue: "file", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(StepStartPart{}), + DiscriminatorValue: "step-start", + }, + ) +} + +type MessagePartType string + +const ( + MessagePartTypeText MessagePartType = "text" + MessagePartTypeReasoning MessagePartType = "reasoning" + MessagePartTypeToolInvocation MessagePartType = "tool-invocation" + MessagePartTypeSourceURL MessagePartType = "source-url" + MessagePartTypeFile MessagePartType = "file" + MessagePartTypeStepStart MessagePartType = "step-start" +) + +func (r MessagePartType) IsKnown() bool { + switch r { + case MessagePartTypeText, MessagePartTypeReasoning, MessagePartTypeToolInvocation, MessagePartTypeSourceURL, MessagePartTypeFile, MessagePartTypeStepStart: + return true + } + return false +} + +type MessagePartParam struct { + Type param.Field[MessagePartType] `json:"type,required"` + Filename param.Field[string] `json:"filename"` + MediaType param.Field[string] `json:"mediaType"` + ProviderMetadata param.Field[interface{}] `json:"providerMetadata"` + SourceID param.Field[string] `json:"sourceId"` + Text param.Field[string] `json:"text"` + Title param.Field[string] `json:"title"` + ToolInvocation param.Field[interface{}] `json:"toolInvocation"` + URL param.Field[string] `json:"url"` +} + +func (r MessagePartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r MessagePartParam) implementsMessagePartUnionParam() {} + +// Satisfied by [TextPartParam], [ReasoningPartParam], [ToolInvocationPartParam], +// [SourceURLPartParam], [FilePartParam], [StepStartPartParam], [MessagePartParam]. +type MessagePartUnionParam interface { + implementsMessagePartUnionParam() +} + +type ReasoningPart struct { + Text string `json:"text,required"` + Type ReasoningPartType `json:"type,required"` + ProviderMetadata map[string]interface{} `json:"providerMetadata"` + JSON reasoningPartJSON `json:"-"` +} + +// reasoningPartJSON contains the JSON metadata for the struct [ReasoningPart] +type reasoningPartJSON struct { + Text apijson.Field + Type apijson.Field + ProviderMetadata apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ReasoningPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r reasoningPartJSON) RawJSON() string { + return r.raw +} + +func (r ReasoningPart) implementsMessagePart() {} + +type ReasoningPartType string + +const ( + ReasoningPartTypeReasoning ReasoningPartType = "reasoning" +) + +func (r ReasoningPartType) IsKnown() bool { + switch r { + case ReasoningPartTypeReasoning: + return true + } + return false +} + +type ReasoningPartParam struct { + Text param.Field[string] `json:"text,required"` + Type param.Field[ReasoningPartType] `json:"type,required"` + ProviderMetadata param.Field[map[string]interface{}] `json:"providerMetadata"` +} + +func (r ReasoningPartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ReasoningPartParam) implementsMessagePartUnionParam() {} + +type Session struct { + ID string `json:"id,required"` + Time SessionTime `json:"time,required"` + Title string `json:"title,required"` + Version string `json:"version,required"` + ParentID string `json:"parentID"` + Revert SessionRevert `json:"revert"` + Share SessionShare `json:"share"` + JSON sessionJSON `json:"-"` +} + +// sessionJSON contains the JSON metadata for the struct [Session] +type sessionJSON struct { + ID apijson.Field + Time apijson.Field + Title apijson.Field + Version apijson.Field + ParentID apijson.Field + Revert apijson.Field + Share apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Session) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionJSON) RawJSON() string { + return r.raw +} + +type SessionTime struct { + Created float64 `json:"created,required"` + Updated float64 `json:"updated,required"` + JSON sessionTimeJSON `json:"-"` +} + +// sessionTimeJSON contains the JSON metadata for the struct [SessionTime] +type sessionTimeJSON struct { + Created apijson.Field + Updated apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SessionTime) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionTimeJSON) RawJSON() string { + return r.raw +} + +type SessionRevert struct { + MessageID string `json:"messageID,required"` + Part float64 `json:"part,required"` + Snapshot string `json:"snapshot"` + JSON sessionRevertJSON `json:"-"` +} + +// sessionRevertJSON contains the JSON metadata for the struct [SessionRevert] +type sessionRevertJSON struct { + MessageID apijson.Field + Part apijson.Field + Snapshot apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SessionRevert) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionRevertJSON) RawJSON() string { + return r.raw +} + +type SessionShare struct { + URL string `json:"url,required"` + JSON sessionShareJSON `json:"-"` +} + +// sessionShareJSON contains the JSON metadata for the struct [SessionShare] +type sessionShareJSON struct { + URL apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SessionShare) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionShareJSON) RawJSON() string { + return r.raw +} + +type SourceURLPart struct { + SourceID string `json:"sourceId,required"` + Type SourceURLPartType `json:"type,required"` + URL string `json:"url,required"` + ProviderMetadata map[string]interface{} `json:"providerMetadata"` + Title string `json:"title"` + JSON sourceURLPartJSON `json:"-"` +} + +// sourceURLPartJSON contains the JSON metadata for the struct [SourceURLPart] +type sourceURLPartJSON struct { + SourceID apijson.Field + Type apijson.Field + URL apijson.Field + ProviderMetadata apijson.Field + Title apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SourceURLPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sourceURLPartJSON) RawJSON() string { + return r.raw +} + +func (r SourceURLPart) implementsMessagePart() {} + +type SourceURLPartType string + +const ( + SourceURLPartTypeSourceURL SourceURLPartType = "source-url" +) + +func (r SourceURLPartType) IsKnown() bool { + switch r { + case SourceURLPartTypeSourceURL: + return true + } + return false +} + +type SourceURLPartParam struct { + SourceID param.Field[string] `json:"sourceId,required"` + Type param.Field[SourceURLPartType] `json:"type,required"` + URL param.Field[string] `json:"url,required"` + ProviderMetadata param.Field[map[string]interface{}] `json:"providerMetadata"` + Title param.Field[string] `json:"title"` +} + +func (r SourceURLPartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r SourceURLPartParam) implementsMessagePartUnionParam() {} + +type StepStartPart struct { + Type StepStartPartType `json:"type,required"` + JSON stepStartPartJSON `json:"-"` +} + +// stepStartPartJSON contains the JSON metadata for the struct [StepStartPart] +type stepStartPartJSON struct { + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *StepStartPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r stepStartPartJSON) RawJSON() string { + return r.raw +} + +func (r StepStartPart) implementsMessagePart() {} + +type StepStartPartType string + +const ( + StepStartPartTypeStepStart StepStartPartType = "step-start" +) + +func (r StepStartPartType) IsKnown() bool { + switch r { + case StepStartPartTypeStepStart: + return true + } + return false +} + +type StepStartPartParam struct { + Type param.Field[StepStartPartType] `json:"type,required"` +} + +func (r StepStartPartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r StepStartPartParam) implementsMessagePartUnionParam() {} + +type TextPart struct { + Text string `json:"text,required"` + Type TextPartType `json:"type,required"` + JSON textPartJSON `json:"-"` +} + +// textPartJSON contains the JSON metadata for the struct [TextPart] +type textPartJSON struct { + Text apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *TextPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r textPartJSON) RawJSON() string { + return r.raw +} + +func (r TextPart) implementsMessagePart() {} + +type TextPartType string + +const ( + TextPartTypeText TextPartType = "text" +) + +func (r TextPartType) IsKnown() bool { + switch r { + case TextPartTypeText: + return true + } + return false +} + +type TextPartParam struct { + Text param.Field[string] `json:"text,required"` + Type param.Field[TextPartType] `json:"type,required"` +} + +func (r TextPartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r TextPartParam) implementsMessagePartUnionParam() {} + +type ToolCall struct { + State ToolCallState `json:"state,required"` + ToolCallID string `json:"toolCallId,required"` + ToolName string `json:"toolName,required"` + Args interface{} `json:"args"` + Step float64 `json:"step"` + JSON toolCallJSON `json:"-"` +} + +// toolCallJSON contains the JSON metadata for the struct [ToolCall] +type toolCallJSON struct { + State apijson.Field + ToolCallID apijson.Field + ToolName apijson.Field + Args apijson.Field + Step apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ToolCall) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r toolCallJSON) RawJSON() string { + return r.raw +} + +func (r ToolCall) implementsToolInvocationPartToolInvocation() {} + +type ToolCallState string + +const ( + ToolCallStateCall ToolCallState = "call" +) + +func (r ToolCallState) IsKnown() bool { + switch r { + case ToolCallStateCall: + return true + } + return false +} + +type ToolCallParam struct { + State param.Field[ToolCallState] `json:"state,required"` + ToolCallID param.Field[string] `json:"toolCallId,required"` + ToolName param.Field[string] `json:"toolName,required"` + Args param.Field[interface{}] `json:"args"` + Step param.Field[float64] `json:"step"` +} + +func (r ToolCallParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolCallParam) implementsToolInvocationPartToolInvocationUnionParam() {} + +type ToolInvocationPart struct { + ToolInvocation ToolInvocationPartToolInvocation `json:"toolInvocation,required"` + Type ToolInvocationPartType `json:"type,required"` + JSON toolInvocationPartJSON `json:"-"` +} + +// toolInvocationPartJSON contains the JSON metadata for the struct +// [ToolInvocationPart] +type toolInvocationPartJSON struct { + ToolInvocation apijson.Field + Type apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ToolInvocationPart) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r toolInvocationPartJSON) RawJSON() string { + return r.raw +} + +func (r ToolInvocationPart) implementsMessagePart() {} + +type ToolInvocationPartToolInvocation struct { + State ToolInvocationPartToolInvocationState `json:"state,required"` + ToolCallID string `json:"toolCallId,required"` + ToolName string `json:"toolName,required"` + // This field can have the runtime type of [interface{}]. + Args interface{} `json:"args"` + Result string `json:"result"` + Step float64 `json:"step"` + JSON toolInvocationPartToolInvocationJSON `json:"-"` + union ToolInvocationPartToolInvocationUnion +} + +// toolInvocationPartToolInvocationJSON contains the JSON metadata for the struct +// [ToolInvocationPartToolInvocation] +type toolInvocationPartToolInvocationJSON struct { + State apijson.Field + ToolCallID apijson.Field + ToolName apijson.Field + Args apijson.Field + Result apijson.Field + Step apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r toolInvocationPartToolInvocationJSON) RawJSON() string { + return r.raw +} + +func (r *ToolInvocationPartToolInvocation) UnmarshalJSON(data []byte) (err error) { + *r = ToolInvocationPartToolInvocation{} + err = apijson.UnmarshalRoot(data, &r.union) + if err != nil { + return err + } + return apijson.Port(r.union, &r) +} + +// AsUnion returns a [ToolInvocationPartToolInvocationUnion] interface which you +// can cast to the specific types for more type safety. +// +// Possible runtime types of the union are [ToolCall], [ToolPartialCall], +// [ToolResult]. +func (r ToolInvocationPartToolInvocation) AsUnion() ToolInvocationPartToolInvocationUnion { + return r.union +} + +// Union satisfied by [ToolCall], [ToolPartialCall] or [ToolResult]. +type ToolInvocationPartToolInvocationUnion interface { + implementsToolInvocationPartToolInvocation() +} + +func init() { + apijson.RegisterUnion( + reflect.TypeOf((*ToolInvocationPartToolInvocationUnion)(nil)).Elem(), + "state", + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ToolCall{}), + DiscriminatorValue: "call", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ToolPartialCall{}), + DiscriminatorValue: "partial-call", + }, + apijson.UnionVariant{ + TypeFilter: gjson.JSON, + Type: reflect.TypeOf(ToolResult{}), + DiscriminatorValue: "result", + }, + ) +} + +type ToolInvocationPartToolInvocationState string + +const ( + ToolInvocationPartToolInvocationStateCall ToolInvocationPartToolInvocationState = "call" + ToolInvocationPartToolInvocationStatePartialCall ToolInvocationPartToolInvocationState = "partial-call" + ToolInvocationPartToolInvocationStateResult ToolInvocationPartToolInvocationState = "result" +) + +func (r ToolInvocationPartToolInvocationState) IsKnown() bool { + switch r { + case ToolInvocationPartToolInvocationStateCall, ToolInvocationPartToolInvocationStatePartialCall, ToolInvocationPartToolInvocationStateResult: + return true + } + return false +} + +type ToolInvocationPartType string + +const ( + ToolInvocationPartTypeToolInvocation ToolInvocationPartType = "tool-invocation" +) + +func (r ToolInvocationPartType) IsKnown() bool { + switch r { + case ToolInvocationPartTypeToolInvocation: + return true + } + return false +} + +type ToolInvocationPartParam struct { + ToolInvocation param.Field[ToolInvocationPartToolInvocationUnionParam] `json:"toolInvocation,required"` + Type param.Field[ToolInvocationPartType] `json:"type,required"` +} + +func (r ToolInvocationPartParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolInvocationPartParam) implementsMessagePartUnionParam() {} + +type ToolInvocationPartToolInvocationParam struct { + State param.Field[ToolInvocationPartToolInvocationState] `json:"state,required"` + ToolCallID param.Field[string] `json:"toolCallId,required"` + ToolName param.Field[string] `json:"toolName,required"` + Args param.Field[interface{}] `json:"args"` + Result param.Field[string] `json:"result"` + Step param.Field[float64] `json:"step"` +} + +func (r ToolInvocationPartToolInvocationParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolInvocationPartToolInvocationParam) implementsToolInvocationPartToolInvocationUnionParam() { +} + +// Satisfied by [ToolCallParam], [ToolPartialCallParam], [ToolResultParam], +// [ToolInvocationPartToolInvocationParam]. +type ToolInvocationPartToolInvocationUnionParam interface { + implementsToolInvocationPartToolInvocationUnionParam() +} + +type ToolPartialCall struct { + State ToolPartialCallState `json:"state,required"` + ToolCallID string `json:"toolCallId,required"` + ToolName string `json:"toolName,required"` + Args interface{} `json:"args"` + Step float64 `json:"step"` + JSON toolPartialCallJSON `json:"-"` +} + +// toolPartialCallJSON contains the JSON metadata for the struct [ToolPartialCall] +type toolPartialCallJSON struct { + State apijson.Field + ToolCallID apijson.Field + ToolName apijson.Field + Args apijson.Field + Step apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ToolPartialCall) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r toolPartialCallJSON) RawJSON() string { + return r.raw +} + +func (r ToolPartialCall) implementsToolInvocationPartToolInvocation() {} + +type ToolPartialCallState string + +const ( + ToolPartialCallStatePartialCall ToolPartialCallState = "partial-call" +) + +func (r ToolPartialCallState) IsKnown() bool { + switch r { + case ToolPartialCallStatePartialCall: + return true + } + return false +} + +type ToolPartialCallParam struct { + State param.Field[ToolPartialCallState] `json:"state,required"` + ToolCallID param.Field[string] `json:"toolCallId,required"` + ToolName param.Field[string] `json:"toolName,required"` + Args param.Field[interface{}] `json:"args"` + Step param.Field[float64] `json:"step"` +} + +func (r ToolPartialCallParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolPartialCallParam) implementsToolInvocationPartToolInvocationUnionParam() {} + +type ToolResult struct { + Result string `json:"result,required"` + State ToolResultState `json:"state,required"` + ToolCallID string `json:"toolCallId,required"` + ToolName string `json:"toolName,required"` + Args interface{} `json:"args"` + Step float64 `json:"step"` + JSON toolResultJSON `json:"-"` +} + +// toolResultJSON contains the JSON metadata for the struct [ToolResult] +type toolResultJSON struct { + Result apijson.Field + State apijson.Field + ToolCallID apijson.Field + ToolName apijson.Field + Args apijson.Field + Step apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ToolResult) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r toolResultJSON) RawJSON() string { + return r.raw +} + +func (r ToolResult) implementsToolInvocationPartToolInvocation() {} + +type ToolResultState string + +const ( + ToolResultStateResult ToolResultState = "result" +) + +func (r ToolResultState) IsKnown() bool { + switch r { + case ToolResultStateResult: + return true + } + return false +} + +type ToolResultParam struct { + Result param.Field[string] `json:"result,required"` + State param.Field[ToolResultState] `json:"state,required"` + ToolCallID param.Field[string] `json:"toolCallId,required"` + ToolName param.Field[string] `json:"toolName,required"` + Args param.Field[interface{}] `json:"args"` + Step param.Field[float64] `json:"step"` +} + +func (r ToolResultParam) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +func (r ToolResultParam) implementsToolInvocationPartToolInvocationUnionParam() {} + +type SessionChatParams struct { + ModelID param.Field[string] `json:"modelID,required"` + Parts param.Field[[]MessagePartUnionParam] `json:"parts,required"` + ProviderID param.Field[string] `json:"providerID,required"` +} + +func (r SessionChatParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +type SessionInitParams struct { + ModelID param.Field[string] `json:"modelID,required"` + ProviderID param.Field[string] `json:"providerID,required"` +} + +func (r SessionInitParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +type SessionSummarizeParams struct { + ModelID param.Field[string] `json:"modelID,required"` + ProviderID param.Field[string] `json:"providerID,required"` +} + +func (r SessionSummarizeParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} diff --git a/packages/tui/sdk/session_test.go b/packages/tui/sdk/session_test.go new file mode 100644 index 00000000..da9fb825 --- /dev/null +++ b/packages/tui/sdk/session_test.go @@ -0,0 +1,259 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestSessionNew(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.New(context.TODO()) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionList(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.List(context.TODO()) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionDelete(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Delete(context.TODO(), "id") + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionAbort(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Abort(context.TODO(), "id") + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionChat(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Chat( + context.TODO(), + "id", + opencode.SessionChatParams{ + ModelID: opencode.F("modelID"), + Parts: opencode.F([]opencode.MessagePartUnionParam{opencode.TextPartParam{ + Text: opencode.F("text"), + Type: opencode.F(opencode.TextPartTypeText), + }}), + ProviderID: opencode.F("providerID"), + }, + ) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionInit(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Init( + context.TODO(), + "id", + opencode.SessionInitParams{ + ModelID: opencode.F("modelID"), + ProviderID: opencode.F("providerID"), + }, + ) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionMessages(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Messages(context.TODO(), "id") + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionShare(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Share(context.TODO(), "id") + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionSummarize(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Summarize( + context.TODO(), + "id", + opencode.SessionSummarizeParams{ + ModelID: opencode.F("modelID"), + ProviderID: opencode.F("providerID"), + }, + ) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestSessionUnshare(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Unshare(context.TODO(), "id") + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/packages/tui/sdk/shared/shared.go b/packages/tui/sdk/shared/shared.go new file mode 100644 index 00000000..121f64a5 --- /dev/null +++ b/packages/tui/sdk/shared/shared.go @@ -0,0 +1,132 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package shared + +import ( + "github.com/sst/opencode-sdk-go/internal/apijson" +) + +type ProviderAuthError struct { + Data ProviderAuthErrorData `json:"data,required"` + Name ProviderAuthErrorName `json:"name,required"` + JSON providerAuthErrorJSON `json:"-"` +} + +// providerAuthErrorJSON contains the JSON metadata for the struct +// [ProviderAuthError] +type providerAuthErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ProviderAuthError) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r providerAuthErrorJSON) RawJSON() string { + return r.raw +} + +func (r ProviderAuthError) ImplementsEventListResponseEventSessionErrorPropertiesError() {} + +func (r ProviderAuthError) ImplementsMessageMetadataError() {} + +type ProviderAuthErrorData struct { + Message string `json:"message,required"` + ProviderID string `json:"providerID,required"` + JSON providerAuthErrorDataJSON `json:"-"` +} + +// providerAuthErrorDataJSON contains the JSON metadata for the struct +// [ProviderAuthErrorData] +type providerAuthErrorDataJSON struct { + Message apijson.Field + ProviderID apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ProviderAuthErrorData) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r providerAuthErrorDataJSON) RawJSON() string { + return r.raw +} + +type ProviderAuthErrorName string + +const ( + ProviderAuthErrorNameProviderAuthError ProviderAuthErrorName = "ProviderAuthError" +) + +func (r ProviderAuthErrorName) IsKnown() bool { + switch r { + case ProviderAuthErrorNameProviderAuthError: + return true + } + return false +} + +type UnknownError struct { + Data UnknownErrorData `json:"data,required"` + Name UnknownErrorName `json:"name,required"` + JSON unknownErrorJSON `json:"-"` +} + +// unknownErrorJSON contains the JSON metadata for the struct [UnknownError] +type unknownErrorJSON struct { + Data apijson.Field + Name apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *UnknownError) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r unknownErrorJSON) RawJSON() string { + return r.raw +} + +func (r UnknownError) ImplementsEventListResponseEventSessionErrorPropertiesError() {} + +func (r UnknownError) ImplementsMessageMetadataError() {} + +type UnknownErrorData struct { + Message string `json:"message,required"` + JSON unknownErrorDataJSON `json:"-"` +} + +// unknownErrorDataJSON contains the JSON metadata for the struct +// [UnknownErrorData] +type unknownErrorDataJSON struct { + Message apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *UnknownErrorData) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r unknownErrorDataJSON) RawJSON() string { + return r.raw +} + +type UnknownErrorName string + +const ( + UnknownErrorNameUnknownError UnknownErrorName = "UnknownError" +) + +func (r UnknownErrorName) IsKnown() bool { + switch r { + case UnknownErrorNameUnknownError: + return true + } + return false +} diff --git a/packages/tui/sdk/usage_test.go b/packages/tui/sdk/usage_test.go new file mode 100644 index 00000000..0e261a7a --- /dev/null +++ b/packages/tui/sdk/usage_test.go @@ -0,0 +1,32 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestUsage(t *testing.T) { + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + events, err := client.Event.List(context.TODO()) + if err != nil { + t.Error(err) + return + } + t.Logf("%+v\n", events) +} diff --git a/scripts/stainless b/scripts/stainless new file mode 100755 index 00000000..dae9016f --- /dev/null +++ b/scripts/stainless @@ -0,0 +1,26 @@ +#!/bin/bash + +set -e + +echo "Starting opencode server on port 4096..." +bun run ./packages/opencode/src/index.ts serve --port 4096 & +SERVER_PID=$! + +echo "Waiting for server to start..." +sleep 3 + +echo "Fetching OpenAPI spec from http://localhost:4096/doc..." +curl -s http://localhost:4096/doc > openapi.json + +echo "Stopping server..." +kill $SERVER_PID + +echo "Running stl builds create..." +stl builds create --branch dev --pull --allow-empty --targets go + +echo "Cleaning up..." +rm -rf packages/tui/sdk +mv opencode-go/ packages/tui/sdk/ +rm -rf packages/tui/sdk/.git + +echo "Done!" diff --git a/stainless-workspace.json b/stainless-workspace.json new file mode 100644 index 00000000..b4230b05 --- /dev/null +++ b/stainless-workspace.json @@ -0,0 +1,5 @@ +{ + "project": "opencode", + "openapi_spec": "openapi.json", + "stainless_config": "stainless.yml" +}