mirror of
https://github.com/sst/opencode.git
synced 2025-07-07 08:05:00 +00:00
chore: better local dev with stainless script
This commit is contained in:
parent
1dffabcfda
commit
5a0910ea79
70 changed files with 11281 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ node_modules
|
|||
.env
|
||||
.idea
|
||||
.vscode
|
||||
openapi.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": {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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=
|
||||
|
|
7
packages/tui/sdk/.devcontainer/devcontainer.json
Normal file
7
packages/tui/sdk/.devcontainer/devcontainer.json
Normal file
|
@ -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"
|
||||
}
|
49
packages/tui/sdk/.github/workflows/ci.yml
vendored
Normal file
49
packages/tui/sdk/.github/workflows/ci.yml
vendored
Normal file
|
@ -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
|
4
packages/tui/sdk/.gitignore
vendored
Normal file
4
packages/tui/sdk/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
.prism.log
|
||||
codegen.log
|
||||
Brewfile.lock.json
|
||||
.idea/
|
3
packages/tui/sdk/.release-please-manifest.json
Normal file
3
packages/tui/sdk/.release-please-manifest.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
".": "0.1.0-alpha.8"
|
||||
}
|
4
packages/tui/sdk/.stats.yml
Normal file
4
packages/tui/sdk/.stats.yml
Normal file
|
@ -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
|
1
packages/tui/sdk/Brewfile
Normal file
1
packages/tui/sdk/Brewfile
Normal file
|
@ -0,0 +1 @@
|
|||
brew "go"
|
73
packages/tui/sdk/CHANGELOG.md
Normal file
73
packages/tui/sdk/CHANGELOG.md
Normal file
|
@ -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))
|
66
packages/tui/sdk/CONTRIBUTING.md
Normal file
66
packages/tui/sdk/CONTRIBUTING.md
Normal file
|
@ -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/<your-example>/main.go
|
||||
|
||||
package main
|
||||
|
||||
func main() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
$ go run ./examples/<your-example>
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
201
packages/tui/sdk/LICENSE
Normal file
201
packages/tui/sdk/LICENSE
Normal file
|
@ -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.
|
354
packages/tui/sdk/README.md
Normal file
354
packages/tui/sdk/README.md
Normal file
|
@ -0,0 +1,354 @@
|
|||
# Opencode Go API Library
|
||||
|
||||
<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go"><img src="https://pkg.go.dev/badge/github.com/sst/opencode-sdk-go.svg" alt="Go Reference"></a>
|
||||
|
||||
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
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/sst/opencode-sdk-go" // imported as opencode
|
||||
)
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
Or to pin the version:
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```sh
|
||||
go get -u 'github.com/sst/opencode-sdk-go@v0.1.0-alpha.8'
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
## 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).
|
27
packages/tui/sdk/SECURITY.md
Normal file
27
packages/tui/sdk/SECURITY.md
Normal file
|
@ -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.
|
34
packages/tui/sdk/aliases.go
Normal file
34
packages/tui/sdk/aliases.go
Normal file
|
@ -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
|
110
packages/tui/sdk/api.md
Normal file
110
packages/tui/sdk/api.md
Normal file
|
@ -0,0 +1,110 @@
|
|||
# Shared Response Types
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#ProviderAuthError">ProviderAuthError</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#UnknownError">UnknownError</a>
|
||||
|
||||
# Event
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventListResponse">EventListResponse</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /event">client.Event.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventListResponse">EventListResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# App
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /app">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /app/init">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Find
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsResponse">FindSymbolsResponse</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextResponse">FindTextResponse</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /find/file">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Files">Files</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindFilesParams">FindFilesParams</a>) ([]<a href="https://pkg.go.dev/builtin#string">string</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /find/symbol">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Symbols">Symbols</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsParams">FindSymbolsParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindSymbolsResponse">FindSymbolsResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /find">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Text">Text</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextParams">FindTextParams</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindTextResponse">FindTextResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# File
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileStatusResponse">FileStatusResponse</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /file">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Read">Read</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadParams">FileReadParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileReadResponse">FileReadResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /file/status">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Status">Status</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileStatusResponse">FileStatusResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Config
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Keybinds">Keybinds</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpLocal">McpLocal</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpRemote">McpRemote</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Model">Model</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Provider">Provider</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigProvidersResponse">ConfigProvidersResponse</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /config">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /config/providers">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Providers">Providers</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigProvidersResponse">ConfigProvidersResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Session
|
||||
|
||||
Params Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartParam">FilePartParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#MessagePartUnionParam">MessagePartUnionParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ReasoningPartParam">ReasoningPartParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SourceURLPartParam">SourceURLPartParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPartParam">StepStartPartParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPartParam">TextPartParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolCallParam">ToolCallParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolInvocationPartParam">ToolInvocationPartParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolPartialCallParam">ToolPartialCallParam</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolResultParam">ToolResultParam</a>
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePart">FilePart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#MessagePart">MessagePart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ReasoningPart">ReasoningPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SourceURLPart">SourceURLPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPart">StepStartPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPart">TextPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolCall">ToolCall</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolInvocationPart">ToolInvocationPart</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolPartialCall">ToolPartialCall</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolResult">ToolResult</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="post /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.New">New</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/summarize">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Summarize">Summarize</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionSummarizeParams">SessionSummarizeParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
123
packages/tui/sdk/app.go
Normal file
123
packages/tui/sdk/app.go
Normal file
|
@ -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
|
||||
}
|
58
packages/tui/sdk/app_test.go
Normal file
58
packages/tui/sdk/app_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
123
packages/tui/sdk/client.go
Normal file
123
packages/tui/sdk/client.go
Normal file
|
@ -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...)
|
||||
}
|
332
packages/tui/sdk/client_test.go
Normal file
332
packages/tui/sdk/client_test.go
Normal file
|
@ -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 }
|
724
packages/tui/sdk/config.go
Normal file
724
packages/tui/sdk/config.go
Normal file
|
@ -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
|
||||
}
|
58
packages/tui/sdk/config_test.go
Normal file
58
packages/tui/sdk/config_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
1180
packages/tui/sdk/event.go
Normal file
1180
packages/tui/sdk/event.go
Normal file
File diff suppressed because it is too large
Load diff
4
packages/tui/sdk/examples/.keep
Normal file
4
packages/tui/sdk/examples/.keep
Normal file
|
@ -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.
|
50
packages/tui/sdk/field.go
Normal file
50
packages/tui/sdk/field.go
Normal file
|
@ -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 }
|
143
packages/tui/sdk/file.go
Normal file
143
packages/tui/sdk/file.go
Normal file
|
@ -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,
|
||||
})
|
||||
}
|
60
packages/tui/sdk/file_test.go
Normal file
60
packages/tui/sdk/file_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
213
packages/tui/sdk/find.go
Normal file
213
packages/tui/sdk/find.go
Normal file
|
@ -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,
|
||||
})
|
||||
}
|
86
packages/tui/sdk/find_test.go
Normal file
86
packages/tui/sdk/find_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
13
packages/tui/sdk/go.mod
Normal file
13
packages/tui/sdk/go.mod
Normal file
|
@ -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
|
||||
)
|
10
packages/tui/sdk/go.sum
Normal file
10
packages/tui/sdk/go.sum
Normal file
|
@ -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=
|
53
packages/tui/sdk/internal/apierror/apierror.go
Normal file
53
packages/tui/sdk/internal/apierror/apierror.go
Normal file
|
@ -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
|
||||
}
|
383
packages/tui/sdk/internal/apiform/encoder.go
Normal file
383
packages/tui/sdk/internal/apiform/encoder.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
5
packages/tui/sdk/internal/apiform/form.go
Normal file
5
packages/tui/sdk/internal/apiform/form.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package apiform
|
||||
|
||||
type Marshaler interface {
|
||||
MarshalMultipart() ([]byte, string, error)
|
||||
}
|
440
packages/tui/sdk/internal/apiform/form_test.go
Normal file
440
packages/tui/sdk/internal/apiform/form_test.go
Normal file
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
48
packages/tui/sdk/internal/apiform/tag.go
Normal file
48
packages/tui/sdk/internal/apiform/tag.go
Normal file
|
@ -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
|
||||
}
|
670
packages/tui/sdk/internal/apijson/decoder.go
Normal file
670
packages/tui/sdk/internal/apijson/decoder.go
Normal file
|
@ -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
|
||||
}
|
398
packages/tui/sdk/internal/apijson/encoder.go
Normal file
398
packages/tui/sdk/internal/apijson/encoder.go
Normal file
|
@ -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(".", "\\.", ":", "\\:", "*", "\\*")
|
41
packages/tui/sdk/internal/apijson/field.go
Normal file
41
packages/tui/sdk/internal/apijson/field.go
Normal file
|
@ -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
|
||||
}
|
66
packages/tui/sdk/internal/apijson/field_test.go
Normal file
66
packages/tui/sdk/internal/apijson/field_test.go
Normal file
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
617
packages/tui/sdk/internal/apijson/json_test.go
Normal file
617
packages/tui/sdk/internal/apijson/json_test.go
Normal file
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
120
packages/tui/sdk/internal/apijson/port.go
Normal file
120
packages/tui/sdk/internal/apijson/port.go
Normal file
|
@ -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
|
||||
}
|
257
packages/tui/sdk/internal/apijson/port_test.go
Normal file
257
packages/tui/sdk/internal/apijson/port_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
41
packages/tui/sdk/internal/apijson/registry.go
Normal file
41
packages/tui/sdk/internal/apijson/registry.go
Normal file
|
@ -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)
|
||||
}
|
47
packages/tui/sdk/internal/apijson/tag.go
Normal file
47
packages/tui/sdk/internal/apijson/tag.go
Normal file
|
@ -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
|
||||
}
|
341
packages/tui/sdk/internal/apiquery/encoder.go
Normal file
341
packages/tui/sdk/internal/apiquery/encoder.go
Normal file
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
50
packages/tui/sdk/internal/apiquery/query.go
Normal file
50
packages/tui/sdk/internal/apiquery/query.go
Normal file
|
@ -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
|
||||
)
|
335
packages/tui/sdk/internal/apiquery/query_test.go
Normal file
335
packages/tui/sdk/internal/apiquery/query_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
41
packages/tui/sdk/internal/apiquery/tag.go
Normal file
41
packages/tui/sdk/internal/apiquery/tag.go
Normal file
|
@ -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
|
||||
}
|
29
packages/tui/sdk/internal/param/field.go
Normal file
29
packages/tui/sdk/internal/param/field.go
Normal file
|
@ -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)
|
||||
}
|
629
packages/tui/sdk/internal/requestconfig/requestconfig.go
Normal file
629
packages/tui/sdk/internal/requestconfig/requestconfig.go
Normal file
|
@ -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
|
||||
})
|
||||
}
|
27
packages/tui/sdk/internal/testutil/testutil.go
Normal file
27
packages/tui/sdk/internal/testutil/testutil.go
Normal file
|
@ -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
|
||||
}
|
5
packages/tui/sdk/internal/version.go
Normal file
5
packages/tui/sdk/internal/version.go
Normal file
|
@ -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
|
4
packages/tui/sdk/lib/.keep
Normal file
4
packages/tui/sdk/lib/.keep
Normal file
|
@ -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.
|
38
packages/tui/sdk/option/middleware.go
Normal file
38
packages/tui/sdk/option/middleware.go
Normal file
|
@ -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
|
||||
})
|
||||
}
|
266
packages/tui/sdk/option/requestoption.go
Normal file
266
packages/tui/sdk/option/requestoption.go
Normal file
|
@ -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/")
|
||||
}
|
181
packages/tui/sdk/packages/ssestream/ssestream.go
Normal file
181
packages/tui/sdk/packages/ssestream/ssestream.go
Normal file
|
@ -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()
|
||||
}
|
67
packages/tui/sdk/release-please-config.json
Normal file
67
packages/tui/sdk/release-please-config.json
Normal file
|
@ -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"
|
||||
]
|
||||
}
|
16
packages/tui/sdk/scripts/bootstrap
Executable file
16
packages/tui/sdk/scripts/bootstrap
Executable file
|
@ -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
|
8
packages/tui/sdk/scripts/format
Executable file
8
packages/tui/sdk/scripts/format
Executable file
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "==> Running gofmt -s -w"
|
||||
gofmt -s -w .
|
8
packages/tui/sdk/scripts/lint
Executable file
8
packages/tui/sdk/scripts/lint
Executable file
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "==> Running Go build"
|
||||
go build ./...
|
41
packages/tui/sdk/scripts/mock
Executable file
41
packages/tui/sdk/scripts/mock
Executable file
|
@ -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
|
56
packages/tui/sdk/scripts/test
Executable file
56
packages/tui/sdk/scripts/test
Executable file
|
@ -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 ./... "$@"
|
1385
packages/tui/sdk/session.go
Normal file
1385
packages/tui/sdk/session.go
Normal file
File diff suppressed because it is too large
Load diff
259
packages/tui/sdk/session_test.go
Normal file
259
packages/tui/sdk/session_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
132
packages/tui/sdk/shared/shared.go
Normal file
132
packages/tui/sdk/shared/shared.go
Normal file
|
@ -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
|
||||
}
|
32
packages/tui/sdk/usage_test.go
Normal file
32
packages/tui/sdk/usage_test.go
Normal file
|
@ -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)
|
||||
}
|
26
scripts/stainless
Executable file
26
scripts/stainless
Executable file
|
@ -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!"
|
5
stainless-workspace.json
Normal file
5
stainless-workspace.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"project": "opencode",
|
||||
"openapi_spec": "openapi.json",
|
||||
"stainless_config": "stainless.yml"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue