mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 18:26:09 +00:00
feat(client): support opaque payload schemas (#37773)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
This commit is contained in:
co-authored by
Aiden Cline
parent
fe9a936867
commit
bef6cfbffe
@@ -14,7 +14,9 @@ The package is private while its API is explored. Its tests are the executable s
|
||||
- Keep the Promise surface domain-oriented rather than Hey API compatible: methods return unwrapped values and reject with tagged declared errors or `ClientError`.
|
||||
- Return Promise streams as lazy `AsyncIterable` values and Effect streams as `Stream` values. Neither runtime reconnects automatically.
|
||||
|
||||
- Flatten path, query, header, and payload fields into one input object.
|
||||
- Flatten path, query, header, and struct payload fields into one input object.
|
||||
- Represent every non-struct payload (including unions, arrays, primitives, and records with index signatures) as one required `payload` input field and send that value as the request body directly.
|
||||
- Reject an opaque `payload` field when its name collides with a path, query, or header field.
|
||||
- Reject duplicate field names across input channels.
|
||||
- Emit no method argument for zero fields, an optional object when every field is optional, and a required object when any field is required.
|
||||
- Unwrap exact `{ data: A }` success envelopes.
|
||||
|
||||
@@ -126,10 +126,18 @@ export function compile<Id extends string, Groups extends HttpApiGroup.Constrain
|
||||
...inputFields(headers?.schema, "headers", name),
|
||||
...payloads.flatMap((item) => inputFields(item.schema, "payload", name)),
|
||||
]
|
||||
const names = new Set<string>()
|
||||
const names = new Map<string, InputField["source"]>()
|
||||
for (const field of inputs) {
|
||||
if (names.has(field.name)) throw new GenerationError({ reason: `Input field collision: ${field.name}` })
|
||||
names.add(field.name)
|
||||
const existing = names.get(field.name)
|
||||
if (existing !== undefined) {
|
||||
if (field.source === "payload" && payloads[0] !== undefined && !isFlattenableStruct(payloads[0].schema)) {
|
||||
throw new GenerationError({
|
||||
reason: `Opaque payload field collision: ${name}.payload conflicts with ${existing}.${field.name}`,
|
||||
})
|
||||
}
|
||||
throw new GenerationError({ reason: `Input field collision: ${field.name}` })
|
||||
}
|
||||
names.set(field.name, field.source)
|
||||
}
|
||||
|
||||
const schemaPaths: Array<readonly [string, Schema.Top]> = [
|
||||
@@ -336,7 +344,7 @@ function renderEffectShape(groups: ReadonlyArray<Group>, options: { readonly mod
|
||||
const input = endpoint.input
|
||||
.map(
|
||||
(field) =>
|
||||
`readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${prefix}Request[${JSON.stringify(field.source)}][${JSON.stringify(field.name)}]`,
|
||||
`readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${prefix}Request[${JSON.stringify(field.source)}]${isOpaquePayload(endpoint) && field.source === "payload" ? "" : `[${JSON.stringify(field.name)}]`}`,
|
||||
)
|
||||
.join("; ")
|
||||
const inputType = endpoint.operation.inputMode === "none" ? "" : `export type ${prefix}Input = { ${input} }`
|
||||
@@ -474,26 +482,37 @@ function renderImportedEffectFiles(
|
||||
const rawGroup = group.endpoints[0]?.topLevel ? "RawClient" : `RawClient[${JSON.stringify(group.sourceIdentifier)}]`
|
||||
const methods = group.endpoints.map((item, endpointIndex) => {
|
||||
const prefix = `Endpoint${groupIndex}_${endpointIndex}`
|
||||
const schemaBySource = {
|
||||
params: item.params,
|
||||
query: item.query,
|
||||
headers: item.headers,
|
||||
payload: item.payloads[0],
|
||||
}
|
||||
const request = (["params", "query", "headers", "payload"] as const)
|
||||
.flatMap((source) => {
|
||||
const fields = item.input.filter((field) => field.source === source)
|
||||
if (fields.length === 0) return []
|
||||
return [
|
||||
`${source}: { ${fields.map((field) => `${JSON.stringify(field.name)}: input${item.operation.inputMode === "optional" ? "?." : ""}[${JSON.stringify(field.name)}]`).join(", ")} }`,
|
||||
]
|
||||
})
|
||||
.map((source) =>
|
||||
renderEffectRequestPart(
|
||||
item.input,
|
||||
item.operation.inputMode,
|
||||
source,
|
||||
isOpaquePayload(item),
|
||||
schemaBySource[source] !== undefined,
|
||||
),
|
||||
)
|
||||
.filter((part): part is string => part !== undefined)
|
||||
.join(", ")
|
||||
const input = item.input
|
||||
.map(
|
||||
(field) =>
|
||||
`readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${prefix}Request[${JSON.stringify(field.source)}][${JSON.stringify(field.name)}]`,
|
||||
`readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${prefix}Request[${JSON.stringify(field.source)}]${isOpaquePayload(item) && field.source === "payload" ? "" : `[${JSON.stringify(field.name)}]`}`,
|
||||
)
|
||||
.join("; ")
|
||||
const argument =
|
||||
item.operation.inputMode === "none"
|
||||
? ""
|
||||
: `input${item.operation.inputMode === "optional" ? "?" : ""}: ${prefix}Input`
|
||||
const rawCall = `raw[${JSON.stringify(item.endpoint.identifier)}]({ ${request} })`
|
||||
// HttpApiClient distributes union payloads into union request objects, while rebuilding a flattened input
|
||||
// produces one object containing a union value. The shapes are equivalent but TypeScript cannot correlate them.
|
||||
const rawCall = `raw[${JSON.stringify(item.endpoint.identifier)}]({ ${request} }${isOpaquePayload(item) ? ` as ${prefix}Request` : ""})`
|
||||
const mapped = `${rawCall}.pipe(Effect.mapError(mapClientError)${item.unwrapData ? ", Effect.map((value) => value.data)" : ""})`
|
||||
return `${item.operation.inputMode === "none" ? "" : `type ${prefix}Request = Parameters<${rawGroup}[${JSON.stringify(item.endpoint.identifier)}]>[0]\ntype ${prefix}Input = { ${input} }\n`}const ${prefix} = (raw: ${rawGroup}) => (${argument}) => ${item.operation.success === "stream" ? `Stream.unwrap(${rawCall}.pipe(Effect.mapError(mapClientError), Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError)))))` : mapped}`
|
||||
})
|
||||
@@ -623,7 +642,7 @@ function renderPromiseTypes(
|
||||
const schema = schemas[field.source]
|
||||
if (schema === undefined)
|
||||
throw new GenerationError({ reason: `Missing input schema: ${prefix}.${field.name}` })
|
||||
return `readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: (${typeOf(schema, field.source === "query")})[${JSON.stringify(field.name)}]`
|
||||
return `readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${isOpaquePayload(endpoint) && field.source === "payload" ? typeOf(schema) : `(${typeOf(schema, field.source === "query")})[${JSON.stringify(field.name)}]`}`
|
||||
})
|
||||
.join("; ")
|
||||
const successSchema = endpoint.successes[0]
|
||||
@@ -693,8 +712,12 @@ function renderPromiseClient(groups: ReadonlyArray<Group>) {
|
||||
const part = (source: InputField["source"]) => {
|
||||
const inputs = endpoint.input.filter((field) => field.source === source)
|
||||
return inputs.length === 0
|
||||
? undefined
|
||||
: `{ ${inputs.map((field) => `${JSON.stringify(field.name)}: ${access(field.name)}`).join(", ")} }`
|
||||
? source === "payload" && endpoint.payloads.length > 0
|
||||
? "{ }"
|
||||
: undefined
|
||||
: isOpaquePayload(endpoint) && source === "payload"
|
||||
? access(inputs[0].name)
|
||||
: `{ ${inputs.map((field) => `${JSON.stringify(field.name)}: ${access(field.name)}`).join(", ")} }`
|
||||
}
|
||||
const parts = [
|
||||
endpoint.query === undefined ? undefined : `query: ${part("query")}`,
|
||||
@@ -1146,10 +1169,21 @@ export function generate<Id extends string, Groups extends HttpApiGroup.Constrai
|
||||
}).pipe(Effect.flatMap((output) => write(output, options.directory)))
|
||||
}
|
||||
|
||||
function isFlattenableStruct(schema: Schema.Top) {
|
||||
const ast = Schema.toType(schema).ast
|
||||
return SchemaAST.isObjects(ast) && ast.indexSignatures.length === 0
|
||||
}
|
||||
|
||||
function isOpaquePayload(endpoint: Endpoint) {
|
||||
const payload = endpoint.payloads[0]
|
||||
return payload !== undefined && !isFlattenableStruct(payload)
|
||||
}
|
||||
|
||||
function inputFields(schema: Schema.Top | undefined, source: InputField["source"], operation: string) {
|
||||
if (schema === undefined) return []
|
||||
const ast = Schema.toType(schema).ast
|
||||
if (!SchemaAST.isObjects(ast) || ast.indexSignatures.length > 0) {
|
||||
if (source === "payload") return [{ name: "payload", source, optional: false }] as const
|
||||
throw new GenerationError({ reason: `Input schema must be a struct: ${operation}.${source}` })
|
||||
}
|
||||
return ast.propertySignatures.map((field) => {
|
||||
@@ -1416,7 +1450,7 @@ function renderGroup(group: Group, groupIndex: number) {
|
||||
if (slot === undefined) {
|
||||
throw new GenerationError({ reason: `Missing input schema: ${group.identifier}.${endpoint.identifier}` })
|
||||
}
|
||||
return `readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: (typeof ${slot.name}.Type)[${JSON.stringify(field.name)}]`
|
||||
return `readonly ${JSON.stringify(field.name)}${field.optional ? "?" : ""}: ${isOpaquePayload(operation) && field.source === "payload" ? `typeof ${slot.name}.Type` : `(typeof ${slot.name}.Type)[${JSON.stringify(field.name)}]`}`
|
||||
})
|
||||
.join("; ")
|
||||
const argument =
|
||||
@@ -1424,24 +1458,29 @@ function renderGroup(group: Group, groupIndex: number) {
|
||||
? ""
|
||||
: `input${operation.operation.inputMode === "optional" ? "?" : ""}: ${prefix}Input`
|
||||
const request = (["params", "query", "headers", "payload"] as const)
|
||||
.flatMap((source) => {
|
||||
const slot = schemaBySource[source]
|
||||
if (slot === undefined) return []
|
||||
const fields = operation.input
|
||||
.filter((field) => field.source === source)
|
||||
.map(
|
||||
(field) =>
|
||||
`${JSON.stringify(field.name)}: input${operation.operation.inputMode === "optional" ? "?." : ""}[${JSON.stringify(field.name)}]`,
|
||||
)
|
||||
return [`${source}: { ${fields.join(", ")} }`]
|
||||
})
|
||||
.map((source) =>
|
||||
renderEffectRequestPart(
|
||||
operation.input,
|
||||
operation.operation.inputMode,
|
||||
source,
|
||||
isOpaquePayload(operation),
|
||||
schemaBySource[source] !== undefined,
|
||||
),
|
||||
)
|
||||
.filter((part): part is string => part !== undefined)
|
||||
.join(", ")
|
||||
const declared = [...errorSlots, ...(success.streamError === undefined ? [] : [success.streamError])]
|
||||
const declaredSchema =
|
||||
declared.length === 0 ? "Schema.Never" : `Schema.Union([${declared.map((slot) => slot.name).join(", ")}])`
|
||||
const rawCall = `raw[${JSON.stringify(endpoint.identifier)}]({ ${request} })`
|
||||
const opaquePayload = isOpaquePayload(operation)
|
||||
// HttpApiClient distributes union payloads into union request objects, while rebuilding a flattened input
|
||||
// produces one object containing a union value. The shapes are equivalent but TypeScript cannot correlate them.
|
||||
const rawCall = `raw[${JSON.stringify(endpoint.identifier)}]({ ${request} }${opaquePayload ? ` as ${prefix}Request` : ""})`
|
||||
const mapped = `${rawCall}.pipe(Effect.mapError(map${prefix}Error)${operation.unwrapData ? ", Effect.map((value) => value.data)" : ""})`
|
||||
const inputDeclaration = operation.operation.inputMode === "none" ? "" : `type ${prefix}Input = { ${inputType} }\n`
|
||||
const inputDeclaration =
|
||||
operation.operation.inputMode === "none"
|
||||
? ""
|
||||
: `${opaquePayload ? `type ${prefix}Request = Parameters<RawGroup[${JSON.stringify(endpoint.identifier)}]>[0]\n` : ""}type ${prefix}Input = { ${inputType} }\n`
|
||||
adapters.push(
|
||||
`${inputDeclaration}const ${prefix}DeclaredError = ${declaredSchema}\nconst map${prefix}Error = (error: unknown) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : Schema.is(${prefix}DeclaredError)(error) ? error : new ClientError({ cause: error })\nconst ${prefix} = (raw: RawGroup) => (${argument}) => ${operation.operation.success === "stream" ? `Stream.unwrap(${rawCall}.pipe(Effect.mapError(map${prefix}Error), Effect.map((stream) => stream.pipe(Stream.mapError(map${prefix}Error)))))` : mapped}`,
|
||||
)
|
||||
@@ -1491,6 +1530,20 @@ function renderGroup(group: Group, groupIndex: number) {
|
||||
return `// Generated by @opencode-ai/httpapi-codegen. Do not edit.\nimport { Effect, Schema${usesStream ? ", Stream" : ""} } from "effect"\nimport { Sse } from "effect/unstable/encoding"\nimport { HttpClientError } from "effect/unstable/http"\nimport { HttpApiClient, HttpApiEndpoint, HttpApiGroup${usesHttpApiSchema ? ", HttpApiSchema" : ""} } from "effect/unstable/httpapi"\nimport { ClientError } from "./client-error"\n\n${declarations}\n\nexport const Group${groupIndex} = ${groupSource}\n\ntype RawGroup = ${rawGroup}\n\n${adapters.join("\n\n")}\n\nexport const adaptGroup${groupIndex} = (raw: RawGroup) => ({ ${methods} })\n`
|
||||
}
|
||||
|
||||
function renderEffectRequestPart(
|
||||
input: Endpoint["input"],
|
||||
mode: Operation["inputMode"],
|
||||
source: InputField["source"],
|
||||
opaquePayload: boolean,
|
||||
present = false,
|
||||
) {
|
||||
const fields = input.filter((field) => field.source === source)
|
||||
if (fields.length === 0) return present ? `${source}: { }` : undefined
|
||||
const access = (name: string) => `input${mode === "optional" ? "?." : ""}[${JSON.stringify(name)}]`
|
||||
if (opaquePayload && source === "payload") return `${source}: ${access(fields[0].name)}`
|
||||
return `${source}: { ${fields.map((field) => `${JSON.stringify(field.name)}: ${access(field.name)}`).join(", ")} }`
|
||||
}
|
||||
|
||||
function renderSchemas(slots: ReadonlyArray<Slot>) {
|
||||
if (slots.length === 0) return ""
|
||||
const classes = new Map(
|
||||
|
||||
@@ -27,6 +27,18 @@ export const Api = HttpApi.make("fixture")
|
||||
params: { sessionID: Schema.String },
|
||||
success: HttpApiSchema.NoContent,
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("configure", "/session/:sessionID/configure", {
|
||||
params: { sessionID: Schema.String },
|
||||
query: { dryRun: Schema.optional(Schema.Boolean) },
|
||||
headers: { traceID: Schema.String },
|
||||
payload: Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("local"), command: Schema.Array(Schema.String) }),
|
||||
Schema.Struct({ type: Schema.Literal("remote"), url: Schema.String }),
|
||||
]),
|
||||
success: Schema.String,
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
|
||||
@@ -773,6 +773,42 @@ describe("HttpApiCodegen.generate", () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("serializes an opaque union payload as the direct JSON body", async () => {
|
||||
const output = emitPromise(
|
||||
compileContract(
|
||||
api(
|
||||
HttpApiEndpoint.post("configure", "/session", {
|
||||
payload: Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("local"), command: Schema.Array(Schema.String) }),
|
||||
Schema.Struct({ type: Schema.Literal("remote"), url: Schema.String }),
|
||||
]),
|
||||
success: HttpApiSchema.NoContent,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-httpapi-codegen-"))
|
||||
|
||||
try {
|
||||
await Promise.all(output.files.map((file) => Bun.write(join(directory, file.path), file.content)))
|
||||
const generated = await import(`${join(directory, "index.ts")}?t=${crypto.randomUUID()}`)
|
||||
let request: Request | undefined
|
||||
const client = generated.OpenCode.make({
|
||||
baseUrl: "https://example.com",
|
||||
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
request = input instanceof Request ? input : new Request(input, init)
|
||||
return new Response(null, { status: 204 })
|
||||
},
|
||||
})
|
||||
|
||||
await client.session.configure({ payload: { type: "local", command: ["opencode"] } })
|
||||
|
||||
expect(await request?.json()).toEqual({ type: "local", command: ["opencode"] })
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("serializes explicit null query values", async () => {
|
||||
const output = emitPromise(
|
||||
compileContract(
|
||||
@@ -971,6 +1007,74 @@ describe("HttpApiCodegen.generate", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("uses one opaque field for non-struct payloads across emitters", () => {
|
||||
const source = api(
|
||||
HttpApiEndpoint.post("configure", "/session/configure", {
|
||||
payload: Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("local"), command: Schema.Array(Schema.String) }),
|
||||
Schema.Struct({ type: Schema.Literal("remote"), url: Schema.String }),
|
||||
]),
|
||||
success: Schema.String,
|
||||
}),
|
||||
)
|
||||
const contract = compileContract(source)
|
||||
const effect = emitEffect(contract)
|
||||
const imported = emitEffectImported(contract, { module: "@example/api", api: "Api" })
|
||||
const shape = emitEffectShape(contract, { module: "@example/api", api: "Api" })
|
||||
const promise = emitPromise(contract)
|
||||
|
||||
expect(effect.operations[0]).toMatchObject({
|
||||
input: [{ name: "payload", source: "payload" }],
|
||||
inputMode: "required",
|
||||
})
|
||||
expect(effect.files.find((file) => file.path === "session.ts")?.content).toContain('payload: input["payload"]')
|
||||
expect(imported.files.find((file) => file.path === "client.ts")?.content).toContain('payload: input["payload"]')
|
||||
expect(shape.files[0]?.content).toContain('Endpoint0_0Request["payload"]')
|
||||
expect(promise.files.find((file) => file.path === "types.ts")?.content).toContain(
|
||||
'readonly "payload": { readonly "type": "local", readonly "command": ReadonlyArray<string> } | { readonly "type": "remote", readonly "url": string }',
|
||||
)
|
||||
expect(promise.files.find((file) => file.path === "client.ts")?.content).toContain('body: input["payload"]')
|
||||
})
|
||||
|
||||
test("routes arrays, primitives, and index-signature records through the opaque payload path", () => {
|
||||
for (const payload of [Schema.Array(Schema.String), Schema.String, Schema.Record(Schema.String, Schema.Number)]) {
|
||||
expect(
|
||||
compileContract(api(HttpApiEndpoint.post("set", "/session", { payload, success: HttpApiSchema.NoContent })))
|
||||
.groups[0]?.endpoints[0]?.operation.input,
|
||||
).toEqual([{ name: "payload", source: "payload" }])
|
||||
}
|
||||
})
|
||||
|
||||
test("rejects an opaque payload field that collides with another input channel", () => {
|
||||
expect(() =>
|
||||
compileContract(
|
||||
api(
|
||||
HttpApiEndpoint.post("configure", "/session", {
|
||||
query: { payload: Schema.String },
|
||||
payload: Schema.Union([Schema.String, Schema.Number]),
|
||||
success: Schema.String,
|
||||
}),
|
||||
),
|
||||
),
|
||||
).toThrow("Opaque payload field collision: session.configure.payload conflicts with query.payload")
|
||||
})
|
||||
|
||||
test("preserves required empty struct payloads in imported Effect adapters", () => {
|
||||
const contract = compileContract(
|
||||
api(
|
||||
HttpApiEndpoint.post("empty", "/session", {
|
||||
payload: Schema.Struct({}),
|
||||
success: Schema.String,
|
||||
}),
|
||||
),
|
||||
)
|
||||
const effect = emitEffectImported(contract, { module: "@example/api", api: "Api" })
|
||||
const promise = emitPromise(contract)
|
||||
|
||||
expect(effect.files.find((file) => file.path === "client.ts")?.content).toContain("payload: { }")
|
||||
expect(promise.files.find((file) => file.path === "client.ts")?.content).toContain("body: { }")
|
||||
})
|
||||
|
||||
test("uses no argument when an operation has no input fields", () => {
|
||||
const output = compile(api(HttpApiEndpoint.get("health", "/health", { success: Schema.String })))
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ export const program = OpenCode.make().pipe(
|
||||
const filtered = client.session.list({ archived: true })
|
||||
const get = client.session.get({ sessionID: "session" })
|
||||
const interrupt = client.session.interrupt({ sessionID: "session" })
|
||||
const configure = client.session.configure({
|
||||
sessionID: "session",
|
||||
dryRun: true,
|
||||
traceID: "trace",
|
||||
payload: { type: "local", command: ["opencode"] },
|
||||
})
|
||||
const status = client.status()
|
||||
const subscribe = client.event.subscribe()
|
||||
|
||||
@@ -18,10 +24,11 @@ export const program = OpenCode.make().pipe(
|
||||
const _filtered: Effect.Effect<ReadonlyArray<string>, ClientError> = filtered
|
||||
const _get: Effect.Effect<string, Missing | ClientError> = get
|
||||
const _interrupt: Effect.Effect<void, ClientError> = interrupt
|
||||
const _configure: Effect.Effect<string, ClientError> = configure
|
||||
const _status: Effect.Effect<string, ClientError> = status
|
||||
const _subscribe: Stream.Stream<{ readonly type: string }, ClientError> = subscribe
|
||||
|
||||
return { _health, _list, _filtered, _get, _interrupt, _status, _subscribe }
|
||||
return { _health, _list, _filtered, _get, _interrupt, _configure, _status, _subscribe }
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -24,6 +24,19 @@ const Endpoint3Params = Schema.Struct({ sessionID: Schema.String })
|
||||
|
||||
const Endpoint3Success = Schema.Void.annotate({ httpApiStatus: 204 })
|
||||
|
||||
const Endpoint4Params = Schema.Struct({ sessionID: Schema.String })
|
||||
|
||||
const Endpoint4Query = Schema.Struct({ dryRun: Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Undefined])) })
|
||||
|
||||
const Endpoint4Headers = Schema.Struct({ traceID: Schema.String })
|
||||
|
||||
const Endpoint4Payload0 = Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("local"), command: Schema.Array(Schema.String) }),
|
||||
Schema.Struct({ type: Schema.Literal("remote"), url: Schema.String }),
|
||||
])
|
||||
|
||||
const Endpoint4Success = Schema.String
|
||||
|
||||
export const Group0 = HttpApiGroup.make("session", { topLevel: false })
|
||||
.add(HttpApiEndpoint.make("GET")("health", "/session/health", { success: Endpoint0Success }))
|
||||
.add(HttpApiEndpoint.make("GET")("list", "/session", { query: Endpoint1Query, success: Endpoint1Success }))
|
||||
@@ -40,6 +53,15 @@ export const Group0 = HttpApiGroup.make("session", { topLevel: false })
|
||||
success: Endpoint3Success,
|
||||
}),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.make("POST")("configure", "/session/:sessionID/configure", {
|
||||
params: Endpoint4Params,
|
||||
query: Endpoint4Query,
|
||||
headers: Endpoint4Headers,
|
||||
payload: Endpoint4Payload0,
|
||||
success: Endpoint4Success,
|
||||
}),
|
||||
)
|
||||
|
||||
type RawGroup = HttpApiClient.Client.Group<typeof Group0, never, never>
|
||||
|
||||
@@ -88,9 +110,32 @@ const mapEndpoint3Error = (error: unknown) =>
|
||||
const Endpoint3 = (raw: RawGroup) => (input: Endpoint3Input) =>
|
||||
raw["interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapEndpoint3Error))
|
||||
|
||||
type Endpoint4Request = Parameters<RawGroup["configure"]>[0]
|
||||
type Endpoint4Input = {
|
||||
readonly sessionID: (typeof Endpoint4Params.Type)["sessionID"]
|
||||
readonly dryRun?: (typeof Endpoint4Query.Type)["dryRun"]
|
||||
readonly traceID: (typeof Endpoint4Headers.Type)["traceID"]
|
||||
readonly payload: typeof Endpoint4Payload0.Type
|
||||
}
|
||||
const Endpoint4DeclaredError = Schema.Never
|
||||
const mapEndpoint4Error = (error: unknown) =>
|
||||
HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error)
|
||||
? new ClientError({ cause: error })
|
||||
: Schema.is(Endpoint4DeclaredError)(error)
|
||||
? error
|
||||
: new ClientError({ cause: error })
|
||||
const Endpoint4 = (raw: RawGroup) => (input: Endpoint4Input) =>
|
||||
raw["configure"]({
|
||||
params: { sessionID: input["sessionID"] },
|
||||
query: { dryRun: input["dryRun"] },
|
||||
headers: { traceID: input["traceID"] },
|
||||
payload: input["payload"],
|
||||
} as Endpoint4Request).pipe(Effect.mapError(mapEndpoint4Error))
|
||||
|
||||
export const adaptGroup0 = (raw: RawGroup) => ({
|
||||
health: Endpoint0(raw),
|
||||
list: Endpoint1(raw),
|
||||
get: Endpoint2(raw),
|
||||
interrupt: Endpoint3(raw),
|
||||
configure: Endpoint4(raw),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user