ci: new publish method (#1451)

This commit is contained in:
Dax 2025-07-31 01:00:29 -04:00 committed by GitHub
parent b09ebf4645
commit 33cef075d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
190 changed files with 16142 additions and 13342 deletions

View file

@ -2,13 +2,11 @@ name: publish
on: on:
workflow_dispatch: workflow_dispatch:
push: inputs:
branches: version:
- dev description: "Version to publish"
tags: required: true
- "*" type: string
- "!vscode-v*"
- "!github-v*"
concurrency: ${{ github.workflow }}-${{ github.ref }} concurrency: ${{ github.workflow }}-${{ github.ref }}
@ -53,11 +51,7 @@ jobs:
- name: Publish - name: Publish
run: | run: |
bun install bun install
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then OPENCODE_VERSION=${{ inputs.version }} ./script/publish.ts
./script/publish.ts
else
./script/publish.ts --snapshot
fi
working-directory: ./packages/opencode working-directory: ./packages/opencode
env: env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}

985
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -12,10 +12,12 @@
}, },
"workspaces": { "workspaces": {
"packages": [ "packages": [
"packages/*" "packages/*",
"packages/sdk/js"
], ],
"catalog": { "catalog": {
"@types/node": "22.13.9", "@types/node": "22.13.9",
"@tsconfig/node22": "22.0.2",
"ai": "5.0.0-beta.33", "ai": "5.0.0-beta.33",
"hono": "4.7.10", "hono": "4.7.10",
"typescript": "5.8.2", "typescript": "5.8.2",

View file

@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "0.0.5", "version": "0.0.0",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"private": true, "private": true,

View file

@ -1,21 +1,13 @@
#!/usr/bin/env bun #!/usr/bin/env bun
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import { $ } from "bun" import { $ } from "bun"
import pkg from "../package.json" import pkg from "../package.json"
const dry = process.argv.includes("--dry") const dry = process.env["OPENCODE_DRY"] === "true"
const snapshot = process.argv.includes("--snapshot") const version = process.env["OPENCODE_VERSION"]!
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
const version = snapshot
? `0.0.0-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
: await $`git describe --tags --abbrev=0`
.text()
.then((x) => x.substring(1).trim())
.catch(() => {
console.error("tag not found")
process.exit(1)
})
console.log(`publishing ${version}`) console.log(`publishing ${version}`)

View file

@ -1,7 +1,6 @@
import { App } from "../app/app" import { App } from "../app/app"
import { BunProc } from "../bun" import { BunProc } from "../bun"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import path from "path"
export interface Info { export interface Info {
name: string name: string
@ -65,14 +64,57 @@ export const prettier: Info = {
], ],
async enabled() { async enabled() {
const app = App.info() const app = App.info()
const nms = await Filesystem.findUp("node_modules", app.path.cwd, app.path.root) const items = await Filesystem.findUp("package.json", app.path.cwd, app.path.root)
for (const item of nms) { for (const item of items) {
if (await Bun.file(path.join(item, ".bin", "prettier")).exists()) return true const json = await Bun.file(item).json()
if (json.dependencies?.prettier) return true
if (json.devDependencies?.prettier) return true
} }
return false return false
}, },
} }
export const biome: Info = {
name: "biome",
command: [BunProc.which(), "x", "biome", "format", "--write", "$FILE"],
environment: {
BUN_BE_BUN: "1",
},
extensions: [
".js",
".jsx",
".mjs",
".cjs",
".ts",
".tsx",
".mts",
".cts",
".html",
".htm",
".css",
".scss",
".sass",
".less",
".vue",
".svelte",
".json",
".jsonc",
".yaml",
".yml",
".toml",
".xml",
".md",
".mdx",
".graphql",
".gql",
],
async enabled() {
const app = App.info()
const items = await Filesystem.findUp("biome.json", app.path.cwd, app.path.root)
return items.length > 0
},
}
export const zig: Info = { export const zig: Info = {
name: "zig", name: "zig",
command: ["zig", "fmt", "$FILE"], command: ["zig", "fmt", "$FILE"],

View file

@ -94,6 +94,7 @@ export namespace Server {
"/event", "/event",
describeRoute({ describeRoute({
description: "Get events", description: "Get events",
operationId: "event.subscribe",
responses: { responses: {
200: { 200: {
description: "Event stream", description: "Event stream",
@ -137,6 +138,7 @@ export namespace Server {
"/app", "/app",
describeRoute({ describeRoute({
description: "Get app info", description: "Get app info",
operationId: "app.get",
responses: { responses: {
200: { 200: {
description: "200", description: "200",
@ -156,6 +158,7 @@ export namespace Server {
"/app/init", "/app/init",
describeRoute({ describeRoute({
description: "Initialize the app", description: "Initialize the app",
operationId: "app.init",
responses: { responses: {
200: { 200: {
description: "Initialize the app", description: "Initialize the app",
@ -176,6 +179,7 @@ export namespace Server {
"/config", "/config",
describeRoute({ describeRoute({
description: "Get config info", description: "Get config info",
operationId: "config.get",
responses: { responses: {
200: { 200: {
description: "Get config info", description: "Get config info",
@ -195,6 +199,7 @@ export namespace Server {
"/session", "/session",
describeRoute({ describeRoute({
description: "List all sessions", description: "List all sessions",
operationId: "session.list",
responses: { responses: {
200: { 200: {
description: "List of sessions", description: "List of sessions",
@ -216,6 +221,7 @@ export namespace Server {
"/session", "/session",
describeRoute({ describeRoute({
description: "Create a new session", description: "Create a new session",
operationId: "session.create",
responses: { responses: {
...ERRORS, ...ERRORS,
200: { 200: {
@ -237,6 +243,7 @@ export namespace Server {
"/session/:id", "/session/:id",
describeRoute({ describeRoute({
description: "Delete a session and all its data", description: "Delete a session and all its data",
operationId: "session.delete",
responses: { responses: {
200: { 200: {
description: "Successfully deleted session", description: "Successfully deleted session",
@ -263,6 +270,7 @@ export namespace Server {
"/session/:id/init", "/session/:id/init",
describeRoute({ describeRoute({
description: "Analyze the app and create an AGENTS.md file", description: "Analyze the app and create an AGENTS.md file",
operationId: "session.init",
responses: { responses: {
200: { 200: {
description: "200", description: "200",
@ -299,6 +307,7 @@ export namespace Server {
"/session/:id/abort", "/session/:id/abort",
describeRoute({ describeRoute({
description: "Abort a session", description: "Abort a session",
operationId: "session.abort",
responses: { responses: {
200: { 200: {
description: "Aborted session", description: "Aborted session",
@ -324,6 +333,7 @@ export namespace Server {
"/session/:id/share", "/session/:id/share",
describeRoute({ describeRoute({
description: "Share a session", description: "Share a session",
operationId: "session.share",
responses: { responses: {
200: { 200: {
description: "Successfully shared session", description: "Successfully shared session",
@ -352,6 +362,7 @@ export namespace Server {
"/session/:id/share", "/session/:id/share",
describeRoute({ describeRoute({
description: "Unshare the session", description: "Unshare the session",
operationId: "session.unshare",
responses: { responses: {
200: { 200: {
description: "Successfully unshared session", description: "Successfully unshared session",
@ -380,6 +391,7 @@ export namespace Server {
"/session/:id/summarize", "/session/:id/summarize",
describeRoute({ describeRoute({
description: "Summarize the session", description: "Summarize the session",
operationId: "session.summarize",
responses: { responses: {
200: { 200: {
description: "Summarized session", description: "Summarized session",
@ -415,6 +427,7 @@ export namespace Server {
"/session/:id/message", "/session/:id/message",
describeRoute({ describeRoute({
description: "List messages for a session", description: "List messages for a session",
operationId: "session.messages",
responses: { responses: {
200: { 200: {
description: "List of messages", description: "List of messages",
@ -448,6 +461,7 @@ export namespace Server {
"/session/:id/message", "/session/:id/message",
describeRoute({ describeRoute({
description: "Create and send a new message to a session", description: "Create and send a new message to a session",
operationId: "session.chat",
responses: { responses: {
200: { 200: {
description: "Created message", description: "Created message",
@ -477,6 +491,7 @@ export namespace Server {
"/session/:id/revert", "/session/:id/revert",
describeRoute({ describeRoute({
description: "Revert a message", description: "Revert a message",
operationId: "session.revert",
responses: { responses: {
200: { 200: {
description: "Updated session", description: "Updated session",
@ -506,6 +521,7 @@ export namespace Server {
"/session/:id/unrevert", "/session/:id/unrevert",
describeRoute({ describeRoute({
description: "Restore all reverted messages", description: "Restore all reverted messages",
operationId: "session.unrevert",
responses: { responses: {
200: { 200: {
description: "Updated session", description: "Updated session",
@ -533,6 +549,7 @@ export namespace Server {
"/config/providers", "/config/providers",
describeRoute({ describeRoute({
description: "List all providers", description: "List all providers",
operationId: "config.providers",
responses: { responses: {
200: { 200: {
description: "List of providers", description: "List of providers",
@ -561,6 +578,7 @@ export namespace Server {
"/find", "/find",
describeRoute({ describeRoute({
description: "Find text in files", description: "Find text in files",
operationId: "find.text",
responses: { responses: {
200: { 200: {
description: "Matches", description: "Matches",
@ -593,6 +611,7 @@ export namespace Server {
"/find/file", "/find/file",
describeRoute({ describeRoute({
description: "Find files", description: "Find files",
operationId: "find.files",
responses: { responses: {
200: { 200: {
description: "File paths", description: "File paths",
@ -625,6 +644,7 @@ export namespace Server {
"/find/symbol", "/find/symbol",
describeRoute({ describeRoute({
description: "Find workspace symbols", description: "Find workspace symbols",
operationId: "find.symbols",
responses: { responses: {
200: { 200: {
description: "Symbols", description: "Symbols",
@ -652,6 +672,7 @@ export namespace Server {
"/file", "/file",
describeRoute({ describeRoute({
description: "Read a file", description: "Read a file",
operationId: "file.read",
responses: { responses: {
200: { 200: {
description: "File content", description: "File content",
@ -688,6 +709,7 @@ export namespace Server {
"/file/status", "/file/status",
describeRoute({ describeRoute({
description: "Get file status", description: "Get file status",
operationId: "file.status",
responses: { responses: {
200: { 200: {
description: "File status", description: "File status",
@ -708,6 +730,7 @@ export namespace Server {
"/log", "/log",
describeRoute({ describeRoute({
description: "Write a log entry to the server logs", description: "Write a log entry to the server logs",
operationId: "app.log",
responses: { responses: {
200: { 200: {
description: "Log entry written successfully", description: "Log entry written successfully",
@ -757,6 +780,7 @@ export namespace Server {
"/mode", "/mode",
describeRoute({ describeRoute({
description: "List all modes", description: "List all modes",
operationId: "app.modes",
responses: { responses: {
200: { 200: {
description: "List of modes", description: "List of modes",
@ -777,6 +801,7 @@ export namespace Server {
"/tui/append-prompt", "/tui/append-prompt",
describeRoute({ describeRoute({
description: "Append prompt to the TUI", description: "Append prompt to the TUI",
operationId: "tui.appendPrompt",
responses: { responses: {
200: { 200: {
description: "Prompt processed successfully", description: "Prompt processed successfully",
@ -800,6 +825,7 @@ export namespace Server {
"/tui/open-help", "/tui/open-help",
describeRoute({ describeRoute({
description: "Open the help dialog", description: "Open the help dialog",
operationId: "tui.openHelp",
responses: { responses: {
200: { 200: {
description: "Help dialog opened successfully", description: "Help dialog opened successfully",

View file

@ -1,15 +0,0 @@
// 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/typescript-node:latest",
"features": {
"ghcr.io/devcontainers/features/node:1": {}
},
"postCreateCommand": "yarn install",
"customizations": {
"vscode": {
"extensions": ["esbenp.prettier-vscode"]
}
}
}

View file

@ -1,7 +0,0 @@
CHANGELOG.md
/ecosystem-tests/*/**
/node_modules
/deno
# don't format tsc output, will break source maps
/dist

View file

@ -1,7 +0,0 @@
{
"arrowParens": "always",
"experimentalTernaries": true,
"printWidth": 110,
"singleQuote": true,
"trailingComma": "all"
}

View file

@ -1,3 +0,0 @@
{
".": "0.1.0-alpha.20"
}

View file

@ -1,4 +0,0 @@
configured_endpoints: 26
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-62d8fccba4eb8dc3a80434e0849eab3352e49fb96a718bb7b6d17ed8e582b716.yml
openapi_spec_hash: 4ff9376cf9634e91731e63fe482ea532
config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3

View file

@ -1 +0,0 @@
brew "node"

View file

@ -1,196 +0,0 @@
# Changelog
## 0.1.0-alpha.20 (2025-07-16)
Full Changelog: [v0.1.0-alpha.19...v0.1.0-alpha.20](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.19...v0.1.0-alpha.20)
### Features
* **api:** api update ([d296473](https://github.com/sst/opencode-sdk-js/commit/d296473db58378932b85d1afaa60942ac5599c49))
* **api:** api update ([af2b587](https://github.com/sst/opencode-sdk-js/commit/af2b5875534a4782fac186542ecb9b04393c9b0a))
## 0.1.0-alpha.19 (2025-07-16)
Full Changelog: [v0.1.0-alpha.18...v0.1.0-alpha.19](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.18...v0.1.0-alpha.19)
### Features
* **api:** api update ([2e505ef](https://github.com/sst/opencode-sdk-js/commit/2e505ef451fdcf49358189c5f76bdc42fb821352))
## 0.1.0-alpha.18 (2025-07-15)
Full Changelog: [v0.1.0-alpha.17...v0.1.0-alpha.18](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.17...v0.1.0-alpha.18)
### Features
* **api:** api update ([25a23e5](https://github.com/sst/opencode-sdk-js/commit/25a23e599f1180754910961df65f0cc044aa2935))
## 0.1.0-alpha.17 (2025-07-15)
Full Changelog: [v0.1.0-alpha.16...v0.1.0-alpha.17](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.16...v0.1.0-alpha.17)
### Features
* **api:** api update ([8b5d592](https://github.com/sst/opencode-sdk-js/commit/8b5d59243a0212f98269412f4483e729e2367a77))
* **api:** api update ([ebd8986](https://github.com/sst/opencode-sdk-js/commit/ebd89862c48be2742eda727c83c01430413e00c0))
## 0.1.0-alpha.16 (2025-07-15)
Full Changelog: [v0.1.0-alpha.15...v0.1.0-alpha.16](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.15...v0.1.0-alpha.16)
### Features
* **api:** api update ([f26379d](https://github.com/sst/opencode-sdk-js/commit/f26379d83ae7094d6ba91c6705a97a3fbd88a55a))
### Chores
* make some internal functions async ([36b1db9](https://github.com/sst/opencode-sdk-js/commit/36b1db9ca9d47d9199e2eab5f0b454b7cd31f58f))
## 0.1.0-alpha.15 (2025-07-05)
Full Changelog: [v0.1.0-alpha.14...v0.1.0-alpha.15](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.14...v0.1.0-alpha.15)
### Features
* **api:** manual updates ([f6ee467](https://github.com/sst/opencode-sdk-js/commit/f6ee46752d0c174c8b934894cf2b140864864208))
### Chores
* **internal:** codegen related update ([47a1a97](https://github.com/sst/opencode-sdk-js/commit/47a1a972e755735d6b5472c61f726ab2face9e62))
## 0.1.0-alpha.14 (2025-07-03)
Full Changelog: [v0.1.0-alpha.13...v0.1.0-alpha.14](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.13...v0.1.0-alpha.14)
### Features
* **api:** api update ([a1d7cf9](https://github.com/sst/opencode-sdk-js/commit/a1d7cf948a2ff47ce4e98b4a52d0e4d213b87bf6))
### Chores
* **internal:** version bump ([f8ad145](https://github.com/sst/opencode-sdk-js/commit/f8ad145b9af0c4a465642630043e59236d5f4e8d))
## 0.1.0-alpha.13 (2025-07-03)
Full Changelog: [v0.1.0-alpha.12...v0.1.0-alpha.13](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.12...v0.1.0-alpha.13)
### Bug Fixes
* avoid console usage ([f96ac97](https://github.com/sst/opencode-sdk-js/commit/f96ac97fbaf7417efda306d8727654d1a4138386))
### Chores
* add docs to RequestOptions type ([1ca6677](https://github.com/sst/opencode-sdk-js/commit/1ca667765c22b706732c61ea3d9d2823aeda0a8e))
## 0.1.0-alpha.12 (2025-07-02)
Full Changelog: [v0.1.0-alpha.11...v0.1.0-alpha.12](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.11...v0.1.0-alpha.12)
### Features
* **api:** update via SDK Studio ([7739340](https://github.com/sst/opencode-sdk-js/commit/77393403648067fe937637c39e80067c347a8c5b))
## 0.1.0-alpha.11 (2025-06-30)
Full Changelog: [v0.1.0-alpha.10...v0.1.0-alpha.11](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.10...v0.1.0-alpha.11)
### Features
* **api:** update via SDK Studio ([2ce98e5](https://github.com/sst/opencode-sdk-js/commit/2ce98e55bf330cca0c38f60f011ffd9063b34ea0))
## 0.1.0-alpha.10 (2025-06-30)
Full Changelog: [v0.1.0-alpha.9...v0.1.0-alpha.10](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.9...v0.1.0-alpha.10)
### Features
* **api:** update via SDK Studio ([fa7c91c](https://github.com/sst/opencode-sdk-js/commit/fa7c91cc2fe52d42be7365ca2c4ce3e48c2e76ac))
### Chores
* **ci:** only run for pushes and fork pull requests ([0e850e5](https://github.com/sst/opencode-sdk-js/commit/0e850e51daac413dcf2d5e30c0ea7a1cd5346c4b))
* **client:** improve path param validation ([bc3ff0e](https://github.com/sst/opencode-sdk-js/commit/bc3ff0ee2de9af8be42deae87d12f003fb5f7aa5))
## 0.1.0-alpha.9 (2025-06-27)
Full Changelog: [v0.1.0-alpha.8...v0.1.0-alpha.9](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.8...v0.1.0-alpha.9)
### Features
* **api:** update via SDK Studio ([7009d10](https://github.com/sst/opencode-sdk-js/commit/7009d10aab99be7102371cee49013ab3edae4450))
* **api:** update via SDK Studio ([e60aa00](https://github.com/sst/opencode-sdk-js/commit/e60aa0024079671e3725ee6f6bfbf8c2dad78da2))
## 0.1.0-alpha.8 (2025-06-27)
Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.7...v0.1.0-alpha.8)
### Features
* **api:** update via SDK Studio ([171e3d5](https://github.com/sst/opencode-sdk-js/commit/171e3d5f3ba69ff9ba8547dac90d85b1a0a137c1))
## 0.1.0-alpha.7 (2025-06-27)
Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.6...v0.1.0-alpha.7)
### Features
* **api:** update via SDK Studio ([14d2d04](https://github.com/sst/opencode-sdk-js/commit/14d2d04d80c1d5880940c9c70a5c1ea200df2ebc))
## 0.1.0-alpha.6 (2025-06-27)
Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.5...v0.1.0-alpha.6)
### Features
* **api:** update via SDK Studio ([45e78b2](https://github.com/sst/opencode-sdk-js/commit/45e78b2f0fca18f537de9986e358aa876fb0b686))
## 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-js/compare/v0.1.0-alpha.4...v0.1.0-alpha.5)
### Features
* **api:** update via SDK Studio ([10a5be9](https://github.com/sst/opencode-sdk-js/commit/10a5be9261c4ba8aeece7bb6921752f5fa6d9f28))
## 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-js/compare/v0.1.0-alpha.3...v0.1.0-alpha.4)
### Features
* **api:** update via SDK Studio ([20dcd17](https://github.com/sst/opencode-sdk-js/commit/20dcd171405b05801e5a56f1b40fd635259b6a94))
## 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-js/compare/v0.1.0-alpha.2...v0.1.0-alpha.3)
### Bug Fixes
* **ci:** release-doctor — report correct token name ([128884f](https://github.com/sst/opencode-sdk-js/commit/128884f4bc64e618177a0b090cd6d52b122a059a))
## 0.1.0-alpha.2 (2025-06-24)
Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencode-sdk-js/compare/v0.1.0-alpha.1...v0.1.0-alpha.2)
### Features
* **api:** update via SDK Studio ([2320f32](https://github.com/sst/opencode-sdk-js/commit/2320f32190ab58d15d00d7c3328f9fba2421536c))
## 0.1.0-alpha.1 (2025-06-24)
Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencode-sdk-js/compare/v0.0.1-alpha.0...v0.1.0-alpha.1)
### Features
* **api:** update via SDK Studio ([e448306](https://github.com/sst/opencode-sdk-js/commit/e4483068738cbb10233fca5a9d9d44a9c9815c8b))
* **api:** update via SDK Studio ([b222c96](https://github.com/sst/opencode-sdk-js/commit/b222c96a679a8aeecb60bcf92c247fef90c75b3d))
### Chores
* update SDK settings ([c1481ea](https://github.com/sst/opencode-sdk-js/commit/c1481ea7949c1422bedaeac278600b4ec3f58038))

View file

@ -1,107 +0,0 @@
## Setting up the environment
This repository uses [`yarn@v1`](https://classic.yarnpkg.com/lang/en/docs/install).
Other package managers may work but are not officially supported for development.
To set up the repository, run:
```sh
$ yarn
$ yarn build
```
This will install all the required dependencies and build output files to `dist/`.
## 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 `src/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.
```ts
// add an example to examples/<your-example>.ts
#!/usr/bin/env -S npm run tsn -T
```
```sh
$ chmod +x examples/<your-example>.ts
# run the example against your api
$ yarn tsn -T examples/<your-example>.ts
```
## Using the repository from source
If youd like to use the repository from source, you can either install from git or link to a cloned repository:
To install via git:
```sh
$ npm install git+ssh://git@github.com:sst/opencode-sdk-js.git
```
Alternatively, to link a local copy of the repo:
```sh
# Clone
$ git clone https://www.github.com/sst/opencode-sdk-js
$ cd opencode-sdk-js
# With yarn
$ yarn link
$ cd ../my-package
$ yarn link @opencode-ai/sdk
# With pnpm
$ pnpm link --global
$ cd ../my-package
$ pnpm link -—global @opencode-ai/sdk
```
## 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
$ npx prism mock path/to/your/openapi.yml
```
```sh
$ yarn run test
```
## Linting and formatting
This repository uses [prettier](https://www.npmjs.com/package/prettier) and
[eslint](https://www.npmjs.com/package/eslint) to format the code in the repository.
To lint:
```sh
$ yarn lint
```
To format and fix all lint issues automatically:
```sh
$ yarn fix
```
## Publishing and releases
Changes made to this repository via the automated release PR pipeline should publish to npm automatically. If
the changes aren't made through the automated pipeline, you may want to make releases manually.
### Publish with a GitHub workflow
You can release to package managers by using [the `Publish NPM` GitHub action](https://www.github.com/sst/opencode-sdk-js/actions/workflows/publish-npm.yml). This requires a setup organization or repository secret to be set up.
### Publish manually
If you need to manually release a package, you can run the `bin/publish-npm` script with an `NPM_TOKEN` set on
the environment.

View file

@ -1,370 +0,0 @@
# Opencode TypeScript API Library
[![NPM version](<https://img.shields.io/npm/v/@opencode-ai/sdk.svg?label=npm%20(stable)>)](https://npmjs.org/package/@opencode-ai/sdk) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/@opencode-ai/sdk)
This library provides convenient access to the Opencode REST API from server-side TypeScript or JavaScript.
The REST API documentation can be found on [opencode.ai](https://opencode.ai/docs). The full API of this library can be found in [api.md](api.md).
It is generated with [Stainless](https://www.stainless.com/).
## Installation
```sh
npm install @opencode-ai/sdk
```
## Usage
The full API of this library can be found in [api.md](api.md).
<!-- prettier-ignore -->
```js
import Opencode from '@opencode-ai/sdk';
const client = new Opencode();
const sessions = await client.session.list();
```
## Streaming responses
We provide support for streaming responses using Server Sent Events (SSE).
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode();
const stream = await client.event.list();
for await (const eventListResponse of stream) {
console.log(eventListResponse);
}
```
If you need to cancel a stream, you can `break` from the loop
or call `stream.controller.abort()`.
### Request & Response types
This library includes TypeScript definitions for all request params and response fields. You may import and use them like so:
<!-- prettier-ignore -->
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode();
const sessions: Opencode.SessionListResponse = await client.session.list();
```
Documentation for each method, request param, and response field are available in docstrings and will appear on hover in most modern editors.
## Handling errors
When the library is unable to connect to the API,
or if the API returns a non-success status code (i.e., 4xx or 5xx response),
a subclass of `APIError` will be thrown:
<!-- prettier-ignore -->
```ts
const sessions = await client.session.list().catch(async (err) => {
if (err instanceof Opencode.APIError) {
console.log(err.status); // 400
console.log(err.name); // BadRequestError
console.log(err.headers); // {server: 'nginx', ...}
} else {
throw err;
}
});
```
Error codes are as follows:
| Status Code | Error Type |
| ----------- | -------------------------- |
| 400 | `BadRequestError` |
| 401 | `AuthenticationError` |
| 403 | `PermissionDeniedError` |
| 404 | `NotFoundError` |
| 422 | `UnprocessableEntityError` |
| 429 | `RateLimitError` |
| >=500 | `InternalServerError` |
| N/A | `APIConnectionError` |
### Retries
Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict,
429 Rate Limit, and >=500 Internal errors will all be retried by default.
You can use the `maxRetries` option to configure or disable this:
<!-- prettier-ignore -->
```js
// Configure the default for all requests:
const client = new Opencode({
maxRetries: 0, // default is 2
});
// Or, configure per-request:
await client.session.list({
maxRetries: 5,
});
```
### Timeouts
Requests time out after 1 minute by default. You can configure this with a `timeout` option:
<!-- prettier-ignore -->
```ts
// Configure the default for all requests:
const client = new Opencode({
timeout: 20 * 1000, // 20 seconds (default is 1 minute)
});
// Override per-request:
await client.session.list({
timeout: 5 * 1000,
});
```
On timeout, an `APIConnectionTimeoutError` is thrown.
Note that requests which time out will be [retried twice by default](#retries).
## Advanced Usage
### Accessing raw Response data (e.g., headers)
The "raw" `Response` returned by `fetch()` can be accessed through the `.asResponse()` method on the `APIPromise` type that all methods return.
This method returns as soon as the headers for a successful response are received and does not consume the response body, so you are free to write custom parsing or streaming logic.
You can also use the `.withResponse()` method to get the raw `Response` along with the parsed data.
Unlike `.asResponse()` this method consumes the body, returning once it is parsed.
<!-- prettier-ignore -->
```ts
const client = new Opencode();
const response = await client.session.list().asResponse();
console.log(response.headers.get('X-My-Header'));
console.log(response.statusText); // access the underlying Response object
const { data: sessions, response: raw } = await client.session.list().withResponse();
console.log(raw.headers.get('X-My-Header'));
console.log(sessions);
```
### Logging
> [!IMPORTANT]
> All log messages are intended for debugging only. The format and content of log messages
> may change between releases.
#### Log levels
The log level can be configured in two ways:
1. Via the `OPENCODE_LOG` environment variable
2. Using the `logLevel` client option (overrides the environment variable if set)
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({
logLevel: 'debug', // Show all log messages
});
```
Available log levels, from most to least verbose:
- `'debug'` - Show debug messages, info, warnings, and errors
- `'info'` - Show info messages, warnings, and errors
- `'warn'` - Show warnings and errors (default)
- `'error'` - Show only errors
- `'off'` - Disable all logging
At the `'debug'` level, all HTTP requests and responses are logged, including headers and bodies.
Some authentication-related headers are redacted, but sensitive data in request and response bodies
may still be visible.
#### Custom logger
By default, this library logs to `globalThis.console`. You can also provide a custom logger.
Most logging libraries are supported, including [pino](https://www.npmjs.com/package/pino), [winston](https://www.npmjs.com/package/winston), [bunyan](https://www.npmjs.com/package/bunyan), [consola](https://www.npmjs.com/package/consola), [signale](https://www.npmjs.com/package/signale), and [@std/log](https://jsr.io/@std/log). If your logger doesn't work, please open an issue.
When providing a custom logger, the `logLevel` option still controls which messages are emitted, messages
below the configured level will not be sent to your logger.
```ts
import Opencode from '@opencode-ai/sdk';
import pino from 'pino';
const logger = pino();
const client = new Opencode({
logger: logger.child({ name: 'Opencode' }),
logLevel: 'debug', // Send all messages to pino, allowing it to filter
});
```
### 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.
Options on the client, such as retries, will be respected when making these requests.
```ts
await client.post('/some/path', {
body: { some_prop: 'foo' },
query: { some_query_arg: 'bar' },
});
```
#### Undocumented request params
To make requests using undocumented parameters, you may use `// @ts-expect-error` on the undocumented
parameter. This library doesn't validate at runtime that the request matches the type, so any extra values you
send will be sent as-is.
```ts
client.session.list({
// ...
// @ts-expect-error baz is not yet public
baz: 'undocumented option',
});
```
For requests with the `GET` verb, any extra params will be in the query, all other requests will send the
extra param in the body.
If you want to explicitly send an extra argument, you can do so with the `query`, `body`, and `headers` request
options.
#### Undocumented response properties
To access undocumented response properties, you may access the response object with `// @ts-expect-error` on
the response object, or cast the response object to the requisite type. Like the request params, we do not
validate or strip extra properties from the response from the API.
### Customizing the fetch client
By default, this library expects a global `fetch` function is defined.
If you want to use a different `fetch` function, you can either polyfill the global:
```ts
import fetch from 'my-fetch';
globalThis.fetch = fetch;
```
Or pass it to the client:
```ts
import Opencode from '@opencode-ai/sdk';
import fetch from 'my-fetch';
const client = new Opencode({ fetch });
```
### Fetch options
If you want to set custom `fetch` options without overriding the `fetch` function, you can provide a `fetchOptions` object when instantiating the client or making a request. (Request-specific options override client options.)
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({
fetchOptions: {
// `RequestInit` options
},
});
```
#### Configuring proxies
To modify proxy behavior, you can provide custom `fetchOptions` that add runtime-specific proxy
options to requests:
<img src="https://raw.githubusercontent.com/stainless-api/sdk-assets/refs/heads/main/node.svg" align="top" width="18" height="21"> **Node** <sup>[[docs](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md#example---proxyagent-with-fetch)]</sup>
```ts
import Opencode from '@opencode-ai/sdk';
import * as undici from 'undici';
const proxyAgent = new undici.ProxyAgent('http://localhost:8888');
const client = new Opencode({
fetchOptions: {
dispatcher: proxyAgent,
},
});
```
<img src="https://raw.githubusercontent.com/stainless-api/sdk-assets/refs/heads/main/bun.svg" align="top" width="18" height="21"> **Bun** <sup>[[docs](https://bun.sh/guides/http/proxy)]</sup>
```ts
import Opencode from '@opencode-ai/sdk';
const client = new Opencode({
fetchOptions: {
proxy: 'http://localhost:8888',
},
});
```
<img src="https://raw.githubusercontent.com/stainless-api/sdk-assets/refs/heads/main/deno.svg" align="top" width="18" height="21"> **Deno** <sup>[[docs](https://docs.deno.com/api/deno/~/Deno.createHttpClient)]</sup>
```ts
import Opencode from 'npm:@opencode-ai/sdk';
const httpClient = Deno.createHttpClient({ proxy: { url: 'http://localhost:8888' } });
const client = new Opencode({
fetchOptions: {
client: httpClient,
},
});
```
## Frequently Asked Questions
## 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 that only affect static types, without breaking runtime behavior.
2. 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.)_
3. 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-js/issues) with questions, bugs, or suggestions.
## Requirements
TypeScript >= 4.9 is supported.
The following runtimes are supported:
- Web browsers (Up-to-date Chrome, Firefox, Safari, Edge, and more)
- Node.js 20 LTS or later ([non-EOL](https://endoflife.date/nodejs)) versions.
- Deno v1.28.0 or higher.
- Bun 1.0 or later.
- Cloudflare Workers.
- Vercel Edge Runtime.
- Jest 28 or greater with the `"node"` environment (`"jsdom"` is not supported at this time).
- Nitro v2.6 or greater.
Note that React Native is not supported at this time.
If you are interested in other runtime environments, please open or upvote an issue on GitHub.
## Contributing
See [the contributing documentation](./CONTRIBUTING.md).

View file

@ -1,139 +0,0 @@
# Shared
Types:
- <code><a href="./src/resources/shared.ts">MessageAbortedError</a></code>
- <code><a href="./src/resources/shared.ts">ProviderAuthError</a></code>
- <code><a href="./src/resources/shared.ts">UnknownError</a></code>
# Event
Types:
- <code><a href="./src/resources/event.ts">EventListResponse</a></code>
Methods:
- <code title="get /event">client.event.<a href="./src/resources/event.ts">list</a>() -> EventListResponse</code>
# App
Types:
- <code><a href="./src/resources/app.ts">App</a></code>
- <code><a href="./src/resources/app.ts">Mode</a></code>
- <code><a href="./src/resources/app.ts">Model</a></code>
- <code><a href="./src/resources/app.ts">Provider</a></code>
- <code><a href="./src/resources/app.ts">AppInitResponse</a></code>
- <code><a href="./src/resources/app.ts">AppLogResponse</a></code>
- <code><a href="./src/resources/app.ts">AppModesResponse</a></code>
- <code><a href="./src/resources/app.ts">AppProvidersResponse</a></code>
Methods:
- <code title="get /app">client.app.<a href="./src/resources/app.ts">get</a>() -> App</code>
- <code title="post /app/init">client.app.<a href="./src/resources/app.ts">init</a>() -> AppInitResponse</code>
- <code title="post /log">client.app.<a href="./src/resources/app.ts">log</a>({ ...params }) -> AppLogResponse</code>
- <code title="get /mode">client.app.<a href="./src/resources/app.ts">modes</a>() -> AppModesResponse</code>
- <code title="get /config/providers">client.app.<a href="./src/resources/app.ts">providers</a>() -> AppProvidersResponse</code>
# Find
Types:
- <code><a href="./src/resources/find.ts">Symbol</a></code>
- <code><a href="./src/resources/find.ts">FindFilesResponse</a></code>
- <code><a href="./src/resources/find.ts">FindSymbolsResponse</a></code>
- <code><a href="./src/resources/find.ts">FindTextResponse</a></code>
Methods:
- <code title="get /find/file">client.find.<a href="./src/resources/find.ts">files</a>({ ...params }) -> FindFilesResponse</code>
- <code title="get /find/symbol">client.find.<a href="./src/resources/find.ts">symbols</a>({ ...params }) -> FindSymbolsResponse</code>
- <code title="get /find">client.find.<a href="./src/resources/find.ts">text</a>({ ...params }) -> FindTextResponse</code>
# File
Types:
- <code><a href="./src/resources/file.ts">File</a></code>
- <code><a href="./src/resources/file.ts">FileReadResponse</a></code>
- <code><a href="./src/resources/file.ts">FileStatusResponse</a></code>
Methods:
- <code title="get /file">client.file.<a href="./src/resources/file.ts">read</a>({ ...params }) -> FileReadResponse</code>
- <code title="get /file/status">client.file.<a href="./src/resources/file.ts">status</a>() -> FileStatusResponse</code>
# Config
Types:
- <code><a href="./src/resources/config.ts">Config</a></code>
- <code><a href="./src/resources/config.ts">KeybindsConfig</a></code>
- <code><a href="./src/resources/config.ts">McpLocalConfig</a></code>
- <code><a href="./src/resources/config.ts">McpRemoteConfig</a></code>
- <code><a href="./src/resources/config.ts">ModeConfig</a></code>
Methods:
- <code title="get /config">client.config.<a href="./src/resources/config.ts">get</a>() -> Config</code>
# Session
Types:
- <code><a href="./src/resources/session.ts">AssistantMessage</a></code>
- <code><a href="./src/resources/session.ts">FilePart</a></code>
- <code><a href="./src/resources/session.ts">FilePartInput</a></code>
- <code><a href="./src/resources/session.ts">FilePartSource</a></code>
- <code><a href="./src/resources/session.ts">FilePartSourceText</a></code>
- <code><a href="./src/resources/session.ts">FileSource</a></code>
- <code><a href="./src/resources/session.ts">Message</a></code>
- <code><a href="./src/resources/session.ts">Part</a></code>
- <code><a href="./src/resources/session.ts">Session</a></code>
- <code><a href="./src/resources/session.ts">SnapshotPart</a></code>
- <code><a href="./src/resources/session.ts">StepFinishPart</a></code>
- <code><a href="./src/resources/session.ts">StepStartPart</a></code>
- <code><a href="./src/resources/session.ts">SymbolSource</a></code>
- <code><a href="./src/resources/session.ts">TextPart</a></code>
- <code><a href="./src/resources/session.ts">TextPartInput</a></code>
- <code><a href="./src/resources/session.ts">ToolPart</a></code>
- <code><a href="./src/resources/session.ts">ToolStateCompleted</a></code>
- <code><a href="./src/resources/session.ts">ToolStateError</a></code>
- <code><a href="./src/resources/session.ts">ToolStatePending</a></code>
- <code><a href="./src/resources/session.ts">ToolStateRunning</a></code>
- <code><a href="./src/resources/session.ts">UserMessage</a></code>
- <code><a href="./src/resources/session.ts">SessionListResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionDeleteResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionAbortResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionInitResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionMessagesResponse</a></code>
- <code><a href="./src/resources/session.ts">SessionSummarizeResponse</a></code>
Methods:
- <code title="post /session">client.session.<a href="./src/resources/session.ts">create</a>() -> Session</code>
- <code title="get /session">client.session.<a href="./src/resources/session.ts">list</a>() -> SessionListResponse</code>
- <code title="delete /session/{id}">client.session.<a href="./src/resources/session.ts">delete</a>(id) -> SessionDeleteResponse</code>
- <code title="post /session/{id}/abort">client.session.<a href="./src/resources/session.ts">abort</a>(id) -> SessionAbortResponse</code>
- <code title="post /session/{id}/message">client.session.<a href="./src/resources/session.ts">chat</a>(id, { ...params }) -> AssistantMessage</code>
- <code title="post /session/{id}/init">client.session.<a href="./src/resources/session.ts">init</a>(id, { ...params }) -> SessionInitResponse</code>
- <code title="get /session/{id}/message">client.session.<a href="./src/resources/session.ts">messages</a>(id) -> SessionMessagesResponse</code>
- <code title="post /session/{id}/revert">client.session.<a href="./src/resources/session.ts">revert</a>(id, { ...params }) -> Session</code>
- <code title="post /session/{id}/share">client.session.<a href="./src/resources/session.ts">share</a>(id) -> Session</code>
- <code title="post /session/{id}/summarize">client.session.<a href="./src/resources/session.ts">summarize</a>(id, { ...params }) -> SessionSummarizeResponse</code>
- <code title="post /session/{id}/unrevert">client.session.<a href="./src/resources/session.ts">unrevert</a>(id) -> Session</code>
- <code title="delete /session/{id}/share">client.session.<a href="./src/resources/session.ts">unshare</a>(id) -> Session</code>
# Tui
Types:
- <code><a href="./src/resources/tui.ts">TuiAppendPromptResponse</a></code>
- <code><a href="./src/resources/tui.ts">TuiOpenHelpResponse</a></code>
Methods:
- <code title="post /tui/append-prompt">client.tui.<a href="./src/resources/tui.ts">appendPrompt</a>({ ...params }) -> TuiAppendPromptResponse</code>
- <code title="post /tui/open-help">client.tui.<a href="./src/resources/tui.ts">openHelp</a>() -> TuiOpenHelpResponse</code>

View file

@ -1,22 +0,0 @@
#!/usr/bin/env bash
errors=()
if [ -z "${NPM_TOKEN}" ]; then
errors+=("The NPM_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
lenErrors=${#errors[@]}
if [[ lenErrors -gt 0 ]]; then
echo -e "Found the following errors in the release environment:\n"
for error in "${errors[@]}"; do
echo -e "- $error\n"
done
exit 1
fi
echo "The environment is ready to push releases!"

View file

@ -1,61 +0,0 @@
#!/usr/bin/env bash
set -eux
npm config set '//registry.npmjs.org/:_authToken' "$NPM_TOKEN"
yarn build
cd dist
# Get package name and version from package.json
PACKAGE_NAME="$(jq -r -e '.name' ./package.json)"
VERSION="$(jq -r -e '.version' ./package.json)"
# Get latest version from npm
#
# If the package doesn't exist, npm will return:
# {
# "error": {
# "code": "E404",
# "summary": "Unpublished on 2025-06-05T09:54:53.528Z",
# "detail": "'the_package' is not in this registry..."
# }
# }
NPM_INFO="$(npm view "$PACKAGE_NAME" version --json 2>/dev/null || true)"
# Check if we got an E404 error
if echo "$NPM_INFO" | jq -e '.error.code == "E404"' > /dev/null 2>&1; then
# Package doesn't exist yet, no last version
LAST_VERSION=""
elif echo "$NPM_INFO" | jq -e '.error' > /dev/null 2>&1; then
# Report other errors
echo "ERROR: npm returned unexpected data:"
echo "$NPM_INFO"
exit 1
else
# Success - get the version
LAST_VERSION=$(echo "$NPM_INFO" | jq -r '.') # strip quotes
fi
# Check if current version is pre-release (e.g. alpha / beta / rc)
CURRENT_IS_PRERELEASE=false
if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then
CURRENT_IS_PRERELEASE=true
CURRENT_TAG="${BASH_REMATCH[1]}"
fi
# Check if last version is a stable release
LAST_IS_STABLE_RELEASE=true
if [[ -z "$LAST_VERSION" || "$LAST_VERSION" =~ -([a-zA-Z]+) ]]; then
LAST_IS_STABLE_RELEASE=false
fi
# Use a corresponding alpha/beta tag if there already is a stable release and we're publishing a prerelease.
if $CURRENT_IS_PRERELEASE && $LAST_IS_STABLE_RELEASE; then
TAG="$CURRENT_TAG"
else
TAG="latest"
fi
# Publish with the appropriate tag
yarn publish --access public --tag "$TAG"

View file

@ -1,42 +0,0 @@
// @ts-check
import tseslint from 'typescript-eslint';
import unusedImports from 'eslint-plugin-unused-imports';
import prettier from 'eslint-plugin-prettier';
export default tseslint.config(
{
languageOptions: {
parser: tseslint.parser,
parserOptions: { sourceType: 'module' },
},
files: ['**/*.ts', '**/*.mts', '**/*.cts', '**/*.js', '**/*.mjs', '**/*.cjs'],
ignores: ['dist/'],
plugins: {
'@typescript-eslint': tseslint.plugin,
'unused-imports': unusedImports,
prettier,
},
rules: {
'no-unused-vars': 'off',
'prettier/prettier': 'error',
'unused-imports/no-unused-imports': 'error',
'no-restricted-imports': [
'error',
{
patterns: [
{
regex: '^@opencode-ai/sdk(/.*)?',
message: 'Use a relative import, not a package import.',
},
],
},
],
},
},
{
files: ['tests/**', 'examples/**'],
rules: {
'no-restricted-imports': 'off',
},
},
);

View 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"
}

View 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/sdk/go/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.prism.log
codegen.log
Brewfile.lock.json
.idea/

View file

@ -0,0 +1,3 @@
{
".": "0.1.0-alpha.8"
}

View file

@ -0,0 +1,4 @@
configured_endpoints: 26
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-5bf6a39123d248d306490c1dee61b46ba113ea2c415a4de1a631c76462769c49.yml
openapi_spec_hash: 3c5b25f121429281275ffd70c9d5cfe4
config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3

1
packages/sdk/go/Brewfile Normal file
View file

@ -0,0 +1 @@
brew "go"

View 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))

View 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
```

354
packages/sdk/go/README.md Normal file
View 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()
sessions, err := client.Session.List(context.TODO())
if err != nil {
panic(err.Error())
}
fmt.Printf("%+v\n", sessions)
}
```
### 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.Session.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.Session.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 "/session": 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.Session.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.Session.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
sessions, err := client.Session.List(context.TODO(), option.WithResponseInto(&response))
if err != nil {
// handle error
}
fmt.Printf("%+v\n", sessions)
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).

View file

@ -0,0 +1,43 @@
// 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 MessageAbortedError = shared.MessageAbortedError
// This is an alias to an internal type.
type MessageAbortedErrorName = shared.MessageAbortedErrorName
// This is an alias to an internal value.
const MessageAbortedErrorNameMessageAbortedError = shared.MessageAbortedErrorNameMessageAbortedError
// 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

128
packages/sdk/go/api.md Normal file
View file

@ -0,0 +1,128 @@
# 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#MessageAbortedError">MessageAbortedError</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#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>
- <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#Mode">Mode</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#AppProvidersResponse">AppProvidersResponse</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>
- <code title="post /log">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Log">Log</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</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#AppLogParams">AppLogParams</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 /mode">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Modes">Modes</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#Mode">Mode</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="get /config/providers">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.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#AppProvidersResponse">AppProvidersResponse</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#Symbol">Symbol</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#Symbol">Symbol</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#File">File</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>
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#File">File</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#KeybindsConfig">KeybindsConfig</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#McpLocalConfig">McpLocalConfig</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#McpRemoteConfig">McpRemoteConfig</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#ModeConfig">ModeConfig</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>
# 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#FilePartInputParam">FilePartInputParam</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#FilePartSourceUnionParam">FilePartSourceUnionParam</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#FilePartSourceTextParam">FilePartSourceTextParam</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#FileSourceParam">FileSourceParam</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#SymbolSourceParam">SymbolSourceParam</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#TextPartInputParam">TextPartInputParam</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#AssistantMessage">AssistantMessage</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#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#FilePartSource">FilePartSource</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#FilePartSourceText">FilePartSourceText</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#FileSource">FileSource</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#Part">Part</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#SnapshotPart">SnapshotPart</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#StepFinishPart">StepFinishPart</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#SymbolSource">SymbolSource</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#ToolPart">ToolPart</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#ToolStateCompleted">ToolStateCompleted</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#ToolStateError">ToolStateError</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#ToolStatePending">ToolStatePending</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#ToolStateRunning">ToolStateRunning</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#UserMessage">UserMessage</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#SessionMessagesResponse">SessionMessagesResponse</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#AssistantMessage">AssistantMessage</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#SessionMessagesResponse">SessionMessagesResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/revert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Revert">Revert</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#SessionRevertParams">SessionRevertParams</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}/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="post /session/{id}/unrevert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unrevert">Unrevert</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="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>
# Tui
Methods:
- <code title="post /tui/append-prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.AppendPrompt">AppendPrompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</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#TuiAppendPromptParams">TuiAppendPromptParams</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 /tui/open-help">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenHelp">OpenHelp</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>

368
packages/sdk/go/app.go Normal file
View file

@ -0,0 +1,368 @@
// 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/param"
"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
}
// Write a log entry to the server logs
func (r *AppService) Log(ctx context.Context, body AppLogParams, opts ...option.RequestOption) (res *bool, err error) {
opts = append(r.Options[:], opts...)
path := "log"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
return
}
// List all modes
func (r *AppService) Modes(ctx context.Context, opts ...option.RequestOption) (res *[]Mode, err error) {
opts = append(r.Options[:], opts...)
path := "mode"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
return
}
// List all providers
func (r *AppService) Providers(ctx context.Context, opts ...option.RequestOption) (res *AppProvidersResponse, err error) {
opts = append(r.Options[:], opts...)
path := "config/providers"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, 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"`
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
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
}
type Mode struct {
Name string `json:"name,required"`
Tools map[string]bool `json:"tools,required"`
Model ModeModel `json:"model"`
Prompt string `json:"prompt"`
Temperature float64 `json:"temperature"`
JSON modeJSON `json:"-"`
}
// modeJSON contains the JSON metadata for the struct [Mode]
type modeJSON struct {
Name apijson.Field
Tools apijson.Field
Model apijson.Field
Prompt apijson.Field
Temperature apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *Mode) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r modeJSON) RawJSON() string {
return r.raw
}
type ModeModel struct {
ModelID string `json:"modelID,required"`
ProviderID string `json:"providerID,required"`
JSON modeModelJSON `json:"-"`
}
// modeModelJSON contains the JSON metadata for the struct [ModeModel]
type modeModelJSON struct {
ModelID apijson.Field
ProviderID apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ModeModel) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r modeModelJSON) RawJSON() string {
return r.raw
}
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 AppProvidersResponse struct {
Default map[string]string `json:"default,required"`
Providers []Provider `json:"providers,required"`
JSON appProvidersResponseJSON `json:"-"`
}
// appProvidersResponseJSON contains the JSON metadata for the struct
// [AppProvidersResponse]
type appProvidersResponseJSON struct {
Default apijson.Field
Providers apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *AppProvidersResponse) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r appProvidersResponseJSON) RawJSON() string {
return r.raw
}
type AppLogParams struct {
// Log level
Level param.Field[AppLogParamsLevel] `json:"level,required"`
// Log message
Message param.Field[string] `json:"message,required"`
// Service name for the log entry
Service param.Field[string] `json:"service,required"`
// Additional metadata for the log entry
Extra param.Field[map[string]interface{}] `json:"extra"`
}
func (r AppLogParams) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
// Log level
type AppLogParamsLevel string
const (
AppLogParamsLevelDebug AppLogParamsLevel = "debug"
AppLogParamsLevelInfo AppLogParamsLevel = "info"
AppLogParamsLevelError AppLogParamsLevel = "error"
AppLogParamsLevelWarn AppLogParamsLevel = "warn"
)
func (r AppLogParamsLevel) IsKnown() bool {
switch r {
case AppLogParamsLevelDebug, AppLogParamsLevelInfo, AppLogParamsLevelError, AppLogParamsLevelWarn:
return true
}
return false
}

