perf(codemode): batch OpenAPI query parameters (#36978)

This commit is contained in:
Aiden Cline 2026-07-14 20:37:36 -05:00 committed by GitHub
commit ba1f3d3d32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 69 additions and 19 deletions

View file

@ -68,14 +68,16 @@ const buildRequest = (
}
let request = HttpClientRequest.make(plan.operation.method as HttpMethod.HttpMethod)(url)
const query: Array<readonly [string, string]> = []
for (const field of plan.fields) {
if (field.location !== "query") continue
const item = own(input, field.inputName)
if (item === undefined) continue
const serialized = serializeQuery(request, field, item)
const serialized = serializeQuery(field, item)
if (serialized instanceof ToolError) return yield* Effect.fail(serialized)
request = serialized
for (const parameter of serialized) query.push(parameter)
}
if (query.length > 0) request = HttpClientRequest.appendUrlParams(request, query)
request = HttpClientRequest.setHeaders(request, plan.headers)
for (const field of plan.fields) {
@ -246,40 +248,46 @@ const serializeSimple = (
}
const serializeQuery = (
request: HttpClientRequest.HttpClientRequest,
field: Plan["fields"][number],
value: unknown,
): HttpClientRequest.HttpClientRequest | ToolError => {
): ReadonlyArray<readonly [string, string]> | ToolError => {
if (field.style === "deepObject") {
if (!isRecord(value)) return toolError(`Deep-object parameter '${field.inputName}' must be an object.`)
return Object.entries(value).reduce<HttpClientRequest.HttpClientRequest | ToolError>((current, [name, item]) => {
if (current instanceof ToolError) return current
const parameters: Array<readonly [string, string]> = []
for (const [name, item] of Object.entries(value)) {
if (item === undefined || (item !== null && typeof item === "object")) {
return toolError(`Deep-object parameter '${field.inputName}' contains an unsupported nested value.`)
}
return HttpClientRequest.appendUrlParam(current, `${field.name}[${name}]`, String(item))
}, request)
parameters.push([`${field.name}[${name}]`, String(item)])
}
return parameters
}
if (Array.isArray(value)) {
const rendered = serializeSimple(field, value, String)
if (rendered instanceof ToolError) return rendered
if (!field.explode) return HttpClientRequest.appendUrlParam(request, field.name, rendered)
if (value.some((item) => item === undefined || (item !== null && typeof item === "object"))) {
return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`)
if (!field.explode) {
const rendered = serializeSimple(field, value, String)
return rendered instanceof ToolError ? rendered : [[field.name, rendered]]
}
return value.reduce((current, item) => HttpClientRequest.appendUrlParam(current, field.name, String(item)), request)
const parameters: Array<readonly [string, string]> = []
for (const item of value) {
if (item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
return toolError(`Parameter '${field.inputName}' contains an unsupported nested value.`)
}
parameters.push([field.name, String(item)])
}
return parameters
}
if (isRecord(value) && field.explode) {
return Object.entries(value).reduce<HttpClientRequest.HttpClientRequest | ToolError>((current, [name, item]) => {
if (current instanceof ToolError) return current
const parameters: Array<readonly [string, string]> = []
for (const [name, item] of Object.entries(value)) {
if (item === undefined || (item !== null && typeof item === "object")) {
return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`)
}
return HttpClientRequest.appendUrlParam(current, name, String(item))
}, request)
parameters.push([name, String(item)])
}
return parameters
}
const rendered = serializeSimple(field, value, String)
return rendered instanceof ToolError ? rendered : HttpClientRequest.appendUrlParam(request, field.name, rendered)
return rendered instanceof ToolError ? rendered : [[field.name, rendered]]
}
const readResponseBody = (

View file

@ -495,6 +495,48 @@ describe("OpenAPI.fromSpec", () => {
)
})
test("preserves ordered exploded and deep-object query parameters", async () => {
const client = recordingClient(() => json({ ok: true }))
const tool = toolAt(
OpenAPI.fromSpec({
baseUrl,
spec: singleOperation({
parameters: [
{ name: "tags", in: "query", style: "form", explode: true, schema: { type: "array" } },
{ name: "filter", in: "query", style: "form", explode: true, schema: { type: "object" } },
{ name: "location", in: "query", style: "deepObject", explode: true, schema: { type: "object" } },
],
}),
}).tools,
"test",
)
if (!Tool.isDefinition(tool)) throw new Error("test was not generated")
await Effect.runPromise(
tool
.run({
tags: ["first value", "second&value"],
filter: { state: "open now", page: 2 },
location: { directory: "/tmp/a b", workspace: "work&1" },
})
.pipe(Effect.provide(client.layer)),
)
expect(client.requests[0]?.url).toBe(
`${baseUrl}/test?tags=first+value&tags=second%26value&state=open+now&page=2&location%5Bdirectory%5D=%2Ftmp%2Fa+b&location%5Bworkspace%5D=work%261`,
)
await expect(
Effect.runPromise(tool.run({ tags: [{}] }).pipe(Effect.provide(client.layer))),
).rejects.toThrow("Parameter 'tags' contains an unsupported nested value.")
await expect(
Effect.runPromise(tool.run({ filter: { state: {} } }).pipe(Effect.provide(client.layer))),
).rejects.toThrow("Query parameter 'filter' contains an unsupported nested value.")
await expect(
Effect.runPromise(tool.run({ location: { directory: [] } }).pipe(Effect.provide(client.layer))),
).rejects.toThrow("Deep-object parameter 'location' contains an unsupported nested value.")
expect(client.requests).toHaveLength(1)
})
test("skips unsupported parameter encodings and malformed security", () => {
const result = OpenAPI.fromSpec({
baseUrl,