From 2ccd9b898aeba53dd9e30499050244d08e62bcff Mon Sep 17 00:00:00 2001
From: "maxholman[bot]" <321308195+maxholman[bot]@users.noreply.github.com>
Date: Sun, 27 Sep 2026 14:44:32 +0800
Subject: [PATCH 1/6] feat!: emit sequential media commands from itemSchema
A 2xx response whose media type has an OAS 3.2 itemSchema, and appears in
the sequentialMediaTypes table, becomes a sequential command. For
text/event-stream it extends rest-client's EventStreamCommand . Output is the union of the item variants' `data` contentSchema.
The contentMediaType picks a decoding from the dataDecodings table: JSON is
rest-client's default, and text/plain sets `dataTransformer =
textDataTransformer`. Constructs that don't fit throw at codegen and name
the operation. AllOutputs carries ParsedStreamEvent, so
client.stream() accepts the command.
Validated subclasses now set an instance field, `responseSchema` or
`dataSchema`, in place of `static responseSchema`, as rest-client's parse
hooks read it.
A text/event-stream response with no itemSchema is a plain Command again.
It is no longer typed as Uint8Array chunks.
A command with a non-JSON body now widens only the parameters beside the
body, in place of Except/Pick around the whole input.
Co-Authored-By: LLM
---
.../__snapshots__/event-stream.test.ts.snap | 188 +++++++++
.../__snapshots__/nullables.test.ts.snap | 2 +-
__tests__/event-stream.test.ts | 180 +++++++++
.../docker/.openapi-codegen-manifest.json | 8 +-
.../fixtures/docker/commands-validated.ts | 162 +++++---
__tests__/fixtures/docker/commands.ts | 57 +--
__tests__/fixtures/docker/main.ts | 40 +-
.../openai/.openapi-codegen-manifest.json | 8 +-
.../fixtures/openai/commands-validated.ts | 269 ++++++++-----
__tests__/fixtures/openai/commands.ts | 59 +--
__tests__/fixtures/openai/main.ts | 24 +-
.../petstore/.openapi-codegen-manifest.json | 4 +-
.../fixtures/petstore/commands-validated.ts | 11 +-
__tests__/fixtures/test1.json | 76 +++-
.../test1/.openapi-codegen-manifest.json | 14 +-
.../fixtures/test1/commands-validated.ts | 49 ++-
__tests__/fixtures/test1/commands.ts | 41 +-
__tests__/fixtures/test1/hono.ts | 4 +
__tests__/fixtures/test1/main.ts | 14 +-
__tests__/fixtures/test1/types.ts | 10 +
__tests__/fixtures/test1/valibot.ts | 18 +
__tests__/test1.test.ts | 52 ++-
lib/hono.ts | 7 +-
lib/process-document.ts | 373 ++++++++++++++++--
lib/valibot.ts | 5 +-
25 files changed, 1329 insertions(+), 346 deletions(-)
create mode 100644 __tests__/__snapshots__/event-stream.test.ts.snap
create mode 100644 __tests__/event-stream.test.ts
diff --git a/__tests__/__snapshots__/event-stream.test.ts.snap b/__tests__/__snapshots__/event-stream.test.ts.snap
new file mode 100644
index 0000000..ef9b9cc
--- /dev/null
+++ b/__tests__/__snapshots__/event-stream.test.ts.snap
@@ -0,0 +1,188 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`JSON data variants make the output a union > commands.ts 1`] = `
+"import { EventStreamCommand } from "@block65/rest-client";
+import type { UndefinedOnPartialDeep } from "type-fest";
+import type { StreamEventsCommandInput, StreamEventsCommandOutput } from "./types.js";
+
+
+/**
+ * StreamEventsCommand
+ *
+ */
+export class StreamEventsCommand extends EventStreamCommand, StreamEventsCommandOutput> {
+ public override method = "get" as const;
+
+ constructor() {
+ super("/events");
+ }
+}
+"
+`;
+
+exports[`JSON data variants make the output a union > commands-validated.ts 1`] = `
+"import * as commands from "./commands.js";
+import * as schemas from "./valibot.js";
+
+export class StreamEventsCommand extends commands.StreamEventsCommand {
+ public override readonly dataSchema = schemas.streamEventsCommandDataSchema;
+}
+"
+`;
+
+exports[`JSON data variants make the output a union > main.ts 1`] = `
+"import { RestServiceClient, type ParsedStreamEvent, type RestServiceClientConfig } from "@block65/rest-client";
+import type { StreamEventsCommandOutput } from "./types.js";
+
+export { ResponseValidationError } from "@block65/rest-client";
+
+type AllOutputs = ParsedStreamEvent;
+
+export class TestRestClient extends RestServiceClient {
+ constructor(baseUrl: string | URL = new URL('https://api.example.com/'), config?: RestServiceClientConfig) {
+ super(baseUrl, config);
+ }
+}
+"
+`;
+
+exports[`JSON data variants make the output a union > types.ts 1`] = `
+"export type Thing = {
+ "id": string;
+ };
+export type StreamEventsCommandInput = never;
+export type StreamEventsCommandOutput = Thing | {
+ "sequence": number;
+ };
+"
+`;
+
+exports[`JSON data variants make the output a union > valibot.ts 1`] = `
+"import * as v from "valibot";
+export const inputThingSchema = v.strictObject(
+ {
+ "id": v.string()
+ ,
+ });
+export const thingSchema = v.strictObject(
+ {
+ "id": v.pipe(v.string(), v.trim())
+ ,
+ });
+export const inputStreamEventsCommandDataSchema = v.union([inputThingSchema, v.strictObject(
+ {
+ "sequence": v.pipe(v.number(), v.integer())
+ ,
+ })]);
+export const streamEventsCommandDataSchema = v.union([thingSchema, v.strictObject(
+ {
+ "sequence": v.pipe(v.number(), v.integer())
+ ,
+ })]);
+"
+`;
+
+exports[`data with no contentMediaType is text > commands.ts 1`] = `
+"import { EventStreamCommand, textDataTransformer } from "@block65/rest-client";
+import type { UndefinedOnPartialDeep } from "type-fest";
+import type { StreamEventsCommandInput, StreamEventsCommandOutput } from "./types.js";
+
+
+/**
+ * StreamEventsCommand
+ *
+ */
+export class StreamEventsCommand extends EventStreamCommand, StreamEventsCommandOutput> {
+ public override method = "get" as const;
+ public override readonly dataTransformer = textDataTransformer;
+
+ constructor() {
+ super("/events");
+ }
+}
+"
+`;
+
+exports[`data with no contentMediaType is text > commands-validated.ts 1`] = `
+"import * as commands from "./commands.js";
+import * as schemas from "./valibot.js";
+
+export class StreamEventsCommand extends commands.StreamEventsCommand {
+ public override readonly dataSchema = schemas.streamEventsCommandDataSchema;
+}
+"
+`;
+
+exports[`data with no contentMediaType is text > main.ts 1`] = `
+"import { RestServiceClient, type ParsedStreamEvent, type RestServiceClientConfig } from "@block65/rest-client";
+import type { StreamEventsCommandOutput } from "./types.js";
+
+export { ResponseValidationError } from "@block65/rest-client";
+
+type AllOutputs = ParsedStreamEvent;
+
+export class TestRestClient extends RestServiceClient {
+ constructor(baseUrl: string | URL = new URL('https://api.example.com/'), config?: RestServiceClientConfig) {
+ super(baseUrl, config);
+ }
+}
+"
+`;
+
+exports[`data with no contentMediaType is text > types.ts 1`] = `
+"export type StreamEventsCommandInput = never;
+export type StreamEventsCommandOutput = string;
+"
+`;
+
+exports[`data with no contentMediaType is text > valibot.ts 1`] = `
+"import * as v from "valibot";
+export const inputStreamEventsCommandDataSchema = v.string();
+export const streamEventsCommandDataSchema = v.pipe(v.string(), v.trim());
+"
+`;
+
+exports[`without an itemSchema, a plain command > commands.ts 1`] = `
+"import { Command } from "@block65/rest-client";
+import type { UndefinedOnPartialDeep } from "type-fest";
+import type { StreamEventsCommandInput } from "./types.js";
+
+
+/**
+ * StreamEventsCommand
+ *
+ */
+export class StreamEventsCommand extends Command> {
+ public override method = "get" as const;
+
+ constructor() {
+ super("/events");
+ }
+}
+"
+`;
+
+exports[`without an itemSchema, a plain command > commands-validated.ts 1`] = `
+"export { StreamEventsCommand } from "./commands.js";
+"
+`;
+
+exports[`without an itemSchema, a plain command > main.ts 1`] = `
+"import { RestServiceClient, type RestServiceClientConfig } from "@block65/rest-client";
+
+export { ResponseValidationError } from "@block65/rest-client";
+
+export class TestRestClient extends RestServiceClient {
+ constructor(baseUrl: string | URL = new URL('https://api.example.com/'), config?: RestServiceClientConfig) {
+ super(baseUrl, config);
+ }
+}
+"
+`;
+
+exports[`without an itemSchema, a plain command > types.ts 1`] = `
+"export type StreamEventsCommandInput = never;
+"
+`;
+
+exports[`without an itemSchema, a plain command > valibot.ts 1`] = `""`;
diff --git a/__tests__/__snapshots__/nullables.test.ts.snap b/__tests__/__snapshots__/nullables.test.ts.snap
index bc0ae30..4ff6d6f 100644
--- a/__tests__/__snapshots__/nullables.test.ts.snap
+++ b/__tests__/__snapshots__/nullables.test.ts.snap
@@ -169,7 +169,7 @@ exports[`header parameters > commands-validated.ts 1`] = `
import * as schemas from "./valibot.js";
export class UploadDataCommand extends commands.UploadDataCommand {
- static responseSchema = schemas.uploadDataCommandResponseSchema;
+ public override readonly responseSchema = schemas.uploadDataCommandResponseSchema;
}
"
`;
diff --git a/__tests__/event-stream.test.ts b/__tests__/event-stream.test.ts
new file mode 100644
index 0000000..c8cfbf5
--- /dev/null
+++ b/__tests__/event-stream.test.ts
@@ -0,0 +1,180 @@
+import type { oas31 } from "openapi3-ts";
+import { describe, expect, test } from "vitest";
+import { processOpenApiDocument } from "../lib/process-document.ts";
+
+// OAS 3.2 added itemSchema, which the 3.1 types predate
+type ItemSchema = oas31.SchemaObject | oas31.ReferenceObject;
+
+function documentWith(
+ itemSchema: ItemSchema | undefined,
+ schemas: Record = {},
+) {
+ const document: oas31.OpenAPIObject = {
+ openapi: "3.2.0",
+ info: { title: "Test", version: "1.0.0" },
+ components: { schemas },
+ paths: {
+ "/events": {
+ get: {
+ operationId: "streamEventsCommand",
+ responses: {
+ "200": {
+ description: "OK",
+ content: {
+ "text/event-stream": itemSchema
+ ? ({ itemSchema } as oas31.MediaTypeObject)
+ : { schema: { type: "string", format: "binary" } },
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+
+ return document;
+}
+
+function jsonData(contentSchema: ItemSchema) {
+ return {
+ type: "string" as const,
+ contentMediaType: "application/json",
+ contentSchema,
+ };
+}
+
+test("without an itemSchema, a plain command", async () => {
+ const result = await processOpenApiDocument(
+ "/tmp/whatever",
+ documentWith(undefined),
+ );
+
+ expect(result.commandsFile.getText()).toMatchSnapshot("commands.ts");
+ expect(result.commandsValidatedFile.getText()).toMatchSnapshot(
+ "commands-validated.ts",
+ );
+ expect(result.typesFile.getText()).toMatchSnapshot("types.ts");
+ expect(result.mainFile.getText()).toMatchSnapshot("main.ts");
+ expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts");
+});
+
+test("JSON data variants make the output a union", async () => {
+ const result = await processOpenApiDocument(
+ "/tmp/whatever",
+ documentWith(
+ {
+ oneOf: [
+ {
+ type: "object",
+ properties: {
+ event: { const: "thing" },
+ data: jsonData({ $ref: "#/components/schemas/Thing" }),
+ },
+ },
+ {
+ type: "object",
+ properties: {
+ event: { const: "heartbeat" },
+ data: jsonData({
+ type: "object",
+ required: ["sequence"],
+ properties: { sequence: { type: "integer" } },
+ additionalProperties: false,
+ }),
+ },
+ },
+ ],
+ },
+ {
+ Thing: {
+ type: "object",
+ required: ["id"],
+ properties: { id: { type: "string" } },
+ additionalProperties: false,
+ },
+ },
+ ),
+ );
+
+ expect(result.commandsFile.getText()).toMatchSnapshot("commands.ts");
+ expect(result.commandsValidatedFile.getText()).toMatchSnapshot(
+ "commands-validated.ts",
+ );
+ expect(result.typesFile.getText()).toMatchSnapshot("types.ts");
+ expect(result.mainFile.getText()).toMatchSnapshot("main.ts");
+ expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts");
+});
+
+test("data with no contentMediaType is text", async () => {
+ const result = await processOpenApiDocument(
+ "/tmp/whatever",
+ documentWith({
+ type: "object",
+ properties: { data: { type: "string" } },
+ }),
+ );
+
+ expect(result.commandsFile.getText()).toMatchSnapshot("commands.ts");
+ expect(result.commandsValidatedFile.getText()).toMatchSnapshot(
+ "commands-validated.ts",
+ );
+ expect(result.typesFile.getText()).toMatchSnapshot("types.ts");
+ expect(result.mainFile.getText()).toMatchSnapshot("main.ts");
+ expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts");
+});
+
+describe("event-stream refusals", () => {
+ test.each<[string, ItemSchema, RegExp]>([
+ [
+ "variants with different content media types",
+ {
+ oneOf: [
+ {
+ type: "object",
+ properties: { data: jsonData({ type: "string" }) },
+ },
+ { type: "object", properties: { data: { type: "string" } } },
+ ],
+ },
+ /streamEventsCommand: item content media types application\/json, text\/plain need one decoding/,
+ ],
+ [
+ "a variant with no data property",
+ {
+ type: "object",
+ properties: { data: jsonData({ type: "string" }) },
+ oneOf: [
+ { type: "object", properties: { event: { const: "a" } } },
+ { type: "object", properties: { event: { const: "b" } } },
+ ],
+ },
+ /streamEventsCommand: each itemSchema variant needs its own data schema/,
+ ],
+ [
+ "an allOf item",
+ {
+ allOf: [
+ {
+ type: "object",
+ properties: { data: jsonData({ type: "string" }) },
+ },
+ ],
+ },
+ /streamEventsCommand: each itemSchema variant needs its own data schema/,
+ ],
+ [
+ "JSON data with no contentSchema",
+ {
+ type: "object",
+ properties: {
+ data: { type: "string", contentMediaType: "application/json" },
+ },
+ },
+ /streamEventsCommand: item content of application\/json needs a contentSchema/,
+ ],
+ ])("%s", async (_, itemSchema, message) => {
+ await expect(
+ processOpenApiDocument("/tmp/whatever", documentWith(itemSchema)),
+ ).rejects.toThrow(message);
+ });
+});
diff --git a/__tests__/fixtures/docker/.openapi-codegen-manifest.json b/__tests__/fixtures/docker/.openapi-codegen-manifest.json
index 24a8e04..b545407 100644
--- a/__tests__/fixtures/docker/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/docker/.openapi-codegen-manifest.json
@@ -1,10 +1,10 @@
{
- "#generator": "cf9ab7bd8a80e3339d9237d5dee0deb3",
- "commands.ts": "137aa1b06856ca44a0c780958480af81",
+ "#generator": "99360df640587900347f0725f28f06a7",
+ "commands.ts": "0971e86b831d9a354c1e219ecf7e8b7f",
"types.ts": "f1e7d6c61bb9e15c5034a3cc3cc729b5",
- "main.ts": "0f596fab7f6e9bb140fcd133caccb38d",
+ "main.ts": "38065305823906f3aa1c7f968e278002",
"valibot.ts": "25d47efaf15282d72c20aed794bd32da",
"hono.ts": "4dee43760e6f999bf0da89d4c063bcd4",
- "commands-validated.ts": "c6066f3a4ead3dadfad70443e923a265",
+ "commands-validated.ts": "633c805329787be3de004224eeb86e5c",
"enums.ts": "6d2420ab590db68349e2e8463a1b026d"
}
diff --git a/__tests__/fixtures/docker/commands-validated.ts b/__tests__/fixtures/docker/commands-validated.ts
index 0c07c72..1eaad39 100644
--- a/__tests__/fixtures/docker/commands-validated.ts
+++ b/__tests__/fixtures/docker/commands-validated.ts
@@ -8,223 +8,277 @@ import * as commands from "./commands.js";
import * as schemas from "./valibot.js";
export class ContainerListCommand extends commands.ContainerListCommand {
- static responseSchema = schemas.containerListCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.containerListCommandResponseSchema;
}
export class ContainerCreateCommand extends commands.ContainerCreateCommand {
- static responseSchema = schemas.containerCreateCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.containerCreateCommandResponseSchema;
}
export class ContainerInspectCommand extends commands.ContainerInspectCommand {
- static responseSchema = schemas.containerInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.containerInspectCommandResponseSchema;
}
export class ContainerTopCommand extends commands.ContainerTopCommand {
- static responseSchema = schemas.containerTopCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.containerTopCommandResponseSchema;
}
export class ContainerChangesCommand extends commands.ContainerChangesCommand {
- static responseSchema = schemas.containerChangesCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.containerChangesCommandResponseSchema;
}
export class ContainerStatsCommand extends commands.ContainerStatsCommand {
- static responseSchema = schemas.containerStatsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.containerStatsCommandResponseSchema;
}
export class ContainerUpdateCommand extends commands.ContainerUpdateCommand {
- static responseSchema = schemas.containerUpdateCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.containerUpdateCommandResponseSchema;
}
export class ContainerWaitCommand extends commands.ContainerWaitCommand {
- static responseSchema = schemas.containerWaitCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.containerWaitCommandResponseSchema;
}
export class ContainerPruneCommand extends commands.ContainerPruneCommand {
- static responseSchema = schemas.containerPruneCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.containerPruneCommandResponseSchema;
}
export class ImageListCommand extends commands.ImageListCommand {
- static responseSchema = schemas.imageListCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.imageListCommandResponseSchema;
}
export class BuildPruneCommand extends commands.BuildPruneCommand {
- static responseSchema = schemas.buildPruneCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.buildPruneCommandResponseSchema;
}
export class ImageInspectCommand extends commands.ImageInspectCommand {
- static responseSchema = schemas.imageInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.imageInspectCommandResponseSchema;
}
export class ImageHistoryCommand extends commands.ImageHistoryCommand {
- static responseSchema = schemas.imageHistoryCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.imageHistoryCommandResponseSchema;
}
export class ImageDeleteCommand extends commands.ImageDeleteCommand {
- static responseSchema = schemas.imageDeleteCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.imageDeleteCommandResponseSchema;
}
export class ImageSearchCommand extends commands.ImageSearchCommand {
- static responseSchema = schemas.imageSearchCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.imageSearchCommandResponseSchema;
}
export class ImagePruneCommand extends commands.ImagePruneCommand {
- static responseSchema = schemas.imagePruneCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.imagePruneCommandResponseSchema;
}
export class SystemAuthCommand extends commands.SystemAuthCommand {
- static responseSchema = schemas.systemAuthCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.systemAuthCommandResponseSchema;
}
export class SystemInfoCommand extends commands.SystemInfoCommand {
- static responseSchema = schemas.systemInfoCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.systemInfoCommandResponseSchema;
}
export class SystemVersionCommand extends commands.SystemVersionCommand {
- static responseSchema = schemas.systemVersionCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.systemVersionCommandResponseSchema;
}
export class ImageCommitCommand extends commands.ImageCommitCommand {
- static responseSchema = schemas.imageCommitCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.imageCommitCommandResponseSchema;
}
export class SystemEventsCommand extends commands.SystemEventsCommand {
- static responseSchema = schemas.systemEventsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.systemEventsCommandResponseSchema;
}
export class SystemDataUsageCommand extends commands.SystemDataUsageCommand {
- static responseSchema = schemas.systemDataUsageCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.systemDataUsageCommandResponseSchema;
}
export class ContainerExecCommand extends commands.ContainerExecCommand {
- static responseSchema = schemas.containerExecCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.containerExecCommandResponseSchema;
}
export class ExecInspectCommand extends commands.ExecInspectCommand {
- static responseSchema = schemas.execInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.execInspectCommandResponseSchema;
}
export class VolumeListCommand extends commands.VolumeListCommand {
- static responseSchema = schemas.volumeListCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.volumeListCommandResponseSchema;
}
export class VolumeCreateCommand extends commands.VolumeCreateCommand {
- static responseSchema = schemas.volumeCreateCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.volumeCreateCommandResponseSchema;
}
export class VolumeInspectCommand extends commands.VolumeInspectCommand {
- static responseSchema = schemas.volumeInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.volumeInspectCommandResponseSchema;
}
export class VolumePruneCommand extends commands.VolumePruneCommand {
- static responseSchema = schemas.volumePruneCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.volumePruneCommandResponseSchema;
}
export class NetworkListCommand extends commands.NetworkListCommand {
- static responseSchema = schemas.networkListCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.networkListCommandResponseSchema;
}
export class NetworkInspectCommand extends commands.NetworkInspectCommand {
- static responseSchema = schemas.networkInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.networkInspectCommandResponseSchema;
}
export class NetworkCreateCommand extends commands.NetworkCreateCommand {
- static responseSchema = schemas.networkCreateCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.networkCreateCommandResponseSchema;
}
export class NetworkPruneCommand extends commands.NetworkPruneCommand {
- static responseSchema = schemas.networkPruneCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.networkPruneCommandResponseSchema;
}
export class PluginListCommand extends commands.PluginListCommand {
- static responseSchema = schemas.pluginListCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.pluginListCommandResponseSchema;
}
export class GetPluginPrivilegesCommand
extends commands.GetPluginPrivilegesCommand
{
- static responseSchema = schemas.getPluginPrivilegesCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getPluginPrivilegesCommandResponseSchema;
}
export class PluginInspectCommand extends commands.PluginInspectCommand {
- static responseSchema = schemas.pluginInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.pluginInspectCommandResponseSchema;
}
export class PluginDeleteCommand extends commands.PluginDeleteCommand {
- static responseSchema = schemas.pluginDeleteCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.pluginDeleteCommandResponseSchema;
}
export class NodeListCommand extends commands.NodeListCommand {
- static responseSchema = schemas.nodeListCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.nodeListCommandResponseSchema;
}
export class NodeInspectCommand extends commands.NodeInspectCommand {
- static responseSchema = schemas.nodeInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.nodeInspectCommandResponseSchema;
}
export class SwarmInspectCommand extends commands.SwarmInspectCommand {
- static responseSchema = schemas.swarmInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.swarmInspectCommandResponseSchema;
}
export class SwarmInitCommand extends commands.SwarmInitCommand {
- static responseSchema = schemas.swarmInitCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.swarmInitCommandResponseSchema;
}
export class SwarmUnlockkeyCommand extends commands.SwarmUnlockkeyCommand {
- static responseSchema = schemas.swarmUnlockkeyCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.swarmUnlockkeyCommandResponseSchema;
}
export class ServiceListCommand extends commands.ServiceListCommand {
- static responseSchema = schemas.serviceListCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.serviceListCommandResponseSchema;
}
export class ServiceCreateCommand extends commands.ServiceCreateCommand {
- static responseSchema = schemas.serviceCreateCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.serviceCreateCommandResponseSchema;
}
export class ServiceInspectCommand extends commands.ServiceInspectCommand {
- static responseSchema = schemas.serviceInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.serviceInspectCommandResponseSchema;
}
export class ServiceUpdateCommand extends commands.ServiceUpdateCommand {
- static responseSchema = schemas.serviceUpdateCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.serviceUpdateCommandResponseSchema;
}
export class TaskListCommand extends commands.TaskListCommand {
- static responseSchema = schemas.taskListCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.taskListCommandResponseSchema;
}
export class TaskInspectCommand extends commands.TaskInspectCommand {
- static responseSchema = schemas.taskInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.taskInspectCommandResponseSchema;
}
export class SecretListCommand extends commands.SecretListCommand {
- static responseSchema = schemas.secretListCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.secretListCommandResponseSchema;
}
export class SecretCreateCommand extends commands.SecretCreateCommand {
- static responseSchema = schemas.secretCreateCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.secretCreateCommandResponseSchema;
}
export class SecretInspectCommand extends commands.SecretInspectCommand {
- static responseSchema = schemas.secretInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.secretInspectCommandResponseSchema;
}
export class ConfigListCommand extends commands.ConfigListCommand {
- static responseSchema = schemas.configListCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.configListCommandResponseSchema;
}
export class ConfigCreateCommand extends commands.ConfigCreateCommand {
- static responseSchema = schemas.configCreateCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.configCreateCommandResponseSchema;
}
export class ConfigInspectCommand extends commands.ConfigInspectCommand {
- static responseSchema = schemas.configInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.configInspectCommandResponseSchema;
}
export class DistributionInspectCommand
extends commands.DistributionInspectCommand
{
- static responseSchema = schemas.distributionInspectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.distributionInspectCommandResponseSchema;
}
export {
diff --git a/__tests__/fixtures/docker/commands.ts b/__tests__/fixtures/docker/commands.ts
index 71656b1..9ef6cdd 100644
--- a/__tests__/fixtures/docker/commands.ts
+++ b/__tests__/fixtures/docker/commands.ts
@@ -12,7 +12,7 @@ import {
jsonStringify,
formJoinSerializer,
} from "@block65/rest-client";
-import type { Except, UndefinedOnPartialDeep } from "type-fest";
+import type { UndefinedOnPartialDeep } from "type-fest";
import type {
ContainerListCommandQuery,
ContainerListCommandInput,
@@ -62,7 +62,8 @@ import type {
ContainerArchiveCommandQuery,
ContainerArchiveCommandInput,
PutContainerArchiveCommandQuery,
- PutContainerArchiveCommandInput,
+ PutContainerArchiveCommandBodyNonJson,
+ PutContainerArchiveCommandParams,
ContainerArchiveInfoCommandQuery,
ContainerArchiveInfoCommandInput,
ContainerPruneCommandQuery,
@@ -73,13 +74,13 @@ import type {
ImageListCommandOutput,
ImageBuildCommandQuery,
ImageBuildCommandHeader,
- ImageBuildCommandInput,
+ ImageBuildCommandBodyNonJson,
BuildPruneCommandQuery,
BuildPruneCommandInput,
BuildPruneCommandOutput,
ImageCreateCommandQuery,
ImageCreateCommandHeader,
- ImageCreateCommandInput,
+ ImageCreateCommandBodyNonJson,
ImageInspectCommandInput,
ImageInspectCommandOutput,
ImageHistoryCommandInput,
@@ -119,7 +120,7 @@ import type {
ImageGetAllCommandQuery,
ImageGetAllCommandInput,
ImageLoadCommandQuery,
- ImageLoadCommandInput,
+ ImageLoadCommandBodyNonJson,
ContainerExecCommandInput,
ContainerExecCommandOutput,
ExecStartCommandInput,
@@ -177,7 +178,7 @@ import type {
PluginUpgradeCommandHeader,
PluginUpgradeCommandInput,
PluginCreateCommandQuery,
- PluginCreateCommandInput,
+ PluginCreateCommandBodyNonJson,
PluginPushCommandInput,
PluginSetCommandInput,
NodeListCommandQuery,
@@ -886,18 +887,20 @@ export class ContainerArchiveCommand extends Command<
* @summary Extract an archive of files or folders to a directory in a container
*/
export class PutContainerArchiveCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ PutContainerArchiveCommandBodyNonJson &
+ UndefinedOnPartialDeep<
+ PutContainerArchiveCommandParams & PutContainerArchiveCommandQuery
+ >,
unknown,
PutContainerArchiveCommandQuery
> {
public override method = "put" as const;
constructor(
- input: UndefinedOnPartialDeep<
- Except
- > &
- Pick,
+ input: PutContainerArchiveCommandBodyNonJson &
+ UndefinedOnPartialDeep<
+ PutContainerArchiveCommandParams & PutContainerArchiveCommandQuery
+ >,
) {
const { id, path, noOverwriteDirNonDir, copyUIDGID, body } = input;
super(
@@ -993,8 +996,7 @@ export class ImageListCommand extends Command<
* @summary Build an image
*/
export class ImageBuildCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ ImageBuildCommandBodyNonJson & UndefinedOnPartialDeep,
unknown,
ImageBuildCommandQuery,
ImageBuildCommandHeader
@@ -1002,8 +1004,8 @@ export class ImageBuildCommand extends Command<
public override method = "post" as const;
constructor(
- input: UndefinedOnPartialDeep> &
- Pick,
+ input: ImageBuildCommandBodyNonJson &
+ UndefinedOnPartialDeep,
headers?: ImageBuildCommandHeader,
) {
const {
@@ -1097,8 +1099,8 @@ export class BuildPruneCommand extends Command<
* @summary Create an image
*/
export class ImageCreateCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ ImageCreateCommandBodyNonJson &
+ UndefinedOnPartialDeep,
unknown,
ImageCreateCommandQuery,
ImageCreateCommandHeader
@@ -1107,8 +1109,8 @@ export class ImageCreateCommand extends Command<
public override querySerializer = formJoinSerializer;
constructor(
- input: UndefinedOnPartialDeep> &
- Pick,
+ input: ImageCreateCommandBodyNonJson &
+ UndefinedOnPartialDeep,
headers?: ImageCreateCommandHeader,
) {
const { fromImage, fromSrc, repo, tag, message, changes, platform, body } =
@@ -1542,16 +1544,15 @@ export class ImageGetAllCommand extends Command<
* @summary Import images
*/
export class ImageLoadCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ ImageLoadCommandBodyNonJson & UndefinedOnPartialDeep,
unknown,
ImageLoadCommandQuery
> {
public override method = "post" as const;
constructor(
- input: UndefinedOnPartialDeep> &
- Pick,
+ input: ImageLoadCommandBodyNonJson &
+ UndefinedOnPartialDeep,
) {
const { quiet, body } = input;
super("/images/load", body, stripUndefined({ quiet }));
@@ -2045,16 +2046,16 @@ export class PluginUpgradeCommand extends Command<
* @summary Create a plugin
*/
export class PluginCreateCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ PluginCreateCommandBodyNonJson &
+ UndefinedOnPartialDeep,
undefined,
PluginCreateCommandQuery
> {
public override method = "post" as const;
constructor(
- input: UndefinedOnPartialDeep> &
- Pick,
+ input: PluginCreateCommandBodyNonJson &
+ UndefinedOnPartialDeep,
) {
const { name, body } = input;
super("/plugins/create", body, stripUndefined({ name }));
diff --git a/__tests__/fixtures/docker/main.ts b/__tests__/fixtures/docker/main.ts
index bc30913..bb8ab10 100644
--- a/__tests__/fixtures/docker/main.ts
+++ b/__tests__/fixtures/docker/main.ts
@@ -8,7 +8,7 @@ import {
RestServiceClient,
type RestServiceClientConfig,
} from "@block65/rest-client";
-import type { Except, UndefinedOnPartialDeep } from "type-fest";
+import type { UndefinedOnPartialDeep } from "type-fest";
import type {
BuildPruneCommandInput,
BuildPruneCommandOutput,
@@ -63,10 +63,12 @@ import type {
ExecStartCommandInput,
GetPluginPrivilegesCommandInput,
GetPluginPrivilegesCommandOutput,
- ImageBuildCommandInput,
+ ImageBuildCommandBodyNonJson,
+ ImageBuildCommandQuery,
ImageCommitCommandInput,
ImageCommitCommandOutput,
- ImageCreateCommandInput,
+ ImageCreateCommandBodyNonJson,
+ ImageCreateCommandQuery,
ImageDeleteCommandInput,
ImageDeleteCommandOutput,
ImageGetAllCommandInput,
@@ -77,7 +79,8 @@ import type {
ImageInspectCommandOutput,
ImageListCommandInput,
ImageListCommandOutput,
- ImageLoadCommandInput,
+ ImageLoadCommandBodyNonJson,
+ ImageLoadCommandQuery,
ImagePruneCommandInput,
ImagePruneCommandOutput,
ImagePushCommandInput,
@@ -101,7 +104,8 @@ import type {
NodeListCommandInput,
NodeListCommandOutput,
NodeUpdateCommandInput,
- PluginCreateCommandInput,
+ PluginCreateCommandBodyNonJson,
+ PluginCreateCommandQuery,
PluginDeleteCommandInput,
PluginDeleteCommandOutput,
PluginDisableCommandInput,
@@ -114,7 +118,9 @@ import type {
PluginPushCommandInput,
PluginSetCommandInput,
PluginUpgradeCommandInput,
- PutContainerArchiveCommandInput,
+ PutContainerArchiveCommandBodyNonJson,
+ PutContainerArchiveCommandParams,
+ PutContainerArchiveCommandQuery,
SecretCreateCommandInput,
SecretCreateCommandOutput,
SecretDeleteCommandInput,
@@ -169,6 +175,18 @@ import type {
export { ResponseValidationError } from "@block65/rest-client";
type AllInputs =
+ | (ImageBuildCommandBodyNonJson &
+ UndefinedOnPartialDeep)
+ | (ImageCreateCommandBodyNonJson &
+ UndefinedOnPartialDeep)
+ | (ImageLoadCommandBodyNonJson &
+ UndefinedOnPartialDeep)
+ | (PluginCreateCommandBodyNonJson &
+ UndefinedOnPartialDeep)
+ | (PutContainerArchiveCommandBodyNonJson &
+ UndefinedOnPartialDeep<
+ PutContainerArchiveCommandParams & PutContainerArchiveCommandQuery
+ >)
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
@@ -201,16 +219,6 @@ type AllInputs =
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
- | (UndefinedOnPartialDeep> &
- Pick)
- | (UndefinedOnPartialDeep> &
- Pick)
- | (UndefinedOnPartialDeep> &
- Pick)
- | (UndefinedOnPartialDeep> &
- Pick)
- | (UndefinedOnPartialDeep> &
- Pick)
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
diff --git a/__tests__/fixtures/openai/.openapi-codegen-manifest.json b/__tests__/fixtures/openai/.openapi-codegen-manifest.json
index e6f86d9..4870177 100644
--- a/__tests__/fixtures/openai/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/openai/.openapi-codegen-manifest.json
@@ -1,10 +1,10 @@
{
- "#generator": "cf9ab7bd8a80e3339d9237d5dee0deb3",
- "commands.ts": "82d7434078d4f7ba525c36db35c4c33b",
+ "#generator": "99360df640587900347f0725f28f06a7",
+ "commands.ts": "99ac148005b23e09a53adb6b62620dc0",
"types.ts": "bc453b1a7fcd330909a23ae01be97f52",
- "main.ts": "5ba91c2efb44e3e5c5bd1e1a1b90bb51",
+ "main.ts": "8147604a37254400a015b78650466094",
"valibot.ts": "3be60d2491963c2904292438b7de247b",
"hono.ts": "54130437a23164b228769eb3ec967e4e",
- "commands-validated.ts": "e4929484f6a64b784ff7b03327f58a3a",
+ "commands-validated.ts": "9725b4bdcac4632927d011b9a54886d9",
"enums.ts": "87a5ccd7e57cc937104b2d256e8d67b1"
}
diff --git a/__tests__/fixtures/openai/commands-validated.ts b/__tests__/fixtures/openai/commands-validated.ts
index 4582139..75e4092 100644
--- a/__tests__/fixtures/openai/commands-validated.ts
+++ b/__tests__/fixtures/openai/commands-validated.ts
@@ -10,450 +10,533 @@ import * as schemas from "./valibot.js";
export class CreateChatCompletionCommand
extends commands.CreateChatCompletionCommand
{
- static responseSchema = schemas.createChatCompletionCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createChatCompletionCommandResponseSchema;
}
export class CreateCompletionCommand extends commands.CreateCompletionCommand {
- static responseSchema = schemas.createCompletionCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createCompletionCommandResponseSchema;
}
export class CreateImageCommand extends commands.CreateImageCommand {
- static responseSchema = schemas.createImageCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createImageCommandResponseSchema;
}
export class CreateImageEditCommand extends commands.CreateImageEditCommand {
- static responseSchema = schemas.createImageEditCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createImageEditCommandResponseSchema;
}
export class CreateImageVariationCommand
extends commands.CreateImageVariationCommand
{
- static responseSchema = schemas.createImageVariationCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createImageVariationCommandResponseSchema;
}
export class CreateEmbeddingCommand extends commands.CreateEmbeddingCommand {
- static responseSchema = schemas.createEmbeddingCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createEmbeddingCommandResponseSchema;
}
export class CreateTranscriptionCommand
extends commands.CreateTranscriptionCommand
{
- static responseSchema = schemas.createTranscriptionCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createTranscriptionCommandResponseSchema;
}
export class CreateTranslationCommand
extends commands.CreateTranslationCommand
{
- static responseSchema = schemas.createTranslationCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createTranslationCommandResponseSchema;
}
export class ListFilesCommand extends commands.ListFilesCommand {
- static responseSchema = schemas.listFilesCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listFilesCommandResponseSchema;
}
export class CreateFileCommand extends commands.CreateFileCommand {
- static responseSchema = schemas.createFileCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createFileCommandResponseSchema;
}
export class DeleteFileCommand extends commands.DeleteFileCommand {
- static responseSchema = schemas.deleteFileCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteFileCommandResponseSchema;
}
export class RetrieveFileCommand extends commands.RetrieveFileCommand {
- static responseSchema = schemas.retrieveFileCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.retrieveFileCommandResponseSchema;
}
export class DownloadFileCommand extends commands.DownloadFileCommand {
- static responseSchema = schemas.downloadFileCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.downloadFileCommandResponseSchema;
}
export class CreateUploadCommand extends commands.CreateUploadCommand {
- static responseSchema = schemas.createUploadCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createUploadCommandResponseSchema;
}
export class AddUploadPartCommand extends commands.AddUploadPartCommand {
- static responseSchema = schemas.addUploadPartCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.addUploadPartCommandResponseSchema;
}
export class CompleteUploadCommand extends commands.CompleteUploadCommand {
- static responseSchema = schemas.completeUploadCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.completeUploadCommandResponseSchema;
}
export class CancelUploadCommand extends commands.CancelUploadCommand {
- static responseSchema = schemas.cancelUploadCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.cancelUploadCommandResponseSchema;
}
export class CreateFineTuningJobCommand
extends commands.CreateFineTuningJobCommand
{
- static responseSchema = schemas.createFineTuningJobCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createFineTuningJobCommandResponseSchema;
}
export class ListPaginatedFineTuningJobsCommand
extends commands.ListPaginatedFineTuningJobsCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.listPaginatedFineTuningJobsCommandResponseSchema;
}
export class RetrieveFineTuningJobCommand
extends commands.RetrieveFineTuningJobCommand
{
- static responseSchema = schemas.retrieveFineTuningJobCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.retrieveFineTuningJobCommandResponseSchema;
}
export class ListFineTuningEventsCommand
extends commands.ListFineTuningEventsCommand
{
- static responseSchema = schemas.listFineTuningEventsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listFineTuningEventsCommandResponseSchema;
}
export class CancelFineTuningJobCommand
extends commands.CancelFineTuningJobCommand
{
- static responseSchema = schemas.cancelFineTuningJobCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.cancelFineTuningJobCommandResponseSchema;
}
export class ListFineTuningJobCheckpointsCommand
extends commands.ListFineTuningJobCheckpointsCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.listFineTuningJobCheckpointsCommandResponseSchema;
}
export class ListModelsCommand extends commands.ListModelsCommand {
- static responseSchema = schemas.listModelsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listModelsCommandResponseSchema;
}
export class RetrieveModelCommand extends commands.RetrieveModelCommand {
- static responseSchema = schemas.retrieveModelCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.retrieveModelCommandResponseSchema;
}
export class DeleteModelCommand extends commands.DeleteModelCommand {
- static responseSchema = schemas.deleteModelCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteModelCommandResponseSchema;
}
export class CreateModerationCommand extends commands.CreateModerationCommand {
- static responseSchema = schemas.createModerationCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createModerationCommandResponseSchema;
}
export class ListAssistantsCommand extends commands.ListAssistantsCommand {
- static responseSchema = schemas.listAssistantsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listAssistantsCommandResponseSchema;
}
export class CreateAssistantCommand extends commands.CreateAssistantCommand {
- static responseSchema = schemas.createAssistantCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createAssistantCommandResponseSchema;
}
export class GetAssistantCommand extends commands.GetAssistantCommand {
- static responseSchema = schemas.getAssistantCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getAssistantCommandResponseSchema;
}
export class ModifyAssistantCommand extends commands.ModifyAssistantCommand {
- static responseSchema = schemas.modifyAssistantCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.modifyAssistantCommandResponseSchema;
}
export class DeleteAssistantCommand extends commands.DeleteAssistantCommand {
- static responseSchema = schemas.deleteAssistantCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteAssistantCommandResponseSchema;
}
export class CreateThreadCommand extends commands.CreateThreadCommand {
- static responseSchema = schemas.createThreadCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createThreadCommandResponseSchema;
}
export class GetThreadCommand extends commands.GetThreadCommand {
- static responseSchema = schemas.getThreadCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getThreadCommandResponseSchema;
}
export class ModifyThreadCommand extends commands.ModifyThreadCommand {
- static responseSchema = schemas.modifyThreadCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.modifyThreadCommandResponseSchema;
}
export class DeleteThreadCommand extends commands.DeleteThreadCommand {
- static responseSchema = schemas.deleteThreadCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteThreadCommandResponseSchema;
}
export class ListMessagesCommand extends commands.ListMessagesCommand {
- static responseSchema = schemas.listMessagesCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listMessagesCommandResponseSchema;
}
export class CreateMessageCommand extends commands.CreateMessageCommand {
- static responseSchema = schemas.createMessageCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createMessageCommandResponseSchema;
}
export class GetMessageCommand extends commands.GetMessageCommand {
- static responseSchema = schemas.getMessageCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getMessageCommandResponseSchema;
}
export class ModifyMessageCommand extends commands.ModifyMessageCommand {
- static responseSchema = schemas.modifyMessageCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.modifyMessageCommandResponseSchema;
}
export class DeleteMessageCommand extends commands.DeleteMessageCommand {
- static responseSchema = schemas.deleteMessageCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteMessageCommandResponseSchema;
}
export class CreateThreadAndRunCommand
extends commands.CreateThreadAndRunCommand
{
- static responseSchema = schemas.createThreadAndRunCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createThreadAndRunCommandResponseSchema;
}
export class ListRunsCommand extends commands.ListRunsCommand {
- static responseSchema = schemas.listRunsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listRunsCommandResponseSchema;
}
export class CreateRunCommand extends commands.CreateRunCommand {
- static responseSchema = schemas.createRunCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createRunCommandResponseSchema;
}
export class GetRunCommand extends commands.GetRunCommand {
- static responseSchema = schemas.getRunCommandResponseSchema;
+ public override readonly responseSchema = schemas.getRunCommandResponseSchema;
}
export class ModifyRunCommand extends commands.ModifyRunCommand {
- static responseSchema = schemas.modifyRunCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.modifyRunCommandResponseSchema;
}
export class SubmitToolOuputsToRunCommand
extends commands.SubmitToolOuputsToRunCommand
{
- static responseSchema = schemas.submitToolOuputsToRunCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.submitToolOuputsToRunCommandResponseSchema;
}
export class CancelRunCommand extends commands.CancelRunCommand {
- static responseSchema = schemas.cancelRunCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.cancelRunCommandResponseSchema;
}
export class ListRunStepsCommand extends commands.ListRunStepsCommand {
- static responseSchema = schemas.listRunStepsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listRunStepsCommandResponseSchema;
}
export class GetRunStepCommand extends commands.GetRunStepCommand {
- static responseSchema = schemas.getRunStepCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getRunStepCommandResponseSchema;
}
export class ListVectorStoresCommand extends commands.ListVectorStoresCommand {
- static responseSchema = schemas.listVectorStoresCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listVectorStoresCommandResponseSchema;
}
export class CreateVectorStoreCommand
extends commands.CreateVectorStoreCommand
{
- static responseSchema = schemas.createVectorStoreCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createVectorStoreCommandResponseSchema;
}
export class GetVectorStoreCommand extends commands.GetVectorStoreCommand {
- static responseSchema = schemas.getVectorStoreCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getVectorStoreCommandResponseSchema;
}
export class ModifyVectorStoreCommand
extends commands.ModifyVectorStoreCommand
{
- static responseSchema = schemas.modifyVectorStoreCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.modifyVectorStoreCommandResponseSchema;
}
export class DeleteVectorStoreCommand
extends commands.DeleteVectorStoreCommand
{
- static responseSchema = schemas.deleteVectorStoreCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteVectorStoreCommandResponseSchema;
}
export class ListVectorStoreFilesCommand
extends commands.ListVectorStoreFilesCommand
{
- static responseSchema = schemas.listVectorStoreFilesCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listVectorStoreFilesCommandResponseSchema;
}
export class CreateVectorStoreFileCommand
extends commands.CreateVectorStoreFileCommand
{
- static responseSchema = schemas.createVectorStoreFileCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createVectorStoreFileCommandResponseSchema;
}
export class GetVectorStoreFileCommand
extends commands.GetVectorStoreFileCommand
{
- static responseSchema = schemas.getVectorStoreFileCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getVectorStoreFileCommandResponseSchema;
}
export class DeleteVectorStoreFileCommand
extends commands.DeleteVectorStoreFileCommand
{
- static responseSchema = schemas.deleteVectorStoreFileCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteVectorStoreFileCommandResponseSchema;
}
export class CreateVectorStoreFileBatchCommand
extends commands.CreateVectorStoreFileBatchCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.createVectorStoreFileBatchCommandResponseSchema;
}
export class GetVectorStoreFileBatchCommand
extends commands.GetVectorStoreFileBatchCommand
{
- static responseSchema = schemas.getVectorStoreFileBatchCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getVectorStoreFileBatchCommandResponseSchema;
}
export class CancelVectorStoreFileBatchCommand
extends commands.CancelVectorStoreFileBatchCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.cancelVectorStoreFileBatchCommandResponseSchema;
}
export class ListFilesInVectorStoreBatchCommand
extends commands.ListFilesInVectorStoreBatchCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.listFilesInVectorStoreBatchCommandResponseSchema;
}
export class CreateBatchCommand extends commands.CreateBatchCommand {
- static responseSchema = schemas.createBatchCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createBatchCommandResponseSchema;
}
export class ListBatchesCommand extends commands.ListBatchesCommand {
- static responseSchema = schemas.listBatchesCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listBatchesCommandResponseSchema;
}
export class RetrieveBatchCommand extends commands.RetrieveBatchCommand {
- static responseSchema = schemas.retrieveBatchCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.retrieveBatchCommandResponseSchema;
}
export class CancelBatchCommand extends commands.CancelBatchCommand {
- static responseSchema = schemas.cancelBatchCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.cancelBatchCommandResponseSchema;
}
export class ListAuditLogsCommand extends commands.ListAuditLogsCommand {
- static responseSchema = schemas.listAuditLogsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listAuditLogsCommandResponseSchema;
}
export class ListInvitesCommand extends commands.ListInvitesCommand {
- static responseSchema = schemas.listInvitesCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listInvitesCommandResponseSchema;
}
export class InviteUserCommand extends commands.InviteUserCommand {
- static responseSchema = schemas.inviteUserCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.inviteUserCommandResponseSchema;
}
export class RetrieveInviteCommand extends commands.RetrieveInviteCommand {
- static responseSchema = schemas.retrieveInviteCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.retrieveInviteCommandResponseSchema;
}
export class DeleteInviteCommand extends commands.DeleteInviteCommand {
- static responseSchema = schemas.deleteInviteCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteInviteCommandResponseSchema;
}
export class ListUsersCommand extends commands.ListUsersCommand {
- static responseSchema = schemas.listUsersCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listUsersCommandResponseSchema;
}
export class RetrieveUserCommand extends commands.RetrieveUserCommand {
- static responseSchema = schemas.retrieveUserCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.retrieveUserCommandResponseSchema;
}
export class ModifyUserCommand extends commands.ModifyUserCommand {
- static responseSchema = schemas.modifyUserCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.modifyUserCommandResponseSchema;
}
export class DeleteUserCommand extends commands.DeleteUserCommand {
- static responseSchema = schemas.deleteUserCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteUserCommandResponseSchema;
}
export class ListProjectsCommand extends commands.ListProjectsCommand {
- static responseSchema = schemas.listProjectsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listProjectsCommandResponseSchema;
}
export class CreateProjectCommand extends commands.CreateProjectCommand {
- static responseSchema = schemas.createProjectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createProjectCommandResponseSchema;
}
export class RetrieveProjectCommand extends commands.RetrieveProjectCommand {
- static responseSchema = schemas.retrieveProjectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.retrieveProjectCommandResponseSchema;
}
export class ModifyProjectCommand extends commands.ModifyProjectCommand {
- static responseSchema = schemas.modifyProjectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.modifyProjectCommandResponseSchema;
}
export class ArchiveProjectCommand extends commands.ArchiveProjectCommand {
- static responseSchema = schemas.archiveProjectCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.archiveProjectCommandResponseSchema;
}
export class ListProjectUsersCommand extends commands.ListProjectUsersCommand {
- static responseSchema = schemas.listProjectUsersCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listProjectUsersCommandResponseSchema;
}
export class CreateProjectUserCommand
extends commands.CreateProjectUserCommand
{
- static responseSchema = schemas.createProjectUserCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createProjectUserCommandResponseSchema;
}
export class RetrieveProjectUserCommand
extends commands.RetrieveProjectUserCommand
{
- static responseSchema = schemas.retrieveProjectUserCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.retrieveProjectUserCommandResponseSchema;
}
export class ModifyProjectUserCommand
extends commands.ModifyProjectUserCommand
{
- static responseSchema = schemas.modifyProjectUserCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.modifyProjectUserCommandResponseSchema;
}
export class DeleteProjectUserCommand
extends commands.DeleteProjectUserCommand
{
- static responseSchema = schemas.deleteProjectUserCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteProjectUserCommandResponseSchema;
}
export class ListProjectServiceAccountsCommand
extends commands.ListProjectServiceAccountsCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.listProjectServiceAccountsCommandResponseSchema;
}
export class CreateProjectServiceAccountCommand
extends commands.CreateProjectServiceAccountCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.createProjectServiceAccountCommandResponseSchema;
}
export class RetrieveProjectServiceAccountCommand
extends commands.RetrieveProjectServiceAccountCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.retrieveProjectServiceAccountCommandResponseSchema;
}
export class DeleteProjectServiceAccountCommand
extends commands.DeleteProjectServiceAccountCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.deleteProjectServiceAccountCommandResponseSchema;
}
export class ListProjectApiKeysCommand
extends commands.ListProjectApiKeysCommand
{
- static responseSchema = schemas.listProjectApiKeysCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listProjectApiKeysCommandResponseSchema;
}
export class RetrieveProjectApiKeyCommand
extends commands.RetrieveProjectApiKeyCommand
{
- static responseSchema = schemas.retrieveProjectApiKeyCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.retrieveProjectApiKeyCommandResponseSchema;
}
export class DeleteProjectApiKeyCommand
extends commands.DeleteProjectApiKeyCommand
{
- static responseSchema = schemas.deleteProjectApiKeyCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deleteProjectApiKeyCommandResponseSchema;
}
export { CreateSpeechCommand } from "./commands.js";
diff --git a/__tests__/fixtures/openai/commands.ts b/__tests__/fixtures/openai/commands.ts
index 995eb42..2d3c1dc 100644
--- a/__tests__/fixtures/openai/commands.ts
+++ b/__tests__/fixtures/openai/commands.ts
@@ -7,7 +7,7 @@
// oxlint-disable block65/max-comment-lines, block65/no-comment-punctuation, block65/no-narrative-comment, block65/no-trailing-comment-punctuation
import { Command, stripUndefined, jsonStringify } from "@block65/rest-client";
-import type { Except, UndefinedOnPartialDeep } from "type-fest";
+import type { UndefinedOnPartialDeep } from "type-fest";
import type {
CreateChatCompletionCommandInput,
CreateChatCompletionResponse,
@@ -36,7 +36,8 @@ import type {
DownloadFileCommandOutput,
CreateUploadCommandInput,
Upload,
- AddUploadPartCommandInput,
+ AddUploadPartCommandBodyNonJson,
+ AddUploadPartCommandParams,
UploadPart,
CompleteUploadCommandInput,
CancelUploadCommandInput,
@@ -255,16 +256,12 @@ export class CreateImageCommand extends Command<
* @summary Creates an edited or extended image given an original image and a prompt.
*/
export class CreateImageEditCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ CreateImageEditCommandInput,
ImagesResponse
> {
public override method = "post" as const;
- constructor(
- input: UndefinedOnPartialDeep> &
- Pick,
- ) {
+ constructor(input: CreateImageEditCommandInput) {
const { body } = input;
super("/images/edits", body);
}
@@ -276,18 +273,12 @@ export class CreateImageEditCommand extends Command<
* @summary Creates a variation of a given image.
*/
export class CreateImageVariationCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ CreateImageVariationCommandInput,
ImagesResponse
> {
public override method = "post" as const;
- constructor(
- input: UndefinedOnPartialDeep<
- Except
- > &
- Pick,
- ) {
+ constructor(input: CreateImageVariationCommandInput) {
const { body } = input;
super("/images/variations", body);
}
@@ -332,18 +323,12 @@ export class CreateSpeechCommand extends Command<
* @summary Transcribes audio into the input language.
*/
export class CreateTranscriptionCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ CreateTranscriptionCommandInput,
CreateTranscriptionCommandOutput
> {
public override method = "post" as const;
- constructor(
- input: UndefinedOnPartialDeep<
- Except
- > &
- Pick,
- ) {
+ constructor(input: CreateTranscriptionCommandInput) {
const { body } = input;
super("/audio/transcriptions", body);
}
@@ -355,18 +340,12 @@ export class CreateTranscriptionCommand extends Command<
* @summary Translates audio into English.
*/
export class CreateTranslationCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ CreateTranslationCommandInput,
CreateTranslationCommandOutput
> {
public override method = "post" as const;
- constructor(
- input: UndefinedOnPartialDeep<
- Except
- > &
- Pick,
- ) {
+ constructor(input: CreateTranslationCommandInput) {
const { body } = input;
super("/audio/translations", body);
}
@@ -414,16 +393,12 @@ export class ListFilesCommand extends Command<
* storage limits.
*/
export class CreateFileCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ CreateFileCommandInput,
OpenAiFile
> {
public override method = "post" as const;
- constructor(
- input: UndefinedOnPartialDeep> &
- Pick,
- ) {
+ constructor(input: CreateFileCommandInput) {
const { body } = input;
super("/files", body);
}
@@ -528,15 +503,15 @@ export class CreateUploadCommand extends Command<
* Upload](/docs/api-reference/uploads/complete).
*/
export class AddUploadPartCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ AddUploadPartCommandBodyNonJson &
+ UndefinedOnPartialDeep,
UploadPart
> {
public override method = "post" as const;
constructor(
- input: UndefinedOnPartialDeep> &
- Pick,
+ input: AddUploadPartCommandBodyNonJson &
+ UndefinedOnPartialDeep,
) {
const { upload_id, body } = input;
super(encodePath`/uploads/${upload_id}/parts`, body);
diff --git a/__tests__/fixtures/openai/main.ts b/__tests__/fixtures/openai/main.ts
index cb21a13..539fd57 100644
--- a/__tests__/fixtures/openai/main.ts
+++ b/__tests__/fixtures/openai/main.ts
@@ -8,9 +8,10 @@ import {
RestServiceClient,
type RestServiceClientConfig,
} from "@block65/rest-client";
-import type { Except, UndefinedOnPartialDeep } from "type-fest";
+import type { UndefinedOnPartialDeep } from "type-fest";
import type {
- AddUploadPartCommandInput,
+ AddUploadPartCommandBodyNonJson,
+ AddUploadPartCommandParams,
ArchiveProjectCommandInput,
AssistantObject,
Batch,
@@ -168,6 +169,13 @@ import type {
export { ResponseValidationError } from "@block65/rest-client";
type AllInputs =
+ | (AddUploadPartCommandBodyNonJson &
+ UndefinedOnPartialDeep)
+ | CreateFileCommandInput
+ | CreateImageEditCommandInput
+ | CreateImageVariationCommandInput
+ | CreateTranscriptionCommandInput
+ | CreateTranslationCommandInput
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
@@ -208,18 +216,6 @@ type AllInputs =
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
- | (UndefinedOnPartialDeep> &
- Pick)
- | (UndefinedOnPartialDeep> &
- Pick)
- | (UndefinedOnPartialDeep> &
- Pick)
- | (UndefinedOnPartialDeep> &
- Pick)
- | (UndefinedOnPartialDeep> &
- Pick)
- | (UndefinedOnPartialDeep> &
- Pick)
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
diff --git a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
index bb3e0e4..498a130 100644
--- a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
@@ -1,9 +1,9 @@
{
- "#generator": "cf9ab7bd8a80e3339d9237d5dee0deb3",
+ "#generator": "99360df640587900347f0725f28f06a7",
"commands.ts": "69c6a9f2924568fc08fd489508c9df34",
"types.ts": "ea65c3e67352d4e22b97af80085727b4",
"main.ts": "64edb526dcbcbd345e631ccff959f11d",
"valibot.ts": "a754e3506dd239dc9fa4973e39198cbd",
"hono.ts": "be7061d9a002b7d6f42bbdfc53727ec7",
- "commands-validated.ts": "90b52a2210f1b9cc0e123225210fe74a"
+ "commands-validated.ts": "21f5e9e15233b504d8509d93e00911fd"
}
diff --git a/__tests__/fixtures/petstore/commands-validated.ts b/__tests__/fixtures/petstore/commands-validated.ts
index fbb7905..faae21f 100644
--- a/__tests__/fixtures/petstore/commands-validated.ts
+++ b/__tests__/fixtures/petstore/commands-validated.ts
@@ -8,19 +8,22 @@ import * as commands from "./commands.js";
import * as schemas from "./valibot.js";
export class FindPetsCommand extends commands.FindPetsCommand {
- static responseSchema = schemas.findPetsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.findPetsCommandResponseSchema;
}
export class AddPetCommand extends commands.AddPetCommand {
- static responseSchema = schemas.addPetCommandResponseSchema;
+ public override readonly responseSchema = schemas.addPetCommandResponseSchema;
}
export class FindPetByIdCommand extends commands.FindPetByIdCommand {
- static responseSchema = schemas.findPetByIdCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.findPetByIdCommandResponseSchema;
}
export class FindPetWrappedCommand extends commands.FindPetWrappedCommand {
- static responseSchema = schemas.findPetWrappedCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.findPetWrappedCommandResponseSchema;
}
export { DeletePetCommand } from "./commands.js";
diff --git a/__tests__/fixtures/test1.json b/__tests__/fixtures/test1.json
index 0f073a0..fa71c40 100644
--- a/__tests__/fixtures/test1.json
+++ b/__tests__/fixtures/test1.json
@@ -1,5 +1,5 @@
{
- "openapi": "3.1.0",
+ "openapi": "3.2.0",
"info": {
"title": "Billing Service REST API",
"version": "1.0.0"
@@ -925,6 +925,80 @@
}
]
},
+ "/operations/{operationId}/events": {
+ "get": {
+ "operationId": "streamOperationEventsCommand",
+ "security": [
+ {
+ "HttpBearerJwtScheme": ["billing.read"]
+ }
+ ],
+ "tags": [],
+ "responses": {
+ "200": {
+ "description": "Operation progress as server-sent events",
+ "content": {
+ "text/event-stream": {
+ "itemSchema": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": ["event", "data"],
+ "properties": {
+ "event": {
+ "const": "progress"
+ },
+ "data": {
+ "type": "string",
+ "contentMediaType": "application/json",
+ "contentSchema": {
+ "$ref": "#/components/schemas/LongRunningOperation"
+ }
+ },
+ "id": {
+ "type": "string"
+ },
+ "retry": {
+ "type": "integer"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": ["event", "data"],
+ "properties": {
+ "event": {
+ "const": "heartbeat"
+ },
+ "data": {
+ "type": "string",
+ "contentMediaType": "application/json",
+ "contentSchema": {
+ "type": "object",
+ "required": ["sequence"],
+ "properties": {
+ "sequence": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/OperationIdParam"
+ }
+ ]
+ },
"/billing-accounts": {
"get": {
"operationId": "listBillingAccountsCommand",
diff --git a/__tests__/fixtures/test1/.openapi-codegen-manifest.json b/__tests__/fixtures/test1/.openapi-codegen-manifest.json
index 819b6d4..05ffd95 100644
--- a/__tests__/fixtures/test1/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/test1/.openapi-codegen-manifest.json
@@ -1,10 +1,10 @@
{
- "#generator": "cf9ab7bd8a80e3339d9237d5dee0deb3",
- "commands.ts": "19fd590dfc5cc8616a70dcf78326c027",
- "types.ts": "2f7230842affe9bd6e7f727702f0c581",
- "main.ts": "1e2091a697e1aa9d8b770d777c172bf8",
- "valibot.ts": "d6a449becdabb642ffc6d904277f3f41",
- "hono.ts": "26885b9a53e8c0343a2fdb21f4eb8a37",
- "commands-validated.ts": "cc5f84090799e163e229aabd160311f8",
+ "#generator": "99360df640587900347f0725f28f06a7",
+ "commands.ts": "0e6777a5287ed2535587a0f5180bbbd5",
+ "types.ts": "7bb3f162d3321e4db51673d3f3ba35ac",
+ "main.ts": "5c768be2e48d0b06d7ed8da665653404",
+ "valibot.ts": "c8e831e3378ad09f85be0d368e1f0cf2",
+ "hono.ts": "12cc7ef64bfc4acf632ba9d9011880d8",
+ "commands-validated.ts": "afd10b6462e83dbe33e5d42dd227cd97",
"enums.ts": "26ade5ccad3bf30118ce8691b99b7c91"
}
diff --git a/__tests__/fixtures/test1/commands-validated.ts b/__tests__/fixtures/test1/commands-validated.ts
index c851e99..62a5886 100644
--- a/__tests__/fixtures/test1/commands-validated.ts
+++ b/__tests__/fixtures/test1/commands-validated.ts
@@ -10,89 +10,108 @@ import * as schemas from "./valibot.js";
export class ImportBillingDataCommand
extends commands.ImportBillingDataCommand
{
- static responseSchema = schemas.importBillingDataCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.importBillingDataCommandResponseSchema;
}
export class GetOperationCommand extends commands.GetOperationCommand {
- static responseSchema = schemas.getOperationCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getOperationCommandResponseSchema;
+}
+
+export class StreamOperationEventsCommand
+ extends commands.StreamOperationEventsCommand
+{
+ public override readonly dataSchema =
+ schemas.streamOperationEventsCommandDataSchema;
}
export class ListBillingAccountsCommand
extends commands.ListBillingAccountsCommand
{
- static responseSchema = schemas.listBillingAccountsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listBillingAccountsCommandResponseSchema;
}
export class CreateBillingAccountCommand
extends commands.CreateBillingAccountCommand
{
- static responseSchema = schemas.createBillingAccountCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createBillingAccountCommandResponseSchema;
}
export class GetBillingAccountCommand
extends commands.GetBillingAccountCommand
{
- static responseSchema = schemas.getBillingAccountCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getBillingAccountCommandResponseSchema;
}
export class UpdateBillingAccountCommand
extends commands.UpdateBillingAccountCommand
{
- static responseSchema = schemas.updateBillingAccountCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.updateBillingAccountCommandResponseSchema;
}
export class GetBillingAccountPortalCommand
extends commands.GetBillingAccountPortalCommand
{
- static responseSchema = schemas.getBillingAccountPortalCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getBillingAccountPortalCommandResponseSchema;
}
export class ListPaymentMethodsCommand
extends commands.ListPaymentMethodsCommand
{
- static responseSchema = schemas.listPaymentMethodsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listPaymentMethodsCommandResponseSchema;
}
export class CreatePaymentMethodCommand
extends commands.CreatePaymentMethodCommand
{
- static responseSchema = schemas.createPaymentMethodCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.createPaymentMethodCommandResponseSchema;
}
export class GetPaymentMethodFromStripeCommand
extends commands.GetPaymentMethodFromStripeCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.getPaymentMethodFromStripeCommandResponseSchema;
}
export class GetPaymentMethodCommand extends commands.GetPaymentMethodCommand {
- static responseSchema = schemas.getPaymentMethodCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.getPaymentMethodCommandResponseSchema;
}
export class DeletePaymentMethodCommand
extends commands.DeletePaymentMethodCommand
{
- static responseSchema = schemas.deletePaymentMethodCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.deletePaymentMethodCommandResponseSchema;
}
export class ListBillingSubscriptionsCommand
extends commands.ListBillingSubscriptionsCommand
{
- static responseSchema = schemas.listBillingSubscriptionsCommandResponseSchema;
+ public override readonly responseSchema =
+ schemas.listBillingSubscriptionsCommandResponseSchema;
}
export class CreateBillingSubscriptionCommand
extends commands.CreateBillingSubscriptionCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.createBillingSubscriptionCommandResponseSchema;
}
export class UpdateBillingSubscriptionPromoCodeCommand
extends commands.UpdateBillingSubscriptionPromoCodeCommand
{
- static responseSchema =
+ public override readonly responseSchema =
schemas.updateBillingSubscriptionPromoCodeCommandResponseSchema;
}
diff --git a/__tests__/fixtures/test1/commands.ts b/__tests__/fixtures/test1/commands.ts
index e2c1808..0bf032b 100644
--- a/__tests__/fixtures/test1/commands.ts
+++ b/__tests__/fixtures/test1/commands.ts
@@ -4,13 +4,20 @@
* Do not edit directly
*/
-import { Command, jsonStringify } from "@block65/rest-client";
-import type { Except, UndefinedOnPartialDeep } from "type-fest";
+import {
+ Command,
+ EventStreamCommand,
+ jsonStringify,
+} from "@block65/rest-client";
+import type { UndefinedOnPartialDeep } from "type-fest";
import type {
ImportBillingDataCommandHeader,
- ImportBillingDataCommandInput,
+ ImportBillingDataCommandBodyNonJson,
+ ImportBillingDataCommandParams,
LongRunningOperation,
GetOperationCommandInput,
+ StreamOperationEventsCommandInput,
+ StreamOperationEventsCommandOutput,
ListBillingAccountsCommandInput,
BillingAccountList,
CreateBillingAccountCommandInput,
@@ -56,8 +63,8 @@ function encodePath(strings: TemplateStringsArray, ...values: string[]) {
*
*/
export class ImportBillingDataCommand extends Command<
- UndefinedOnPartialDeep> &
- Pick,
+ ImportBillingDataCommandBodyNonJson &
+ UndefinedOnPartialDeep,
LongRunningOperation,
never,
ImportBillingDataCommandHeader
@@ -65,10 +72,8 @@ export class ImportBillingDataCommand extends Command<
public override method = "post" as const;
constructor(
- input: UndefinedOnPartialDeep<
- Except
- > &
- Pick,
+ input: ImportBillingDataCommandBodyNonJson &
+ UndefinedOnPartialDeep,
headers: ImportBillingDataCommandHeader,
) {
const { billingAccountId, body } = input;
@@ -97,6 +102,24 @@ export class GetOperationCommand extends Command<
}
}
+/**
+ * StreamOperationEventsCommand
+ *
+ */
+export class StreamOperationEventsCommand extends EventStreamCommand<
+ UndefinedOnPartialDeep,
+ StreamOperationEventsCommandOutput
+> {
+ public override method = "get" as const;
+
+ constructor(
+ input: UndefinedOnPartialDeep,
+ ) {
+ const { operationId } = input;
+ super(encodePath`/operations/${operationId}/events`);
+ }
+}
+
/**
* ListBillingAccountsCommand
*
diff --git a/__tests__/fixtures/test1/hono.ts b/__tests__/fixtures/test1/hono.ts
index 73e6b9f..bd739b1 100644
--- a/__tests__/fixtures/test1/hono.ts
+++ b/__tests__/fixtures/test1/hono.ts
@@ -27,6 +27,7 @@ import {
linkBillingAccountCommandParamsSchema,
listBillingSubscriptionsCommandParamsSchema,
listPaymentMethodsCommandParamsSchema,
+ streamOperationEventsCommandParamsSchema,
updateBillingAccountCommandBodySchema,
updateBillingAccountCommandParamsSchema,
updateBillingSubscriptionCommandBodySchema,
@@ -62,6 +63,9 @@ export const importBillingData = [
export const getOperation = [
validate("param", getOperationCommandParamsSchema),
] as const;
+export const streamOperationEvents = [
+ validate("param", streamOperationEventsCommandParamsSchema),
+] as const;
export const listBillingAccounts = [] as const;
export const createBillingAccount = [
validate("json", createBillingAccountCommandBodySchema),
diff --git a/__tests__/fixtures/test1/main.ts b/__tests__/fixtures/test1/main.ts
index 134ebd1..f80832a 100644
--- a/__tests__/fixtures/test1/main.ts
+++ b/__tests__/fixtures/test1/main.ts
@@ -6,9 +6,10 @@
import {
RestServiceClient,
+ type ParsedStreamEvent,
type RestServiceClientConfig,
} from "@block65/rest-client";
-import type { Except, UndefinedOnPartialDeep } from "type-fest";
+import type { UndefinedOnPartialDeep } from "type-fest";
import type {
BillingAccount,
BillingAccountList,
@@ -25,7 +26,8 @@ import type {
GetOperationCommandInput,
GetPaymentMethodCommandInput,
GetPaymentMethodFromStripeCommandInput,
- ImportBillingDataCommandInput,
+ ImportBillingDataCommandBodyNonJson,
+ ImportBillingDataCommandParams,
LinkBillingAccountCommandInput,
ListBillingSubscriptionsCommandInput,
ListPaymentMethodsCommandInput,
@@ -34,6 +36,8 @@ import type {
PaymentMethodDeletedLro,
PaymentMethodIntendedLro,
PaymentMethods,
+ StreamOperationEventsCommandInput,
+ StreamOperationEventsCommandOutput,
UpdateBillingAccountCommandInput,
UpdateBillingSubscriptionCommandInput,
UpdateBillingSubscriptionPromoCodeCommandInput,
@@ -43,13 +47,13 @@ import type {
export { ResponseValidationError } from "@block65/rest-client";
type AllInputs =
+ | (ImportBillingDataCommandBodyNonJson &
+ UndefinedOnPartialDeep)
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
- | (UndefinedOnPartialDeep> &
- Pick)
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
@@ -58,6 +62,7 @@ type AllInputs =
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
+ | UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
| UndefinedOnPartialDeep
@@ -69,6 +74,7 @@ type AllOutputs =
| BillingSubscriptionLro
| BillingSubscriptions
| LongRunningOperation
+ | ParsedStreamEvent
| PaymentMethod
| PaymentMethodDeletedLro
| PaymentMethodIntendedLro
diff --git a/__tests__/fixtures/test1/types.ts b/__tests__/fixtures/test1/types.ts
index e1593e3..377c0e7 100644
--- a/__tests__/fixtures/test1/types.ts
+++ b/__tests__/fixtures/test1/types.ts
@@ -244,6 +244,16 @@ export type GetOperationCommandParams = {
export type GetOperationCommandInput = GetOperationCommandParams;
export type InputGetOperationCommandResponse =
UndefinedOnPartialDeep;
+export type StreamOperationEventsCommandParams = {
+ operationId: Uuid;
+};
+export type StreamOperationEventsCommandInput =
+ StreamOperationEventsCommandParams;
+export type StreamOperationEventsCommandOutput =
+ | LongRunningOperation
+ | {
+ sequence: number;
+ };
export type ListBillingAccountsCommandInput = never;
export type InputListBillingAccountsCommandResponse =
UndefinedOnPartialDeep;
diff --git a/__tests__/fixtures/test1/valibot.ts b/__tests__/fixtures/test1/valibot.ts
index 6e03a5e..4bc4ae8 100644
--- a/__tests__/fixtures/test1/valibot.ts
+++ b/__tests__/fixtures/test1/valibot.ts
@@ -556,6 +556,24 @@ export const inputGetOperationCommandParamsSchema = v.strictObject({
export const getOperationCommandParamsSchema = v.strictObject({
operationId: uuidSchema,
});
+export const inputStreamOperationEventsCommandDataSchema = v.union([
+ inputLongRunningOperationSchema,
+ v.strictObject({
+ sequence: v.pipe(v.number(), v.integer()),
+ }),
+]);
+export const streamOperationEventsCommandDataSchema = v.union([
+ longRunningOperationSchema,
+ v.strictObject({
+ sequence: v.pipe(v.number(), v.integer()),
+ }),
+]);
+export const inputStreamOperationEventsCommandParamsSchema = v.strictObject({
+ operationId: inputUuidSchema,
+});
+export const streamOperationEventsCommandParamsSchema = v.strictObject({
+ operationId: uuidSchema,
+});
export const inputListBillingAccountsCommandResponseSchema =
inputBillingAccountListSchema;
export const listBillingAccountsCommandResponseSchema =
diff --git a/__tests__/test1.test.ts b/__tests__/test1.test.ts
index 35b19ae..781db7a 100644
--- a/__tests__/test1.test.ts
+++ b/__tests__/test1.test.ts
@@ -1,11 +1,16 @@
-import { createIsomorphicNativeFetcher } from "@block65/rest-client";
+import {
+ createIsomorphicNativeFetcher,
+ type ParsedStreamEvent,
+} from "@block65/rest-client";
import { MockAgent, fetch as undiciFetch } from "undici";
-import { describe, expect, test, vi } from "vitest";
+import { describe, expect, expectTypeOf, test, vi } from "vitest";
import {
GetBillingAccountCommand,
ListBillingAccountsCommand,
+ StreamOperationEventsCommand,
} from "./fixtures/test1/commands.ts";
import { BillingServiceRestApiRestClient } from "./fixtures/test1/main.ts";
+import type { StreamOperationEventsCommandOutput } from "./fixtures/test1/types.ts";
const mockAgent = new MockAgent();
mockAgent.disableNetConnect();
@@ -31,6 +36,15 @@ describe("Test1", () => {
.reply(200, { ok: 1 })
.times(1);
+ const operationId = "00000000-0000-4000-8000-000000000000";
+
+ pool
+ .intercept({ path: `/operations/${operationId}/events`, method: "GET" })
+ .reply(200, 'event: heartbeat\ndata: {"sequence":1}\n\n', {
+ headers: { "content-type": "text/event-stream" },
+ })
+ .times(1);
+
test("zero-input command has correct pathname", () => {
const command = new ListBillingAccountsCommand();
expect(command.pathname).toBe("/billing-accounts");
@@ -53,4 +67,38 @@ describe("Test1", () => {
expect(bodySpy).toBeTruthy();
});
+
+ test("event stream yields each event with its data decoded", async () => {
+ const client = new BillingServiceRestApiRestClient(apiUrl, {
+ fetcher: createIsomorphicNativeFetcher({
+ retry: { retries: 0 },
+ fetch: (input, init) =>
+ undiciFetch(input, { ...init, dispatcher: mockAgent }),
+ }),
+ });
+
+ const stream = await client.stream(
+ new StreamOperationEventsCommand({ operationId }),
+ );
+
+ expectTypeOf(stream).toEqualTypeOf<
+ ReadableStream>
+ >();
+
+ const reader = stream.getReader();
+
+ await expect(reader.read()).resolves.toStrictEqual({
+ done: false,
+ value: {
+ type: "heartbeat",
+ lastEventId: "",
+ retry: undefined,
+ data: { sequence: 1 },
+ },
+ });
+ await expect(reader.read()).resolves.toEqual({
+ done: true,
+ value: undefined,
+ });
+ });
});
diff --git a/lib/hono.ts b/lib/hono.ts
index 829c626..1bffae2 100644
--- a/lib/hono.ts
+++ b/lib/hono.ts
@@ -93,6 +93,7 @@ export function createHonoMiddleware(
schemas: {
json?: string;
response?: string;
+ data?: string;
param?: string;
query?: string;
header?: string;
@@ -127,10 +128,10 @@ export function createHonoMiddleware(
writer.write("[");
writer.indent(() => {
// Hono validators run on inbound request data alone. Response
- // schemas are emitted for client-side consumption, and `header`
- // is skipped so extra HTTP headers pass
+ // and event data schemas are emitted for client-side
+ // consumption, and `header` is skipped so extra HTTP headers pass
for (const [target, schemaName] of typedEntries(schemas).filter(
- ([t]) => t !== "header" && t !== "response",
+ ([t]) => t !== "header" && t !== "response" && t !== "data",
)) {
writer.writeLine(
`validate(${JSON.stringify(target)}, ${schemaName}),`,
diff --git a/lib/process-document.ts b/lib/process-document.ts
index 69e2678..777a1e6 100644
--- a/lib/process-document.ts
+++ b/lib/process-document.ts
@@ -33,6 +33,7 @@ import {
castToValidJsIdentifier,
getDependents,
iife,
+ isReferenceObject,
pascalCase,
typedEntries,
wordWrap,
@@ -274,6 +275,61 @@ type OperationContext = {
type OperationWithId = oas31.OperationObject & { operationId: string };
+type ValidatedSubclass = {
+ commandName: string;
+ field: "responseSchema" | "dataSchema";
+ schema: string;
+};
+
+type SequentialMedia = {
+ baseClass: string;
+
+ // the type rest-client's stream() yields for an output type
+ itemType: (output: string) => string;
+
+ // the item property holding the content, as SSE's `data`. Without it, the
+ // item is the content
+ contentProperty?: string;
+};
+
+// media types rest-client splits into items, when they have an itemSchema
+const sequentialMediaTypes: Readonly> = {
+ "text/event-stream": {
+ baseClass: "EventStreamCommand",
+ itemType: (output) => `ParsedStreamEvent<${output}>`,
+ contentProperty: "data",
+ },
+};
+
+type SchemaOrRef = oas31.SchemaObject | oas31.ReferenceObject;
+
+// openapi3-ts's oas31 SchemaObject lacks contentSchema
+type ContentSchemaObject = oas31.SchemaObject & { contentSchema?: SchemaOrRef };
+
+// OAS 3.2 added itemSchema, which openapi3-ts's oas31 types predate
+type ItemSchemaMediaTypeObject = oas31.MediaTypeObject & {
+ itemSchema?: SchemaOrRef;
+};
+
+type DataDecoding = {
+ // a rest-client DataTransformer export. Without one, rest-client's
+ // jsonDataTransformer applies
+ transformer?: string;
+
+ decodedSchema: (content: ContentSchemaObject) => SchemaOrRef | undefined;
+};
+
+// keyed by an item content's contentMediaType
+const dataDecodings: Readonly> = {
+ "application/json": {
+ decodedSchema: (content) => content.contentSchema,
+ },
+ "text/plain": {
+ transformer: "textDataTransformer",
+ decodedSchema: () => ({ type: "string" }),
+ },
+};
+
function hasOperationId(
operationObject: oas31.OperationObject,
): operationObject is OperationWithId {
@@ -315,7 +371,10 @@ type DocumentContext = OutputFiles & {
outputTypes: Set;
inputTypeArgs: Set;
inputTypeNames: Set;
- validatedSubclasses: { commandName: string; responseSchema: string }[];
+ validatedSubclasses: ValidatedSubclass[];
+
+ // apart from outputTypes, as AllOutputs takes the item type, not the alias
+ sequentialOutputs: { output: string; item: string }[];
validatedReExports: string[];
inputOnly: boolean | undefined;
};
@@ -329,6 +388,10 @@ function addModulePreambles({ commandsFile, typesFile }: OutputFiles) {
namedImports: [
// command classes
"Command",
+ ...Object.values(sequentialMediaTypes).map(({ baseClass }) => baseClass),
+ ...Object.values(dataDecodings)
+ .map(({ transformer }) => transformer)
+ .filter((transformer) => transformer !== undefined),
"stripUndefined",
"jsonStringify",
],
@@ -1088,9 +1151,8 @@ function addInputType(
return { inputType, inputTypeNode, wrapJsonBody };
}
-function firstJsonResponseSchema(operationObject: OperationWithId) {
- // Resolve the first 2xx JSON response schema (inline or $ref) so the
- // validator pipeline treats responses the same as request bodies
+// the first 2xx response with content, which settles the command's output
+function firstSuccessResponse(operationObject: OperationWithId) {
const firstSuccess = Object.entries(operationObject.responses ?? {}).find(
([s]) => s.startsWith("2"),
);
@@ -1105,7 +1167,14 @@ function firstJsonResponseSchema(operationObject: OperationWithId) {
return;
}
- return response.content?.["application/json"]?.schema;
+ return response;
+}
+
+function firstJsonResponseSchema(operationObject: OperationWithId) {
+ // Resolve the first 2xx JSON response schema (inline or $ref) so the
+ // validator pipeline treats responses the same as request bodies
+ return firstSuccessResponse(operationObject)?.content?.["application/json"]
+ ?.schema;
}
// Generate the valibot validator for the operation input
@@ -1119,6 +1188,7 @@ function registerOperationValidators(
jsonRequestBodyObject,
}: OperationContext,
operationObject: OperationWithId,
+ sequential: SequentialContent | undefined,
) {
const responseSchema = firstJsonResponseSchema(operationObject);
@@ -1133,6 +1203,9 @@ function registerOperationValidators(
...(responseSchema && {
response: responseSchema,
}),
+ ...(sequential && {
+ data: sequential.schema,
+ }),
params: pathParameters,
query: queryParameters,
header: headerParameters,
@@ -1162,28 +1235,65 @@ function registerOperationValidators(
return wireSchemas;
}
-function widenedInputType(inputTypeName: string, hasNonJsonBody: boolean) {
+// the parts of a non-JSON body command's input that the widening applies to
+function widenedInputParts({
+ nonJsonBodyType,
+ pathType,
+ queryType,
+}: Pick) {
+ return nonJsonBodyType
+ ? [pathType, queryType].filter((part) => part !== undefined)
+ : [];
+}
+
+function widenedInputType({
+ inputType,
+ nonJsonBodyType,
+ pathType,
+ queryType,
+}: Pick<
+ OperationContext,
+ "inputType" | "nonJsonBodyType" | "pathType" | "queryType"
+>) {
// Widen optional fields with `| undefined` at the serialization
// boundary. Outbound payloads are JSON.stringified, which drops
// `undefined`, so callers can pass `{ field: undefined }` even
// under exactOptionalPropertyTypes. A non-JSON `body` field holds
// a BodyInit class instance, which UndefinedOnPartialDeep would
- // mangle, so widen everything else and re-intersect `body`
- return hasNonJsonBody
- ? `UndefinedOnPartialDeep> & Pick<${inputTypeName}, "body">`
- : `UndefinedOnPartialDeep<${inputTypeName}>`;
+ // mangle, so only the parameters beside it are widened
+ if (!nonJsonBodyType) {
+ return `UndefinedOnPartialDeep<${inputType.getName()}>`;
+ }
+
+ const parts = widenedInputParts({ nonJsonBodyType, pathType, queryType });
+
+ return parts.length > 0
+ ? `${nonJsonBodyType.getName()} & UndefinedOnPartialDeep<${parts.map((part) => part.getName()).join(" & ")}>`
+ : inputType.getName();
}
function addInputTypeArgument(
documentCtx: DocumentContext,
- { commandClass, inputType, inputTypeNode, nonJsonBodyType }: OperationContext,
+ operationCtx: OperationContext,
) {
- const inputTypeArg = widenedInputType(inputType.getName(), !!nonJsonBodyType);
+ const { commandClass, inputType, inputTypeNode, nonJsonBodyType } =
+ operationCtx;
+ const inputTypeArg = widenedInputType(operationCtx);
+ const parts = widenedInputParts(operationCtx);
+
+ for (const part of parts) {
+ ensureTypeImport(documentCtx.typesImportDecl, part);
+ }
// `A | never` is `A`, so the member is left out
if (inputTypeNode !== neverKeyword) {
documentCtx.inputTypeArgs.add(inputTypeArg);
- documentCtx.inputTypeNames.add(inputType.getName());
+
+ for (const name of parts.length > 0 && nonJsonBodyType
+ ? [nonJsonBodyType, ...parts].map((part) => part.getName())
+ : [inputType.getName()]) {
+ documentCtx.inputTypeNames.add(name);
+ }
}
commandClass.getExtends()?.addTypeArgument(inputTypeArg);
@@ -1254,6 +1364,154 @@ function addInlineOutput(
});
}
+function resolveSchema(
+ refs: $Refs,
+ operationId: string,
+ schema: SchemaOrRef,
+ // oxlint-disable-next-line block65/no-explicit-return-type -- refs.get gives a JSON Schema draft type, which callers can't index as an OAS schema
+): ContentSchemaObject {
+ if (!isReferenceObject(schema)) {
+ return schema;
+ }
+
+ const resolved = refs.get(schema.$ref);
+
+ if (!resolved || typeof resolved !== "object" || Array.isArray(resolved)) {
+ throw new Error(`${operationId}: ${schema.$ref} does not resolve`);
+ }
+
+ return resolved;
+}
+
+function sequentialContent(refs: $Refs, operationObject: OperationWithId) {
+ const { operationId } = operationObject;
+ const res = firstSuccessResponse(operationObject);
+
+ if (!res || res.content?.["application/json"]) {
+ return;
+ }
+
+ const found = Object.entries(
+ res.content ?? {},
+ ).find(
+ ([mediaType, media]) =>
+ media.itemSchema && mediaType in sequentialMediaTypes,
+ );
+
+ const media = found && sequentialMediaTypes[found[0]];
+
+ if (!found?.[1].itemSchema || !media) {
+ return;
+ }
+
+ const { contentProperty } = media;
+ const itemSchema = resolveSchema(refs, operationId, found[1].itemSchema);
+ const variants = (itemSchema.oneOf ?? itemSchema.anyOf ?? [itemSchema]).map(
+ (variant) => resolveSchema(refs, operationId, variant),
+ );
+
+ const contents = variants.map((variant) => {
+ const content = contentProperty
+ ? variant.properties?.[contentProperty]
+ : variant;
+
+ // a variant composed further, or content declared outside the variant,
+ // would otherwise pass as plain text
+ if (
+ !content ||
+ variant.allOf ||
+ (variant !== itemSchema && (variant.oneOf || variant.anyOf))
+ ) {
+ throw new Error(
+ `${operationId}: each itemSchema variant needs its own ${contentProperty ?? "content"} schema`,
+ );
+ }
+
+ return resolveSchema(refs, operationId, content);
+ });
+
+ // an item content with no contentMediaType is a string, as SSE data is
+ const mediaTypes = new Set(
+ contents.map((content) => content.contentMediaType ?? "text/plain"),
+ );
+
+ // dataTransformer is per command, so every variant shares one decoding
+ const [contentMediaType] = mediaTypes;
+ const decoding = contentMediaType && dataDecodings[contentMediaType];
+
+ if (mediaTypes.size !== 1 || !decoding) {
+ throw new Error(
+ `${operationId}: item content media types ${[...mediaTypes].join(", ")} need one decoding, from ${Object.keys(dataDecodings).join(", ")}`,
+ );
+ }
+
+ const decodedSchemas = contents.map((content) => {
+ const decoded = decoding.decodedSchema(content);
+
+ if (!decoded) {
+ throw new Error(
+ `${operationId}: item content of ${contentMediaType} needs a contentSchema`,
+ );
+ }
+
+ return decoded;
+ });
+
+ const schemas = [
+ ...new Map(
+ decodedSchemas.map((schema) => [JSON.stringify(schema), schema]),
+ ).values(),
+ ];
+
+ return {
+ media,
+ decoding,
+ schema:
+ schemas.length === 1 && schemas[0] ? schemas[0] : { anyOf: schemas },
+ };
+}
+
+type SequentialContent = NonNullable>;
+
+function addSequentialOutput(
+ documentCtx: DocumentContext,
+ { commandClass }: OperationContext,
+ { media, decoding, schema }: SequentialContent,
+) {
+ const outputType = schemaToType(
+ documentCtx.typesAndInterfaces,
+ {},
+ "",
+ schema,
+ );
+
+ const outputTypeAlias = documentCtx.typesFile.addTypeAlias({
+ name: pascalCase(commandClass.getName() || "INVALID", "Output"),
+ type: outputType.type ?? unspecifiedKeyword,
+ isExported: true,
+ });
+
+ ensureTypeImport(documentCtx.typesImportDecl, outputTypeAlias);
+
+ commandClass.getExtends()?.getExpression().replaceWithText(media.baseClass);
+ commandClass.getExtends()?.addTypeArgument(outputTypeAlias.getName());
+
+ documentCtx.sequentialOutputs.push({
+ output: outputTypeAlias.getName(),
+ item: media.itemType(outputTypeAlias.getName()),
+ });
+
+ if (decoding.transformer) {
+ commandClass.addProperty({
+ name: "dataTransformer",
+ scope: Scope.Public,
+ hasOverrideKeyword: true,
+ isReadonly: true,
+ initializer: decoding.transformer,
+ });
+ }
+}
+
function jsonOutputRef(jsonResponse: oas31.MediaTypeObject) {
const arrayRef =
jsonResponse.schema &&
@@ -1275,6 +1533,7 @@ function addOutputTypeArgument(
documentCtx: DocumentContext,
operationCtx: OperationContext,
operationObject: OperationWithId,
+ sequential: SequentialContent | undefined,
) {
const { commandClass } = operationCtx;
@@ -1317,6 +1576,11 @@ function addOutputTypeArgument(
const jsonResponse = response.content?.["application/json"];
if (!jsonResponse) {
+ if (sequential) {
+ addSequentialOutput(documentCtx, operationCtx, sequential);
+ hasOutputType = true;
+ }
+
break;
}
@@ -1344,17 +1608,19 @@ function addOutputTypeArgument(
function registerValidatedCommand(
documentCtx: DocumentContext,
commandName: string,
- wireSchemas: { response?: string },
+ wireSchemas: { response?: string; data?: string },
) {
- // Static schema attachment is deferred to the validated module, so
- // the base command module imports zero schemas. rest-client reads
- // the response schema from the validated subclass, and the server
- // middleware imports body, param and query schemas directly
- if (wireSchemas.response) {
- documentCtx.validatedSubclasses.push({
- commandName,
- responseSchema: wireSchemas.response,
- });
+ // Lean commands import zero schemas. A validated subclass sets
+ // the field that rest-client's parse hooks read. The server middleware
+ // imports body, param and query schemas directly.
+ //
+ // sequentialContent skips a JSON response, so one of these is set at most
+ const [field, schema] = wireSchemas.data
+ ? (["dataSchema", wireSchemas.data] as const)
+ : (["responseSchema", wireSchemas.response] as const);
+
+ if (schema) {
+ documentCtx.validatedSubclasses.push({ commandName, field, schema });
} else {
documentCtx.validatedReExports.push(commandName);
}
@@ -1530,18 +1796,18 @@ function superArguments({
}
function addInputConstructor(
- { commandClass, inputType, headerType, wrapJsonBody }: OperationContext,
+ operationCtx: OperationContext,
inputs: ConstructorInputs,
) {
- const { hasNonJsonBody, hasHeaders, allInputOptional, allHeadersOptional } =
- inputs;
+ const { commandClass, inputType, headerType, wrapJsonBody } = operationCtx;
+ const { hasHeaders, allInputOptional, allHeadersOptional } = inputs;
const ctor = commandClass.addConstructor();
if (!isUnspecifiedKeyword(inputType)) {
const cctorParam = ctor.addParameter({
name: "input",
- type: widenedInputType(inputType.getName(), hasNonJsonBody),
+ type: widenedInputType(operationCtx),
...(allInputOptional && { hasQuestionToken: true }),
});
@@ -1644,14 +1910,17 @@ function processOperation(
...input,
};
+ const sequential = sequentialContent(documentCtx.refs, operationObject);
+
const wireSchemas = registerOperationValidators(
documentCtx,
operationCtx,
operationObject,
+ sequential,
);
addInputTypeArgument(documentCtx, operationCtx);
- addOutputTypeArgument(documentCtx, operationCtx, operationObject);
+ addOutputTypeArgument(documentCtx, operationCtx, operationObject, sequential);
registerValidatedCommand(documentCtx, operationCtx.commandName, wireSchemas);
addQueryAndHeaderTypeArguments(operationCtx);
addCommandConstructor(operationCtx, path);
@@ -1728,8 +1997,14 @@ function emitClientModule(
documentCtx: DocumentContext,
schema: oas31.OpenAPIObject,
) {
- const { mainFile, typesFile, outputTypes, inputTypeArgs, inputTypeNames } =
- documentCtx;
+ const {
+ mainFile,
+ typesFile,
+ outputTypes,
+ sequentialOutputs,
+ inputTypeArgs,
+ inputTypeNames,
+ } = documentCtx;
const serviceClientClassName = "RestServiceClient";
const fetcherName = "createIsomorphicNativeFetcher";
@@ -1744,6 +2019,9 @@ function emitClientModule(
name: configType,
isTypeOnly: true,
},
+ ...(sequentialOutputs.length > 0
+ ? [{ name: "ParsedStreamEvent", isTypeOnly: true }]
+ : []),
],
});
@@ -1755,10 +2033,14 @@ function emitClientModule(
});
// Commands from another generated client fail the ``
- // constraint on `.json()`, which guards against mixing clients
- const outputUnionMembers = [...outputTypes]
- .map((t) => (typeof t === "string" ? t : t.getName()))
- .filter((name): name is string => !!name && name !== unspecifiedKeyword);
+ // constraint on `.json()` and `.stream()`, which guards against mixing
+ // clients. stream() checks a sequential command's item type
+ const outputUnionMembers = [
+ ...[...outputTypes]
+ .map((t) => (typeof t === "string" ? t : t.getName()))
+ .filter((name): name is string => !!name && name !== unspecifiedKeyword),
+ ...sequentialOutputs.map(({ item }) => item),
+ ];
const allInputs =
inputTypeArgs.size > 0
@@ -1783,7 +2065,11 @@ function emitClientModule(
.map((n) => n.replace(/\[\]$/, "")),
);
- const importNames = new Set([...inputTypeNames, ...outputTypeNames]);
+ const importNames = new Set([
+ ...inputTypeNames,
+ ...outputTypeNames,
+ ...sequentialOutputs.map(({ output }) => output),
+ ]);
if (importNames.size > 0) {
mainFile.addImportDeclaration({
@@ -1817,9 +2103,9 @@ function emitValidatedModule({
validatedSubclasses,
validatedReExports,
}: DocumentContext) {
- // Build the validated module. Subclasses attach `static responseSchema`,
- // and commands lacking one are re-exported unchanged so the module keeps
- // export parity with the base and stays alias-safe
+ // Build the validated module. Subclasses set a schema field, and commands
+ // lacking a schema are re-exported unchanged so the module keeps export
+ // parity with the base and stays alias-safe
const commandsModuleSpecifier = `./${commandsFile.getBaseNameWithoutExtension()}.js`;
if (validatedSubclasses.length > 0) {
@@ -1840,16 +2126,18 @@ function emitValidatedModule({
namespaceImport: schemasNs,
});
- for (const { commandName, responseSchema } of validatedSubclasses) {
+ for (const { commandName, field, schema } of validatedSubclasses) {
commandsValidatedFile.addClass({
name: commandName,
isExported: true,
extends: `${commandsNs}.${commandName}`,
properties: [
{
- name: "responseSchema",
- isStatic: true,
- initializer: `${schemasNs}.${responseSchema}`,
+ name: field,
+ scope: Scope.Public,
+ hasOverrideKeyword: true,
+ isReadonly: true,
+ initializer: `${schemasNs}.${schema}`,
},
],
});
@@ -1933,6 +2221,7 @@ export async function processOpenApiDocument(
inputTypeArgs: new Set(),
inputTypeNames: new Set(),
validatedSubclasses: [],
+ sequentialOutputs: [],
validatedReExports: [],
inputOnly: options?.inputOnly,
};
diff --git a/lib/valibot.ts b/lib/valibot.ts
index 489330f..8ed023e 100644
--- a/lib/valibot.ts
+++ b/lib/valibot.ts
@@ -1042,6 +1042,7 @@ type SchemaNamePair = { inputName: string; wireName: string };
type OperationSchemaNames = {
json?: string;
response?: string;
+ data?: string;
param?: string;
query?: string;
header?: string;
@@ -1093,7 +1094,7 @@ function emitNamePair(
function emitSchemaPair(
target: OperationTarget,
- segment: "body" | "response",
+ segment: "body" | "response" | "data",
schema: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject,
) {
return emitNamePair(target, segment, (mode) =>
@@ -1178,6 +1179,7 @@ export function createValidatorForOperationInput(
input: {
body?: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject;
response?: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject;
+ data?: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject;
params: oas30.ParameterObject[];
query: oas30.ParameterObject[];
header: oas30.ParameterObject[];
@@ -1191,6 +1193,7 @@ export function createValidatorForOperationInput(
response: input.response
? emitSchemaPair(target, "response", input.response)
: undefined,
+ data: input.data ? emitSchemaPair(target, "data", input.data) : undefined,
param:
input.params.length > 0
? addParams(target, "params", input.params)
From d92d4af5b9a355b9fda5e304d0950196de6c7ed0 Mon Sep 17 00:00:00 2001
From: "maxholman[bot]" <321308195+maxholman[bot]@users.noreply.github.com>
Date: Sun, 27 Sep 2026 14:44:32 +0800
Subject: [PATCH 2/6] fix: type a bodiless success as undefined, and no success
as never
A 2xx response without content, a 204 or otherwise, gives the command an
output of undefined, which rest-client's json() and send() resolve with,
and undefined stays in AllOutputs. An operation documenting no 2xx
response at all gets never, which drops out of AllOutputs. Both used to
fall back to unknown, which the generated client's AllOutputs constraint
rejects.
Co-Authored-By: LLM
---
.../codegen-regressions.test.ts.snap | 83 +++++++++++++++++++
__tests__/codegen-regressions.test.ts | 44 ++++++++++
.../docker/.openapi-codegen-manifest.json | 4 +-
__tests__/fixtures/docker/commands.ts | 69 ++++++++-------
.../openai/.openapi-codegen-manifest.json | 2 +-
.../petstore/.openapi-codegen-manifest.json | 2 +-
.../test1/.openapi-codegen-manifest.json | 4 +-
__tests__/fixtures/test1/commands.ts | 3 +-
lib/process-document.ts | 31 ++++---
9 files changed, 194 insertions(+), 48 deletions(-)
diff --git a/__tests__/__snapshots__/codegen-regressions.test.ts.snap b/__tests__/__snapshots__/codegen-regressions.test.ts.snap
index 8526dc0..856e8f6 100644
--- a/__tests__/__snapshots__/codegen-regressions.test.ts.snap
+++ b/__tests__/__snapshots__/codegen-regressions.test.ts.snap
@@ -64,6 +64,89 @@ export const inArrayItemSchema = v.array(v.looseObject(
"
`;
+exports[`a bodiless success is undefined, and no documented success is never > commands.ts 1`] = `
+"import { Command } from "@block65/rest-client";
+import type { UndefinedOnPartialDeep } from "type-fest";
+import type { NoContentCommandInput, OkWithoutBodyCommandInput, FailuresOnlyCommandInput, NothingDocumentedCommandInput, BytesCommandInput } from "./types.js";
+
+
+/**
+ * NoContentCommand
+ *
+ */
+export class NoContentCommand extends Command, undefined> {
+ public override method = "delete" as const;
+
+ constructor() {
+ super("/no-content");
+ }
+}
+
+/**
+ * OkWithoutBodyCommand
+ *
+ */
+export class OkWithoutBodyCommand extends Command, undefined> {
+ public override method = "post" as const;
+
+ constructor() {
+ super("/ok-without-body");
+ }
+}
+
+/**
+ * FailuresOnlyCommand
+ *
+ */
+export class FailuresOnlyCommand extends Command, never> {
+ public override method = "post" as const;
+
+ constructor() {
+ super("/failures-only");
+ }
+}
+
+/**
+ * NothingDocumentedCommand
+ *
+ */
+export class NothingDocumentedCommand extends Command, never> {
+ public override method = "post" as const;
+
+ constructor() {
+ super("/nothing-documented");
+ }
+}
+
+/**
+ * BytesCommand
+ *
+ */
+export class BytesCommand extends Command> {
+ public override method = "get" as const;
+
+ constructor() {
+ super("/bytes");
+ }
+}
+"
+`;
+
+exports[`a bodiless success is undefined, and no documented success is never > main.ts 1`] = `
+"import { RestServiceClient, type RestServiceClientConfig } from "@block65/rest-client";
+
+export { ResponseValidationError } from "@block65/rest-client";
+
+type AllOutputs = undefined;
+
+export class TestRestClient extends RestServiceClient {
+ constructor(baseUrl: string | URL = new URL('https://api.example.com/'), config?: RestServiceClientConfig) {
+ super(baseUrl, config);
+ }
+}
+"
+`;
+
exports[`a oneOf query param keeps the stringish wire types in every branch > types.ts 1`] = `
"import type { UndefinedOnPartialDeep } from "type-fest";
diff --git a/__tests__/codegen-regressions.test.ts b/__tests__/codegen-regressions.test.ts
index d659e92..8523868 100644
--- a/__tests__/codegen-regressions.test.ts
+++ b/__tests__/codegen-regressions.test.ts
@@ -551,3 +551,47 @@ test("a value the schema leaves open is a JsonValue on both sides", async () =>
expect(result.typesFile.getText()).toMatchSnapshot("types.ts");
expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts");
});
+
+test("a bodiless success is undefined, and no documented success is never", async () => {
+ const result = await processOpenApiDocument("/tmp/whatever", {
+ openapi: "3.1.0",
+ info: { title: "Test", version: "1.0.0" },
+ paths: {
+ "/no-content": {
+ delete: {
+ operationId: "noContentCommand",
+ responses: { "204": { description: "No Content" } },
+ },
+ },
+ "/ok-without-body": {
+ post: {
+ operationId: "okWithoutBodyCommand",
+ responses: { "200": { description: "OK" } },
+ },
+ },
+ "/failures-only": {
+ post: {
+ operationId: "failuresOnlyCommand",
+ responses: { "400": { description: "Bad Request" } },
+ },
+ },
+ "/nothing-documented": {
+ post: { operationId: "nothingDocumentedCommand", responses: {} },
+ },
+ "/bytes": {
+ get: {
+ operationId: "bytesCommand",
+ responses: {
+ "200": {
+ description: "OK",
+ content: { "application/octet-stream": {} },
+ },
+ },
+ },
+ },
+ },
+ });
+
+ expect(result.commandsFile.getText()).toMatchSnapshot("commands.ts");
+ expect(result.mainFile.getText()).toMatchSnapshot("main.ts");
+});
diff --git a/__tests__/fixtures/docker/.openapi-codegen-manifest.json b/__tests__/fixtures/docker/.openapi-codegen-manifest.json
index b545407..d3c7dd6 100644
--- a/__tests__/fixtures/docker/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/docker/.openapi-codegen-manifest.json
@@ -1,6 +1,6 @@
{
- "#generator": "99360df640587900347f0725f28f06a7",
- "commands.ts": "0971e86b831d9a354c1e219ecf7e8b7f",
+ "#generator": "b0df896f873e55d15a420e4641684139",
+ "commands.ts": "593afb99bb4daed5e9491b2a55d75d75",
"types.ts": "f1e7d6c61bb9e15c5034a3cc3cc729b5",
"main.ts": "38065305823906f3aa1c7f968e278002",
"valibot.ts": "25d47efaf15282d72c20aed794bd32da",
diff --git a/__tests__/fixtures/docker/commands.ts b/__tests__/fixtures/docker/commands.ts
index 9ef6cdd..30ae456 100644
--- a/__tests__/fixtures/docker/commands.ts
+++ b/__tests__/fixtures/docker/commands.ts
@@ -418,7 +418,8 @@ export class ContainerChangesCommand extends Command<
* @summary Export a container
*/
export class ContainerExportCommand extends Command<
- UndefinedOnPartialDeep
+ UndefinedOnPartialDeep,
+ undefined
> {
public override method = "get" as const;
@@ -485,7 +486,7 @@ export class ContainerStatsCommand extends Command<
*/
export class ContainerResizeCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
ContainerResizeCommandQuery
> {
public override method = "post" as const;
@@ -772,7 +773,7 @@ export class ContainerUnpauseCommand extends Command<
*/
export class ContainerAttachCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
ContainerAttachCommandQuery
> {
public override method = "post" as const;
@@ -794,7 +795,7 @@ export class ContainerAttachCommand extends Command<
*/
export class ContainerAttachWebsocketCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
ContainerAttachWebsocketCommandQuery
> {
public override method = "get" as const;
@@ -862,7 +863,7 @@ export class ContainerDeleteCommand extends Command<
*/
export class ContainerArchiveCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
ContainerArchiveCommandQuery
> {
public override method = "get" as const;
@@ -891,7 +892,7 @@ export class PutContainerArchiveCommand extends Command<
UndefinedOnPartialDeep<
PutContainerArchiveCommandParams & PutContainerArchiveCommandQuery
>,
- unknown,
+ undefined,
PutContainerArchiveCommandQuery
> {
public override method = "put" as const;
@@ -920,7 +921,7 @@ export class PutContainerArchiveCommand extends Command<
*/
export class ContainerArchiveInfoCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
ContainerArchiveInfoCommandQuery
> {
public override method = "head" as const;
@@ -997,7 +998,7 @@ export class ImageListCommand extends Command<
*/
export class ImageBuildCommand extends Command<
ImageBuildCommandBodyNonJson & UndefinedOnPartialDeep,
- unknown,
+ undefined,
ImageBuildCommandQuery,
ImageBuildCommandHeader
> {
@@ -1101,7 +1102,7 @@ export class BuildPruneCommand extends Command<
export class ImageCreateCommand extends Command<
ImageCreateCommandBodyNonJson &
UndefinedOnPartialDeep,
- unknown,
+ undefined,
ImageCreateCommandQuery,
ImageCreateCommandHeader
> {
@@ -1179,7 +1180,7 @@ export class ImageHistoryCommand extends Command<
*/
export class ImagePushCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
ImagePushCommandQuery,
ImagePushCommandHeader
> {
@@ -1206,7 +1207,7 @@ export class ImagePushCommand extends Command<
*/
export class ImageTagCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
ImageTagCommandQuery
> {
public override method = "post" as const;
@@ -1545,7 +1546,7 @@ export class ImageGetAllCommand extends Command<
*/
export class ImageLoadCommand extends Command<
ImageLoadCommandBodyNonJson & UndefinedOnPartialDeep,
- unknown,
+ undefined,
ImageLoadCommandQuery
> {
public override method = "post" as const;
@@ -1584,7 +1585,8 @@ export class ContainerExecCommand extends Command<
* @summary Start an exec instance
*/
export class ExecStartCommand extends Command<
- UndefinedOnPartialDeep
+ UndefinedOnPartialDeep,
+ undefined
> {
public override method = "post" as const;
@@ -1602,7 +1604,7 @@ export class ExecStartCommand extends Command<
*/
export class ExecResizeCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
ExecResizeCommandQuery
> {
public override method = "post" as const;
@@ -1689,7 +1691,7 @@ export class VolumeInspectCommand extends Command<
*/
export class VolumeUpdateCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
VolumeUpdateCommandQuery
> {
public override method = "put" as const;
@@ -1825,7 +1827,8 @@ export class NetworkCreateCommand extends Command<
* @summary Connect a container to a network
*/
export class NetworkConnectCommand extends Command<
- UndefinedOnPartialDeep
+ UndefinedOnPartialDeep,
+ undefined
> {
public override method = "post" as const;
@@ -1841,7 +1844,8 @@ export class NetworkConnectCommand extends Command<
* @summary Disconnect a container from a network
*/
export class NetworkDisconnectCommand extends Command<
- UndefinedOnPartialDeep
+ UndefinedOnPartialDeep,
+ undefined
> {
public override method = "post" as const;
@@ -1976,7 +1980,7 @@ export class PluginDeleteCommand extends Command<
*/
export class PluginEnableCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
PluginEnableCommandQuery
> {
public override method = "post" as const;
@@ -1998,7 +2002,7 @@ export class PluginEnableCommand extends Command<
*/
export class PluginDisableCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
PluginDisableCommandQuery
> {
public override method = "post" as const;
@@ -2068,7 +2072,8 @@ export class PluginCreateCommand extends Command<
* @summary Push a plugin
*/
export class PluginPushCommand extends Command<
- UndefinedOnPartialDeep
+ UndefinedOnPartialDeep,
+ undefined
> {
public override method = "post" as const;
@@ -2137,7 +2142,7 @@ export class NodeInspectCommand extends Command<
*/
export class NodeDeleteCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
NodeDeleteCommandQuery
> {
public override method = "delete" as const;
@@ -2155,7 +2160,7 @@ export class NodeDeleteCommand extends Command<
*/
export class NodeUpdateCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
NodeUpdateCommandQuery
> {
public override method = "post" as const;
@@ -2209,7 +2214,8 @@ export class SwarmInitCommand extends Command<
* @summary Join an existing swarm
*/
export class SwarmJoinCommand extends Command<
- UndefinedOnPartialDeep
+ UndefinedOnPartialDeep,
+ undefined
> {
public override method = "post" as const;
@@ -2226,7 +2232,7 @@ export class SwarmJoinCommand extends Command<
*/
export class SwarmLeaveCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
SwarmLeaveCommandQuery
> {
public override method = "post" as const;
@@ -2244,7 +2250,7 @@ export class SwarmLeaveCommand extends Command<
*/
export class SwarmUpdateCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
SwarmUpdateCommandQuery
> {
public override method = "post" as const;
@@ -2292,7 +2298,8 @@ export class SwarmUnlockkeyCommand extends Command<
* @summary Unlock a locked manager
*/
export class SwarmUnlockCommand extends Command<
- UndefinedOnPartialDeep
+ UndefinedOnPartialDeep,
+ undefined
> {
public override method = "post" as const;
@@ -2370,7 +2377,8 @@ export class ServiceInspectCommand extends Command<
* @summary Delete a service
*/
export class ServiceDeleteCommand extends Command<
- UndefinedOnPartialDeep
+ UndefinedOnPartialDeep,
+ undefined
> {
public override method = "delete" as const;
@@ -2588,7 +2596,7 @@ export class SecretDeleteCommand extends Command<
*/
export class SecretUpdateCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
SecretUpdateCommandQuery
> {
public override method = "post" as const;
@@ -2679,7 +2687,7 @@ export class ConfigDeleteCommand extends Command<
*/
export class ConfigUpdateCommand extends Command<
UndefinedOnPartialDeep,
- unknown,
+ undefined,
ConfigUpdateCommandQuery
> {
public override method = "post" as const;
@@ -2740,7 +2748,8 @@ export class DistributionInspectCommand extends Command<
* @summary Initialize interactive session
*/
export class SessionCommand extends Command<
- UndefinedOnPartialDeep
+ UndefinedOnPartialDeep,
+ never
> {
public override method = "post" as const;
diff --git a/__tests__/fixtures/openai/.openapi-codegen-manifest.json b/__tests__/fixtures/openai/.openapi-codegen-manifest.json
index 4870177..a6aa0b7 100644
--- a/__tests__/fixtures/openai/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/openai/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "99360df640587900347f0725f28f06a7",
+ "#generator": "b0df896f873e55d15a420e4641684139",
"commands.ts": "99ac148005b23e09a53adb6b62620dc0",
"types.ts": "bc453b1a7fcd330909a23ae01be97f52",
"main.ts": "8147604a37254400a015b78650466094",
diff --git a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
index 498a130..1f6421e 100644
--- a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "99360df640587900347f0725f28f06a7",
+ "#generator": "b0df896f873e55d15a420e4641684139",
"commands.ts": "69c6a9f2924568fc08fd489508c9df34",
"types.ts": "ea65c3e67352d4e22b97af80085727b4",
"main.ts": "64edb526dcbcbd345e631ccff959f11d",
diff --git a/__tests__/fixtures/test1/.openapi-codegen-manifest.json b/__tests__/fixtures/test1/.openapi-codegen-manifest.json
index 05ffd95..d64d38c 100644
--- a/__tests__/fixtures/test1/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/test1/.openapi-codegen-manifest.json
@@ -1,6 +1,6 @@
{
- "#generator": "99360df640587900347f0725f28f06a7",
- "commands.ts": "0e6777a5287ed2535587a0f5180bbbd5",
+ "#generator": "b0df896f873e55d15a420e4641684139",
+ "commands.ts": "eab0f690af8f1e2d973f6504de470ba9",
"types.ts": "7bb3f162d3321e4db51673d3f3ba35ac",
"main.ts": "5c768be2e48d0b06d7ed8da665653404",
"valibot.ts": "c8e831e3378ad09f85be0d368e1f0cf2",
diff --git a/__tests__/fixtures/test1/commands.ts b/__tests__/fixtures/test1/commands.ts
index 0bf032b..ca3d661 100644
--- a/__tests__/fixtures/test1/commands.ts
+++ b/__tests__/fixtures/test1/commands.ts
@@ -212,7 +212,8 @@ export class GetBillingAccountPortalCommand extends Command<
*
*/
export class LinkBillingAccountCommand extends Command<
- UndefinedOnPartialDeep
+ UndefinedOnPartialDeep,
+ never
> {
public override method = "post" as const;
diff --git a/lib/process-document.ts b/lib/process-document.ts
index 777a1e6..d47d5b6 100644
--- a/lib/process-document.ts
+++ b/lib/process-document.ts
@@ -1529,6 +1529,17 @@ function jsonOutputRef(jsonResponse: oas31.MediaTypeObject) {
return outputRef ? { ref: outputRef, isArray: !!arrayRef } : undefined;
}
+// rest-client resolves a bodiless success, a 204 included, with undefined
+function isBodiless(
+ statusCode: string,
+ res: oas31.ResponseObject | oas31.ReferenceObject,
+) {
+ return (
+ statusCode === "204" ||
+ (!("$ref" in res) && Object.keys(res.content ?? {}).length === 0)
+ );
+}
+
function addOutputTypeArgument(
documentCtx: DocumentContext,
operationCtx: OperationContext,
@@ -1537,20 +1548,19 @@ function addOutputTypeArgument(
) {
const { commandClass } = operationCtx;
- // this is just like a 204 response
let hasOutputType = false;
- if (
- !operationObject.responses ||
- Object.keys(operationObject.responses).length === 0
- ) {
- commandClass.getExtends()?.addTypeArgument(unspecifiedKeyword);
+ const successResponses = Object.entries({
+ ...operationObject.responses,
+ }).filter(([s]) => s.startsWith("2"));
+
+ // with only failures documented, json() and send() only ever reject
+ if (successResponses.length === 0) {
+ commandClass.getExtends()?.addTypeArgument(neverKeyword);
hasOutputType = true;
}
- for (const [statusCode, response] of Object.entries({
- ...operationObject.responses,
- }).filter(([s]) => s.startsWith("2"))) {
+ for (const [statusCode, response] of successResponses) {
// Output is one type argument, so the first usable 2xx response
// settles it. An operation documenting both a 200 and a 204 would
// otherwise add a second argument, which lands in the query slot
@@ -1559,8 +1569,7 @@ function addOutputTypeArgument(
break;
}
- // early out if response is 204
- if (statusCode === "204") {
+ if (isBodiless(statusCode, response)) {
commandClass.getExtends()?.addTypeArgument(emptyKeyword);
documentCtx.outputTypes.add(emptyKeyword);
From 78bf5bb26f79a263c381f29e8c8afd7e299b80c2 Mon Sep 17 00:00:00 2001
From: "maxholman[bot]" <321308195+maxholman[bot]@users.noreply.github.com>
Date: Sun, 27 Sep 2026 14:44:32 +0800
Subject: [PATCH 3/6] refactor: read documents as OAS 3.2 through openapi3-ts's
oas32 types
The generator took oas31 types and patched in what 3.2 added (itemSchema,
contentSchema, `in: querystring`) with local shims and casts. It now takes
oas32.OpenAPIObject, which types all of them, and reads a 3.1 document
unchanged. Two 3.2 rules the old types hid are handled: a schema may be
true or false, which becomes JsonValue/never and jsonValueSchema/v.never(),
and a media type may be a $ref, which resolves before use.
The test helpers move into __tests__/helpers.ts, with names that say what
each does in place of a `For` suffix, and expectGenerated writes each
snapshot under the test file and test names as written.
Co-Authored-By: LLM
---
__tests__/codegen-regressions.test.ts | 24 +-
__tests__/event-stream.test.ts | 17 +-
.../docker/.openapi-codegen-manifest.json | 2 +-
.../openai/.openapi-codegen-manifest.json | 2 +-
.../petstore/.openapi-codegen-manifest.json | 2 +-
.../test1/.openapi-codegen-manifest.json | 2 +-
__tests__/generate.ts | 49 -----
__tests__/helpers.ts | 46 ++++
__tests__/nullables.test.ts | 8 +-
__tests__/query-roundtrip.test.ts | 42 ++--
__tests__/query-serializer.test.ts | 29 ++-
__tests__/typescript-hint.test.ts | 8 +-
lib/build.ts | 4 +-
lib/process-document.ts | 207 ++++++++++--------
lib/process-schema.ts | 95 ++++----
lib/utils.ts | 17 +-
lib/valibot.ts | 110 ++++++----
17 files changed, 361 insertions(+), 303 deletions(-)
delete mode 100644 __tests__/generate.ts
create mode 100644 __tests__/helpers.ts
diff --git a/__tests__/codegen-regressions.test.ts b/__tests__/codegen-regressions.test.ts
index 8523868..3e17a03 100644
--- a/__tests__/codegen-regressions.test.ts
+++ b/__tests__/codegen-regressions.test.ts
@@ -1,4 +1,4 @@
-import type { oas31 } from "openapi3-ts";
+import type { oas32 } from "openapi3-ts";
import { expect, test } from "vitest";
import { processOpenApiDocument } from "../lib/process-document.ts";
@@ -10,7 +10,7 @@ const respOk = {
} as const;
test("main.ts emits file-level `import type` for type-only imports", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
paths: {
@@ -39,7 +39,7 @@ test("main.ts emits file-level `import type` for type-only imports", async () =>
});
test("optional query params do not carry `| undefined` in their property type", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
paths: {
@@ -72,7 +72,7 @@ test("optional query params do not carry `| undefined` in their property type",
});
test("AllInputs union carries every command that takes an input", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
paths: {
@@ -148,7 +148,7 @@ test.for([
// A one-member `anyOf` or `oneOf` is that member. `v.union` of one option
// only wraps its issues, and the block65 valibot rules reject it
-test.for<[string, oas31.SchemaObject]>([
+test.for<[string, oas32.SchemaObject]>([
["anyOf", { anyOf: [{ type: "string" }] }],
["oneOf", { oneOf: [{ type: "string" }] }],
["two-member oneOf", { oneOf: [{ type: "string" }, { type: "number" }] }],
@@ -164,7 +164,7 @@ test.for<[string, oas31.SchemaObject]>([
},
);
-function docWithSchema(name: string, schema: oas31.SchemaObject) {
+function docWithSchema(name: string, schema: oas32.SchemaObject) {
return {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
@@ -238,7 +238,7 @@ test("an empty properties bag is a record, not an empty object type", async () =
});
test("the generated JSON body type is PascalCase", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
paths: {
@@ -267,7 +267,7 @@ test("the generated JSON body type is PascalCase", async () => {
});
test("an operation with both a 200 and a 204 emits one output type argument", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
paths: {
@@ -299,7 +299,7 @@ test("an operation with both a 200 and a 204 emits one output type argument", as
});
test("an array request body with parameters stays readable as both", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
paths: {
@@ -334,7 +334,7 @@ test("an array request body with parameters stays readable as both", async () =>
});
test("nested query param members get the same stringish treatment as top-level ones", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
paths: {
@@ -381,7 +381,7 @@ test("nested query param members get the same stringish treatment as top-level o
});
test("json request body members keep their real JSON types, nested included", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
paths: {
@@ -423,7 +423,7 @@ test("json request body members keep their real JSON types, nested included", as
// Composition has to forward the codegen options the same way the array and
// object branches do, or a oneOf collapses back to the JSON types
test("a oneOf query param keeps the stringish wire types in every branch", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: { title: "Test", version: "1.0.0" },
paths: {
diff --git a/__tests__/event-stream.test.ts b/__tests__/event-stream.test.ts
index c8cfbf5..a533578 100644
--- a/__tests__/event-stream.test.ts
+++ b/__tests__/event-stream.test.ts
@@ -1,15 +1,12 @@
-import type { oas31 } from "openapi3-ts";
+import type { oas32 } from "openapi3-ts";
import { describe, expect, test } from "vitest";
import { processOpenApiDocument } from "../lib/process-document.ts";
-// OAS 3.2 added itemSchema, which the 3.1 types predate
-type ItemSchema = oas31.SchemaObject | oas31.ReferenceObject;
-
function documentWith(
- itemSchema: ItemSchema | undefined,
- schemas: Record = {},
+ itemSchema: oas32.SchemaObject | oas32.ReferenceObject | undefined,
+ schemas: Record = {},
) {
- const document: oas31.OpenAPIObject = {
+ const document: oas32.OpenAPIObject = {
openapi: "3.2.0",
info: { title: "Test", version: "1.0.0" },
components: { schemas },
@@ -22,7 +19,7 @@ function documentWith(
description: "OK",
content: {
"text/event-stream": itemSchema
- ? ({ itemSchema } as oas31.MediaTypeObject)
+ ? { itemSchema }
: { schema: { type: "string", format: "binary" } },
},
},
@@ -35,7 +32,7 @@ function documentWith(
return document;
}
-function jsonData(contentSchema: ItemSchema) {
+function jsonData(contentSchema: oas32.SchemaObject | oas32.ReferenceObject) {
return {
type: "string" as const,
contentMediaType: "application/json",
@@ -124,7 +121,7 @@ test("data with no contentMediaType is text", async () => {
});
describe("event-stream refusals", () => {
- test.each<[string, ItemSchema, RegExp]>([
+ test.each<[string, oas32.SchemaObject, RegExp]>([
[
"variants with different content media types",
{
diff --git a/__tests__/fixtures/docker/.openapi-codegen-manifest.json b/__tests__/fixtures/docker/.openapi-codegen-manifest.json
index d3c7dd6..47fd1e9 100644
--- a/__tests__/fixtures/docker/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/docker/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "b0df896f873e55d15a420e4641684139",
+ "#generator": "6ced852ea5759e52e00d4a09f1cc0f47",
"commands.ts": "593afb99bb4daed5e9491b2a55d75d75",
"types.ts": "f1e7d6c61bb9e15c5034a3cc3cc729b5",
"main.ts": "38065305823906f3aa1c7f968e278002",
diff --git a/__tests__/fixtures/openai/.openapi-codegen-manifest.json b/__tests__/fixtures/openai/.openapi-codegen-manifest.json
index a6aa0b7..270aa2e 100644
--- a/__tests__/fixtures/openai/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/openai/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "b0df896f873e55d15a420e4641684139",
+ "#generator": "6ced852ea5759e52e00d4a09f1cc0f47",
"commands.ts": "99ac148005b23e09a53adb6b62620dc0",
"types.ts": "bc453b1a7fcd330909a23ae01be97f52",
"main.ts": "8147604a37254400a015b78650466094",
diff --git a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
index 1f6421e..af3b74d 100644
--- a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "b0df896f873e55d15a420e4641684139",
+ "#generator": "6ced852ea5759e52e00d4a09f1cc0f47",
"commands.ts": "69c6a9f2924568fc08fd489508c9df34",
"types.ts": "ea65c3e67352d4e22b97af80085727b4",
"main.ts": "64edb526dcbcbd345e631ccff959f11d",
diff --git a/__tests__/fixtures/test1/.openapi-codegen-manifest.json b/__tests__/fixtures/test1/.openapi-codegen-manifest.json
index d64d38c..879e0d7 100644
--- a/__tests__/fixtures/test1/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/test1/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "b0df896f873e55d15a420e4641684139",
+ "#generator": "6ced852ea5759e52e00d4a09f1cc0f47",
"commands.ts": "eab0f690af8f1e2d973f6504de470ba9",
"types.ts": "7bb3f162d3321e4db51673d3f3ba35ac",
"main.ts": "5c768be2e48d0b06d7ed8da665653404",
diff --git a/__tests__/generate.ts b/__tests__/generate.ts
deleted file mode 100644
index bc7b76c..0000000
--- a/__tests__/generate.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import path from "node:path";
-import type { oas31 } from "openapi3-ts";
-import { processOpenApiDocument } from "../lib/process-document.ts";
-
-// OAS 3.2 added `in: "querystring"`, which the 3.1 types predate
-export type TestParameter =
- | oas31.ParameterObject
- | {
- name: string;
- in: "querystring";
- content: oas31.ParameterObject["content"];
- };
-
-export function documentFor(
- parameters: readonly TestParameter[],
-): oas31.OpenAPIObject {
- return {
- openapi: "3.1.0",
- info: { title: "Test", version: "1.0.0" },
- paths: {
- "/things": {
- get: {
- operationId: "listThingsCommand",
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- TestParameter widens the 3.1 union by the one 3.2 location these tests exercise, and processOpenApiDocument takes a 3.1 document
- parameters: parameters as oas31.ParameterObject[],
- responses: {
- "200": {
- description: "OK",
- content: { "application/json": { schema: { type: "string" } } },
- },
- },
- },
- },
- },
- };
-}
-
-export async function generateFor(parameters: readonly TestParameter[]) {
- // This path names the emitted files, which stay in memory
- const outputDir = path.join(import.meta.dirname, ".generated");
-
- return processOpenApiDocument(outputDir, documentFor(parameters));
-}
-
-export async function commandsFor(parameters: readonly TestParameter[]) {
- const result = await generateFor(parameters);
-
- return result.commandsFile.getText();
-}
diff --git a/__tests__/helpers.ts b/__tests__/helpers.ts
new file mode 100644
index 0000000..e397322
--- /dev/null
+++ b/__tests__/helpers.ts
@@ -0,0 +1,46 @@
+import path from "node:path";
+import type { oas32 } from "openapi3-ts";
+import { processOpenApiDocument } from "../lib/process-document.ts";
+
+function buildQueryParameterDocument(
+ parameters: readonly oas32.ParameterObject[],
+): oas32.OpenAPIObject {
+ return {
+ openapi: "3.2.0",
+ info: { title: "Test", version: "1.0.0" },
+ paths: {
+ "/things": {
+ get: {
+ operationId: "listThingsCommand",
+ parameters: [...parameters],
+ responses: {
+ "200": {
+ description: "OK",
+ content: { "application/json": { schema: { type: "string" } } },
+ },
+ },
+ },
+ },
+ },
+ };
+}
+
+export async function generateWithQueryParameters(
+ parameters: readonly oas32.ParameterObject[],
+) {
+ // This path names the emitted files, which stay in memory
+ const outputDir = path.join(import.meta.dirname, ".generated");
+
+ return processOpenApiDocument(
+ outputDir,
+ buildQueryParameterDocument(parameters),
+ );
+}
+
+export async function generateCommandsText(
+ parameters: readonly oas32.ParameterObject[],
+) {
+ const result = await generateWithQueryParameters(parameters);
+
+ return result.commandsFile.getText();
+}
diff --git a/__tests__/nullables.test.ts b/__tests__/nullables.test.ts
index 6873a5b..ae0e29e 100644
--- a/__tests__/nullables.test.ts
+++ b/__tests__/nullables.test.ts
@@ -1,4 +1,4 @@
-import type { oas31 } from "openapi3-ts";
+import type { oas32 } from "openapi3-ts";
import { expect, test } from "vitest";
import { processOpenApiDocument } from "../lib/process-document.ts";
@@ -120,7 +120,7 @@ test("3.0 nullable objects, arrays, strings and combinator members admit null",
},
},
},
- } as oas31.OpenAPIObject);
+ });
expect(result.typesFile.getText()).toMatchSnapshot("types.ts");
expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts");
@@ -228,7 +228,7 @@ test("oneOf with type null generates v.null()", async () => {
});
test("query and header integer params coerce strings to numbers", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: {
title: "Test",
@@ -304,7 +304,7 @@ test("query and header integer params coerce strings to numbers", async () => {
});
test("header parameters", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
openapi: "3.1.0",
info: {
title: "Test",
diff --git a/__tests__/query-roundtrip.test.ts b/__tests__/query-roundtrip.test.ts
index 79eb594..380b280 100644
--- a/__tests__/query-roundtrip.test.ts
+++ b/__tests__/query-roundtrip.test.ts
@@ -1,10 +1,11 @@
import { Hono } from "hono";
-import type { MiddlewareHandler } from "hono";
-import type { oas31 } from "openapi3-ts";
+import type { Context, MiddlewareHandler } from "hono";
+import type { BlankEnv } from "hono/types";
+import type { oas32 } from "openapi3-ts";
import { expect, test } from "vitest";
import { listauditlogs } from "./fixtures/openai/hono.ts";
import { findPets } from "./fixtures/petstore/hono.ts";
-import { generateFor, type TestParameter } from "./generate.ts";
+import { generateWithQueryParameters } from "./helpers.ts";
// Runs a real query string through the generated middleware and back out
async function validatedQuery(
@@ -13,7 +14,7 @@ async function validatedQuery(
) {
// Coverage of the client half lives with the "query string building" tests
// in @block65/rest-client, so the wire strings here are written by hand
- const res = await appFor(middleware).request(`/target?${search}`);
+ const res = await createHonoApp(middleware).request(`/target?${search}`);
const body = await res.clone().text();
// the body says why a route rejected the query, so the failure output
@@ -36,7 +37,7 @@ test("an absent object query parameter does not materialise", async () => {
// encodings below reach the schema in a shape it turns down. The hook
// throws PublicValidationError, and a bare app returns 500 for it
test("a single value for an array parameter is rejected", async () => {
- const res = await appFor(findPets).request("/target?tags=cat");
+ const res = await createHonoApp(findPets).request("/target?tags=cat");
expect(res.status).toBe(500);
});
@@ -46,7 +47,7 @@ test("joined values for an array parameter are rejected", async () => {
const results = await Promise.all(
searches.map(async (search) => {
- const res = await appFor(findPets).request(`/target?${search}`);
+ const res = await createHonoApp(findPets).request(`/target?${search}`);
return { search, status: res.status };
}),
@@ -64,28 +65,33 @@ test("repeated keys for an array parameter are accepted", async () => {
).resolves.toStrictEqual({ tags: ["cat", "dog"] });
});
+type ValidatedQuery = { out: { query: Record } };
+
// Mounts the middleware on a Hono app, with an untyped handler reading it
-function appFor(middleware: readonly MiddlewareHandler[]) {
+function createHonoApp(middleware: readonly MiddlewareHandler[]) {
const app = new Hono();
for (const handler of middleware) {
app.use("/target", handler);
}
- // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- c.req.valid reads its key from the validator types a route was built with, and these middleware arrive as an opaque array, so the key is unreachable through the spread
- app.get("/target", (c) => c.json(c.req.valid("query" as never)));
+ // the middleware arrive as an opaque array, so the handler names the
+ // validated input it reads
+ app.get("/target", (c: Context) =>
+ c.json(c.req.valid("query")),
+ );
return app;
}
// Collects what the generator says while it walks a document
-async function warningsFrom(parameters: readonly TestParameter[]) {
+async function warningsFrom(parameters: readonly oas32.ParameterObject[]) {
const warnings: string[] = [];
const original = console.warn;
console.warn = (message: string) => warnings.push(message);
try {
- await generateFor(parameters);
+ await generateWithQueryParameters(parameters);
} finally {
console.warn = original;
}
@@ -96,7 +102,7 @@ async function warningsFrom(parameters: readonly TestParameter[]) {
// rest-client encodes only these four
test("a style rest-client cannot encode stops generation", async () => {
await expect(
- generateFor([
+ generateWithQueryParameters([
{
name: "id",
in: "query",
@@ -111,7 +117,7 @@ test("a style rest-client cannot encode stops generation", async () => {
// n/a check through its encoding
test("a scalar in an n/a style and explode stops generation", async () => {
await expect(
- generateFor([
+ generateWithQueryParameters([
{
name: "id",
in: "query",
@@ -123,7 +129,7 @@ test("a scalar in an n/a style and explode stops generation", async () => {
).rejects.toThrow("which OpenAPI marks n/a and leaves undefined");
});
-const rangeSchema: oas31.SchemaObject = {
+const rangeSchema: oas32.SchemaObject = {
type: "object",
properties: {
gt: { type: "integer" },
@@ -161,7 +167,7 @@ test("style and explode combinations the spec leaves undefined stop generation",
} as const;
await expect(
- generateFor([
+ generateWithQueryParameters([
{
name: "ids",
in: "query",
@@ -173,7 +179,7 @@ test("style and explode combinations the spec leaves undefined stop generation",
).rejects.toThrow("`style: pipeDelimited` with `explode: true`");
await expect(
- generateFor([
+ generateWithQueryParameters([
{
name: "ids",
in: "query",
@@ -188,7 +194,7 @@ test("style and explode combinations the spec leaves undefined stop generation",
// the operation would lose its query in silence
test("an in: querystring parameter stops generation", async () => {
await expect(
- generateFor([
+ generateWithQueryParameters([
{
name: "whole",
in: "querystring",
@@ -200,7 +206,7 @@ test("an in: querystring parameter stops generation", async () => {
test("an in: cookie parameter stops generation", async () => {
await expect(
- generateFor([
+ generateWithQueryParameters([
{ name: "session", in: "cookie", schema: { type: "string" } },
]),
).rejects.toThrow('parameter "session" uses `in: cookie`');
diff --git a/__tests__/query-serializer.test.ts b/__tests__/query-serializer.test.ts
index 5b58f4d..f2ec400 100644
--- a/__tests__/query-serializer.test.ts
+++ b/__tests__/query-serializer.test.ts
@@ -1,17 +1,20 @@
import { RestServiceClient } from "@block65/rest-client";
-import type { oas31 } from "openapi3-ts";
+import type { oas32 } from "openapi3-ts";
import { assert, expect, test, vi } from "vitest";
import { ImageCreateCommand } from "./fixtures/docker/commands.ts";
import { FindPetsCommand } from "./fixtures/petstore/commands.ts";
import { SwaggerPetstoreRestClient } from "./fixtures/petstore/main.ts";
-import { commandsFor, generateFor, type TestParameter } from "./generate.ts";
+import {
+ generateCommandsText,
+ generateWithQueryParameters,
+} from "./helpers.ts";
-const arrayOfStrings: oas31.SchemaObject = {
+const arrayOfStrings: oas32.SchemaObject = {
type: "array",
items: { type: "string" },
};
-const rangeSchema: oas31.SchemaObject = {
+const rangeSchema: oas32.SchemaObject = {
type: "object",
properties: {
gt: { type: "integer" },
@@ -22,7 +25,7 @@ const rangeSchema: oas31.SchemaObject = {
// What a serializer writes is rest-client's to test, from the spec's own
// examples. What is tested here is which one a command names
test("a departure from the default encoding names a serializer", async () => {
- const commands = await commandsFor([
+ const commands = await generateCommandsText([
{
name: "names",
in: "query",
@@ -42,7 +45,7 @@ test("a departure from the default encoding names a serializer", async () => {
// An unnamed serializer is form with explode, so naming it would be noise
test("the default encoding names nothing", async () => {
- const commands = await commandsFor([
+ const commands = await generateCommandsText([
{
name: "tags",
in: "query",
@@ -59,7 +62,7 @@ test("the default encoding names nothing", async () => {
// style, and deepObject writes an array as form with explode, so deepObject
// is the serializer all three parameters share
test("a deepObject parameter sets the serializer for its operation", async () => {
- const commands = await commandsFor([
+ const commands = await generateCommandsText([
{ name: "limit", in: "query", schema: { type: "integer" } },
{ name: "tags", in: "query", schema: arrayOfStrings },
{ name: "filter", in: "query", style: "deepObject", schema: rangeSchema },
@@ -74,7 +77,7 @@ test("a deepObject parameter sets the serializer for its operation", async () =>
// ones stop generation
test("query parameters needing different serializers stop generation", async () => {
await expect(
- generateFor([
+ generateWithQueryParameters([
{
name: "tags",
in: "query",
@@ -88,8 +91,10 @@ test("query parameters needing different serializers stop generation", async ()
});
// A consumer compiles against the import and the property
-async function commandSourceFor(parameters: readonly TestParameter[]) {
- const { commandsFile } = await generateFor(parameters);
+async function extractCommandSource(
+ parameters: readonly oas32.ParameterObject[],
+) {
+ const { commandsFile } = await generateWithQueryParameters(parameters);
const restClientImport = commandsFile.getImportDeclarationOrThrow(
(declaration) =>
@@ -105,7 +110,7 @@ async function commandSourceFor(parameters: readonly TestParameter[]) {
test("the source of a command that names a serializer", async () => {
await expect(
- commandSourceFor([
+ extractCommandSource([
{ name: "limit", in: "query", schema: { type: "integer" } },
{ name: "filter", in: "query", style: "deepObject", schema: rangeSchema },
]),
@@ -114,7 +119,7 @@ test("the source of a command that names a serializer", async () => {
test("the source of a command that names none", async () => {
await expect(
- commandSourceFor([
+ extractCommandSource([
{ name: "limit", in: "query", schema: { type: "integer" } },
{ name: "tags", in: "query", schema: arrayOfStrings },
]),
diff --git a/__tests__/typescript-hint.test.ts b/__tests__/typescript-hint.test.ts
index 986e9f4..d18be04 100644
--- a/__tests__/typescript-hint.test.ts
+++ b/__tests__/typescript-hint.test.ts
@@ -1,4 +1,4 @@
-import type { oas31 } from "openapi3-ts";
+import type { oas32 } from "openapi3-ts";
import { expect, test } from "vitest";
import { processOpenApiDocument } from "../lib/process-document.ts";
@@ -9,7 +9,7 @@ const baseDoc = {
};
test("x-typescript-hint on top-level string schema", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
...baseDoc,
components: {
schemas: {
@@ -30,7 +30,7 @@ test("x-typescript-hint on top-level string schema", async () => {
});
test("x-typescript-hint honored inside oneOf branches", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
...baseDoc,
components: {
schemas: {
@@ -54,7 +54,7 @@ test("x-typescript-hint honored inside oneOf branches", async () => {
});
test("x-typescript-hint honored inside anyOf branches", async () => {
- const schema: oas31.OpenAPIObject = {
+ const schema: oas32.OpenAPIObject = {
...baseDoc,
components: {
schemas: {
diff --git a/lib/build.ts b/lib/build.ts
index a136a2a..9bebea1 100644
--- a/lib/build.ts
+++ b/lib/build.ts
@@ -1,7 +1,7 @@
import { createHash } from "node:crypto";
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
-import type { oas31 } from "openapi3-ts";
+import type { oas32 } from "openapi3-ts";
import { firedExemptions, withDirectives } from "./oxlint.ts";
import {
type CodegenOptions,
@@ -16,7 +16,7 @@ const BANNER = `/**
*/`;
// A JSON import is typed `any`, so the document is checked before it is used
-function isOpenApiDocument(value: unknown): value is oas31.OpenAPIObject {
+function isOpenApiDocument(value: unknown): value is oas32.OpenAPIObject {
return (
typeof value === "object" &&
value !== null &&
diff --git a/lib/process-document.ts b/lib/process-document.ts
index d47d5b6..7a23b94 100644
--- a/lib/process-document.ts
+++ b/lib/process-document.ts
@@ -1,6 +1,6 @@
import nodePath from "node:path";
import { $RefParser, type $Refs } from "@apidevtools/json-schema-ref-parser";
-import type { oas30, oas31 } from "openapi3-ts";
+import type { oas30, oas32 } from "openapi3-ts";
import toposort from "toposort";
import {
type ClassDeclaration,
@@ -29,11 +29,15 @@ import {
} from "./hono.ts";
import { registerTypesFromSchema, schemaToType } from "./process-schema.ts";
import {
+ type ReferenceObject,
+ type SchemaNode,
+ type SchemaObject,
camelCase,
castToValidJsIdentifier,
getDependents,
iife,
isReferenceObject,
+ isSchemaObject,
pascalCase,
typedEntries,
wordWrap,
@@ -265,7 +269,7 @@ type OperationContext = {
headerType: TypeAliasDeclaration | undefined;
pathParameters: oas30.ParameterObject[];
pathType: TypeAliasDeclaration | undefined;
- jsonRequestBodyObject: oas31.MediaTypeObject | undefined;
+ jsonRequestBodyObject: oas32.MediaTypeObject | undefined;
jsonBodyType: NamedDeclaration | undefined;
nonJsonBodyType: TypeAliasDeclaration | undefined;
wrapJsonBody: boolean;
@@ -273,7 +277,7 @@ type OperationContext = {
inputTypeNode: string | WriterFunction;
};
-type OperationWithId = oas31.OperationObject & { operationId: string };
+type OperationWithId = oas32.OperationObject & { operationId: string };
type ValidatedSubclass = {
commandName: string;
@@ -301,28 +305,50 @@ const sequentialMediaTypes: Readonly> = {
},
};
-type SchemaOrRef = oas31.SchemaObject | oas31.ReferenceObject;
+// a $ref resolves to the object it names, which the document must hold
+function resolveObject(
+ refs: $Refs,
+ node: T | ReferenceObject,
+) {
+ if (!isReferenceObject(node)) {
+ return node;
+ }
-// openapi3-ts's oas31 SchemaObject lacks contentSchema
-type ContentSchemaObject = oas31.SchemaObject & { contentSchema?: SchemaOrRef };
+ const target = refs.get(node.$ref);
-// OAS 3.2 added itemSchema, which openapi3-ts's oas31 types predate
-type ItemSchemaMediaTypeObject = oas31.MediaTypeObject & {
- itemSchema?: SchemaOrRef;
-};
+ if (!target || typeof target !== "object" || Array.isArray(target)) {
+ throw new Error(`${node.$ref} does not resolve to an object`);
+ }
+
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the $ref names an object of the kind its position holds
+ return target as T;
+}
+
+// OAS 3.2 lets a media type be a $ref, so a content map resolves before use
+function resolveContent(refs: $Refs, content: oas32.ContentObject | undefined) {
+ return Object.fromEntries(
+ Object.entries(content ?? {}).map(([mediaType, media]) => [
+ mediaType,
+ resolveObject(refs, media),
+ ]),
+ );
+}
type DataDecoding = {
// a rest-client DataTransformer export. Without one, rest-client's
// jsonDataTransformer applies
transformer?: string;
- decodedSchema: (content: ContentSchemaObject) => SchemaOrRef | undefined;
+ decodedSchema: (
+ content: SchemaObject,
+ ) => oas32.SchemaObject | oas32.ReferenceObject | undefined;
};
// keyed by an item content's contentMediaType
const dataDecodings: Readonly> = {
"application/json": {
- decodedSchema: (content) => content.contentSchema,
+ decodedSchema: (content) =>
+ "contentSchema" in content ? content.contentSchema : undefined,
},
"text/plain": {
transformer: "textDataTransformer",
@@ -331,7 +357,7 @@ const dataDecodings: Readonly> = {
};
function hasOperationId(
- operationObject: oas31.OperationObject,
+ operationObject: oas32.OperationObject,
): operationObject is OperationWithId {
return "operationId" in operationObject;
}
@@ -470,7 +496,7 @@ function ensureTypeImport(
}
}
-function sortedComponentSchemas(schema: oas31.OpenAPIObject) {
+function sortedComponentSchemas(schema: oas32.OpenAPIObject) {
const schemas = Object.entries(schema.components?.schemas || {});
const schemaGraph = schemas.flatMap(([schemaName, schemaObject]) => {
@@ -495,10 +521,10 @@ function sortedComponentSchemas(schema: oas31.OpenAPIObject) {
function addEnumValues(
enumsFile: SourceFile,
schemaName: string,
- schemaObject: oas31.SchemaObject | oas31.ReferenceObject,
+ schemaObject: SchemaNode,
) {
if (
- "$ref" in schemaObject ||
+ !isSchemaObject(schemaObject) ||
!("enum" in schemaObject) ||
!Array.isArray(schemaObject.enum)
) {
@@ -545,7 +571,7 @@ function addEnumValues(
function registerComponentSchemas(
documentCtx: DocumentContext,
- schema: oas31.OpenAPIObject,
+ schema: oas32.OpenAPIObject,
) {
for (const [schemaName, schemaObject] of sortedComponentSchemas(schema)) {
registerTypesFromSchema(
@@ -650,7 +676,7 @@ function withResolvedSchema(refs: $Refs, parameter: oas30.ParameterObject) {
function collectParameters(
refs: $Refs,
path: string,
- pathItemObject: oas31.PathItemObject,
+ pathItemObject: oas32.PathItemObject,
operationObject: OperationWithId,
) {
const pathParameters: oas30.ParameterObject[] = [];
@@ -735,7 +761,7 @@ const serializers = {
deepObject: "deepObjectSerializer",
} as const satisfies Record;
-function serializerFor(spec: QueryParamSpec) {
+function pickSerializer(spec: QueryParamSpec) {
return spec.style === "form" && spec.explode
? defaultSerializer
: serializers[spec.style];
@@ -743,7 +769,7 @@ function serializerFor(spec: QueryParamSpec) {
// deepObject brackets an object and writes an array as form with explode
function serializersWriting(spec: QueryParamSpec) {
- const named = serializerFor(spec);
+ const named = pickSerializer(spec);
return named === defaultSerializer && spec.type === "array"
? [defaultSerializer, serializers.deepObject]
@@ -751,7 +777,7 @@ function serializersWriting(spec: QueryParamSpec) {
}
// A command names one serializer for its whole query
-function querySerializerFor(
+function chooseQuerySerializer(
operationId: string,
queryParameters: oas30.ParameterObject[],
) {
@@ -809,7 +835,7 @@ function addQuerySerializer(
operationId: string,
queryParameters: oas30.ParameterObject[],
) {
- const serializer = querySerializerFor(operationId, queryParameters);
+ const serializer = chooseQuerySerializer(operationId, queryParameters);
if (!serializer) {
return;
@@ -935,26 +961,24 @@ function jsonBodyTypeOf(
documentCtx: DocumentContext,
deprecationDocs: DeprecationDocs,
operationId: string,
- jsonRequestBodyObject: oas31.MediaTypeObject | undefined,
+ jsonRequestBodyObject: oas32.MediaTypeObject | undefined,
) {
- if (!jsonRequestBodyObject?.schema) {
+ const schema = jsonRequestBodyObject?.schema;
+
+ if (schema === undefined) {
return;
}
- if ("$ref" in jsonRequestBodyObject.schema) {
- return documentCtx.typesAndInterfaces.get(
- jsonRequestBodyObject.schema.$ref,
- );
+ if (isReferenceObject(schema)) {
+ return documentCtx.typesAndInterfaces.get(schema.$ref);
}
if (
- jsonRequestBodyObject.schema.type === "array" &&
- "items" in jsonRequestBodyObject.schema &&
- "$ref" in jsonRequestBodyObject.schema.items
+ isSchemaObject(schema) &&
+ schema.type === "array" &&
+ isReferenceObject(schema.items)
) {
- return documentCtx.typesAndInterfaces.get(
- jsonRequestBodyObject.schema.items.$ref,
- );
+ return documentCtx.typesAndInterfaces.get(schema.items.$ref);
}
// Named for the media type because only an application/json
@@ -963,13 +987,13 @@ function jsonBodyTypeOf(
const type = schemaToType(
documentCtx.typesAndInterfaces,
- jsonRequestBodyObject.schema.required
+ isSchemaObject(schema) && schema.required
? {
required: [name],
}
: {},
name,
- jsonRequestBodyObject.schema,
+ schema,
);
return documentCtx.typesFile.addTypeAlias({
@@ -989,7 +1013,8 @@ function resolveBodyTypes(
? operationObject.requestBody
: undefined;
- const jsonRequestBodyObject = requestBodyObject?.content["application/json"];
+ const content = resolveContent(documentCtx.refs, requestBodyObject?.content);
+ const jsonRequestBodyObject = content["application/json"];
const jsonBodyType = jsonBodyTypeOf(
documentCtx,
@@ -998,11 +1023,9 @@ function resolveBodyTypes(
jsonRequestBodyObject,
);
- const nonJsonBodyEntries = requestBodyObject?.content
- ? Object.entries(requestBodyObject.content).filter(
- ([, o]) => o !== jsonRequestBodyObject,
- )
- : [];
+ const nonJsonBodyEntries = Object.entries(content).filter(
+ ([, o]) => o !== jsonRequestBodyObject,
+ );
if (jsonBodyType && nonJsonBodyEntries.length > 0) {
console.warn(
@@ -1118,8 +1141,8 @@ function addInputType(
// way a non-JSON body already does
const jsonBodySchema = jsonRequestBodyObject?.schema;
const jsonBodyIsArray =
- !!jsonBodySchema &&
- !("$ref" in jsonBodySchema) &&
+ jsonBodySchema !== undefined &&
+ isSchemaObject(jsonBodySchema) &&
jsonBodySchema.type === "array";
const wrapJsonBody = jsonBodyIsArray && (!!paramsType || !!queryType);
@@ -1170,11 +1193,15 @@ function firstSuccessResponse(operationObject: OperationWithId) {
return response;
}
-function firstJsonResponseSchema(operationObject: OperationWithId) {
+function firstJsonResponseSchema(
+ refs: $Refs,
+ operationObject: OperationWithId,
+) {
// Resolve the first 2xx JSON response schema (inline or $ref) so the
// validator pipeline treats responses the same as request bodies
- return firstSuccessResponse(operationObject)?.content?.["application/json"]
- ?.schema;
+ return resolveContent(refs, firstSuccessResponse(operationObject)?.content)[
+ "application/json"
+ ]?.schema;
}
// Generate the valibot validator for the operation input
@@ -1190,17 +1217,20 @@ function registerOperationValidators(
operationObject: OperationWithId,
sequential: SequentialContent | undefined,
) {
- const responseSchema = firstJsonResponseSchema(operationObject);
+ const responseSchema = firstJsonResponseSchema(
+ documentCtx.refs,
+ operationObject,
+ );
const operationSchemas = createValidatorForOperationInput(
documentCtx.validators,
documentCtx.valibotFile,
commandName,
{
- ...(jsonRequestBodyObject?.schema && {
- body: jsonRequestBodyObject?.schema,
+ ...(jsonRequestBodyObject?.schema !== undefined && {
+ body: jsonRequestBodyObject.schema,
}),
- ...(responseSchema && {
+ ...(responseSchema !== undefined && {
response: responseSchema,
}),
...(sequential && {
@@ -1334,7 +1364,7 @@ function addReferencedOutput(
function addInlineOutput(
documentCtx: DocumentContext,
{ commandClass }: OperationContext,
- schema: oas31.SchemaObject | oas31.ReferenceObject,
+ schema: SchemaNode,
) {
const outputType = schemaToType(
documentCtx.typesAndInterfaces,
@@ -1364,36 +1394,28 @@ function addInlineOutput(
});
}
-function resolveSchema(
- refs: $Refs,
- operationId: string,
- schema: SchemaOrRef,
- // oxlint-disable-next-line block65/no-explicit-return-type -- refs.get gives a JSON Schema draft type, which callers can't index as an OAS schema
-): ContentSchemaObject {
- if (!isReferenceObject(schema)) {
- return schema;
- }
-
- const resolved = refs.get(schema.$ref);
-
- if (!resolved || typeof resolved !== "object" || Array.isArray(resolved)) {
- throw new Error(`${operationId}: ${schema.$ref} does not resolve`);
+function resolveSchema(refs: $Refs, operationId: string, schema: SchemaNode) {
+ if (typeof schema === "boolean") {
+ throw new TypeError(
+ `${operationId}: a boolean item schema has no content to decode`,
+ );
}
- return resolved;
+ return resolveObject(refs, schema);
}
function sequentialContent(refs: $Refs, operationObject: OperationWithId) {
const { operationId } = operationObject;
- const res = firstSuccessResponse(operationObject);
+ const responseContent = resolveContent(
+ refs,
+ firstSuccessResponse(operationObject)?.content,
+ );
- if (!res || res.content?.["application/json"]) {
+ if (responseContent["application/json"]) {
return;
}
- const found = Object.entries(
- res.content ?? {},
- ).find(
+ const found = Object.entries(responseContent).find(
([mediaType, media]) =>
media.itemSchema && mediaType in sequentialMediaTypes,
);
@@ -1432,7 +1454,11 @@ function sequentialContent(refs: $Refs, operationObject: OperationWithId) {
// an item content with no contentMediaType is a string, as SSE data is
const mediaTypes = new Set(
- contents.map((content) => content.contentMediaType ?? "text/plain"),
+ contents.map((content) =>
+ "contentMediaType" in content && content.contentMediaType
+ ? content.contentMediaType
+ : "text/plain",
+ ),
);
// dataTransformer is per command, so every variant shares one decoding
@@ -1512,17 +1538,14 @@ function addSequentialOutput(
}
}
-function jsonOutputRef(jsonResponse: oas31.MediaTypeObject) {
+function jsonOutputRef({ schema }: oas32.MediaTypeObject) {
const arrayRef =
- jsonResponse.schema &&
- "items" in jsonResponse.schema &&
- "$ref" in jsonResponse.schema.items &&
- jsonResponse.schema.items.$ref;
+ schema !== undefined &&
+ isSchemaObject(schema) &&
+ isReferenceObject(schema.items) &&
+ schema.items.$ref;
- const regularRef =
- jsonResponse.schema &&
- "$ref" in jsonResponse.schema &&
- jsonResponse.schema.$ref;
+ const regularRef = isReferenceObject(schema) && schema.$ref;
const outputRef = arrayRef || regularRef;
@@ -1532,7 +1555,7 @@ function jsonOutputRef(jsonResponse: oas31.MediaTypeObject) {
// rest-client resolves a bodiless success, a 204 included, with undefined
function isBodiless(
statusCode: string,
- res: oas31.ResponseObject | oas31.ReferenceObject,
+ res: oas32.ResponseObject | oas32.ReferenceObject,
) {
return (
statusCode === "204" ||
@@ -1582,7 +1605,9 @@ function addOutputTypeArgument(
break;
}
- const jsonResponse = response.content?.["application/json"];
+ const jsonResponse = resolveContent(documentCtx.refs, response.content)[
+ "application/json"
+ ];
if (!jsonResponse) {
if (sequential) {
@@ -1874,7 +1899,7 @@ function addCommandConstructor(operationCtx: OperationContext, path: string) {
function processOperation(
documentCtx: DocumentContext,
path: string,
- pathItemObject: oas31.PathItemObject,
+ pathItemObject: oas32.PathItemObject,
method: string,
operationObject: OperationWithId,
) {
@@ -1937,17 +1962,17 @@ function processOperation(
function emitOperations(
documentCtx: DocumentContext,
- schema: oas31.OpenAPIObject,
+ schema: oas32.OpenAPIObject,
tags: string[] | undefined,
) {
- for (const [path, pathItemObject] of Object.entries(
+ for (const [path, pathItemObject] of Object.entries(
schema.paths || {},
)) {
if (pathItemObject) {
for (const [method, operationObject] of Object.entries(pathItemObject)
// ensure op is an object
.filter(
- (e): e is [string, oas31.OperationObject] => typeof e[1] === "object",
+ (e): e is [string, oas32.OperationObject] => typeof e[1] === "object",
)
// tags
.filter(([, o]) => !tags || o.tags?.some((t) => tags?.includes(t)))) {
@@ -1970,7 +1995,7 @@ function emitOperations(
function addClientConstructor(
clientClassDeclaration: ClassDeclaration,
- schema: oas31.OpenAPIObject,
+ schema: oas32.OpenAPIObject,
configType: string,
) {
const ctor = clientClassDeclaration.addConstructor();
@@ -2004,7 +2029,7 @@ function addClientConstructor(
function emitClientModule(
documentCtx: DocumentContext,
- schema: oas31.OpenAPIObject,
+ schema: oas32.OpenAPIObject,
) {
const {
mainFile,
@@ -2210,7 +2235,7 @@ function trimDefaultOutputArguments(commandsFile: SourceFile) {
export async function processOpenApiDocument(
outputDir: string,
- schema: Simplify,
+ schema: Simplify,
tags?: string[],
options?: CodegenOptions,
) {
diff --git a/lib/process-schema.ts b/lib/process-schema.ts
index 6ec1c09..da73ff6 100644
--- a/lib/process-schema.ts
+++ b/lib/process-schema.ts
@@ -1,4 +1,4 @@
-import type { oas30, oas31 } from "openapi3-ts";
+import type { oas30, oas32 } from "openapi3-ts";
import {
type CodeBlockWriter,
type EnumDeclaration,
@@ -12,9 +12,11 @@ import {
Writers,
} from "ts-morph";
import {
+ type SchemaNode,
+ type SchemaObject,
isNotNullOrUndefined,
- isNotReferenceObject,
isReferenceObject,
+ isSchemaObject,
pascalCase,
wordWrap,
} from "./utils.ts";
@@ -55,11 +57,11 @@ function numericType(isInt64: boolean, stringish: boolean | undefined) {
return stringish ? "`${number}`" : "number";
}
-function isInt64Schema(schema: oas30.SchemaObject | oas31.SchemaObject) {
+function isInt64Schema(schema: SchemaObject) {
return schema.type === "integer" && schema.format === "int64";
}
-function schemaTypeIsNull(schema: oas30.SchemaObject | oas31.SchemaObject) {
+function schemaTypeIsNull(schema: SchemaObject) {
return (
schema.type === "null" ||
("nullable" in schema && schema.nullable) ||
@@ -69,7 +71,7 @@ function schemaTypeIsNull(schema: oas30.SchemaObject | oas31.SchemaObject) {
// valibot wraps every nullable schema in v.nullable, so the type admits null
function withNullable(
- schema: oas30.SchemaObject | oas31.SchemaObject,
+ schema: SchemaObject,
result: T,
) {
return {
@@ -131,16 +133,8 @@ function recordType(value: string | WriterFunction) {
}
// LiteralUnion keeps known values in completion and accepts any other string
-function literalUnionType(
- maybeSchemaObjects: (
- | oas31.SchemaObject
- | oas30.SchemaObject
- | oas31.ReferenceObject
- )[],
-) {
- const schemaObjects = maybeSchemaObjects.filter((obj) =>
- isNotReferenceObject(obj),
- );
+function literalUnionType(maybeSchemaObjects: SchemaNode[]) {
+ const schemaObjects = maybeSchemaObjects.filter((obj) => isSchemaObject(obj));
if (
schemaObjects.length !== maybeSchemaObjects.length ||
@@ -226,7 +220,7 @@ function refPropertyDocs(
function refType(
typesAndInterfaces: TypesAndInterfaces,
- schemaObject: oas31.ReferenceObject,
+ schemaObject: oas32.ReferenceObject,
) {
const existingSchema = typesAndInterfaces.get(schemaObject.$ref);
@@ -248,9 +242,7 @@ function refType(
return property;
}
-function schemaJsDocTags(
- schemaObject: oas31.SchemaObject | oas30.SchemaObject,
-) {
+function schemaJsDocTags(schemaObject: SchemaObject) {
return [
...(schemaObject.default
? [{ tagName: "default", text: String(schemaObject.default) }]
@@ -290,7 +282,7 @@ function schemaJsDocTags(
];
}
-function schemaDocs(schemaObject: oas31.SchemaObject | oas30.SchemaObject) {
+function schemaDocs(schemaObject: SchemaObject) {
const jsdocTags = schemaJsDocTags(schemaObject);
const maybeJsDoc = {
@@ -308,9 +300,9 @@ function schemaDocs(schemaObject: oas31.SchemaObject | oas30.SchemaObject) {
function typeArrayType(
typesAndInterfaces: TypesAndInterfaces,
- schemaObject: oas31.SchemaObject | oas30.SchemaObject,
+ schemaObject: SchemaObject,
propertyName: string,
- types: (oas31.SchemaObjectType | oas30.SchemaObjectType)[],
+ types: (oas32.SchemaObjectType | oas30.SchemaObjectType)[],
options: SchemaToTypeOptions,
) {
if (types.length === 1) {
@@ -350,7 +342,7 @@ function typeArrayType(
function arrayType(
typesAndInterfaces: TypesAndInterfaces,
propertyName: string,
- schemaObject: oas31.SchemaObject | oas30.SchemaObject,
+ schemaObject: SchemaObject,
options: SchemaToTypeOptions,
) {
const type = schemaToType(
@@ -383,9 +375,9 @@ function arrayType(
function combinatorType(
typesAndInterfaces: TypesAndInterfaces,
- parentSchema: oas31.SchemaObject | oas30.SchemaObject,
+ parentSchema: SchemaObject,
propertyName: string,
- schemaObject: oas31.SchemaObject | oas30.SchemaObject,
+ schemaObject: SchemaObject,
options: SchemaToTypeOptions,
) {
const schemaItems =
@@ -403,7 +395,7 @@ function combinatorType(
// member types stay free of it
const nullableMembers = schemaItems.filter(
(schema) =>
- !isReferenceObject(schema) && "nullable" in schema && schema.nullable,
+ isSchemaObject(schema) && "nullable" in schema && schema.nullable,
);
const types = schemaItems
@@ -412,7 +404,7 @@ function combinatorType(
typesAndInterfaces,
parentSchema,
propertyName,
- nullableMembers.includes(schema)
+ isSchemaObject(schema) && nullableMembers.includes(schema)
? { ...schema, nullable: false }
: schema,
options,
@@ -455,7 +447,7 @@ function combinatorType(
}
// Keys unique to objects stand in for the `type` a document often omits
-function isObjectSchema(schemaObject: oas31.SchemaObject | oas30.SchemaObject) {
+function isObjectSchema(schemaObject: SchemaObject) {
return (
schemaObject.type === "object" ||
(schemaObject.type === undefined &&
@@ -467,7 +459,7 @@ function isObjectSchema(schemaObject: oas31.SchemaObject | oas30.SchemaObject) {
function objectType(
typesAndInterfaces: TypesAndInterfaces,
propertyName: string,
- schemaObject: oas31.SchemaObject | oas30.SchemaObject,
+ schemaObject: SchemaObject,
options: SchemaToTypeOptions,
) {
// type=object and enum null is common openapi workaround
@@ -527,7 +519,7 @@ function objectType(
};
}
-function stringType(schemaObject: oas31.SchemaObject | oas30.SchemaObject) {
+function stringType(schemaObject: SchemaObject) {
if ("enum" in schemaObject) {
return maybeUnion(...schemaObject.enum.map((e) => JSON.stringify(e)));
}
@@ -546,9 +538,9 @@ function stringType(schemaObject: oas31.SchemaObject | oas30.SchemaObject) {
function schemaObjectType(
typesAndInterfaces: TypesAndInterfaces,
- parentSchema: oas31.SchemaObject | oas30.SchemaObject,
+ parentSchema: SchemaObject,
propertyName: string,
- schemaObject: oas31.SchemaObject | oas30.SchemaObject,
+ schemaObject: SchemaObject,
options: SchemaToTypeOptions,
) {
if (Array.isArray(schemaObject.type)) {
@@ -657,9 +649,9 @@ export function schemaToType(
string,
InterfaceDeclaration | TypeAliasDeclaration | EnumDeclaration
>,
- parentSchema: oas31.SchemaObject | oas30.SchemaObject,
+ parentSchema: SchemaObject,
propertyName: string,
- schemaObject: oas31.SchemaObject | oas30.SchemaObject | oas31.ReferenceObject,
+ schemaNode: SchemaNode,
options: {
booleanAsStringish?: boolean;
integerAsStringish?: boolean;
@@ -670,6 +662,14 @@ export function schemaToType(
isObjectSchema(parentSchema) &&
!parentSchema.required?.includes(propertyName);
+ // the false schema admits no value
+ if (schemaNode === false) {
+ return { name, hasQuestionToken, type: "never" };
+ }
+
+ // the true schema admits any value, as the empty schema does
+ const schemaObject = schemaNode === true ? {} : schemaNode;
+
if (isReferenceObject(schemaObject)) {
const property: OptionalKind = {
name,
@@ -733,7 +733,7 @@ function registerAlias(
function combinatorAliasType(
typesAndInterfaces: TypesAndInterfaces,
schemaName: string,
- schemaObject: oas30.SchemaObject | oas31.SchemaObject,
+ schemaObject: SchemaObject,
) {
const schemaItems =
schemaObject.allOf || schemaObject.oneOf || schemaObject.anyOf || [];
@@ -745,7 +745,7 @@ function combinatorAliasType(
.map((s) => resolveRef(typesAndInterfaces, s.$ref));
const objectTypesFromNonRefSchemas = schemaItems
- .filter((value) => isNotReferenceObject(value))
+ .filter((value) => isSchemaObject(value))
.filter((schema) => schema.type === "object")
.map((subSchemaObject) =>
Writers.objectType({
@@ -763,7 +763,7 @@ function combinatorAliasType(
.filter((value) => isNotNullOrUndefined(value));
const nonObjectTypesFromNonRefSchemas = schemaItems
- .filter((value) => isNotReferenceObject(value))
+ .filter((value) => isSchemaObject(value))
.filter((schema) => schema.type !== "object")
.map((subSchemaObject) =>
schemaToType(
@@ -794,9 +794,7 @@ function combinatorAliasType(
return intersect ? maybeIntersection(...typeArgs) : maybeUnion(...typeArgs);
}
-function stringAliasType(
- schemaObject: oas30.SchemaObject | oas31.SchemaObject,
-) {
+function stringAliasType(schemaObject: SchemaObject) {
// custom extension
if (
"x-typescript-hint" in schemaObject &&
@@ -824,15 +822,19 @@ export function registerTypesFromSchema(
>,
typesFile: SourceFile,
schemaName: string,
- schemaObject:
- | oas30.SchemaObject
- | oas30.ReferenceObject
- | oas31.SchemaObject
- | oas31.ReferenceObject,
+ schemaNode: SchemaNode,
) {
const register = (type: string | WriterFunction, description?: string) =>
registerAlias(typesAndInterfaces, typesFile, schemaName, type, description);
+ if (typeof schemaNode === "boolean") {
+ register(schemaNode ? "JsonValue" : "never");
+
+ return;
+ }
+
+ const schemaObject = schemaNode;
+
// deal with refs
if ("$ref" in schemaObject) {
register(resolveRef(typesAndInterfaces, schemaObject.$ref).getName());
@@ -923,8 +925,7 @@ export function registerTypesFromSchema(
// deal with arrays of refs
else if (
schemaObject.type === "array" &&
- schemaObject.items &&
- "$ref" in schemaObject.items
+ isReferenceObject(schemaObject.items)
) {
register(
`${resolveRef(typesAndInterfaces, schemaObject.items.$ref).getName()}[]`,
diff --git a/lib/utils.ts b/lib/utils.ts
index 9adc0ed..9a0208c 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1,15 +1,20 @@
import camelcase from "camelcase";
-import type { oas31 } from "openapi3-ts";
+import type { oas30, oas32 } from "openapi3-ts";
import wrap from "word-wrap";
-export function isReferenceObject(obj: unknown): obj is oas31.ReferenceObject {
+export type SchemaObject = oas30.SchemaObject | oas32.SchemaObjectValue;
+
+export type ReferenceObject = oas30.ReferenceObject | oas32.ReferenceObject;
+
+// JSON Schema 2020-12, which OAS 3.1 and 3.2 use, also allows true and false
+export type SchemaNode = SchemaObject | ReferenceObject | boolean;
+
+export function isReferenceObject(obj: unknown): obj is ReferenceObject {
return typeof obj === "object" && obj !== null && "$ref" in obj;
}
-export function isNotReferenceObject(
- obj: T,
-): obj is Exclude {
- return !isReferenceObject(obj);
+export function isSchemaObject(node: SchemaNode): node is SchemaObject {
+ return typeof node === "object" && !isReferenceObject(node);
}
export function isNotNullOrUndefined(obj: T | null | undefined): obj is T {
diff --git a/lib/valibot.ts b/lib/valibot.ts
index 8ed023e..267d5b7 100644
--- a/lib/valibot.ts
+++ b/lib/valibot.ts
@@ -1,6 +1,6 @@
import path from "node:path";
import camelcase from "camelcase";
-import type { oas30, oas31 } from "openapi3-ts";
+import type { oas30, oas32 } from "openapi3-ts";
import {
type CodeBlockWriter,
type Project,
@@ -12,7 +12,13 @@ import {
} from "ts-morph";
import type { Primitive } from "type-fest";
import type * as v from "valibot";
-import { typedEntries, wordWrap } from "./utils.ts";
+import {
+ type SchemaNode,
+ type SchemaObject,
+ isSchemaObject,
+ typedEntries,
+ wordWrap,
+} from "./utils.ts";
// input uses `v.optional` and skips coercion, wire uses `v.exactOptional`
type SchemaMode = "input" | "wire";
@@ -92,7 +98,7 @@ function vcall(
};
}
-function schemaIsNullable(schema: oas30.SchemaObject | oas31.SchemaObject) {
+function schemaIsNullable(schema: SchemaObject) {
return (
schema.type === "null" ||
("nullable" in schema && schema.nullable) ||
@@ -115,7 +121,7 @@ function maybePipe(
return valid.length > 0 ? vcall("pipe", base, ...valid) : base;
}
-function minMaxProperties(schema: oas30.SchemaObject | oas31.SchemaObject) {
+function minMaxProperties(schema: SchemaObject) {
return [
schema.minProperties === undefined
? undefined
@@ -190,7 +196,7 @@ function temporalHintSchema(format: string | undefined) {
return type ? `v.custom<${type}>(() => true)` : undefined;
}
-function shouldCoerceString(schema: oas30.SchemaObject | oas31.SchemaObject) {
+function shouldCoerceString(schema: SchemaObject) {
return (
!schema.enum &&
!schema.pattern &&
@@ -198,9 +204,7 @@ function shouldCoerceString(schema: oas30.SchemaObject | oas31.SchemaObject) {
);
}
-function propertiesNeedCoercion(
- schema: oas30.SchemaObject | oas31.SchemaObject,
-) {
+function propertiesNeedCoercion(schema: SchemaObject) {
const properties = schema.properties ?? {};
const required = new Set(schema.required);
const hasOptional = Object.keys(properties).some((k) => !required.has(k));
@@ -210,10 +214,8 @@ function propertiesNeedCoercion(
);
}
-function shouldCoerceSchema(
- schema: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject,
-): boolean {
- if ("$ref" in schema || "const" in schema) {
+function shouldCoerceSchema(schema: SchemaNode): boolean {
+ if (!isSchemaObject(schema) || "const" in schema) {
return false;
}
@@ -229,7 +231,7 @@ function shouldCoerceSchema(
return propertiesNeedCoercion(schema);
}
- if (schema.items && !("$ref" in schema.items)) {
+ if (schema.items) {
return shouldCoerceSchema(schema.items);
}
@@ -256,21 +258,15 @@ function resolveRef(
return mode === "input" ? entry.input : entry.wire;
}
-type AnySchemaOrRef =
- | oas30.SchemaObject
- | oas30.ReferenceObject
- | oas31.SchemaObject
- | oas31.ReferenceObject;
-
function writeStrictObjectEntries(
writer: CodeBlockWriter,
validators: Map,
- properties: Record,
+ properties: Record,
requiredProps: ReadonlySet,
mode: SchemaMode,
toValidator: (
validators: Map,
- schema: AnySchemaOrRef,
+ schema: SchemaNode,
mode: SchemaMode,
) => WriterFunction | string = schemaToValidator,
) {
@@ -285,7 +281,7 @@ function writeStrictObjectEntries(
? validator
: vcall(optionalWrapper, validator);
- if (!("$ref" in s) && s.description) {
+ if (isSchemaObject(s) && s.description) {
writer.writeLine("/**");
writer.writeLine(
` * ${wordWrap(s.description).split("\n").join("\n * ")}`,
@@ -305,10 +301,8 @@ function writeStrictObjectEntries(
});
}
-type AnySchema = oas30.SchemaObject | oas31.SchemaObject;
-
// Narrows the inferred output to the `x-typescript-hint` type
-function extensionHintSchema(schema: AnySchema) {
+function extensionHintSchema(schema: SchemaObject) {
const typescriptHint =
"x-typescript-hint" in schema &&
typeof schema["x-typescript-hint"] === "string"
@@ -362,8 +356,8 @@ function enumValidator(values: unknown[], isNullable: boolean) {
// Handle type arrays, added in OpenAPI 3.1
function typeArrayValidator(
validators: Map,
- schema: AnySchema,
- types: oas31.SchemaObjectType[],
+ schema: SchemaObject,
+ types: oas32.SchemaObjectType[],
mode: SchemaMode,
isNullable: boolean,
) {
@@ -402,7 +396,7 @@ function typeArrayValidator(
}
function stringValidator(
- schema: AnySchema,
+ schema: SchemaObject,
mode: SchemaMode,
isNullable: boolean,
typescriptHintSchema: string | undefined,
@@ -438,7 +432,7 @@ function stringValidator(
}
function int64Validator(
- schema: AnySchema,
+ schema: SchemaObject,
mode: SchemaMode,
isNullable: boolean,
typescriptHintSchema: string | undefined,
@@ -481,7 +475,7 @@ function int64Validator(
}
function numberValidator(
- schema: AnySchema,
+ schema: SchemaObject,
isNullable: boolean,
typescriptHintSchema: string | undefined,
) {
@@ -521,9 +515,18 @@ function writeSpreadEntries(
function writeAllOfMember(
writer: CodeBlockWriter,
validators: Map,
- member: AnySchemaOrRef,
+ member: SchemaNode,
mode: SchemaMode,
) {
+ // the true schema constrains nothing, so it contributes zero entries
+ if (member === true) {
+ return;
+ }
+
+ if (member === false) {
+ throw new TypeError("an allOf member of false admits no value");
+ }
+
if ("$ref" in member) {
writeSpreadEntries(writer, resolveRef(validators, member.$ref, mode));
@@ -554,8 +557,8 @@ function writeAllOfMember(
function allOfObjectValidator(
validators: Map,
- schema: AnySchema,
- allOfMembers: AnySchemaOrRef[],
+ schema: SchemaObject,
+ allOfMembers: SchemaNode[],
mode: SchemaMode,
isNullable: boolean,
) {
@@ -588,8 +591,8 @@ function allOfObjectValidator(
function combinatorValidator(
validators: Map,
- schema: AnySchema,
- combinator: AnySchemaOrRef[],
+ schema: SchemaObject,
+ combinator: SchemaNode[],
mode: SchemaMode,
isNullable: boolean,
) {
@@ -637,7 +640,7 @@ function combinatorValidator(
function objectValidator(
validators: Map,
- schema: AnySchema,
+ schema: SchemaObject,
mode: SchemaMode,
isNullable: boolean,
) {
@@ -692,13 +695,27 @@ function objectValidator(
function schemaToValidator(
validators: Map,
- schema: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject,
+ schema: SchemaNode,
mode: SchemaMode,
+ // oxlint-disable-next-line block65/no-explicit-return-type -- the validators recurse through here, and inference cannot type a cycle
): WriterFunction | string {
+ // the true schema admits any value, as the empty schema does, and false none
+ if (typeof schema === "boolean") {
+ return schema ? jsonValueSchema : vcall("never");
+ }
+
if ("$ref" in schema) {
return resolveRef(validators, schema.$ref, mode);
}
+ return schemaObjectToValidator(validators, schema, mode);
+}
+
+function schemaObjectToValidator(
+ validators: Map,
+ schema: SchemaObject,
+ mode: SchemaMode,
+) {
const isNullable = schemaIsNullable(schema);
const typescriptHintSchema = extensionHintSchema(schema);
@@ -846,7 +863,7 @@ export function registerValidatorFromSchema(
validators: Map,
valibotFile: SourceFile,
schemaName: string,
- schemaObject: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject,
+ schemaObject: SchemaNode,
inputOnly?: boolean,
) {
const inputName = camelcase(["input", schemaName, "schema"]);
@@ -858,7 +875,7 @@ export function registerValidatorFromSchema(
});
const docs =
- !("$ref" in schemaObject) && schemaObject.description
+ isSchemaObject(schemaObject) && schemaObject.description
? [
{
description: wordWrap(schemaObject.description),
@@ -938,8 +955,13 @@ export function registerValidatorFromSchema(
// Coerces HTTP param strings to native values, leaving other types alone
function asHttpParamValidator(
validatorSchemas: Map,
- schema: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject,
+ schema: SchemaNode,
): WriterFunction | string {
+ // coercion follows a declared type, which a boolean schema lacks
+ if (typeof schema === "boolean") {
+ return schemaToValidator(validatorSchemas, schema, "wire");
+ }
+
if ("$ref" in schema) {
return resolveRef(validatorSchemas, schema.$ref, "wire");
}
@@ -1095,7 +1117,7 @@ function emitNamePair(
function emitSchemaPair(
target: OperationTarget,
segment: "body" | "response" | "data",
- schema: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject,
+ schema: SchemaNode,
) {
return emitNamePair(target, segment, (mode) =>
schemaToValidator(target.validatorSchemas, schema, mode),
@@ -1177,9 +1199,9 @@ export function createValidatorForOperationInput(
valibotFile: SourceFile,
commandName: string,
input: {
- body?: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject;
- response?: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject;
- data?: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject;
+ body?: SchemaNode;
+ response?: SchemaNode;
+ data?: SchemaNode;
params: oas30.ParameterObject[];
query: oas30.ParameterObject[];
header: oas30.ParameterObject[];
From 0599a795a2a7c5f32870901957b1d3b4ad4ab5a2 Mon Sep 17 00:00:00 2001
From: "maxholman[bot]" <321308195+maxholman[bot]@users.noreply.github.com>
Date: Sun, 27 Sep 2026 14:44:32 +0800
Subject: [PATCH 4/6] fix: refuse an itemSchema in a document older than OAS
3.2
itemSchema arrived in OAS 3.2, so a document declaring an earlier version
cannot state one. Codegen used to accept it silently, and now stops and
names the operation.
Co-Authored-By: LLM
---
__tests__/event-stream.test.ts | 13 +++++++++
.../docker/.openapi-codegen-manifest.json | 2 +-
.../openai/.openapi-codegen-manifest.json | 2 +-
.../petstore/.openapi-codegen-manifest.json | 2 +-
.../test1/.openapi-codegen-manifest.json | 2 +-
lib/process-document.ts | 27 +++++++++++++++++--
6 files changed, 42 insertions(+), 6 deletions(-)
diff --git a/__tests__/event-stream.test.ts b/__tests__/event-stream.test.ts
index a533578..aa266b4 100644
--- a/__tests__/event-stream.test.ts
+++ b/__tests__/event-stream.test.ts
@@ -175,3 +175,16 @@ describe("event-stream refusals", () => {
).rejects.toThrow(message);
});
});
+
+test("an itemSchema in a document older than OAS 3.2 is refused", async () => {
+ const document = documentWith({
+ type: "object",
+ properties: { data: { type: "string" } },
+ });
+
+ await expect(
+ processOpenApiDocument("/tmp/whatever", { ...document, openapi: "3.1.0" }),
+ ).rejects.toThrow(
+ "streamEventsCommand: itemSchema is OAS 3.2, and the document declares openapi 3.1.0",
+ );
+});
diff --git a/__tests__/fixtures/docker/.openapi-codegen-manifest.json b/__tests__/fixtures/docker/.openapi-codegen-manifest.json
index 47fd1e9..cd34d73 100644
--- a/__tests__/fixtures/docker/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/docker/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "6ced852ea5759e52e00d4a09f1cc0f47",
+ "#generator": "56fb0358336de212959b78613495c9e1",
"commands.ts": "593afb99bb4daed5e9491b2a55d75d75",
"types.ts": "f1e7d6c61bb9e15c5034a3cc3cc729b5",
"main.ts": "38065305823906f3aa1c7f968e278002",
diff --git a/__tests__/fixtures/openai/.openapi-codegen-manifest.json b/__tests__/fixtures/openai/.openapi-codegen-manifest.json
index 270aa2e..6b0b904 100644
--- a/__tests__/fixtures/openai/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/openai/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "6ced852ea5759e52e00d4a09f1cc0f47",
+ "#generator": "56fb0358336de212959b78613495c9e1",
"commands.ts": "99ac148005b23e09a53adb6b62620dc0",
"types.ts": "bc453b1a7fcd330909a23ae01be97f52",
"main.ts": "8147604a37254400a015b78650466094",
diff --git a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
index af3b74d..38e6b05 100644
--- a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "6ced852ea5759e52e00d4a09f1cc0f47",
+ "#generator": "56fb0358336de212959b78613495c9e1",
"commands.ts": "69c6a9f2924568fc08fd489508c9df34",
"types.ts": "ea65c3e67352d4e22b97af80085727b4",
"main.ts": "64edb526dcbcbd345e631ccff959f11d",
diff --git a/__tests__/fixtures/test1/.openapi-codegen-manifest.json b/__tests__/fixtures/test1/.openapi-codegen-manifest.json
index 879e0d7..d0ff437 100644
--- a/__tests__/fixtures/test1/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/test1/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "6ced852ea5759e52e00d4a09f1cc0f47",
+ "#generator": "56fb0358336de212959b78613495c9e1",
"commands.ts": "eab0f690af8f1e2d973f6504de470ba9",
"types.ts": "7bb3f162d3321e4db51673d3f3ba35ac",
"main.ts": "5c768be2e48d0b06d7ed8da665653404",
diff --git a/lib/process-document.ts b/lib/process-document.ts
index 7a23b94..157dead 100644
--- a/lib/process-document.ts
+++ b/lib/process-document.ts
@@ -390,6 +390,7 @@ type OutputFiles = ReturnType;
type DocumentContext = OutputFiles & {
refs: $Refs;
+ openapiVersion: string;
typesImportDecl: ImportDeclaration;
typesAndInterfaces: Map;
validators: Map;
@@ -1404,7 +1405,18 @@ function resolveSchema(refs: $Refs, operationId: string, schema: SchemaNode) {
return resolveObject(refs, schema);
}
-function sequentialContent(refs: $Refs, operationObject: OperationWithId) {
+// itemSchema arrived in OAS 3.2
+function canStateItemSchema(openapiVersion: string) {
+ const [major = 0, minor = 0] = openapiVersion.split(".").map(Number);
+
+ return major > 3 || (major === 3 && minor >= 2);
+}
+
+function sequentialContent(
+ refs: $Refs,
+ openapiVersion: string,
+ operationObject: OperationWithId,
+) {
const { operationId } = operationObject;
const responseContent = resolveContent(
refs,
@@ -1426,6 +1438,12 @@ function sequentialContent(refs: $Refs, operationObject: OperationWithId) {
return;
}
+ if (!canStateItemSchema(openapiVersion)) {
+ throw new Error(
+ `${operationId}: itemSchema is OAS 3.2, and the document declares openapi ${openapiVersion}`,
+ );
+ }
+
const { contentProperty } = media;
const itemSchema = resolveSchema(refs, operationId, found[1].itemSchema);
const variants = (itemSchema.oneOf ?? itemSchema.anyOf ?? [itemSchema]).map(
@@ -1944,7 +1962,11 @@ function processOperation(
...input,
};
- const sequential = sequentialContent(documentCtx.refs, operationObject);
+ const sequential = sequentialContent(
+ documentCtx.refs,
+ documentCtx.openapiVersion,
+ operationObject,
+ );
const wireSchemas = registerOperationValidators(
documentCtx,
@@ -2247,6 +2269,7 @@ export async function processOpenApiDocument(
const documentCtx: DocumentContext = {
...files,
refs,
+ openapiVersion: schema.openapi,
typesImportDecl,
typesAndInterfaces: new Map(),
validators: new Map(),
From 3c90444848a2a969b431a68c25107526bd9422e2 Mon Sep 17 00:00:00 2001
From: "maxholman[bot]" <321308195+maxholman[bot]@users.noreply.github.com>
Date: Sun, 27 Sep 2026 14:44:32 +0800
Subject: [PATCH 5/6] fix: keep the blank line after the valibot import
The JSON value schema was declared in every valibot module and removed
where unused, and the removal took the blank line after the imports with
it. It is now declared only in a module that uses it.
Co-Authored-By: LLM
---
.../codegen-regressions.test.ts.snap | 11 ++++
.../__snapshots__/event-stream.test.ts.snap | 2 +
.../__snapshots__/nullables.test.ts.snap | 9 ++++
.../docker/.openapi-codegen-manifest.json | 2 +-
.../openai/.openapi-codegen-manifest.json | 2 +-
.../petstore/.openapi-codegen-manifest.json | 4 +-
__tests__/fixtures/petstore/valibot.ts | 1 +
.../test1/.openapi-codegen-manifest.json | 2 +-
lib/process-document.ts | 4 +-
lib/valibot.ts | 54 +++++++++----------
10 files changed, 54 insertions(+), 37 deletions(-)
diff --git a/__tests__/__snapshots__/codegen-regressions.test.ts.snap b/__tests__/__snapshots__/codegen-regressions.test.ts.snap
index 856e8f6..7ed4e94 100644
--- a/__tests__/__snapshots__/codegen-regressions.test.ts.snap
+++ b/__tests__/__snapshots__/codegen-regressions.test.ts.snap
@@ -29,6 +29,7 @@ export type InRecord = Record;
exports[`a $ref nested past the top level registers after its target > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputLeafSchema = v.strictObject(
{
"name": v.optional(v.string())
@@ -165,6 +166,7 @@ export type InputListThingsCommandResponse = UndefinedOnPartialDeep valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputOnlySchema = v.string();
export const onlySchema = v.pipe(v.string(), v.trim());
export const inputGetThingCommandResponseSchema = inputOnlySchema;
@@ -174,6 +176,7 @@ export const getThingCommandResponseSchema = onlySchema;
exports[`a single-member combinator emits the member alone: oneOf > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputOnlySchema = v.string();
export const onlySchema = v.pipe(v.string(), v.trim());
export const inputGetThingCommandResponseSchema = inputOnlySchema;
@@ -183,6 +186,7 @@ export const getThingCommandResponseSchema = onlySchema;
exports[`a single-member combinator emits the member alone: two-member oneOf > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputOnlySchema = v.union([v.string(), v.number()]);
export const onlySchema = v.union([v.pipe(v.string(), v.trim()), v.number()]);
export const inputGetThingCommandResponseSchema = inputOnlySchema;
@@ -221,6 +225,7 @@ export const openSchema = inputOpenSchema;
exports[`additionalProperties alongside properties keeps the extra keys valid > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputConfigSchema = v.objectWithRest(
{
"name": v.optional(v.string())
@@ -238,6 +243,7 @@ export const getThingCommandResponseSchema = configSchema;
exports[`additionalProperties chooses the object schema: empty schema > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputOpenSchema = v.looseObject(
{
"a": v.optional(v.string())
@@ -255,6 +261,7 @@ export const getThingCommandResponseSchema = openSchema;
exports[`additionalProperties chooses the object schema: false > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputOpenSchema = v.strictObject(
{
"a": v.optional(v.string())
@@ -272,6 +279,7 @@ export const getThingCommandResponseSchema = openSchema;
exports[`additionalProperties chooses the object schema: string schema > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputOpenSchema = v.objectWithRest(
{
"a": v.optional(v.string())
@@ -289,6 +297,7 @@ export const getThingCommandResponseSchema = openSchema;
exports[`additionalProperties chooses the object schema: true > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputOpenSchema = v.looseObject(
{
"a": v.optional(v.string())
@@ -315,6 +324,7 @@ export type InputGetThingCommandResponse = UndefinedOnPartialDeep;
exports[`additionalProperties is a string-keyed record of the value type > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputLabelsSchema = v.record(v.string(), v.string());
export const labelsSchema = inputLabelsSchema;
export const inputGetThingCommandResponseSchema = inputLabelsSchema;
@@ -324,6 +334,7 @@ export const getThingCommandResponseSchema = labelsSchema;
exports[`additionalProperties true accepts any key > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputOpenSchema = v.looseObject(
{
"name": v.optional(v.string())
diff --git a/__tests__/__snapshots__/event-stream.test.ts.snap b/__tests__/__snapshots__/event-stream.test.ts.snap
index ef9b9cc..b780076 100644
--- a/__tests__/__snapshots__/event-stream.test.ts.snap
+++ b/__tests__/__snapshots__/event-stream.test.ts.snap
@@ -59,6 +59,7 @@ export type StreamEventsCommandOutput = Thing | {
exports[`JSON data variants make the output a union > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputThingSchema = v.strictObject(
{
"id": v.string()
@@ -137,6 +138,7 @@ export type StreamEventsCommandOutput = string;
exports[`data with no contentMediaType is text > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputStreamEventsCommandDataSchema = v.string();
export const streamEventsCommandDataSchema = v.pipe(v.string(), v.trim());
"
diff --git a/__tests__/__snapshots__/nullables.test.ts.snap b/__tests__/__snapshots__/nullables.test.ts.snap
index 4ff6d6f..36c411b 100644
--- a/__tests__/__snapshots__/nullables.test.ts.snap
+++ b/__tests__/__snapshots__/nullables.test.ts.snap
@@ -21,6 +21,7 @@ export type Nullables = {
exports[`3.0 nullable objects, arrays, strings and combinator members admit null > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputBaseSchema = v.looseObject(
{
"id": v.optional(v.string())
@@ -94,6 +95,7 @@ export type MyDuration = \`P\${string}\`;
exports[`RFC 3339 temporal formats > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputMyDateSchema = v.pipe(v.string(), v.regex(/^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/u, "date"), v.custom<\`\${number}-\${number}-\${number}\`>(() => true));
export const myDateSchema = v.pipe(v.string(), v.trim(), v.regex(/^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$/u, "date"), v.custom<\`\${number}-\${number}-\${number}\`>(() => true));
export const inputMyTimeSchema = v.pipe(v.string(), v.regex(/^([01]\\d|2[0-3]):[0-5]\\d:([0-5]\\d|60)(\\.\\d+)?([Zz]|[+-]([01]\\d|2[0-3]):[0-5]\\d)$/u, "time"), v.custom<\`\${number}:\${number}:\${number}\${string}\`>(() => true));
@@ -115,6 +117,7 @@ export type NullConst = null;
exports[`const values > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputStringConstSchema = v.literal("hello");
export const stringConstSchema = inputStringConstSchema;
export const inputNumberConstSchema = v.literal(42);
@@ -128,6 +131,7 @@ export const nullConstSchema = inputNullConstSchema;
exports[`enums short-circuit type constraints (picklist only) > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputIntegerEnumSchema = v.picklist([0, 1, 2]);
export const integerEnumSchema = inputIntegerEnumSchema;
export const inputStringEnumSchema = v.picklist(["a@example.com", "b@example.com"]);
@@ -223,6 +227,7 @@ export type InputUploadDataCommandResponse = UndefinedOnPartialDeep valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputUploadStatusSchema = v.picklist(["pending", "complete"]);
export const uploadStatusSchema = inputUploadStatusSchema;
export const inputUploadDataCommandResponseSchema = inputUploadStatusSchema;
@@ -248,6 +253,7 @@ export const uploadDataCommandHeaderSchema = v.object({
exports[`input-only mode omits wire schemas > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputNameSchema = v.pipe(v.string(), v.minLength(1));
export const inputAmountSchema = v.pipe(v.bigint(), v.minValue(0n));
"
@@ -260,6 +266,7 @@ exports[`nullables > types.ts 1`] = `
exports[`oneOf with type null generates v.null() > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputNullableImageSchema = v.union([v.string(), v.null()]);
export const nullableImageSchema = v.union([v.pipe(v.string(), v.trim()), v.null()]);
"
@@ -284,6 +291,7 @@ export type InputListFilesCommandResponse = UndefinedOnPartialDeep;
exports[`query and header integer params coerce strings to numbers > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputDummySchema = v.string();
export const dummySchema = v.pipe(v.string(), v.trim());
export const inputExpireTimeSchema = v.pipe(v.bigint(), v.minValue(0n));
@@ -322,6 +330,7 @@ export type MultiType = string | number;
exports[`top-level type array with null > valibot.ts 1`] = `
"import * as v from "valibot";
+
export const inputNullableStringSchema = v.nullable(v.string());
export const nullableStringSchema = inputNullableStringSchema;
export const inputNullableStringEnumSchema = v.nullable(v.picklist(["active", "inactive"]));
diff --git a/__tests__/fixtures/docker/.openapi-codegen-manifest.json b/__tests__/fixtures/docker/.openapi-codegen-manifest.json
index cd34d73..2a95ecc 100644
--- a/__tests__/fixtures/docker/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/docker/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "56fb0358336de212959b78613495c9e1",
+ "#generator": "73e95b200e00f1d54784476e106a00fd",
"commands.ts": "593afb99bb4daed5e9491b2a55d75d75",
"types.ts": "f1e7d6c61bb9e15c5034a3cc3cc729b5",
"main.ts": "38065305823906f3aa1c7f968e278002",
diff --git a/__tests__/fixtures/openai/.openapi-codegen-manifest.json b/__tests__/fixtures/openai/.openapi-codegen-manifest.json
index 6b0b904..039ee59 100644
--- a/__tests__/fixtures/openai/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/openai/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "56fb0358336de212959b78613495c9e1",
+ "#generator": "73e95b200e00f1d54784476e106a00fd",
"commands.ts": "99ac148005b23e09a53adb6b62620dc0",
"types.ts": "bc453b1a7fcd330909a23ae01be97f52",
"main.ts": "8147604a37254400a015b78650466094",
diff --git a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
index 38e6b05..b4f3c70 100644
--- a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json
@@ -1,9 +1,9 @@
{
- "#generator": "56fb0358336de212959b78613495c9e1",
+ "#generator": "73e95b200e00f1d54784476e106a00fd",
"commands.ts": "69c6a9f2924568fc08fd489508c9df34",
"types.ts": "ea65c3e67352d4e22b97af80085727b4",
"main.ts": "64edb526dcbcbd345e631ccff959f11d",
- "valibot.ts": "a754e3506dd239dc9fa4973e39198cbd",
+ "valibot.ts": "2383058357d21e704546455f4e073d67",
"hono.ts": "be7061d9a002b7d6f42bbdfc53727ec7",
"commands-validated.ts": "21f5e9e15233b504d8509d93e00911fd"
}
diff --git a/__tests__/fixtures/petstore/valibot.ts b/__tests__/fixtures/petstore/valibot.ts
index cb5dcce..4971f65 100644
--- a/__tests__/fixtures/petstore/valibot.ts
+++ b/__tests__/fixtures/petstore/valibot.ts
@@ -7,6 +7,7 @@
// oxlint-disable block65/prefer-exact-optional
import * as v from "valibot";
+
export const inputErrorSchema = v.looseObject({
code: v.pipe(v.number(), v.integer()),
message: v.string(),
diff --git a/__tests__/fixtures/test1/.openapi-codegen-manifest.json b/__tests__/fixtures/test1/.openapi-codegen-manifest.json
index d0ff437..f397e5b 100644
--- a/__tests__/fixtures/test1/.openapi-codegen-manifest.json
+++ b/__tests__/fixtures/test1/.openapi-codegen-manifest.json
@@ -1,5 +1,5 @@
{
- "#generator": "56fb0358336de212959b78613495c9e1",
+ "#generator": "73e95b200e00f1d54784476e106a00fd",
"commands.ts": "eab0f690af8f1e2d973f6504de470ba9",
"types.ts": "7bb3f162d3321e4db51673d3f3ba35ac",
"main.ts": "5c768be2e48d0b06d7ed8da665653404",
diff --git a/lib/process-document.ts b/lib/process-document.ts
index 157dead..109f6ac 100644
--- a/lib/process-document.ts
+++ b/lib/process-document.ts
@@ -46,7 +46,7 @@ import {
createValibotFile,
createValidatorForOperationInput,
registerValidatorFromSchema,
- removeUnusedJsonValueSchema,
+ addJsonValueSchemaWhenUsed,
} from "./valibot.ts";
export type CodegenOptions = {
@@ -2294,7 +2294,7 @@ export async function processOpenApiDocument(
files.typesFile.fixUnusedIdentifiers();
files.commandsFile.fixUnusedIdentifiers();
files.commandsValidatedFile.fixUnusedIdentifiers();
- removeUnusedJsonValueSchema(files.valibotFile);
+ addJsonValueSchemaWhenUsed(files.valibotFile);
files.valibotFile.fixUnusedIdentifiers();
const honoFile = emitHonoModule(
diff --git a/lib/valibot.ts b/lib/valibot.ts
index 267d5b7..07d7413 100644
--- a/lib/valibot.ts
+++ b/lib/valibot.ts
@@ -803,39 +803,17 @@ function schemaObjectToValidator(
}
/**
- * Removes the JSON value schema when no validator uses it. The recursion
- * inside it refers to it, so fixUnusedIdentifiers keeps it otherwise
+ * Declares the JSON value schema once a validator refers to it, after the
+ * imports, and leaves a module that needs none without it
*/
-export function removeUnusedJsonValueSchema(file: SourceFile) {
- const declaration = file.getVariableDeclaration(jsonValueSchema);
-
- const usedOutside = file
+export function addJsonValueSchemaWhenUsed(file: SourceFile) {
+ const used = file
.getDescendantsOfKind(SyntaxKind.Identifier)
- .some(
- (identifier) =>
- identifier.getText() === jsonValueSchema &&
- !identifier.getAncestors().includes(declaration ?? identifier),
- );
+ .some((identifier) => identifier.getText() === jsonValueSchema);
- if (declaration && !usedOutside) {
- declaration.getVariableStatementOrThrow().remove();
+ if (!used) {
+ return;
}
-}
-
-export function createValibotFile(project: Project, outputDir: string) {
- const file = project.createSourceFile(
- path.join(outputDir, "valibot.ts"),
- "",
- {
- overwrite: true,
- },
- );
-
- // Valibot import
- file.addImportDeclaration({
- moduleSpecifier: "valibot",
- namespaceImport: "v",
- });
file.addImportDeclaration({
moduleSpecifier: "type-fest",
@@ -845,7 +823,7 @@ export function createValibotFile(project: Project, outputDir: string) {
// A value a schema leaves open is checked as JSON, recursively, so both
// sides type it as JsonValue
- file.addVariableStatement({
+ file.insertVariableStatement(file.getImportDeclarations().length, {
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
@@ -855,6 +833,22 @@ export function createValibotFile(project: Project, outputDir: string) {
},
],
});
+}
+
+export function createValibotFile(project: Project, outputDir: string) {
+ const file = project.createSourceFile(
+ path.join(outputDir, "valibot.ts"),
+ "",
+ {
+ overwrite: true,
+ },
+ );
+
+ // Valibot import
+ file.addImportDeclaration({
+ moduleSpecifier: "valibot",
+ namespaceImport: "v",
+ });
return file;
}
From 699a6878e135dbab4d3ff75c3f0e2429da3372b4 Mon Sep 17 00:00:00 2001
From: "maxholman[bot]" <321308195+maxholman[bot]@users.noreply.github.com>
Date: Sun, 27 Sep 2026 19:44:01 +0800
Subject: [PATCH 6/6] chore(deps)!: require @block65/rest-client 16
Co-Authored-By: LLM
---
package.json | 4 ++--
pnpm-lock.yaml | 17 ++++++++++++-----
pnpm-workspace.yaml | 1 +
3 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/package.json b/package.json
index 5c3069a..25aeac9 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"yargs": "^18.2.0"
},
"devDependencies": {
- "@block65/rest-client": "^15.0.1",
+ "@block65/rest-client": "^16.0.0",
"@block65/shared-config": "^0.6.0",
"@block65/tsconfig": "^0.3.1",
"@hono/standard-validator": "^0.4.0",
@@ -50,7 +50,7 @@
"vitest": "^5.0.2"
},
"peerDependencies": {
- "@block65/rest-client": "^15.0.0",
+ "@block65/rest-client": "^16.0.0",
"@hono/standard-validator": "^0.4.0",
"hono": "^4.13.8",
"type-fest": "^5.10.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c32c3a1..a07ff88 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -190,8 +190,8 @@ importers:
version: 18.2.0
devDependencies:
'@block65/rest-client':
- specifier: ^15.0.1
- version: 15.0.1(valibot@1.5.0(typescript@7.0.2))
+ specifier: ^16.0.0
+ version: 16.0.0(valibot@1.5.0(typescript@7.0.2))
'@block65/shared-config':
specifier: ^0.6.0
version: 0.6.0(@block65/oxlint-tsgolint@7.0.200300)(@block65/oxlint@1.83.0(@block65/oxlint-tsgolint@7.0.200300))(eslint@10.11.0)(fallow@3.29.0)(oxfmt@0.70.0)
@@ -308,8 +308,8 @@ packages:
vite-plus:
optional: true
- '@block65/rest-client@15.0.1':
- resolution: {integrity: sha512-XL2ke0VAvmDv2Y23xNegWU2h9BRCKzxKGnCure4ae/w6MQV8TTqmyDfcXIgSUiPSb9j1FJUYX8uqHeZWM6YpeA==}
+ '@block65/rest-client@16.0.0':
+ resolution: {integrity: sha512-pQ6Tr1MGIwH4AsF8Y9zc/ITqILHZdEnnFy4yK592Q4fQZpJNAVCJcWyRfh1ya1Z3cerii+XNVxY+cmOh0pRYRw==}
peerDependencies:
valibot: ^1.5.0
peerDependenciesMeta:
@@ -1354,6 +1354,10 @@ packages:
resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==}
engines: {node: '>=14.16'}
+ parse-sse@0.1.0:
+ resolution: {integrity: sha512-8bObUwtEuLp2Z1gP6iRVMBzmEqaU5ohmofa3WmZVFfFZFKRP/+c03y8NVYzzXmQMotQ6mDS5Mnyk6tT1gKOTcQ==}
+ engines: {node: '>=20'}
+
path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
@@ -1701,11 +1705,12 @@ snapshots:
'@block65/oxlint-binding-linux-x64-gnu': 1.83.0
oxlint-tsgolint: '@block65/oxlint-tsgolint@7.0.200300'
- '@block65/rest-client@15.0.1(valibot@1.5.0(typescript@7.0.2))':
+ '@block65/rest-client@16.0.0(valibot@1.5.0(typescript@7.0.2))':
dependencies:
'@block65/custom-error': 14.1.0
'@standard-schema/spec': 1.1.0
p-retry: 8.0.1
+ parse-sse: 0.1.0
query-string: 9.5.1
optionalDependencies:
valibot: 1.5.0(typescript@7.0.2)
@@ -2503,6 +2508,8 @@ snapshots:
p-timeout@6.1.4: {}
+ parse-sse@0.1.0: {}
+
path-browserify@1.0.1: {}
path-exists@4.0.0: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 187875a..85226a0 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -8,6 +8,7 @@ minimumReleaseAgeExclude:
- "@block65/oxlint@1.83.0"
- "@block65/oxlint-plugin@0.12.1"
- "@block65/shared-config@0.6.0"
+ - "@block65/rest-client@16.0.0"
overrides:
oxlint: npm:@block65/oxlint@>=1.83.0