131
packages/sdk/go/app_test.go Normal file
View file

@ -0,0 +1,131 @@
// 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())
}
}
func TestAppLogWithOptionalParams(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.Log(context.TODO(), opencode.AppLogParams{
Level: opencode.F(opencode.AppLogParamsLevelDebug),
Message: opencode.F("message"),
Service: opencode.F("service"),
Extra: opencode.F(map[string]interface{}{
"foo": "bar",
}),
})
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 TestAppModes(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.Modes(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 TestAppProviders(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.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())
}
}

125
packages/sdk/go/client.go Normal file
View file

@ -0,0 +1,125 @@
// 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
Tui *TuiService
}
// 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...)
r.Tui = NewTuiService(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...)
}

View 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.Session.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.Session.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.Session.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.Session.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.Session.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.Session.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.Session.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.Session.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 }

887
packages/sdk/go/config.go Normal file
View file

@ -0,0 +1,887 @@
// 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
}
type Config struct {
// JSON schema reference for configuration validation
Schema string `json:"$schema"`
// Modes configuration, see https://opencode.ai/docs/modes
Agent ConfigAgent `json:"agent"`
// @deprecated Use 'share' field instead. 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 KeybindsConfig `json:"keybinds"`
// @deprecated Always uses stretch layout.
Layout ConfigLayout `json:"layout"`
// MCP (Model Context Protocol) server configurations
Mcp map[string]ConfigMcp `json:"mcp"`
// Modes configuration, see https://opencode.ai/docs/modes
Mode ConfigMode `json:"mode"`
// Model to use in the format of provider/model, eg anthropic/claude-2
Model string `json:"model"`
Permission ConfigPermission `json:"permission"`
// Custom provider configurations and model overrides
Provider map[string]ConfigProvider `json:"provider"`
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
// enables automatic sharing, 'disabled' disables all sharing
Share ConfigShare `json:"share"`
// Small model to use for tasks like summarization and title generation in the
// format of provider/model
SmallModel string `json:"small_model"`
// Theme name to use for the interface
Theme string `json:"theme"`
// Custom username to display in conversations instead of system username
Username string `json:"username"`
JSON configJSON `json:"-"`
}
// configJSON contains the JSON metadata for the struct [Config]
type configJSON struct {
Schema apijson.Field
Agent apijson.Field
Autoshare apijson.Field
Autoupdate apijson.Field
DisabledProviders apijson.Field
Experimental apijson.Field
Instructions apijson.Field
Keybinds apijson.Field
Layout apijson.Field
Mcp apijson.Field
Mode apijson.Field
Model apijson.Field
Permission apijson.Field
Provider apijson.Field
Share apijson.Field
SmallModel apijson.Field
Theme apijson.Field
Username 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
}
// Modes configuration, see https://opencode.ai/docs/modes
type ConfigAgent struct {
General ConfigAgentGeneral `json:"general"`
ExtraFields map[string]ConfigAgent `json:"-,extras"`
JSON configAgentJSON `json:"-"`
}
// configAgentJSON contains the JSON metadata for the struct [ConfigAgent]
type configAgentJSON struct {
General apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigAgent) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configAgentJSON) RawJSON() string {
return r.raw
}
type ConfigAgentGeneral struct {
Description string `json:"description,required"`
JSON configAgentGeneralJSON `json:"-"`
ModeConfig
}
// configAgentGeneralJSON contains the JSON metadata for the struct
// [ConfigAgentGeneral]
type configAgentGeneralJSON struct {
Description apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigAgentGeneral) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configAgentGeneralJSON) 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
}
// @deprecated Always uses stretch layout.
type ConfigLayout string
const (
ConfigLayoutAuto ConfigLayout = "auto"
ConfigLayoutStretch ConfigLayout = "stretch"
)
func (r ConfigLayout) IsKnown() bool {
switch r {
case ConfigLayoutAuto, ConfigLayoutStretch:
return true
}
return false
}
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"`
// This field can have the runtime type of [map[string]string].
Headers interface{} `json:"headers"`
// 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
Headers 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 [McpLocalConfig], [McpRemoteConfig].
func (r ConfigMcp) AsUnion() ConfigMcpUnion {
return r.union
}
// Union satisfied by [McpLocalConfig] or [McpRemoteConfig].
type ConfigMcpUnion interface {
implementsConfigMcp()
}
func init() {
apijson.RegisterUnion(
reflect.TypeOf((*ConfigMcpUnion)(nil)).Elem(),
"type",
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(McpLocalConfig{}),
DiscriminatorValue: "local",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(McpRemoteConfig{}),
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
}
// Modes configuration, see https://opencode.ai/docs/modes
type ConfigMode struct {
Build ModeConfig `json:"build"`
Plan ModeConfig `json:"plan"`
ExtraFields map[string]ModeConfig `json:"-,extras"`
JSON configModeJSON `json:"-"`
}
// configModeJSON contains the JSON metadata for the struct [ConfigMode]
type configModeJSON struct {
Build apijson.Field
Plan apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigMode) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configModeJSON) RawJSON() string {
return r.raw
}
type ConfigPermission struct {
Bash ConfigPermissionBashUnion `json:"bash"`
Edit ConfigPermissionEdit `json:"edit"`
JSON configPermissionJSON `json:"-"`
}
// configPermissionJSON contains the JSON metadata for the struct
// [ConfigPermission]
type configPermissionJSON struct {
Bash apijson.Field
Edit apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigPermission) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configPermissionJSON) RawJSON() string {
return r.raw
}
// Union satisfied by [ConfigPermissionBashString] or [ConfigPermissionBashMap].
type ConfigPermissionBashUnion interface {
implementsConfigPermissionBashUnion()
}
func init() {
apijson.RegisterUnion(
reflect.TypeOf((*ConfigPermissionBashUnion)(nil)).Elem(),
"",
apijson.UnionVariant{
TypeFilter: gjson.String,
Type: reflect.TypeOf(ConfigPermissionBashString("")),
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(ConfigPermissionBashMap{}),
},
)
}
type ConfigPermissionBashString string
const (
ConfigPermissionBashStringAsk ConfigPermissionBashString = "ask"
ConfigPermissionBashStringAllow ConfigPermissionBashString = "allow"
)
func (r ConfigPermissionBashString) IsKnown() bool {
switch r {
case ConfigPermissionBashStringAsk, ConfigPermissionBashStringAllow:
return true
}
return false
}
func (r ConfigPermissionBashString) implementsConfigPermissionBashUnion() {}
type ConfigPermissionBashMap map[string]ConfigPermissionBashMapItem
func (r ConfigPermissionBashMap) implementsConfigPermissionBashUnion() {}
type ConfigPermissionBashMapItem string
const (
ConfigPermissionBashMapAsk ConfigPermissionBashMapItem = "ask"
ConfigPermissionBashMapAllow ConfigPermissionBashMapItem = "allow"
)
func (r ConfigPermissionBashMapItem) IsKnown() bool {
switch r {
case ConfigPermissionBashMapAsk, ConfigPermissionBashMapAllow:
return true
}
return false
}
type ConfigPermissionEdit string
const (
ConfigPermissionEditAsk ConfigPermissionEdit = "ask"
ConfigPermissionEditAllow ConfigPermissionEdit = "allow"
)
func (r ConfigPermissionEdit) IsKnown() bool {
switch r {
case ConfigPermissionEditAsk, ConfigPermissionEditAllow:
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 ConfigProviderOptions `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 ConfigProviderOptions struct {
APIKey string `json:"apiKey"`
BaseURL string `json:"baseURL"`
ExtraFields map[string]interface{} `json:"-,extras"`
JSON configProviderOptionsJSON `json:"-"`
}
// configProviderOptionsJSON contains the JSON metadata for the struct
// [ConfigProviderOptions]
type configProviderOptionsJSON struct {
APIKey apijson.Field
BaseURL apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigProviderOptions) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configProviderOptionsJSON) RawJSON() string {
return r.raw
}
// Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
// enables automatic sharing, 'disabled' disables all sharing
type ConfigShare string
const (
ConfigShareManual ConfigShare = "manual"
ConfigShareAuto ConfigShare = "auto"
ConfigShareDisabled ConfigShare = "disabled"
)
func (r ConfigShare) IsKnown() bool {
switch r {
case ConfigShareManual, ConfigShareAuto, ConfigShareDisabled:
return true
}
return false
}
type KeybindsConfig struct {
// Exit the application
AppExit string `json:"app_exit,required"`
// Show help dialog
AppHelp string `json:"app_help,required"`
// Open external editor
EditorOpen string `json:"editor_open,required"`
// Close file
FileClose string `json:"file_close,required"`
// Split/unified diff
FileDiffToggle string `json:"file_diff_toggle,required"`
// List files
FileList string `json:"file_list,required"`
// Search file
FileSearch string `json:"file_search,required"`
// Clear input field
InputClear string `json:"input_clear,required"`
// Insert newline in input
InputNewline string `json:"input_newline,required"`
// Paste from clipboard
InputPaste string `json:"input_paste,required"`
// Submit input
InputSubmit string `json:"input_submit,required"`
// Leader key for keybind combinations
Leader string `json:"leader,required"`
// Copy message
MessagesCopy string `json:"messages_copy,required"`
// Navigate to first message
MessagesFirst string `json:"messages_first,required"`
// Scroll messages down by half page
MessagesHalfPageDown string `json:"messages_half_page_down,required"`
// Scroll messages up by half page
MessagesHalfPageUp string `json:"messages_half_page_up,required"`
// Navigate to last message
MessagesLast string `json:"messages_last,required"`
// Toggle layout
MessagesLayoutToggle string `json:"messages_layout_toggle,required"`
// Navigate to next message
MessagesNext string `json:"messages_next,required"`
// Scroll messages down by one page
MessagesPageDown string `json:"messages_page_down,required"`
// Scroll messages up by one page
MessagesPageUp string `json:"messages_page_up,required"`
// Navigate to previous message
MessagesPrevious string `json:"messages_previous,required"`
// Redo message
MessagesRedo string `json:"messages_redo,required"`
// @deprecated use messages_undo. Revert message
MessagesRevert string `json:"messages_revert,required"`
// Undo message
MessagesUndo string `json:"messages_undo,required"`
// List available models
ModelList string `json:"model_list,required"`
// Create/update AGENTS.md
ProjectInit string `json:"project_init,required"`
// Compact the session
SessionCompact string `json:"session_compact,required"`
// Export session to editor
SessionExport string `json:"session_export,required"`
// Interrupt current session
SessionInterrupt string `json:"session_interrupt,required"`
// List all sessions
SessionList string `json:"session_list,required"`
// Create a new session
SessionNew string `json:"session_new,required"`
// Share current session
SessionShare string `json:"session_share,required"`
// Unshare current session
SessionUnshare string `json:"session_unshare,required"`
// Next mode
SwitchMode string `json:"switch_mode,required"`
// Previous Mode
SwitchModeReverse string `json:"switch_mode_reverse,required"`
// List available themes
ThemeList string `json:"theme_list,required"`
// Toggle tool details
ToolDetails string `json:"tool_details,required"`
JSON keybindsConfigJSON `json:"-"`
}
// keybindsConfigJSON contains the JSON metadata for the struct [KeybindsConfig]
type keybindsConfigJSON struct {
AppExit apijson.Field
AppHelp apijson.Field
EditorOpen apijson.Field
FileClose apijson.Field
FileDiffToggle apijson.Field
FileList apijson.Field
FileSearch apijson.Field
InputClear apijson.Field
InputNewline apijson.Field
InputPaste apijson.Field
InputSubmit apijson.Field
Leader apijson.Field
MessagesCopy apijson.Field
MessagesFirst apijson.Field
MessagesHalfPageDown apijson.Field
MessagesHalfPageUp apijson.Field
MessagesLast apijson.Field
MessagesLayoutToggle apijson.Field
MessagesNext apijson.Field
MessagesPageDown apijson.Field
MessagesPageUp apijson.Field
MessagesPrevious apijson.Field
MessagesRedo apijson.Field
MessagesRevert apijson.Field
MessagesUndo apijson.Field
ModelList apijson.Field
ProjectInit apijson.Field
SessionCompact apijson.Field
SessionExport apijson.Field
SessionInterrupt apijson.Field
SessionList apijson.Field
SessionNew apijson.Field
SessionShare apijson.Field
SessionUnshare apijson.Field
SwitchMode apijson.Field
SwitchModeReverse apijson.Field
ThemeList apijson.Field
ToolDetails apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *KeybindsConfig) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r keybindsConfigJSON) RawJSON() string {
return r.raw
}
type McpLocalConfig struct {
// Command and arguments to run the MCP server
Command []string `json:"command,required"`
// Type of MCP server connection
Type McpLocalConfigType `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 mcpLocalConfigJSON `json:"-"`
}
// mcpLocalConfigJSON contains the JSON metadata for the struct [McpLocalConfig]
type mcpLocalConfigJSON struct {
Command apijson.Field
Type apijson.Field
Enabled apijson.Field
Environment apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *McpLocalConfig) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r mcpLocalConfigJSON) RawJSON() string {
return r.raw
}
func (r McpLocalConfig) implementsConfigMcp() {}
// Type of MCP server connection
type McpLocalConfigType string
const (
McpLocalConfigTypeLocal McpLocalConfigType = "local"
)
func (r McpLocalConfigType) IsKnown() bool {
switch r {
case McpLocalConfigTypeLocal:
return true
}
return false
}
type McpRemoteConfig struct {
// Type of MCP server connection
Type McpRemoteConfigType `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"`
// Headers to send with the request
Headers map[string]string `json:"headers"`
JSON mcpRemoteConfigJSON `json:"-"`
}
// mcpRemoteConfigJSON contains the JSON metadata for the struct [McpRemoteConfig]
type mcpRemoteConfigJSON struct {
Type apijson.Field
URL apijson.Field
Enabled apijson.Field
Headers apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *McpRemoteConfig) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r mcpRemoteConfigJSON) RawJSON() string {
return r.raw
}
func (r McpRemoteConfig) implementsConfigMcp() {}
// Type of MCP server connection
type McpRemoteConfigType string
const (
McpRemoteConfigTypeRemote McpRemoteConfigType = "remote"
)
func (r McpRemoteConfigType) IsKnown() bool {
switch r {
case McpRemoteConfigTypeRemote:
return true
}
return false
}
type ModeConfig struct {
Disable bool `json:"disable"`
Model string `json:"model"`
Prompt string `json:"prompt"`
Temperature float64 `json:"temperature"`
Tools map[string]bool `json:"tools"`
JSON modeConfigJSON `json:"-"`
}
// modeConfigJSON contains the JSON metadata for the struct [ModeConfig]
type modeConfigJSON struct {
Disable apijson.Field
Model apijson.Field
Prompt apijson.Field
Temperature apijson.Field
Tools apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ModeConfig) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r modeConfigJSON) RawJSON() string {
return r.raw
}

