perf(codemode): batch OpenAPI query parameters (#36978)
This commit is contained in:
parent
05fdbcce04
commit
ba1f3d3d32
2 changed files with 69 additions and 19 deletions
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue