mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-19 08:44:22 +01:00
ci: new publish method (#1451)
This commit is contained in:
18
.github/workflows/publish.yml
vendored
18
.github/workflows/publish.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}`)
|
||||||
|
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
CHANGELOG.md
|
|
||||||
/ecosystem-tests/*/**
|
|
||||||
/node_modules
|
|
||||||
/deno
|
|
||||||
|
|
||||||
# don't format tsc output, will break source maps
|
|
||||||
/dist
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"arrowParens": "always",
|
|
||||||
"experimentalTernaries": true,
|
|
||||||
"printWidth": 110,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
".": "0.1.0-alpha.20"
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
brew "node"
|
|
||||||
@@ -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))
|
|
||||||
@@ -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 you’d 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.
|
|
||||||
@@ -1,370 +0,0 @@
|
|||||||
# Opencode TypeScript API Library
|
|
||||||
|
|
||||||
[>)](https://npmjs.org/package/@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).
|
|
||||||
@@ -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>
|
|
||||||
@@ -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!"
|
|
||||||
|
|
||||||
@@ -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"
|
|
||||||
@@ -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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
7
packages/sdk/go/.devcontainer/devcontainer.json
Normal file
7
packages/sdk/go/.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/debian
|
||||||
|
{
|
||||||
|
"name": "Development",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/go:1.23-bookworm",
|
||||||
|
"postCreateCommand": "go mod tidy"
|
||||||
|
}
|
||||||
49
packages/sdk/go/.github/workflows/ci.yml
vendored
Normal file
49
packages/sdk/go/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- 'generated'
|
||||||
|
- 'codegen/**'
|
||||||
|
- 'integrated/**'
|
||||||
|
- 'stl-preview-head/**'
|
||||||
|
- 'stl-preview-base/**'
|
||||||
|
pull_request:
|
||||||
|
branches-ignore:
|
||||||
|
- 'stl-preview-head/**'
|
||||||
|
- 'stl-preview-base/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
timeout-minutes: 10
|
||||||
|
name: lint
|
||||||
|
runs-on: ${{ github.repository == 'stainless-sdks/opencode-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
|
||||||
|
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: ./go.mod
|
||||||
|
|
||||||
|
- name: Run lints
|
||||||
|
run: ./scripts/lint
|
||||||
|
test:
|
||||||
|
timeout-minutes: 10
|
||||||
|
name: test
|
||||||
|
runs-on: ${{ github.repository == 'stainless-sdks/opencode-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
|
||||||
|
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: ./go.mod
|
||||||
|
|
||||||
|
- name: Bootstrap
|
||||||
|
run: ./scripts/bootstrap
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: ./scripts/test
|
||||||
4
packages/sdk/go/.gitignore
vendored
Normal file
4
packages/sdk/go/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.prism.log
|
||||||
|
codegen.log
|
||||||
|
Brewfile.lock.json
|
||||||
|
.idea/
|
||||||
3
packages/sdk/go/.release-please-manifest.json
Normal file
3
packages/sdk/go/.release-please-manifest.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
".": "0.1.0-alpha.8"
|
||||||
|
}
|
||||||
4
packages/sdk/go/.stats.yml
Normal file
4
packages/sdk/go/.stats.yml
Normal 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
1
packages/sdk/go/Brewfile
Normal file
@@ -0,0 +1 @@
|
|||||||
|
brew "go"
|
||||||
73
packages/sdk/go/CHANGELOG.md
Normal file
73
packages/sdk/go/CHANGELOG.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.0-alpha.8 (2025-07-02)
|
||||||
|
|
||||||
|
Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.7...v0.1.0-alpha.8)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519))
|
||||||
|
|
||||||
|
## 0.1.0-alpha.7 (2025-06-30)
|
||||||
|
|
||||||
|
Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.6...v0.1.0-alpha.7)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
|
||||||
|
* **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
|
||||||
|
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
|
||||||
|
* **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
|
||||||
|
|
||||||
|
## 0.1.0-alpha.6 (2025-06-28)
|
||||||
|
|
||||||
|
Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.5...v0.1.0-alpha.6)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d))
|
||||||
|
|
||||||
|
## 0.1.0-alpha.5 (2025-06-27)
|
||||||
|
|
||||||
|
Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.4...v0.1.0-alpha.5)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
|
||||||
|
|
||||||
|
## 0.1.0-alpha.4 (2025-06-27)
|
||||||
|
|
||||||
|
Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.3...v0.1.0-alpha.4)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
|
||||||
|
|
||||||
|
## 0.1.0-alpha.3 (2025-06-27)
|
||||||
|
|
||||||
|
Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.2...v0.1.0-alpha.3)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61))
|
||||||
|
|
||||||
|
## 0.1.0-alpha.2 (2025-06-27)
|
||||||
|
|
||||||
|
Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.1...v0.1.0-alpha.2)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6))
|
||||||
|
|
||||||
|
## 0.1.0-alpha.1 (2025-06-27)
|
||||||
|
|
||||||
|
Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencode-sdk-go/compare/v0.0.1-alpha.0...v0.1.0-alpha.1)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a))
|
||||||
|
* **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
|
||||||
|
* **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))
|
||||||
66
packages/sdk/go/CONTRIBUTING.md
Normal file
66
packages/sdk/go/CONTRIBUTING.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
## Setting up the environment
|
||||||
|
|
||||||
|
To set up the repository, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ ./scripts/bootstrap
|
||||||
|
$ ./scripts/build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install all the required dependencies and build the SDK.
|
||||||
|
|
||||||
|
You can also [install go 1.18+ manually](https://go.dev/doc/install).
|
||||||
|
|
||||||
|
## Modifying/Adding code
|
||||||
|
|
||||||
|
Most of the SDK is generated code. Modifications to code will be persisted between generations, but may
|
||||||
|
result in merge conflicts between manual patches and changes from the generator. The generator will never
|
||||||
|
modify the contents of the `lib/` and `examples/` directories.
|
||||||
|
|
||||||
|
## Adding and running examples
|
||||||
|
|
||||||
|
All files in the `examples/` directory are not modified by the generator and can be freely edited or added to.
|
||||||
|
|
||||||
|
```go
|
||||||
|
# add an example to examples/<your-example>/main.go
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ go run ./examples/<your-example>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using the repository from source
|
||||||
|
|
||||||
|
To use a local version of this library from source in another project, edit the `go.mod` with a replace
|
||||||
|
directive. This can be done through the CLI with the following:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ go mod edit -replace github.com/sst/opencode-sdk-go=/path/to/opencode-sdk-go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# you will need npm installed
|
||||||
|
$ npx prism mock path/to/your/openapi.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ ./scripts/test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
This library uses the standard gofmt code formatter:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ ./scripts/format
|
||||||
|
```
|
||||||
354
packages/sdk/go/README.md
Normal file
354
packages/sdk/go/README.md
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
# Opencode Go API Library
|
||||||
|
|
||||||
|
<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go"><img src="https://pkg.go.dev/badge/github.com/sst/opencode-sdk-go.svg" alt="Go Reference"></a>
|
||||||
|
|
||||||
|
The Opencode Go library provides convenient access to the [Opencode REST API](https://opencode.ai/docs)
|
||||||
|
from applications written in Go.
|
||||||
|
|
||||||
|
It is generated with [Stainless](https://www.stainless.com/).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<!-- x-release-please-start-version -->
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/sst/opencode-sdk-go" // imported as opencode
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- x-release-please-end -->
|
||||||
|
|
||||||
|
Or to pin the version:
|
||||||
|
|
||||||
|
<!-- x-release-please-start-version -->
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get -u 'github.com/sst/opencode-sdk-go@v0.1.0-alpha.8'
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- x-release-please-end -->
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
This library requires Go 1.18+.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The full API of this library can be found in [api.md](api.md).
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := opencode.NewClient()
|
||||||
|
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).
|
||||||
43
packages/sdk/go/aliases.go
Normal file
43
packages/sdk/go/aliases.go
Normal 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
128
packages/sdk/go/api.md
Normal 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
368
packages/sdk/go/app.go
Normal 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
131
packages/sdk/go/app_test.go
Normal 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
125
packages/sdk/go/client.go
Normal 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...)
|
||||||
|
}
|
||||||
332
packages/sdk/go/client_test.go
Normal file
332
packages/sdk/go/client_test.go
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||||
|
|
||||||
|
package opencode_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
"github.com/sst/opencode-sdk-go/internal"
|
||||||
|
"github.com/sst/opencode-sdk-go/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
type closureTransport struct {
|
||||||
|
fn func(req *http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *closureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
return t.fn(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserAgentHeader(t *testing.T) {
|
||||||
|
var userAgent string
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithHTTPClient(&http.Client{
|
||||||
|
Transport: &closureTransport{
|
||||||
|
fn: func(req *http.Request) (*http.Response, error) {
|
||||||
|
userAgent = req.Header.Get("User-Agent")
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
client.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
887
packages/sdk/go/config.go
Normal 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
|
||||||
|
}
|
||||||
36
packages/sdk/go/config_test.go
Normal file
36
packages/sdk/go/config_test.go
Normal 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
1373
packages/sdk/go/event.go
Normal file
File diff suppressed because it is too large
Load Diff
50
packages/sdk/go/field.go
Normal file
50
packages/sdk/go/field.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package opencode
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/param"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// F is a param field helper used to initialize a [param.Field] generic struct.
|
||||||
|
// This helps specify null, zero values, and overrides, as well as normal values.
|
||||||
|
// You can read more about this in our [README].
|
||||||
|
//
|
||||||
|
// [README]: https://pkg.go.dev/github.com/sst/opencode-sdk-go#readme-request-fields
|
||||||
|
func F[T any](value T) param.Field[T] { return param.Field[T]{Value: value, Present: true} }
|
||||||
|
|
||||||
|
// Null is a param field helper which explicitly sends null to the API.
|
||||||
|
func Null[T any]() param.Field[T] { return param.Field[T]{Null: true, Present: true} }
|
||||||
|
|
||||||
|
// Raw is a param field helper for specifying values for fields when the
|
||||||
|
// type you are looking to send is different from the type that is specified in
|
||||||
|
// the SDK. For example, if the type of the field is an integer, but you want
|
||||||
|
// to send a float, you could do that by setting the corresponding field with
|
||||||
|
// Raw[int](0.5).
|
||||||
|
func Raw[T any](value any) param.Field[T] { return param.Field[T]{Raw: value, Present: true} }
|
||||||
|
|
||||||
|
// Int is a param field helper which helps specify integers. This is
|
||||||
|
// particularly helpful when specifying integer constants for fields.
|
||||||
|
func Int(value int64) param.Field[int64] { return F(value) }
|
||||||
|
|
||||||
|
// String is a param field helper which helps specify strings.
|
||||||
|
func String(value string) param.Field[string] { return F(value) }
|
||||||
|
|
||||||
|
// Float is a param field helper which helps specify floats.
|
||||||
|
func Float(value float64) param.Field[float64] { return F(value) }
|
||||||
|
|
||||||
|
// Bool is a param field helper which helps specify bools.
|
||||||
|
func Bool(value bool) param.Field[bool] { return F(value) }
|
||||||
|
|
||||||
|
// FileParam is a param field helper which helps files with a mime content-type.
|
||||||
|
func FileParam(reader io.Reader, filename string, contentType string) param.Field[io.Reader] {
|
||||||
|
return F[io.Reader](&file{reader, filename, contentType})
|
||||||
|
}
|
||||||
|
|
||||||
|
type file struct {
|
||||||
|
io.Reader
|
||||||
|
name string
|
||||||
|
contentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *file) ContentType() string { return f.contentType }
|
||||||
|
func (f *file) Filename() string { return f.name }
|
||||||
142
packages/sdk/go/file.go
Normal file
142
packages/sdk/go/file.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
60
packages/sdk/go/file_test.go
Normal file
60
packages/sdk/go/file_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||||
|
|
||||||
|
package opencode_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/testutil"
|
||||||
|
"github.com/sst/opencode-sdk-go/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileRead(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.File.Read(context.TODO(), opencode.FileReadParams{
|
||||||
|
Path: opencode.F("path"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileStatus(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.File.Status(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
326
packages/sdk/go/find.go
Normal file
326
packages/sdk/go/find.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
86
packages/sdk/go/find_test.go
Normal file
86
packages/sdk/go/find_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||||
|
|
||||||
|
package opencode_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/testutil"
|
||||||
|
"github.com/sst/opencode-sdk-go/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindFiles(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.Find.Files(context.TODO(), opencode.FindFilesParams{
|
||||||
|
Query: opencode.F("query"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindSymbols(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.Find.Symbols(context.TODO(), opencode.FindSymbolsParams{
|
||||||
|
Query: opencode.F("query"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindText(t *testing.T) {
|
||||||
|
t.Skip("skipped: tests are disabled for the time being")
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
_, err := client.Find.Text(context.TODO(), opencode.FindTextParams{
|
||||||
|
Pattern: opencode.F("pattern"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var apierr *opencode.Error
|
||||||
|
if errors.As(err, &apierr) {
|
||||||
|
t.Log(string(apierr.DumpRequest(true)))
|
||||||
|
}
|
||||||
|
t.Fatalf("err should be nil: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/sdk/go/go.mod
Normal file
13
packages/sdk/go/go.mod
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
module github.com/sst/opencode-sdk-go
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/tidwall/gjson v1.14.4
|
||||||
|
github.com/tidwall/sjson v1.2.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
)
|
||||||
10
packages/sdk/go/go.sum
Normal file
10
packages/sdk/go/go.sum
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||||
|
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
53
packages/sdk/go/internal/apierror/apierror.go
Normal file
53
packages/sdk/go/internal/apierror/apierror.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||||
|
|
||||||
|
package apierror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/apijson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error represents an error that originates from the API, i.e. when a request is
|
||||||
|
// made and the API returns a response with a HTTP status code. Other errors are
|
||||||
|
// not wrapped by this SDK.
|
||||||
|
type Error struct {
|
||||||
|
JSON errorJSON `json:"-"`
|
||||||
|
StatusCode int
|
||||||
|
Request *http.Request
|
||||||
|
Response *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorJSON contains the JSON metadata for the struct [Error]
|
||||||
|
type errorJSON struct {
|
||||||
|
raw string
|
||||||
|
ExtraFields map[string]apijson.Field
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Error) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return apijson.UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r errorJSON) RawJSON() string {
|
||||||
|
return r.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Error) Error() string {
|
||||||
|
// Attempt to re-populate the response body
|
||||||
|
return fmt.Sprintf("%s \"%s\": %d %s %s", r.Request.Method, r.Request.URL, r.Response.StatusCode, http.StatusText(r.Response.StatusCode), r.JSON.RawJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Error) DumpRequest(body bool) []byte {
|
||||||
|
if r.Request.GetBody != nil {
|
||||||
|
r.Request.Body, _ = r.Request.GetBody()
|
||||||
|
}
|
||||||
|
out, _ := httputil.DumpRequestOut(r.Request, body)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Error) DumpResponse(body bool) []byte {
|
||||||
|
out, _ := httputil.DumpResponse(r.Response, body)
|
||||||
|
return out
|
||||||
|
}
|
||||||
383
packages/sdk/go/internal/apiform/encoder.go
Normal file
383
packages/sdk/go/internal/apiform/encoder.go
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
package apiform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/textproto"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/param"
|
||||||
|
)
|
||||||
|
|
||||||
|
var encoders sync.Map // map[encoderEntry]encoderFunc
|
||||||
|
|
||||||
|
func Marshal(value interface{}, writer *multipart.Writer) error {
|
||||||
|
e := &encoder{dateFormat: time.RFC3339}
|
||||||
|
return e.marshal(value, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalRoot(value interface{}, writer *multipart.Writer) error {
|
||||||
|
e := &encoder{root: true, dateFormat: time.RFC3339}
|
||||||
|
return e.marshal(value, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoder struct {
|
||||||
|
dateFormat string
|
||||||
|
root bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderFunc func(key string, value reflect.Value, writer *multipart.Writer) error
|
||||||
|
|
||||||
|
type encoderField struct {
|
||||||
|
tag parsedStructTag
|
||||||
|
fn encoderFunc
|
||||||
|
idx []int
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderEntry struct {
|
||||||
|
reflect.Type
|
||||||
|
dateFormat string
|
||||||
|
root bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) marshal(value interface{}, writer *multipart.Writer) error {
|
||||||
|
val := reflect.ValueOf(value)
|
||||||
|
if !val.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
typ := val.Type()
|
||||||
|
enc := e.typeEncoder(typ)
|
||||||
|
return enc("", val, writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
entry := encoderEntry{
|
||||||
|
Type: t,
|
||||||
|
dateFormat: e.dateFormat,
|
||||||
|
root: e.root,
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi, ok := encoders.Load(entry); ok {
|
||||||
|
return fi.(encoderFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To deal with recursive types, populate the map with an
|
||||||
|
// indirect func before we build it. This type waits on the
|
||||||
|
// real func (f) to be ready and then calls it. This indirect
|
||||||
|
// func is only used for recursive types.
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
f encoderFunc
|
||||||
|
)
|
||||||
|
wg.Add(1)
|
||||||
|
fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
wg.Wait()
|
||||||
|
return f(key, v, writer)
|
||||||
|
}))
|
||||||
|
if loaded {
|
||||||
|
return fi.(encoderFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the real encoder and replace the indirect func with it.
|
||||||
|
f = e.newTypeEncoder(t)
|
||||||
|
wg.Done()
|
||||||
|
encoders.Store(entry, f)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
|
||||||
|
return e.newTimeTypeEncoder()
|
||||||
|
}
|
||||||
|
if t.ConvertibleTo(reflect.TypeOf((*io.Reader)(nil)).Elem()) {
|
||||||
|
return e.newReaderTypeEncoder()
|
||||||
|
}
|
||||||
|
e.root = false
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Pointer:
|
||||||
|
inner := t.Elem()
|
||||||
|
|
||||||
|
innerEncoder := e.typeEncoder(inner)
|
||||||
|
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
if !v.IsValid() || v.IsNil() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return innerEncoder(key, v.Elem(), writer)
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
return e.newStructTypeEncoder(t)
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
return e.newArrayTypeEncoder(t)
|
||||||
|
case reflect.Map:
|
||||||
|
return e.newMapEncoder(t)
|
||||||
|
case reflect.Interface:
|
||||||
|
return e.newInterfaceEncoder()
|
||||||
|
default:
|
||||||
|
return e.newPrimitiveTypeEncoder(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
switch t.Kind() {
|
||||||
|
// Note that we could use `gjson` to encode these types but it would complicate our
|
||||||
|
// code more and this current code shouldn't cause any issues
|
||||||
|
case reflect.String:
|
||||||
|
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
return writer.WriteField(key, v.String())
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
if v.Bool() {
|
||||||
|
return writer.WriteField(key, "true")
|
||||||
|
}
|
||||||
|
return writer.WriteField(key, "false")
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
return writer.WriteField(key, strconv.FormatInt(v.Int(), 10))
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
return writer.WriteField(key, strconv.FormatUint(v.Uint(), 10))
|
||||||
|
}
|
||||||
|
case reflect.Float32:
|
||||||
|
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 32))
|
||||||
|
}
|
||||||
|
case reflect.Float64:
|
||||||
|
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
return fmt.Errorf("unknown type received at primitive encoder: %s", t.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
itemEncoder := e.typeEncoder(t.Elem())
|
||||||
|
|
||||||
|
return func(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
if key != "" {
|
||||||
|
key = key + "."
|
||||||
|
}
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
err := itemEncoder(key+strconv.Itoa(i), v.Index(i), writer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
|
||||||
|
return e.newFieldTypeEncoder(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoderFields := []encoderField{}
|
||||||
|
extraEncoder := (*encoderField)(nil)
|
||||||
|
|
||||||
|
// This helper allows us to recursively collect field encoders into a flat
|
||||||
|
// array. The parameter `index` keeps track of the access patterns necessary
|
||||||
|
// to get to some field.
|
||||||
|
var collectEncoderFields func(r reflect.Type, index []int)
|
||||||
|
collectEncoderFields = func(r reflect.Type, index []int) {
|
||||||
|
for i := 0; i < r.NumField(); i++ {
|
||||||
|
idx := append(index, i)
|
||||||
|
field := t.FieldByIndex(idx)
|
||||||
|
if !field.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If this is an embedded struct, traverse one level deeper to extract
|
||||||
|
// the field and get their encoders as well.
|
||||||
|
if field.Anonymous {
|
||||||
|
collectEncoderFields(field.Type, idx)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If json tag is not present, then we skip, which is intentionally
|
||||||
|
// different behavior from the stdlib.
|
||||||
|
ptag, ok := parseFormStructTag(field)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// We only want to support unexported field if they're tagged with
|
||||||
|
// `extras` because that field shouldn't be part of the public API. We
|
||||||
|
// also want to only keep the top level extras
|
||||||
|
if ptag.extras && len(index) == 0 {
|
||||||
|
extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ptag.name == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dateFormat, ok := parseFormatStructTag(field)
|
||||||
|
oldFormat := e.dateFormat
|
||||||
|
if ok {
|
||||||
|
switch dateFormat {
|
||||||
|
case "date-time":
|
||||||
|
e.dateFormat = time.RFC3339
|
||||||
|
case "date":
|
||||||
|
e.dateFormat = "2006-01-02"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
|
||||||
|
e.dateFormat = oldFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectEncoderFields(t, []int{})
|
||||||
|
|
||||||
|
// Ensure deterministic output by sorting by lexicographic order
|
||||||
|
sort.Slice(encoderFields, func(i, j int) bool {
|
||||||
|
return encoderFields[i].tag.name < encoderFields[j].tag.name
|
||||||
|
})
|
||||||
|
|
||||||
|
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||||
|
if key != "" {
|
||||||
|
key = key + "."
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ef := range encoderFields {
|
||||||
|
field := value.FieldByIndex(ef.idx)
|
||||||
|
err := ef.fn(key+ef.tag.name, field, writer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if extraEncoder != nil {
|
||||||
|
err := e.encodeMapEntries(key, value.FieldByIndex(extraEncoder.idx), writer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
f, _ := t.FieldByName("Value")
|
||||||
|
enc := e.typeEncoder(f.Type)
|
||||||
|
|
||||||
|
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||||
|
present := value.FieldByName("Present")
|
||||||
|
if !present.Bool() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
null := value.FieldByName("Null")
|
||||||
|
if null.Bool() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw := value.FieldByName("Raw")
|
||||||
|
if !raw.IsNil() {
|
||||||
|
return e.typeEncoder(raw.Type())(key, raw, writer)
|
||||||
|
}
|
||||||
|
return enc(key, value.FieldByName("Value"), writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newTimeTypeEncoder() encoderFunc {
|
||||||
|
format := e.dateFormat
|
||||||
|
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||||
|
return writer.WriteField(key, value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) newInterfaceEncoder() encoderFunc {
|
||||||
|
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||||
|
value = value.Elem()
|
||||||
|
if !value.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.typeEncoder(value.Type())(key, value, writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||||
|
|
||||||
|
func escapeQuotes(s string) string {
|
||||||
|
return quoteEscaper.Replace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newReaderTypeEncoder() encoderFunc {
|
||||||
|
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||||
|
reader := value.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader)
|
||||||
|
filename := "anonymous_file"
|
||||||
|
contentType := "application/octet-stream"
|
||||||
|
if named, ok := reader.(interface{ Filename() string }); ok {
|
||||||
|
filename = named.Filename()
|
||||||
|
} else if named, ok := reader.(interface{ Name() string }); ok {
|
||||||
|
filename = path.Base(named.Name())
|
||||||
|
}
|
||||||
|
if typed, ok := reader.(interface{ ContentType() string }); ok {
|
||||||
|
contentType = typed.ContentType()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Below is taken almost 1-for-1 from [multipart.CreateFormFile]
|
||||||
|
h := make(textproto.MIMEHeader)
|
||||||
|
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(key), escapeQuotes(filename)))
|
||||||
|
h.Set("Content-Type", contentType)
|
||||||
|
filewriter, err := writer.CreatePart(h)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(filewriter, reader)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a []byte of json (may either be an empty object or an object that already contains entries)
|
||||||
|
// encode all of the entries in the map to the json byte array.
|
||||||
|
func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipart.Writer) error {
|
||||||
|
type mapPair struct {
|
||||||
|
key string
|
||||||
|
value reflect.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if key != "" {
|
||||||
|
key = key + "."
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs := []mapPair{}
|
||||||
|
|
||||||
|
iter := v.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
if iter.Key().Type().Kind() == reflect.String {
|
||||||
|
pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()})
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("cannot encode a map with a non string key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure deterministic output
|
||||||
|
sort.Slice(pairs, func(i, j int) bool {
|
||||||
|
return pairs[i].key < pairs[j].key
|
||||||
|
})
|
||||||
|
|
||||||
|
elementEncoder := e.typeEncoder(v.Type().Elem())
|
||||||
|
for _, p := range pairs {
|
||||||
|
err := elementEncoder(key+string(p.key), p.value, writer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
|
||||||
|
return func(key string, value reflect.Value, writer *multipart.Writer) error {
|
||||||
|
return e.encodeMapEntries(key, value, writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/sdk/go/internal/apiform/form.go
Normal file
5
packages/sdk/go/internal/apiform/form.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package apiform
|
||||||
|
|
||||||
|
type Marshaler interface {
|
||||||
|
MarshalMultipart() ([]byte, string, error)
|
||||||
|
}
|
||||||
440
packages/sdk/go/internal/apiform/form_test.go
Normal file
440
packages/sdk/go/internal/apiform/form_test.go
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
package apiform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"mime/multipart"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func P[T any](v T) *T { return &v }
|
||||||
|
|
||||||
|
type Primitives struct {
|
||||||
|
A bool `form:"a"`
|
||||||
|
B int `form:"b"`
|
||||||
|
C uint `form:"c"`
|
||||||
|
D float64 `form:"d"`
|
||||||
|
E float32 `form:"e"`
|
||||||
|
F []int `form:"f"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrimitivePointers struct {
|
||||||
|
A *bool `form:"a"`
|
||||||
|
B *int `form:"b"`
|
||||||
|
C *uint `form:"c"`
|
||||||
|
D *float64 `form:"d"`
|
||||||
|
E *float32 `form:"e"`
|
||||||
|
F *[]int `form:"f"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Slices struct {
|
||||||
|
Slice []Primitives `form:"slices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DateTime struct {
|
||||||
|
Date time.Time `form:"date" format:"date"`
|
||||||
|
DateTime time.Time `form:"date-time" format:"date-time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdditionalProperties struct {
|
||||||
|
A bool `form:"a"`
|
||||||
|
Extras map[string]interface{} `form:"-,extras"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypedAdditionalProperties struct {
|
||||||
|
A bool `form:"a"`
|
||||||
|
Extras map[string]int `form:"-,extras"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmbeddedStructs struct {
|
||||||
|
AdditionalProperties
|
||||||
|
A *int `form:"number2"`
|
||||||
|
Extras map[string]interface{} `form:"-,extras"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Recursive struct {
|
||||||
|
Name string `form:"name"`
|
||||||
|
Child *Recursive `form:"child"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnknownStruct struct {
|
||||||
|
Unknown interface{} `form:"unknown"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnionStruct struct {
|
||||||
|
Union Union `form:"union" format:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Union interface {
|
||||||
|
union()
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnionInteger int64
|
||||||
|
|
||||||
|
func (UnionInteger) union() {}
|
||||||
|
|
||||||
|
type UnionStructA struct {
|
||||||
|
Type string `form:"type"`
|
||||||
|
A string `form:"a"`
|
||||||
|
B string `form:"b"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnionStructA) union() {}
|
||||||
|
|
||||||
|
type UnionStructB struct {
|
||||||
|
Type string `form:"type"`
|
||||||
|
A string `form:"a"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnionStructB) union() {}
|
||||||
|
|
||||||
|
type UnionTime time.Time
|
||||||
|
|
||||||
|
func (UnionTime) union() {}
|
||||||
|
|
||||||
|
type ReaderStruct struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var tests = map[string]struct {
|
||||||
|
buf string
|
||||||
|
val interface{}
|
||||||
|
}{
|
||||||
|
"map_string": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="foo"
|
||||||
|
|
||||||
|
bar
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
map[string]string{"foo": "bar"},
|
||||||
|
},
|
||||||
|
|
||||||
|
"map_interface": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
1
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="b"
|
||||||
|
|
||||||
|
str
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="c"
|
||||||
|
|
||||||
|
false
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
map[string]interface{}{"a": float64(1), "b": "str", "c": false},
|
||||||
|
},
|
||||||
|
|
||||||
|
"primitive_struct": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
false
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="b"
|
||||||
|
|
||||||
|
237628372683
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="c"
|
||||||
|
|
||||||
|
654
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="d"
|
||||||
|
|
||||||
|
9999.43
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="e"
|
||||||
|
|
||||||
|
43.76
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="f.0"
|
||||||
|
|
||||||
|
1
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="f.1"
|
||||||
|
|
||||||
|
2
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="f.2"
|
||||||
|
|
||||||
|
3
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="f.3"
|
||||||
|
|
||||||
|
4
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"slices": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="slices.0.a"
|
||||||
|
|
||||||
|
false
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="slices.0.b"
|
||||||
|
|
||||||
|
237628372683
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="slices.0.c"
|
||||||
|
|
||||||
|
654
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="slices.0.d"
|
||||||
|
|
||||||
|
9999.43
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="slices.0.e"
|
||||||
|
|
||||||
|
43.76
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="slices.0.f.0"
|
||||||
|
|
||||||
|
1
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="slices.0.f.1"
|
||||||
|
|
||||||
|
2
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="slices.0.f.2"
|
||||||
|
|
||||||
|
3
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="slices.0.f.3"
|
||||||
|
|
||||||
|
4
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
Slices{
|
||||||
|
Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"primitive_pointer_struct": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
false
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="b"
|
||||||
|
|
||||||
|
237628372683
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="c"
|
||||||
|
|
||||||
|
654
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="d"
|
||||||
|
|
||||||
|
9999.43
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="e"
|
||||||
|
|
||||||
|
43.76
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="f.0"
|
||||||
|
|
||||||
|
1
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="f.1"
|
||||||
|
|
||||||
|
2
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="f.2"
|
||||||
|
|
||||||
|
3
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="f.3"
|
||||||
|
|
||||||
|
4
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="f.4"
|
||||||
|
|
||||||
|
5
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
PrimitivePointers{
|
||||||
|
A: P(false),
|
||||||
|
B: P(237628372683),
|
||||||
|
C: P(uint(654)),
|
||||||
|
D: P(9999.43),
|
||||||
|
E: P(float32(43.76)),
|
||||||
|
F: &[]int{1, 2, 3, 4, 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"datetime_struct": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="date"
|
||||||
|
|
||||||
|
2006-01-02
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="date-time"
|
||||||
|
|
||||||
|
2006-01-02T15:04:05Z
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
DateTime{
|
||||||
|
Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC),
|
||||||
|
DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"additional_properties": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="a"
|
||||||
|
|
||||||
|
true
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="bar"
|
||||||
|
|
||||||
|
value
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="foo"
|
||||||
|
|
||||||
|
true
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
AdditionalProperties{
|
||||||
|
A: true,
|
||||||
|
Extras: map[string]interface{}{
|
||||||
|
"bar": "value",
|
||||||
|
"foo": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"recursive_struct": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="child.name"
|
||||||
|
|
||||||
|
Alex
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="name"
|
||||||
|
|
||||||
|
Robert
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"unknown_struct_number": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="unknown"
|
||||||
|
|
||||||
|
12
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
UnknownStruct{
|
||||||
|
Unknown: 12.,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"unknown_struct_map": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="unknown.foo"
|
||||||
|
|
||||||
|
bar
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
UnknownStruct{
|
||||||
|
Unknown: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_integer": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="union"
|
||||||
|
|
||||||
|
12
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionInteger(12),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_struct_discriminated_a": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="union.a"
|
||||||
|
|
||||||
|
foo
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="union.b"
|
||||||
|
|
||||||
|
bar
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="union.type"
|
||||||
|
|
||||||
|
typeA
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionStructA{
|
||||||
|
Type: "typeA",
|
||||||
|
A: "foo",
|
||||||
|
B: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_struct_discriminated_b": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="union.a"
|
||||||
|
|
||||||
|
foo
|
||||||
|
--xxx
|
||||||
|
Content-Disposition: form-data; name="union.type"
|
||||||
|
|
||||||
|
typeB
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionStructB{
|
||||||
|
Type: "typeB",
|
||||||
|
A: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_struct_time": {
|
||||||
|
`--xxx
|
||||||
|
Content-Disposition: form-data; name="union"
|
||||||
|
|
||||||
|
2010-05-23
|
||||||
|
--xxx--
|
||||||
|
`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
writer := multipart.NewWriter(buf)
|
||||||
|
writer.SetBoundary("xxx")
|
||||||
|
err := Marshal(test.val, writer)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("serialization of %v failed with error %v", test.val, err)
|
||||||
|
}
|
||||||
|
err = writer.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("serialization of %v failed with error %v", test.val, err)
|
||||||
|
}
|
||||||
|
raw := buf.Bytes()
|
||||||
|
if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") {
|
||||||
|
t.Errorf("expected %+#v to serialize to '%s' but got '%s'", test.val, test.buf, string(raw))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
48
packages/sdk/go/internal/apiform/tag.go
Normal file
48
packages/sdk/go/internal/apiform/tag.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package apiform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const jsonStructTag = "json"
|
||||||
|
const formStructTag = "form"
|
||||||
|
const formatStructTag = "format"
|
||||||
|
|
||||||
|
type parsedStructTag struct {
|
||||||
|
name string
|
||||||
|
required bool
|
||||||
|
extras bool
|
||||||
|
metadata bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
|
||||||
|
raw, ok := field.Tag.Lookup(formStructTag)
|
||||||
|
if !ok {
|
||||||
|
raw, ok = field.Tag.Lookup(jsonStructTag)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return tag, false
|
||||||
|
}
|
||||||
|
tag.name = parts[0]
|
||||||
|
for _, part := range parts[1:] {
|
||||||
|
switch part {
|
||||||
|
case "required":
|
||||||
|
tag.required = true
|
||||||
|
case "extras":
|
||||||
|
tag.extras = true
|
||||||
|
case "metadata":
|
||||||
|
tag.metadata = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFormatStructTag(field reflect.StructField) (format string, ok bool) {
|
||||||
|
format, ok = field.Tag.Lookup(formatStructTag)
|
||||||
|
return
|
||||||
|
}
|
||||||
670
packages/sdk/go/internal/apijson/decoder.go
Normal file
670
packages/sdk/go/internal/apijson/decoder.go
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
package apijson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
// decoders is a synchronized map with roughly the following type:
|
||||||
|
// map[reflect.Type]decoderFunc
|
||||||
|
var decoders sync.Map
|
||||||
|
|
||||||
|
// Unmarshal is similar to [encoding/json.Unmarshal] and parses the JSON-encoded
|
||||||
|
// data and stores it in the given pointer.
|
||||||
|
func Unmarshal(raw []byte, to any) error {
|
||||||
|
d := &decoderBuilder{dateFormat: time.RFC3339}
|
||||||
|
return d.unmarshal(raw, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalRoot is like Unmarshal, but doesn't try to call MarshalJSON on the
|
||||||
|
// root element. Useful if a struct's UnmarshalJSON is overrode to use the
|
||||||
|
// behavior of this encoder versus the standard library.
|
||||||
|
func UnmarshalRoot(raw []byte, to any) error {
|
||||||
|
d := &decoderBuilder{dateFormat: time.RFC3339, root: true}
|
||||||
|
return d.unmarshal(raw, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decoderBuilder contains the 'compile-time' state of the decoder.
|
||||||
|
type decoderBuilder struct {
|
||||||
|
// Whether or not this is the first element and called by [UnmarshalRoot], see
|
||||||
|
// the documentation there to see why this is necessary.
|
||||||
|
root bool
|
||||||
|
// The dateFormat (a format string for [time.Format]) which is chosen by the
|
||||||
|
// last struct tag that was seen.
|
||||||
|
dateFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
// decoderState contains the 'run-time' state of the decoder.
|
||||||
|
type decoderState struct {
|
||||||
|
strict bool
|
||||||
|
exactness exactness
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exactness refers to how close to the type the result was if deserialization
|
||||||
|
// was successful. This is useful in deserializing unions, where you want to try
|
||||||
|
// each entry, first with strict, then with looser validation, without actually
|
||||||
|
// having to do a lot of redundant work by marshalling twice (or maybe even more
|
||||||
|
// times).
|
||||||
|
type exactness int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Some values had to fudged a bit, for example by converting a string to an
|
||||||
|
// int, or an enum with extra values.
|
||||||
|
loose exactness = iota
|
||||||
|
// There are some extra arguments, but other wise it matches the union.
|
||||||
|
extras
|
||||||
|
// Exactly right.
|
||||||
|
exact
|
||||||
|
)
|
||||||
|
|
||||||
|
type decoderFunc func(node gjson.Result, value reflect.Value, state *decoderState) error
|
||||||
|
|
||||||
|
type decoderField struct {
|
||||||
|
tag parsedStructTag
|
||||||
|
fn decoderFunc
|
||||||
|
idx []int
|
||||||
|
goname string
|
||||||
|
}
|
||||||
|
|
||||||
|
type decoderEntry struct {
|
||||||
|
reflect.Type
|
||||||
|
dateFormat string
|
||||||
|
root bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoderBuilder) unmarshal(raw []byte, to any) error {
|
||||||
|
value := reflect.ValueOf(to).Elem()
|
||||||
|
result := gjson.ParseBytes(raw)
|
||||||
|
if !value.IsValid() {
|
||||||
|
return fmt.Errorf("apijson: cannot marshal into invalid value")
|
||||||
|
}
|
||||||
|
return d.typeDecoder(value.Type())(result, value, &decoderState{strict: false, exactness: exact})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoderBuilder) typeDecoder(t reflect.Type) decoderFunc {
|
||||||
|
entry := decoderEntry{
|
||||||
|
Type: t,
|
||||||
|
dateFormat: d.dateFormat,
|
||||||
|
root: d.root,
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi, ok := decoders.Load(entry); ok {
|
||||||
|
return fi.(decoderFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To deal with recursive types, populate the map with an
|
||||||
|
// indirect func before we build it. This type waits on the
|
||||||
|
// real func (f) to be ready and then calls it. This indirect
|
||||||
|
// func is only used for recursive types.
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
f decoderFunc
|
||||||
|
)
|
||||||
|
wg.Add(1)
|
||||||
|
fi, loaded := decoders.LoadOrStore(entry, decoderFunc(func(node gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
wg.Wait()
|
||||||
|
return f(node, v, state)
|
||||||
|
}))
|
||||||
|
if loaded {
|
||||||
|
return fi.(decoderFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the real decoder and replace the indirect func with it.
|
||||||
|
f = d.newTypeDecoder(t)
|
||||||
|
wg.Done()
|
||||||
|
decoders.Store(entry, f)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func indirectUnmarshalerDecoder(n gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
return v.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalerDecoder(n gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
if v.Kind() == reflect.Pointer && v.CanSet() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
return v.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoderBuilder) newTypeDecoder(t reflect.Type) decoderFunc {
|
||||||
|
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
|
||||||
|
return d.newTimeTypeDecoder(t)
|
||||||
|
}
|
||||||
|
if !d.root && t.Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) {
|
||||||
|
return unmarshalerDecoder
|
||||||
|
}
|
||||||
|
if !d.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) {
|
||||||
|
if _, ok := unionVariants[t]; !ok {
|
||||||
|
return indirectUnmarshalerDecoder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.root = false
|
||||||
|
|
||||||
|
if _, ok := unionRegistry[t]; ok {
|
||||||
|
return d.newUnionDecoder(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Pointer:
|
||||||
|
inner := t.Elem()
|
||||||
|
innerDecoder := d.typeDecoder(inner)
|
||||||
|
|
||||||
|
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
if !v.IsValid() {
|
||||||
|
return fmt.Errorf("apijson: unexpected invalid reflection value %+#v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
newValue := reflect.New(inner).Elem()
|
||||||
|
err := innerDecoder(n, newValue, state)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Set(newValue.Addr())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
return d.newStructTypeDecoder(t)
|
||||||
|
case reflect.Array:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Slice:
|
||||||
|
return d.newArrayTypeDecoder(t)
|
||||||
|
case reflect.Map:
|
||||||
|
return d.newMapDecoder(t)
|
||||||
|
case reflect.Interface:
|
||||||
|
return func(node gjson.Result, value reflect.Value, state *decoderState) error {
|
||||||
|
if !value.IsValid() {
|
||||||
|
return fmt.Errorf("apijson: unexpected invalid value %+#v", value)
|
||||||
|
}
|
||||||
|
if node.Value() != nil && value.CanSet() {
|
||||||
|
value.Set(reflect.ValueOf(node.Value()))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return d.newPrimitiveTypeDecoder(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newUnionDecoder returns a decoderFunc that deserializes into a union using an
|
||||||
|
// algorithm roughly similar to Pydantic's [smart algorithm].
|
||||||
|
//
|
||||||
|
// Conceptually this is equivalent to choosing the best schema based on how 'exact'
|
||||||
|
// the deserialization is for each of the schemas.
|
||||||
|
//
|
||||||
|
// If there is a tie in the level of exactness, then the tie is broken
|
||||||
|
// left-to-right.
|
||||||
|
//
|
||||||
|
// [smart algorithm]: https://docs.pydantic.dev/latest/concepts/unions/#smart-mode
|
||||||
|
func (d *decoderBuilder) newUnionDecoder(t reflect.Type) decoderFunc {
|
||||||
|
unionEntry, ok := unionRegistry[t]
|
||||||
|
if !ok {
|
||||||
|
panic("apijson: couldn't find union of type " + t.String() + " in union registry")
|
||||||
|
}
|
||||||
|
decoders := []decoderFunc{}
|
||||||
|
for _, variant := range unionEntry.variants {
|
||||||
|
decoder := d.typeDecoder(variant.Type)
|
||||||
|
decoders = append(decoders, decoder)
|
||||||
|
}
|
||||||
|
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
// If there is a discriminator match, circumvent the exactness logic entirely
|
||||||
|
for idx, variant := range unionEntry.variants {
|
||||||
|
decoder := decoders[idx]
|
||||||
|
if variant.TypeFilter != n.Type {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unionEntry.discriminatorKey) != 0 {
|
||||||
|
discriminatorValue := n.Get(unionEntry.discriminatorKey).Value()
|
||||||
|
if discriminatorValue == variant.DiscriminatorValue {
|
||||||
|
inner := reflect.New(variant.Type).Elem()
|
||||||
|
err := decoder(n, inner, state)
|
||||||
|
v.Set(inner)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set bestExactness to worse than loose
|
||||||
|
bestExactness := loose - 1
|
||||||
|
for idx, variant := range unionEntry.variants {
|
||||||
|
decoder := decoders[idx]
|
||||||
|
if variant.TypeFilter != n.Type {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sub := decoderState{strict: state.strict, exactness: exact}
|
||||||
|
inner := reflect.New(variant.Type).Elem()
|
||||||
|
err := decoder(n, inner, &sub)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sub.exactness == exact {
|
||||||
|
v.Set(inner)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if sub.exactness > bestExactness {
|
||||||
|
v.Set(inner)
|
||||||
|
bestExactness = sub.exactness
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestExactness < loose {
|
||||||
|
return errors.New("apijson: was not able to coerce type as union")
|
||||||
|
}
|
||||||
|
|
||||||
|
if guardStrict(state, bestExactness != exact) {
|
||||||
|
return errors.New("apijson: was not able to coerce type as union strictly")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoderBuilder) newMapDecoder(t reflect.Type) decoderFunc {
|
||||||
|
keyType := t.Key()
|
||||||
|
itemType := t.Elem()
|
||||||
|
itemDecoder := d.typeDecoder(itemType)
|
||||||
|
|
||||||
|
return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) {
|
||||||
|
mapValue := reflect.MakeMapWithSize(t, len(node.Map()))
|
||||||
|
|
||||||
|
node.ForEach(func(key, value gjson.Result) bool {
|
||||||
|
// It's fine for us to just use `ValueOf` here because the key types will
|
||||||
|
// always be primitive types so we don't need to decode it using the standard pattern
|
||||||
|
keyValue := reflect.ValueOf(key.Value())
|
||||||
|
if !keyValue.IsValid() {
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("apijson: received invalid key type %v", keyValue.String())
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if keyValue.Type() != keyType {
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("apijson: expected key type %v but got %v", keyType, keyValue.Type())
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
itemValue := reflect.New(itemType).Elem()
|
||||||
|
itemerr := itemDecoder(value, itemValue, state)
|
||||||
|
if itemerr != nil {
|
||||||
|
if err == nil {
|
||||||
|
err = itemerr
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
mapValue.SetMapIndex(keyValue, itemValue)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value.Set(mapValue)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoderBuilder) newArrayTypeDecoder(t reflect.Type) decoderFunc {
|
||||||
|
itemDecoder := d.typeDecoder(t.Elem())
|
||||||
|
|
||||||
|
return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) {
|
||||||
|
if !node.IsArray() {
|
||||||
|
return fmt.Errorf("apijson: could not deserialize to an array")
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayNode := node.Array()
|
||||||
|
|
||||||
|
arrayValue := reflect.MakeSlice(reflect.SliceOf(t.Elem()), len(arrayNode), len(arrayNode))
|
||||||
|
for i, itemNode := range arrayNode {
|
||||||
|
err = itemDecoder(itemNode, arrayValue.Index(i), state)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value.Set(arrayValue)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc {
|
||||||
|
// map of json field name to struct field decoders
|
||||||
|
decoderFields := map[string]decoderField{}
|
||||||
|
anonymousDecoders := []decoderField{}
|
||||||
|
extraDecoder := (*decoderField)(nil)
|
||||||
|
inlineDecoder := (*decoderField)(nil)
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
idx := []int{i}
|
||||||
|
field := t.FieldByIndex(idx)
|
||||||
|
if !field.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If this is an embedded struct, traverse one level deeper to extract
|
||||||
|
// the fields and get their encoders as well.
|
||||||
|
if field.Anonymous {
|
||||||
|
anonymousDecoders = append(anonymousDecoders, decoderField{
|
||||||
|
fn: d.typeDecoder(field.Type),
|
||||||
|
idx: idx[:],
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If json tag is not present, then we skip, which is intentionally
|
||||||
|
// different behavior from the stdlib.
|
||||||
|
ptag, ok := parseJSONStructTag(field)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// We only want to support unexported fields if they're tagged with
|
||||||
|
// `extras` because that field shouldn't be part of the public API.
|
||||||
|
if ptag.extras {
|
||||||
|
extraDecoder = &decoderField{ptag, d.typeDecoder(field.Type.Elem()), idx, field.Name}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ptag.inline {
|
||||||
|
inlineDecoder = &decoderField{ptag, d.typeDecoder(field.Type), idx, field.Name}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ptag.metadata {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
oldFormat := d.dateFormat
|
||||||
|
dateFormat, ok := parseFormatStructTag(field)
|
||||||
|
if ok {
|
||||||
|
switch dateFormat {
|
||||||
|
case "date-time":
|
||||||
|
d.dateFormat = time.RFC3339
|
||||||
|
case "date":
|
||||||
|
d.dateFormat = "2006-01-02"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decoderFields[ptag.name] = decoderField{ptag, d.typeDecoder(field.Type), idx, field.Name}
|
||||||
|
d.dateFormat = oldFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) {
|
||||||
|
if field := value.FieldByName("JSON"); field.IsValid() {
|
||||||
|
if raw := field.FieldByName("raw"); raw.IsValid() {
|
||||||
|
setUnexportedField(raw, node.Raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, decoder := range anonymousDecoders {
|
||||||
|
// ignore errors
|
||||||
|
decoder.fn(node, value.FieldByIndex(decoder.idx), state)
|
||||||
|
}
|
||||||
|
|
||||||
|
if inlineDecoder != nil {
|
||||||
|
var meta Field
|
||||||
|
dest := value.FieldByIndex(inlineDecoder.idx)
|
||||||
|
isValid := false
|
||||||
|
if dest.IsValid() && node.Type != gjson.Null {
|
||||||
|
err = inlineDecoder.fn(node, dest, state)
|
||||||
|
if err == nil {
|
||||||
|
isValid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Type == gjson.Null {
|
||||||
|
meta = Field{
|
||||||
|
raw: node.Raw,
|
||||||
|
status: null,
|
||||||
|
}
|
||||||
|
} else if !isValid {
|
||||||
|
meta = Field{
|
||||||
|
raw: node.Raw,
|
||||||
|
status: invalid,
|
||||||
|
}
|
||||||
|
} else if isValid {
|
||||||
|
meta = Field{
|
||||||
|
raw: node.Raw,
|
||||||
|
status: valid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if metadata := getSubField(value, inlineDecoder.idx, inlineDecoder.goname); metadata.IsValid() {
|
||||||
|
metadata.Set(reflect.ValueOf(meta))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
typedExtraType := reflect.Type(nil)
|
||||||
|
typedExtraFields := reflect.Value{}
|
||||||
|
if extraDecoder != nil {
|
||||||
|
typedExtraType = value.FieldByIndex(extraDecoder.idx).Type()
|
||||||
|
typedExtraFields = reflect.MakeMap(typedExtraType)
|
||||||
|
}
|
||||||
|
untypedExtraFields := map[string]Field{}
|
||||||
|
|
||||||
|
for fieldName, itemNode := range node.Map() {
|
||||||
|
df, explicit := decoderFields[fieldName]
|
||||||
|
var (
|
||||||
|
dest reflect.Value
|
||||||
|
fn decoderFunc
|
||||||
|
meta Field
|
||||||
|
)
|
||||||
|
if explicit {
|
||||||
|
fn = df.fn
|
||||||
|
dest = value.FieldByIndex(df.idx)
|
||||||
|
}
|
||||||
|
if !explicit && extraDecoder != nil {
|
||||||
|
dest = reflect.New(typedExtraType.Elem()).Elem()
|
||||||
|
fn = extraDecoder.fn
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid := false
|
||||||
|
if dest.IsValid() && itemNode.Type != gjson.Null {
|
||||||
|
err = fn(itemNode, dest, state)
|
||||||
|
if err == nil {
|
||||||
|
isValid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemNode.Type == gjson.Null {
|
||||||
|
meta = Field{
|
||||||
|
raw: itemNode.Raw,
|
||||||
|
status: null,
|
||||||
|
}
|
||||||
|
} else if !isValid {
|
||||||
|
meta = Field{
|
||||||
|
raw: itemNode.Raw,
|
||||||
|
status: invalid,
|
||||||
|
}
|
||||||
|
} else if isValid {
|
||||||
|
meta = Field{
|
||||||
|
raw: itemNode.Raw,
|
||||||
|
status: valid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if explicit {
|
||||||
|
if metadata := getSubField(value, df.idx, df.goname); metadata.IsValid() {
|
||||||
|
metadata.Set(reflect.ValueOf(meta))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !explicit {
|
||||||
|
untypedExtraFields[fieldName] = meta
|
||||||
|
}
|
||||||
|
if !explicit && extraDecoder != nil {
|
||||||
|
typedExtraFields.SetMapIndex(reflect.ValueOf(fieldName), dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if extraDecoder != nil && typedExtraFields.Len() > 0 {
|
||||||
|
value.FieldByIndex(extraDecoder.idx).Set(typedExtraFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set exactness to 'extras' if there are untyped, extra fields.
|
||||||
|
if len(untypedExtraFields) > 0 && state.exactness > extras {
|
||||||
|
state.exactness = extras
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata := getSubField(value, []int{-1}, "ExtraFields"); metadata.IsValid() && len(untypedExtraFields) > 0 {
|
||||||
|
metadata.Set(reflect.ValueOf(untypedExtraFields))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoderBuilder) newPrimitiveTypeDecoder(t reflect.Type) decoderFunc {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
v.SetString(n.String())
|
||||||
|
if guardStrict(state, n.Type != gjson.String) {
|
||||||
|
return fmt.Errorf("apijson: failed to parse string strictly")
|
||||||
|
}
|
||||||
|
// Everything that is not an object can be loosely stringified.
|
||||||
|
if n.Type == gjson.JSON {
|
||||||
|
return fmt.Errorf("apijson: failed to parse string")
|
||||||
|
}
|
||||||
|
if guardUnknown(state, v) {
|
||||||
|
return fmt.Errorf("apijson: failed string enum validation")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
v.SetBool(n.Bool())
|
||||||
|
if guardStrict(state, n.Type != gjson.True && n.Type != gjson.False) {
|
||||||
|
return fmt.Errorf("apijson: failed to parse bool strictly")
|
||||||
|
}
|
||||||
|
// Numbers and strings that are either 'true' or 'false' can be loosely
|
||||||
|
// deserialized as bool.
|
||||||
|
if n.Type == gjson.String && (n.Raw != "true" && n.Raw != "false") || n.Type == gjson.JSON {
|
||||||
|
return fmt.Errorf("apijson: failed to parse bool")
|
||||||
|
}
|
||||||
|
if guardUnknown(state, v) {
|
||||||
|
return fmt.Errorf("apijson: failed bool enum validation")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
v.SetInt(n.Int())
|
||||||
|
if guardStrict(state, n.Type != gjson.Number || n.Num != float64(int(n.Num))) {
|
||||||
|
return fmt.Errorf("apijson: failed to parse int strictly")
|
||||||
|
}
|
||||||
|
// Numbers, booleans, and strings that maybe look like numbers can be
|
||||||
|
// loosely deserialized as numbers.
|
||||||
|
if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) {
|
||||||
|
return fmt.Errorf("apijson: failed to parse int")
|
||||||
|
}
|
||||||
|
if guardUnknown(state, v) {
|
||||||
|
return fmt.Errorf("apijson: failed int enum validation")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
v.SetUint(n.Uint())
|
||||||
|
if guardStrict(state, n.Type != gjson.Number || n.Num != float64(int(n.Num)) || n.Num < 0) {
|
||||||
|
return fmt.Errorf("apijson: failed to parse uint strictly")
|
||||||
|
}
|
||||||
|
// Numbers, booleans, and strings that maybe look like numbers can be
|
||||||
|
// loosely deserialized as uint.
|
||||||
|
if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) {
|
||||||
|
return fmt.Errorf("apijson: failed to parse uint")
|
||||||
|
}
|
||||||
|
if guardUnknown(state, v) {
|
||||||
|
return fmt.Errorf("apijson: failed uint enum validation")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
v.SetFloat(n.Float())
|
||||||
|
if guardStrict(state, n.Type != gjson.Number) {
|
||||||
|
return fmt.Errorf("apijson: failed to parse float strictly")
|
||||||
|
}
|
||||||
|
// Numbers, booleans, and strings that maybe look like numbers can be
|
||||||
|
// loosely deserialized as floats.
|
||||||
|
if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) {
|
||||||
|
return fmt.Errorf("apijson: failed to parse float")
|
||||||
|
}
|
||||||
|
if guardUnknown(state, v) {
|
||||||
|
return fmt.Errorf("apijson: failed float enum validation")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return func(node gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
return fmt.Errorf("unknown type received at primitive decoder: %s", t.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *decoderBuilder) newTimeTypeDecoder(t reflect.Type) decoderFunc {
|
||||||
|
format := d.dateFormat
|
||||||
|
return func(n gjson.Result, v reflect.Value, state *decoderState) error {
|
||||||
|
parsed, err := time.Parse(format, n.Str)
|
||||||
|
if err == nil {
|
||||||
|
v.Set(reflect.ValueOf(parsed).Convert(t))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if guardStrict(state, true) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
layouts := []string{
|
||||||
|
"2006-01-02",
|
||||||
|
"2006-01-02T15:04:05Z07:00",
|
||||||
|
"2006-01-02T15:04:05Z0700",
|
||||||
|
"2006-01-02T15:04:05",
|
||||||
|
"2006-01-02 15:04:05Z07:00",
|
||||||
|
"2006-01-02 15:04:05Z0700",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, layout := range layouts {
|
||||||
|
parsed, err := time.Parse(layout, n.Str)
|
||||||
|
if err == nil {
|
||||||
|
v.Set(reflect.ValueOf(parsed).Convert(t))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unable to leniently parse date-time string: %s", n.Str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUnexportedField(field reflect.Value, value interface{}) {
|
||||||
|
reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func guardStrict(state *decoderState, cond bool) bool {
|
||||||
|
if !cond {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.strict {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
state.exactness = loose
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func canParseAsNumber(str string) bool {
|
||||||
|
_, err := strconv.ParseFloat(str, 64)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func guardUnknown(state *decoderState, v reflect.Value) bool {
|
||||||
|
if have, ok := v.Interface().(interface{ IsKnown() bool }); guardStrict(state, ok && !have.IsKnown()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
398
packages/sdk/go/internal/apijson/encoder.go
Normal file
398
packages/sdk/go/internal/apijson/encoder.go
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
package apijson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/param"
|
||||||
|
)
|
||||||
|
|
||||||
|
var encoders sync.Map // map[encoderEntry]encoderFunc
|
||||||
|
|
||||||
|
func Marshal(value interface{}) ([]byte, error) {
|
||||||
|
e := &encoder{dateFormat: time.RFC3339}
|
||||||
|
return e.marshal(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MarshalRoot(value interface{}) ([]byte, error) {
|
||||||
|
e := &encoder{root: true, dateFormat: time.RFC3339}
|
||||||
|
return e.marshal(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoder struct {
|
||||||
|
dateFormat string
|
||||||
|
root bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderFunc func(value reflect.Value) ([]byte, error)
|
||||||
|
|
||||||
|
type encoderField struct {
|
||||||
|
tag parsedStructTag
|
||||||
|
fn encoderFunc
|
||||||
|
idx []int
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderEntry struct {
|
||||||
|
reflect.Type
|
||||||
|
dateFormat string
|
||||||
|
root bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) marshal(value interface{}) ([]byte, error) {
|
||||||
|
val := reflect.ValueOf(value)
|
||||||
|
if !val.IsValid() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
typ := val.Type()
|
||||||
|
enc := e.typeEncoder(typ)
|
||||||
|
return enc(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
entry := encoderEntry{
|
||||||
|
Type: t,
|
||||||
|
dateFormat: e.dateFormat,
|
||||||
|
root: e.root,
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi, ok := encoders.Load(entry); ok {
|
||||||
|
return fi.(encoderFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To deal with recursive types, populate the map with an
|
||||||
|
// indirect func before we build it. This type waits on the
|
||||||
|
// real func (f) to be ready and then calls it. This indirect
|
||||||
|
// func is only used for recursive types.
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
f encoderFunc
|
||||||
|
)
|
||||||
|
wg.Add(1)
|
||||||
|
fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(v reflect.Value) ([]byte, error) {
|
||||||
|
wg.Wait()
|
||||||
|
return f(v)
|
||||||
|
}))
|
||||||
|
if loaded {
|
||||||
|
return fi.(encoderFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the real encoder and replace the indirect func with it.
|
||||||
|
f = e.newTypeEncoder(t)
|
||||||
|
wg.Done()
|
||||||
|
encoders.Store(entry, f)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalerEncoder(v reflect.Value) ([]byte, error) {
|
||||||
|
return v.Interface().(json.Marshaler).MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func indirectMarshalerEncoder(v reflect.Value) ([]byte, error) {
|
||||||
|
return v.Addr().Interface().(json.Marshaler).MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
|
||||||
|
return e.newTimeTypeEncoder()
|
||||||
|
}
|
||||||
|
if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) {
|
||||||
|
return marshalerEncoder
|
||||||
|
}
|
||||||
|
if !e.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) {
|
||||||
|
return indirectMarshalerEncoder
|
||||||
|
}
|
||||||
|
e.root = false
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Pointer:
|
||||||
|
inner := t.Elem()
|
||||||
|
|
||||||
|
innerEncoder := e.typeEncoder(inner)
|
||||||
|
return func(v reflect.Value) ([]byte, error) {
|
||||||
|
if !v.IsValid() || v.IsNil() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return innerEncoder(v.Elem())
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
return e.newStructTypeEncoder(t)
|
||||||
|
case reflect.Array:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Slice:
|
||||||
|
return e.newArrayTypeEncoder(t)
|
||||||
|
case reflect.Map:
|
||||||
|
return e.newMapEncoder(t)
|
||||||
|
case reflect.Interface:
|
||||||
|
return e.newInterfaceEncoder()
|
||||||
|
default:
|
||||||
|
return e.newPrimitiveTypeEncoder(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
switch t.Kind() {
|
||||||
|
// Note that we could use `gjson` to encode these types but it would complicate our
|
||||||
|
// code more and this current code shouldn't cause any issues
|
||||||
|
case reflect.String:
|
||||||
|
return func(v reflect.Value) ([]byte, error) {
|
||||||
|
return json.Marshal(v.Interface())
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
return func(v reflect.Value) ([]byte, error) {
|
||||||
|
if v.Bool() {
|
||||||
|
return []byte("true"), nil
|
||||||
|
}
|
||||||
|
return []byte("false"), nil
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return func(v reflect.Value) ([]byte, error) {
|
||||||
|
return []byte(strconv.FormatInt(v.Int(), 10)), nil
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return func(v reflect.Value) ([]byte, error) {
|
||||||
|
return []byte(strconv.FormatUint(v.Uint(), 10)), nil
|
||||||
|
}
|
||||||
|
case reflect.Float32:
|
||||||
|
return func(v reflect.Value) ([]byte, error) {
|
||||||
|
return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 32)), nil
|
||||||
|
}
|
||||||
|
case reflect.Float64:
|
||||||
|
return func(v reflect.Value) ([]byte, error) {
|
||||||
|
return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 64)), nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return func(v reflect.Value) ([]byte, error) {
|
||||||
|
return nil, fmt.Errorf("unknown type received at primitive encoder: %s", t.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
itemEncoder := e.typeEncoder(t.Elem())
|
||||||
|
|
||||||
|
return func(value reflect.Value) ([]byte, error) {
|
||||||
|
json := []byte("[]")
|
||||||
|
for i := 0; i < value.Len(); i++ {
|
||||||
|
var value, err = itemEncoder(value.Index(i))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if value == nil {
|
||||||
|
// Assume that empty items should be inserted as `null` so that the output array
|
||||||
|
// will be the same length as the input array
|
||||||
|
value = []byte("null")
|
||||||
|
}
|
||||||
|
|
||||||
|
json, err = sjson.SetRawBytes(json, "-1", value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
|
||||||
|
return e.newFieldTypeEncoder(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoderFields := []encoderField{}
|
||||||
|
extraEncoder := (*encoderField)(nil)
|
||||||
|
|
||||||
|
// This helper allows us to recursively collect field encoders into a flat
|
||||||
|
// array. The parameter `index` keeps track of the access patterns necessary
|
||||||
|
// to get to some field.
|
||||||
|
var collectEncoderFields func(r reflect.Type, index []int)
|
||||||
|
collectEncoderFields = func(r reflect.Type, index []int) {
|
||||||
|
for i := 0; i < r.NumField(); i++ {
|
||||||
|
idx := append(index, i)
|
||||||
|
field := t.FieldByIndex(idx)
|
||||||
|
if !field.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If this is an embedded struct, traverse one level deeper to extract
|
||||||
|
// the field and get their encoders as well.
|
||||||
|
if field.Anonymous {
|
||||||
|
collectEncoderFields(field.Type, idx)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If json tag is not present, then we skip, which is intentionally
|
||||||
|
// different behavior from the stdlib.
|
||||||
|
ptag, ok := parseJSONStructTag(field)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// We only want to support unexported field if they're tagged with
|
||||||
|
// `extras` because that field shouldn't be part of the public API. We
|
||||||
|
// also want to only keep the top level extras
|
||||||
|
if ptag.extras && len(index) == 0 {
|
||||||
|
extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ptag.name == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dateFormat, ok := parseFormatStructTag(field)
|
||||||
|
oldFormat := e.dateFormat
|
||||||
|
if ok {
|
||||||
|
switch dateFormat {
|
||||||
|
case "date-time":
|
||||||
|
e.dateFormat = time.RFC3339
|
||||||
|
case "date":
|
||||||
|
e.dateFormat = "2006-01-02"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
|
||||||
|
e.dateFormat = oldFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectEncoderFields(t, []int{})
|
||||||
|
|
||||||
|
// Ensure deterministic output by sorting by lexicographic order
|
||||||
|
sort.Slice(encoderFields, func(i, j int) bool {
|
||||||
|
return encoderFields[i].tag.name < encoderFields[j].tag.name
|
||||||
|
})
|
||||||
|
|
||||||
|
return func(value reflect.Value) (json []byte, err error) {
|
||||||
|
json = []byte("{}")
|
||||||
|
|
||||||
|
for _, ef := range encoderFields {
|
||||||
|
field := value.FieldByIndex(ef.idx)
|
||||||
|
encoded, err := ef.fn(field)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if encoded == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
json, err = sjson.SetRawBytes(json, ef.tag.name, encoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if extraEncoder != nil {
|
||||||
|
json, err = e.encodeMapEntries(json, value.FieldByIndex(extraEncoder.idx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
f, _ := t.FieldByName("Value")
|
||||||
|
enc := e.typeEncoder(f.Type)
|
||||||
|
|
||||||
|
return func(value reflect.Value) (json []byte, err error) {
|
||||||
|
present := value.FieldByName("Present")
|
||||||
|
if !present.Bool() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
null := value.FieldByName("Null")
|
||||||
|
if null.Bool() {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
raw := value.FieldByName("Raw")
|
||||||
|
if !raw.IsNil() {
|
||||||
|
return e.typeEncoder(raw.Type())(raw)
|
||||||
|
}
|
||||||
|
return enc(value.FieldByName("Value"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newTimeTypeEncoder() encoderFunc {
|
||||||
|
format := e.dateFormat
|
||||||
|
return func(value reflect.Value) (json []byte, err error) {
|
||||||
|
return []byte(`"` + value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format) + `"`), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) newInterfaceEncoder() encoderFunc {
|
||||||
|
return func(value reflect.Value) ([]byte, error) {
|
||||||
|
value = value.Elem()
|
||||||
|
if !value.IsValid() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return e.typeEncoder(value.Type())(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a []byte of json (may either be an empty object or an object that already contains entries)
|
||||||
|
// encode all of the entries in the map to the json byte array.
|
||||||
|
func (e *encoder) encodeMapEntries(json []byte, v reflect.Value) ([]byte, error) {
|
||||||
|
type mapPair struct {
|
||||||
|
key []byte
|
||||||
|
value reflect.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
pairs := []mapPair{}
|
||||||
|
keyEncoder := e.typeEncoder(v.Type().Key())
|
||||||
|
|
||||||
|
iter := v.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
var encodedKeyString string
|
||||||
|
if iter.Key().Type().Kind() == reflect.String {
|
||||||
|
encodedKeyString = iter.Key().String()
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
encodedKeyBytes, err := keyEncoder(iter.Key())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encodedKeyString = string(encodedKeyBytes)
|
||||||
|
}
|
||||||
|
encodedKey := []byte(sjsonReplacer.Replace(encodedKeyString))
|
||||||
|
pairs = append(pairs, mapPair{key: encodedKey, value: iter.Value()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure deterministic output
|
||||||
|
sort.Slice(pairs, func(i, j int) bool {
|
||||||
|
return bytes.Compare(pairs[i].key, pairs[j].key) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
elementEncoder := e.typeEncoder(v.Type().Elem())
|
||||||
|
for _, p := range pairs {
|
||||||
|
encodedValue, err := elementEncoder(p.value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(encodedValue) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
json, err = sjson.SetRawBytes(json, string(p.key), encodedValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
|
||||||
|
return func(value reflect.Value) ([]byte, error) {
|
||||||
|
json := []byte("{}")
|
||||||
|
var err error
|
||||||
|
json, err = e.encodeMapEntries(json, value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return json, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we want to set a literal key value into JSON using sjson, we need to make sure it doesn't have
|
||||||
|
// special characters that sjson interprets as a path.
|
||||||
|
var sjsonReplacer *strings.Replacer = strings.NewReplacer(".", "\\.", ":", "\\:", "*", "\\*")
|
||||||
41
packages/sdk/go/internal/apijson/field.go
Normal file
41
packages/sdk/go/internal/apijson/field.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package apijson
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
type status uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
missing status = iota
|
||||||
|
null
|
||||||
|
invalid
|
||||||
|
valid
|
||||||
|
)
|
||||||
|
|
||||||
|
type Field struct {
|
||||||
|
raw string
|
||||||
|
status status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the field is explicitly `null` _or_ if it is not present at all (ie, missing).
|
||||||
|
// To check if the field's key is present in the JSON with an explicit null value,
|
||||||
|
// you must check `f.IsNull() && !f.IsMissing()`.
|
||||||
|
func (j Field) IsNull() bool { return j.status <= null }
|
||||||
|
func (j Field) IsMissing() bool { return j.status == missing }
|
||||||
|
func (j Field) IsInvalid() bool { return j.status == invalid }
|
||||||
|
func (j Field) Raw() string { return j.raw }
|
||||||
|
|
||||||
|
func getSubField(root reflect.Value, index []int, name string) reflect.Value {
|
||||||
|
strct := root.FieldByIndex(index[:len(index)-1])
|
||||||
|
if !strct.IsValid() {
|
||||||
|
panic("couldn't find encapsulating struct for field " + name)
|
||||||
|
}
|
||||||
|
meta := strct.FieldByName("JSON")
|
||||||
|
if !meta.IsValid() {
|
||||||
|
return reflect.Value{}
|
||||||
|
}
|
||||||
|
field := meta.FieldByName(name)
|
||||||
|
if !field.IsValid() {
|
||||||
|
return reflect.Value{}
|
||||||
|
}
|
||||||
|
return field
|
||||||
|
}
|
||||||
66
packages/sdk/go/internal/apijson/field_test.go
Normal file
66
packages/sdk/go/internal/apijson/field_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package apijson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/param"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Struct struct {
|
||||||
|
A string `json:"a"`
|
||||||
|
B int64 `json:"b"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldStruct struct {
|
||||||
|
A param.Field[string] `json:"a"`
|
||||||
|
B param.Field[int64] `json:"b"`
|
||||||
|
C param.Field[Struct] `json:"c"`
|
||||||
|
D param.Field[time.Time] `json:"d" format:"date"`
|
||||||
|
E param.Field[time.Time] `json:"e" format:"date-time"`
|
||||||
|
F param.Field[int64] `json:"f"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldMarshal(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
value interface{}
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
"null_string": {param.Field[string]{Present: true, Null: true}, "null"},
|
||||||
|
"null_int": {param.Field[int]{Present: true, Null: true}, "null"},
|
||||||
|
"null_int64": {param.Field[int64]{Present: true, Null: true}, "null"},
|
||||||
|
"null_struct": {param.Field[Struct]{Present: true, Null: true}, "null"},
|
||||||
|
|
||||||
|
"string": {param.Field[string]{Present: true, Value: "string"}, `"string"`},
|
||||||
|
"int": {param.Field[int]{Present: true, Value: 123}, "123"},
|
||||||
|
"int64": {param.Field[int64]{Present: true, Value: int64(123456789123456789)}, "123456789123456789"},
|
||||||
|
"struct": {param.Field[Struct]{Present: true, Value: Struct{A: "yo", B: 123}}, `{"a":"yo","b":123}`},
|
||||||
|
|
||||||
|
"string_raw": {param.Field[int]{Present: true, Raw: "string"}, `"string"`},
|
||||||
|
"int_raw": {param.Field[int]{Present: true, Raw: 123}, "123"},
|
||||||
|
"int64_raw": {param.Field[int]{Present: true, Raw: int64(123456789123456789)}, "123456789123456789"},
|
||||||
|
"struct_raw": {param.Field[int]{Present: true, Raw: Struct{A: "yo", B: 123}}, `{"a":"yo","b":123}`},
|
||||||
|
|
||||||
|
"param_struct": {
|
||||||
|
FieldStruct{
|
||||||
|
A: param.Field[string]{Present: true, Value: "hello"},
|
||||||
|
B: param.Field[int64]{Present: true, Value: int64(12)},
|
||||||
|
D: param.Field[time.Time]{Present: true, Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)},
|
||||||
|
E: param.Field[time.Time]{Present: true, Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)},
|
||||||
|
},
|
||||||
|
`{"a":"hello","b":12,"d":"2023-03-18","e":"2023-03-18T14:47:38Z"}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
b, err := Marshal(test.value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("didn't expect error %v", err)
|
||||||
|
}
|
||||||
|
if string(b) != test.expected {
|
||||||
|
t.Fatalf("expected %s, received %s", test.expected, string(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
617
packages/sdk/go/internal/apijson/json_test.go
Normal file
617
packages/sdk/go/internal/apijson/json_test.go
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
package apijson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func P[T any](v T) *T { return &v }
|
||||||
|
|
||||||
|
type Primitives struct {
|
||||||
|
A bool `json:"a"`
|
||||||
|
B int `json:"b"`
|
||||||
|
C uint `json:"c"`
|
||||||
|
D float64 `json:"d"`
|
||||||
|
E float32 `json:"e"`
|
||||||
|
F []int `json:"f"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrimitivePointers struct {
|
||||||
|
A *bool `json:"a"`
|
||||||
|
B *int `json:"b"`
|
||||||
|
C *uint `json:"c"`
|
||||||
|
D *float64 `json:"d"`
|
||||||
|
E *float32 `json:"e"`
|
||||||
|
F *[]int `json:"f"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Slices struct {
|
||||||
|
Slice []Primitives `json:"slices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DateTime struct {
|
||||||
|
Date time.Time `json:"date" format:"date"`
|
||||||
|
DateTime time.Time `json:"date-time" format:"date-time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdditionalProperties struct {
|
||||||
|
A bool `json:"a"`
|
||||||
|
ExtraFields map[string]interface{} `json:"-,extras"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypedAdditionalProperties struct {
|
||||||
|
A bool `json:"a"`
|
||||||
|
ExtraFields map[string]int `json:"-,extras"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmbeddedStruct struct {
|
||||||
|
A bool `json:"a"`
|
||||||
|
B string `json:"b"`
|
||||||
|
|
||||||
|
JSON EmbeddedStructJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmbeddedStructJSON struct {
|
||||||
|
A Field
|
||||||
|
B Field
|
||||||
|
ExtraFields map[string]Field
|
||||||
|
raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmbeddedStructs struct {
|
||||||
|
EmbeddedStruct
|
||||||
|
A *int `json:"a"`
|
||||||
|
ExtraFields map[string]interface{} `json:"-,extras"`
|
||||||
|
|
||||||
|
JSON EmbeddedStructsJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmbeddedStructsJSON struct {
|
||||||
|
A Field
|
||||||
|
ExtraFields map[string]Field
|
||||||
|
raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Recursive struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Child *Recursive `json:"child"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONFieldStruct struct {
|
||||||
|
A bool `json:"a"`
|
||||||
|
B int64 `json:"b"`
|
||||||
|
C string `json:"c"`
|
||||||
|
D string `json:"d"`
|
||||||
|
ExtraFields map[string]int64 `json:"-,extras"`
|
||||||
|
JSON JSONFieldStructJSON `json:"-,metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONFieldStructJSON struct {
|
||||||
|
A Field
|
||||||
|
B Field
|
||||||
|
C Field
|
||||||
|
D Field
|
||||||
|
ExtraFields map[string]Field
|
||||||
|
raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnknownStruct struct {
|
||||||
|
Unknown interface{} `json:"unknown"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnionStruct struct {
|
||||||
|
Union Union `json:"union" format:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Union interface {
|
||||||
|
union()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Inline struct {
|
||||||
|
InlineField Primitives `json:"-,inline"`
|
||||||
|
JSON InlineJSON `json:"-,metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InlineArray struct {
|
||||||
|
InlineField []string `json:"-,inline"`
|
||||||
|
JSON InlineJSON `json:"-,metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InlineJSON struct {
|
||||||
|
InlineField Field
|
||||||
|
raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnionInteger int64
|
||||||
|
|
||||||
|
func (UnionInteger) union() {}
|
||||||
|
|
||||||
|
type UnionStructA struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
A string `json:"a"`
|
||||||
|
B string `json:"b"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnionStructA) union() {}
|
||||||
|
|
||||||
|
type UnionStructB struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
A string `json:"a"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnionStructB) union() {}
|
||||||
|
|
||||||
|
type UnionTime time.Time
|
||||||
|
|
||||||
|
func (UnionTime) union() {}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterUnion(reflect.TypeOf((*Union)(nil)).Elem(), "type",
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.String,
|
||||||
|
Type: reflect.TypeOf(UnionTime{}),
|
||||||
|
},
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.Number,
|
||||||
|
Type: reflect.TypeOf(UnionInteger(0)),
|
||||||
|
},
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
DiscriminatorValue: "typeA",
|
||||||
|
Type: reflect.TypeOf(UnionStructA{}),
|
||||||
|
},
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
DiscriminatorValue: "typeB",
|
||||||
|
Type: reflect.TypeOf(UnionStructB{}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComplexUnionStruct struct {
|
||||||
|
Union ComplexUnion `json:"union"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComplexUnion interface {
|
||||||
|
complexUnion()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComplexUnionA struct {
|
||||||
|
Boo string `json:"boo"`
|
||||||
|
Foo bool `json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ComplexUnionA) complexUnion() {}
|
||||||
|
|
||||||
|
type ComplexUnionB struct {
|
||||||
|
Boo bool `json:"boo"`
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ComplexUnionB) complexUnion() {}
|
||||||
|
|
||||||
|
type ComplexUnionC struct {
|
||||||
|
Boo int64 `json:"boo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ComplexUnionC) complexUnion() {}
|
||||||
|
|
||||||
|
type ComplexUnionTypeA struct {
|
||||||
|
Baz int64 `json:"baz"`
|
||||||
|
Type TypeA `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ComplexUnionTypeA) complexUnion() {}
|
||||||
|
|
||||||
|
type TypeA string
|
||||||
|
|
||||||
|
func (t TypeA) IsKnown() bool {
|
||||||
|
return t == "a"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComplexUnionTypeB struct {
|
||||||
|
Baz int64 `json:"baz"`
|
||||||
|
Type TypeB `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeB string
|
||||||
|
|
||||||
|
func (t TypeB) IsKnown() bool {
|
||||||
|
return t == "b"
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnmarshalStruct struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
prop bool `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UnmarshalStruct) UnmarshalJSON(json []byte) error {
|
||||||
|
r.prop = true
|
||||||
|
return UnmarshalRoot(json, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ComplexUnionTypeB) complexUnion() {}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterUnion(reflect.TypeOf((*ComplexUnion)(nil)).Elem(), "",
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(ComplexUnionA{}),
|
||||||
|
},
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(ComplexUnionB{}),
|
||||||
|
},
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(ComplexUnionC{}),
|
||||||
|
},
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(ComplexUnionTypeA{}),
|
||||||
|
},
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(ComplexUnionTypeB{}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarshallingUnionStruct struct {
|
||||||
|
Union MarshallingUnion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MarshallingUnionStruct) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
*r = MarshallingUnionStruct{}
|
||||||
|
err = UnmarshalRoot(data, &r.Union)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MarshallingUnionStruct) MarshalJSON() (data []byte, err error) {
|
||||||
|
return MarshalRoot(r.Union)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarshallingUnion interface {
|
||||||
|
marshallingUnion()
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarshallingUnionA struct {
|
||||||
|
Boo string `json:"boo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MarshallingUnionA) marshallingUnion() {}
|
||||||
|
|
||||||
|
func (r *MarshallingUnionA) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarshallingUnionB struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MarshallingUnionB) marshallingUnion() {}
|
||||||
|
|
||||||
|
func (r *MarshallingUnionB) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
return UnmarshalRoot(data, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterUnion(
|
||||||
|
reflect.TypeOf((*MarshallingUnion)(nil)).Elem(),
|
||||||
|
"",
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(MarshallingUnionA{}),
|
||||||
|
},
|
||||||
|
UnionVariant{
|
||||||
|
TypeFilter: gjson.JSON,
|
||||||
|
Type: reflect.TypeOf(MarshallingUnionB{}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tests = map[string]struct {
|
||||||
|
buf string
|
||||||
|
val interface{}
|
||||||
|
}{
|
||||||
|
"true": {"true", true},
|
||||||
|
"false": {"false", false},
|
||||||
|
"int": {"1", 1},
|
||||||
|
"int_bigger": {"12324", 12324},
|
||||||
|
"int_string_coerce": {`"65"`, 65},
|
||||||
|
"int_boolean_coerce": {"true", 1},
|
||||||
|
"int64": {"1", int64(1)},
|
||||||
|
"int64_huge": {"123456789123456789", int64(123456789123456789)},
|
||||||
|
"uint": {"1", uint(1)},
|
||||||
|
"uint_bigger": {"12324", uint(12324)},
|
||||||
|
"uint_coerce": {`"65"`, uint(65)},
|
||||||
|
"float_1.54": {"1.54", float32(1.54)},
|
||||||
|
"float_1.89": {"1.89", float64(1.89)},
|
||||||
|
"string": {`"str"`, "str"},
|
||||||
|
"string_int_coerce": {`12`, "12"},
|
||||||
|
"array_string": {`["foo","bar"]`, []string{"foo", "bar"}},
|
||||||
|
"array_int": {`[1,2]`, []int{1, 2}},
|
||||||
|
"array_int_coerce": {`["1",2]`, []int{1, 2}},
|
||||||
|
|
||||||
|
"ptr_true": {"true", P(true)},
|
||||||
|
"ptr_false": {"false", P(false)},
|
||||||
|
"ptr_int": {"1", P(1)},
|
||||||
|
"ptr_int_bigger": {"12324", P(12324)},
|
||||||
|
"ptr_int_string_coerce": {`"65"`, P(65)},
|
||||||
|
"ptr_int_boolean_coerce": {"true", P(1)},
|
||||||
|
"ptr_int64": {"1", P(int64(1))},
|
||||||
|
"ptr_int64_huge": {"123456789123456789", P(int64(123456789123456789))},
|
||||||
|
"ptr_uint": {"1", P(uint(1))},
|
||||||
|
"ptr_uint_bigger": {"12324", P(uint(12324))},
|
||||||
|
"ptr_uint_coerce": {`"65"`, P(uint(65))},
|
||||||
|
"ptr_float_1.54": {"1.54", P(float32(1.54))},
|
||||||
|
"ptr_float_1.89": {"1.89", P(float64(1.89))},
|
||||||
|
|
||||||
|
"date_time": {`"2007-03-01T13:00:00Z"`, time.Date(2007, time.March, 1, 13, 0, 0, 0, time.UTC)},
|
||||||
|
"date_time_nano_coerce": {`"2007-03-01T13:03:05.123456789Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 123456789, time.UTC)},
|
||||||
|
|
||||||
|
"date_time_missing_t_coerce": {`"2007-03-01 13:03:05Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.UTC)},
|
||||||
|
"date_time_missing_timezone_coerce": {`"2007-03-01T13:03:05"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.UTC)},
|
||||||
|
// note: using -1200 to minimize probability of conflicting with the local timezone of the test runner
|
||||||
|
// see https://en.wikipedia.org/wiki/UTC%E2%88%9212:00
|
||||||
|
"date_time_missing_timezone_colon_coerce": {`"2007-03-01T13:03:05-1200"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.FixedZone("", -12*60*60))},
|
||||||
|
"date_time_nano_missing_t_coerce": {`"2007-03-01 13:03:05.123456789Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 123456789, time.UTC)},
|
||||||
|
|
||||||
|
"map_string": {`{"foo":"bar"}`, map[string]string{"foo": "bar"}},
|
||||||
|
"map_string_with_sjson_path_chars": {`{":a.b.c*:d*-1e.f":"bar"}`, map[string]string{":a.b.c*:d*-1e.f": "bar"}},
|
||||||
|
"map_interface": {`{"a":1,"b":"str","c":false}`, map[string]interface{}{"a": float64(1), "b": "str", "c": false}},
|
||||||
|
|
||||||
|
"primitive_struct": {
|
||||||
|
`{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}`,
|
||||||
|
Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"slices": {
|
||||||
|
`{"slices":[{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}]}`,
|
||||||
|
Slices{
|
||||||
|
Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"primitive_pointer_struct": {
|
||||||
|
`{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4,5]}`,
|
||||||
|
PrimitivePointers{
|
||||||
|
A: P(false),
|
||||||
|
B: P(237628372683),
|
||||||
|
C: P(uint(654)),
|
||||||
|
D: P(9999.43),
|
||||||
|
E: P(float32(43.76)),
|
||||||
|
F: &[]int{1, 2, 3, 4, 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"datetime_struct": {
|
||||||
|
`{"date":"2006-01-02","date-time":"2006-01-02T15:04:05Z"}`,
|
||||||
|
DateTime{
|
||||||
|
Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC),
|
||||||
|
DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"additional_properties": {
|
||||||
|
`{"a":true,"bar":"value","foo":true}`,
|
||||||
|
AdditionalProperties{
|
||||||
|
A: true,
|
||||||
|
ExtraFields: map[string]interface{}{
|
||||||
|
"bar": "value",
|
||||||
|
"foo": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"embedded_struct": {
|
||||||
|
`{"a":1,"b":"bar"}`,
|
||||||
|
EmbeddedStructs{
|
||||||
|
EmbeddedStruct: EmbeddedStruct{
|
||||||
|
A: true,
|
||||||
|
B: "bar",
|
||||||
|
JSON: EmbeddedStructJSON{
|
||||||
|
A: Field{raw: `1`, status: valid},
|
||||||
|
B: Field{raw: `"bar"`, status: valid},
|
||||||
|
raw: `{"a":1,"b":"bar"}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
A: P(1),
|
||||||
|
ExtraFields: map[string]interface{}{"b": "bar"},
|
||||||
|
JSON: EmbeddedStructsJSON{
|
||||||
|
A: Field{raw: `1`, status: valid},
|
||||||
|
ExtraFields: map[string]Field{
|
||||||
|
"b": {raw: `"bar"`, status: valid},
|
||||||
|
},
|
||||||
|
raw: `{"a":1,"b":"bar"}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"recursive_struct": {
|
||||||
|
`{"child":{"name":"Alex"},"name":"Robert"}`,
|
||||||
|
Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"metadata_coerce": {
|
||||||
|
`{"a":"12","b":"12","c":null,"extra_typed":12,"extra_untyped":{"foo":"bar"}}`,
|
||||||
|
JSONFieldStruct{
|
||||||
|
A: false,
|
||||||
|
B: 12,
|
||||||
|
C: "",
|
||||||
|
JSON: JSONFieldStructJSON{
|
||||||
|
raw: `{"a":"12","b":"12","c":null,"extra_typed":12,"extra_untyped":{"foo":"bar"}}`,
|
||||||
|
A: Field{raw: `"12"`, status: invalid},
|
||||||
|
B: Field{raw: `"12"`, status: valid},
|
||||||
|
C: Field{raw: "null", status: null},
|
||||||
|
D: Field{raw: "", status: missing},
|
||||||
|
ExtraFields: map[string]Field{
|
||||||
|
"extra_typed": {
|
||||||
|
raw: "12",
|
||||||
|
status: valid,
|
||||||
|
},
|
||||||
|
"extra_untyped": {
|
||||||
|
raw: `{"foo":"bar"}`,
|
||||||
|
status: invalid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExtraFields: map[string]int64{
|
||||||
|
"extra_typed": 12,
|
||||||
|
"extra_untyped": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"unknown_struct_number": {
|
||||||
|
`{"unknown":12}`,
|
||||||
|
UnknownStruct{
|
||||||
|
Unknown: 12.,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"unknown_struct_map": {
|
||||||
|
`{"unknown":{"foo":"bar"}}`,
|
||||||
|
UnknownStruct{
|
||||||
|
Unknown: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_integer": {
|
||||||
|
`{"union":12}`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionInteger(12),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_struct_discriminated_a": {
|
||||||
|
`{"union":{"a":"foo","b":"bar","type":"typeA"}}`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionStructA{
|
||||||
|
Type: "typeA",
|
||||||
|
A: "foo",
|
||||||
|
B: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_struct_discriminated_b": {
|
||||||
|
`{"union":{"a":"foo","type":"typeB"}}`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionStructB{
|
||||||
|
Type: "typeB",
|
||||||
|
A: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_struct_time": {
|
||||||
|
`{"union":"2010-05-23"}`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"complex_union_a": {
|
||||||
|
`{"union":{"boo":"12","foo":true}}`,
|
||||||
|
ComplexUnionStruct{Union: ComplexUnionA{Boo: "12", Foo: true}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"complex_union_b": {
|
||||||
|
`{"union":{"boo":true,"foo":"12"}}`,
|
||||||
|
ComplexUnionStruct{Union: ComplexUnionB{Boo: true, Foo: "12"}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"complex_union_c": {
|
||||||
|
`{"union":{"boo":12}}`,
|
||||||
|
ComplexUnionStruct{Union: ComplexUnionC{Boo: 12}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"complex_union_type_a": {
|
||||||
|
`{"union":{"baz":12,"type":"a"}}`,
|
||||||
|
ComplexUnionStruct{Union: ComplexUnionTypeA{Baz: 12, Type: TypeA("a")}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"complex_union_type_b": {
|
||||||
|
`{"union":{"baz":12,"type":"b"}}`,
|
||||||
|
ComplexUnionStruct{Union: ComplexUnionTypeB{Baz: 12, Type: TypeB("b")}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"marshalling_union_a": {
|
||||||
|
`{"boo":"hello"}`,
|
||||||
|
MarshallingUnionStruct{Union: MarshallingUnionA{Boo: "hello"}},
|
||||||
|
},
|
||||||
|
"marshalling_union_b": {
|
||||||
|
`{"foo":"hi"}`,
|
||||||
|
MarshallingUnionStruct{Union: MarshallingUnionB{Foo: "hi"}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"unmarshal": {
|
||||||
|
`{"foo":"hello"}`,
|
||||||
|
&UnmarshalStruct{Foo: "hello", prop: true},
|
||||||
|
},
|
||||||
|
|
||||||
|
"array_of_unmarshal": {
|
||||||
|
`[{"foo":"hello"}]`,
|
||||||
|
[]UnmarshalStruct{{Foo: "hello", prop: true}},
|
||||||
|
},
|
||||||
|
|
||||||
|
"inline_coerce": {
|
||||||
|
`{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}`,
|
||||||
|
Inline{
|
||||||
|
InlineField: Primitives{A: false, B: 237628372683, C: 0x28e, D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||||
|
JSON: InlineJSON{
|
||||||
|
InlineField: Field{raw: "{\"a\":false,\"b\":237628372683,\"c\":654,\"d\":9999.43,\"e\":43.76,\"f\":[1,2,3,4]}", status: 3},
|
||||||
|
raw: "{\"a\":false,\"b\":237628372683,\"c\":654,\"d\":9999.43,\"e\":43.76,\"f\":[1,2,3,4]}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
"inline_array_coerce": {
|
||||||
|
`["Hello","foo","bar"]`,
|
||||||
|
InlineArray{
|
||||||
|
InlineField: []string{"Hello", "foo", "bar"},
|
||||||
|
JSON: InlineJSON{
|
||||||
|
InlineField: Field{raw: `["Hello","foo","bar"]`, status: 3},
|
||||||
|
raw: `["Hello","foo","bar"]`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
result := reflect.New(reflect.TypeOf(test.val))
|
||||||
|
if err := Unmarshal([]byte(test.buf), result.Interface()); err != nil {
|
||||||
|
t.Fatalf("deserialization of %v failed with error %v", result, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(result.Elem().Interface(), test.val) {
|
||||||
|
t.Fatalf("expected '%s' to deserialize to \n%#v\nbut got\n%#v", test.buf, test.val, result.Elem().Interface())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
for name, test := range tests {
|
||||||
|
if strings.HasSuffix(name, "_coerce") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
raw, err := Marshal(test.val)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("serialization of %v failed with error %v", test.val, err)
|
||||||
|
}
|
||||||
|
if string(raw) != test.buf {
|
||||||
|
t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.buf, string(raw))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
120
packages/sdk/go/internal/apijson/port.go
Normal file
120
packages/sdk/go/internal/apijson/port.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package apijson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Port copies over values from one struct to another struct.
|
||||||
|
func Port(from any, to any) error {
|
||||||
|
toVal := reflect.ValueOf(to)
|
||||||
|
fromVal := reflect.ValueOf(from)
|
||||||
|
|
||||||
|
if toVal.Kind() != reflect.Ptr || toVal.IsNil() {
|
||||||
|
return fmt.Errorf("destination must be a non-nil pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
for toVal.Kind() == reflect.Ptr {
|
||||||
|
toVal = toVal.Elem()
|
||||||
|
}
|
||||||
|
toType := toVal.Type()
|
||||||
|
|
||||||
|
for fromVal.Kind() == reflect.Ptr {
|
||||||
|
fromVal = fromVal.Elem()
|
||||||
|
}
|
||||||
|
fromType := fromVal.Type()
|
||||||
|
|
||||||
|
if toType.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("destination must be a non-nil pointer to a struct (%v %v)", toType, toType.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
values := map[string]reflect.Value{}
|
||||||
|
fields := map[string]reflect.Value{}
|
||||||
|
|
||||||
|
fromJSON := fromVal.FieldByName("JSON")
|
||||||
|
toJSON := toVal.FieldByName("JSON")
|
||||||
|
|
||||||
|
// Iterate through the fields of v and load all the "normal" fields in the struct to the map of
|
||||||
|
// string to reflect.Value, as well as their raw .JSON.Foo counterpart indicated by j.
|
||||||
|
var getFields func(t reflect.Type, v reflect.Value)
|
||||||
|
getFields = func(t reflect.Type, v reflect.Value) {
|
||||||
|
j := v.FieldByName("JSON")
|
||||||
|
|
||||||
|
// Recurse into anonymous fields first, since the fields on the object should win over the fields in the
|
||||||
|
// embedded object.
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
if field.Anonymous {
|
||||||
|
getFields(field.Type, v.Field(i))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
ptag, ok := parseJSONStructTag(field)
|
||||||
|
if !ok || ptag.name == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values[ptag.name] = v.Field(i)
|
||||||
|
if j.IsValid() {
|
||||||
|
fields[ptag.name] = j.FieldByName(field.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getFields(fromType, fromVal)
|
||||||
|
|
||||||
|
// Use the values from the previous step to populate the 'to' struct.
|
||||||
|
for i := 0; i < toType.NumField(); i++ {
|
||||||
|
field := toType.Field(i)
|
||||||
|
ptag, ok := parseJSONStructTag(field)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ptag.name == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if value, ok := values[ptag.name]; ok {
|
||||||
|
delete(values, ptag.name)
|
||||||
|
if field.Type.Kind() == reflect.Interface {
|
||||||
|
toVal.Field(i).Set(value)
|
||||||
|
} else {
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
toVal.Field(i).SetString(value.String())
|
||||||
|
case reflect.Bool:
|
||||||
|
toVal.Field(i).SetBool(value.Bool())
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
toVal.Field(i).SetInt(value.Int())
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
toVal.Field(i).SetUint(value.Uint())
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
toVal.Field(i).SetFloat(value.Float())
|
||||||
|
default:
|
||||||
|
toVal.Field(i).Set(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fromJSONField, ok := fields[ptag.name]; ok {
|
||||||
|
if toJSONField := toJSON.FieldByName(field.Name); toJSONField.IsValid() {
|
||||||
|
toJSONField.Set(fromJSONField)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, copy over the .JSON.raw and .JSON.ExtraFields
|
||||||
|
if toJSON.IsValid() {
|
||||||
|
if raw := toJSON.FieldByName("raw"); raw.IsValid() {
|
||||||
|
setUnexportedField(raw, fromJSON.Interface().(interface{ RawJSON() string }).RawJSON())
|
||||||
|
}
|
||||||
|
|
||||||
|
if toExtraFields := toJSON.FieldByName("ExtraFields"); toExtraFields.IsValid() {
|
||||||
|
if fromExtraFields := fromJSON.FieldByName("ExtraFields"); fromExtraFields.IsValid() {
|
||||||
|
setUnexportedField(toExtraFields, fromExtraFields.Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
257
packages/sdk/go/internal/apijson/port_test.go
Normal file
257
packages/sdk/go/internal/apijson/port_test.go
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
package apijson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card is the "combined" type of CardVisa and CardMastercard
|
||||||
|
type Card struct {
|
||||||
|
Processor CardProcessor `json:"processor"`
|
||||||
|
Data any `json:"data"`
|
||||||
|
IsFoo bool `json:"is_foo"`
|
||||||
|
IsBar bool `json:"is_bar"`
|
||||||
|
Metadata Metadata `json:"metadata"`
|
||||||
|
Value interface{} `json:"value"`
|
||||||
|
|
||||||
|
JSON cardJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
type cardJSON struct {
|
||||||
|
Processor Field
|
||||||
|
Data Field
|
||||||
|
IsFoo Field
|
||||||
|
IsBar Field
|
||||||
|
Metadata Field
|
||||||
|
Value Field
|
||||||
|
ExtraFields map[string]Field
|
||||||
|
raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r cardJSON) RawJSON() string { return r.raw }
|
||||||
|
|
||||||
|
type CardProcessor string
|
||||||
|
|
||||||
|
// CardVisa
|
||||||
|
type CardVisa struct {
|
||||||
|
Processor CardVisaProcessor `json:"processor"`
|
||||||
|
Data CardVisaData `json:"data"`
|
||||||
|
IsFoo bool `json:"is_foo"`
|
||||||
|
Metadata Metadata `json:"metadata"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
|
||||||
|
JSON cardVisaJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
type cardVisaJSON struct {
|
||||||
|
Processor Field
|
||||||
|
Data Field
|
||||||
|
IsFoo Field
|
||||||
|
Metadata Field
|
||||||
|
Value Field
|
||||||
|
ExtraFields map[string]Field
|
||||||
|
raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r cardVisaJSON) RawJSON() string { return r.raw }
|
||||||
|
|
||||||
|
type CardVisaProcessor string
|
||||||
|
|
||||||
|
type CardVisaData struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CardMastercard
|
||||||
|
type CardMastercard struct {
|
||||||
|
Processor CardMastercardProcessor `json:"processor"`
|
||||||
|
Data CardMastercardData `json:"data"`
|
||||||
|
IsBar bool `json:"is_bar"`
|
||||||
|
Metadata Metadata `json:"metadata"`
|
||||||
|
Value bool `json:"value"`
|
||||||
|
|
||||||
|
JSON cardMastercardJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
type cardMastercardJSON struct {
|
||||||
|
Processor Field
|
||||||
|
Data Field
|
||||||
|
IsBar Field
|
||||||
|
Metadata Field
|
||||||
|
Value Field
|
||||||
|
ExtraFields map[string]Field
|
||||||
|
raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r cardMastercardJSON) RawJSON() string { return r.raw }
|
||||||
|
|
||||||
|
type CardMastercardProcessor string
|
||||||
|
|
||||||
|
type CardMastercardData struct {
|
||||||
|
Bar int64 `json:"bar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommonFields struct {
|
||||||
|
Metadata Metadata `json:"metadata"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
|
||||||
|
JSON commonFieldsJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
type commonFieldsJSON struct {
|
||||||
|
Metadata Field
|
||||||
|
Value Field
|
||||||
|
ExtraFields map[string]Field
|
||||||
|
raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CardEmbedded struct {
|
||||||
|
CommonFields
|
||||||
|
Processor CardVisaProcessor `json:"processor"`
|
||||||
|
Data CardVisaData `json:"data"`
|
||||||
|
IsFoo bool `json:"is_foo"`
|
||||||
|
|
||||||
|
JSON cardEmbeddedJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
type cardEmbeddedJSON struct {
|
||||||
|
Processor Field
|
||||||
|
Data Field
|
||||||
|
IsFoo Field
|
||||||
|
ExtraFields map[string]Field
|
||||||
|
raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r cardEmbeddedJSON) RawJSON() string { return r.raw }
|
||||||
|
|
||||||
|
var portTests = map[string]struct {
|
||||||
|
from any
|
||||||
|
to any
|
||||||
|
}{
|
||||||
|
"visa to card": {
|
||||||
|
CardVisa{
|
||||||
|
Processor: "visa",
|
||||||
|
IsFoo: true,
|
||||||
|
Data: CardVisaData{
|
||||||
|
Foo: "foo",
|
||||||
|
},
|
||||||
|
Metadata: Metadata{
|
||||||
|
CreatedAt: "Mar 29 2024",
|
||||||
|
},
|
||||||
|
Value: "value",
|
||||||
|
JSON: cardVisaJSON{
|
||||||
|
raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`,
|
||||||
|
Processor: Field{raw: `"visa"`, status: valid},
|
||||||
|
IsFoo: Field{raw: `true`, status: valid},
|
||||||
|
Data: Field{raw: `{"foo":"foo"}`, status: valid},
|
||||||
|
Value: Field{raw: `"value"`, status: valid},
|
||||||
|
ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Card{
|
||||||
|
Processor: "visa",
|
||||||
|
IsFoo: true,
|
||||||
|
IsBar: false,
|
||||||
|
Data: CardVisaData{
|
||||||
|
Foo: "foo",
|
||||||
|
},
|
||||||
|
Metadata: Metadata{
|
||||||
|
CreatedAt: "Mar 29 2024",
|
||||||
|
},
|
||||||
|
Value: "value",
|
||||||
|
JSON: cardJSON{
|
||||||
|
raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`,
|
||||||
|
Processor: Field{raw: `"visa"`, status: valid},
|
||||||
|
IsFoo: Field{raw: `true`, status: valid},
|
||||||
|
Data: Field{raw: `{"foo":"foo"}`, status: valid},
|
||||||
|
Value: Field{raw: `"value"`, status: valid},
|
||||||
|
ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"mastercard to card": {
|
||||||
|
CardMastercard{
|
||||||
|
Processor: "mastercard",
|
||||||
|
IsBar: true,
|
||||||
|
Data: CardMastercardData{
|
||||||
|
Bar: 13,
|
||||||
|
},
|
||||||
|
Value: false,
|
||||||
|
},
|
||||||
|
Card{
|
||||||
|
Processor: "mastercard",
|
||||||
|
IsFoo: false,
|
||||||
|
IsBar: true,
|
||||||
|
Data: CardMastercardData{
|
||||||
|
Bar: 13,
|
||||||
|
},
|
||||||
|
Value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"embedded to card": {
|
||||||
|
CardEmbedded{
|
||||||
|
CommonFields: CommonFields{
|
||||||
|
Metadata: Metadata{
|
||||||
|
CreatedAt: "Mar 29 2024",
|
||||||
|
},
|
||||||
|
Value: "embedded_value",
|
||||||
|
JSON: commonFieldsJSON{
|
||||||
|
Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: valid},
|
||||||
|
Value: Field{raw: `"embedded_value"`, status: valid},
|
||||||
|
raw: `should not matter`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Processor: "visa",
|
||||||
|
IsFoo: true,
|
||||||
|
Data: CardVisaData{
|
||||||
|
Foo: "embedded_foo",
|
||||||
|
},
|
||||||
|
JSON: cardEmbeddedJSON{
|
||||||
|
raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`,
|
||||||
|
Processor: Field{raw: `"visa"`, status: valid},
|
||||||
|
IsFoo: Field{raw: `true`, status: valid},
|
||||||
|
Data: Field{raw: `{"foo":"embedded_foo"}`, status: valid},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Card{
|
||||||
|
Processor: "visa",
|
||||||
|
IsFoo: true,
|
||||||
|
IsBar: false,
|
||||||
|
Data: CardVisaData{
|
||||||
|
Foo: "embedded_foo",
|
||||||
|
},
|
||||||
|
Metadata: Metadata{
|
||||||
|
CreatedAt: "Mar 29 2024",
|
||||||
|
},
|
||||||
|
Value: "embedded_value",
|
||||||
|
JSON: cardJSON{
|
||||||
|
raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`,
|
||||||
|
Processor: Field{raw: `"visa"`, status: 0x3},
|
||||||
|
IsFoo: Field{raw: "true", status: 0x3},
|
||||||
|
Data: Field{raw: `{"foo":"embedded_foo"}`, status: 0x3},
|
||||||
|
Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: 0x3},
|
||||||
|
Value: Field{raw: `"embedded_value"`, status: 0x3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPort(t *testing.T) {
|
||||||
|
for name, test := range portTests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
toVal := reflect.New(reflect.TypeOf(test.to))
|
||||||
|
|
||||||
|
err := Port(test.from, toVal.Interface())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("port of %v failed with error %v", test.from, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(toVal.Elem().Interface(), test.to) {
|
||||||
|
t.Fatalf("expected:\n%+#v\n\nto port to:\n%+#v\n\nbut got:\n%+#v", test.from, test.to, toVal.Elem().Interface())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
41
packages/sdk/go/internal/apijson/registry.go
Normal file
41
packages/sdk/go/internal/apijson/registry.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package apijson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnionVariant struct {
|
||||||
|
TypeFilter gjson.Type
|
||||||
|
DiscriminatorValue interface{}
|
||||||
|
Type reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
var unionRegistry = map[reflect.Type]unionEntry{}
|
||||||
|
var unionVariants = map[reflect.Type]interface{}{}
|
||||||
|
|
||||||
|
type unionEntry struct {
|
||||||
|
discriminatorKey string
|
||||||
|
variants []UnionVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterUnion(typ reflect.Type, discriminator string, variants ...UnionVariant) {
|
||||||
|
unionRegistry[typ] = unionEntry{
|
||||||
|
discriminatorKey: discriminator,
|
||||||
|
variants: variants,
|
||||||
|
}
|
||||||
|
for _, variant := range variants {
|
||||||
|
unionVariants[variant.Type] = typ
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Useful to wrap a union type to force it to use [apijson.UnmarshalJSON] since you cannot define an
|
||||||
|
// UnmarshalJSON function on the interface itself.
|
||||||
|
type UnionUnmarshaler[T any] struct {
|
||||||
|
Value T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UnionUnmarshaler[T]) UnmarshalJSON(buf []byte) error {
|
||||||
|
return UnmarshalRoot(buf, &c.Value)
|
||||||
|
}
|
||||||
47
packages/sdk/go/internal/apijson/tag.go
Normal file
47
packages/sdk/go/internal/apijson/tag.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package apijson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const jsonStructTag = "json"
|
||||||
|
const formatStructTag = "format"
|
||||||
|
|
||||||
|
type parsedStructTag struct {
|
||||||
|
name string
|
||||||
|
required bool
|
||||||
|
extras bool
|
||||||
|
metadata bool
|
||||||
|
inline bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
|
||||||
|
raw, ok := field.Tag.Lookup(jsonStructTag)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return tag, false
|
||||||
|
}
|
||||||
|
tag.name = parts[0]
|
||||||
|
for _, part := range parts[1:] {
|
||||||
|
switch part {
|
||||||
|
case "required":
|
||||||
|
tag.required = true
|
||||||
|
case "extras":
|
||||||
|
tag.extras = true
|
||||||
|
case "metadata":
|
||||||
|
tag.metadata = true
|
||||||
|
case "inline":
|
||||||
|
tag.inline = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFormatStructTag(field reflect.StructField) (format string, ok bool) {
|
||||||
|
format, ok = field.Tag.Lookup(formatStructTag)
|
||||||
|
return
|
||||||
|
}
|
||||||
341
packages/sdk/go/internal/apiquery/encoder.go
Normal file
341
packages/sdk/go/internal/apiquery/encoder.go
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
package apiquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/param"
|
||||||
|
)
|
||||||
|
|
||||||
|
var encoders sync.Map // map[reflect.Type]encoderFunc
|
||||||
|
|
||||||
|
type encoder struct {
|
||||||
|
dateFormat string
|
||||||
|
root bool
|
||||||
|
settings QuerySettings
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderFunc func(key string, value reflect.Value) []Pair
|
||||||
|
|
||||||
|
type encoderField struct {
|
||||||
|
tag parsedStructTag
|
||||||
|
fn encoderFunc
|
||||||
|
idx []int
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderEntry struct {
|
||||||
|
reflect.Type
|
||||||
|
dateFormat string
|
||||||
|
root bool
|
||||||
|
settings QuerySettings
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pair struct {
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
entry := encoderEntry{
|
||||||
|
Type: t,
|
||||||
|
dateFormat: e.dateFormat,
|
||||||
|
root: e.root,
|
||||||
|
settings: e.settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi, ok := encoders.Load(entry); ok {
|
||||||
|
return fi.(encoderFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To deal with recursive types, populate the map with an
|
||||||
|
// indirect func before we build it. This type waits on the
|
||||||
|
// real func (f) to be ready and then calls it. This indirect
|
||||||
|
// func is only used for recursive types.
|
||||||
|
var (
|
||||||
|
wg sync.WaitGroup
|
||||||
|
f encoderFunc
|
||||||
|
)
|
||||||
|
wg.Add(1)
|
||||||
|
fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value) []Pair {
|
||||||
|
wg.Wait()
|
||||||
|
return f(key, v)
|
||||||
|
}))
|
||||||
|
if loaded {
|
||||||
|
return fi.(encoderFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the real encoder and replace the indirect func with it.
|
||||||
|
f = e.newTypeEncoder(t)
|
||||||
|
wg.Done()
|
||||||
|
encoders.Store(entry, f)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalerEncoder(key string, value reflect.Value) []Pair {
|
||||||
|
s, _ := value.Interface().(json.Marshaler).MarshalJSON()
|
||||||
|
return []Pair{{key, string(s)}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
|
||||||
|
return e.newTimeTypeEncoder(t)
|
||||||
|
}
|
||||||
|
if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) {
|
||||||
|
return marshalerEncoder
|
||||||
|
}
|
||||||
|
e.root = false
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Pointer:
|
||||||
|
encoder := e.typeEncoder(t.Elem())
|
||||||
|
return func(key string, value reflect.Value) (pairs []Pair) {
|
||||||
|
if !value.IsValid() || value.IsNil() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pairs = encoder(key, value.Elem())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case reflect.Struct:
|
||||||
|
return e.newStructTypeEncoder(t)
|
||||||
|
case reflect.Array:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Slice:
|
||||||
|
return e.newArrayTypeEncoder(t)
|
||||||
|
case reflect.Map:
|
||||||
|
return e.newMapEncoder(t)
|
||||||
|
case reflect.Interface:
|
||||||
|
return e.newInterfaceEncoder()
|
||||||
|
default:
|
||||||
|
return e.newPrimitiveTypeEncoder(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
|
||||||
|
return e.newFieldTypeEncoder(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoderFields := []encoderField{}
|
||||||
|
|
||||||
|
// This helper allows us to recursively collect field encoders into a flat
|
||||||
|
// array. The parameter `index` keeps track of the access patterns necessary
|
||||||
|
// to get to some field.
|
||||||
|
var collectEncoderFields func(r reflect.Type, index []int)
|
||||||
|
collectEncoderFields = func(r reflect.Type, index []int) {
|
||||||
|
for i := 0; i < r.NumField(); i++ {
|
||||||
|
idx := append(index, i)
|
||||||
|
field := t.FieldByIndex(idx)
|
||||||
|
if !field.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If this is an embedded struct, traverse one level deeper to extract
|
||||||
|
// the field and get their encoders as well.
|
||||||
|
if field.Anonymous {
|
||||||
|
collectEncoderFields(field.Type, idx)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If query tag is not present, then we skip, which is intentionally
|
||||||
|
// different behavior from the stdlib.
|
||||||
|
ptag, ok := parseQueryStructTag(field)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ptag.name == "-" && !ptag.inline {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dateFormat, ok := parseFormatStructTag(field)
|
||||||
|
oldFormat := e.dateFormat
|
||||||
|
if ok {
|
||||||
|
switch dateFormat {
|
||||||
|
case "date-time":
|
||||||
|
e.dateFormat = time.RFC3339
|
||||||
|
case "date":
|
||||||
|
e.dateFormat = "2006-01-02"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
|
||||||
|
e.dateFormat = oldFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectEncoderFields(t, []int{})
|
||||||
|
|
||||||
|
return func(key string, value reflect.Value) (pairs []Pair) {
|
||||||
|
for _, ef := range encoderFields {
|
||||||
|
var subkey string = e.renderKeyPath(key, ef.tag.name)
|
||||||
|
if ef.tag.inline {
|
||||||
|
subkey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
field := value.FieldByIndex(ef.idx)
|
||||||
|
pairs = append(pairs, ef.fn(subkey, field)...)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
|
||||||
|
keyEncoder := e.typeEncoder(t.Key())
|
||||||
|
elementEncoder := e.typeEncoder(t.Elem())
|
||||||
|
return func(key string, value reflect.Value) (pairs []Pair) {
|
||||||
|
iter := value.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
encodedKey := keyEncoder("", iter.Key())
|
||||||
|
if len(encodedKey) != 1 {
|
||||||
|
panic("Unexpected number of parts for encoded map key. Are you using a non-primitive for this map?")
|
||||||
|
}
|
||||||
|
subkey := encodedKey[0].value
|
||||||
|
keyPath := e.renderKeyPath(key, subkey)
|
||||||
|
pairs = append(pairs, elementEncoder(keyPath, iter.Value())...)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) renderKeyPath(key string, subkey string) string {
|
||||||
|
if len(key) == 0 {
|
||||||
|
return subkey
|
||||||
|
}
|
||||||
|
if e.settings.NestedFormat == NestedQueryFormatDots {
|
||||||
|
return fmt.Sprintf("%s.%s", key, subkey)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s[%s]", key, subkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
switch e.settings.ArrayFormat {
|
||||||
|
case ArrayQueryFormatComma:
|
||||||
|
innerEncoder := e.typeEncoder(t.Elem())
|
||||||
|
return func(key string, v reflect.Value) []Pair {
|
||||||
|
elements := []string{}
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
for _, pair := range innerEncoder("", v.Index(i)) {
|
||||||
|
elements = append(elements, pair.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(elements) == 0 {
|
||||||
|
return []Pair{}
|
||||||
|
}
|
||||||
|
return []Pair{{key, strings.Join(elements, ",")}}
|
||||||
|
}
|
||||||
|
case ArrayQueryFormatRepeat:
|
||||||
|
innerEncoder := e.typeEncoder(t.Elem())
|
||||||
|
return func(key string, value reflect.Value) (pairs []Pair) {
|
||||||
|
for i := 0; i < value.Len(); i++ {
|
||||||
|
pairs = append(pairs, innerEncoder(key, value.Index(i))...)
|
||||||
|
}
|
||||||
|
return pairs
|
||||||
|
}
|
||||||
|
case ArrayQueryFormatIndices:
|
||||||
|
panic("The array indices format is not supported yet")
|
||||||
|
case ArrayQueryFormatBrackets:
|
||||||
|
innerEncoder := e.typeEncoder(t.Elem())
|
||||||
|
return func(key string, value reflect.Value) []Pair {
|
||||||
|
pairs := []Pair{}
|
||||||
|
for i := 0; i < value.Len(); i++ {
|
||||||
|
pairs = append(pairs, innerEncoder(key+"[]", value.Index(i))...)
|
||||||
|
}
|
||||||
|
return pairs
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Pointer:
|
||||||
|
inner := t.Elem()
|
||||||
|
|
||||||
|
innerEncoder := e.newPrimitiveTypeEncoder(inner)
|
||||||
|
return func(key string, v reflect.Value) []Pair {
|
||||||
|
if !v.IsValid() || v.IsNil() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return innerEncoder(key, v.Elem())
|
||||||
|
}
|
||||||
|
case reflect.String:
|
||||||
|
return func(key string, v reflect.Value) []Pair {
|
||||||
|
return []Pair{{key, v.String()}}
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
return func(key string, v reflect.Value) []Pair {
|
||||||
|
if v.Bool() {
|
||||||
|
return []Pair{{key, "true"}}
|
||||||
|
}
|
||||||
|
return []Pair{{key, "false"}}
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return func(key string, v reflect.Value) []Pair {
|
||||||
|
return []Pair{{key, strconv.FormatInt(v.Int(), 10)}}
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return func(key string, v reflect.Value) []Pair {
|
||||||
|
return []Pair{{key, strconv.FormatUint(v.Uint(), 10)}}
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return func(key string, v reflect.Value) []Pair {
|
||||||
|
return []Pair{{key, strconv.FormatFloat(v.Float(), 'f', -1, 64)}}
|
||||||
|
}
|
||||||
|
case reflect.Complex64, reflect.Complex128:
|
||||||
|
bitSize := 64
|
||||||
|
if t.Kind() == reflect.Complex128 {
|
||||||
|
bitSize = 128
|
||||||
|
}
|
||||||
|
return func(key string, v reflect.Value) []Pair {
|
||||||
|
return []Pair{{key, strconv.FormatComplex(v.Complex(), 'f', -1, bitSize)}}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return func(key string, v reflect.Value) []Pair {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
f, _ := t.FieldByName("Value")
|
||||||
|
enc := e.typeEncoder(f.Type)
|
||||||
|
|
||||||
|
return func(key string, value reflect.Value) []Pair {
|
||||||
|
present := value.FieldByName("Present")
|
||||||
|
if !present.Bool() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
null := value.FieldByName("Null")
|
||||||
|
if null.Bool() {
|
||||||
|
// TODO: Error?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw := value.FieldByName("Raw")
|
||||||
|
if !raw.IsNil() {
|
||||||
|
return e.typeEncoder(raw.Type())(key, raw)
|
||||||
|
}
|
||||||
|
return enc(key, value.FieldByName("Value"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encoder) newTimeTypeEncoder(t reflect.Type) encoderFunc {
|
||||||
|
format := e.dateFormat
|
||||||
|
return func(key string, value reflect.Value) []Pair {
|
||||||
|
return []Pair{{
|
||||||
|
key,
|
||||||
|
value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) newInterfaceEncoder() encoderFunc {
|
||||||
|
return func(key string, value reflect.Value) []Pair {
|
||||||
|
value = value.Elem()
|
||||||
|
if !value.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.typeEncoder(value.Type())(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
50
packages/sdk/go/internal/apiquery/query.go
Normal file
50
packages/sdk/go/internal/apiquery/query.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package apiquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MarshalWithSettings(value interface{}, settings QuerySettings) url.Values {
|
||||||
|
e := encoder{time.RFC3339, true, settings}
|
||||||
|
kv := url.Values{}
|
||||||
|
val := reflect.ValueOf(value)
|
||||||
|
if !val.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
typ := val.Type()
|
||||||
|
for _, pair := range e.typeEncoder(typ)("", val) {
|
||||||
|
kv.Add(pair.key, pair.value)
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
func Marshal(value interface{}) url.Values {
|
||||||
|
return MarshalWithSettings(value, QuerySettings{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queryer interface {
|
||||||
|
URLQuery() url.Values
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuerySettings struct {
|
||||||
|
NestedFormat NestedQueryFormat
|
||||||
|
ArrayFormat ArrayQueryFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
type NestedQueryFormat int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NestedQueryFormatBrackets NestedQueryFormat = iota
|
||||||
|
NestedQueryFormatDots
|
||||||
|
)
|
||||||
|
|
||||||
|
type ArrayQueryFormat int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ArrayQueryFormatComma ArrayQueryFormat = iota
|
||||||
|
ArrayQueryFormatRepeat
|
||||||
|
ArrayQueryFormatIndices
|
||||||
|
ArrayQueryFormatBrackets
|
||||||
|
)
|
||||||
335
packages/sdk/go/internal/apiquery/query_test.go
Normal file
335
packages/sdk/go/internal/apiquery/query_test.go
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
package apiquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func P[T any](v T) *T { return &v }
|
||||||
|
|
||||||
|
type Primitives struct {
|
||||||
|
A bool `query:"a"`
|
||||||
|
B int `query:"b"`
|
||||||
|
C uint `query:"c"`
|
||||||
|
D float64 `query:"d"`
|
||||||
|
E float32 `query:"e"`
|
||||||
|
F []int `query:"f"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrimitivePointers struct {
|
||||||
|
A *bool `query:"a"`
|
||||||
|
B *int `query:"b"`
|
||||||
|
C *uint `query:"c"`
|
||||||
|
D *float64 `query:"d"`
|
||||||
|
E *float32 `query:"e"`
|
||||||
|
F *[]int `query:"f"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Slices struct {
|
||||||
|
Slice []Primitives `query:"slices"`
|
||||||
|
Mixed []interface{} `query:"mixed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DateTime struct {
|
||||||
|
Date time.Time `query:"date" format:"date"`
|
||||||
|
DateTime time.Time `query:"date-time" format:"date-time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdditionalProperties struct {
|
||||||
|
A bool `query:"a"`
|
||||||
|
Extras map[string]interface{} `query:"-,inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Recursive struct {
|
||||||
|
Name string `query:"name"`
|
||||||
|
Child *Recursive `query:"child"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnknownStruct struct {
|
||||||
|
Unknown interface{} `query:"unknown"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnionStruct struct {
|
||||||
|
Union Union `query:"union" format:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Union interface {
|
||||||
|
union()
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnionInteger int64
|
||||||
|
|
||||||
|
func (UnionInteger) union() {}
|
||||||
|
|
||||||
|
type UnionString string
|
||||||
|
|
||||||
|
func (UnionString) union() {}
|
||||||
|
|
||||||
|
type UnionStructA struct {
|
||||||
|
Type string `query:"type"`
|
||||||
|
A string `query:"a"`
|
||||||
|
B string `query:"b"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnionStructA) union() {}
|
||||||
|
|
||||||
|
type UnionStructB struct {
|
||||||
|
Type string `query:"type"`
|
||||||
|
A string `query:"a"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnionStructB) union() {}
|
||||||
|
|
||||||
|
type UnionTime time.Time
|
||||||
|
|
||||||
|
func (UnionTime) union() {}
|
||||||
|
|
||||||
|
type DeeplyNested struct {
|
||||||
|
A DeeplyNested1 `query:"a"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeeplyNested1 struct {
|
||||||
|
B DeeplyNested2 `query:"b"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeeplyNested2 struct {
|
||||||
|
C DeeplyNested3 `query:"c"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeeplyNested3 struct {
|
||||||
|
D *string `query:"d"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var tests = map[string]struct {
|
||||||
|
enc string
|
||||||
|
val interface{}
|
||||||
|
settings QuerySettings
|
||||||
|
}{
|
||||||
|
"primitives": {
|
||||||
|
"a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4",
|
||||||
|
Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||||
|
QuerySettings{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"slices_brackets": {
|
||||||
|
`mixed[]=1&mixed[]=2.3&mixed[]=hello&slices[][a]=false&slices[][a]=false&slices[][b]=237628372683&slices[][b]=237628372683&slices[][c]=654&slices[][c]=654&slices[][d]=9999.43&slices[][d]=9999.43&slices[][e]=43.7599983215332&slices[][e]=43.7599983215332&slices[][f][]=1&slices[][f][]=2&slices[][f][]=3&slices[][f][]=4&slices[][f][]=1&slices[][f][]=2&slices[][f][]=3&slices[][f][]=4`,
|
||||||
|
Slices{
|
||||||
|
Slice: []Primitives{
|
||||||
|
{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||||
|
{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||||
|
},
|
||||||
|
Mixed: []interface{}{1, 2.3, "hello"},
|
||||||
|
},
|
||||||
|
QuerySettings{ArrayFormat: ArrayQueryFormatBrackets},
|
||||||
|
},
|
||||||
|
|
||||||
|
"slices_comma": {
|
||||||
|
`mixed=1,2.3,hello`,
|
||||||
|
Slices{
|
||||||
|
Mixed: []interface{}{1, 2.3, "hello"},
|
||||||
|
},
|
||||||
|
QuerySettings{ArrayFormat: ArrayQueryFormatComma},
|
||||||
|
},
|
||||||
|
|
||||||
|
"slices_repeat": {
|
||||||
|
`mixed=1&mixed=2.3&mixed=hello&slices[a]=false&slices[a]=false&slices[b]=237628372683&slices[b]=237628372683&slices[c]=654&slices[c]=654&slices[d]=9999.43&slices[d]=9999.43&slices[e]=43.7599983215332&slices[e]=43.7599983215332&slices[f]=1&slices[f]=2&slices[f]=3&slices[f]=4&slices[f]=1&slices[f]=2&slices[f]=3&slices[f]=4`,
|
||||||
|
Slices{
|
||||||
|
Slice: []Primitives{
|
||||||
|
{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||||
|
{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
|
||||||
|
},
|
||||||
|
Mixed: []interface{}{1, 2.3, "hello"},
|
||||||
|
},
|
||||||
|
QuerySettings{ArrayFormat: ArrayQueryFormatRepeat},
|
||||||
|
},
|
||||||
|
|
||||||
|
"primitive_pointer_struct": {
|
||||||
|
"a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4,5",
|
||||||
|
PrimitivePointers{
|
||||||
|
A: P(false),
|
||||||
|
B: P(237628372683),
|
||||||
|
C: P(uint(654)),
|
||||||
|
D: P(9999.43),
|
||||||
|
E: P(float32(43.76)),
|
||||||
|
F: &[]int{1, 2, 3, 4, 5},
|
||||||
|
},
|
||||||
|
QuerySettings{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"datetime_struct": {
|
||||||
|
`date=2006-01-02&date-time=2006-01-02T15:04:05Z`,
|
||||||
|
DateTime{
|
||||||
|
Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC),
|
||||||
|
DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),
|
||||||
|
},
|
||||||
|
QuerySettings{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"additional_properties": {
|
||||||
|
`a=true&bar=value&foo=true`,
|
||||||
|
AdditionalProperties{
|
||||||
|
A: true,
|
||||||
|
Extras: map[string]interface{}{
|
||||||
|
"bar": "value",
|
||||||
|
"foo": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuerySettings{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"recursive_struct_brackets": {
|
||||||
|
`child[name]=Alex&name=Robert`,
|
||||||
|
Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
|
||||||
|
QuerySettings{NestedFormat: NestedQueryFormatBrackets},
|
||||||
|
},
|
||||||
|
|
||||||
|
"recursive_struct_dots": {
|
||||||
|
`child.name=Alex&name=Robert`,
|
||||||
|
Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
|
||||||
|
QuerySettings{NestedFormat: NestedQueryFormatDots},
|
||||||
|
},
|
||||||
|
|
||||||
|
"unknown_struct_number": {
|
||||||
|
`unknown=12`,
|
||||||
|
UnknownStruct{
|
||||||
|
Unknown: 12.,
|
||||||
|
},
|
||||||
|
QuerySettings{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"unknown_struct_map_brackets": {
|
||||||
|
`unknown[foo]=bar`,
|
||||||
|
UnknownStruct{
|
||||||
|
Unknown: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuerySettings{NestedFormat: NestedQueryFormatBrackets},
|
||||||
|
},
|
||||||
|
|
||||||
|
"unknown_struct_map_dots": {
|
||||||
|
`unknown.foo=bar`,
|
||||||
|
UnknownStruct{
|
||||||
|
Unknown: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuerySettings{NestedFormat: NestedQueryFormatDots},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_string": {
|
||||||
|
`union=hello`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionString("hello"),
|
||||||
|
},
|
||||||
|
QuerySettings{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_integer": {
|
||||||
|
`union=12`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionInteger(12),
|
||||||
|
},
|
||||||
|
QuerySettings{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_struct_discriminated_a": {
|
||||||
|
`union[a]=foo&union[b]=bar&union[type]=typeA`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionStructA{
|
||||||
|
Type: "typeA",
|
||||||
|
A: "foo",
|
||||||
|
B: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuerySettings{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_struct_discriminated_b": {
|
||||||
|
`union[a]=foo&union[type]=typeB`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionStructB{
|
||||||
|
Type: "typeB",
|
||||||
|
A: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuerySettings{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"union_struct_time": {
|
||||||
|
`union=2010-05-23`,
|
||||||
|
UnionStruct{
|
||||||
|
Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
|
||||||
|
},
|
||||||
|
QuerySettings{},
|
||||||
|
},
|
||||||
|
|
||||||
|
"deeply_nested_brackets": {
|
||||||
|
`a[b][c][d]=hello`,
|
||||||
|
DeeplyNested{
|
||||||
|
A: DeeplyNested1{
|
||||||
|
B: DeeplyNested2{
|
||||||
|
C: DeeplyNested3{
|
||||||
|
D: P("hello"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuerySettings{NestedFormat: NestedQueryFormatBrackets},
|
||||||
|
},
|
||||||
|
|
||||||
|
"deeply_nested_dots": {
|
||||||
|
`a.b.c.d=hello`,
|
||||||
|
DeeplyNested{
|
||||||
|
A: DeeplyNested1{
|
||||||
|
B: DeeplyNested2{
|
||||||
|
C: DeeplyNested3{
|
||||||
|
D: P("hello"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuerySettings{NestedFormat: NestedQueryFormatDots},
|
||||||
|
},
|
||||||
|
|
||||||
|
"deeply_nested_brackets_empty": {
|
||||||
|
``,
|
||||||
|
DeeplyNested{
|
||||||
|
A: DeeplyNested1{
|
||||||
|
B: DeeplyNested2{
|
||||||
|
C: DeeplyNested3{
|
||||||
|
D: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuerySettings{NestedFormat: NestedQueryFormatBrackets},
|
||||||
|
},
|
||||||
|
|
||||||
|
"deeply_nested_dots_empty": {
|
||||||
|
``,
|
||||||
|
DeeplyNested{
|
||||||
|
A: DeeplyNested1{
|
||||||
|
B: DeeplyNested2{
|
||||||
|
C: DeeplyNested3{
|
||||||
|
D: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QuerySettings{NestedFormat: NestedQueryFormatDots},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
values := MarshalWithSettings(test.val, test.settings)
|
||||||
|
str, _ := url.QueryUnescape(values.Encode())
|
||||||
|
if str != test.enc {
|
||||||
|
t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.enc, str)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
41
packages/sdk/go/internal/apiquery/tag.go
Normal file
41
packages/sdk/go/internal/apiquery/tag.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package apiquery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const queryStructTag = "query"
|
||||||
|
const formatStructTag = "format"
|
||||||
|
|
||||||
|
type parsedStructTag struct {
|
||||||
|
name string
|
||||||
|
omitempty bool
|
||||||
|
inline bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
|
||||||
|
raw, ok := field.Tag.Lookup(queryStructTag)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return tag, false
|
||||||
|
}
|
||||||
|
tag.name = parts[0]
|
||||||
|
for _, part := range parts[1:] {
|
||||||
|
switch part {
|
||||||
|
case "omitempty":
|
||||||
|
tag.omitempty = true
|
||||||
|
case "inline":
|
||||||
|
tag.inline = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFormatStructTag(field reflect.StructField) (format string, ok bool) {
|
||||||
|
format, ok = field.Tag.Lookup(formatStructTag)
|
||||||
|
return
|
||||||
|
}
|
||||||
29
packages/sdk/go/internal/param/field.go
Normal file
29
packages/sdk/go/internal/param/field.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package param
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FieldLike interface{ field() }
|
||||||
|
|
||||||
|
// Field is a wrapper used for all values sent to the API,
|
||||||
|
// to distinguish zero values from null or omitted fields.
|
||||||
|
//
|
||||||
|
// It also allows sending arbitrary deserializable values.
|
||||||
|
//
|
||||||
|
// To instantiate a Field, use the helpers exported from
|
||||||
|
// the package root: `F()`, `Null()`, `Raw()`, etc.
|
||||||
|
type Field[T any] struct {
|
||||||
|
FieldLike
|
||||||
|
Value T
|
||||||
|
Null bool
|
||||||
|
Present bool
|
||||||
|
Raw any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Field[T]) String() string {
|
||||||
|
if s, ok := any(f.Value).(fmt.Stringer); ok {
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", f.Value)
|
||||||
|
}
|
||||||
629
packages/sdk/go/internal/requestconfig/requestconfig.go
Normal file
629
packages/sdk/go/internal/requestconfig/requestconfig.go
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||||
|
|
||||||
|
package requestconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go/internal"
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/apierror"
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/apiform"
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/apiquery"
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/param"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getDefaultHeaders() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"User-Agent": fmt.Sprintf("Opencode/Go %s", internal.PackageVersion),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNormalizedOS() string {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "ios":
|
||||||
|
return "iOS"
|
||||||
|
case "android":
|
||||||
|
return "Android"
|
||||||
|
case "darwin":
|
||||||
|
return "MacOS"
|
||||||
|
case "window":
|
||||||
|
return "Windows"
|
||||||
|
case "freebsd":
|
||||||
|
return "FreeBSD"
|
||||||
|
case "openbsd":
|
||||||
|
return "OpenBSD"
|
||||||
|
case "linux":
|
||||||
|
return "Linux"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Other:%s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNormalizedArchitecture() string {
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "386":
|
||||||
|
return "x32"
|
||||||
|
case "amd64":
|
||||||
|
return "x64"
|
||||||
|
case "arm":
|
||||||
|
return "arm"
|
||||||
|
case "arm64":
|
||||||
|
return "arm64"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("other:%s", runtime.GOARCH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPlatformProperties() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"X-Stainless-Lang": "go",
|
||||||
|
"X-Stainless-Package-Version": internal.PackageVersion,
|
||||||
|
"X-Stainless-OS": getNormalizedOS(),
|
||||||
|
"X-Stainless-Arch": getNormalizedArchitecture(),
|
||||||
|
"X-Stainless-Runtime": "go",
|
||||||
|
"X-Stainless-Runtime-Version": runtime.Version(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestOption interface {
|
||||||
|
Apply(*RequestConfig) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestOptionFunc func(*RequestConfig) error
|
||||||
|
type PreRequestOptionFunc func(*RequestConfig) error
|
||||||
|
|
||||||
|
func (s RequestOptionFunc) Apply(r *RequestConfig) error { return s(r) }
|
||||||
|
func (s PreRequestOptionFunc) Apply(r *RequestConfig) error { return s(r) }
|
||||||
|
|
||||||
|
func NewRequestConfig(ctx context.Context, method string, u string, body interface{}, dst interface{}, opts ...RequestOption) (*RequestConfig, error) {
|
||||||
|
var reader io.Reader
|
||||||
|
|
||||||
|
contentType := "application/json"
|
||||||
|
hasSerializationFunc := false
|
||||||
|
|
||||||
|
if body, ok := body.(json.Marshaler); ok {
|
||||||
|
content, err := body.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reader = bytes.NewBuffer(content)
|
||||||
|
hasSerializationFunc = true
|
||||||
|
}
|
||||||
|
if body, ok := body.(apiform.Marshaler); ok {
|
||||||
|
var (
|
||||||
|
content []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
content, contentType, err = body.MarshalMultipart()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reader = bytes.NewBuffer(content)
|
||||||
|
hasSerializationFunc = true
|
||||||
|
}
|
||||||
|
if body, ok := body.(apiquery.Queryer); ok {
|
||||||
|
hasSerializationFunc = true
|
||||||
|
params := body.URLQuery().Encode()
|
||||||
|
if params != "" {
|
||||||
|
u = u + "?" + params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if body, ok := body.([]byte); ok {
|
||||||
|
reader = bytes.NewBuffer(body)
|
||||||
|
hasSerializationFunc = true
|
||||||
|
}
|
||||||
|
if body, ok := body.(io.Reader); ok {
|
||||||
|
reader = body
|
||||||
|
hasSerializationFunc = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to json serialization if none of the serialization functions that we expect
|
||||||
|
// to see is present.
|
||||||
|
if body != nil && !hasSerializationFunc {
|
||||||
|
content, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reader = bytes.NewBuffer(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if reader != nil {
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("X-Stainless-Retry-Count", "0")
|
||||||
|
req.Header.Set("X-Stainless-Timeout", "0")
|
||||||
|
for k, v := range getDefaultHeaders() {
|
||||||
|
req.Header.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range getPlatformProperties() {
|
||||||
|
req.Header.Add(k, v)
|
||||||
|
}
|
||||||
|
cfg := RequestConfig{
|
||||||
|
MaxRetries: 2,
|
||||||
|
Context: ctx,
|
||||||
|
Request: req,
|
||||||
|
HTTPClient: http.DefaultClient,
|
||||||
|
Body: reader,
|
||||||
|
}
|
||||||
|
cfg.ResponseBodyInto = dst
|
||||||
|
err = cfg.Apply(opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// This must run after `cfg.Apply(...)` above in case the request timeout gets modified. We also only
|
||||||
|
// apply our own logic for it if it's still "0" from above. If it's not, then it was deleted or modified
|
||||||
|
// by the user and we should respect that.
|
||||||
|
if req.Header.Get("X-Stainless-Timeout") == "0" {
|
||||||
|
if cfg.RequestTimeout == time.Duration(0) {
|
||||||
|
req.Header.Del("X-Stainless-Timeout")
|
||||||
|
} else {
|
||||||
|
req.Header.Set("X-Stainless-Timeout", strconv.Itoa(int(cfg.RequestTimeout.Seconds())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UseDefaultParam[T any](dst *param.Field[T], src *T) {
|
||||||
|
if !dst.Present && src != nil {
|
||||||
|
dst.Value = *src
|
||||||
|
dst.Present = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This interface is primarily used to describe an [*http.Client], but also
|
||||||
|
// supports custom HTTP implementations.
|
||||||
|
type HTTPDoer interface {
|
||||||
|
Do(req *http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestConfig represents all the state related to one request.
|
||||||
|
//
|
||||||
|
// Editing the variables inside RequestConfig directly is unstable api. Prefer
|
||||||
|
// composing the RequestOption instead if possible.
|
||||||
|
type RequestConfig struct {
|
||||||
|
MaxRetries int
|
||||||
|
RequestTimeout time.Duration
|
||||||
|
Context context.Context
|
||||||
|
Request *http.Request
|
||||||
|
BaseURL *url.URL
|
||||||
|
// DefaultBaseURL will be used if BaseURL is not explicitly overridden using
|
||||||
|
// WithBaseURL.
|
||||||
|
DefaultBaseURL *url.URL
|
||||||
|
CustomHTTPDoer HTTPDoer
|
||||||
|
HTTPClient *http.Client
|
||||||
|
Middlewares []middleware
|
||||||
|
// If ResponseBodyInto not nil, then we will attempt to deserialize into
|
||||||
|
// ResponseBodyInto. If Destination is a []byte, then it will return the body as
|
||||||
|
// is.
|
||||||
|
ResponseBodyInto interface{}
|
||||||
|
// ResponseInto copies the \*http.Response of the corresponding request into the
|
||||||
|
// given address
|
||||||
|
ResponseInto **http.Response
|
||||||
|
Body io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// middleware is exactly the same type as the Middleware type found in the [option] package,
|
||||||
|
// but it is redeclared here for circular dependency issues.
|
||||||
|
type middleware = func(*http.Request, middlewareNext) (*http.Response, error)
|
||||||
|
|
||||||
|
// middlewareNext is exactly the same type as the MiddlewareNext type found in the [option] package,
|
||||||
|
// but it is redeclared here for circular dependency issues.
|
||||||
|
type middlewareNext = func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func applyMiddleware(middleware middleware, next middlewareNext) middlewareNext {
|
||||||
|
return func(req *http.Request) (res *http.Response, err error) {
|
||||||
|
return middleware(req, next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRetry(req *http.Request, res *http.Response) bool {
|
||||||
|
// If there is no way to recover the Body, then we shouldn't retry.
|
||||||
|
if req.Body != nil && req.GetBody == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no response, that indicates that there is a connection error
|
||||||
|
// so we retry the request.
|
||||||
|
if res == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the header explicitly wants a retry behavior, respect that over the
|
||||||
|
// http status code.
|
||||||
|
if res.Header.Get("x-should-retry") == "true" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if res.Header.Get("x-should-retry") == "false" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.StatusCode == http.StatusRequestTimeout ||
|
||||||
|
res.StatusCode == http.StatusConflict ||
|
||||||
|
res.StatusCode == http.StatusTooManyRequests ||
|
||||||
|
res.StatusCode >= http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRetryAfterHeader(resp *http.Response) (time.Duration, bool) {
|
||||||
|
if resp == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type retryData struct {
|
||||||
|
header string
|
||||||
|
units time.Duration
|
||||||
|
|
||||||
|
// custom is used when the regular algorithm failed and is optional.
|
||||||
|
// the returned duration is used verbatim (units is not applied).
|
||||||
|
custom func(string) (time.Duration, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
nop := func(string) (time.Duration, bool) { return 0, false }
|
||||||
|
|
||||||
|
// the headers are listed in order of preference
|
||||||
|
retries := []retryData{
|
||||||
|
{
|
||||||
|
header: "Retry-After-Ms",
|
||||||
|
units: time.Millisecond,
|
||||||
|
custom: nop,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Retry-After",
|
||||||
|
units: time.Second,
|
||||||
|
|
||||||
|
// retry-after values are expressed in either number of
|
||||||
|
// seconds or an HTTP-date indicating when to try again
|
||||||
|
custom: func(ra string) (time.Duration, bool) {
|
||||||
|
t, err := time.Parse(time.RFC1123, ra)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return time.Until(t), true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, retry := range retries {
|
||||||
|
v := resp.Header.Get(retry.header)
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if retryAfter, err := strconv.ParseFloat(v, 64); err == nil {
|
||||||
|
return time.Duration(retryAfter * float64(retry.units)), true
|
||||||
|
}
|
||||||
|
if d, ok := retry.custom(v); ok {
|
||||||
|
return d, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBeforeContextDeadline reports whether the non-zero Time t is
|
||||||
|
// before ctx's deadline. If ctx does not have a deadline, it
|
||||||
|
// always reports true (the deadline is considered infinite).
|
||||||
|
func isBeforeContextDeadline(t time.Time, ctx context.Context) bool {
|
||||||
|
d, ok := ctx.Deadline()
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return t.Before(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bodyWithTimeout is an io.ReadCloser which can observe a context's cancel func
|
||||||
|
// to handle timeouts etc. It wraps an existing io.ReadCloser.
|
||||||
|
type bodyWithTimeout struct {
|
||||||
|
stop func() // stops the time.Timer waiting to cancel the request
|
||||||
|
rc io.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bodyWithTimeout) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = b.rc.Read(p)
|
||||||
|
if err == nil {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bodyWithTimeout) Close() error {
|
||||||
|
err := b.rc.Close()
|
||||||
|
b.stop()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func retryDelay(res *http.Response, retryCount int) time.Duration {
|
||||||
|
// If the API asks us to wait a certain amount of time (and it's a reasonable amount),
|
||||||
|
// just do what it says.
|
||||||
|
|
||||||
|
if retryAfterDelay, ok := parseRetryAfterHeader(res); ok && 0 <= retryAfterDelay && retryAfterDelay < time.Minute {
|
||||||
|
return retryAfterDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
maxDelay := 8 * time.Second
|
||||||
|
delay := time.Duration(0.5 * float64(time.Second) * math.Pow(2, float64(retryCount)))
|
||||||
|
if delay > maxDelay {
|
||||||
|
delay = maxDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
jitter := rand.Int63n(int64(delay / 4))
|
||||||
|
delay -= time.Duration(jitter)
|
||||||
|
return delay
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *RequestConfig) Execute() (err error) {
|
||||||
|
if cfg.BaseURL == nil {
|
||||||
|
if cfg.DefaultBaseURL != nil {
|
||||||
|
cfg.BaseURL = cfg.DefaultBaseURL
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("requestconfig: base url is not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Request.URL, err = cfg.BaseURL.Parse(strings.TrimLeft(cfg.Request.URL.String(), "/"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Body != nil && cfg.Request.Body == nil {
|
||||||
|
switch body := cfg.Body.(type) {
|
||||||
|
case *bytes.Buffer:
|
||||||
|
b := body.Bytes()
|
||||||
|
cfg.Request.ContentLength = int64(body.Len())
|
||||||
|
cfg.Request.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }
|
||||||
|
cfg.Request.Body, _ = cfg.Request.GetBody()
|
||||||
|
case *bytes.Reader:
|
||||||
|
cfg.Request.ContentLength = int64(body.Len())
|
||||||
|
cfg.Request.GetBody = func() (io.ReadCloser, error) {
|
||||||
|
_, err := body.Seek(0, 0)
|
||||||
|
return io.NopCloser(body), err
|
||||||
|
}
|
||||||
|
cfg.Request.Body, _ = cfg.Request.GetBody()
|
||||||
|
default:
|
||||||
|
if rc, ok := body.(io.ReadCloser); ok {
|
||||||
|
cfg.Request.Body = rc
|
||||||
|
} else {
|
||||||
|
cfg.Request.Body = io.NopCloser(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := cfg.HTTPClient.Do
|
||||||
|
if cfg.CustomHTTPDoer != nil {
|
||||||
|
handler = cfg.CustomHTTPDoer.Do
|
||||||
|
}
|
||||||
|
for i := len(cfg.Middlewares) - 1; i >= 0; i -= 1 {
|
||||||
|
handler = applyMiddleware(cfg.Middlewares[i], handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't send the current retry count in the headers if the caller modified the header defaults.
|
||||||
|
shouldSendRetryCount := cfg.Request.Header.Get("X-Stainless-Retry-Count") == "0"
|
||||||
|
|
||||||
|
var res *http.Response
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
for retryCount := 0; retryCount <= cfg.MaxRetries; retryCount += 1 {
|
||||||
|
ctx := cfg.Request.Context()
|
||||||
|
if cfg.RequestTimeout != time.Duration(0) && isBeforeContextDeadline(time.Now().Add(cfg.RequestTimeout), ctx) {
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, cfg.RequestTimeout)
|
||||||
|
defer func() {
|
||||||
|
// The cancel function is nil if it was handed off to be handled in a different scope.
|
||||||
|
if cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
req := cfg.Request.Clone(ctx)
|
||||||
|
if shouldSendRetryCount {
|
||||||
|
req.Header.Set("X-Stainless-Retry-Count", strconv.Itoa(retryCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err = handler(req)
|
||||||
|
if ctx != nil && ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
if !shouldRetry(cfg.Request, res) || retryCount >= cfg.MaxRetries {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare next request and wait for the retry delay
|
||||||
|
if cfg.Request.GetBody != nil {
|
||||||
|
cfg.Request.Body, err = cfg.Request.GetBody()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't actually refresh the body, so we don't attempt to retry here
|
||||||
|
if cfg.Request.GetBody == nil && cfg.Request.Body != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(retryDelay(res, retryCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save *http.Response if it is requested to, even if there was an error making the request. This is
|
||||||
|
// useful in cases where you might want to debug by inspecting the response. Note that if err != nil,
|
||||||
|
// the response should be generally be empty, but there are edge cases.
|
||||||
|
if cfg.ResponseInto != nil {
|
||||||
|
*cfg.ResponseInto = res
|
||||||
|
}
|
||||||
|
if responseBodyInto, ok := cfg.ResponseBodyInto.(**http.Response); ok {
|
||||||
|
*responseBodyInto = res
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was a connection error in the final request or any other transport error,
|
||||||
|
// return that early without trying to coerce into an APIError.
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode >= 400 {
|
||||||
|
contents, err := io.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is an APIError, re-populate the response body so that debugging
|
||||||
|
// utilities can conveniently dump the response without issue.
|
||||||
|
res.Body = io.NopCloser(bytes.NewBuffer(contents))
|
||||||
|
|
||||||
|
// Load the contents into the error format if it is provided.
|
||||||
|
aerr := apierror.Error{Request: cfg.Request, Response: res, StatusCode: res.StatusCode}
|
||||||
|
err = aerr.UnmarshalJSON(contents)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return &aerr
|
||||||
|
}
|
||||||
|
|
||||||
|
_, intoCustomResponseBody := cfg.ResponseBodyInto.(**http.Response)
|
||||||
|
if cfg.ResponseBodyInto == nil || intoCustomResponseBody {
|
||||||
|
// We aren't reading the response body in this scope, but whoever is will need the
|
||||||
|
// cancel func from the context to observe request timeouts.
|
||||||
|
// Put the cancel function in the response body so it can be handled elsewhere.
|
||||||
|
if cancel != nil {
|
||||||
|
res.Body = &bodyWithTimeout{rc: res.Body, stop: cancel}
|
||||||
|
cancel = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contents, err := io.ReadAll(res.Body)
|
||||||
|
res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are not json, return plaintext
|
||||||
|
contentType := res.Header.Get("content-type")
|
||||||
|
mediaType, _, _ := mime.ParseMediaType(contentType)
|
||||||
|
isJSON := strings.Contains(mediaType, "application/json") || strings.HasSuffix(mediaType, "+json")
|
||||||
|
if !isJSON {
|
||||||
|
switch dst := cfg.ResponseBodyInto.(type) {
|
||||||
|
case *string:
|
||||||
|
*dst = string(contents)
|
||||||
|
case **string:
|
||||||
|
tmp := string(contents)
|
||||||
|
*dst = &tmp
|
||||||
|
case *[]byte:
|
||||||
|
*dst = contents
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("expected destination type of 'string' or '[]byte' for responses with content-type '%s' that is not 'application/json'", contentType)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch dst := cfg.ResponseBodyInto.(type) {
|
||||||
|
// If the response happens to be a byte array, deserialize the body as-is.
|
||||||
|
case *[]byte:
|
||||||
|
*dst = contents
|
||||||
|
default:
|
||||||
|
err = json.NewDecoder(bytes.NewReader(contents)).Decode(cfg.ResponseBodyInto)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing response json: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteNewRequest(ctx context.Context, method string, u string, body interface{}, dst interface{}, opts ...RequestOption) error {
|
||||||
|
cfg, err := NewRequestConfig(ctx, method, u, body, dst, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cfg.Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *RequestConfig) Clone(ctx context.Context) *RequestConfig {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
req := cfg.Request.Clone(ctx)
|
||||||
|
var err error
|
||||||
|
if req.Body != nil {
|
||||||
|
req.Body, err = req.GetBody()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
new := &RequestConfig{
|
||||||
|
MaxRetries: cfg.MaxRetries,
|
||||||
|
RequestTimeout: cfg.RequestTimeout,
|
||||||
|
Context: ctx,
|
||||||
|
Request: req,
|
||||||
|
BaseURL: cfg.BaseURL,
|
||||||
|
HTTPClient: cfg.HTTPClient,
|
||||||
|
Middlewares: cfg.Middlewares,
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *RequestConfig) Apply(opts ...RequestOption) error {
|
||||||
|
for _, opt := range opts {
|
||||||
|
err := opt.Apply(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreRequestOptions is used to collect all the options which need to be known before
|
||||||
|
// a call to [RequestConfig.ExecuteNewRequest], such as path parameters
|
||||||
|
// or global defaults.
|
||||||
|
// PreRequestOptions will return a [RequestConfig] with the options applied.
|
||||||
|
//
|
||||||
|
// Only request option functions of type [PreRequestOptionFunc] are applied.
|
||||||
|
func PreRequestOptions(opts ...RequestOption) (RequestConfig, error) {
|
||||||
|
cfg := RequestConfig{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt, ok := opt.(PreRequestOptionFunc); ok {
|
||||||
|
err := opt.Apply(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDefaultBaseURL returns a RequestOption that sets the client's default Base URL.
|
||||||
|
// This is always overridden by setting a base URL with WithBaseURL.
|
||||||
|
// WithBaseURL should be used instead of WithDefaultBaseURL except in internal code.
|
||||||
|
func WithDefaultBaseURL(baseURL string) RequestOption {
|
||||||
|
u, err := url.Parse(baseURL)
|
||||||
|
return RequestOptionFunc(func(r *RequestConfig) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.DefaultBaseURL = u
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
27
packages/sdk/go/internal/testutil/testutil.go
Normal file
27
packages/sdk/go/internal/testutil/testutil.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CheckTestServer(t *testing.T, url string) bool {
|
||||||
|
if _, err := http.Get(url); err != nil {
|
||||||
|
const SKIP_MOCK_TESTS = "SKIP_MOCK_TESTS"
|
||||||
|
if str, ok := os.LookupEnv(SKIP_MOCK_TESTS); ok {
|
||||||
|
skip, err := strconv.ParseBool(str)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("strconv.ParseBool(os.LookupEnv(%s)) failed: %s", SKIP_MOCK_TESTS, err)
|
||||||
|
}
|
||||||
|
if skip {
|
||||||
|
t.Skip("The test will not run without a mock Prism server running against your OpenAPI spec")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t.Errorf("The test will not run without a mock Prism server running against your OpenAPI spec. You can set the environment variable %s to true to skip running any tests that require the mock server", SKIP_MOCK_TESTS)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
5
packages/sdk/go/internal/version.go
Normal file
5
packages/sdk/go/internal/version.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
const PackageVersion = "0.1.0-alpha.8" // x-release-please-version
|
||||||
38
packages/sdk/go/option/middleware.go
Normal file
38
packages/sdk/go/option/middleware.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||||
|
|
||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithDebugLog logs the HTTP request and response content.
|
||||||
|
// If the logger parameter is nil, it uses the default logger.
|
||||||
|
//
|
||||||
|
// WithDebugLog is for debugging and development purposes only.
|
||||||
|
// It should not be used in production code. The behavior and interface
|
||||||
|
// of WithDebugLog is not guaranteed to be stable.
|
||||||
|
func WithDebugLog(logger *log.Logger) RequestOption {
|
||||||
|
return WithMiddleware(func(req *http.Request, nxt MiddlewareNext) (*http.Response, error) {
|
||||||
|
if logger == nil {
|
||||||
|
logger = log.Default()
|
||||||
|
}
|
||||||
|
|
||||||
|
if reqBytes, err := httputil.DumpRequest(req, true); err == nil {
|
||||||
|
logger.Printf("Request Content:\n%s\n", reqBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := nxt(req)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if respBytes, err := httputil.DumpResponse(resp, true); err == nil {
|
||||||
|
logger.Printf("Response Content:\n%s\n", respBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
})
|
||||||
|
}
|
||||||
267
packages/sdk/go/option/requestoption.go
Normal file
267
packages/sdk/go/option/requestoption.go
Normal 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/")
|
||||||
|
}
|
||||||
181
packages/sdk/go/packages/ssestream/ssestream.go
Normal file
181
packages/sdk/go/packages/ssestream/ssestream.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||||
|
|
||||||
|
package ssestream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Decoder interface {
|
||||||
|
Event() Event
|
||||||
|
Next() bool
|
||||||
|
Close() error
|
||||||
|
Err() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDecoder(res *http.Response) Decoder {
|
||||||
|
if res == nil || res.Body == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoder Decoder
|
||||||
|
contentType := res.Header.Get("content-type")
|
||||||
|
if t, ok := decoderTypes[contentType]; ok {
|
||||||
|
decoder = t(res.Body)
|
||||||
|
} else {
|
||||||
|
scn := bufio.NewScanner(res.Body)
|
||||||
|
scn.Buffer(nil, bufio.MaxScanTokenSize<<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()
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -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
8
packages/sdk/go/scripts/format
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "==> Running gofmt -s -w"
|
||||||
|
gofmt -s -w .
|
||||||
11
packages/sdk/go/scripts/lint
Executable file
11
packages/sdk/go/scripts/lint
Executable 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=^$ ./...
|
||||||
@@ -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
2117
packages/sdk/go/session.go
Normal file
File diff suppressed because it is too large
Load Diff
323
packages/sdk/go/session_test.go
Normal file
323
packages/sdk/go/session_test.go
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
173
packages/sdk/go/shared/shared.go
Normal file
173
packages/sdk/go/shared/shared.go
Normal 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
56
packages/sdk/go/tui.go
Normal 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)
|
||||||
|
}
|
||||||
60
packages/sdk/go/tui_test.go
Normal file
60
packages/sdk/go/tui_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||||
|
|
||||||
|
package opencode_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/testutil"
|
||||||
|
"github.com/sst/opencode-sdk-go/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
32
packages/sdk/go/usage_test.go
Normal file
32
packages/sdk/go/usage_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
||||||
|
|
||||||
|
package opencode_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sst/opencode-sdk-go"
|
||||||
|
"github.com/sst/opencode-sdk-go/internal/testutil"
|
||||||
|
"github.com/sst/opencode-sdk-go/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUsage(t *testing.T) {
|
||||||
|
baseURL := "http://localhost:4010"
|
||||||
|
if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
|
||||||
|
baseURL = envURL
|
||||||
|
}
|
||||||
|
if !testutil.CheckTestServer(t, baseURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := opencode.NewClient(
|
||||||
|
option.WithBaseURL(baseURL),
|
||||||
|
)
|
||||||
|
sessions, err := client.Session.List(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Logf("%+v\n", sessions)
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
17
packages/sdk/js/package.json
Normal file
17
packages/sdk/js/package.json
Normal 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:"
|
||||||
|
}
|
||||||
|
}
|
||||||
41
packages/sdk/js/script/generate.ts
Executable file
41
packages/sdk/js/script/generate.ts
Executable 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`
|
||||||
24
packages/sdk/js/script/publish.ts
Normal file
24
packages/sdk/js/script/publish.ts
Normal 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`
|
||||||
18
packages/sdk/js/src/gen/client.gen.ts
Normal file
18
packages/sdk/js/src/gen/client.gen.ts
Normal 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'
|
||||||
|
}));
|
||||||
195
packages/sdk/js/src/gen/client/client.ts
Normal file
195
packages/sdk/js/src/gen/client/client.ts
Normal 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' }),
|
||||||
|
};
|
||||||
|
};
|
||||||
22
packages/sdk/js/src/gen/client/index.ts
Normal file
22
packages/sdk/js/src/gen/client/index.ts
Normal 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';
|
||||||
222
packages/sdk/js/src/gen/client/types.ts
Normal file
222
packages/sdk/js/src/gen/client/types.ts
Normal 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;
|
||||||
417
packages/sdk/js/src/gen/client/utils.ts
Normal file
417
packages/sdk/js/src/gen/client/utils.ts
Normal 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,
|
||||||
|
});
|
||||||
40
packages/sdk/js/src/gen/core/auth.ts
Normal file
40
packages/sdk/js/src/gen/core/auth.ts
Normal 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;
|
||||||
|
};
|
||||||
88
packages/sdk/js/src/gen/core/bodySerializer.ts
Normal file
88
packages/sdk/js/src/gen/core/bodySerializer.ts
Normal 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();
|
||||||
|
},
|
||||||
|
};
|
||||||
151
packages/sdk/js/src/gen/core/params.ts
Normal file
151
packages/sdk/js/src/gen/core/params.ts
Normal 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;
|
||||||
|
};
|
||||||
179
packages/sdk/js/src/gen/core/pathSerializer.ts
Normal file
179
packages/sdk/js/src/gen/core/pathSerializer.ts
Normal 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 aren’t 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;
|
||||||
|
};
|
||||||
118
packages/sdk/js/src/gen/core/types.ts
Normal file
118
packages/sdk/js/src/gen/core/types.ts
Normal 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
Reference in New Issue
Block a user