View file

@ -0,0 +1,36 @@
// 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())
}
}

1373
packages/sdk/go/event.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
File generated from our OpenAPI spec by Stainless. File generated from our OpenAPI spec by Stainless.
This directory can be used to store example files demonstrating usage of this SDK. 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. It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.

50
packages/sdk/go/field.go Normal file
View 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 }

142
packages/sdk/go/file.go Normal file
View file

@ -0,0 +1,142 @@
// 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 *[]File, err error) {
opts = append(r.Options[:], opts...)
path := "file/status"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
return
}
type File struct {
Added int64 `json:"added,required"`
Path string `json:"path,required"`
Removed int64 `json:"removed,required"`
Status FileStatus `json:"status,required"`
JSON fileJSON `json:"-"`
}
// fileJSON contains the JSON metadata for the struct [File]
type fileJSON struct {
Added apijson.Field
Path apijson.Field
Removed apijson.Field
Status apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *File) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r fileJSON) RawJSON() string {
return r.raw
}
type FileStatus string
const (
FileStatusAdded FileStatus = "added"
FileStatusDeleted FileStatus = "deleted"
FileStatusModified FileStatus = "modified"
)
func (r FileStatus) IsKnown() bool {
switch r {
case FileStatusAdded, FileStatusDeleted, FileStatusModified:
return true
}
return false
}
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 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,
})
}

