From 326a72b5a089504ae40e169ee9963facf96b8a9a Mon Sep 17 00:00:00 2001 From: "maxholman[bot]" <321308195+maxholman[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:09:56 +0800 Subject: [PATCH 1/2] test: snapshot generated files with toMatchSnapshot expectGenerated wrote a folder per suite and test, with a file per generated module. It now records each file whole in the suite's standard .snap, keyed by test name and file name. All 50 snapshots are byte-identical to the files they replace. With no generated .ts left under __snapshots__, oxfmt, oxlint, fallow and tsconfig no longer need to ignore it. Co-Authored-By: LLM --- .fallowrc.jsonc | 5 +- .../codegen-regressions.test.ts.snap | 414 ++++++++++++++++++ .../types.ts | 12 - .../types.ts | 5 - .../valibot.ts | 33 -- .../valibot.ts | 5 - .../valibot.ts | 5 - .../valibot.ts | 5 - .../types.ts | 7 - .../valibot.ts | 14 - .../valibot.ts | 13 - .../valibot.ts | 13 - .../valibot.ts | 13 - .../valibot.ts | 13 - .../valibot.ts | 13 - .../types.ts | 5 - .../valibot.ts | 5 - .../valibot.ts | 13 - .../main.ts | 14 - .../commands.ts | 17 - .../types.ts | 13 - .../types.ts | 5 - .../types.ts | 4 - .../commands.ts | 16 - .../types.ts | 15 - .../main.ts | 14 - .../types.ts | 15 - .../types.ts | 9 - .../types.ts | 9 - .../__snapshots__/nullables.test.ts.snap | 334 ++++++++++++++ .../types.ts | 15 - .../valibot.ts | 61 --- .../nullables/const-values/types.ts | 4 - .../nullables/const-values/valibot.ts | 9 - .../valibot.ts | 5 - .../header-parameters/commands-validated.ts | 6 - .../nullables/header-parameters/commands.ts | 25 -- .../nullables/header-parameters/hono.ts | 26 -- .../nullables/header-parameters/types.ts | 13 - .../nullables/header-parameters/valibot.ts | 21 - .../valibot.ts | 3 - .../nullables/nullables/types.ts | 1 - .../valibot.ts | 3 - .../types.ts | 13 - .../valibot.ts | 21 - .../rfc-3339-temporal-formats/types.ts | 4 - .../rfc-3339-temporal-formats/valibot.ts | 9 - .../top-level-type-array-with-null/enums.ts | 1 - .../top-level-type-array-with-null/types.ts | 4 - .../top-level-type-array-with-null/valibot.ts | 9 - .../typescript-hint.test.ts.snap | 16 + .../types.ts | 1 - .../types.ts | 1 - .../types.ts | 1 - __tests__/codegen-regressions.test.ts | 36 +- __tests__/generated-snapshot.ts | 27 +- __tests__/nullables.test.ts | 24 +- __tests__/typescript-hint.test.ts | 6 +- oxfmt.config.ts | 5 +- oxlint.config.ts | 3 - tsconfig.json | 5 +- 61 files changed, 804 insertions(+), 632 deletions(-) create mode 100644 __tests__/__snapshots__/codegen-regressions.test.ts.snap delete mode 100644 __tests__/__snapshots__/codegen-regressions/a-oneof-query-param-keeps-the-stringish-wire-types-in-every-branch/types.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/a-ref-nested-past-the-top-level-registers-after-its-target/types.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/a-ref-nested-past-the-top-level-registers-after-its-target/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-anyof/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-oneof/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-two-member-oneof/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/a-value-the-schema-leaves-open-is-a-jsonvalue-on-both-sides/types.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/a-value-the-schema-leaves-open-is-a-jsonvalue-on-both-sides/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/additionalproperties-alongside-properties-keeps-the-extra-keys-valid/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-empty-schema/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-false/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-string-schema/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-true/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/additionalproperties-is-a-string-keyed-record-of-the-value-type/types.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/additionalproperties-is-a-string-keyed-record-of-the-value-type/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/additionalproperties-true-accepts-any-key/valibot.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/allinputs-union-carries-every-command-that-takes-an-input/main.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/an-array-request-body-with-parameters-stays-readable-as-both/commands.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/an-array-request-body-with-parameters-stays-readable-as-both/types.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/an-empty-properties-bag-is-a-record-not-an-empty-object-type/types.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/an-object-schema-that-omits-type-still-honours-required/types.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/an-operation-with-both-a-200-and-a-204-emits-one-output-type-argument/commands.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/json-request-body-members-keep-their-real-json-types-nested-included/types.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/main-ts-emits-file-level-import-type-for-type-only-imports/main.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/nested-query-param-members-get-the-same-stringish-treatment-as-top-level-ones/types.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/optional-query-params-do-not-carry-undefined-in-their-property-type/types.ts delete mode 100644 __tests__/__snapshots__/codegen-regressions/the-generated-json-body-type-is-pascalcase/types.ts create mode 100644 __tests__/__snapshots__/nullables.test.ts.snap delete mode 100644 __tests__/__snapshots__/nullables/3-0-nullable-objects-arrays-strings-and-combinator-members-admit-null/types.ts delete mode 100644 __tests__/__snapshots__/nullables/3-0-nullable-objects-arrays-strings-and-combinator-members-admit-null/valibot.ts delete mode 100644 __tests__/__snapshots__/nullables/const-values/types.ts delete mode 100644 __tests__/__snapshots__/nullables/const-values/valibot.ts delete mode 100644 __tests__/__snapshots__/nullables/enums-short-circuit-type-constraints-picklist-only/valibot.ts delete mode 100644 __tests__/__snapshots__/nullables/header-parameters/commands-validated.ts delete mode 100644 __tests__/__snapshots__/nullables/header-parameters/commands.ts delete mode 100644 __tests__/__snapshots__/nullables/header-parameters/hono.ts delete mode 100644 __tests__/__snapshots__/nullables/header-parameters/types.ts delete mode 100644 __tests__/__snapshots__/nullables/header-parameters/valibot.ts delete mode 100644 __tests__/__snapshots__/nullables/input-only-mode-omits-wire-schemas/valibot.ts delete mode 100644 __tests__/__snapshots__/nullables/nullables/types.ts delete mode 100644 __tests__/__snapshots__/nullables/oneof-with-type-null-generates-v-null/valibot.ts delete mode 100644 __tests__/__snapshots__/nullables/query-and-header-integer-params-coerce-strings-to-numbers/types.ts delete mode 100644 __tests__/__snapshots__/nullables/query-and-header-integer-params-coerce-strings-to-numbers/valibot.ts delete mode 100644 __tests__/__snapshots__/nullables/rfc-3339-temporal-formats/types.ts delete mode 100644 __tests__/__snapshots__/nullables/rfc-3339-temporal-formats/valibot.ts delete mode 100644 __tests__/__snapshots__/nullables/top-level-type-array-with-null/enums.ts delete mode 100644 __tests__/__snapshots__/nullables/top-level-type-array-with-null/types.ts delete mode 100644 __tests__/__snapshots__/nullables/top-level-type-array-with-null/valibot.ts create mode 100644 __tests__/__snapshots__/typescript-hint.test.ts.snap delete mode 100644 __tests__/__snapshots__/typescript-hint/x-typescript-hint-honored-inside-anyof-branches/types.ts delete mode 100644 __tests__/__snapshots__/typescript-hint/x-typescript-hint-honored-inside-oneof-branches/types.ts delete mode 100644 __tests__/__snapshots__/typescript-hint/x-typescript-hint-on-top-level-string-schema/types.ts diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index 4c989c2..d8bd843 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -4,8 +4,5 @@ "entry": ["bin/index.ts", "oxfmt.config.ts", "oxlint.config.ts"], // the Makefile runs its CLI to turn the openai document into JSON - "ignoreDependencies": ["js-yaml"], - - // generated code under test, whose imports name files a snapshot may omit - "ignorePatterns": ["__tests__/__snapshots__/**"] + "ignoreDependencies": ["js-yaml"] } diff --git a/__tests__/__snapshots__/codegen-regressions.test.ts.snap b/__tests__/__snapshots__/codegen-regressions.test.ts.snap new file mode 100644 index 0000000..8526dc0 --- /dev/null +++ b/__tests__/__snapshots__/codegen-regressions.test.ts.snap @@ -0,0 +1,414 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`AllInputs union carries every command that takes an input > main.ts 1`] = ` +"import { RestServiceClient, type RestServiceClientConfig } from "@block65/rest-client"; +import type { UndefinedOnPartialDeep } from "type-fest"; +import type { AlphaCommandOutput, BetaCommandOutput, GammaCommandOutput, WithBodyCommandInput, WithBodyCommandOutput, WithQueryCommandInput, WithQueryCommandOutput } from "./types.js"; + +export { ResponseValidationError } from "@block65/rest-client"; + +type AllInputs = UndefinedOnPartialDeep | UndefinedOnPartialDeep; +type AllOutputs = AlphaCommandOutput | BetaCommandOutput | GammaCommandOutput | WithBodyCommandOutput | WithQueryCommandOutput; + +export class TestRestClient extends RestServiceClient { + constructor(baseUrl: string | URL = new URL('https://api.example.com/'), config?: RestServiceClientConfig) { + super(baseUrl, config); + } +} +" +`; + +exports[`a $ref nested past the top level registers after its target > types.ts 1`] = ` +"export type Leaf = { + "name"?: string; + }; +export type InUnionBesideProperties = Leaf; +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()) + , + }); +export const leafSchema = v.strictObject( + { + "name": v.exactOptional(v.pipe(v.string(), v.trim())) + , + }); +export const inputInUnionBesidePropertiesSchema = v.looseObject( + { + ...inputLeafSchema + .entries, + }); +export const inUnionBesidePropertiesSchema = v.looseObject( + { + ...leafSchema + .entries, + }); +export const inputInRecordSchema = v.record(v.string(), inputLeafSchema); +export const inRecordSchema = inputInRecordSchema; +export const inputInArrayItemSchema = v.array(v.looseObject( + { + "leaf": v.optional(inputLeafSchema) + , + })); +export const inArrayItemSchema = v.array(v.looseObject( + { + "leaf": v.exactOptional(leafSchema) + , + })); +" +`; + +exports[`a oneOf query param keeps the stringish wire types in every branch > types.ts 1`] = ` +"import type { UndefinedOnPartialDeep } from "type-fest"; + +export type ListThingsCommandQuery = { + size?: \`\${number}\` | string; + flag?: "true" | "false" | string; + nested?: { + "count"?: \`\${number}\` | string; + }; + }; +export type ListThingsCommandInput = ListThingsCommandQuery; +export type ListThingsCommandOutput = string | undefined; +export type InputListThingsCommandResponse = UndefinedOnPartialDeep; +" +`; + +exports[`a single-member combinator emits the member alone: anyOf > 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; +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; +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; +export const getThingCommandResponseSchema = onlySchema; +" +`; + +exports[`a value the schema leaves open is a JsonValue on both sides > types.ts 1`] = ` +"import type { JsonValue } from "type-fest"; + +export type Open = { + "anything": JsonValue; + "record": Record; + "list": readonly (JsonValue)[]; + }; +" +`; + +exports[`a value the schema leaves open is a JsonValue on both sides > valibot.ts 1`] = ` +"import * as v from "valibot"; +import type { JsonValue } from "type-fest"; + +const jsonValueSchema: v.GenericSchema = v.lazy(() => v.union([v.string(), v.number(), v.boolean(), v.null(), v.record(v.string(), jsonValueSchema), v.array(jsonValueSchema)])); +export const inputOpenSchema = v.strictObject( + { + "anything": jsonValueSchema + , + "record": v.record(v.string(), jsonValueSchema) + , + "list": v.array(jsonValueSchema) + , + }); +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()) + , + }, v.number()); +export const configSchema = v.objectWithRest( + { + "name": v.exactOptional(v.pipe(v.string(), v.trim())) + , + }, v.number()); +export const inputGetThingCommandResponseSchema = inputConfigSchema; +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()) + , + }); +export const openSchema = v.looseObject( + { + "a": v.exactOptional(v.pipe(v.string(), v.trim())) + , + }); +export const inputGetThingCommandResponseSchema = inputOpenSchema; +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()) + , + }); +export const openSchema = v.strictObject( + { + "a": v.exactOptional(v.pipe(v.string(), v.trim())) + , + }); +export const inputGetThingCommandResponseSchema = inputOpenSchema; +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()) + , + }, v.string()); +export const openSchema = v.objectWithRest( + { + "a": v.exactOptional(v.pipe(v.string(), v.trim())) + , + }, v.pipe(v.string(), v.trim())); +export const inputGetThingCommandResponseSchema = inputOpenSchema; +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()) + , + }); +export const openSchema = v.looseObject( + { + "a": v.exactOptional(v.pipe(v.string(), v.trim())) + , + }); +export const inputGetThingCommandResponseSchema = inputOpenSchema; +export const getThingCommandResponseSchema = openSchema; +" +`; + +exports[`additionalProperties is a string-keyed record of the value type > types.ts 1`] = ` +"import type { UndefinedOnPartialDeep } from "type-fest"; + +export type Labels = Record; +export type GetThingCommandInput = never; +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; +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()) + , + }); +export const openSchema = v.looseObject( + { + "name": v.exactOptional(v.pipe(v.string(), v.trim())) + , + }); +export const inputGetThingCommandResponseSchema = inputOpenSchema; +export const getThingCommandResponseSchema = openSchema; +" +`; + +exports[`an array request body with parameters stays readable as both > commands.ts 1`] = ` +"import { Command, stripUndefined, jsonStringify } from "@block65/rest-client"; +import type { UndefinedOnPartialDeep } from "type-fest"; +import type { PluginPullCommandQuery, PluginPullCommandInput, PluginPullCommandOutput } from "./types.js"; + + +/** + * PluginPullCommand + * + */ +export class PluginPullCommand extends Command, PluginPullCommandOutput, PluginPullCommandQuery> { + public override method = "post" as const; + + constructor(input: UndefinedOnPartialDeep) { + const {remote, body } = input; + super("/plugins/pull", jsonStringify(body), stripUndefined({remote})); + } +} +" +`; + +exports[`an array request body with parameters stays readable as both > types.ts 1`] = ` +"import type { UndefinedOnPartialDeep } from "type-fest"; + +export type PluginPullCommandQuery = { + remote: string; + }; +type PluginPullCommandJsonBody = readonly (string)[]; +export type PluginPullCommandBody = PluginPullCommandJsonBody; +type PluginPullCommandBodyWrapper = { + body: PluginPullCommandJsonBody; + }; +export type PluginPullCommandInput = PluginPullCommandBodyWrapper & PluginPullCommandQuery; +export type PluginPullCommandOutput = string | undefined; +export type InputPluginPullCommandResponse = UndefinedOnPartialDeep; +" +`; + +exports[`an empty properties bag is a record, not an empty object type > types.ts 1`] = ` +"import type { JsonValue, UndefinedOnPartialDeep } from "type-fest"; + +export type Empty = Record; +export type GetThingCommandInput = never; +export type InputGetThingCommandResponse = UndefinedOnPartialDeep; +" +`; + +exports[`an object schema that omits \`type\` still honours \`required\` > types.ts 1`] = ` +"export type Untyped = { + "id": string; + "note"?: string; + }; +" +`; + +exports[`an operation with both a 200 and a 204 emits one output type argument > commands.ts 1`] = ` +"import { Command } from "@block65/rest-client"; +import type { UndefinedOnPartialDeep } from "type-fest"; +import type { SystemAuthCommandInput, SystemAuthCommandOutput } from "./types.js"; + + +/** + * SystemAuthCommand + * + */ +export class SystemAuthCommand extends Command, SystemAuthCommandOutput> { + public override method = "post" as const; + + constructor() { + super("/auth"); + } +} +" +`; + +exports[`json request body members keep their real JSON types, nested included > types.ts 1`] = ` +"import type { UndefinedOnPartialDeep } from "type-fest"; + +type CreateContainerCommandJsonBody = { + "tty"?: boolean; + "retries"?: number; + "health"?: { + "interval"?: bigint; + "enabled"?: boolean; + }; + "sizes"?: readonly (number)[]; + }; +export type CreateContainerCommandBody = CreateContainerCommandJsonBody; +export type CreateContainerCommandInput = CreateContainerCommandJsonBody; +export type CreateContainerCommandOutput = string | undefined; +export type InputCreateContainerCommandResponse = UndefinedOnPartialDeep; +" +`; + +exports[`main.ts emits file-level \`import type\` for type-only imports > main.ts 1`] = ` +"import { RestServiceClient, type RestServiceClientConfig } from "@block65/rest-client"; +import type { UndefinedOnPartialDeep } from "type-fest"; +import type { OneCommandInput, OneCommandOutput } from "./types.js"; + +export { ResponseValidationError } from "@block65/rest-client"; + +type AllInputs = UndefinedOnPartialDeep; +type AllOutputs = OneCommandOutput; + +export class TestRestClient extends RestServiceClient { + constructor(baseUrl: string | URL = new URL('https://api.example.com/'), config?: RestServiceClientConfig) { + super(baseUrl, config); + } +} +" +`; + +exports[`nested query param members get the same stringish treatment as top-level ones > types.ts 1`] = ` +"import type { UndefinedOnPartialDeep } from "type-fest"; + +export type SearchCommandQuery = { + limit?: \`\${number}\`; + filter?: { + "age"?: \`\${number}\`; + "big"?: \`\${bigint}\`; + "active"?: "true" | "false"; + "label"?: string; + }; + ids?: readonly (\`\${number}\`)[]; + }; +export type SearchCommandInput = SearchCommandQuery; +export type SearchCommandOutput = string | undefined; +export type InputSearchCommandResponse = UndefinedOnPartialDeep; +" +`; + +exports[`optional query params do not carry \`| undefined\` in their property type > types.ts 1`] = ` +"import type { UndefinedOnPartialDeep } from "type-fest"; + +export type ListFilesCommandQuery = { + purpose?: string; + limit?: \`\${number}\`; + }; +export type ListFilesCommandInput = ListFilesCommandQuery; +export type ListFilesCommandOutput = string | undefined; +export type InputListFilesCommandResponse = UndefinedOnPartialDeep; +" +`; + +exports[`the generated JSON body type is PascalCase > types.ts 1`] = ` +"import type { UndefinedOnPartialDeep } from "type-fest"; + +type CreateBatchJsonBody = { + "input_file_id"?: string; + }; +export type CreateBatchCommandBody = CreateBatchJsonBody; +export type CreateBatchCommandInput = CreateBatchJsonBody; +export type CreateBatchCommandOutput = string | undefined; +export type InputCreateBatchCommandResponse = UndefinedOnPartialDeep; +" +`; diff --git a/__tests__/__snapshots__/codegen-regressions/a-oneof-query-param-keeps-the-stringish-wire-types-in-every-branch/types.ts b/__tests__/__snapshots__/codegen-regressions/a-oneof-query-param-keeps-the-stringish-wire-types-in-every-branch/types.ts deleted file mode 100644 index 125522a..0000000 --- a/__tests__/__snapshots__/codegen-regressions/a-oneof-query-param-keeps-the-stringish-wire-types-in-every-branch/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { UndefinedOnPartialDeep } from "type-fest"; - -export type ListThingsCommandQuery = { - size?: `${number}` | string; - flag?: "true" | "false" | string; - nested?: { - "count"?: `${number}` | string; - }; - }; -export type ListThingsCommandInput = ListThingsCommandQuery; -export type ListThingsCommandOutput = string | undefined; -export type InputListThingsCommandResponse = UndefinedOnPartialDeep; diff --git a/__tests__/__snapshots__/codegen-regressions/a-ref-nested-past-the-top-level-registers-after-its-target/types.ts b/__tests__/__snapshots__/codegen-regressions/a-ref-nested-past-the-top-level-registers-after-its-target/types.ts deleted file mode 100644 index 63f1128..0000000 --- a/__tests__/__snapshots__/codegen-regressions/a-ref-nested-past-the-top-level-registers-after-its-target/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type Leaf = { - "name"?: string; - }; -export type InUnionBesideProperties = Leaf; -export type InRecord = Record; diff --git a/__tests__/__snapshots__/codegen-regressions/a-ref-nested-past-the-top-level-registers-after-its-target/valibot.ts b/__tests__/__snapshots__/codegen-regressions/a-ref-nested-past-the-top-level-registers-after-its-target/valibot.ts deleted file mode 100644 index 10f338b..0000000 --- a/__tests__/__snapshots__/codegen-regressions/a-ref-nested-past-the-top-level-registers-after-its-target/valibot.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as v from "valibot"; -export const inputLeafSchema = v.strictObject( - { - "name": v.optional(v.string()) - , - }); -export const leafSchema = v.strictObject( - { - "name": v.exactOptional(v.pipe(v.string(), v.trim())) - , - }); -export const inputInUnionBesidePropertiesSchema = v.looseObject( - { - ...inputLeafSchema - .entries, - }); -export const inUnionBesidePropertiesSchema = v.looseObject( - { - ...leafSchema - .entries, - }); -export const inputInRecordSchema = v.record(v.string(), inputLeafSchema); -export const inRecordSchema = inputInRecordSchema; -export const inputInArrayItemSchema = v.array(v.looseObject( - { - "leaf": v.optional(inputLeafSchema) - , - })); -export const inArrayItemSchema = v.array(v.looseObject( - { - "leaf": v.exactOptional(leafSchema) - , - })); diff --git a/__tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-anyof/valibot.ts b/__tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-anyof/valibot.ts deleted file mode 100644 index 940955c..0000000 --- a/__tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-anyof/valibot.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as v from "valibot"; -export const inputOnlySchema = v.string(); -export const onlySchema = v.pipe(v.string(), v.trim()); -export const inputGetThingCommandResponseSchema = inputOnlySchema; -export const getThingCommandResponseSchema = onlySchema; diff --git a/__tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-oneof/valibot.ts b/__tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-oneof/valibot.ts deleted file mode 100644 index 940955c..0000000 --- a/__tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-oneof/valibot.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as v from "valibot"; -export const inputOnlySchema = v.string(); -export const onlySchema = v.pipe(v.string(), v.trim()); -export const inputGetThingCommandResponseSchema = inputOnlySchema; -export const getThingCommandResponseSchema = onlySchema; diff --git a/__tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-two-member-oneof/valibot.ts b/__tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-two-member-oneof/valibot.ts deleted file mode 100644 index 8267dcf..0000000 --- a/__tests__/__snapshots__/codegen-regressions/a-single-member-combinator-emits-the-member-alone-two-member-oneof/valibot.ts +++ /dev/null @@ -1,5 +0,0 @@ -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; -export const getThingCommandResponseSchema = onlySchema; diff --git a/__tests__/__snapshots__/codegen-regressions/a-value-the-schema-leaves-open-is-a-jsonvalue-on-both-sides/types.ts b/__tests__/__snapshots__/codegen-regressions/a-value-the-schema-leaves-open-is-a-jsonvalue-on-both-sides/types.ts deleted file mode 100644 index 1cbe9e9..0000000 --- a/__tests__/__snapshots__/codegen-regressions/a-value-the-schema-leaves-open-is-a-jsonvalue-on-both-sides/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { JsonValue } from "type-fest"; - -export type Open = { - "anything": JsonValue; - "record": Record; - "list": readonly (JsonValue)[]; - }; diff --git a/__tests__/__snapshots__/codegen-regressions/a-value-the-schema-leaves-open-is-a-jsonvalue-on-both-sides/valibot.ts b/__tests__/__snapshots__/codegen-regressions/a-value-the-schema-leaves-open-is-a-jsonvalue-on-both-sides/valibot.ts deleted file mode 100644 index a665298..0000000 --- a/__tests__/__snapshots__/codegen-regressions/a-value-the-schema-leaves-open-is-a-jsonvalue-on-both-sides/valibot.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as v from "valibot"; -import type { JsonValue } from "type-fest"; - -const jsonValueSchema: v.GenericSchema = v.lazy(() => v.union([v.string(), v.number(), v.boolean(), v.null(), v.record(v.string(), jsonValueSchema), v.array(jsonValueSchema)])); -export const inputOpenSchema = v.strictObject( - { - "anything": jsonValueSchema - , - "record": v.record(v.string(), jsonValueSchema) - , - "list": v.array(jsonValueSchema) - , - }); -export const openSchema = inputOpenSchema; diff --git a/__tests__/__snapshots__/codegen-regressions/additionalproperties-alongside-properties-keeps-the-extra-keys-valid/valibot.ts b/__tests__/__snapshots__/codegen-regressions/additionalproperties-alongside-properties-keeps-the-extra-keys-valid/valibot.ts deleted file mode 100644 index 4d2ad55..0000000 --- a/__tests__/__snapshots__/codegen-regressions/additionalproperties-alongside-properties-keeps-the-extra-keys-valid/valibot.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as v from "valibot"; -export const inputConfigSchema = v.objectWithRest( - { - "name": v.optional(v.string()) - , - }, v.number()); -export const configSchema = v.objectWithRest( - { - "name": v.exactOptional(v.pipe(v.string(), v.trim())) - , - }, v.number()); -export const inputGetThingCommandResponseSchema = inputConfigSchema; -export const getThingCommandResponseSchema = configSchema; diff --git a/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-empty-schema/valibot.ts b/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-empty-schema/valibot.ts deleted file mode 100644 index efdce05..0000000 --- a/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-empty-schema/valibot.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as v from "valibot"; -export const inputOpenSchema = v.looseObject( - { - "a": v.optional(v.string()) - , - }); -export const openSchema = v.looseObject( - { - "a": v.exactOptional(v.pipe(v.string(), v.trim())) - , - }); -export const inputGetThingCommandResponseSchema = inputOpenSchema; -export const getThingCommandResponseSchema = openSchema; diff --git a/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-false/valibot.ts b/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-false/valibot.ts deleted file mode 100644 index 30eb592..0000000 --- a/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-false/valibot.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as v from "valibot"; -export const inputOpenSchema = v.strictObject( - { - "a": v.optional(v.string()) - , - }); -export const openSchema = v.strictObject( - { - "a": v.exactOptional(v.pipe(v.string(), v.trim())) - , - }); -export const inputGetThingCommandResponseSchema = inputOpenSchema; -export const getThingCommandResponseSchema = openSchema; diff --git a/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-string-schema/valibot.ts b/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-string-schema/valibot.ts deleted file mode 100644 index 134699b..0000000 --- a/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-string-schema/valibot.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as v from "valibot"; -export const inputOpenSchema = v.objectWithRest( - { - "a": v.optional(v.string()) - , - }, v.string()); -export const openSchema = v.objectWithRest( - { - "a": v.exactOptional(v.pipe(v.string(), v.trim())) - , - }, v.pipe(v.string(), v.trim())); -export const inputGetThingCommandResponseSchema = inputOpenSchema; -export const getThingCommandResponseSchema = openSchema; diff --git a/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-true/valibot.ts b/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-true/valibot.ts deleted file mode 100644 index efdce05..0000000 --- a/__tests__/__snapshots__/codegen-regressions/additionalproperties-chooses-the-object-schema-true/valibot.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as v from "valibot"; -export const inputOpenSchema = v.looseObject( - { - "a": v.optional(v.string()) - , - }); -export const openSchema = v.looseObject( - { - "a": v.exactOptional(v.pipe(v.string(), v.trim())) - , - }); -export const inputGetThingCommandResponseSchema = inputOpenSchema; -export const getThingCommandResponseSchema = openSchema; diff --git a/__tests__/__snapshots__/codegen-regressions/additionalproperties-is-a-string-keyed-record-of-the-value-type/types.ts b/__tests__/__snapshots__/codegen-regressions/additionalproperties-is-a-string-keyed-record-of-the-value-type/types.ts deleted file mode 100644 index 8eb1824..0000000 --- a/__tests__/__snapshots__/codegen-regressions/additionalproperties-is-a-string-keyed-record-of-the-value-type/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { UndefinedOnPartialDeep } from "type-fest"; - -export type Labels = Record; -export type GetThingCommandInput = never; -export type InputGetThingCommandResponse = UndefinedOnPartialDeep; diff --git a/__tests__/__snapshots__/codegen-regressions/additionalproperties-is-a-string-keyed-record-of-the-value-type/valibot.ts b/__tests__/__snapshots__/codegen-regressions/additionalproperties-is-a-string-keyed-record-of-the-value-type/valibot.ts deleted file mode 100644 index f258c90..0000000 --- a/__tests__/__snapshots__/codegen-regressions/additionalproperties-is-a-string-keyed-record-of-the-value-type/valibot.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as v from "valibot"; -export const inputLabelsSchema = v.record(v.string(), v.string()); -export const labelsSchema = inputLabelsSchema; -export const inputGetThingCommandResponseSchema = inputLabelsSchema; -export const getThingCommandResponseSchema = labelsSchema; diff --git a/__tests__/__snapshots__/codegen-regressions/additionalproperties-true-accepts-any-key/valibot.ts b/__tests__/__snapshots__/codegen-regressions/additionalproperties-true-accepts-any-key/valibot.ts deleted file mode 100644 index a56b6be..0000000 --- a/__tests__/__snapshots__/codegen-regressions/additionalproperties-true-accepts-any-key/valibot.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as v from "valibot"; -export const inputOpenSchema = v.looseObject( - { - "name": v.optional(v.string()) - , - }); -export const openSchema = v.looseObject( - { - "name": v.exactOptional(v.pipe(v.string(), v.trim())) - , - }); -export const inputGetThingCommandResponseSchema = inputOpenSchema; -export const getThingCommandResponseSchema = openSchema; diff --git a/__tests__/__snapshots__/codegen-regressions/allinputs-union-carries-every-command-that-takes-an-input/main.ts b/__tests__/__snapshots__/codegen-regressions/allinputs-union-carries-every-command-that-takes-an-input/main.ts deleted file mode 100644 index dcab762..0000000 --- a/__tests__/__snapshots__/codegen-regressions/allinputs-union-carries-every-command-that-takes-an-input/main.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { RestServiceClient, type RestServiceClientConfig } from "@block65/rest-client"; -import type { UndefinedOnPartialDeep } from "type-fest"; -import type { AlphaCommandOutput, BetaCommandOutput, GammaCommandOutput, WithBodyCommandInput, WithBodyCommandOutput, WithQueryCommandInput, WithQueryCommandOutput } from "./types.js"; - -export { ResponseValidationError } from "@block65/rest-client"; - -type AllInputs = UndefinedOnPartialDeep | UndefinedOnPartialDeep; -type AllOutputs = AlphaCommandOutput | BetaCommandOutput | GammaCommandOutput | WithBodyCommandOutput | WithQueryCommandOutput; - -export class TestRestClient extends RestServiceClient { - constructor(baseUrl: string | URL = new URL('https://api.example.com/'), config?: RestServiceClientConfig) { - super(baseUrl, config); - } -} diff --git a/__tests__/__snapshots__/codegen-regressions/an-array-request-body-with-parameters-stays-readable-as-both/commands.ts b/__tests__/__snapshots__/codegen-regressions/an-array-request-body-with-parameters-stays-readable-as-both/commands.ts deleted file mode 100644 index fb77ee8..0000000 --- a/__tests__/__snapshots__/codegen-regressions/an-array-request-body-with-parameters-stays-readable-as-both/commands.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Command, stripUndefined, jsonStringify } from "@block65/rest-client"; -import type { UndefinedOnPartialDeep } from "type-fest"; -import type { PluginPullCommandQuery, PluginPullCommandInput, PluginPullCommandOutput } from "./types.js"; - - -/** - * PluginPullCommand - * - */ -export class PluginPullCommand extends Command, PluginPullCommandOutput, PluginPullCommandQuery> { - public override method = "post" as const; - - constructor(input: UndefinedOnPartialDeep) { - const {remote, body } = input; - super("/plugins/pull", jsonStringify(body), stripUndefined({remote})); - } -} diff --git a/__tests__/__snapshots__/codegen-regressions/an-array-request-body-with-parameters-stays-readable-as-both/types.ts b/__tests__/__snapshots__/codegen-regressions/an-array-request-body-with-parameters-stays-readable-as-both/types.ts deleted file mode 100644 index 81fac9e..0000000 --- a/__tests__/__snapshots__/codegen-regressions/an-array-request-body-with-parameters-stays-readable-as-both/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { UndefinedOnPartialDeep } from "type-fest"; - -export type PluginPullCommandQuery = { - remote: string; - }; -type PluginPullCommandJsonBody = readonly (string)[]; -export type PluginPullCommandBody = PluginPullCommandJsonBody; -type PluginPullCommandBodyWrapper = { - body: PluginPullCommandJsonBody; - }; -export type PluginPullCommandInput = PluginPullCommandBodyWrapper & PluginPullCommandQuery; -export type PluginPullCommandOutput = string | undefined; -export type InputPluginPullCommandResponse = UndefinedOnPartialDeep; diff --git a/__tests__/__snapshots__/codegen-regressions/an-empty-properties-bag-is-a-record-not-an-empty-object-type/types.ts b/__tests__/__snapshots__/codegen-regressions/an-empty-properties-bag-is-a-record-not-an-empty-object-type/types.ts deleted file mode 100644 index 0c78c93..0000000 --- a/__tests__/__snapshots__/codegen-regressions/an-empty-properties-bag-is-a-record-not-an-empty-object-type/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { JsonValue, UndefinedOnPartialDeep } from "type-fest"; - -export type Empty = Record; -export type GetThingCommandInput = never; -export type InputGetThingCommandResponse = UndefinedOnPartialDeep; diff --git a/__tests__/__snapshots__/codegen-regressions/an-object-schema-that-omits-type-still-honours-required/types.ts b/__tests__/__snapshots__/codegen-regressions/an-object-schema-that-omits-type-still-honours-required/types.ts deleted file mode 100644 index 83425c3..0000000 --- a/__tests__/__snapshots__/codegen-regressions/an-object-schema-that-omits-type-still-honours-required/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Untyped = { - "id": string; - "note"?: string; - }; diff --git a/__tests__/__snapshots__/codegen-regressions/an-operation-with-both-a-200-and-a-204-emits-one-output-type-argument/commands.ts b/__tests__/__snapshots__/codegen-regressions/an-operation-with-both-a-200-and-a-204-emits-one-output-type-argument/commands.ts deleted file mode 100644 index 05c8bd3..0000000 --- a/__tests__/__snapshots__/codegen-regressions/an-operation-with-both-a-200-and-a-204-emits-one-output-type-argument/commands.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Command } from "@block65/rest-client"; -import type { UndefinedOnPartialDeep } from "type-fest"; -import type { SystemAuthCommandInput, SystemAuthCommandOutput } from "./types.js"; - - -/** - * SystemAuthCommand - * - */ -export class SystemAuthCommand extends Command, SystemAuthCommandOutput> { - public override method = "post" as const; - - constructor() { - super("/auth"); - } -} diff --git a/__tests__/__snapshots__/codegen-regressions/json-request-body-members-keep-their-real-json-types-nested-included/types.ts b/__tests__/__snapshots__/codegen-regressions/json-request-body-members-keep-their-real-json-types-nested-included/types.ts deleted file mode 100644 index 4b211f4..0000000 --- a/__tests__/__snapshots__/codegen-regressions/json-request-body-members-keep-their-real-json-types-nested-included/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { UndefinedOnPartialDeep } from "type-fest"; - -type CreateContainerCommandJsonBody = { - "tty"?: boolean; - "retries"?: number; - "health"?: { - "interval"?: bigint; - "enabled"?: boolean; - }; - "sizes"?: readonly (number)[]; - }; -export type CreateContainerCommandBody = CreateContainerCommandJsonBody; -export type CreateContainerCommandInput = CreateContainerCommandJsonBody; -export type CreateContainerCommandOutput = string | undefined; -export type InputCreateContainerCommandResponse = UndefinedOnPartialDeep; diff --git a/__tests__/__snapshots__/codegen-regressions/main-ts-emits-file-level-import-type-for-type-only-imports/main.ts b/__tests__/__snapshots__/codegen-regressions/main-ts-emits-file-level-import-type-for-type-only-imports/main.ts deleted file mode 100644 index bfb52e8..0000000 --- a/__tests__/__snapshots__/codegen-regressions/main-ts-emits-file-level-import-type-for-type-only-imports/main.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { RestServiceClient, type RestServiceClientConfig } from "@block65/rest-client"; -import type { UndefinedOnPartialDeep } from "type-fest"; -import type { OneCommandInput, OneCommandOutput } from "./types.js"; - -export { ResponseValidationError } from "@block65/rest-client"; - -type AllInputs = UndefinedOnPartialDeep; -type AllOutputs = OneCommandOutput; - -export class TestRestClient extends RestServiceClient { - constructor(baseUrl: string | URL = new URL('https://api.example.com/'), config?: RestServiceClientConfig) { - super(baseUrl, config); - } -} diff --git a/__tests__/__snapshots__/codegen-regressions/nested-query-param-members-get-the-same-stringish-treatment-as-top-level-ones/types.ts b/__tests__/__snapshots__/codegen-regressions/nested-query-param-members-get-the-same-stringish-treatment-as-top-level-ones/types.ts deleted file mode 100644 index 4d8aea6..0000000 --- a/__tests__/__snapshots__/codegen-regressions/nested-query-param-members-get-the-same-stringish-treatment-as-top-level-ones/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { UndefinedOnPartialDeep } from "type-fest"; - -export type SearchCommandQuery = { - limit?: `${number}`; - filter?: { - "age"?: `${number}`; - "big"?: `${bigint}`; - "active"?: "true" | "false"; - "label"?: string; - }; - ids?: readonly (`${number}`)[]; - }; -export type SearchCommandInput = SearchCommandQuery; -export type SearchCommandOutput = string | undefined; -export type InputSearchCommandResponse = UndefinedOnPartialDeep; diff --git a/__tests__/__snapshots__/codegen-regressions/optional-query-params-do-not-carry-undefined-in-their-property-type/types.ts b/__tests__/__snapshots__/codegen-regressions/optional-query-params-do-not-carry-undefined-in-their-property-type/types.ts deleted file mode 100644 index 4f971be..0000000 --- a/__tests__/__snapshots__/codegen-regressions/optional-query-params-do-not-carry-undefined-in-their-property-type/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { UndefinedOnPartialDeep } from "type-fest"; - -export type ListFilesCommandQuery = { - purpose?: string; - limit?: `${number}`; - }; -export type ListFilesCommandInput = ListFilesCommandQuery; -export type ListFilesCommandOutput = string | undefined; -export type InputListFilesCommandResponse = UndefinedOnPartialDeep; diff --git a/__tests__/__snapshots__/codegen-regressions/the-generated-json-body-type-is-pascalcase/types.ts b/__tests__/__snapshots__/codegen-regressions/the-generated-json-body-type-is-pascalcase/types.ts deleted file mode 100644 index dd36a71..0000000 --- a/__tests__/__snapshots__/codegen-regressions/the-generated-json-body-type-is-pascalcase/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { UndefinedOnPartialDeep } from "type-fest"; - -type CreateBatchJsonBody = { - "input_file_id"?: string; - }; -export type CreateBatchCommandBody = CreateBatchJsonBody; -export type CreateBatchCommandInput = CreateBatchJsonBody; -export type CreateBatchCommandOutput = string | undefined; -export type InputCreateBatchCommandResponse = UndefinedOnPartialDeep; diff --git a/__tests__/__snapshots__/nullables.test.ts.snap b/__tests__/__snapshots__/nullables.test.ts.snap new file mode 100644 index 0000000..bc0ae30 --- /dev/null +++ b/__tests__/__snapshots__/nullables.test.ts.snap @@ -0,0 +1,334 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`3.0 nullable objects, arrays, strings and combinator members admit null > types.ts 1`] = ` +"export type Base = { + "id"?: string; + }; +export type Nullables = { + "object": { + "id"?: string; + } | null; + "array": readonly (string)[] | null; + "string": string | null; + "stringEnum": "a" | "b" | null; + "unionMember": string | readonly (string)[] | null; + "intersectionMember": Base & { + "name"?: string; + }; + }; +" +`; + +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()) + , + }); +export const baseSchema = v.looseObject( + { + "id": v.exactOptional(v.pipe(v.string(), v.trim())) + , + }); +export const inputNullablesSchema = v.looseObject( + { + "object": v.nullable(v.looseObject( + { + "id": v.optional(v.string()) + , + })) + , + "array": v.nullable(v.array(v.string())) + , + "string": v.nullable(v.string()) + , + "stringEnum": v.nullable(v.picklist(["a", "b"])) + , + "unionMember": v.nullable(v.union([v.nullable(v.string()), v.array(v.string())])) + , + "intersectionMember": v.looseObject( + { + ...inputBaseSchema + .entries, + "name": v.optional(v.string()) + , + }) + , + }); +export const nullablesSchema = v.looseObject( + { + "object": v.nullable(v.looseObject( + { + "id": v.exactOptional(v.pipe(v.string(), v.trim())) + , + })) + , + "array": v.nullable(v.array(v.pipe(v.string(), v.trim()))) + , + "string": v.nullable(v.pipe(v.string(), v.trim())) + , + "stringEnum": v.nullable(v.picklist(["a", "b"])) + , + "unionMember": v.nullable(v.union([v.nullable(v.pipe(v.string(), v.trim())), v.array(v.pipe(v.string(), v.trim()))])) + , + "intersectionMember": v.looseObject( + { + ...baseSchema + .entries, + "name": v.exactOptional(v.pipe(v.string(), v.trim())) + , + }) + , + }); +" +`; + +exports[`RFC 3339 temporal formats > types.ts 1`] = ` +"export type MyDate = \`\${number}-\${number}-\${number}\`; +export type MyTime = \`\${number}:\${number}:\${number}\${string}\`; +export type MyDateTime = \`\${number}-\${number}-\${number}T\${number}:\${number}:\${number}\${string}\`; +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)); +export const myTimeSchema = v.pipe(v.string(), v.trim(), 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)); +export const inputMyDateTimeSchema = v.pipe(v.string(), v.regex(/^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])[Tt ]([01]\\d|2[0-3]):[0-5]\\d:([0-5]\\d|60)(\\.\\d+)?([Zz]|[+-]([01]\\d|2[0-3]):[0-5]\\d)$/u, "date-time"), v.custom<\`\${number}-\${number}-\${number}T\${number}:\${number}:\${number}\${string}\`>(() => true)); +export const myDateTimeSchema = v.pipe(v.string(), v.trim(), v.regex(/^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])[Tt ]([01]\\d|2[0-3]):[0-5]\\d:([0-5]\\d|60)(\\.\\d+)?([Zz]|[+-]([01]\\d|2[0-3]):[0-5]\\d)$/u, "date-time"), v.custom<\`\${number}-\${number}-\${number}T\${number}:\${number}:\${number}\${string}\`>(() => true)); +export const inputMyDurationSchema = v.pipe(v.string(), v.regex(/^P(?!$)((\\d+Y)?(\\d+M)?(\\d+W)?(\\d+D)?)(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+(\\.\\d+)?S)?)?$/u, "duration"), v.custom<\`P\${string}\`>(() => true)); +export const myDurationSchema = v.pipe(v.string(), v.trim(), v.regex(/^P(?!$)((\\d+Y)?(\\d+M)?(\\d+W)?(\\d+D)?)(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+(\\.\\d+)?S)?)?$/u, "duration"), v.custom<\`P\${string}\`>(() => true)); +" +`; + +exports[`const values > types.ts 1`] = ` +"export type StringConst = "hello"; +export type NumberConst = 42; +export type BooleanConst = true; +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); +export const numberConstSchema = inputNumberConstSchema; +export const inputBooleanConstSchema = v.literal(true); +export const booleanConstSchema = inputBooleanConstSchema; +export const inputNullConstSchema = v.null(); +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"]); +export const stringEnumSchema = inputStringEnumSchema; +" +`; + +exports[`header parameters > commands.ts 1`] = ` +"import { Command } from "@block65/rest-client"; +import type { UndefinedOnPartialDeep } from "type-fest"; +import type { UploadDataCommandHeader, UploadDataCommandInput, UploadStatus } from "./types.js"; + +/** + * Tagged template literal that applies encodeURIComponent to all interpolated + * values, protecting path integrity from characters like \`/\` and \`#\`. + * @example encodePath\`/users/\${userId}\` // "/users/foo%2Fbar" + */ +function encodePath(strings: TemplateStringsArray, ...values: string[]) { + return String.raw({ raw: strings }, ...values.map((value) => encodeURIComponent(value))); +} + +/** + * UploadDataCommand + * + */ +export class UploadDataCommand extends Command, UploadStatus, never, UploadDataCommandHeader> { + public override method = "post" as const; + + constructor(input: UndefinedOnPartialDeep, headers: UploadDataCommandHeader) { + const {uploadId } = input; + super(encodePath\`/uploads/\${uploadId}\`, undefined, undefined, headers); + } +} +" +`; + +exports[`header parameters > commands-validated.ts 1`] = ` +"import * as commands from "./commands.js"; +import * as schemas from "./valibot.js"; + +export class UploadDataCommand extends commands.UploadDataCommand { + static responseSchema = schemas.uploadDataCommandResponseSchema; +} +" +`; + +exports[`header parameters > hono.ts 1`] = ` +"import { sValidator } from "@hono/standard-validator"; +import type { ValidationTargets } from "hono"; +import { PublicValidationError } from "@block65/rest-client"; +import { uploadDataCommandParamsSchema } from "./valibot.js"; + +type StandardSchema = Parameters[1]; + +// The query arrives here as Hono parsed it, and nothing below decodes +// it. A lone value for a repeated-key array stays a string, comma, +// space and pipe joined values stay joined, and a deepObject key stays +// bracketed, so each of those fails validation. A caller sending them +// decodes the query before this middleware or replaces it, reading the +// style and explode from the query spec each operation exports below. +function validate(target: TTarget, schema: TSchema) { + + return sValidator(target, schema, (result) => { + if (!result.success) { + throw PublicValidationError.fromIssues(result.error); + } + }); + +} + +export const uploadData = [ + validate("param", uploadDataCommandParamsSchema), + ] as const; +" +`; + +exports[`header parameters > types.ts 1`] = ` +"import type { UndefinedOnPartialDeep } from "type-fest"; + +export type UploadStatus = "pending" | "complete"; +export type UploadDataCommandHeader = { + "content-type": "application/json" | "text/csv" | "application/xml"; + "content-length": \`\${bigint}\`; + "x-idempotency-key"?: string; + }; +export type UploadDataCommandParams = { + uploadId: string; + }; +export type UploadDataCommandInput = UploadDataCommandParams; +export type InputUploadDataCommandResponse = UndefinedOnPartialDeep; +" +`; + +exports[`header parameters > valibot.ts 1`] = ` +"import * as v from "valibot"; +export const inputUploadStatusSchema = v.picklist(["pending", "complete"]); +export const uploadStatusSchema = inputUploadStatusSchema; +export const inputUploadDataCommandResponseSchema = inputUploadStatusSchema; +export const uploadDataCommandResponseSchema = uploadStatusSchema; +export const inputUploadDataCommandParamsSchema = v.strictObject({ + "uploadId": v.string() + }); +export const uploadDataCommandParamsSchema = v.strictObject({ + "uploadId": v.pipe(v.string(), v.trim()) + }); +export const inputUploadDataCommandHeaderSchema = v.object({ + "content-type": v.picklist(["application/json", "text/csv", "application/xml"]), + "content-length": v.bigint(), + "x-idempotency-key": v.optional(v.pipe(v.string(), v.uuid())) + }); +export const uploadDataCommandHeaderSchema = v.object({ + "content-type": v.picklist(["application/json", "text/csv", "application/xml"]), + "content-length": v.union([v.pipe(v.string(), v.decimal(), v.toBigint(), v.bigint()), v.pipe(v.number(), v.integer(), v.toBigint(), v.bigint()), v.bigint()]), + "x-idempotency-key": v.exactOptional(v.pipe(v.string(), v.uuid())) + }); +" +`; + +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)); +" +`; + +exports[`nullables > types.ts 1`] = ` +"export type MySchemaLolOrNullable = "lol" | "kek" | null; +" +`; + +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()]); +" +`; + +exports[`query and header integer params coerce strings to numbers > types.ts 1`] = ` +"import type { UndefinedOnPartialDeep } from "type-fest"; + +export type Dummy = string; +export type ExpireTime = bigint; +export type ListFilesCommandQuery = { + exp: \`\${bigint}\`; + limit?: \`\${number}\`; + }; +export type ListFilesCommandHeader = { + "x-rate-limit": \`\${number}\`; + }; +export type ListFilesCommandInput = ListFilesCommandQuery; +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)); +export const expireTimeSchema = v.union([v.pipe(v.string(), v.decimal(), v.toBigint(), v.pipe(v.bigint(), v.minValue(0n))), v.pipe(v.number(), v.integer(), v.toBigint(), v.pipe(v.bigint(), v.minValue(0n))), v.pipe(v.bigint(), v.minValue(0n))]); +export const inputListFilesCommandResponseSchema = inputDummySchema; +export const listFilesCommandResponseSchema = dummySchema; +export const inputListFilesCommandQuerySchema = v.strictObject({ + "exp": v.pipe(v.bigint(), v.minValue(0n)), + "limit": v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100))) + }); +export const listFilesCommandQuerySchema = v.strictObject({ + "exp": v.union([v.pipe(v.string(), v.decimal(), v.toBigint(), v.pipe(v.bigint(), v.minValue(0n))), v.pipe(v.number(), v.integer(), v.toBigint(), v.pipe(v.bigint(), v.minValue(0n))), v.pipe(v.bigint(), v.minValue(0n))]), + "limit": v.exactOptional(v.union([v.pipe(v.string(), v.decimal(), v.toNumber(), v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100))), v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100))])) + }); +export const inputListFilesCommandHeaderSchema = v.object({ + "x-rate-limit": v.pipe(v.number(), v.integer(), v.minValue(0)) + }); +export const listFilesCommandHeaderSchema = v.object({ + "x-rate-limit": v.union([v.pipe(v.string(), v.decimal(), v.toNumber(), v.pipe(v.number(), v.integer(), v.minValue(0))), v.pipe(v.number(), v.integer(), v.minValue(0))]) + }); +" +`; + +exports[`top-level type array with null > enums.ts 1`] = ` +"export const nullableStringEnum = ["active", "inactive"] as const; +" +`; + +exports[`top-level type array with null > types.ts 1`] = ` +"export type NullableString = string | null; +export type NullableStringEnum = "active" | "inactive" | null; +export type NullableInteger = number | null; +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"])); +export const nullableStringEnumSchema = inputNullableStringEnumSchema; +export const inputNullableIntegerSchema = v.nullable(v.pipe(v.number(), v.integer())); +export const nullableIntegerSchema = inputNullableIntegerSchema; +export const inputMultiTypeSchema = v.union([v.string(), v.number()]); +export const multiTypeSchema = inputMultiTypeSchema; +" +`; diff --git a/__tests__/__snapshots__/nullables/3-0-nullable-objects-arrays-strings-and-combinator-members-admit-null/types.ts b/__tests__/__snapshots__/nullables/3-0-nullable-objects-arrays-strings-and-combinator-members-admit-null/types.ts deleted file mode 100644 index ec2f3bb..0000000 --- a/__tests__/__snapshots__/nullables/3-0-nullable-objects-arrays-strings-and-combinator-members-admit-null/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type Base = { - "id"?: string; - }; -export type Nullables = { - "object": { - "id"?: string; - } | null; - "array": readonly (string)[] | null; - "string": string | null; - "stringEnum": "a" | "b" | null; - "unionMember": string | readonly (string)[] | null; - "intersectionMember": Base & { - "name"?: string; - }; - }; diff --git a/__tests__/__snapshots__/nullables/3-0-nullable-objects-arrays-strings-and-combinator-members-admit-null/valibot.ts b/__tests__/__snapshots__/nullables/3-0-nullable-objects-arrays-strings-and-combinator-members-admit-null/valibot.ts deleted file mode 100644 index b8a1e89..0000000 --- a/__tests__/__snapshots__/nullables/3-0-nullable-objects-arrays-strings-and-combinator-members-admit-null/valibot.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as v from "valibot"; -export const inputBaseSchema = v.looseObject( - { - "id": v.optional(v.string()) - , - }); -export const baseSchema = v.looseObject( - { - "id": v.exactOptional(v.pipe(v.string(), v.trim())) - , - }); -export const inputNullablesSchema = v.looseObject( - { - "object": v.nullable(v.looseObject( - { - "id": v.optional(v.string()) - , - })) - , - "array": v.nullable(v.array(v.string())) - , - "string": v.nullable(v.string()) - , - "stringEnum": v.nullable(v.picklist(["a", "b"])) - , - "unionMember": v.nullable(v.union([v.nullable(v.string()), v.array(v.string())])) - , - "intersectionMember": v.looseObject( - { - ...inputBaseSchema - .entries, - "name": v.optional(v.string()) - , - }) - , - }); -export const nullablesSchema = v.looseObject( - { - "object": v.nullable(v.looseObject( - { - "id": v.exactOptional(v.pipe(v.string(), v.trim())) - , - })) - , - "array": v.nullable(v.array(v.pipe(v.string(), v.trim()))) - , - "string": v.nullable(v.pipe(v.string(), v.trim())) - , - "stringEnum": v.nullable(v.picklist(["a", "b"])) - , - "unionMember": v.nullable(v.union([v.nullable(v.pipe(v.string(), v.trim())), v.array(v.pipe(v.string(), v.trim()))])) - , - "intersectionMember": v.looseObject( - { - ...baseSchema - .entries, - "name": v.exactOptional(v.pipe(v.string(), v.trim())) - , - }) - , - }); diff --git a/__tests__/__snapshots__/nullables/const-values/types.ts b/__tests__/__snapshots__/nullables/const-values/types.ts deleted file mode 100644 index 92be38c..0000000 --- a/__tests__/__snapshots__/nullables/const-values/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type StringConst = "hello"; -export type NumberConst = 42; -export type BooleanConst = true; -export type NullConst = null; diff --git a/__tests__/__snapshots__/nullables/const-values/valibot.ts b/__tests__/__snapshots__/nullables/const-values/valibot.ts deleted file mode 100644 index a59e06a..0000000 --- a/__tests__/__snapshots__/nullables/const-values/valibot.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as v from "valibot"; -export const inputStringConstSchema = v.literal("hello"); -export const stringConstSchema = inputStringConstSchema; -export const inputNumberConstSchema = v.literal(42); -export const numberConstSchema = inputNumberConstSchema; -export const inputBooleanConstSchema = v.literal(true); -export const booleanConstSchema = inputBooleanConstSchema; -export const inputNullConstSchema = v.null(); -export const nullConstSchema = inputNullConstSchema; diff --git a/__tests__/__snapshots__/nullables/enums-short-circuit-type-constraints-picklist-only/valibot.ts b/__tests__/__snapshots__/nullables/enums-short-circuit-type-constraints-picklist-only/valibot.ts deleted file mode 100644 index ee43703..0000000 --- a/__tests__/__snapshots__/nullables/enums-short-circuit-type-constraints-picklist-only/valibot.ts +++ /dev/null @@ -1,5 +0,0 @@ -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"]); -export const stringEnumSchema = inputStringEnumSchema; diff --git a/__tests__/__snapshots__/nullables/header-parameters/commands-validated.ts b/__tests__/__snapshots__/nullables/header-parameters/commands-validated.ts deleted file mode 100644 index 1ea8051..0000000 --- a/__tests__/__snapshots__/nullables/header-parameters/commands-validated.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as commands from "./commands.js"; -import * as schemas from "./valibot.js"; - -export class UploadDataCommand extends commands.UploadDataCommand { - static responseSchema = schemas.uploadDataCommandResponseSchema; -} diff --git a/__tests__/__snapshots__/nullables/header-parameters/commands.ts b/__tests__/__snapshots__/nullables/header-parameters/commands.ts deleted file mode 100644 index 3814d94..0000000 --- a/__tests__/__snapshots__/nullables/header-parameters/commands.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Command } from "@block65/rest-client"; -import type { UndefinedOnPartialDeep } from "type-fest"; -import type { UploadDataCommandHeader, UploadDataCommandInput, UploadStatus } from "./types.js"; - -/** - * Tagged template literal that applies encodeURIComponent to all interpolated - * values, protecting path integrity from characters like `/` and `#`. - * @example encodePath`/users/${userId}` // "/users/foo%2Fbar" - */ -function encodePath(strings: TemplateStringsArray, ...values: string[]) { - return String.raw({ raw: strings }, ...values.map((value) => encodeURIComponent(value))); -} - -/** - * UploadDataCommand - * - */ -export class UploadDataCommand extends Command, UploadStatus, never, UploadDataCommandHeader> { - public override method = "post" as const; - - constructor(input: UndefinedOnPartialDeep, headers: UploadDataCommandHeader) { - const {uploadId } = input; - super(encodePath`/uploads/${uploadId}`, undefined, undefined, headers); - } -} diff --git a/__tests__/__snapshots__/nullables/header-parameters/hono.ts b/__tests__/__snapshots__/nullables/header-parameters/hono.ts deleted file mode 100644 index b4b5679..0000000 --- a/__tests__/__snapshots__/nullables/header-parameters/hono.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { sValidator } from "@hono/standard-validator"; -import type { ValidationTargets } from "hono"; -import { PublicValidationError } from "@block65/rest-client"; -import { uploadDataCommandParamsSchema } from "./valibot.js"; - -type StandardSchema = Parameters[1]; - -// The query arrives here as Hono parsed it, and nothing below decodes -// it. A lone value for a repeated-key array stays a string, comma, -// space and pipe joined values stay joined, and a deepObject key stays -// bracketed, so each of those fails validation. A caller sending them -// decodes the query before this middleware or replaces it, reading the -// style and explode from the query spec each operation exports below. -function validate(target: TTarget, schema: TSchema) { - - return sValidator(target, schema, (result) => { - if (!result.success) { - throw PublicValidationError.fromIssues(result.error); - } - }); - -} - -export const uploadData = [ - validate("param", uploadDataCommandParamsSchema), - ] as const; diff --git a/__tests__/__snapshots__/nullables/header-parameters/types.ts b/__tests__/__snapshots__/nullables/header-parameters/types.ts deleted file mode 100644 index fa46be2..0000000 --- a/__tests__/__snapshots__/nullables/header-parameters/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { UndefinedOnPartialDeep } from "type-fest"; - -export type UploadStatus = "pending" | "complete"; -export type UploadDataCommandHeader = { - "content-type": "application/json" | "text/csv" | "application/xml"; - "content-length": `${bigint}`; - "x-idempotency-key"?: string; - }; -export type UploadDataCommandParams = { - uploadId: string; - }; -export type UploadDataCommandInput = UploadDataCommandParams; -export type InputUploadDataCommandResponse = UndefinedOnPartialDeep; diff --git a/__tests__/__snapshots__/nullables/header-parameters/valibot.ts b/__tests__/__snapshots__/nullables/header-parameters/valibot.ts deleted file mode 100644 index 8a73cd0..0000000 --- a/__tests__/__snapshots__/nullables/header-parameters/valibot.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as v from "valibot"; -export const inputUploadStatusSchema = v.picklist(["pending", "complete"]); -export const uploadStatusSchema = inputUploadStatusSchema; -export const inputUploadDataCommandResponseSchema = inputUploadStatusSchema; -export const uploadDataCommandResponseSchema = uploadStatusSchema; -export const inputUploadDataCommandParamsSchema = v.strictObject({ - "uploadId": v.string() - }); -export const uploadDataCommandParamsSchema = v.strictObject({ - "uploadId": v.pipe(v.string(), v.trim()) - }); -export const inputUploadDataCommandHeaderSchema = v.object({ - "content-type": v.picklist(["application/json", "text/csv", "application/xml"]), - "content-length": v.bigint(), - "x-idempotency-key": v.optional(v.pipe(v.string(), v.uuid())) - }); -export const uploadDataCommandHeaderSchema = v.object({ - "content-type": v.picklist(["application/json", "text/csv", "application/xml"]), - "content-length": v.union([v.pipe(v.string(), v.decimal(), v.toBigint(), v.bigint()), v.pipe(v.number(), v.integer(), v.toBigint(), v.bigint()), v.bigint()]), - "x-idempotency-key": v.exactOptional(v.pipe(v.string(), v.uuid())) - }); diff --git a/__tests__/__snapshots__/nullables/input-only-mode-omits-wire-schemas/valibot.ts b/__tests__/__snapshots__/nullables/input-only-mode-omits-wire-schemas/valibot.ts deleted file mode 100644 index 92d4676..0000000 --- a/__tests__/__snapshots__/nullables/input-only-mode-omits-wire-schemas/valibot.ts +++ /dev/null @@ -1,3 +0,0 @@ -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)); diff --git a/__tests__/__snapshots__/nullables/nullables/types.ts b/__tests__/__snapshots__/nullables/nullables/types.ts deleted file mode 100644 index 1689904..0000000 --- a/__tests__/__snapshots__/nullables/nullables/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type MySchemaLolOrNullable = "lol" | "kek" | null; diff --git a/__tests__/__snapshots__/nullables/oneof-with-type-null-generates-v-null/valibot.ts b/__tests__/__snapshots__/nullables/oneof-with-type-null-generates-v-null/valibot.ts deleted file mode 100644 index a3deba3..0000000 --- a/__tests__/__snapshots__/nullables/oneof-with-type-null-generates-v-null/valibot.ts +++ /dev/null @@ -1,3 +0,0 @@ -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()]); diff --git a/__tests__/__snapshots__/nullables/query-and-header-integer-params-coerce-strings-to-numbers/types.ts b/__tests__/__snapshots__/nullables/query-and-header-integer-params-coerce-strings-to-numbers/types.ts deleted file mode 100644 index 615a4a0..0000000 --- a/__tests__/__snapshots__/nullables/query-and-header-integer-params-coerce-strings-to-numbers/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { UndefinedOnPartialDeep } from "type-fest"; - -export type Dummy = string; -export type ExpireTime = bigint; -export type ListFilesCommandQuery = { - exp: `${bigint}`; - limit?: `${number}`; - }; -export type ListFilesCommandHeader = { - "x-rate-limit": `${number}`; - }; -export type ListFilesCommandInput = ListFilesCommandQuery; -export type InputListFilesCommandResponse = UndefinedOnPartialDeep; diff --git a/__tests__/__snapshots__/nullables/query-and-header-integer-params-coerce-strings-to-numbers/valibot.ts b/__tests__/__snapshots__/nullables/query-and-header-integer-params-coerce-strings-to-numbers/valibot.ts deleted file mode 100644 index 5a2fefd..0000000 --- a/__tests__/__snapshots__/nullables/query-and-header-integer-params-coerce-strings-to-numbers/valibot.ts +++ /dev/null @@ -1,21 +0,0 @@ -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)); -export const expireTimeSchema = v.union([v.pipe(v.string(), v.decimal(), v.toBigint(), v.pipe(v.bigint(), v.minValue(0n))), v.pipe(v.number(), v.integer(), v.toBigint(), v.pipe(v.bigint(), v.minValue(0n))), v.pipe(v.bigint(), v.minValue(0n))]); -export const inputListFilesCommandResponseSchema = inputDummySchema; -export const listFilesCommandResponseSchema = dummySchema; -export const inputListFilesCommandQuerySchema = v.strictObject({ - "exp": v.pipe(v.bigint(), v.minValue(0n)), - "limit": v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100))) - }); -export const listFilesCommandQuerySchema = v.strictObject({ - "exp": v.union([v.pipe(v.string(), v.decimal(), v.toBigint(), v.pipe(v.bigint(), v.minValue(0n))), v.pipe(v.number(), v.integer(), v.toBigint(), v.pipe(v.bigint(), v.minValue(0n))), v.pipe(v.bigint(), v.minValue(0n))]), - "limit": v.exactOptional(v.union([v.pipe(v.string(), v.decimal(), v.toNumber(), v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100))), v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100))])) - }); -export const inputListFilesCommandHeaderSchema = v.object({ - "x-rate-limit": v.pipe(v.number(), v.integer(), v.minValue(0)) - }); -export const listFilesCommandHeaderSchema = v.object({ - "x-rate-limit": v.union([v.pipe(v.string(), v.decimal(), v.toNumber(), v.pipe(v.number(), v.integer(), v.minValue(0))), v.pipe(v.number(), v.integer(), v.minValue(0))]) - }); diff --git a/__tests__/__snapshots__/nullables/rfc-3339-temporal-formats/types.ts b/__tests__/__snapshots__/nullables/rfc-3339-temporal-formats/types.ts deleted file mode 100644 index 5d23cac..0000000 --- a/__tests__/__snapshots__/nullables/rfc-3339-temporal-formats/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type MyDate = `${number}-${number}-${number}`; -export type MyTime = `${number}:${number}:${number}${string}`; -export type MyDateTime = `${number}-${number}-${number}T${number}:${number}:${number}${string}`; -export type MyDuration = `P${string}`; diff --git a/__tests__/__snapshots__/nullables/rfc-3339-temporal-formats/valibot.ts b/__tests__/__snapshots__/nullables/rfc-3339-temporal-formats/valibot.ts deleted file mode 100644 index a91445d..0000000 --- a/__tests__/__snapshots__/nullables/rfc-3339-temporal-formats/valibot.ts +++ /dev/null @@ -1,9 +0,0 @@ -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)); -export const myTimeSchema = v.pipe(v.string(), v.trim(), 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)); -export const inputMyDateTimeSchema = v.pipe(v.string(), v.regex(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt ]([01]\d|2[0-3]):[0-5]\d:([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)$/u, "date-time"), v.custom<`${number}-${number}-${number}T${number}:${number}:${number}${string}`>(() => true)); -export const myDateTimeSchema = v.pipe(v.string(), v.trim(), v.regex(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt ]([01]\d|2[0-3]):[0-5]\d:([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)$/u, "date-time"), v.custom<`${number}-${number}-${number}T${number}:${number}:${number}${string}`>(() => true)); -export const inputMyDurationSchema = v.pipe(v.string(), v.regex(/^P(?!$)((\d+Y)?(\d+M)?(\d+W)?(\d+D)?)(T(?=\d)(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/u, "duration"), v.custom<`P${string}`>(() => true)); -export const myDurationSchema = v.pipe(v.string(), v.trim(), v.regex(/^P(?!$)((\d+Y)?(\d+M)?(\d+W)?(\d+D)?)(T(?=\d)(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/u, "duration"), v.custom<`P${string}`>(() => true)); diff --git a/__tests__/__snapshots__/nullables/top-level-type-array-with-null/enums.ts b/__tests__/__snapshots__/nullables/top-level-type-array-with-null/enums.ts deleted file mode 100644 index a417b46..0000000 --- a/__tests__/__snapshots__/nullables/top-level-type-array-with-null/enums.ts +++ /dev/null @@ -1 +0,0 @@ -export const nullableStringEnum = ["active", "inactive"] as const; diff --git a/__tests__/__snapshots__/nullables/top-level-type-array-with-null/types.ts b/__tests__/__snapshots__/nullables/top-level-type-array-with-null/types.ts deleted file mode 100644 index b96214b..0000000 --- a/__tests__/__snapshots__/nullables/top-level-type-array-with-null/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type NullableString = string | null; -export type NullableStringEnum = "active" | "inactive" | null; -export type NullableInteger = number | null; -export type MultiType = string | number; diff --git a/__tests__/__snapshots__/nullables/top-level-type-array-with-null/valibot.ts b/__tests__/__snapshots__/nullables/top-level-type-array-with-null/valibot.ts deleted file mode 100644 index 44fe2df..0000000 --- a/__tests__/__snapshots__/nullables/top-level-type-array-with-null/valibot.ts +++ /dev/null @@ -1,9 +0,0 @@ -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"])); -export const nullableStringEnumSchema = inputNullableStringEnumSchema; -export const inputNullableIntegerSchema = v.nullable(v.pipe(v.number(), v.integer())); -export const nullableIntegerSchema = inputNullableIntegerSchema; -export const inputMultiTypeSchema = v.union([v.string(), v.number()]); -export const multiTypeSchema = inputMultiTypeSchema; diff --git a/__tests__/__snapshots__/typescript-hint.test.ts.snap b/__tests__/__snapshots__/typescript-hint.test.ts.snap new file mode 100644 index 0000000..05cb864 --- /dev/null +++ b/__tests__/__snapshots__/typescript-hint.test.ts.snap @@ -0,0 +1,16 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`x-typescript-hint honored inside anyOf branches > types.ts 1`] = ` +"export type MaybeUrl = AbsoluteUrl | RelativeUrl; +" +`; + +exports[`x-typescript-hint honored inside oneOf branches > types.ts 1`] = ` +"export type EventSource = "native" | EmbedUrl | SyndicatedUrl; +" +`; + +exports[`x-typescript-hint on top-level string schema > types.ts 1`] = ` +"export type EmbedUrl = \`https://embed.example.com/\${string}\`; +" +`; diff --git a/__tests__/__snapshots__/typescript-hint/x-typescript-hint-honored-inside-anyof-branches/types.ts b/__tests__/__snapshots__/typescript-hint/x-typescript-hint-honored-inside-anyof-branches/types.ts deleted file mode 100644 index b1ee74b..0000000 --- a/__tests__/__snapshots__/typescript-hint/x-typescript-hint-honored-inside-anyof-branches/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type MaybeUrl = AbsoluteUrl | RelativeUrl; diff --git a/__tests__/__snapshots__/typescript-hint/x-typescript-hint-honored-inside-oneof-branches/types.ts b/__tests__/__snapshots__/typescript-hint/x-typescript-hint-honored-inside-oneof-branches/types.ts deleted file mode 100644 index 194ccc0..0000000 --- a/__tests__/__snapshots__/typescript-hint/x-typescript-hint-honored-inside-oneof-branches/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type EventSource = "native" | EmbedUrl | SyndicatedUrl; diff --git a/__tests__/__snapshots__/typescript-hint/x-typescript-hint-on-top-level-string-schema/types.ts b/__tests__/__snapshots__/typescript-hint/x-typescript-hint-on-top-level-string-schema/types.ts deleted file mode 100644 index a00244e..0000000 --- a/__tests__/__snapshots__/typescript-hint/x-typescript-hint-on-top-level-string-schema/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type EmbedUrl = `https://embed.example.com/${string}`; diff --git a/__tests__/codegen-regressions.test.ts b/__tests__/codegen-regressions.test.ts index cc74217..5909c20 100644 --- a/__tests__/codegen-regressions.test.ts +++ b/__tests__/codegen-regressions.test.ts @@ -36,7 +36,7 @@ test("main.ts emits file-level `import type` for type-only imports", async () => const result = await processOpenApiDocument("/tmp/whatever", schema); - await expectGenerated([result.mainFile]); + expectGenerated([result.mainFile]); }); test("optional query params do not carry `| undefined` in their property type", async () => { @@ -69,7 +69,7 @@ test("optional query params do not carry `| undefined` in their property type", const result = await processOpenApiDocument("/tmp/whatever", schema); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); test("AllInputs union carries every command that takes an input", async () => { @@ -121,7 +121,7 @@ test("AllInputs union carries every command that takes an input", async () => { const result = await processOpenApiDocument("/tmp/whatever", schema); - await expectGenerated([result.mainFile]); + expectGenerated([result.mainFile]); }); // An empty schema permits any value, so the keys outside `properties` are @@ -143,7 +143,7 @@ test.for([ }), ); - await expectGenerated([result.valibotFile]); + expectGenerated([result.valibotFile]); }, ); @@ -161,7 +161,7 @@ test.for<[string, oas31.SchemaObject]>([ docWithSchema("Only", schema), ); - await expectGenerated([result.valibotFile]); + expectGenerated([result.valibotFile]); }, ); @@ -199,7 +199,7 @@ test("additionalProperties is a string-keyed record of the value type", async () }), ); - await expectGenerated([result.typesFile, result.valibotFile]); + expectGenerated([result.typesFile, result.valibotFile]); }); test("additionalProperties alongside properties keeps the extra keys valid", async () => { @@ -212,7 +212,7 @@ test("additionalProperties alongside properties keeps the extra keys valid", asy }), ); - await expectGenerated([result.valibotFile]); + expectGenerated([result.valibotFile]); }); test("additionalProperties true accepts any key", async () => { @@ -225,7 +225,7 @@ test("additionalProperties true accepts any key", async () => { }), ); - await expectGenerated([result.valibotFile]); + expectGenerated([result.valibotFile]); }); test("an empty properties bag is a record, not an empty object type", async () => { @@ -234,7 +234,7 @@ test("an empty properties bag is a record, not an empty object type", async () = docWithSchema("Empty", { type: "object", properties: {} }), ); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); test("the generated JSON body type is PascalCase", async () => { @@ -263,7 +263,7 @@ test("the generated JSON body type is PascalCase", async () => { const result = await processOpenApiDocument("/tmp/whatever", schema); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); test("an operation with both a 200 and a 204 emits one output type argument", async () => { @@ -295,7 +295,7 @@ test("an operation with both a 200 and a 204 emits one output type argument", as const result = await processOpenApiDocument("/tmp/whatever", schema); - await expectGenerated([result.commandsFile]); + expectGenerated([result.commandsFile]); }); test("an array request body with parameters stays readable as both", async () => { @@ -329,7 +329,7 @@ test("an array request body with parameters stays readable as both", async () => const result = await processOpenApiDocument("/tmp/whatever", schema); - await expectGenerated([result.typesFile, result.commandsFile]); + expectGenerated([result.typesFile, result.commandsFile]); }); test("nested query param members get the same stringish treatment as top-level ones", async () => { @@ -376,7 +376,7 @@ test("nested query param members get the same stringish treatment as top-level o const result = await processOpenApiDocument("/tmp/whatever", schema); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); test("json request body members keep their real JSON types, nested included", async () => { @@ -416,7 +416,7 @@ test("json request body members keep their real JSON types, nested included", as const result = await processOpenApiDocument("/tmp/whatever", schema); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); // Composition has to forward the codegen options the same way the array and @@ -468,7 +468,7 @@ test("a oneOf query param keeps the stringish wire types in every branch", async const result = await processOpenApiDocument("/tmp/whatever", schema); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); test("an object schema that omits `type` still honours `required`", async () => { @@ -486,7 +486,7 @@ test("an object schema that omits `type` still honours `required`", async () => }, }); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); test("a $ref nested past the top level registers after its target", async () => { @@ -522,7 +522,7 @@ test("a $ref nested past the top level registers after its target", async () => }, }); - await expectGenerated([result.typesFile, result.valibotFile]); + expectGenerated([result.typesFile, result.valibotFile]); }); test("a value the schema leaves open is a JsonValue on both sides", async () => { @@ -546,5 +546,5 @@ test("a value the schema leaves open is a JsonValue on both sides", async () => }, }); - await expectGenerated([result.typesFile, result.valibotFile]); + expectGenerated([result.typesFile, result.valibotFile]); }); diff --git a/__tests__/generated-snapshot.ts b/__tests__/generated-snapshot.ts index 2d7d997..c97c0f9 100644 --- a/__tests__/generated-snapshot.ts +++ b/__tests__/generated-snapshot.ts @@ -1,28 +1,13 @@ -import path from "node:path"; import { expect } from "vitest"; type GeneratedFile = { getBaseName(): string; getText(): string }; -function slug(text: string) { - return text - .toLowerCase() - .replaceAll(/[^a-z0-9]+/g, "-") - .replaceAll(/^-|-$/g, ""); -} - /** - * Snapshots each file into a folder named for the current test, so a generator - * change reviews as a diff of the code it emits + * Snapshots each file whole, keyed by its name, so a generator change reviews + * as a diff of the code it emits */ -export async function expectGenerated(files: GeneratedFile[]) { - const { currentTestName, testPath } = expect.getState(); - const folder = `${path.basename(testPath ?? "unknown", ".test.ts")}/${slug(currentTestName ?? "unknown")}`; - - await Promise.all( - files.map((file) => - expect(file.getText()).toMatchFileSnapshot( - `__snapshots__/${folder}/${file.getBaseName()}`, - ), - ), - ); +export function expectGenerated(files: GeneratedFile[]) { + for (const file of files) { + expect(file.getText()).toMatchSnapshot(file.getBaseName()); + } } diff --git a/__tests__/nullables.test.ts b/__tests__/nullables.test.ts index ad25b42..6137052 100644 --- a/__tests__/nullables.test.ts +++ b/__tests__/nullables.test.ts @@ -29,7 +29,7 @@ test("nullables", async () => { [], ); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); test("top-level type array with null", async () => { @@ -63,11 +63,7 @@ test("top-level type array with null", async () => { [], ); - await expectGenerated([ - result.typesFile, - result.valibotFile, - result.enumsFile, - ]); + expectGenerated([result.typesFile, result.valibotFile, result.enumsFile]); }); test("3.0 nullable objects, arrays, strings and combinator members admit null", async () => { @@ -125,7 +121,7 @@ test("3.0 nullable objects, arrays, strings and combinator members admit null", }, } as oas31.OpenAPIObject); - await expectGenerated([result.typesFile, result.valibotFile]); + expectGenerated([result.typesFile, result.valibotFile]); }); test("const values", async () => { @@ -161,7 +157,7 @@ test("const values", async () => { [], ); - await expectGenerated([result.typesFile, result.valibotFile]); + expectGenerated([result.typesFile, result.valibotFile]); }); test("RFC 3339 temporal formats", async () => { @@ -179,7 +175,7 @@ test("RFC 3339 temporal formats", async () => { }, }); - await expectGenerated([result.typesFile, result.valibotFile]); + expectGenerated([result.typesFile, result.valibotFile]); }); test("enums short-circuit type constraints (picklist only)", async () => { @@ -207,7 +203,7 @@ test("enums short-circuit type constraints (picklist only)", async () => { }, }); - await expectGenerated([result.valibotFile]); + expectGenerated([result.valibotFile]); }); test("oneOf with type null generates v.null()", async () => { @@ -224,7 +220,7 @@ test("oneOf with type null generates v.null()", async () => { }, }); - await expectGenerated([result.valibotFile]); + expectGenerated([result.valibotFile]); }); test("query and header integer params coerce strings to numbers", async () => { @@ -299,7 +295,7 @@ test("query and header integer params coerce strings to numbers", async () => { schema, ); - await expectGenerated([result.typesFile, result.valibotFile]); + expectGenerated([result.typesFile, result.valibotFile]); }); test("header parameters", async () => { @@ -378,7 +374,7 @@ test("header parameters", async () => { schema, ); - await expectGenerated([ + expectGenerated([ result.typesFile, result.commandsFile, result.commandsValidatedFile, @@ -436,5 +432,5 @@ test("input-only mode omits wire schemas", async () => { { inputOnly: true }, ); - await expectGenerated([result.valibotFile]); + expectGenerated([result.valibotFile]); }); diff --git a/__tests__/typescript-hint.test.ts b/__tests__/typescript-hint.test.ts index 9033bc6..3de8029 100644 --- a/__tests__/typescript-hint.test.ts +++ b/__tests__/typescript-hint.test.ts @@ -27,7 +27,7 @@ test("x-typescript-hint on top-level string schema", async () => { schema, ); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); test("x-typescript-hint honored inside oneOf branches", async () => { @@ -51,7 +51,7 @@ test("x-typescript-hint honored inside oneOf branches", async () => { schema, ); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); test("x-typescript-hint honored inside anyOf branches", async () => { @@ -74,5 +74,5 @@ test("x-typescript-hint honored inside anyOf branches", async () => { schema, ); - await expectGenerated([result.typesFile]); + expectGenerated([result.typesFile]); }); diff --git a/oxfmt.config.ts b/oxfmt.config.ts index 2220acc..32ad454 100644 --- a/oxfmt.config.ts +++ b/oxfmt.config.ts @@ -1,6 +1,3 @@ import { defineConfig } from "@block65/shared-config/oxfmt"; -export default defineConfig({ - // generated code under test, compared byte for byte - ignorePatterns: ["__tests__/__snapshots__"], -}); +export default defineConfig({}); diff --git a/oxlint.config.ts b/oxlint.config.ts index a2cc905..dea004e 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -1,9 +1,6 @@ import { defineConfig } from "@block65/shared-config/oxlint"; export default defineConfig({ - // generated code under test, compared byte for byte - ignorePatterns: ["__tests__/__snapshots__/**"], - // a consumer's config enables the valibot group, so the fixtures lint under it groups: { vitest: "on", valibot: "on" }, diff --git a/tsconfig.json b/tsconfig.json index f3931db..464f5fb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,8 +15,5 @@ "oxfmt.config.ts", "oxlint.config.ts", "vitest.config.ts" - ], - - // generator output compared byte for byte, without its imports' modules - "exclude": ["__tests__/__snapshots__"] + ] } From 87ad579047bd834477d6e60fa318a9f04a27b63d Mon Sep 17 00:00:00 2001 From: "maxholman[bot]" <321308195+maxholman[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 16:13:19 +0800 Subject: [PATCH 2/2] test: call toMatchSnapshot directly instead of expectGenerated The helper had become a loop around one matcher call. Each test now snapshots its generated files itself, with the same hints, so the .snap keys don't change. Co-Authored-By: LLM --- __tests__/codegen-regressions.test.ts | 43 ++++++++++++++------------- __tests__/generated-snapshot.ts | 13 -------- __tests__/nullables.test.ts | 41 ++++++++++++++----------- __tests__/typescript-hint.test.ts | 9 +++--- 4 files changed, 50 insertions(+), 56 deletions(-) delete mode 100644 __tests__/generated-snapshot.ts diff --git a/__tests__/codegen-regressions.test.ts b/__tests__/codegen-regressions.test.ts index 5909c20..d659e92 100644 --- a/__tests__/codegen-regressions.test.ts +++ b/__tests__/codegen-regressions.test.ts @@ -1,7 +1,6 @@ import type { oas31 } from "openapi3-ts"; -import { test } from "vitest"; +import { expect, test } from "vitest"; import { processOpenApiDocument } from "../lib/process-document.ts"; -import { expectGenerated } from "./generated-snapshot.ts"; const respOk = { "200": { @@ -36,7 +35,7 @@ test("main.ts emits file-level `import type` for type-only imports", async () => const result = await processOpenApiDocument("/tmp/whatever", schema); - expectGenerated([result.mainFile]); + expect(result.mainFile.getText()).toMatchSnapshot("main.ts"); }); test("optional query params do not carry `| undefined` in their property type", async () => { @@ -69,7 +68,7 @@ test("optional query params do not carry `| undefined` in their property type", const result = await processOpenApiDocument("/tmp/whatever", schema); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); }); test("AllInputs union carries every command that takes an input", async () => { @@ -121,7 +120,7 @@ test("AllInputs union carries every command that takes an input", async () => { const result = await processOpenApiDocument("/tmp/whatever", schema); - expectGenerated([result.mainFile]); + expect(result.mainFile.getText()).toMatchSnapshot("main.ts"); }); // An empty schema permits any value, so the keys outside `properties` are @@ -143,7 +142,7 @@ test.for([ }), ); - expectGenerated([result.valibotFile]); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }, ); @@ -161,7 +160,7 @@ test.for<[string, oas31.SchemaObject]>([ docWithSchema("Only", schema), ); - expectGenerated([result.valibotFile]); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }, ); @@ -199,7 +198,8 @@ test("additionalProperties is a string-keyed record of the value type", async () }), ); - expectGenerated([result.typesFile, result.valibotFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); test("additionalProperties alongside properties keeps the extra keys valid", async () => { @@ -212,7 +212,7 @@ test("additionalProperties alongside properties keeps the extra keys valid", asy }), ); - expectGenerated([result.valibotFile]); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); test("additionalProperties true accepts any key", async () => { @@ -225,7 +225,7 @@ test("additionalProperties true accepts any key", async () => { }), ); - expectGenerated([result.valibotFile]); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); test("an empty properties bag is a record, not an empty object type", async () => { @@ -234,7 +234,7 @@ test("an empty properties bag is a record, not an empty object type", async () = docWithSchema("Empty", { type: "object", properties: {} }), ); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); }); test("the generated JSON body type is PascalCase", async () => { @@ -263,7 +263,7 @@ test("the generated JSON body type is PascalCase", async () => { const result = await processOpenApiDocument("/tmp/whatever", schema); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); }); test("an operation with both a 200 and a 204 emits one output type argument", async () => { @@ -295,7 +295,7 @@ test("an operation with both a 200 and a 204 emits one output type argument", as const result = await processOpenApiDocument("/tmp/whatever", schema); - expectGenerated([result.commandsFile]); + expect(result.commandsFile.getText()).toMatchSnapshot("commands.ts"); }); test("an array request body with parameters stays readable as both", async () => { @@ -329,7 +329,8 @@ test("an array request body with parameters stays readable as both", async () => const result = await processOpenApiDocument("/tmp/whatever", schema); - expectGenerated([result.typesFile, result.commandsFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); + expect(result.commandsFile.getText()).toMatchSnapshot("commands.ts"); }); test("nested query param members get the same stringish treatment as top-level ones", async () => { @@ -376,7 +377,7 @@ test("nested query param members get the same stringish treatment as top-level o const result = await processOpenApiDocument("/tmp/whatever", schema); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); }); test("json request body members keep their real JSON types, nested included", async () => { @@ -416,7 +417,7 @@ test("json request body members keep their real JSON types, nested included", as const result = await processOpenApiDocument("/tmp/whatever", schema); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); }); // Composition has to forward the codegen options the same way the array and @@ -468,7 +469,7 @@ test("a oneOf query param keeps the stringish wire types in every branch", async const result = await processOpenApiDocument("/tmp/whatever", schema); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); }); test("an object schema that omits `type` still honours `required`", async () => { @@ -486,7 +487,7 @@ test("an object schema that omits `type` still honours `required`", async () => }, }); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); }); test("a $ref nested past the top level registers after its target", async () => { @@ -522,7 +523,8 @@ test("a $ref nested past the top level registers after its target", async () => }, }); - expectGenerated([result.typesFile, result.valibotFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); test("a value the schema leaves open is a JsonValue on both sides", async () => { @@ -546,5 +548,6 @@ test("a value the schema leaves open is a JsonValue on both sides", async () => }, }); - expectGenerated([result.typesFile, result.valibotFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); diff --git a/__tests__/generated-snapshot.ts b/__tests__/generated-snapshot.ts deleted file mode 100644 index c97c0f9..0000000 --- a/__tests__/generated-snapshot.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { expect } from "vitest"; - -type GeneratedFile = { getBaseName(): string; getText(): string }; - -/** - * Snapshots each file whole, keyed by its name, so a generator change reviews - * as a diff of the code it emits - */ -export function expectGenerated(files: GeneratedFile[]) { - for (const file of files) { - expect(file.getText()).toMatchSnapshot(file.getBaseName()); - } -} diff --git a/__tests__/nullables.test.ts b/__tests__/nullables.test.ts index 6137052..6873a5b 100644 --- a/__tests__/nullables.test.ts +++ b/__tests__/nullables.test.ts @@ -1,7 +1,6 @@ import type { oas31 } from "openapi3-ts"; -import { test } from "vitest"; +import { expect, test } from "vitest"; import { processOpenApiDocument } from "../lib/process-document.ts"; -import { expectGenerated } from "./generated-snapshot.ts"; test("nullables", async () => { const result = await processOpenApiDocument( @@ -29,7 +28,7 @@ test("nullables", async () => { [], ); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); }); test("top-level type array with null", async () => { @@ -63,7 +62,9 @@ test("top-level type array with null", async () => { [], ); - expectGenerated([result.typesFile, result.valibotFile, result.enumsFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); + expect(result.enumsFile.getText()).toMatchSnapshot("enums.ts"); }); test("3.0 nullable objects, arrays, strings and combinator members admit null", async () => { @@ -121,7 +122,8 @@ test("3.0 nullable objects, arrays, strings and combinator members admit null", }, } as oas31.OpenAPIObject); - expectGenerated([result.typesFile, result.valibotFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); test("const values", async () => { @@ -157,7 +159,8 @@ test("const values", async () => { [], ); - expectGenerated([result.typesFile, result.valibotFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); test("RFC 3339 temporal formats", async () => { @@ -175,7 +178,8 @@ test("RFC 3339 temporal formats", async () => { }, }); - expectGenerated([result.typesFile, result.valibotFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); test("enums short-circuit type constraints (picklist only)", async () => { @@ -203,7 +207,7 @@ test("enums short-circuit type constraints (picklist only)", async () => { }, }); - expectGenerated([result.valibotFile]); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); test("oneOf with type null generates v.null()", async () => { @@ -220,7 +224,7 @@ test("oneOf with type null generates v.null()", async () => { }, }); - expectGenerated([result.valibotFile]); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); test("query and header integer params coerce strings to numbers", async () => { @@ -295,7 +299,8 @@ test("query and header integer params coerce strings to numbers", async () => { schema, ); - expectGenerated([result.typesFile, result.valibotFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); test("header parameters", async () => { @@ -374,13 +379,13 @@ test("header parameters", async () => { schema, ); - expectGenerated([ - result.typesFile, - result.commandsFile, - result.commandsValidatedFile, - result.valibotFile, - result.honoFile, - ]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); + expect(result.commandsFile.getText()).toMatchSnapshot("commands.ts"); + expect(result.commandsValidatedFile.getText()).toMatchSnapshot( + "commands-validated.ts", + ); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); + expect(result.honoFile.getText()).toMatchSnapshot("hono.ts"); }); test("input-only mode omits wire schemas", async () => { @@ -432,5 +437,5 @@ test("input-only mode omits wire schemas", async () => { { inputOnly: true }, ); - expectGenerated([result.valibotFile]); + expect(result.valibotFile.getText()).toMatchSnapshot("valibot.ts"); }); diff --git a/__tests__/typescript-hint.test.ts b/__tests__/typescript-hint.test.ts index 3de8029..986e9f4 100644 --- a/__tests__/typescript-hint.test.ts +++ b/__tests__/typescript-hint.test.ts @@ -1,7 +1,6 @@ import type { oas31 } from "openapi3-ts"; -import { test } from "vitest"; +import { expect, test } from "vitest"; import { processOpenApiDocument } from "../lib/process-document.ts"; -import { expectGenerated } from "./generated-snapshot.ts"; const baseDoc = { openapi: "3.1.0" as const, @@ -27,7 +26,7 @@ test("x-typescript-hint on top-level string schema", async () => { schema, ); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); }); test("x-typescript-hint honored inside oneOf branches", async () => { @@ -51,7 +50,7 @@ test("x-typescript-hint honored inside oneOf branches", async () => { schema, ); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); }); test("x-typescript-hint honored inside anyOf branches", async () => { @@ -74,5 +73,5 @@ test("x-typescript-hint honored inside anyOf branches", async () => { schema, ); - expectGenerated([result.typesFile]); + expect(result.typesFile.getText()).toMatchSnapshot("types.ts"); });