mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-03 15:21:31 +08:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10a0b7f60c | ||
|
|
ac8709ac7a | ||
|
|
2d9ed06367 | ||
|
|
50be2aee39 | ||
|
|
0bdbe6261a | ||
|
|
33cef075d2 | ||
|
|
b09ebf4645 | ||
|
|
3268c61813 | ||
|
|
4a221868da | ||
|
|
31b8e3d5ab | ||
|
|
1a78d833a8 | ||
|
|
18888351e9 | ||
|
|
3b7085ca28 | ||
|
|
160923dcf0 | ||
|
|
c38b091895 | ||
|
|
eecfd6d0ca | ||
|
|
6ef4cfa2fa | ||
|
|
190dee080c | ||
|
|
09074dc639 | ||
|
|
1b3d58e791 | ||
|
|
772c83c1d5 | ||
|
|
54dc937fa1 | ||
|
|
b5219f7585 | ||
|
|
0bd0453866 | ||
|
|
8bf36d174b | ||
|
|
9bedd62da4 | ||
|
|
4c34b69ae6 | ||
|
|
7e9ac35666 | ||
|
|
4a46144419 | ||
|
|
a129e122aa | ||
|
|
c0ee6a6d05 | ||
|
|
68ae0d107c | ||
|
|
df63008a94 | ||
|
|
3bd2b340c8 | ||
|
|
df03e182d2 | ||
|
|
862a50d61d | ||
|
|
a7cfd36b07 | ||
|
|
c165360e17 | ||
|
|
9cb0f21b4e | ||
|
|
9c9cbb3e81 | ||
|
|
c24fbb4292 | ||
|
|
99dfe65862 | ||
|
|
4506e5a824 | ||
|
|
b65172a2b7 |
24
.github/workflows/publish.yml
vendored
24
.github/workflows/publish.yml
vendored
@@ -1,14 +1,17 @@
|
||||
name: publish
|
||||
run-name: "${{ format('v{0}', inputs.version) }}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
tags:
|
||||
- "*"
|
||||
- "!vscode-v*"
|
||||
- "!github-v*"
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish"
|
||||
required: true
|
||||
type: string
|
||||
title:
|
||||
description: "Custom title for this run"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -53,12 +56,7 @@ jobs:
|
||||
- name: Publish
|
||||
run: |
|
||||
bun install
|
||||
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
|
||||
./script/publish.ts
|
||||
else
|
||||
./script/publish.ts --snapshot
|
||||
fi
|
||||
working-directory: ./packages/opencode
|
||||
OPENCODE_VERSION=${{ inputs.version }} ./script/publish.ts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
|
||||
2
.github/workflows/stats.yml
vendored
2
.github/workflows/stats.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
bun-version: latest
|
||||
|
||||
- name: Run stats script
|
||||
run: bun scripts/stats.ts
|
||||
run: bun script/stats.ts
|
||||
|
||||
- name: Commit stats
|
||||
run: |
|
||||
|
||||
2
STATS.md
2
STATS.md
@@ -30,3 +30,5 @@
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
|
||||
12
package.json
12
package.json
@@ -8,17 +8,21 @@
|
||||
"dev": "bun run packages/opencode/src/index.ts",
|
||||
"typecheck": "bun run --filter='*' typecheck",
|
||||
"stainless": "./scripts/stainless",
|
||||
"postinstall": "./scripts/hooks"
|
||||
"postinstall": "./script/hooks"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*"
|
||||
"packages/*",
|
||||
"packages/sdk/js"
|
||||
],
|
||||
"catalog": {
|
||||
"typescript": "5.8.2",
|
||||
"@types/node": "22.13.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"ai": "5.0.0-beta.33",
|
||||
"hono": "4.7.10",
|
||||
"typescript": "5.8.2",
|
||||
"zod": "3.25.49",
|
||||
"ai": "5.0.0-beta.21"
|
||||
"remeda": "2.26.0"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"hono": "catalog:",
|
||||
"jose": "6.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Hono } from "hono"
|
||||
import { DurableObject } from "cloudflare:workers"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { jwtVerify, createRemoteJWKSet } from "jose"
|
||||
@@ -111,165 +112,156 @@ export class SyncServer extends DurableObject<Env> {
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const url = new URL(request.url)
|
||||
const splits = url.pathname.split("/")
|
||||
const method = splits[1]
|
||||
|
||||
if (request.method === "GET" && method === "") {
|
||||
return new Response("Hello, world!", {
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
})
|
||||
export default new Hono<{ Bindings: Env }>()
|
||||
.get("/", (c) => c.text("Hello, world!"))
|
||||
.post("/share_create", async (c) => {
|
||||
const body = await c.req.json<{ sessionID: string }>()
|
||||
const sessionID = body.sessionID
|
||||
const short = SyncServer.shortName(sessionID)
|
||||
const id = c.env.SYNC_SERVER.idFromName(short)
|
||||
const stub = c.env.SYNC_SERVER.get(id)
|
||||
const secret = await stub.share(sessionID)
|
||||
return c.json({
|
||||
secret,
|
||||
url: `https://${c.env.WEB_DOMAIN}/s/${short}`,
|
||||
})
|
||||
})
|
||||
.post("/share_delete", async (c) => {
|
||||
const body = await c.req.json<{ sessionID: string; secret: string }>()
|
||||
const sessionID = body.sessionID
|
||||
const secret = body.secret
|
||||
const id = c.env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
|
||||
const stub = c.env.SYNC_SERVER.get(id)
|
||||
await stub.assertSecret(secret)
|
||||
await stub.clear()
|
||||
return c.json({})
|
||||
})
|
||||
.post("/share_delete_admin", async (c) => {
|
||||
const id = c.env.SYNC_SERVER.idFromName("oVF8Rsiv")
|
||||
const stub = c.env.SYNC_SERVER.get(id)
|
||||
await stub.clear()
|
||||
return c.json({})
|
||||
})
|
||||
.post("/share_sync", async (c) => {
|
||||
const body = await c.req.json<{
|
||||
sessionID: string
|
||||
secret: string
|
||||
key: string
|
||||
content: any
|
||||
}>()
|
||||
const name = SyncServer.shortName(body.sessionID)
|
||||
const id = c.env.SYNC_SERVER.idFromName(name)
|
||||
const stub = c.env.SYNC_SERVER.get(id)
|
||||
await stub.assertSecret(body.secret)
|
||||
await stub.publish(body.key, body.content)
|
||||
return c.json({})
|
||||
})
|
||||
.get("/share_poll", async (c) => {
|
||||
const upgradeHeader = c.req.header("Upgrade")
|
||||
if (!upgradeHeader || upgradeHeader !== "websocket") {
|
||||
return c.text("Error: Upgrade header is required", { status: 426 })
|
||||
}
|
||||
const id = c.req.query("id")
|
||||
console.log("share_poll", id)
|
||||
if (!id) return c.text("Error: Share ID is required", { status: 400 })
|
||||
const stub = c.env.SYNC_SERVER.get(c.env.SYNC_SERVER.idFromName(id))
|
||||
return stub.fetch(c.req.raw)
|
||||
})
|
||||
.get("/share_data", async (c) => {
|
||||
const id = c.req.query("id")
|
||||
console.log("share_data", id)
|
||||
if (!id) return c.text("Error: Share ID is required", { status: 400 })
|
||||
const stub = c.env.SYNC_SERVER.get(c.env.SYNC_SERVER.idFromName(id))
|
||||
const data = await stub.getData()
|
||||
|
||||
if (request.method === "POST" && method === "share_create") {
|
||||
const body = await request.json<any>()
|
||||
const sessionID = body.sessionID
|
||||
const short = SyncServer.shortName(sessionID)
|
||||
const id = env.SYNC_SERVER.idFromName(short)
|
||||
const stub = env.SYNC_SERVER.get(id)
|
||||
const secret = await stub.share(sessionID)
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
secret,
|
||||
url: `https://${env.WEB_DOMAIN}/s/${short}`,
|
||||
}),
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (request.method === "POST" && method === "share_delete") {
|
||||
const body = await request.json<any>()
|
||||
const sessionID = body.sessionID
|
||||
const secret = body.secret
|
||||
const id = env.SYNC_SERVER.idFromName(SyncServer.shortName(sessionID))
|
||||
const stub = env.SYNC_SERVER.get(id)
|
||||
await stub.assertSecret(secret)
|
||||
await stub.clear()
|
||||
return new Response(JSON.stringify({}), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
}
|
||||
|
||||
if (request.method === "POST" && method === "share_delete_admin") {
|
||||
const id = env.SYNC_SERVER.idFromName("oVF8Rsiv")
|
||||
const stub = env.SYNC_SERVER.get(id)
|
||||
await stub.clear()
|
||||
return new Response(JSON.stringify({}), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
}
|
||||
|
||||
if (request.method === "POST" && method === "share_sync") {
|
||||
const body = await request.json<{
|
||||
sessionID: string
|
||||
secret: string
|
||||
key: string
|
||||
content: any
|
||||
}>()
|
||||
const name = SyncServer.shortName(body.sessionID)
|
||||
const id = env.SYNC_SERVER.idFromName(name)
|
||||
const stub = env.SYNC_SERVER.get(id)
|
||||
await stub.assertSecret(body.secret)
|
||||
await stub.publish(body.key, body.content)
|
||||
return new Response(JSON.stringify({}), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
}
|
||||
|
||||
if (request.method === "GET" && method === "share_poll") {
|
||||
const upgradeHeader = request.headers.get("Upgrade")
|
||||
if (!upgradeHeader || upgradeHeader !== "websocket") {
|
||||
return new Response("Error: Upgrade header is required", {
|
||||
status: 426,
|
||||
})
|
||||
let info
|
||||
const messages: Record<string, any> = {}
|
||||
data.forEach((d) => {
|
||||
const [root, type, ...splits] = d.key.split("/")
|
||||
if (root !== "session") return
|
||||
if (type === "info") {
|
||||
info = d.content
|
||||
return
|
||||
}
|
||||
const id = url.searchParams.get("id")
|
||||
console.log("share_poll", id)
|
||||
if (!id) return new Response("Error: Share ID is required", { status: 400 })
|
||||
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
|
||||
return stub.fetch(request)
|
||||
}
|
||||
if (type === "message") {
|
||||
messages[d.content.id] = {
|
||||
parts: [],
|
||||
...d.content,
|
||||
}
|
||||
}
|
||||
if (type === "part") {
|
||||
messages[d.content.messageID].parts.push(d.content)
|
||||
}
|
||||
})
|
||||
|
||||
if (request.method === "GET" && method === "share_data") {
|
||||
const id = url.searchParams.get("id")
|
||||
console.log("share_data", id)
|
||||
if (!id) return new Response("Error: Share ID is required", { status: 400 })
|
||||
const stub = env.SYNC_SERVER.get(env.SYNC_SERVER.idFromName(id))
|
||||
const data = await stub.getData()
|
||||
return c.json({ info, messages })
|
||||
})
|
||||
/**
|
||||
* Used by the GitHub action to get GitHub installation access token given the OIDC token
|
||||
*/
|
||||
.post("/exchange_github_app_token", async (c) => {
|
||||
const EXPECTED_AUDIENCE = "opencode-github-action"
|
||||
const GITHUB_ISSUER = "https://token.actions.githubusercontent.com"
|
||||
const JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`
|
||||
|
||||
let info
|
||||
const messages: Record<string, any> = {}
|
||||
data.forEach((d) => {
|
||||
const [root, type, ...splits] = d.key.split("/")
|
||||
if (root !== "session") return
|
||||
if (type === "info") {
|
||||
info = d.content
|
||||
return
|
||||
}
|
||||
if (type === "message") {
|
||||
messages[d.content.id] = {
|
||||
parts: [],
|
||||
...d.content,
|
||||
}
|
||||
}
|
||||
if (type === "part") {
|
||||
messages[d.content.messageID].parts.push(d.content)
|
||||
}
|
||||
// get Authorization header
|
||||
const token = c.req.header("Authorization")?.replace(/^Bearer /, "")
|
||||
if (!token) return c.json({ error: "Authorization header is required" }, { status: 401 })
|
||||
|
||||
// verify token
|
||||
const JWKS = createRemoteJWKSet(new URL(JWKS_URL))
|
||||
let owner, repo
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, JWKS, {
|
||||
issuer: GITHUB_ISSUER,
|
||||
audience: EXPECTED_AUDIENCE,
|
||||
})
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
info,
|
||||
messages,
|
||||
}),
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
)
|
||||
const sub = payload.sub // e.g. 'repo:my-org/my-repo:ref:refs/heads/main'
|
||||
const parts = sub.split(":")[1].split("/")
|
||||
owner = parts[0]
|
||||
repo = parts[1]
|
||||
} catch (err) {
|
||||
console.error("Token verification failed:", err)
|
||||
return c.json({ error: "Invalid or expired token" }, { status: 403 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the GitHub action to get GitHub installation access token given the OIDC token
|
||||
*/
|
||||
if (request.method === "POST" && method === "exchange_github_app_token") {
|
||||
const EXPECTED_AUDIENCE = "opencode-github-action"
|
||||
const GITHUB_ISSUER = "https://token.actions.githubusercontent.com"
|
||||
const JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`
|
||||
// Create app JWT token
|
||||
const auth = createAppAuth({
|
||||
appId: Resource.GITHUB_APP_ID.value,
|
||||
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
|
||||
})
|
||||
const appAuth = await auth({ type: "app" })
|
||||
|
||||
// Lookup installation
|
||||
const octokit = new Octokit({ auth: appAuth.token })
|
||||
const { data: installation } = await octokit.apps.getRepoInstallation({ owner, repo })
|
||||
|
||||
// Get installation token
|
||||
const installationAuth = await auth({ type: "installation", installationId: installation.id })
|
||||
|
||||
return c.json({ token: installationAuth.token })
|
||||
})
|
||||
/**
|
||||
* Used by the GitHub action to get GitHub installation access token given user PAT token (used when testing `opencode github run` locally)
|
||||
*/
|
||||
.post("/exchange_github_app_token_with_pat", async (c) => {
|
||||
const body = await c.req.json<{ owner: string; repo: string }>()
|
||||
const owner = body.owner
|
||||
const repo = body.repo
|
||||
|
||||
try {
|
||||
// get Authorization header
|
||||
const authHeader = request.headers.get("Authorization")
|
||||
const authHeader = c.req.header("Authorization")
|
||||
const token = authHeader?.replace(/^Bearer /, "")
|
||||
if (!token)
|
||||
return new Response(JSON.stringify({ error: "Authorization header is required" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
if (!token) throw new Error("Authorization header is required")
|
||||
|
||||
// verify token
|
||||
const JWKS = createRemoteJWKSet(new URL(JWKS_URL))
|
||||
let owner, repo
|
||||
try {
|
||||
const { payload } = await jwtVerify(token, JWKS, {
|
||||
issuer: GITHUB_ISSUER,
|
||||
audience: EXPECTED_AUDIENCE,
|
||||
})
|
||||
const sub = payload.sub // e.g. 'repo:my-org/my-repo:ref:refs/heads/main'
|
||||
const parts = sub.split(":")[1].split("/")
|
||||
owner = parts[0]
|
||||
repo = parts[1]
|
||||
} catch (err) {
|
||||
console.error("Token verification failed:", err)
|
||||
return new Response(JSON.stringify({ error: "Invalid or expired token" }), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
}
|
||||
// Verify permissions
|
||||
const userClient = new Octokit({ auth: token })
|
||||
const { data: repoData } = await userClient.repos.get({ owner, repo })
|
||||
if (!repoData.permissions.admin && !repoData.permissions.push && !repoData.permissions.maintain)
|
||||
throw new Error("User does not have write permissions")
|
||||
|
||||
// Create app JWT token
|
||||
// Get installation token
|
||||
const auth = createAppAuth({
|
||||
appId: Resource.GITHUB_APP_ID.value,
|
||||
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
|
||||
@@ -277,99 +269,49 @@ export default {
|
||||
const appAuth = await auth({ type: "app" })
|
||||
|
||||
// Lookup installation
|
||||
const octokit = new Octokit({ auth: appAuth.token })
|
||||
const { data: installation } = await octokit.apps.getRepoInstallation({ owner, repo })
|
||||
const appClient = new Octokit({ auth: appAuth.token })
|
||||
const { data: installation } = await appClient.apps.getRepoInstallation({ owner, repo })
|
||||
|
||||
// Get installation token
|
||||
const installationAuth = await auth({ type: "installation", installationId: installation.id })
|
||||
|
||||
return new Response(JSON.stringify({ token: installationAuth.token }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
return c.json({ token: installationAuth.token })
|
||||
} catch (e: any) {
|
||||
let error = e
|
||||
if (e instanceof Error) {
|
||||
error = e.message
|
||||
}
|
||||
|
||||
return c.json({ error }, { status: 401 })
|
||||
}
|
||||
})
|
||||
/**
|
||||
* Used by the opencode CLI to check if the GitHub app is installed
|
||||
*/
|
||||
.get("/get_github_app_installation", async (c) => {
|
||||
const owner = c.req.query("owner")
|
||||
const repo = c.req.query("repo")
|
||||
|
||||
/**
|
||||
* Used by the GitHub action to get GitHub installation access token given user PAT token (used when testing `opencode github run` locally)
|
||||
*/
|
||||
if (request.method === "POST" && method === "exchange_github_app_token_with_pat") {
|
||||
const body = await request.json<any>()
|
||||
const owner = body.owner
|
||||
const repo = body.repo
|
||||
const auth = createAppAuth({
|
||||
appId: Resource.GITHUB_APP_ID.value,
|
||||
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
|
||||
})
|
||||
const appAuth = await auth({ type: "app" })
|
||||
|
||||
try {
|
||||
// get Authorization header
|
||||
const authHeader = request.headers.get("Authorization")
|
||||
const token = authHeader?.replace(/^Bearer /, "")
|
||||
if (!token) throw new Error("Authorization header is required")
|
||||
|
||||
// Verify permissions
|
||||
const userClient = new Octokit({ auth: token })
|
||||
const { data: repoData } = await userClient.repos.get({ owner, repo })
|
||||
if (!repoData.permissions.admin && !repoData.permissions.push && !repoData.permissions.maintain)
|
||||
throw new Error("User does not have write permissions")
|
||||
|
||||
// Get installation token
|
||||
const auth = createAppAuth({
|
||||
appId: Resource.GITHUB_APP_ID.value,
|
||||
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
|
||||
})
|
||||
const appAuth = await auth({ type: "app" })
|
||||
|
||||
// Lookup installation
|
||||
const appClient = new Octokit({ auth: appAuth.token })
|
||||
const { data: installation } = await appClient.apps.getRepoInstallation({ owner, repo })
|
||||
|
||||
// Get installation token
|
||||
const installationAuth = await auth({ type: "installation", installationId: installation.id })
|
||||
|
||||
return new Response(JSON.stringify({ token: installationAuth.token }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
} catch (e: any) {
|
||||
let error = e
|
||||
if (e instanceof Error) {
|
||||
error = e.message
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
// Lookup installation
|
||||
const octokit = new Octokit({ auth: appAuth.token })
|
||||
let installation
|
||||
try {
|
||||
const ret = await octokit.apps.getRepoInstallation({ owner, repo })
|
||||
installation = ret.data
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes("Not Found")) {
|
||||
// not installed
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the opencode CLI to check if the GitHub app is installed
|
||||
*/
|
||||
if (request.method === "GET" && method === "get_github_app_installation") {
|
||||
const owner = url.searchParams.get("owner")
|
||||
const repo = url.searchParams.get("repo")
|
||||
|
||||
const auth = createAppAuth({
|
||||
appId: Resource.GITHUB_APP_ID.value,
|
||||
privateKey: Resource.GITHUB_APP_PRIVATE_KEY.value,
|
||||
})
|
||||
const appAuth = await auth({ type: "app" })
|
||||
|
||||
// Lookup installation
|
||||
const octokit = new Octokit({ auth: appAuth.token })
|
||||
let installation
|
||||
try {
|
||||
const ret = await octokit.apps.getRepoInstallation({ owner, repo })
|
||||
installation = ret.data
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes("Not Found")) {
|
||||
// not installed
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ installation }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 })
|
||||
},
|
||||
}
|
||||
return c.json({ installation })
|
||||
})
|
||||
.all("*", (c) => c.text("Not Found"))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.0",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -33,6 +33,8 @@
|
||||
"@clack/prompts": "0.11.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
"@octokit/graphql": "9.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.4.3",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
@@ -40,12 +42,14 @@
|
||||
"decimal.js": "10.5.0",
|
||||
"diff": "8.0.2",
|
||||
"gray-matter": "4.0.3",
|
||||
"hono": "4.7.10",
|
||||
"hono": "catalog:",
|
||||
"hono-openapi": "0.4.8",
|
||||
"isomorphic-git": "1.32.1",
|
||||
"open": "10.1.2",
|
||||
"remeda": "2.22.3",
|
||||
"remeda": "catalog:",
|
||||
"turndown": "7.2.0",
|
||||
"tree-sitter": "0.22.4",
|
||||
"tree-sitter-bash": "0.23.3",
|
||||
"vscode-jsonrpc": "8.2.1",
|
||||
"xdg-basedir": "5.1.0",
|
||||
"yargs": "18.0.0",
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
const dir = new URL("..", import.meta.url).pathname
|
||||
process.chdir(dir)
|
||||
import { $ } from "bun"
|
||||
|
||||
import pkg from "../package.json"
|
||||
|
||||
const dry = process.argv.includes("--dry")
|
||||
const snapshot = process.argv.includes("--snapshot")
|
||||
|
||||
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)
|
||||
})
|
||||
const dry = process.env["OPENCODE_DRY"] === "true"
|
||||
const version = process.env["OPENCODE_VERSION"]!
|
||||
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
|
||||
|
||||
console.log(`publishing ${version}`)
|
||||
|
||||
|
||||
@@ -16,7 +16,13 @@ export namespace Auth {
|
||||
key: z.string(),
|
||||
})
|
||||
|
||||
export const Info = z.discriminatedUnion("type", [Oauth, Api])
|
||||
export const WellKnown = z.object({
|
||||
type: z.literal("wellknown"),
|
||||
key: z.string(),
|
||||
token: z.string(),
|
||||
})
|
||||
|
||||
export const Info = z.discriminatedUnion("type", [Oauth, Api, WellKnown])
|
||||
export type Info = z.infer<typeof Info>
|
||||
|
||||
const filepath = path.join(Global.Path.data, "auth.json")
|
||||
|
||||
@@ -73,6 +73,7 @@ export namespace BunProc {
|
||||
// Let Bun handle registry resolution:
|
||||
// - If .npmrc files exist, Bun will use them automatically
|
||||
// - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
|
||||
// - No need to pass --registry flag
|
||||
log.info("installing package using Bun's default registry resolution", { pkg, version })
|
||||
|
||||
await BunProc.run(args, {
|
||||
|
||||
@@ -61,17 +61,45 @@ export const AuthListCommand = cmd({
|
||||
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
|
||||
}
|
||||
|
||||
prompts.outro(`${activeEnvVars.length} environment variables`)
|
||||
prompts.outro(`${activeEnvVars.length} environment variable` + (activeEnvVars.length === 1 ? "" : "s"))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const AuthLoginCommand = cmd({
|
||||
command: "login",
|
||||
command: "login [url]",
|
||||
describe: "log in to a provider",
|
||||
async handler() {
|
||||
UI.empty()
|
||||
builder: (yargs) =>
|
||||
yargs.positional("url", {
|
||||
describe: "opencode auth provider",
|
||||
type: "string",
|
||||
}),
|
||||
async handler(args) {
|
||||
prompts.intro("Add credential")
|
||||
if (args.url) {
|
||||
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json())
|
||||
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
||||
const proc = Bun.spawn({
|
||||
cmd: wellknown.auth.command,
|
||||
stdout: "pipe",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
prompts.log.error("Failed")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
const token = await new Response(proc.stdout).text()
|
||||
await Auth.set(args.url, {
|
||||
type: "wellknown",
|
||||
key: wellknown.auth.env,
|
||||
token: token.trim(),
|
||||
})
|
||||
prompts.log.success("Logged into " + args.url)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
UI.empty()
|
||||
const providers = await ModelsDev.get()
|
||||
const priority: Record<string, number> = {
|
||||
anthropic: 0,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cmd } from "../cmd"
|
||||
|
||||
export const SnapshotCommand = cmd({
|
||||
command: "snapshot",
|
||||
builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).demandCommand(),
|
||||
builder: (yargs) => yargs.command(TrackCommand).command(PatchCommand).command(DiffCommand).demandCommand(),
|
||||
async handler() {},
|
||||
})
|
||||
|
||||
@@ -31,3 +31,18 @@ const PatchCommand = cmd({
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const DiffCommand = cmd({
|
||||
command: "diff <hash>",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("hash", {
|
||||
type: "string",
|
||||
description: "hash",
|
||||
demandOption: true,
|
||||
}),
|
||||
async handler(args) {
|
||||
await bootstrap({ cwd: process.cwd() }, async () => {
|
||||
console.log(await Snapshot.diff(args.hash))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Server } from "../../server/server"
|
||||
import fs from "fs/promises"
|
||||
import type { CommandModule } from "yargs"
|
||||
|
||||
export const GenerateCommand = {
|
||||
command: "generate",
|
||||
handler: async () => {
|
||||
const specs = await Server.openapi()
|
||||
const dir = "gen"
|
||||
await fs.rmdir(dir, { recursive: true }).catch(() => {})
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
process.stdout.write(JSON.stringify(specs, null, 2))
|
||||
},
|
||||
} satisfies CommandModule
|
||||
|
||||
@@ -274,7 +274,7 @@ export const GithubInstallCommand = cmd({
|
||||
|
||||
// Wait for installation
|
||||
s.message("Waiting for GitHub app to be installed")
|
||||
const MAX_RETRIES = 60
|
||||
const MAX_RETRIES = 120
|
||||
let retries = 0
|
||||
do {
|
||||
const installation = await getInstallation()
|
||||
|
||||
@@ -113,7 +113,7 @@ export const TuiCommand = cmd({
|
||||
})
|
||||
|
||||
;(async () => {
|
||||
if (Installation.VERSION === "dev") return
|
||||
if (Installation.isDev()) return
|
||||
if (Installation.isSnapshot()) return
|
||||
const config = await Config.global()
|
||||
if (config.autoupdate === false) return
|
||||
|
||||
@@ -10,11 +10,14 @@ import fs from "fs/promises"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { NamedError } from "../util/error"
|
||||
import matter from "gray-matter"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Auth } from "../auth"
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
|
||||
export const state = App.state("config", async (app) => {
|
||||
const auth = await Auth.all()
|
||||
let result = await global()
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
|
||||
@@ -23,6 +26,20 @@ export namespace Config {
|
||||
}
|
||||
}
|
||||
|
||||
// Override with custom config if provided
|
||||
if (Flag.OPENCODE_CONFIG) {
|
||||
result = mergeDeep(result, await load(Flag.OPENCODE_CONFIG))
|
||||
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(auth)) {
|
||||
if (value.type === "wellknown") {
|
||||
process.env[value.key] = value.token
|
||||
const wellknown = await fetch(`${key}/.well-known/opencode`).then((x) => x.json())
|
||||
result = mergeDeep(result, await loadRaw(JSON.stringify(wellknown.config ?? {}), process.cwd()))
|
||||
}
|
||||
}
|
||||
|
||||
result.agent = result.agent || {}
|
||||
const markdownAgents = [
|
||||
...(await Filesystem.globUp("agent/*.md", Global.Path.config, Global.Path.config)),
|
||||
@@ -48,6 +65,32 @@ export namespace Config {
|
||||
throw new InvalidError({ path: item }, { cause: parsed.error })
|
||||
}
|
||||
|
||||
// Load mode markdown files
|
||||
result.mode = result.mode || {}
|
||||
const markdownModes = [
|
||||
...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)),
|
||||
...(await Filesystem.globUp(".opencode/mode/*.md", app.path.cwd, app.path.root)),
|
||||
]
|
||||
for (const item of markdownModes) {
|
||||
const content = await Bun.file(item).text()
|
||||
const md = matter(content)
|
||||
if (!md.data) continue
|
||||
|
||||
const config = {
|
||||
name: path.basename(item, ".md"),
|
||||
...md.data,
|
||||
prompt: md.content.trim(),
|
||||
}
|
||||
const parsed = Mode.safeParse(config)
|
||||
if (parsed.success) {
|
||||
result.mode = mergeDeep(result.mode, {
|
||||
[config.name]: parsed.data,
|
||||
})
|
||||
continue
|
||||
}
|
||||
throw new InvalidError({ path: item }, { cause: parsed.error })
|
||||
}
|
||||
|
||||
// Handle migration from autoshare to share field
|
||||
if (result.autoshare === true && !result.share) {
|
||||
result.share = "auto"
|
||||
@@ -170,6 +213,9 @@ export namespace Config {
|
||||
})
|
||||
export type Layout = z.infer<typeof Layout>
|
||||
|
||||
export const Permission = z.union([z.literal("ask"), z.literal("allow")])
|
||||
export type Permission = z.infer<typeof Permission>
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
@@ -233,6 +279,12 @@ export namespace Config {
|
||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
|
||||
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
|
||||
permission: z
|
||||
.object({
|
||||
edit: Permission.optional(),
|
||||
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
experimental: z
|
||||
.object({
|
||||
hook: z
|
||||
@@ -300,7 +352,10 @@ export namespace Config {
|
||||
throw new JsonError({ path: configPath }, { cause: err })
|
||||
})
|
||||
if (!text) return {}
|
||||
return loadRaw(text, configPath)
|
||||
}
|
||||
|
||||
async function loadRaw(text: string, configPath: string) {
|
||||
text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
|
||||
return process.env[varName] || ""
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export namespace Flag {
|
||||
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
|
||||
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
|
||||
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
|
||||
|
||||
function truthy(key: string) {
|
||||
const value = process.env[key]?.toLowerCase()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { App } from "../app/app"
|
||||
import { BunProc } from "../bun"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import path from "path"
|
||||
|
||||
export interface Info {
|
||||
name: string
|
||||
@@ -65,14 +64,57 @@ export const prettier: Info = {
|
||||
],
|
||||
async enabled() {
|
||||
const app = App.info()
|
||||
const nms = await Filesystem.findUp("node_modules", app.path.cwd, app.path.root)
|
||||
for (const item of nms) {
|
||||
if (await Bun.file(path.join(item, ".bin", "prettier")).exists()) return true
|
||||
const items = await Filesystem.findUp("package.json", app.path.cwd, app.path.root)
|
||||
for (const item of items) {
|
||||
const json = await Bun.file(item).json()
|
||||
if (json.dependencies?.prettier) return true
|
||||
if (json.devDependencies?.prettier) return true
|
||||
}
|
||||
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 = {
|
||||
name: "zig",
|
||||
command: ["zig", "fmt", "$FILE"],
|
||||
|
||||
@@ -97,7 +97,7 @@ export namespace Provider {
|
||||
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
|
||||
)
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
const headers: Record<string, string> = {
|
||||
...init.headers,
|
||||
...copilot.HEADERS,
|
||||
@@ -126,6 +126,15 @@ export namespace Provider {
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
azure: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
return sdk.responses(modelID)
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
|
||||
return { autoload: false }
|
||||
@@ -194,6 +203,17 @@ export namespace Provider {
|
||||
},
|
||||
}
|
||||
},
|
||||
vercel: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
options: {
|
||||
headers: {
|
||||
"http-referer": "https://opencode.ai/",
|
||||
"x-title": "opencode",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const state = App.state("provider", async () => {
|
||||
@@ -263,26 +283,26 @@ export namespace Provider {
|
||||
cost:
|
||||
!model.cost && !existing?.cost
|
||||
? {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
}
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
}
|
||||
: {
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
...existing?.cost,
|
||||
...model.cost,
|
||||
},
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
...existing?.cost,
|
||||
...model.cost,
|
||||
},
|
||||
options: {
|
||||
...existing?.options,
|
||||
...model.options,
|
||||
},
|
||||
limit: model.limit ??
|
||||
existing?.limit ?? {
|
||||
context: 0,
|
||||
output: 0,
|
||||
},
|
||||
context: 0,
|
||||
output: 0,
|
||||
},
|
||||
}
|
||||
parsed.models[modelID] = parsedModel
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ export namespace Server {
|
||||
|
||||
export type Routes = ReturnType<typeof app>
|
||||
|
||||
export const Event = {
|
||||
Connected: Bus.event("server.connected", z.object({})),
|
||||
}
|
||||
|
||||
function app() {
|
||||
const app = new Hono()
|
||||
|
||||
@@ -90,6 +94,7 @@ export namespace Server {
|
||||
"/event",
|
||||
describeRoute({
|
||||
description: "Get events",
|
||||
operationId: "event.subscribe",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Event stream",
|
||||
@@ -109,7 +114,10 @@ export namespace Server {
|
||||
log.info("event connected")
|
||||
return streamSSE(c, async (stream) => {
|
||||
stream.writeSSE({
|
||||
data: JSON.stringify({}),
|
||||
data: JSON.stringify({
|
||||
type: "server.connected",
|
||||
properties: {},
|
||||
}),
|
||||
})
|
||||
const unsub = Bus.subscribeAll(async (event) => {
|
||||
await stream.writeSSE({
|
||||
@@ -130,6 +138,7 @@ export namespace Server {
|
||||
"/app",
|
||||
describeRoute({
|
||||
description: "Get app info",
|
||||
operationId: "app.get",
|
||||
responses: {
|
||||
200: {
|
||||
description: "200",
|
||||
@@ -149,6 +158,7 @@ export namespace Server {
|
||||
"/app/init",
|
||||
describeRoute({
|
||||
description: "Initialize the app",
|
||||
operationId: "app.init",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Initialize the app",
|
||||
@@ -169,6 +179,7 @@ export namespace Server {
|
||||
"/config",
|
||||
describeRoute({
|
||||
description: "Get config info",
|
||||
operationId: "config.get",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Get config info",
|
||||
@@ -188,6 +199,7 @@ export namespace Server {
|
||||
"/session",
|
||||
describeRoute({
|
||||
description: "List all sessions",
|
||||
operationId: "session.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of sessions",
|
||||
@@ -209,6 +221,7 @@ export namespace Server {
|
||||
"/session",
|
||||
describeRoute({
|
||||
description: "Create a new session",
|
||||
operationId: "session.create",
|
||||
responses: {
|
||||
...ERRORS,
|
||||
200: {
|
||||
@@ -230,6 +243,7 @@ export namespace Server {
|
||||
"/session/:id",
|
||||
describeRoute({
|
||||
description: "Delete a session and all its data",
|
||||
operationId: "session.delete",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully deleted session",
|
||||
@@ -256,6 +270,7 @@ export namespace Server {
|
||||
"/session/:id/init",
|
||||
describeRoute({
|
||||
description: "Analyze the app and create an AGENTS.md file",
|
||||
operationId: "session.init",
|
||||
responses: {
|
||||
200: {
|
||||
description: "200",
|
||||
@@ -292,6 +307,7 @@ export namespace Server {
|
||||
"/session/:id/abort",
|
||||
describeRoute({
|
||||
description: "Abort a session",
|
||||
operationId: "session.abort",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Aborted session",
|
||||
@@ -317,6 +333,7 @@ export namespace Server {
|
||||
"/session/:id/share",
|
||||
describeRoute({
|
||||
description: "Share a session",
|
||||
operationId: "session.share",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully shared session",
|
||||
@@ -345,6 +362,7 @@ export namespace Server {
|
||||
"/session/:id/share",
|
||||
describeRoute({
|
||||
description: "Unshare the session",
|
||||
operationId: "session.unshare",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully unshared session",
|
||||
@@ -373,6 +391,7 @@ export namespace Server {
|
||||
"/session/:id/summarize",
|
||||
describeRoute({
|
||||
description: "Summarize the session",
|
||||
operationId: "session.summarize",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Summarized session",
|
||||
@@ -408,6 +427,7 @@ export namespace Server {
|
||||
"/session/:id/message",
|
||||
describeRoute({
|
||||
description: "List messages for a session",
|
||||
operationId: "session.messages",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of messages",
|
||||
@@ -441,6 +461,7 @@ export namespace Server {
|
||||
"/session/:id/message",
|
||||
describeRoute({
|
||||
description: "Create and send a new message to a session",
|
||||
operationId: "session.chat",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Created message",
|
||||
@@ -470,6 +491,7 @@ export namespace Server {
|
||||
"/session/:id/revert",
|
||||
describeRoute({
|
||||
description: "Revert a message",
|
||||
operationId: "session.revert",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated session",
|
||||
@@ -499,6 +521,7 @@ export namespace Server {
|
||||
"/session/:id/unrevert",
|
||||
describeRoute({
|
||||
description: "Restore all reverted messages",
|
||||
operationId: "session.unrevert",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated session",
|
||||
@@ -526,6 +549,7 @@ export namespace Server {
|
||||
"/config/providers",
|
||||
describeRoute({
|
||||
description: "List all providers",
|
||||
operationId: "config.providers",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of providers",
|
||||
@@ -554,6 +578,7 @@ export namespace Server {
|
||||
"/find",
|
||||
describeRoute({
|
||||
description: "Find text in files",
|
||||
operationId: "find.text",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Matches",
|
||||
@@ -586,6 +611,7 @@ export namespace Server {
|
||||
"/find/file",
|
||||
describeRoute({
|
||||
description: "Find files",
|
||||
operationId: "find.files",
|
||||
responses: {
|
||||
200: {
|
||||
description: "File paths",
|
||||
@@ -618,6 +644,7 @@ export namespace Server {
|
||||
"/find/symbol",
|
||||
describeRoute({
|
||||
description: "Find workspace symbols",
|
||||
operationId: "find.symbols",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Symbols",
|
||||
@@ -645,6 +672,7 @@ export namespace Server {
|
||||
"/file",
|
||||
describeRoute({
|
||||
description: "Read a file",
|
||||
operationId: "file.read",
|
||||
responses: {
|
||||
200: {
|
||||
description: "File content",
|
||||
@@ -681,6 +709,7 @@ export namespace Server {
|
||||
"/file/status",
|
||||
describeRoute({
|
||||
description: "Get file status",
|
||||
operationId: "file.status",
|
||||
responses: {
|
||||
200: {
|
||||
description: "File status",
|
||||
@@ -701,6 +730,7 @@ export namespace Server {
|
||||
"/log",
|
||||
describeRoute({
|
||||
description: "Write a log entry to the server logs",
|
||||
operationId: "app.log",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Log entry written successfully",
|
||||
@@ -750,6 +780,7 @@ export namespace Server {
|
||||
"/mode",
|
||||
describeRoute({
|
||||
description: "List all modes",
|
||||
operationId: "app.modes",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of modes",
|
||||
@@ -770,6 +801,7 @@ export namespace Server {
|
||||
"/tui/append-prompt",
|
||||
describeRoute({
|
||||
description: "Append prompt to the TUI",
|
||||
operationId: "tui.appendPrompt",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Prompt processed successfully",
|
||||
@@ -793,6 +825,7 @@ export namespace Server {
|
||||
"/tui/open-help",
|
||||
describeRoute({
|
||||
description: "Open the help dialog",
|
||||
operationId: "tui.openHelp",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Help dialog opened successfully",
|
||||
|
||||
@@ -67,6 +67,7 @@ export namespace Session {
|
||||
messageID: z.string(),
|
||||
partID: z.string().optional(),
|
||||
snapshot: z.string().optional(),
|
||||
diff: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
@@ -289,6 +290,9 @@ export namespace Session {
|
||||
export function abort(sessionID: string) {
|
||||
const controller = state().pending.get(sessionID)
|
||||
if (!controller) return false
|
||||
log.info("aborting", {
|
||||
sessionID,
|
||||
})
|
||||
controller.abort()
|
||||
state().pending.delete(sessionID)
|
||||
return true
|
||||
@@ -539,8 +543,6 @@ export namespace Session {
|
||||
for (const part of userParts) {
|
||||
await updatePart(part)
|
||||
}
|
||||
// mark session as updated since a message has been added to it
|
||||
await update(input.sessionID, (_draft) => {})
|
||||
|
||||
if (isLocked(input.sessionID)) {
|
||||
return new Promise((resolve) => {
|
||||
@@ -565,6 +567,7 @@ export namespace Session {
|
||||
const [preserve, remove] = splitWhen(msgs, (x) => x.info.id === messageID)
|
||||
msgs = preserve
|
||||
for (const msg of remove) {
|
||||
if (msg.info.id === userMsg.id) continue
|
||||
await Storage.remove(`session/message/${input.sessionID}/${msg.info.id}`)
|
||||
await Bus.publish(MessageV2.Event.Removed, { sessionID: input.sessionID, messageID: msg.info.id })
|
||||
}
|
||||
@@ -576,11 +579,15 @@ export namespace Session {
|
||||
for (const part of removeParts) {
|
||||
await Storage.remove(`session/part/${input.sessionID}/${last.info.id}/${part.id}`)
|
||||
await Bus.publish(MessageV2.Event.PartRemoved, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: last.info.id,
|
||||
partID: part.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
await update(input.sessionID, (draft) => {
|
||||
draft.revert = undefined
|
||||
})
|
||||
}
|
||||
|
||||
const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
|
||||
@@ -640,7 +647,7 @@ export namespace Session {
|
||||
return Session.update(input.sessionID, (draft) => {
|
||||
const cleaned = result.text.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
|
||||
const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
|
||||
draft.title = title
|
||||
draft.title = title.trim()
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
@@ -761,7 +768,11 @@ export namespace Session {
|
||||
}
|
||||
|
||||
const stream = streamText({
|
||||
onError() {},
|
||||
onError(e) {
|
||||
log.error("streamText error", {
|
||||
error: e,
|
||||
})
|
||||
},
|
||||
async prepareStep({ messages }) {
|
||||
const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed)
|
||||
if (queue.length) {
|
||||
@@ -808,7 +819,7 @@ export namespace Session {
|
||||
messages,
|
||||
}
|
||||
},
|
||||
maxRetries: 10,
|
||||
maxRetries: 3,
|
||||
maxOutputTokens: outputLimit,
|
||||
abortSignal: abort.signal,
|
||||
stopWhen: stepCountIs(1000),
|
||||
@@ -1026,7 +1037,7 @@ export namespace Session {
|
||||
}
|
||||
break
|
||||
|
||||
case "text":
|
||||
case "text-delta":
|
||||
if (currentText) {
|
||||
currentText.text += value.text
|
||||
if (currentText.text) await updatePart(currentText)
|
||||
@@ -1160,6 +1171,7 @@ export namespace Session {
|
||||
const session = await get(input.sessionID)
|
||||
revert.snapshot = session.revert?.snapshot ?? (await Snapshot.track())
|
||||
await Snapshot.revert(patches)
|
||||
if (revert.snapshot) revert.diff = await Snapshot.diff(revert.snapshot)
|
||||
return update(input.sessionID, (draft) => {
|
||||
draft.revert = revert
|
||||
})
|
||||
|
||||
@@ -284,6 +284,7 @@ export namespace MessageV2 {
|
||||
PartRemoved: Bus.event(
|
||||
"message.part.removed",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
messageID: z.string(),
|
||||
partID: z.string(),
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
You are opencode, an autonomous agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user.
|
||||
You are opencode, an agent - please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user.
|
||||
|
||||
Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
|
||||
|
||||
@@ -6,30 +6,31 @@ You MUST iterate and keep going until the problem is solved.
|
||||
|
||||
You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me.
|
||||
|
||||
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Use the TodoWrite and TodoRead tools to track and manage steps. Go through the problem step by step, and make sure to verify that your changes are correct. Once each step is finished mark it as completed with the TodoWrite tool. NEVER end your turn without having truly and completely solved the problem, use the TodoRead tool to make sure all steps are complete, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn. If a step is impossible to complete, mark it as cancelled using the TodoWrite tool.
|
||||
Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn.
|
||||
|
||||
THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH.
|
||||
|
||||
You must use the webfetch tool to recursively gather all information from URLs provided to you by the user, as well as any links you find in the content of those pages.
|
||||
You must use the webfetch tool to recursively gather all information from URL's provided to you by the user, as well as any links you find in the content of those pages.
|
||||
|
||||
Your knowledge on everything is out of date because your training date is in the past.
|
||||
Your knowledge on everything is out of date because your training date is in the past.
|
||||
|
||||
You CANNOT successfully complete this task without using Bing to verify your understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search bing for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
|
||||
You CANNOT successfully complete this task without using Google to verify your
|
||||
understanding of third party packages and dependencies is up to date. You must use the webfetch tool to search google for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need.
|
||||
|
||||
If the user request is "resume" or "continue" or "try again",use the TodoRead tool to find the next pending step. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all steps are marked as complete or cancelled. Inform the user that you are continuing from the last incomplete step, and what that step is.
|
||||
Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why.
|
||||
|
||||
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, update the plan and iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; run the build, and verify that the changes you made actually build; make sure you handle all edge cases, and run existing tests if they are provided.
|
||||
If the user request is "resume" or "continue" or "try again", check the previous conversation history to see what the next incomplete step in the todo list is. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all items are checked off. Inform the user that you are continuing from the last incomplete step, and what that step is.
|
||||
|
||||
You MUST plan extensively before each tool call, and reflect extensively on the outcomes of the previous tool calls. DO NOT do this entire process by making tool calls only, as this can impair your ability to solve the problem and think insightfully.
|
||||
Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided.
|
||||
|
||||
You MUST keep working until the problem is completely solved, and all steps in the todo list are complete. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
|
||||
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
|
||||
|
||||
You MUST use the ToolRead tool to verify that all steps are complete or cancelled before ending your turn. If any steps are incomplete, you MUST continue working on them until they are all complete.
|
||||
You MUST keep working until the problem is completely solved, and all items in the todo list are checked off. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead just saying that you will do it.
|
||||
|
||||
You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input.
|
||||
|
||||
# Workflow
|
||||
1. Fetch any URL provided by the user using the `webfetch` tool.
|
||||
1. Fetch any URL's provided by the user using the `webfetch` tool.
|
||||
2. Understand the problem deeply. Carefully read the issue and think critically about what is required. Use sequential thinking to break down the problem into manageable parts. Consider the following:
|
||||
- What is the expected behavior?
|
||||
- What are the edge cases?
|
||||
@@ -38,7 +39,7 @@ You are a highly capable and autonomous agent, and you can definitely solve this
|
||||
- What are the dependencies and interactions with other parts of the code?
|
||||
3. Investigate the codebase. Explore relevant files, search for key functions, and gather context.
|
||||
4. Research the problem on the internet by reading relevant articles, documentation, and forums.
|
||||
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using standard markdown format. Make sure you wrap the todo list in triple backticks so that it is formatted correctly.
|
||||
5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using emoji's to indicate the status of each item.
|
||||
6. Implement the fix incrementally. Make small, testable code changes.
|
||||
7. Debug as needed. Use debugging techniques to isolate and resolve issues.
|
||||
8. Test frequently. Run tests after each change to verify correctness.
|
||||
@@ -49,12 +50,12 @@ Refer to the detailed sections below for more information on each step.
|
||||
|
||||
## 1. Fetch Provided URLs
|
||||
- If the user provides a URL, use the `webfetch` tool to retrieve the content of the provided URL.
|
||||
- After fetching, review the content returned by the fetch tool.
|
||||
- After fetching, review the content returned by the webfetch tool.
|
||||
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
|
||||
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
|
||||
|
||||
## 2. Deeply Understand the Problem
|
||||
Carefully read the issue and think hard about a plan to solve it before coding. Use the sequential thinking tool if available.
|
||||
Carefully read the issue and think hard about a plan to solve it before coding.
|
||||
|
||||
## 3. Codebase Investigation
|
||||
- Explore relevant files and directories.
|
||||
@@ -64,49 +65,49 @@ Carefully read the issue and think hard about a plan to solve it before coding.
|
||||
- Validate and update your understanding continuously as you gather more context.
|
||||
|
||||
## 4. Internet Research
|
||||
- Use the `webfetch` tool to search bing by fetching the URL `https://www.bing.com/search?q=your+search+query`.
|
||||
- Use the `webfetch` tool to search google by fetching the URL `https://www.google.com/search?q=your+search+query`.
|
||||
- After fetching, review the content returned by the fetch tool.
|
||||
- If you find any additional URLs or links that are relevant, use the `webfetch` tool again to retrieve those links.
|
||||
- Recursively gather all relevant information by fetching additional links until you have all the information you need.
|
||||
- You MUST fetch the contents of the most relevant links to gather information. Do not rely on the summary that you find in the search results.
|
||||
- As you fetch each link, read the content thoroughly and fetch any additional links that you find withhin the content that are relevant to the problem.
|
||||
- Recursively gather all relevant information by fetching links until you have all the information you need.
|
||||
|
||||
## 5. Develop a Detailed Plan
|
||||
## 5. Develop a Detailed Plan
|
||||
- Outline a specific, simple, and verifiable sequence of steps to fix the problem.
|
||||
- Add steps using the TodoWrite tool.
|
||||
- Each time you complete a step, mark it as complete using the TodoWrite tool.
|
||||
- Each time you check off a step, use the TodoRead tool and display the updated todo list to the user in markdown format.
|
||||
- You MUST continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next.
|
||||
- You may only end your turn when all steps in the todo list are marked as complete or cancelled.
|
||||
- Create a todo list in markdown format to track your progress.
|
||||
- Each time you complete a step, check it off using `[x]` syntax.
|
||||
- Each time you check off a step, display the updated todo list to the user.
|
||||
- Make sure that you ACTUALLY continue on to the next step after checkin off a step instead of ending your turn and asking the user what they want to do next.
|
||||
|
||||
## 6. Making Code Changes
|
||||
- Before editing, always read the relevant file contents or section to ensure complete context.
|
||||
- Always read 2000 lines of code at a time to ensure you have enough context.
|
||||
- If a patch is not applied correctly, attempt to reapply it.
|
||||
- Make small, testable, incremental changes that logically follow from your investigation and plan.
|
||||
- When using the edit tool, include 3-5 lines of unchanged code before and after the string you want to replace, to make it unambiguous which part of the file should be edited.
|
||||
- If a patch or edit is not applied correctly, attempt to reapply it.
|
||||
- Always validate that your changes build and pass tests after each change.
|
||||
- If the build fails or tests fail, debug why before proceeding, update the plan as needed.
|
||||
- Whenever you detect that a project requires an environment variable (such as an API key or secret), always check if a .env file exists in the project root. If it does not exist, automatically create a .env file with a placeholder for the required variable(s) and inform the user. Do this proactively, without waiting for the user to request it.
|
||||
|
||||
## 7. Debugging
|
||||
- Use the `lsp_diagnostics` tool to check for any problems in the code.
|
||||
- Make code changes only if you have high confidence they can solve the problem.
|
||||
- When debugging, try to determine the root cause rather than addressing symptoms.
|
||||
- Debug for as long as needed to identify the root cause and identify a fix.
|
||||
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening.
|
||||
- To test hypotheses, you can also add test statements or functions.
|
||||
- Make code changes only if you have high confidence they can solve the problem
|
||||
- When debugging, try to determine the root cause rather than addressing symptoms
|
||||
- Debug for as long as needed to identify the root cause and identify a fix
|
||||
- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening
|
||||
- To test hypotheses, you can also add test statements or functions
|
||||
- Revisit your assumptions if unexpected behavior occurs.
|
||||
|
||||
# How to create a Todo List
|
||||
Use the following format to show the todo list:
|
||||
```markdown
|
||||
- [ ] Step 1: Description of the first step
|
||||
- [ ] Step 2: Description of the second step
|
||||
- [ ] Step 3: Description of the third step
|
||||
```
|
||||
Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above.
|
||||
Use the following format to create a todo list:
|
||||
```markdown
|
||||
- [ ] Step 1: Description of the first step
|
||||
- [ ] Step 2: Description of the second step
|
||||
- [ ] Step 3: Description of the third step
|
||||
```
|
||||
|
||||
Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above. Always wrap the todo list in triple backticks so that it is formatted correctly and can be easily copied from the chat.
|
||||
|
||||
Always show the completed todo list to the user as the last item in your message, so that they can see that you have addressed all of the steps.
|
||||
|
||||
|
||||
# Communication Guidelines
|
||||
Always communicate clearly and concisely in a casual, friendly yet professional tone.
|
||||
|
||||
Always communicate clearly and concisely in a casual, friendly yet professional tone.
|
||||
<examples>
|
||||
"Let me fetch the URL you provided to gather more information."
|
||||
"Ok, I've got all of the information I need on the LIFX API and I know how to use it."
|
||||
@@ -115,3 +116,44 @@ Always communicate clearly and concisely in a casual, friendly yet professional
|
||||
"OK! Now let's run the tests to make sure everything is working correctly."
|
||||
"Whelp - I see we have some problems. Let's fix those up."
|
||||
</examples>
|
||||
|
||||
- Respond with clear, direct answers. Use bullet points and code blocks for structure. - Avoid unnecessary explanations, repetition, and filler.
|
||||
- Always write code directly to the correct files.
|
||||
- Do not display code to the user unless they specifically ask for it.
|
||||
- Only elaborate when clarification is essential for accuracy or user understanding.
|
||||
|
||||
# Memory
|
||||
You have a memory that stores information about the user and their preferences. This memory is used to provide a more personalized experience. You can access and update this memory as needed. The memory is stored in a file called `.github/instructions/memory.instruction.md`. If the file is empty, you'll need to create it.
|
||||
|
||||
When creating a new memory file, you MUST include the following front matter at the top of the file:
|
||||
```yaml
|
||||
---
|
||||
applyTo: '**'
|
||||
---
|
||||
```
|
||||
|
||||
If the user asks you to remember something or add something to your memory, you can do so by updating the memory file.
|
||||
|
||||
# Reading Files and Folders
|
||||
|
||||
**Always check if you have already read a file, folder, or workspace structure before reading it again.**
|
||||
|
||||
- If you have already read the content and it has not changed, do NOT re-read it.
|
||||
- Only re-read files or folders if:
|
||||
- You suspect the content has changed since your last read.
|
||||
- You have made edits to the file or folder.
|
||||
- You encounter an error that suggests the context may be stale or incomplete.
|
||||
- Use your internal memory and previous context to avoid redundant reads.
|
||||
- This will save time, reduce unnecessary operations, and make your workflow more efficient.
|
||||
|
||||
# Writing Prompts
|
||||
If you are asked to write a prompt, you should always generate the prompt in markdown format.
|
||||
|
||||
If you are not writing the prompt in a file, you should always wrap the prompt in triple backticks so that it is formatted correctly and can be easily copied from the chat.
|
||||
|
||||
Remember that todo lists must always be written in markdown format and must always be wrapped in triple backticks.
|
||||
|
||||
# Git
|
||||
If the user tells you to stage and commit, you may do so.
|
||||
|
||||
You are NEVER allowed to stage and commit files automatically.
|
||||
|
||||
109
packages/opencode/src/session/prompt/qwen.txt
Normal file
109
packages/opencode/src/session/prompt/qwen.txt
Normal file
@@ -0,0 +1,109 @@
|
||||
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
||||
|
||||
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
|
||||
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
|
||||
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
||||
|
||||
If the user asks for help or wants to give feedback inform them of the following:
|
||||
- /help: Get help with using opencode
|
||||
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
|
||||
|
||||
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
|
||||
|
||||
# Tone and style
|
||||
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
||||
Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
|
||||
Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
|
||||
If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
|
||||
Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
|
||||
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
|
||||
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
|
||||
IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity:
|
||||
<example>
|
||||
user: 2 + 2
|
||||
assistant: 4
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: what is 2+2?
|
||||
assistant: 4
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: is 11 a prime number?
|
||||
assistant: Yes
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: what command should I run to list files in the current directory?
|
||||
assistant: ls
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: what command should I run to watch files in the current directory?
|
||||
assistant: [use the ls tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
|
||||
npm run dev
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: How many golf balls fit inside a jetta?
|
||||
assistant: 150000
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: what files are in the directory src/?
|
||||
assistant: [runs ls and sees foo.c, bar.c, baz.c]
|
||||
user: which file contains the implementation of foo?
|
||||
assistant: src/foo.c
|
||||
</example>
|
||||
|
||||
<example>
|
||||
user: write tests for new feature
|
||||
assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests]
|
||||
</example>
|
||||
|
||||
# Proactiveness
|
||||
You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between:
|
||||
1. Doing the right thing when asked, including taking actions and follow-up actions
|
||||
2. Not surprising the user with actions you take without asking
|
||||
For example, if the user asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions.
|
||||
3. Do not add additional code explanation summary unless requested by the user. After working on a file, just stop, rather than providing an explanation of what you did.
|
||||
|
||||
# Following conventions
|
||||
When making changes to files, first understand the file's code conventions. Mimic code style, use existing libraries and utilities, and follow existing patterns.
|
||||
- NEVER assume that a given library is available, even if it is well known. Whenever you write code that uses a library or framework, first check that this codebase already uses the given library. For example, you might look at neighboring files, or check the package.json (or cargo.toml, and so on depending on the language).
|
||||
- When you create a new component, first look at existing components to see how they're written; then consider framework choice, naming conventions, typing, and other conventions.
|
||||
- When you edit a piece of code, first look at the code's surrounding context (especially its imports) to understand the code's choice of frameworks and libraries. Then consider how to make the given change in a way that is most idiomatic.
|
||||
- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
|
||||
|
||||
# Code style
|
||||
- IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked
|
||||
|
||||
# Doing tasks
|
||||
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
|
||||
- Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
|
||||
- Implement the solution using all tools available to you
|
||||
- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
|
||||
- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time.
|
||||
NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
|
||||
|
||||
- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result.
|
||||
|
||||
# Tool usage policy
|
||||
- When doing file search, prefer to use the Task tool in order to reduce context usage.
|
||||
- You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. When making multiple bash tool calls, you MUST send a single message with multiple tools calls to run the calls in parallel. For example, if you need to run "git status" and "git diff", send a single message with two tool calls to run the calls in parallel.
|
||||
|
||||
You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.
|
||||
|
||||
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
|
||||
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
|
||||
|
||||
# Code References
|
||||
|
||||
When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.
|
||||
|
||||
<example>
|
||||
user: Where are errors from the client handled?
|
||||
assistant: Clients are marked as failed in the `connectToServer` function in src/services/process.ts:712.
|
||||
</example>
|
||||
|
||||
@@ -7,6 +7,7 @@ import path from "path"
|
||||
import os from "os"
|
||||
|
||||
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
||||
import PROMPT_ANTHROPIC_WITHOUT_TODO from "./prompt/qwen.txt"
|
||||
import PROMPT_BEAST from "./prompt/beast.txt"
|
||||
import PROMPT_GEMINI from "./prompt/gemini.txt"
|
||||
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
||||
@@ -21,7 +22,8 @@ export namespace SystemPrompt {
|
||||
export function provider(modelID: string) {
|
||||
if (modelID.includes("gpt-") || modelID.includes("o1") || modelID.includes("o3")) return [PROMPT_BEAST]
|
||||
if (modelID.includes("gemini-")) return [PROMPT_GEMINI]
|
||||
return [PROMPT_ANTHROPIC]
|
||||
if (modelID.includes("claude")) return [PROMPT_ANTHROPIC]
|
||||
return [PROMPT_ANTHROPIC_WITHOUT_TODO]
|
||||
}
|
||||
|
||||
export async function environment() {
|
||||
|
||||
@@ -94,6 +94,13 @@ export namespace Snapshot {
|
||||
}
|
||||
}
|
||||
|
||||
export async function diff(hash: string) {
|
||||
const app = App.info()
|
||||
const git = gitdir()
|
||||
const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(app.path.root).text()
|
||||
return result.trim()
|
||||
}
|
||||
|
||||
function gitdir() {
|
||||
const app = App.info()
|
||||
return path.join(app.path.data, "snapshots")
|
||||
|
||||
@@ -3,15 +3,22 @@ import { Tool } from "./tool"
|
||||
import DESCRIPTION from "./bash.txt"
|
||||
import { App } from "../app/app"
|
||||
|
||||
// import Parser from "tree-sitter"
|
||||
// import Bash from "tree-sitter-bash"
|
||||
// import { Config } from "../config/config"
|
||||
|
||||
const MAX_OUTPUT_LENGTH = 30000
|
||||
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||
|
||||
// const parser = new Parser()
|
||||
// parser.setLanguage(Bash.language as any)
|
||||
|
||||
export const BashTool = Tool.define("bash", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
timeout: z.number().min(0).max(MAX_TIMEOUT).describe("Optional timeout in milliseconds").optional(),
|
||||
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
|
||||
description: z
|
||||
.string()
|
||||
.describe(
|
||||
@@ -20,10 +27,83 @@ export const BashTool = Tool.define("bash", {
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
|
||||
const app = App.info()
|
||||
/*
|
||||
const _cfg = await Config.get()
|
||||
const tree = parser.parse(params.command)
|
||||
const permissions = (() => {
|
||||
const value = cfg.permission?.bash
|
||||
if (!value)
|
||||
return {
|
||||
"*": "allow",
|
||||
}
|
||||
if (typeof value === "string")
|
||||
return {
|
||||
"*": value,
|
||||
}
|
||||
return value
|
||||
})()
|
||||
|
||||
let needsAsk = false
|
||||
for (const node of tree.rootNode.descendantsOfType("command")) {
|
||||
const command = []
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i)
|
||||
if (!child) continue
|
||||
if (
|
||||
child.type !== "command_name" &&
|
||||
child.type !== "word" &&
|
||||
child.type !== "string" &&
|
||||
child.type !== "raw_string" &&
|
||||
child.type !== "concatenation"
|
||||
) {
|
||||
continue
|
||||
}
|
||||
command.push(child.text)
|
||||
}
|
||||
|
||||
// not an exhaustive list, but covers most common cases
|
||||
if (["cd", "rm", "cp", "mv", "mkdir", "touch", "chmod", "chown"].includes(command[0])) {
|
||||
for (const arg of command.slice(1)) {
|
||||
if (arg.startsWith("-")) continue
|
||||
const resolved = path.resolve(app.path.cwd, arg)
|
||||
if (!Filesystem.contains(app.path.cwd, resolved)) {
|
||||
throw new Error(
|
||||
`This command references paths outside of ${app.path.cwd} so it is not allowed to be executed.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// always allow cd if it passes above check
|
||||
if (!needsAsk && command[0] !== "cd") {
|
||||
const ask = (() => {
|
||||
for (const [pattern, value] of Object.entries(permissions)) {
|
||||
if (new Bun.Glob(pattern).match(node.text)) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return "ask"
|
||||
})()
|
||||
if (ask === "ask") needsAsk = true
|
||||
}
|
||||
}
|
||||
|
||||
if (needsAsk) {
|
||||
await Permission.ask({
|
||||
id: "bash",
|
||||
sessionID: ctx.sessionID,
|
||||
title: params.command,
|
||||
metadata: {
|
||||
command: params.command,
|
||||
},
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
const process = Bun.spawn({
|
||||
cmd: ["bash", "-c", params.command],
|
||||
cwd: App.info().path.cwd,
|
||||
cwd: app.path.cwd,
|
||||
maxBuffer: MAX_OUTPUT_LENGTH,
|
||||
signal: ctx.abort,
|
||||
timeout: timeout,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
|
||||
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
|
||||
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
|
||||
|
||||
import { z } from "zod"
|
||||
import * as path from "path"
|
||||
import { Tool } from "./tool"
|
||||
@@ -13,6 +14,8 @@ import { App } from "../app/app"
|
||||
import { File } from "../file"
|
||||
import { Bus } from "../bus"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Config } from "../config/config"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
export const EditTool = Tool.define("edit", {
|
||||
description: DESCRIPTION,
|
||||
@@ -33,17 +36,22 @@ export const EditTool = Tool.define("edit", {
|
||||
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
if (!Filesystem.contains(app.path.cwd, filepath)) {
|
||||
throw new Error(`File ${filepath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
await Permission.ask({
|
||||
id: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
title: "Edit this file: " + filepath,
|
||||
metadata: {
|
||||
filePath: filepath,
|
||||
oldString: params.oldString,
|
||||
newString: params.newString,
|
||||
},
|
||||
})
|
||||
const cfg = await Config.get()
|
||||
if (cfg.permission?.edit === "ask")
|
||||
await Permission.ask({
|
||||
id: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
title: "Edit this file: " + filepath,
|
||||
metadata: {
|
||||
filePath: filepath,
|
||||
oldString: params.oldString,
|
||||
newString: params.newString,
|
||||
},
|
||||
})
|
||||
|
||||
let contentOld = ""
|
||||
let contentNew = ""
|
||||
|
||||
@@ -6,6 +6,7 @@ import { LSP } from "../lsp"
|
||||
import { FileTime } from "../file/time"
|
||||
import DESCRIPTION from "./read.txt"
|
||||
import { App } from "../app/app"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
const DEFAULT_READ_LIMIT = 2000
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
@@ -14,19 +15,23 @@ export const ReadTool = Tool.define("read", {
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The path to the file to read"),
|
||||
offset: z.number().describe("The line number to start reading from (0-based)").optional(),
|
||||
limit: z.number().describe("The number of lines to read (defaults to 2000)").optional(),
|
||||
offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(),
|
||||
limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
let filePath = params.filePath
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
filePath = path.join(process.cwd(), filePath)
|
||||
let filepath = params.filePath
|
||||
if (!path.isAbsolute(filepath)) {
|
||||
filepath = path.join(process.cwd(), filepath)
|
||||
}
|
||||
const app = App.info()
|
||||
if (!Filesystem.contains(app.path.cwd, filepath)) {
|
||||
throw new Error(`File ${filepath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
const file = Bun.file(filePath)
|
||||
const file = Bun.file(filepath)
|
||||
if (!(await file.exists())) {
|
||||
const dir = path.dirname(filePath)
|
||||
const base = path.basename(filePath)
|
||||
const dir = path.dirname(filepath)
|
||||
const base = path.basename(filepath)
|
||||
|
||||
const dirEntries = fs.readdirSync(dir)
|
||||
const suggestions = dirEntries
|
||||
@@ -38,16 +43,18 @@ export const ReadTool = Tool.define("read", {
|
||||
.slice(0, 3)
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
throw new Error(`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
|
||||
throw new Error(`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
|
||||
}
|
||||
|
||||
throw new Error(`File not found: ${filePath}`)
|
||||
throw new Error(`File not found: ${filepath}`)
|
||||
}
|
||||
|
||||
const limit = params.limit ?? DEFAULT_READ_LIMIT
|
||||
const offset = params.offset || 0
|
||||
const isImage = isImageFile(filePath)
|
||||
const isImage = isImageFile(filepath)
|
||||
if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`)
|
||||
const isBinary = await isBinaryFile(file)
|
||||
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
|
||||
const lines = await file.text().then((text) => text.split("\n"))
|
||||
const raw = lines.slice(offset, offset + limit).map((line) => {
|
||||
return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
|
||||
@@ -66,11 +73,11 @@ export const ReadTool = Tool.define("read", {
|
||||
output += "\n</file>"
|
||||
|
||||
// just warms the lsp client
|
||||
LSP.touchFile(filePath, false)
|
||||
FileTime.read(ctx.sessionID, filePath)
|
||||
LSP.touchFile(filepath, false)
|
||||
FileTime.read(ctx.sessionID, filepath)
|
||||
|
||||
return {
|
||||
title: path.relative(App.info().path.root, filePath),
|
||||
title: path.relative(App.info().path.root, filepath),
|
||||
output,
|
||||
metadata: {
|
||||
preview,
|
||||
@@ -99,3 +106,14 @@ function isImageFile(filePath: string): string | false {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function isBinaryFile(file: Bun.BunFile): Promise<boolean> {
|
||||
const buffer = await file.arrayBuffer()
|
||||
const bytes = new Uint8Array(buffer.slice(0, 512)) // Check first 512 bytes
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
if (bytes[i] === 0) return true // Null byte indicates binary
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -69,7 +69,14 @@ export namespace ToolRegistry {
|
||||
patch: false,
|
||||
}
|
||||
}
|
||||
if (modelID.toLowerCase().includes("qwen")) {
|
||||
|
||||
if (
|
||||
modelID.toLowerCase().includes("qwen") ||
|
||||
modelID.includes("gpt-") ||
|
||||
modelID.includes("o1") ||
|
||||
modelID.includes("o3") ||
|
||||
modelID.includes("codex")
|
||||
) {
|
||||
return {
|
||||
patch: false,
|
||||
todowrite: false,
|
||||
|
||||
@@ -70,7 +70,7 @@ export const TaskTool = Tool.define("task", async () => {
|
||||
metadata: {
|
||||
summary: result.parts.filter((x) => x.type === "tool"),
|
||||
},
|
||||
output: result.parts.findLast((x) => x.type === "text")!.text,
|
||||
output: result.parts.findLast((x) => x.type === "text")?.text ?? "",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import DESCRIPTION_WRITE from "./todowrite.txt"
|
||||
import { App } from "../app/app"
|
||||
|
||||
const TodoInfo = z.object({
|
||||
content: z.string().min(1).describe("Brief description of the task"),
|
||||
content: z.string().describe("Brief description of the task"),
|
||||
status: z.enum(["pending", "in_progress", "completed", "cancelled"]).describe("Current status of the task"),
|
||||
priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"),
|
||||
id: z.string().describe("Unique identifier for the todo item"),
|
||||
|
||||
@@ -14,12 +14,7 @@ export const WebFetchTool = Tool.define("webfetch", {
|
||||
format: z
|
||||
.enum(["text", "markdown", "html"])
|
||||
.describe("The format to return the content in (text, markdown, or html)"),
|
||||
timeout: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(MAX_TIMEOUT / 1000)
|
||||
.describe("Optional timeout in seconds (max 120)")
|
||||
.optional(),
|
||||
timeout: z.number().describe("Optional timeout in seconds (max 120)").optional(),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
// Validate URL
|
||||
|
||||
@@ -8,6 +8,8 @@ import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
import { File } from "../file"
|
||||
import { FileTime } from "../file/time"
|
||||
import { Config } from "../config/config"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
export const WriteTool = Tool.define("write", {
|
||||
description: DESCRIPTION,
|
||||
@@ -18,21 +20,26 @@ export const WriteTool = Tool.define("write", {
|
||||
async execute(params, ctx) {
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
|
||||
if (!Filesystem.contains(app.path.cwd, filepath)) {
|
||||
throw new Error(`File ${filepath} is not in the current working directory`)
|
||||
}
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
const exists = await file.exists()
|
||||
if (exists) await FileTime.assert(ctx.sessionID, filepath)
|
||||
|
||||
await Permission.ask({
|
||||
id: "write",
|
||||
sessionID: ctx.sessionID,
|
||||
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
|
||||
metadata: {
|
||||
filePath: filepath,
|
||||
content: params.content,
|
||||
exists,
|
||||
},
|
||||
})
|
||||
const cfg = await Config.get()
|
||||
if (cfg.permission?.edit === "ask")
|
||||
await Permission.ask({
|
||||
id: "write",
|
||||
sessionID: ctx.sessionID,
|
||||
title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
|
||||
metadata: {
|
||||
filePath: filepath,
|
||||
content: params.content,
|
||||
exists,
|
||||
},
|
||||
})
|
||||
|
||||
await Bun.write(filepath, params.content)
|
||||
await Bus.publish(File.Event.Edited, {
|
||||
|
||||
@@ -9,7 +9,7 @@ export namespace Filesystem {
|
||||
}
|
||||
|
||||
export function contains(parent: string, child: string) {
|
||||
return relative(parent, child).startsWith("..")
|
||||
return !relative(parent, child).startsWith("..")
|
||||
}
|
||||
|
||||
export async function findUp(target: string, start: string, stop?: string) {
|
||||
|
||||
1
packages/opencode/test/fixtures/example/broken.ts
vendored
Normal file
1
packages/opencode/test/fixtures/example/broken.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
// Test fixture for ListTool
|
||||
1
packages/opencode/test/fixtures/example/cli.ts
vendored
Normal file
1
packages/opencode/test/fixtures/example/cli.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
// Test fixture for ListTool
|
||||
1
packages/opencode/test/fixtures/example/ink.tsx
vendored
Normal file
1
packages/opencode/test/fixtures/example/ink.tsx
vendored
Normal file
@@ -0,0 +1 @@
|
||||
// Test fixture for ListTool
|
||||
@@ -1,17 +1,9 @@
|
||||
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`tool.ls basic 1`] = `
|
||||
"- /home/thdxr/dev/projects/sst/opencode/js/example/
|
||||
- home/
|
||||
- thdxr/
|
||||
- dev/
|
||||
- projects/
|
||||
- sst/
|
||||
- opencode/
|
||||
- js/
|
||||
- example/
|
||||
- ink.tsx
|
||||
- broken.ts
|
||||
- cli.ts
|
||||
"packages/opencode/test/fixtures/example/
|
||||
broken.ts
|
||||
cli.ts
|
||||
ink.tsx
|
||||
"
|
||||
`;
|
||||
|
||||
44
packages/opencode/test/tool/bash.test.ts
Normal file
44
packages/opencode/test/tool/bash.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { App } from "../../src/app/app"
|
||||
import path from "path"
|
||||
import { BashTool } from "../../src/tool/bash"
|
||||
import { Log } from "../../src/util/log"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
messageID: "",
|
||||
abort: AbortSignal.any([]),
|
||||
metadata: () => {},
|
||||
}
|
||||
|
||||
const bash = await BashTool.init()
|
||||
const projectRoot = path.join(__dirname, "../..")
|
||||
Log.init({ print: false })
|
||||
|
||||
describe("tool.bash", () => {
|
||||
test("basic", async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
await bash.execute(
|
||||
{
|
||||
command: "cd foo/bar && ls",
|
||||
description: "List files in foo/bar",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("cd ../ should fail", async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
expect(
|
||||
bash.execute(
|
||||
{
|
||||
command: "cd ../",
|
||||
description: "Try to cd to parent directory",
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
import { App } from "../../src/app/app"
|
||||
import { GlobTool } from "../../src/tool/glob"
|
||||
import { ListTool } from "../../src/tool/ls"
|
||||
import path from "path"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "test",
|
||||
@@ -12,13 +13,16 @@ const ctx = {
|
||||
const glob = await GlobTool.init()
|
||||
const list = await ListTool.init()
|
||||
|
||||
const projectRoot = path.join(__dirname, "../..")
|
||||
const fixturePath = path.join(__dirname, "../fixtures/example")
|
||||
|
||||
describe("tool.glob", () => {
|
||||
test("truncate", async () => {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "../../node_modules/**/*",
|
||||
path: undefined,
|
||||
pattern: "**/*",
|
||||
path: "../../node_modules",
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
@@ -26,7 +30,7 @@ describe("tool.glob", () => {
|
||||
})
|
||||
})
|
||||
test("basic", async () => {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
await App.provide({ cwd: projectRoot }, async () => {
|
||||
let result = await glob.execute(
|
||||
{
|
||||
pattern: "*.json",
|
||||
@@ -36,7 +40,7 @@ describe("tool.glob", () => {
|
||||
)
|
||||
expect(result.metadata).toMatchObject({
|
||||
truncated: false,
|
||||
count: 3,
|
||||
count: 2,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -44,9 +48,12 @@ describe("tool.glob", () => {
|
||||
|
||||
describe("tool.ls", () => {
|
||||
test("basic", async () => {
|
||||
const result = await App.provide({ cwd: process.cwd() }, async () => {
|
||||
return await list.execute({ path: "./example", ignore: [".git"] }, ctx)
|
||||
const result = await App.provide({ cwd: projectRoot }, async () => {
|
||||
return await list.execute({ path: fixturePath, ignore: [".git"] }, ctx)
|
||||
})
|
||||
expect(result.output).toMatchSnapshot()
|
||||
|
||||
// Normalize absolute path to relative for consistent snapshots
|
||||
const normalizedOutput = result.output.replace(fixturePath, "packages/opencode/test/fixtures/example")
|
||||
expect(normalizedOutput).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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-5748199af356c3243a46a466e73b5d0bab7eaa0c56895e1d0f903d637f61d0bb.yml
|
||||
openapi_spec_hash: c04f6b6be54b05d9b1283c24e870163b
|
||||
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,140 +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">Match</a></code>
|
||||
- <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
@@ -1,4 +1,4 @@
|
||||
File generated from our OpenAPI spec by Stainless.
|
||||
|
||||
This directory can be used to store example files demonstrating usage of this SDK.
|
||||
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
|
||||
It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
|
||||
50
packages/sdk/go/field.go
Normal file
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user