View 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())
}
}

326
packages/sdk/go/find.go Normal file
View file

@ -0,0 +1,326 @@
// 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 *[]Symbol, 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 Symbol struct {
Kind float64 `json:"kind,required"`
Location SymbolLocation `json:"location,required"`
Name string `json:"name,required"`
JSON symbolJSON `json:"-"`
}
// symbolJSON contains the JSON metadata for the struct [Symbol]
type symbolJSON struct {
Kind apijson.Field
Location apijson.Field
Name apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *Symbol) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r symbolJSON) RawJSON() string {
return r.raw
}
type SymbolLocation struct {
Range SymbolLocationRange `json:"range,required"`
Uri string `json:"uri,required"`
JSON symbolLocationJSON `json:"-"`
}
// symbolLocationJSON contains the JSON metadata for the struct [SymbolLocation]
type symbolLocationJSON struct {
Range apijson.Field
Uri apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SymbolLocation) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r symbolLocationJSON) RawJSON() string {
return r.raw
}
type SymbolLocationRange struct {
End SymbolLocationRangeEnd `json:"end,required"`
Start SymbolLocationRangeStart `json:"start,required"`
JSON symbolLocationRangeJSON `json:"-"`
}
// symbolLocationRangeJSON contains the JSON metadata for the struct
// [SymbolLocationRange]
type symbolLocationRangeJSON struct {
End apijson.Field
Start apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SymbolLocationRange) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r symbolLocationRangeJSON) RawJSON() string {
return r.raw
}
type SymbolLocationRangeEnd struct {
Character float64 `json:"character,required"`
Line float64 `json:"line,required"`
JSON symbolLocationRangeEndJSON `json:"-"`
}
// symbolLocationRangeEndJSON contains the JSON metadata for the struct
// [SymbolLocationRangeEnd]
type symbolLocationRangeEndJSON struct {
Character apijson.Field
Line apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SymbolLocationRangeEnd) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r symbolLocationRangeEndJSON) RawJSON() string {
return r.raw
}
type SymbolLocationRangeStart struct {
Character float64 `json:"character,required"`
Line float64 `json:"line,required"`
JSON symbolLocationRangeStartJSON `json:"-"`
}
// symbolLocationRangeStartJSON contains the JSON metadata for the struct
// [SymbolLocationRangeStart]
type symbolLocationRangeStartJSON struct {
Character apijson.Field
Line apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *SymbolLocationRangeStart) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r symbolLocationRangeStartJSON) RawJSON() string {
return r.raw
}
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,
})
}

