diff --git a/__tests__/__snapshots__/codegen-regressions.test.ts.snap b/__tests__/__snapshots__/codegen-regressions.test.ts.snap index 8526dc0..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()) @@ -64,6 +65,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"; @@ -82,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; @@ -91,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; @@ -100,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; @@ -138,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()) @@ -155,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()) @@ -172,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()) @@ -189,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()) @@ -206,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()) @@ -232,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; @@ -241,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 new file mode 100644 index 0000000..b780076 --- /dev/null +++ b/__tests__/__snapshots__/event-stream.test.ts.snap @@ -0,0 +1,190 @@ +// 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..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"]); @@ -169,7 +173,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; } " `; @@ -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__/codegen-regressions.test.ts b/__tests__/codegen-regressions.test.ts index d659e92..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: { @@ -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__/event-stream.test.ts b/__tests__/event-stream.test.ts new file mode 100644 index 0000000..aa266b4 --- /dev/null +++ b/__tests__/event-stream.test.ts @@ -0,0 +1,190 @@ +import type { oas32 } from "openapi3-ts"; +import { describe, expect, test } from "vitest"; +import { processOpenApiDocument } from "../lib/process-document.ts"; + +function documentWith( + itemSchema: oas32.SchemaObject | oas32.ReferenceObject | undefined, + schemas: Record = {}, +) { + const document: oas32.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 } + : { schema: { type: "string", format: "binary" } }, + }, + }, + }, + }, + }, + }, + }; + + return document; +} + +function jsonData(contentSchema: oas32.SchemaObject | oas32.ReferenceObject) { + 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, oas32.SchemaObject, 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); + }); +}); + +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 24a8e04..2a95ecc 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": "73e95b200e00f1d54784476e106a00fd", + "commands.ts": "593afb99bb4daed5e9491b2a55d75d75", "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..30ae456 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, @@ -417,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; @@ -484,7 +486,7 @@ export class ContainerStatsCommand extends Command< */ export class ContainerResizeCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, ContainerResizeCommandQuery > { public override method = "post" as const; @@ -771,7 +773,7 @@ export class ContainerUnpauseCommand extends Command< */ export class ContainerAttachCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, ContainerAttachCommandQuery > { public override method = "post" as const; @@ -793,7 +795,7 @@ export class ContainerAttachCommand extends Command< */ export class ContainerAttachWebsocketCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, ContainerAttachWebsocketCommandQuery > { public override method = "get" as const; @@ -861,7 +863,7 @@ export class ContainerDeleteCommand extends Command< */ export class ContainerArchiveCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, ContainerArchiveCommandQuery > { public override method = "get" as const; @@ -886,18 +888,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, - unknown, + PutContainerArchiveCommandBodyNonJson & + UndefinedOnPartialDeep< + PutContainerArchiveCommandParams & PutContainerArchiveCommandQuery + >, + undefined, 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( @@ -917,7 +921,7 @@ export class PutContainerArchiveCommand extends Command< */ export class ContainerArchiveInfoCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, ContainerArchiveInfoCommandQuery > { public override method = "head" as const; @@ -993,17 +997,16 @@ export class ImageListCommand extends Command< * @summary Build an image */ export class ImageBuildCommand extends Command< - UndefinedOnPartialDeep> & - Pick, - unknown, + ImageBuildCommandBodyNonJson & UndefinedOnPartialDeep, + undefined, ImageBuildCommandQuery, ImageBuildCommandHeader > { public override method = "post" as const; constructor( - input: UndefinedOnPartialDeep> & - Pick, + input: ImageBuildCommandBodyNonJson & + UndefinedOnPartialDeep, headers?: ImageBuildCommandHeader, ) { const { @@ -1097,9 +1100,9 @@ export class BuildPruneCommand extends Command< * @summary Create an image */ export class ImageCreateCommand extends Command< - UndefinedOnPartialDeep> & - Pick, - unknown, + ImageCreateCommandBodyNonJson & + UndefinedOnPartialDeep, + undefined, ImageCreateCommandQuery, ImageCreateCommandHeader > { @@ -1107,8 +1110,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 } = @@ -1177,7 +1180,7 @@ export class ImageHistoryCommand extends Command< */ export class ImagePushCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, ImagePushCommandQuery, ImagePushCommandHeader > { @@ -1204,7 +1207,7 @@ export class ImagePushCommand extends Command< */ export class ImageTagCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, ImageTagCommandQuery > { public override method = "post" as const; @@ -1542,16 +1545,15 @@ export class ImageGetAllCommand extends Command< * @summary Import images */ export class ImageLoadCommand extends Command< - UndefinedOnPartialDeep> & - Pick, - unknown, + ImageLoadCommandBodyNonJson & UndefinedOnPartialDeep, + undefined, ImageLoadCommandQuery > { public override method = "post" as const; constructor( - input: UndefinedOnPartialDeep> & - Pick, + input: ImageLoadCommandBodyNonJson & + UndefinedOnPartialDeep, ) { const { quiet, body } = input; super("/images/load", body, stripUndefined({ quiet })); @@ -1583,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; @@ -1601,7 +1604,7 @@ export class ExecStartCommand extends Command< */ export class ExecResizeCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, ExecResizeCommandQuery > { public override method = "post" as const; @@ -1688,7 +1691,7 @@ export class VolumeInspectCommand extends Command< */ export class VolumeUpdateCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, VolumeUpdateCommandQuery > { public override method = "put" as const; @@ -1824,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; @@ -1840,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; @@ -1975,7 +1980,7 @@ export class PluginDeleteCommand extends Command< */ export class PluginEnableCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, PluginEnableCommandQuery > { public override method = "post" as const; @@ -1997,7 +2002,7 @@ export class PluginEnableCommand extends Command< */ export class PluginDisableCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, PluginDisableCommandQuery > { public override method = "post" as const; @@ -2045,16 +2050,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 })); @@ -2067,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; @@ -2136,7 +2142,7 @@ export class NodeInspectCommand extends Command< */ export class NodeDeleteCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, NodeDeleteCommandQuery > { public override method = "delete" as const; @@ -2154,7 +2160,7 @@ export class NodeDeleteCommand extends Command< */ export class NodeUpdateCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, NodeUpdateCommandQuery > { public override method = "post" as const; @@ -2208,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; @@ -2225,7 +2232,7 @@ export class SwarmJoinCommand extends Command< */ export class SwarmLeaveCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, SwarmLeaveCommandQuery > { public override method = "post" as const; @@ -2243,7 +2250,7 @@ export class SwarmLeaveCommand extends Command< */ export class SwarmUpdateCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, SwarmUpdateCommandQuery > { public override method = "post" as const; @@ -2291,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; @@ -2369,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; @@ -2587,7 +2596,7 @@ export class SecretDeleteCommand extends Command< */ export class SecretUpdateCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, SecretUpdateCommandQuery > { public override method = "post" as const; @@ -2678,7 +2687,7 @@ export class ConfigDeleteCommand extends Command< */ export class ConfigUpdateCommand extends Command< UndefinedOnPartialDeep, - unknown, + undefined, ConfigUpdateCommandQuery > { public override method = "post" as const; @@ -2739,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/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..039ee59 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": "73e95b200e00f1d54784476e106a00fd", + "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..b4f3c70 100644 --- a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json +++ b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json @@ -1,9 +1,9 @@ { - "#generator": "cf9ab7bd8a80e3339d9237d5dee0deb3", + "#generator": "73e95b200e00f1d54784476e106a00fd", "commands.ts": "69c6a9f2924568fc08fd489508c9df34", "types.ts": "ea65c3e67352d4e22b97af80085727b4", "main.ts": "64edb526dcbcbd345e631ccff959f11d", - "valibot.ts": "a754e3506dd239dc9fa4973e39198cbd", + "valibot.ts": "2383058357d21e704546455f4e073d67", "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/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.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..f397e5b 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": "73e95b200e00f1d54784476e106a00fd", + "commands.ts": "eab0f690af8f1e2d973f6504de470ba9", + "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..ca3d661 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 * @@ -189,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/__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__/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__/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/__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/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..109f6ac 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,10 +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, @@ -41,7 +46,7 @@ import { createValibotFile, createValidatorForOperationInput, registerValidatorFromSchema, - removeUnusedJsonValueSchema, + addJsonValueSchemaWhenUsed, } from "./valibot.ts"; export type CodegenOptions = { @@ -264,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; @@ -272,10 +277,87 @@ type OperationContext = { inputTypeNode: string | WriterFunction; }; -type OperationWithId = oas31.OperationObject & { operationId: string }; +type OperationWithId = oas32.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", + }, +}; + +// 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; + } + + const target = refs.get(node.$ref); + + 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: SchemaObject, + ) => oas32.SchemaObject | oas32.ReferenceObject | undefined; +}; + +// keyed by an item content's contentMediaType +const dataDecodings: Readonly> = { + "application/json": { + decodedSchema: (content) => + "contentSchema" in content ? content.contentSchema : undefined, + }, + "text/plain": { + transformer: "textDataTransformer", + decodedSchema: () => ({ type: "string" }), + }, +}; function hasOperationId( - operationObject: oas31.OperationObject, + operationObject: oas32.OperationObject, ): operationObject is OperationWithId { return "operationId" in operationObject; } @@ -308,6 +390,7 @@ type OutputFiles = ReturnType; type DocumentContext = OutputFiles & { refs: $Refs; + openapiVersion: string; typesImportDecl: ImportDeclaration; typesAndInterfaces: Map; validators: Map; @@ -315,7 +398,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 +415,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", ], @@ -407,7 +497,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]) => { @@ -432,10 +522,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) ) { @@ -482,7 +572,7 @@ function addEnumValues( function registerComponentSchemas( documentCtx: DocumentContext, - schema: oas31.OpenAPIObject, + schema: oas32.OpenAPIObject, ) { for (const [schemaName, schemaObject] of sortedComponentSchemas(schema)) { registerTypesFromSchema( @@ -587,7 +677,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[] = []; @@ -672,7 +762,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]; @@ -680,7 +770,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] @@ -688,7 +778,7 @@ function serializersWriting(spec: QueryParamSpec) { } // A command names one serializer for its whole query -function querySerializerFor( +function chooseQuerySerializer( operationId: string, queryParameters: oas30.ParameterObject[], ) { @@ -746,7 +836,7 @@ function addQuerySerializer( operationId: string, queryParameters: oas30.ParameterObject[], ) { - const serializer = querySerializerFor(operationId, queryParameters); + const serializer = chooseQuerySerializer(operationId, queryParameters); if (!serializer) { return; @@ -872,26 +962,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 @@ -900,13 +988,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({ @@ -926,7 +1014,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, @@ -935,11 +1024,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( @@ -1055,8 +1142,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); @@ -1088,9 +1175,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 +1191,18 @@ function firstJsonResponseSchema(operationObject: OperationWithId) { return; } - return response.content?.["application/json"]?.schema; + return response; +} + +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 resolveContent(refs, firstSuccessResponse(operationObject)?.content)[ + "application/json" + ]?.schema; } // Generate the valibot validator for the operation input @@ -1119,20 +1216,27 @@ function registerOperationValidators( jsonRequestBodyObject, }: OperationContext, 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 && { + data: sequential.schema, + }), params: pathParameters, query: queryParameters, header: headerParameters, @@ -1162,28 +1266,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); @@ -1224,7 +1365,7 @@ function addReferencedOutput( function addInlineOutput( documentCtx: DocumentContext, { commandClass }: OperationContext, - schema: oas31.SchemaObject | oas31.ReferenceObject, + schema: SchemaNode, ) { const outputType = schemaToType( documentCtx.typesAndInterfaces, @@ -1254,44 +1395,213 @@ function addInlineOutput( }); } -function jsonOutputRef(jsonResponse: oas31.MediaTypeObject) { +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 resolveObject(refs, schema); +} + +// 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, + firstSuccessResponse(operationObject)?.content, + ); + + if (responseContent["application/json"]) { + return; + } + + const found = Object.entries(responseContent).find( + ([mediaType, media]) => + media.itemSchema && mediaType in sequentialMediaTypes, + ); + + const media = found && sequentialMediaTypes[found[0]]; + + if (!found?.[1].itemSchema || !media) { + 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( + (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) => + "contentMediaType" in content && content.contentMediaType + ? 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({ 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; return outputRef ? { ref: outputRef, isArray: !!arrayRef } : undefined; } +// rest-client resolves a bodiless success, a 204 included, with undefined +function isBodiless( + statusCode: string, + res: oas32.ResponseObject | oas32.ReferenceObject, +) { + return ( + statusCode === "204" || + (!("$ref" in res) && Object.keys(res.content ?? {}).length === 0) + ); +} + function addOutputTypeArgument( documentCtx: DocumentContext, operationCtx: OperationContext, operationObject: OperationWithId, + sequential: SequentialContent | undefined, ) { 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 @@ -1300,8 +1610,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); @@ -1314,9 +1623,16 @@ function addOutputTypeArgument( break; } - const jsonResponse = response.content?.["application/json"]; + const jsonResponse = resolveContent(documentCtx.refs, response.content)[ + "application/json" + ]; if (!jsonResponse) { + if (sequential) { + addSequentialOutput(documentCtx, operationCtx, sequential); + hasOutputType = true; + } + break; } @@ -1344,17 +1660,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 +1848,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 }), }); @@ -1599,7 +1917,7 @@ function addCommandConstructor(operationCtx: OperationContext, path: string) { function processOperation( documentCtx: DocumentContext, path: string, - pathItemObject: oas31.PathItemObject, + pathItemObject: oas32.PathItemObject, method: string, operationObject: OperationWithId, ) { @@ -1644,14 +1962,21 @@ function processOperation( ...input, }; + const sequential = sequentialContent( + documentCtx.refs, + documentCtx.openapiVersion, + 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); @@ -1659,17 +1984,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)))) { @@ -1692,7 +2017,7 @@ function emitOperations( function addClientConstructor( clientClassDeclaration: ClassDeclaration, - schema: oas31.OpenAPIObject, + schema: oas32.OpenAPIObject, configType: string, ) { const ctor = clientClassDeclaration.addConstructor(); @@ -1726,10 +2051,16 @@ function addClientConstructor( function emitClientModule( documentCtx: DocumentContext, - schema: oas31.OpenAPIObject, + schema: oas32.OpenAPIObject, ) { - const { mainFile, typesFile, outputTypes, inputTypeArgs, inputTypeNames } = - documentCtx; + const { + mainFile, + typesFile, + outputTypes, + sequentialOutputs, + inputTypeArgs, + inputTypeNames, + } = documentCtx; const serviceClientClassName = "RestServiceClient"; const fetcherName = "createIsomorphicNativeFetcher"; @@ -1744,6 +2075,9 @@ function emitClientModule( name: configType, isTypeOnly: true, }, + ...(sequentialOutputs.length > 0 + ? [{ name: "ParsedStreamEvent", isTypeOnly: true }] + : []), ], }); @@ -1755,10 +2089,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 +2121,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 +2159,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 +2182,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}`, }, ], }); @@ -1913,7 +2257,7 @@ function trimDefaultOutputArguments(commandsFile: SourceFile) { export async function processOpenApiDocument( outputDir: string, - schema: Simplify, + schema: Simplify, tags?: string[], options?: CodegenOptions, ) { @@ -1925,6 +2269,7 @@ export async function processOpenApiDocument( const documentCtx: DocumentContext = { ...files, refs, + openapiVersion: schema.openapi, typesImportDecl, typesAndInterfaces: new Map(), validators: new Map(), @@ -1933,6 +2278,7 @@ export async function processOpenApiDocument( inputTypeArgs: new Set(), inputTypeNames: new Set(), validatedSubclasses: [], + sequentialOutputs: [], validatedReExports: [], inputOnly: options?.inputOnly, }; @@ -1948,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/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 489330f..07d7413 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); @@ -786,39 +803,17 @@ function schemaToValidator( } /** - * 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", @@ -828,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: [ { @@ -838,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; } @@ -846,7 +857,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 +869,7 @@ export function registerValidatorFromSchema( }); const docs = - !("$ref" in schemaObject) && schemaObject.description + isSchemaObject(schemaObject) && schemaObject.description ? [ { description: wordWrap(schemaObject.description), @@ -938,8 +949,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"); } @@ -1042,6 +1058,7 @@ type SchemaNamePair = { inputName: string; wireName: string }; type OperationSchemaNames = { json?: string; response?: string; + data?: string; param?: string; query?: string; header?: string; @@ -1093,8 +1110,8 @@ function emitNamePair( function emitSchemaPair( target: OperationTarget, - segment: "body" | "response", - schema: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject, + segment: "body" | "response" | "data", + schema: SchemaNode, ) { return emitNamePair(target, segment, (mode) => schemaToValidator(target.validatorSchemas, schema, mode), @@ -1176,8 +1193,9 @@ export function createValidatorForOperationInput( valibotFile: SourceFile, commandName: string, input: { - body?: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject; - response?: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject; + body?: SchemaNode; + response?: SchemaNode; + data?: SchemaNode; params: oas30.ParameterObject[]; query: oas30.ParameterObject[]; header: oas30.ParameterObject[]; @@ -1191,6 +1209,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) 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