From ba58d5cbf3f483bbc232f811bd639470e40b32d9 Mon Sep 17 00:00:00 2001 From: "maxholman[bot]" <321308195+maxholman[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:12:47 +0800 Subject: [PATCH 1/2] refactor: name functions by what they do A preposition suffix (`jsonBodyTypeOf`, `separatorIn`, `warningsFrom`) names a relation rather than the work, so each becomes a verb phrase: resolveJsonBodyType, buildTypesModuleSpecifier, resolveTypeName, extractSchemaName, openChunk, readStatementSeparator, and in the tests buildIntoTempDir, collectWarnings, readRequestedUrl. The chunk map becomes chunksByFile. Co-Authored-By: LLM --- .../docker/.openapi-codegen-manifest.json | 2 +- .../openai/.openapi-codegen-manifest.json | 2 +- .../petstore/.openapi-codegen-manifest.json | 2 +- .../test1/.openapi-codegen-manifest.json | 2 +- __tests__/lint-directives.test.ts | 15 ++++--- __tests__/query-roundtrip.test.ts | 6 +-- __tests__/query-serializer.test.ts | 6 +-- lib/chunks.ts | 16 +++---- lib/process-document.ts | 44 +++++++++---------- lib/process-schema.ts | 20 ++++----- lib/refs.ts | 2 +- lib/valibot.ts | 12 ++--- 12 files changed, 66 insertions(+), 63 deletions(-) diff --git a/__tests__/fixtures/docker/.openapi-codegen-manifest.json b/__tests__/fixtures/docker/.openapi-codegen-manifest.json index 5173c6d..82c3e30 100644 --- a/__tests__/fixtures/docker/.openapi-codegen-manifest.json +++ b/__tests__/fixtures/docker/.openapi-codegen-manifest.json @@ -1,5 +1,5 @@ { - "#generator": "06eb706c73e16a15cba65397366f0e38", + "#generator": "8e34237f9aac0959082f1a4be6dfe7dd", "commands.ts": "593afb99bb4daed5e9491b2a55d75d75", "types.ts": "f1e7d6c61bb9e15c5034a3cc3cc729b5", "main.ts": "38065305823906f3aa1c7f968e278002", diff --git a/__tests__/fixtures/openai/.openapi-codegen-manifest.json b/__tests__/fixtures/openai/.openapi-codegen-manifest.json index 44aabd1..7cfb4e6 100644 --- a/__tests__/fixtures/openai/.openapi-codegen-manifest.json +++ b/__tests__/fixtures/openai/.openapi-codegen-manifest.json @@ -1,5 +1,5 @@ { - "#generator": "06eb706c73e16a15cba65397366f0e38", + "#generator": "8e34237f9aac0959082f1a4be6dfe7dd", "commands.ts": "99ac148005b23e09a53adb6b62620dc0", "types.ts": "bc453b1a7fcd330909a23ae01be97f52", "main.ts": "8147604a37254400a015b78650466094", diff --git a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json index e42634b..a3412d7 100644 --- a/__tests__/fixtures/petstore/.openapi-codegen-manifest.json +++ b/__tests__/fixtures/petstore/.openapi-codegen-manifest.json @@ -1,5 +1,5 @@ { - "#generator": "06eb706c73e16a15cba65397366f0e38", + "#generator": "8e34237f9aac0959082f1a4be6dfe7dd", "commands.ts": "69c6a9f2924568fc08fd489508c9df34", "types.ts": "ea65c3e67352d4e22b97af80085727b4", "main.ts": "64edb526dcbcbd345e631ccff959f11d", diff --git a/__tests__/fixtures/test1/.openapi-codegen-manifest.json b/__tests__/fixtures/test1/.openapi-codegen-manifest.json index 66a0da5..8e06187 100644 --- a/__tests__/fixtures/test1/.openapi-codegen-manifest.json +++ b/__tests__/fixtures/test1/.openapi-codegen-manifest.json @@ -1,5 +1,5 @@ { - "#generator": "06eb706c73e16a15cba65397366f0e38", + "#generator": "8e34237f9aac0959082f1a4be6dfe7dd", "commands.ts": "eab0f690af8f1e2d973f6504de470ba9", "types.ts": "7bb3f162d3321e4db51673d3f3ba35ac", "main.ts": "5c768be2e48d0b06d7ed8da665653404", diff --git a/__tests__/lint-directives.test.ts b/__tests__/lint-directives.test.ts index c0b23ed..f450760 100644 --- a/__tests__/lint-directives.test.ts +++ b/__tests__/lint-directives.test.ts @@ -51,7 +51,7 @@ const withHeader = { }, }; -async function buildIn(parent: string, source: object = document) { +async function buildIntoTempDir(parent: string, source: object = document) { const dir = await mkdtemp(path.join(parent, "lint-directives-")); const input = path.join(dir, "document.json"); @@ -67,7 +67,7 @@ async function buildIn(parent: string, source: object = document) { // Output inside this repository finds its oxlint test("a file names the exempt rules that fire in it", async () => { - const { dir, valibot, main } = await buildIn(import.meta.dirname); + const { dir, valibot, main } = await buildIntoTempDir(import.meta.dirname); await rm(dir, { recursive: true }); @@ -80,7 +80,7 @@ test("a file names the exempt rules that fire in it", async () => { // An open object can let a peer's unnamed keys through, so its error reaches // the consumer test("an open object schema stays a lint error", async () => { - const { dir, valibot } = await buildIn(import.meta.dirname); + const { dir, valibot } = await buildIntoTempDir(import.meta.dirname); await rm(dir, { recursive: true }); @@ -124,7 +124,10 @@ async function strictObjectErrors(dir: string) { // Stripping undeclared headers is the point of a header schema, so only the // open body object reaches the consumer's lint test("a header schema is bracketed and an open body object still errors", async () => { - const { dir, valibot } = await buildIn(import.meta.dirname, withHeader); + const { dir, valibot } = await buildIntoTempDir( + import.meta.dirname, + withHeader, + ); const errors = await strictObjectErrors(dir); await rm(dir, { recursive: true }); @@ -143,7 +146,7 @@ test("a header schema is bracketed and an open body object still errors", async // An upgrade from 12, or a changed lint config, leaves a file its manifest // records as current without the directives it now needs test("a regeneration writes directives the recorded files lack", async () => { - const { dir } = await buildIn(import.meta.dirname); + const { dir } = await buildIntoTempDir(import.meta.dirname); const manifestPath = path.join(dir, ".openapi-codegen-manifest.json"); const manifestText = await readFile(manifestPath, "utf8"); const manifest: unknown = JSON.parse(manifestText); @@ -201,7 +204,7 @@ test("no fixture directive names snake-case-wire-keys", async () => { }); test("output outside any oxlint project gets no directive", async () => { - const { dir, valibot } = await buildIn(tmpdir()); + const { dir, valibot } = await buildIntoTempDir(tmpdir()); await rm(dir, { recursive: true }); diff --git a/__tests__/query-roundtrip.test.ts b/__tests__/query-roundtrip.test.ts index 380b280..7d6cf7d 100644 --- a/__tests__/query-roundtrip.test.ts +++ b/__tests__/query-roundtrip.test.ts @@ -85,7 +85,7 @@ function createHonoApp(middleware: readonly MiddlewareHandler[]) { } // Collects what the generator says while it walks a document -async function warningsFrom(parameters: readonly oas32.ParameterObject[]) { +async function collectWarnings(parameters: readonly oas32.ParameterObject[]) { const warnings: string[] = []; const original = console.warn; console.warn = (message: string) => warnings.push(message); @@ -141,7 +141,7 @@ const rangeSchema: oas32.SchemaObject = { // worth saying out loud test("an object query parameter with no declared style is warned about", async () => { await expect( - warningsFrom([{ name: "at", in: "query", schema: rangeSchema }]), + collectWarnings([{ name: "at", in: "query", schema: rangeSchema }]), ).resolves.toContain( 'query parameter "at" is an object but declares no `style`', ); @@ -151,7 +151,7 @@ test("an object query parameter with no declared style is warned about", async ( // every encoding this generator could pick keeps them alike test("two default-style object parameters sharing a member name are warned about", async () => { await expect( - warningsFrom([ + collectWarnings([ { name: "created", in: "query", schema: rangeSchema }, { name: "updated", in: "query", schema: rangeSchema }, ]), diff --git a/__tests__/query-serializer.test.ts b/__tests__/query-serializer.test.ts index f2ec400..e114c7e 100644 --- a/__tests__/query-serializer.test.ts +++ b/__tests__/query-serializer.test.ts @@ -130,7 +130,7 @@ function fetchStub() { return vi.fn(async () => Response.json({})); } -function urlFrom(fetch: ReturnType) { +function readRequestedUrl(fetch: ReturnType) { expect(fetch).toHaveBeenCalledOnce(); const [url] = fetch.mock.calls[0] ?? []; @@ -149,7 +149,7 @@ async function findPetsUrl(sortQuery?: true) { await client.json(new FindPetsCommand({ tags: ["cat", "dog"], limit: "10" })); - return urlFrom(fetch); + return readRequestedUrl(fetch); } // ImageCreate declares seven query parameters and names formJoinSerializer @@ -172,7 +172,7 @@ async function imageCreateUrl(sortQuery?: true) { }), ); - return urlFrom(fetch); + return readRequestedUrl(fetch); } // A change in how the constructor assembles the query — the destructuring, diff --git a/lib/chunks.ts b/lib/chunks.ts index cfd97c5..b7fbbc4 100644 --- a/lib/chunks.ts +++ b/lib/chunks.ts @@ -3,7 +3,7 @@ import type { SourceFile } from "ts-morph"; // an insert re-parses its whole chunk, so a small chunk keeps inserts cheap const chunkSize = 10; -const chunksOf = new WeakMap(); +const chunksByFile = new WeakMap(); /** * ts-morph re-parses a whole file on every insert, so a module of thousands @@ -12,8 +12,8 @@ const chunksOf = new WeakMap(); * A statement declared while building the arguments of another can start a * new chunk and land after it, so declare it first */ -export function chunkOf(file: SourceFile) { - const chunks = chunksOf.get(file) ?? []; +export function openChunk(file: SourceFile) { + const chunks = chunksByFile.get(file) ?? []; const last = chunks.at(-1); if (last && last.getStatements().length < chunkSize) { @@ -27,13 +27,13 @@ export function chunkOf(file: SourceFile) { }); chunks.push(chunk); - chunksOf.set(file, chunks); + chunksByFile.set(file, chunks); return chunk; } // the gap ts-morph left between statements, blank only between classes -function separatorIn(chunk: SourceFile) { +function readStatementSeparator(chunk: SourceFile) { const [before, last] = chunk.getStatements().slice(-2); return before && last @@ -46,7 +46,7 @@ function separatorIn(chunk: SourceFile) { * before anything reads the module back */ export function joinChunks(file: SourceFile) { - const chunks = chunksOf.get(file) ?? []; + const chunks = chunksByFile.get(file) ?? []; if (chunks.length > 0) { const text = chunks @@ -54,7 +54,7 @@ export function joinChunks(file: SourceFile) { const next = chunks[index + 1]; const body = chunk.getFullText().trim(); - return next ? body + separatorIn(chunk) : body; + return next ? body + readStatementSeparator(chunk) : body; }) .join(""); @@ -70,5 +70,5 @@ export function joinChunks(file: SourceFile) { chunk.forget(); } - chunksOf.delete(file); + chunksByFile.delete(file); } diff --git a/lib/process-document.ts b/lib/process-document.ts index 355a27f..cf8a524 100644 --- a/lib/process-document.ts +++ b/lib/process-document.ts @@ -19,7 +19,7 @@ import { Writers, } from "ts-morph"; import type { Simplify } from "type-fest"; -import { chunkOf, joinChunks } from "./chunks.ts"; +import { openChunk, joinChunks } from "./chunks.ts"; import { addSchemaImportsToHonoFile, createHonoFile, @@ -410,7 +410,7 @@ type DocumentContext = OutputFiles & { inputOnly: boolean | undefined; }; -function typesModuleSpecifierOf(typesFile: SourceFile) { +function buildTypesModuleSpecifier(typesFile: SourceFile) { return `./${typesFile.getBaseNameWithoutExtension()}.js`; } @@ -470,7 +470,7 @@ function addModulePreambles({ commandsFile, typesFile }: OutputFiles) { isTypeOnly: true, }); - const typesModuleSpecifier = typesModuleSpecifierOf(typesFile); + const typesModuleSpecifier = buildTypesModuleSpecifier(typesFile); return ( commandsFile @@ -663,7 +663,7 @@ function declareCommandClass( "Command", ); - const commandClass = chunkOf(commandsFile).addClass({ + const commandClass = openChunk(commandsFile).addClass({ name: commandName, isExported: true, extends: "Command", @@ -948,7 +948,7 @@ function addQueryType( ) { const queryType = queryParameters.length > 0 - ? chunkOf(documentCtx.typesFile).addTypeAlias({ + ? openChunk(documentCtx.typesFile).addTypeAlias({ name: pascalCase(commandClass.getName() || "INVALID", "Query"), docs: deprecationDocs, isExported: true, @@ -979,7 +979,7 @@ function addHeaderType( ) { const headerType = headerParameters.length > 0 - ? chunkOf(documentCtx.typesFile).addTypeAlias({ + ? openChunk(documentCtx.typesFile).addTypeAlias({ name: pascalCase(commandClass.getName() || "INVALID", "Header"), docs: deprecationDocs, isExported: true, @@ -1003,7 +1003,7 @@ function addHeaderType( return headerType; } -function jsonBodyTypeOf( +function resolveJsonBodyType( documentCtx: DocumentContext, deprecationDocs: DeprecationDocs, operationId: string, @@ -1042,7 +1042,7 @@ function jsonBodyTypeOf( schema, ); - return chunkOf(documentCtx.typesFile).addTypeAlias({ + return openChunk(documentCtx.typesFile).addTypeAlias({ name, docs: deprecationDocs, type: typeof type.type === "function" ? type.type : String(type.type), @@ -1062,7 +1062,7 @@ function resolveBodyTypes( const content = resolveContent(documentCtx.refs, requestBodyObject?.content); const jsonRequestBodyObject = content["application/json"]; - const jsonBodyType = jsonBodyTypeOf( + const jsonBodyType = resolveJsonBodyType( documentCtx, deprecationDocs, operationObject.operationId, @@ -1082,10 +1082,10 @@ function resolveBodyTypes( const hasNonJsonBody = !jsonBodyType && nonJsonBodyEntries.length > 0; - // declared before the alias that unions them, as chunkOf requires + // declared before the alias that unions them, as openChunk requires const nonJsonBodyNames = hasNonJsonBody ? nonJsonBodyEntries.map(([contentType]) => - chunkOf(documentCtx.typesFile) + openChunk(documentCtx.typesFile) .addTypeAlias({ name: pascalCase( `${commandClass.getName() || "INVALID"} Body ${contentType}`, @@ -1097,7 +1097,7 @@ function resolveBodyTypes( : []; const nonJsonBodyType = hasNonJsonBody - ? chunkOf(documentCtx.typesFile).addTypeAlias({ + ? openChunk(documentCtx.typesFile).addTypeAlias({ docs: deprecationDocs, name: pascalCase(`${commandClass.getName() || "INVALID"} Body NonJson`), isExported: true, @@ -1121,7 +1121,7 @@ function addParamsType( pathParameters: oas30.ParameterObject[], ) { return pathParameters.length > 0 - ? chunkOf(documentCtx.typesFile).addTypeAlias({ + ? openChunk(documentCtx.typesFile).addTypeAlias({ name: pascalCase(`${commandClass.getName() || "INVALID"}Params`), docs: deprecationDocs, type: Writers.objectType({ @@ -1173,7 +1173,7 @@ function addInputType( ) { const bodyType = (jsonBodyType && - chunkOf(documentCtx.typesFile).addTypeAlias({ + openChunk(documentCtx.typesFile).addTypeAlias({ name: pascalCase(commandClass.getName() || "", "Body"), type: jsonBodyType.getName(), isExported: true, @@ -1196,7 +1196,7 @@ function addInputType( const wrappedJsonBodyType = wrapJsonBody && jsonBodyType - ? chunkOf(documentCtx.typesFile).addTypeAlias({ + ? openChunk(documentCtx.typesFile).addTypeAlias({ name: pascalCase(commandClass.getName() || "", "BodyWrapper"), type: Writers.objectType({ properties: [{ name: inputBodyName, type: jsonBodyType.getName() }], @@ -1212,7 +1212,7 @@ function addInputType( queryType?.getName(), ); - const inputType = chunkOf(documentCtx.typesFile).addTypeAlias({ + const inputType = openChunk(documentCtx.typesFile).addTypeAlias({ name: pascalCase(commandClass.getName() || "", "Input"), type: inputTypeNode, isExported: true, @@ -1399,7 +1399,7 @@ function addReferencedOutput( // The value is JSON.stringified, which drops `undefined`, so // optional fields may hold `undefined`. Mirrors the `input*` // prefix used for the lax variant in the valibot module - chunkOf(documentCtx.typesFile).addTypeAlias({ + openChunk(documentCtx.typesFile).addTypeAlias({ name: pascalCase("Input", commandClass.getName() || "INVALID", "Response"), type: `UndefinedOnPartialDeep<${outputTypeName}>`, isExported: true, @@ -1418,7 +1418,7 @@ function addInlineOutput( schema, ); - const responseTypeAlias = chunkOf(documentCtx.typesFile).addTypeAlias({ + const responseTypeAlias = openChunk(documentCtx.typesFile).addTypeAlias({ name: pascalCase(commandClass.getName() || "INVALID", "Output"), type: typeof outputType.type === "function" @@ -1432,7 +1432,7 @@ function addInlineOutput( commandClass.getExtends()?.addTypeArgument(responseTypeAlias.getName()); documentCtx.outputTypes.add(responseTypeAlias); - chunkOf(documentCtx.typesFile).addTypeAlias({ + openChunk(documentCtx.typesFile).addTypeAlias({ name: pascalCase("Input", commandClass.getName() || "INVALID", "Response"), type: `UndefinedOnPartialDeep<${responseTypeAlias.getName()}>`, isExported: true, @@ -1573,7 +1573,7 @@ function addSequentialOutput( schema, ); - const outputTypeAlias = chunkOf(documentCtx.typesFile).addTypeAlias({ + const outputTypeAlias = openChunk(documentCtx.typesFile).addTypeAlias({ name: pascalCase(commandClass.getName() || "INVALID", "Output"), type: outputType.type ?? unspecifiedKeyword, isExported: true, @@ -2174,7 +2174,7 @@ function emitClientModule( if (importNames.size > 0) { mainFile.addImportDeclaration({ - moduleSpecifier: typesModuleSpecifierOf(typesFile), + moduleSpecifier: buildTypesModuleSpecifier(typesFile), namedImports: [...importNames].toSorted(), isTypeOnly: true, }); @@ -2344,7 +2344,7 @@ export async function processOpenApiDocument( importLazyTypes( files.valibotFile, documentCtx.validators, - typesModuleSpecifierOf(files.typesFile), + buildTypesModuleSpecifier(files.typesFile), ); addJsonValueSchemaWhenUsed(files.valibotFile); files.valibotFile.fixUnusedIdentifiers(); diff --git a/lib/process-schema.ts b/lib/process-schema.ts index e1c08c5..63be389 100644 --- a/lib/process-schema.ts +++ b/lib/process-schema.ts @@ -11,8 +11,8 @@ import { type WriterFunction, Writers, } from "ts-morph"; -import { chunkOf } from "./chunks.ts"; -import { schemaNameOf, schemaRef } from "./refs.ts"; +import { openChunk } from "./chunks.ts"; +import { extractSchemaName, schemaRef } from "./refs.ts"; import { type SchemaNode, type SchemaObject, @@ -231,7 +231,7 @@ function refType( OptionalKind, "type" | "docs" > = { - type: typeNameOf(typesAndInterfaces, schemaObject.$ref), + type: resolveTypeName(typesAndInterfaces, schemaObject.$ref), ...(docs.length > 0 && { docs }), }; @@ -695,9 +695,9 @@ export function schemaToType( } // a recursive schema names an alias that is declared later -function typeNameOf(typesAndInterfaces: TypesAndInterfaces, ref: string) { +function resolveTypeName(typesAndInterfaces: TypesAndInterfaces, ref: string) { return ( - typesAndInterfaces.get(ref)?.getName() ?? pascalCase(schemaNameOf(ref)) + typesAndInterfaces.get(ref)?.getName() ?? pascalCase(extractSchemaName(ref)) ); } @@ -708,7 +708,7 @@ function registerAlias( type: string | WriterFunction, description?: string, ) { - const typeAlias = chunkOf(typesFile).addTypeAlias({ + const typeAlias = openChunk(typesFile).addTypeAlias({ name: pascalCase(schemaName), isExported: true, type, @@ -735,7 +735,7 @@ function combinatorAliasType( const typeAliases = schemaItems .filter((value) => isReferenceObject(value)) - .map((s) => typeNameOf(typesAndInterfaces, s.$ref)); + .map((s) => resolveTypeName(typesAndInterfaces, s.$ref)); const objectTypesFromNonRefSchemas = schemaItems .filter((value) => isSchemaObject(value)) @@ -830,7 +830,7 @@ export function registerTypesFromSchema( // deal with refs if ("$ref" in schemaObject) { - register(typeNameOf(typesAndInterfaces, schemaObject.$ref)); + register(resolveTypeName(typesAndInterfaces, schemaObject.$ref)); } // deal with unions and intersections @@ -881,7 +881,7 @@ export function registerTypesFromSchema( ] : []; - const stringUnion = chunkOf(typesFile).addTypeAlias({ + const stringUnion = openChunk(typesFile).addTypeAlias({ name: pascalCase(schemaName), isExported: true, type: maybeUnion(...schemaObject.enum.map((e) => JSON.stringify(e))), @@ -921,7 +921,7 @@ export function registerTypesFromSchema( isReferenceObject(schemaObject.items) ) { register( - `${typeNameOf(typesAndInterfaces, schemaObject.items.$ref)}[]`, + `${resolveTypeName(typesAndInterfaces, schemaObject.items.$ref)}[]`, schemaObject.description, ); } else { diff --git a/lib/refs.ts b/lib/refs.ts index 4b1ba1e..a38b68d 100644 --- a/lib/refs.ts +++ b/lib/refs.ts @@ -15,7 +15,7 @@ export function schemaRef(schemaName: string) { * Recovers a component's name from a whole-component $ref, once refs are * normalized */ -export function schemaNameOf(ref: string) { +export function extractSchemaName(ref: string) { return unescapeToken(ref.slice(componentSchemas.length)); } diff --git a/lib/valibot.ts b/lib/valibot.ts index beddae2..9bcbeb5 100644 --- a/lib/valibot.ts +++ b/lib/valibot.ts @@ -11,7 +11,7 @@ import { } from "ts-morph"; import type { Primitive } from "type-fest"; import type * as v from "valibot"; -import { chunkOf } from "./chunks.ts"; +import { openChunk } from "./chunks.ts"; import { schemaRef } from "./refs.ts"; import { type SchemaNode, @@ -983,7 +983,7 @@ export function registerValidatorFromSchema( : []; // Input schema — always emitted (TS-side, allows undefined, no wire coercion) - chunkOf(valibotFile).addVariableStatement({ + openChunk(valibotFile).addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, docs, @@ -1001,7 +1001,7 @@ export function registerValidatorFromSchema( // exactOptional, which are equivalent on JSON-parsed data if (!inputOnly) { if (shouldCoerceSchema(schemaObject)) { - chunkOf(valibotFile).addVariableStatement({ + openChunk(valibotFile).addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ @@ -1012,7 +1012,7 @@ export function registerValidatorFromSchema( ], }); } else { - chunkOf(valibotFile).addVariableStatement({ + openChunk(valibotFile).addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ @@ -1163,7 +1163,7 @@ function emitNamePair( const inputName = camelCase("input", commandName, segment, "schema"); const wireName = camelCase(commandName, segment, "schema"); - chunkOf(valibotFile).addVariableStatement({ + openChunk(valibotFile).addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ @@ -1175,7 +1175,7 @@ function emitNamePair( }); if (!inputOnly) { - chunkOf(valibotFile).addVariableStatement({ + openChunk(valibotFile).addVariableStatement({ isExported: true, declarationKind: VariableDeclarationKind.Const, declarations: [ From 36c09c0b3ec974d57a19e3decbd139853152692f Mon Sep 17 00:00:00 2001 From: "maxholman[bot]" <321308195+maxholman[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:36:23 +0800 Subject: [PATCH 2/2] test: serialize the generated query from the command The command holds its query and names its serializer, so the tests read them directly instead of sending it through a client to a stubbed fetch. sortQuery is rest-client's to test, so its test here is gone. Co-Authored-By: LLM --- .../query-serializer.test.ts.snap | 8 +-- __tests__/query-serializer.test.ts | 72 +++++-------------- 2 files changed, 18 insertions(+), 62 deletions(-) diff --git a/__tests__/__snapshots__/query-serializer.test.ts.snap b/__tests__/__snapshots__/query-serializer.test.ts.snap index 13bb741..d264ff1 100644 --- a/__tests__/__snapshots__/query-serializer.test.ts.snap +++ b/__tests__/__snapshots__/query-serializer.test.ts.snap @@ -1,12 +1,8 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`sortQuery orders the generated query in the URL > findPets 1`] = `"https://example.com/api/pets?limit=10&tags=cat&tags=dog"`; +exports[`the generated query serializes in document order > findPets 1`] = `"tags=cat&tags=dog&limit=10"`; -exports[`sortQuery orders the generated query in the URL > imageCreate 1`] = `"https://example.com/v1.43/images/create?changes=ENV%20a%3D1,ENV%20b%3D2&fromImage=alpine&platform=linux%2Famd64&tag=latest"`; - -exports[`the generated query reaches the URL in document order > findPets 1`] = `"https://example.com/api/pets?tags=cat&tags=dog&limit=10"`; - -exports[`the generated query reaches the URL in document order > imageCreate 1`] = `"https://example.com/v1.43/images/create?fromImage=alpine&tag=latest&changes=ENV%20a%3D1,ENV%20b%3D2&platform=linux%2Famd64"`; +exports[`the generated query serializes in document order > imageCreate 1`] = `"fromImage=alpine&tag=latest&changes=ENV%20a%3D1,ENV%20b%3D2&platform=linux%2Famd64"`; exports[`the source of a command that names a serializer 1`] = ` "import { Command, stripUndefined, deepObjectSerializer } from "@block65/rest-client"; diff --git a/__tests__/query-serializer.test.ts b/__tests__/query-serializer.test.ts index e114c7e..fa05378 100644 --- a/__tests__/query-serializer.test.ts +++ b/__tests__/query-serializer.test.ts @@ -1,9 +1,7 @@ -import { RestServiceClient } from "@block65/rest-client"; import type { oas32 } from "openapi3-ts"; -import { assert, expect, test, vi } from "vitest"; +import { assert, expect, test } from "vitest"; import { ImageCreateCommand } from "./fixtures/docker/commands.ts"; import { FindPetsCommand } from "./fixtures/petstore/commands.ts"; -import { SwaggerPetstoreRestClient } from "./fixtures/petstore/main.ts"; import { generateCommandsText, generateWithQueryParameters, @@ -126,63 +124,25 @@ test("the source of a command that names none", async () => { ).resolves.toMatchSnapshot(); }); -function fetchStub() { - return vi.fn(async () => Response.json({})); -} - -function readRequestedUrl(fetch: ReturnType) { - expect(fetch).toHaveBeenCalledOnce(); - - const [url] = fetch.mock.calls[0] ?? []; - assert(url instanceof URL); - - return url.href; -} - // findPets declares tags before limit, which is not alphabetical order -async function findPetsUrl(sortQuery?: true) { - const fetch = fetchStub(); - const client = new SwaggerPetstoreRestClient( - new URL("https://example.com/api/"), - { fetch, sortQuery }, - ); - - await client.json(new FindPetsCommand({ tags: ["cat", "dog"], limit: "10" })); - - return readRequestedUrl(fetch); -} +const findPets = new FindPetsCommand({ tags: ["cat", "dog"], limit: "10" }); // ImageCreate declares seven query parameters and names formJoinSerializer -async function imageCreateUrl(sortQuery?: true) { - const fetch = fetchStub(); - - // ImageCreate leaves its response unspecified, so the base client sends it - const client = new RestServiceClient(new URL("https://example.com/v1.43/"), { - fetch, - sortQuery, - }); - - await client.json( - new ImageCreateCommand({ - body: "", - tag: "latest", - fromImage: "alpine", - changes: ["ENV a=1", "ENV b=2"], - platform: "linux/amd64", - }), - ); - - return readRequestedUrl(fetch); -} +const imageCreate = new ImageCreateCommand({ + body: "", + tag: "latest", + fromImage: "alpine", + changes: ["ENV a=1", "ENV b=2"], + platform: "linux/amd64", +}); // A change in how the constructor assembles the query — the destructuring, // stripUndefined, a spread — moves these keys -test("the generated query reaches the URL in document order", async () => { - await expect(findPetsUrl()).resolves.toMatchSnapshot("findPets"); - await expect(imageCreateUrl()).resolves.toMatchSnapshot("imageCreate"); -}); - -test("sortQuery orders the generated query in the URL", async () => { - await expect(findPetsUrl(true)).resolves.toMatchSnapshot("findPets"); - await expect(imageCreateUrl(true)).resolves.toMatchSnapshot("imageCreate"); +test("the generated query serializes in document order", () => { + expect(findPets.querySerializer(findPets.query ?? {})).toMatchSnapshot( + "findPets", + ); + expect(imageCreate.querySerializer(imageCreate.query ?? {})).toMatchSnapshot( + "imageCreate", + ); });