View 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/sdk/go/go.mod Normal file
View 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/sdk/go/go.sum Normal file
View 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=

View 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
}

View 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)
}
}

View file

@ -0,0 +1,5 @@
package apiform
type Marshaler interface {
MarshalMultipart() ([]byte, string, error)
}

View 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))
}
})
}
}

View 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
}

View 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
}

View 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(".", "\\.", ":", "\\:", "*", "\\*")

View 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
}

View 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))
}
})
}
}

View 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))
}
})
}
}

View 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
}

View 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())
}
})
}
}

View 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)
}

View 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
}

View 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)
}
}

View 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
)

View 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)
}
})
}
}

View 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
}

View 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)
}

View 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
})
}

View 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
}

View 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

View file

@ -1,4 +1,4 @@
File generated from our OpenAPI spec by Stainless. File generated from our OpenAPI spec by Stainless.
This directory can be used to store custom files to expand the SDK. 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. It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.

View 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
})
}

View file

@ -0,0 +1,267 @@
// 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)
if err == nil && u.Path != "" && !strings.HasSuffix(u.Path, "/") {
u.Path += "/"
}
return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
if err != nil {
return fmt.Errorf("requestoption: WithBaseURL failed to parse url %s", err)
}
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/")
}

View 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<<9)
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()
}

