(groups: ReadonlyArray<Group>)
| 490 | } |
| 491 | |
| 492 | function renderPromiseClient(groups: ReadonlyArray<Group>) { |
| 493 | const imports = groups.flatMap((group) => |
| 494 | group.endpoints.flatMap((endpoint) => { |
| 495 | const prefix = promiseTypePrefix(group.identifier, endpoint.operation.name) |
| 496 | return [...(endpoint.operation.inputMode === "none" ? [] : [`${prefix}Input`]), `${prefix}Output`] |
| 497 | }), |
| 498 | ) |
| 499 | const fields = groups.map((group) => { |
| 500 | const methods = group.endpoints.map((endpoint) => { |
| 501 | const prefix = promiseTypePrefix(group.identifier, endpoint.operation.name) |
| 502 | const argument = |
| 503 | endpoint.operation.inputMode === "none" |
| 504 | ? "requestOptions?: RequestOptions" |
| 505 | : `input${endpoint.operation.inputMode === "optional" ? "?" : ""}: ${prefix}Input, requestOptions?: RequestOptions` |
| 506 | const path = promisePath(endpoint.endpoint.path, endpoint.input) |
| 507 | const access = (name: string) => |
| 508 | `input${endpoint.operation.inputMode === "optional" ? "?." : ""}[${JSON.stringify(name)}]` |
| 509 | const part = (source: InputField["source"]) => { |
| 510 | const inputs = endpoint.input.filter((field) => field.source === source) |
| 511 | return inputs.length === 0 |
| 512 | ? undefined |
| 513 | : `{ ${inputs.map((field) => `${JSON.stringify(field.name)}: ${access(field.name)}`).join(", ")} }` |
| 514 | } |
| 515 | const parts = [ |
| 516 | endpoint.query === undefined ? undefined : `query: ${part("query")}`, |
| 517 | endpoint.headers === undefined ? undefined : `headers: ${part("headers")}`, |
| 518 | endpoint.payloads.length === 0 ? undefined : `body: ${part("payload")}`, |
| 519 | ].filter((value): value is string => value !== undefined) |
| 520 | const declaredStatuses = [...new Set(endpoint.errors.map((error) => error.status))] |
| 521 | const descriptor = `{ method: ${JSON.stringify(endpoint.endpoint.method)}, path: ${path}${parts.length === 0 ? "" : `, ${parts.join(", ")}`}, successStatus: ${resolveHttpApiStatus(endpoint.successes[0].ast) ?? 200}, declaredStatuses: [${declaredStatuses.join(", ")}], empty: ${endpoint.operation.success === "void"} }` |
| 522 | if (endpoint.operation.success === "stream") { |
| 523 | const success = endpoint.successes[0] |
| 524 | if (!isStreamSchema(success) || success._tag !== "StreamSse" || success.sseMode !== "data") { |
| 525 | throw new GenerationError({ |
| 526 | reason: `Promise stream emission is not implemented: ${group.identifier}.${endpoint.endpoint.name}`, |
| 527 | }) |
| 528 | } |
| 529 | return `${JSON.stringify(endpoint.operation.name)}: (${argument}): AsyncIterable<${prefix}Output> => sse<${prefix}Output>(${descriptor}, requestOptions)` |
| 530 | } |
| 531 | const unwrap = endpoint.unwrapData ? ".then((value) => value.data)" : "" |
| 532 | return `${JSON.stringify(endpoint.operation.name)}: (${argument}) => request<${endpoint.unwrapData ? `{ readonly data: ${prefix}Output }` : `${prefix}Output`}>(${descriptor}, requestOptions)${unwrap}` |
| 533 | }) |
| 534 | if (group.endpoints[0]?.topLevel) return methods.join(", ") |
| 535 | return `${JSON.stringify(group.identifier)}: { ${methods.join(", ")} }` |
| 536 | }) |
| 537 | return `import type { ${imports.join(", ")} } from "./types"\nimport { ClientError } from "./client-error"\n\nexport interface ClientOptions {\n readonly baseUrl: string\n readonly fetch?: typeof globalThis.fetch\n readonly headers?: HeadersInit\n}\n\nexport interface RequestOptions {\n readonly signal?: AbortSignal\n readonly headers?: HeadersInit\n}\n\ninterface RequestDescriptor {\n readonly method: string\n readonly path: string\n readonly query?: Record<string, unknown>\n readonly headers?: Record<string, unknown>\n readonly body?: unknown\n readonly successStatus: number\n readonly declaredStatuses: ReadonlyArray<number>\n readonly empty: boolean\n}\n\nexport function make(options: ClientOptions) {\n const fetch = options.fetch ?? globalThis.fetch\n\n const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {\n const url = new URL(descriptor.path, options.baseUrl)\n for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value)\n const headers = new Headers(options.headers)\n for (const [key, value] of Object.entries(descriptor.headers ?? {})) {\n if (value !== undefined && value !== null) headers.set(key, String(value))\n }\n for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value)\n if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json")\n return {\n url,\n init: {\n method: descriptor.method,\n signal: requestOptions?.signal,\n headers,\n body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body),\n } satisfies RequestInit,\n }\n }\n\n const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {\n try {\n const prepared = prepare(descriptor, requestOptions)\n return await fetch(prepared.url, prepared.init)\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n }\n\n const responseError = async (response: Response, descriptor: RequestDescriptor): Promise<never> => {\n if (descriptor.declaredStatuses.includes(response.status)) throw await json(response)\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnexpectedStatus", { cause: { status: response.status } })\n }\n\n const request = async <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise<A> => {\n const response = await execute(descriptor, requestOptions)\n if (response.status !== descriptor.successStatus) return responseError(response, descriptor)\n if (descriptor.empty) {\n try {\n await response.body?.cancel()\n } catch {}\n return undefined as A\n }\n return await json(response) as A\n }\n\n const sse = <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable<A> => ({\n async *[Symbol.asyncIterator]() {\n const response = await execute(descriptor, requestOptions)\n if (response.status !== descriptor.successStatus) await responseError(response, descriptor)\n if (!isContentType(response, "text/event-stream")) {\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnsupportedContentType")\n }\n if (response.body === null) throw new ClientError("MalformedResponse")\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ""\n try {\n while (true) {\n let next: ReadableStreamReadResult<Uint8Array>\n try {\n next = await reader.read()\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n buffer += decoder.decode(next.value, { stream: !next.done })\n if (buffer.length > 1_048_576) throw new ClientError("MalformedResponse")\n const trailingCarriageReturn = !next.done && buffer.endsWith("\\r")\n if (trailingCarriageReturn) buffer = buffer.slice(0, -1)\n buffer = buffer.replaceAll("\\r\\n", "\\n").replaceAll("\\r", "\\n")\n if (trailingCarriageReturn) buffer += "\\r"\n if (next.done && buffer !== "") buffer += "\\n\\n"\n let boundary = buffer.indexOf("\\n\\n")\n while (boundary >= 0) {\n const block = buffer.slice(0, boundary)\n buffer = buffer.slice(boundary + 2)\n const data = block.split("\\n").flatMap((line) => line.startsWith("data:") ? [line.slice(5).trimStart()] : []).join("\\n")\n if (data !== "") {\n try {\n yield JSON.parse(data) as A\n } catch (cause) {\n throw new ClientError("MalformedResponse", { cause })\n }\n }\n boundary = buffer.indexOf("\\n\\n")\n }\n if (next.done) return\n }\n } finally {\n try {\n await reader.cancel()\n } catch {}\n reader.releaseLock()\n }\n },\n })\n\n return { ${fields.join(", ")} }\n}\n\nfunction appendQuery(params: URLSearchParams, key: string, value: unknown): void {\n if (value === undefined || value === null) return\n if (Array.isArray(value)) {\n for (const item of value) appendQuery(params, key, item)\n return\n }\n if (typeof value === "object") {\n for (const [child, item] of Object.entries(value)) appendQuery(params, \`\${key}[\${child}]\`, item)\n return\n }\n params.append(key, String(value))\n}\n\nasync function json(response: Response): Promise<unknown> {\n if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) {\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnsupportedContentType")\n }\n let text: string\n try {\n text = await response.text()\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n if (text === "") throw new ClientError("MalformedResponse")\n try {\n return JSON.parse(text)\n } catch (cause) {\n throw new ClientError("MalformedResponse", { cause })\n }\n}\n\nfunction isContentType(response: Response, expected: string) {\n return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected\n}\n` |
| 538 | } |
| 539 | |
| 540 | function promiseTypePrefix(group: string, endpoint: string) { |
| 541 | return `${identifierPart(group)}${identifierPart(endpoint)}` |
no test coverage detected