View file

@ -59,6 +59,9 @@
"hidden": true "hidden": true
} }
], ],
"release-type": "node", "release-type": "go",
"extra-files": ["src/version.ts", "README.md"] "extra-files": [
} "internal/version.go",
"README.md"
]
}

View file

@ -11,8 +11,6 @@ if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ];
} }
fi fi
echo "==> Installing Node dependencies…" echo "==> Installing Go dependencies…"
PACKAGE_MANAGER=$(command -v yarn >/dev/null 2>&1 && echo "yarn" || echo "npm") go mod tidy -e
$PACKAGE_MANAGER install

8
packages/sdk/go/scripts/format Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
echo "==> Running gofmt -s -w"
gofmt -s -w .

11
packages/sdk/go/scripts/lint Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
echo "==> Running Go build"
go build ./...
echo "==> Checking tests compile"
go test -run=^$ ./...

View file

@ -53,4 +53,4 @@ else
fi fi
echo "==> Running tests" echo "==> Running tests"
./node_modules/.bin/jest "$@" go test ./... "$@"

2117
packages/sdk/go/session.go Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,323 @@
// 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 TestSessionChatWithOptionalParams(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.SessionChatParamsPartUnion{opencode.TextPartInputParam{
Text: opencode.F("text"),
Type: opencode.F(opencode.TextPartInputTypeText),
ID: opencode.F("id"),
Synthetic: opencode.F(true),
Time: opencode.F(opencode.TextPartInputTimeParam{
Start: opencode.F(0.000000),
End: opencode.F(0.000000),
}),
}}),
ProviderID: opencode.F("providerID"),
MessageID: opencode.F("msg"),
Mode: opencode.F("mode"),
System: opencode.F("system"),
Tools: opencode.F(map[string]bool{
"foo": true,
}),
},
)
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{
MessageID: opencode.F("messageID"),
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 TestSessionRevertWithOptionalParams(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.Revert(
context.TODO(),
"id",
opencode.SessionRevertParams{
MessageID: opencode.F("msg"),
PartID: opencode.F("prt"),
},
)
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 TestSessionUnrevert(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.Unrevert(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 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())
}
}

View file

@ -0,0 +1,173 @@
// 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 MessageAbortedError struct {
Data interface{} `json:"data,required"`
Name MessageAbortedErrorName `json:"name,required"`
JSON messageAbortedErrorJSON `json:"-"`
}
// messageAbortedErrorJSON contains the JSON metadata for the struct
// [MessageAbortedError]
type messageAbortedErrorJSON struct {
Data apijson.Field
Name apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *MessageAbortedError) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r messageAbortedErrorJSON) RawJSON() string {
return r.raw
}
func (r MessageAbortedError) ImplementsEventListResponseEventSessionErrorPropertiesError() {}
func (r MessageAbortedError) ImplementsAssistantMessageError() {}
type MessageAbortedErrorName string
const (
MessageAbortedErrorNameMessageAbortedError MessageAbortedErrorName = "MessageAbortedError"
)
func (r MessageAbortedErrorName) IsKnown() bool {
switch r {
case MessageAbortedErrorNameMessageAbortedError:
return true
}
return false
}
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) ImplementsAssistantMessageError() {}
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) ImplementsAssistantMessageError() {}
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
}

56
packages/sdk/go/tui.go Normal file
View file

@ -0,0 +1,56 @@
// 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/param"
"github.com/sst/opencode-sdk-go/internal/requestconfig"
"github.com/sst/opencode-sdk-go/option"
)
// TuiService 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 [NewTuiService] method instead.
type TuiService struct {
Options []option.RequestOption
}
// NewTuiService 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 NewTuiService(opts ...option.RequestOption) (r *TuiService) {
r = &TuiService{}
r.Options = opts
return
}
// Append prompt to the TUI
func (r *TuiService) AppendPrompt(ctx context.Context, body TuiAppendPromptParams, opts ...option.RequestOption) (res *bool, err error) {
opts = append(r.Options[:], opts...)
path := "tui/append-prompt"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
return
}
// Open the help dialog
func (r *TuiService) OpenHelp(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
opts = append(r.Options[:], opts...)
path := "tui/open-help"
err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
return
}
type TuiAppendPromptParams struct {
Text param.Field[string] `json:"text,required"`
}
func (r TuiAppendPromptParams) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}

View 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 TestTuiAppendPrompt(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.Tui.AppendPrompt(context.TODO(), opencode.TuiAppendPromptParams{
Text: opencode.F("text"),
})
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 TestTuiOpenHelp(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.Tui.OpenHelp(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())
}
}

View 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),
)
sessions, err := client.Session.List(context.TODO())
if err != nil {
t.Error(err)
return
}
t.Logf("%+v\n", sessions)
}

View file

@ -1,23 +0,0 @@
import type { JestConfigWithTsJest } from 'ts-jest';
const config: JestConfigWithTsJest = {
preset: 'ts-jest/presets/default-esm',
testEnvironment: 'node',
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest', { sourceMaps: 'inline' }],
},
moduleNameMapper: {
'^@opencode-ai/sdk$': '<rootDir>/src/index.ts',
'^@opencode-ai/sdk/(.*)$': '<rootDir>/src/$1',
},
modulePathIgnorePatterns: [
'<rootDir>/ecosystem-tests/',
'<rootDir>/dist/',
'<rootDir>/deno/',
'<rootDir>/deno_tests/',
'<rootDir>/packages/',
],
testPathIgnorePatterns: ['scripts'],
};
export default config;

View file

@ -0,0 +1,17 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.0.0",
"type": "module",
"exports": {
".": "./dist/index.js"
},
"files": [
"dist"
],
"devDependencies": {
"typescript": "catalog:",
"@hey-api/openapi-ts": "0.80.1",
"@tsconfig/node22": "catalog:"
}
}

View file

@ -0,0 +1,41 @@
#!/usr/bin/env bun
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
console.log("=== Generating JS SDK ===")
console.log()
import { createClient } from "@hey-api/openapi-ts"
await fs.rm(path.join(dir, "src/gen"), { recursive: true, force: true })
await $`bun run ../../opencode/src/index.ts generate > openapi.json`
await createClient({
input: "./openapi.json",
output: "./src/gen",
plugins: [
{
name: "@hey-api/typescript",
exportFromIndex: false,
},
{
name: "@hey-api/sdk",
instance: "OpencodeClient",
exportFromIndex: false,
auth: false,
},
{
name: "@hey-api/client-fetch",
exportFromIndex: false,
baseUrl: "http://localhost:4096",
},
],
})
await $`rm -rf dist`
await $`bun tsc`

View file

@ -0,0 +1,24 @@
#!/usr/bin/env bun
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import { $ } from "bun"
const version = process.env["OPENCODE_VERSION"]
if (!version) {
throw new Error("OPENCODE_VERSION is required")
}
await import("./generate")
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
await $`bun pm version --allow-same-version --no-git-tag-version ${version}`
if (snapshot) {
await $`bun publish --tag snapshot`
}
if (!snapshot) {
await $`bun publish`
}
await $`bun pm version 0.0.0 --no-git-tag-version`

View file

@ -0,0 +1,18 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { ClientOptions } from './types.gen';
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from './client';
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions>({
baseUrl: 'http://localhost:4096'
}));

View file

@ -0,0 +1,195 @@
import type { Client, Config, RequestOptions } from './types';
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from './utils';
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};
const interceptors = createInterceptors<
Request,
Response,
unknown,
RequestOptions
>();
const request: Client['request'] = async (options) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}
if (opts.requestValidator) {
await opts.requestValidator(opts);
}
if (opts.body && opts.bodySerializer) {
opts.body = opts.bodySerializer(opts.body);
}
// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.body === '') {
opts.headers.delete('Content-Type');
}
const url = buildUrl(opts);
const requestInit: ReqInit = {
redirect: 'follow',
...opts,
};
let request = new Request(url, requestInit);
for (const fn of interceptors.request._fns) {
if (fn) {
request = await fn(request, opts);
}
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response = await _fetch(request);
for (const fn of interceptors.response._fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
const result = {
request,
response,
};
if (response.ok) {
if (
response.status === 204 ||
response.headers.get('Content-Length') === '0'
) {
return opts.responseStyle === 'data'
? {}
: {
data: {},
...result,
};
}
const parseAs =
(opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';
let data: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'json':
case 'text':
data = await response[parseAs]();
break;
case 'stream':
return opts.responseStyle === 'data'
? response.body
: {
data: response.body,
...result,
};
}
if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === 'data'
? data
: {
data,
...result,
};
}
const textError = await response.text();
let jsonError: unknown;
try {
jsonError = JSON.parse(textError);
} catch {
// noop
}
const error = jsonError ?? textError;
let finalError = error;
for (const fn of interceptors.error._fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError;
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
...result,
};
};
return {
buildUrl,
connect: (options) => request({ ...options, method: 'CONNECT' }),
delete: (options) => request({ ...options, method: 'DELETE' }),
get: (options) => request({ ...options, method: 'GET' }),
getConfig,
head: (options) => request({ ...options, method: 'HEAD' }),
interceptors,
options: (options) => request({ ...options, method: 'OPTIONS' }),
patch: (options) => request({ ...options, method: 'PATCH' }),
post: (options) => request({ ...options, method: 'POST' }),
put: (options) => request({ ...options, method: 'PUT' }),
request,
setConfig,
trace: (options) => request({ ...options, method: 'TRACE' }),
};
};

View file

@ -0,0 +1,22 @@
export type { Auth } from '../core/auth';
export type { QuerySerializerOptions } from '../core/bodySerializer';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from '../core/bodySerializer';
export { buildClientParams } from '../core/params';
export { createClient } from './client';
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
OptionsLegacyParser,
RequestOptions,
RequestResult,
ResponseStyle,
TDataShape,
} from './types';
export { createConfig, mergeHeaders } from './utils';

View file

@ -0,0 +1,222 @@
import type { Auth } from '../core/auth';
import type {
Client as CoreClient,
Config as CoreConfig,
} from '../core/types';
import type { Middleware } from './utils';
export type ResponseStyle = 'data' | 'fields';
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
CoreConfig {
/**
* Base URL for all requests made by this client.
*/
baseUrl?: T['baseUrl'];
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: (request: Request) => ReturnType<typeof fetch>;
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
*
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/
next?: never;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
* You can override this behavior with any of the {@link Body} methods.
* Select `stream` if you don't want to parse response data at all.
*
* @default 'auto'
*/
parseAs?:
| 'arrayBuffer'
| 'auto'
| 'blob'
| 'formData'
| 'json'
| 'stream'
| 'text';
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
* @default 'fields'
*/
responseStyle?: ResponseStyle;
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T['throwOnError'];
}
export interface RequestOptions<
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends Config<{
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
}> {
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
}
export type RequestResult<
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
> = ThrowOnError extends true
? Promise<
TResponseStyle extends 'data'
? TData extends Record<string, unknown>
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends 'data'
?
| (TData extends Record<string, unknown>
? TData[keyof TData]
: TData)
| undefined
: (
| {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
error: undefined;
}
| {
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
}
) & {
request: Request;
response: Response;
}
>;
export interface ClientOptions {
baseUrl?: string;
responseStyle?: ResponseStyle;
throwOnError?: boolean;
}
type MethodFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type RequestFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> &
Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = <
TData extends {
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
url: string;
},
>(
options: Pick<TData, 'url'> & Options<TData>,
) => string;
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
interceptors: Middleware<Request, Response, unknown, RequestOptions>;
};
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
export interface TDataShape {
body?: unknown;
headers?: unknown;
path?: unknown;
query?: unknown;
url: string;
}
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
> = OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
'body' | 'path' | 'query' | 'url'
> &
Omit<TData, 'url'>;
export type OptionsLegacyParser<
TData = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
> = TData extends { body?: any }
? TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
'body' | 'headers' | 'url'
> &
TData
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'>
: TData extends { headers?: any }
? OmitKeys<
RequestOptions<TResponseStyle, ThrowOnError>,
'headers' | 'url'
> &
TData &
Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'>
: OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData;

View file

@ -0,0 +1,417 @@
import { getAuthToken } from '../core/auth';
import type {
QuerySerializer,
QuerySerializerOptions,
} from '../core/bodySerializer';
import { jsonBodySerializer } from '../core/bodySerializer';
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer';
import type { Client, ClientOptions, Config, RequestOptions } from './types';
interface PathSerializer {
path: Record<string, unknown>;
url: string;
}
const PATH_PARAM_RE = /\{[^{}]+\}/g;
type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
type MatrixStyle = 'label' | 'matrix' | 'simple';
type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = 'simple';
if (name.endsWith('*')) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith('.')) {
name = name.substring(1);
style = 'label';
} else if (name.startsWith(';')) {
name = name.substring(1);
style = 'matrix';
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(
match,
serializeArrayParam({ explode, name, style, value }),
);
continue;
}
if (typeof value === 'object') {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === 'matrix') {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === 'label' ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const createQuerySerializer = <T = unknown>({
allowReserved,
array,
object,
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
if (queryParams && typeof queryParams === 'object') {
for (const name in queryParams) {
const value = queryParams[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved,
explode: true,
name,
style: 'form',
value,
...array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === 'object') {
const serializedObject = serializeObjectParam({
allowReserved,
explode: true,
name,
style: 'deepObject',
value: value as Record<string, unknown>,
...object,
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join('&');
};
return querySerializer;
};
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (
contentType: string | null,
): Exclude<Config['parseAs'], 'auto'> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
return 'stream';
}
const cleanContent = contentType.split(';')[0]?.trim();
if (!cleanContent) {
return;
}
if (
cleanContent.startsWith('application/json') ||
cleanContent.endsWith('+json')
) {
return 'json';
}
if (cleanContent === 'multipart/form-data') {
return 'formData';
}
if (
['application/', 'audio/', 'image/', 'video/'].some((type) =>
cleanContent.startsWith(type),
)
) {
return 'blob';
}
if (cleanContent.startsWith('text/')) {
return 'text';
}
return;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
}) => {
for (const auth of security) {
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue;
}
const name = auth.name ?? 'Authorization';
switch (auth.in) {
case 'query':
if (!options.query) {
options.query = {};
}
options.query[name] = token;
break;
case 'cookie':
options.headers.append('Cookie', `${name}=${token}`);
break;
case 'header':
default:
options.headers.set(name, token);
break;
}
return;
}
};
export const buildUrl: Client['buildUrl'] = (options) => {
const url = getUrl({
baseUrl: options.baseUrl as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === 'function'
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? '') + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : '';
if (search.startsWith('?')) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseUrl?.endsWith('/')) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
export const mergeHeaders = (
...headers: Array<Required<Config>['headers'] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header || typeof header !== 'object') {
continue;
}
const iterator =
header instanceof Headers ? header.entries() : Object.entries(header);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, v as string);
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
typeof value === 'object' ? JSON.stringify(value) : (value as string),
);
}
}
}
return mergedHeaders;
};
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
options: Options,
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
class Interceptors<Interceptor> {
_fns: (Interceptor | null)[];
constructor() {
this._fns = [];
}
clear() {
this._fns = [];
}
getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === 'number') {
return this._fns[id] ? id : -1;
} else {
return this._fns.indexOf(id);
}
}
exists(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
return !!this._fns[index];
}
eject(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = null;
}
}
update(id: number | Interceptor, fn: Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = fn;
return id;
} else {
return false;
}
}
use(fn: Interceptor) {
this._fns = [...this._fns, fn];
return this._fns.length - 1;
}
}
// `createInterceptors()` response, meant for external use as it does not
// expose internals
export interface Middleware<Req, Res, Err, Options> {
error: Pick<
Interceptors<ErrInterceptor<Err, Res, Req, Options>>,
'eject' | 'use'
>;
request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>;
response: Pick<
Interceptors<ResInterceptor<Res, Req, Options>>,
'eject' | 'use'
>;
}
// do not add `Middleware` as return type so we can use _fns internally
export const createInterceptors = <Req, Res, Err, Options>() => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
});
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: 'form',
},
object: {
explode: true,
style: 'deepObject',
},
});
const defaultHeaders = {
'Content-Type': 'application/json',
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
parseAs: 'auto',
querySerializer: defaultQuerySerializer,
...override,
});

View file

@ -0,0 +1,40 @@
export type AuthToken = string | undefined;
export interface Auth {
/**
* Which part of the request do we use to send the auth?
*
* @default 'header'
*/
in?: 'header' | 'query' | 'cookie';
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string;
scheme?: 'basic' | 'bearer';
type: 'apiKey' | 'http';
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token =
typeof callback === 'function' ? await callback(auth) : callback;
if (!token) {
return;
}
if (auth.scheme === 'bearer') {
return `Bearer ${token}`;
}
if (auth.scheme === 'basic') {
return `Basic ${btoa(token)}`;
}
return token;
};

View file

@ -0,0 +1,88 @@
import type {
ArrayStyle,
ObjectStyle,
SerializerOptions,
} from './pathSerializer';
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: any) => any;
export interface QuerySerializerOptions {
allowReserved?: boolean;
array?: SerializerOptions<ArrayStyle>;
object?: SerializerOptions<ObjectStyle>;
}
const serializeFormDataPair = (
data: FormData,
key: string,
value: unknown,
): void => {
if (typeof value === 'string' || value instanceof Blob) {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
const serializeUrlSearchParamsPair = (
data: URLSearchParams,
key: string,
value: unknown,
): void => {
if (typeof value === 'string') {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): FormData => {
const data = new FormData();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(data, key, value);
}
});
return data;
},
};
export const jsonBodySerializer = {
bodySerializer: <T>(body: T): string =>
JSON.stringify(body, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value,
),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): string => {
const data = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value);
}
});
return data.toString();
},
};

View file

@ -0,0 +1,151 @@
type Slot = 'body' | 'headers' | 'path' | 'query';
export type Field =
| {
in: Exclude<Slot, 'body'>;
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If omitted, we use the same value as `key`.
*/
map?: string;
}
| {
in: Extract<Slot, 'body'>;
/**
* Key isn't required for bodies.
*/
key?: string;
map?: string;
};
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
}
export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = {
$body_: 'body',
$headers_: 'headers',
$path_: 'path',
$query_: 'query',
};
const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map<
string,
{
in: Slot;
map?: string;
}
>;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map();
}
for (const config of fields) {
if ('in' in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
});
}
} else if (config.args) {
buildKeyMap(config.args, map);
}
}
return map;
};
interface Params {
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === 'object' && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
};
export const buildClientParams = (
args: ReadonlyArray<unknown>,
fields: FieldsConfig,
) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
};
const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index];
}
if (!config) {
continue;
}
if ('in' in config) {
if (config.key) {
const field = map.get(config.key)!;
const name = field.map || config.key;
(params[field.in] as Record<string, unknown>)[name] = arg;
} else {
params.body = arg;
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key);
if (field) {
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
} else {
const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix),
);
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[
key.slice(prefix.length)
] = value;
} else {
for (const [slot, allowed] of Object.entries(
config.allowExtra ?? {},
)) {
if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
}
}
}
}
}
}
}
stripEmptySlots(params);
return params;
};

View file

@ -0,0 +1,179 @@
interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
name: string;
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean;
style: T;
}
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = 'label' | 'matrix' | 'simple';
export type ObjectStyle = 'form' | 'deepObject';
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string;
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'form':
return ',';
case 'pipeDelimited':
return '|';
case 'spaceDelimited':
return '%20';
default:
return ',';
}
};
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const serializeArrayParam = ({
allowReserved,
explode,
name,
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[];
}) => {
if (!explode) {
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
switch (style) {
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
case 'simple':
return joinedValues;
default:
return `${name}=${joinedValues}`;
}
}
const separator = separatorArrayExplode(style);
const joinedValues = value
.map((v) => {
if (style === 'label' || style === 'simple') {
return allowReserved ? v : encodeURIComponent(v as string);
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
});
})
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
};
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'object') {
throw new Error(
'Deeply-nested arrays/objects arent supported. Provide your own `querySerializer()` to handle these.',
);
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
export const serializeObjectParam = ({
allowReserved,
explode,
name,
style,
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date;
valueOnly?: boolean;
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
}
if (style !== 'deepObject' && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [
...values,
key,
allowReserved ? (v as string) : encodeURIComponent(v as string),
];
});
const joinedValues = values.join(',');
switch (style) {
case 'form':
return `${name}=${joinedValues}`;
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
default:
return joinedValues;
}
}
const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === 'deepObject' ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
};

View file

@ -0,0 +1,118 @@
import type { Auth, AuthToken } from './auth';
import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from './bodySerializer';
export interface Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
> {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
connect: MethodFn;
delete: MethodFn;
get: MethodFn;
getConfig: () => Config;
head: MethodFn;
options: MethodFn;
patch: MethodFn;
post: MethodFn;
put: MethodFn;
request: RequestFn;
setConfig: (config: Config) => Config;
trace: MethodFn;
}
export interface Config {
/**
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
*
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit['headers']
| Record<
string,
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined
| unknown
>;
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?:
| 'CONNECT'
| 'DELETE'
| 'GET'
| 'HEAD'
| 'OPTIONS'
| 'PATCH'
| 'POST'
| 'PUT'
| 'TRACE';
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
* style, and reserved characters are percent-encoded.
*
* This method will have no effect if the native `paramsSerializer()` Axios
* API function is used.
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function validating request data. This is useful if you want to ensure
* the request conforms to the desired shape, so it can be safely sent to
* the server.
*/
requestValidator?: (data: unknown) => Promise<unknown>;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
}
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
? true
: [T] extends [never | undefined]
? [undefined] extends [T]
? false
: true
: false;
export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
? never
: K]: T[K];
};

Some files were not shown because too many files have changed in this diff Show more