Skip to content

Commit 0fc4aee

Browse files
committed
refactor: prefer non-mutating array methods and explicit control flow
Switch sort()/reverse() to toSorted()/toReversed() so the source arrays aren't mutated as a side effect. Replace deeply-nested ternaries with iife blocks that early-return — paired with dropping the unicorn nested-ternary lint exception in the next commit. Tighten utils by removing unused exports (maybeJsDocDescription, refToName) and scoping helpers that weren't reused. No runtime behaviour change: arrays were locally owned, the iife refactors preserve every branch, and the dropped utils helpers had no remaining callers.
1 parent 9992c2e commit 0fc4aee

5 files changed

Lines changed: 126 additions & 123 deletions

File tree

lib/hono-valibot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,6 @@ export function addValibotImportsToHonoValibotFile(
8888

8989
honoValibotFile.addImportDeclaration({
9090
moduleSpecifier: "./valibot.js",
91-
namedImports: schemaNames.sort(),
91+
namedImports: schemaNames.toSorted(),
9292
});
9393
}

lib/process-document.ts

Lines changed: 80 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-restricted-syntax */
22

3-
import { join, relative } from "node:path";
3+
import { join } from "node:path";
44
import { $RefParser } from "@apidevtools/json-schema-ref-parser";
55
import type { oas30, oas31 } from "openapi3-ts";
66
import toposort from "toposort";
@@ -79,13 +79,16 @@ function createUnion(...types: (string | undefined)[]) {
7979
// create a type of all the inputs
8080
const [type1, type2, ...typeX] = types
8181
.filter((t): t is string => !!t)
82-
.sort((a, b) => a.localeCompare(b));
82+
.toSorted((a, b) => a.localeCompare(b));
8383
return (
8484
(type1 && type2 ? Writers.unionType(type1, type2, ...typeX) : type1) ||
8585
neverKeyword
8686
);
8787
}
8888

89+
const isInput = (t: TypeAliasDeclaration | InterfaceDeclaration) =>
90+
t.getName()?.endsWith("Input");
91+
8992
export async function processOpenApiDocument(
9093
outputDir: string,
9194
schema: Simplify<oas31.OpenAPIObject>,
@@ -187,9 +190,7 @@ export async function processOpenApiDocument(
187190
const valibotModuleSpecifier = `./${valibotFile.getBaseNameWithoutExtension()}.js`;
188191
const commandValibotImports = new Set<string>();
189192

190-
const typesModuleSpecifier =
191-
`./${typesFile.getBaseNameWithoutExtension()}.js` ||
192-
relative(commandsFile.getDirectoryPath(), typesFile.getFilePath());
193+
const typesModuleSpecifier = `./${typesFile.getBaseNameWithoutExtension()}.js`;
193194

194195
const typesImportDecl =
195196
commandsFile
@@ -236,9 +237,11 @@ export async function processOpenApiDocument(
236237
},
237238
);
238239

239-
const sorted = toposort(schemaGraph).reverse();
240+
const sorted = toposort(schemaGraph).toReversed();
240241

241-
const sortedSchemas = Object.entries(schema.components?.schemas || {}).sort(
242+
const sortedSchemas = Object.entries(
243+
schema.components?.schemas || {},
244+
).toSorted(
242245
([a], [b]) =>
243246
sorted.indexOf(`#/components/schemas/${a}`) -
244247
sorted.indexOf(`#/components/schemas/${b}`),
@@ -276,11 +279,10 @@ export async function processOpenApiDocument(
276279
? [
277280
{
278281
description: wordWrap(schemaObject.description),
279-
tags: [
280-
...(schemaObject.deprecated
281-
? [{ tagName: "deprecated" }]
282-
: []),
283-
].filter(Boolean),
282+
tags: (schemaObject.deprecated
283+
? [{ tagName: "deprecated" }]
284+
: []
285+
).filter(Boolean),
284286
},
285287
]
286288
: [],
@@ -291,7 +293,9 @@ export async function processOpenApiDocument(
291293
writer.write("[");
292294
values.forEach((value, index) => {
293295
writer.write(JSON.stringify(value));
294-
if (index < values.length - 1) writer.write(", ");
296+
if (index < values.length - 1) {
297+
writer.write(", ");
298+
}
295299
});
296300
writer.write("]");
297301
}, "const"),
@@ -359,16 +363,14 @@ export async function processOpenApiDocument(
359363
operationObject.description || commandName,
360364
)}\n`,
361365

362-
tags: [
363-
...(operationObject.summary
364-
? [
365-
{
366-
tagName: "summary",
367-
text: wordWrap(operationObject.summary),
368-
},
369-
]
370-
: []),
371-
],
366+
tags: operationObject.summary
367+
? [
368+
{
369+
tagName: "summary",
370+
text: wordWrap(operationObject.summary),
371+
},
372+
]
373+
: [],
372374
};
373375

374376
const jsdoc = commandClassDeclaration.addJsDoc(jsDocStructure);
@@ -489,13 +491,18 @@ export async function processOpenApiDocument(
489491
},
490492
);
491493

492-
const resolvedType = qp.required
493-
? type.type
494-
: typeof type.type === "function"
495-
? type.type
496-
: type.type
497-
? Writers.unionType(`${type.type}`, "undefined")
498-
: undefined;
494+
const resolvedType = iife(() => {
495+
if (qp.required) {
496+
return type.type;
497+
}
498+
if (typeof type.type === "function") {
499+
return type.type;
500+
}
501+
if (type.type) {
502+
return Writers.unionType(`${type.type}`, "undefined");
503+
}
504+
return undefined;
505+
});
499506

500507
return {
501508
...type,
@@ -963,34 +970,43 @@ export async function processOpenApiDocument(
963970
});
964971
}
965972

966-
// Resolve response schema from the 2xx response $ref
973+
// Resolve response schema from the first 2xx response $ref
967974
const responseRef = iife(() => {
968-
for (const [statusCode, response] of Object.entries(
975+
const firstSuccess = Object.entries(
969976
operationObject.responses ?? {},
970-
).filter(([s]) => s.startsWith("2"))) {
971-
if (statusCode === "204" || "$ref" in response) return undefined;
972-
const jsonResp = response.content?.["application/json"];
973-
if (!jsonResp?.schema) return undefined;
974-
if ("$ref" in jsonResp.schema) return jsonResp.schema.$ref;
975-
if (
976-
"items" in jsonResp.schema &&
977-
"$ref" in jsonResp.schema.items
978-
) {
979-
return jsonResp.schema.items.$ref;
980-
}
977+
).find(([s]) => s.startsWith("2"));
978+
if (!firstSuccess) {
979+
return undefined;
980+
}
981+
const [statusCode, response] = firstSuccess;
982+
if (statusCode === "204" || "$ref" in response) {
981983
return undefined;
982984
}
985+
const responseSchema =
986+
response.content?.["application/json"]?.schema;
987+
if (!responseSchema) {
988+
return undefined;
989+
}
990+
if ("$ref" in responseSchema) {
991+
return responseSchema.$ref;
992+
}
993+
if ("items" in responseSchema && "$ref" in responseSchema.items) {
994+
return responseSchema.items.$ref;
995+
}
983996
return undefined;
984997
});
985998

986999
const responseSchemaEntry = responseRef
9871000
? validators.get(responseRef)
9881001
: undefined;
989-
const responseSchemaName = responseSchemaEntry
990-
? options?.exactOnly
1002+
const responseSchemaName = iife(() => {
1003+
if (!responseSchemaEntry) {
1004+
return undefined;
1005+
}
1006+
return options?.exactOnly
9911007
? responseSchemaEntry.exact
992-
: responseSchemaEntry.coerced
993-
: undefined;
1008+
: responseSchemaEntry.coerced;
1009+
});
9941010

9951011
if (responseSchemaName) {
9961012
staticSchemaProps.push({
@@ -1145,28 +1161,30 @@ export async function processOpenApiDocument(
11451161

11461162
const headersArg = hasHeaders ? "headers" : undefined;
11471163

1164+
const queryOrHeaderArgs = iife(() => {
1165+
if (hasQuery) {
1166+
return [`stripUndefined({${queryParameterNames.join(", ")}})`];
1167+
}
1168+
if (hasHeaders) {
1169+
return [emptyKeyword];
1170+
}
1171+
return [];
1172+
});
1173+
11481174
// type narrowing
11491175
if (Node.isCallExpression(callExpr)) {
11501176
if (hasJsonBody) {
11511177
callExpr.addArguments([
11521178
pathname,
11531179
`jsonStringify(${inputBodyName})`,
1154-
...(hasQuery
1155-
? [`stripUndefined({${queryParameterNames.join(", ")}})`]
1156-
: hasHeaders
1157-
? [emptyKeyword]
1158-
: []),
1180+
...queryOrHeaderArgs,
11591181
...(headersArg ? [headersArg] : []),
11601182
]);
11611183
} else if (hasNonJsonBody) {
11621184
callExpr.addArguments([
11631185
pathname,
11641186
nonJsonBodyPropName,
1165-
...(hasQuery
1166-
? [`stripUndefined({${queryParameterNames.join(", ")}})`]
1167-
: hasHeaders
1168-
? [emptyKeyword]
1169-
: []),
1187+
...queryOrHeaderArgs,
11701188
...(headersArg ? [headersArg] : []),
11711189
]);
11721190
} else if (hasQuery) {
@@ -1196,13 +1214,9 @@ export async function processOpenApiDocument(
11961214
}
11971215
}
11981216

1199-
const isInput = (t: TypeAliasDeclaration | InterfaceDeclaration) =>
1200-
t.getName()?.endsWith("Input");
1201-
// const isOutput = (t: string) => t.endsWith('Output');
1202-
12031217
const inputTypes = typesFile.getTypeAliases().filter((t) => isInput(t));
12041218
const inputUnion = createUnion(
1205-
...new Set(inputTypes.sort().map((t) => t.getName())),
1219+
...new Set(inputTypes.toSorted().map((t) => t.getName())),
12061220
);
12071221
const outputUnion = createUnion(
12081222
...[...outputTypes].map((t) => (typeof t === "string" ? t : t.getName())),
@@ -1247,7 +1261,7 @@ export async function processOpenApiDocument(
12471261
mainFile.addImportDeclaration({
12481262
moduleSpecifier: typesModuleSpecifier,
12491263
namedImports: namedImports
1250-
.sort((a, b) => a.getName().localeCompare(b.getName()))
1264+
.toSorted((a, b) => a.getName().localeCompare(b.getName()))
12511265
.map((t) => ({
12521266
name: t.getName(),
12531267
})),
@@ -1305,7 +1319,9 @@ export async function processOpenApiDocument(
13051319
if (commandValibotImports.size > 0) {
13061320
commandsFile.addImportDeclaration({
13071321
moduleSpecifier: valibotModuleSpecifier,
1308-
namedImports: [...commandValibotImports].sort().map((name) => ({ name })),
1322+
namedImports: [...commandValibotImports]
1323+
.toSorted()
1324+
.map((name) => ({ name })),
13091325
});
13101326
}
13111327

lib/process-schema.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Writers,
1414
} from "ts-morph";
1515
import {
16+
iife,
1617
isNotNullOrUndefined,
1718
isNotReferenceObject,
1819
isReferenceObject,
@@ -234,7 +235,7 @@ export function schemaToType(
234235
schemaObject.items || {},
235236
);
236237

237-
if (type.type instanceof Function) {
238+
if (typeof type.type === "function") {
238239
const typeWriter = type.type;
239240
return {
240241
name,
@@ -625,25 +626,22 @@ export function registerTypesFromSchema(
625626
name: pascalCase(schemaName),
626627
isExported: true,
627628
// WARN: Duplicated code - the recursion beat me
628-
type: schemaObject.properties
629-
? Writers.objectType({
629+
type: iife(() => {
630+
if (schemaObject.properties) {
631+
return Writers.objectType({
630632
properties: Object.entries(schemaObject.properties).map(
631-
([key, schema]) => {
632-
const type = schemaToType(
633-
typesAndInterfaces,
634-
schemaObject,
635-
key,
636-
schema,
637-
);
638-
639-
return type;
640-
},
633+
([key, schema]) =>
634+
schemaToType(typesAndInterfaces, schemaObject, key, schema),
641635
),
642-
})
643-
: typeof schemaObject.additionalProperties === "object" &&
644-
"type" in schemaObject.additionalProperties
645-
? `Record<string, ${schemaObject.additionalProperties.type === "array" ? "unknown[]" : schemaObject.additionalProperties.type}>`
646-
: "Record<string | number, Jsonifiable>",
636+
});
637+
}
638+
const ap = schemaObject.additionalProperties;
639+
if (typeof ap === "object" && "type" in ap) {
640+
const valueType = ap.type === "array" ? "unknown[]" : ap.type;
641+
return `Record<string, ${valueType}>`;
642+
}
643+
return "Record<string | number, Jsonifiable>";
644+
}),
647645
});
648646

649647
if (schemaObject.description) {

lib/utils.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,11 @@ import camelcase from "camelcase";
22
import type { oas31 } from "openapi3-ts";
33
import wrap from "word-wrap";
44

5-
export function maybeJsDocDescription(
6-
...str: (string | undefined | false | null)[]
7-
): string {
8-
return str.length > 0 ? ["", ...str].filter(Boolean).join(" - ").trim() : "";
9-
}
10-
115
export function isReferenceObject(obj: unknown): obj is oas31.ReferenceObject {
126
return typeof obj === "object" && obj !== null && "$ref" in obj;
137
}
148

15-
export function getDependency(obj: unknown): string | undefined {
9+
function getDependency(obj: unknown): string | undefined {
1610
return isReferenceObject(obj) ? obj.$ref : undefined;
1711
}
1812

@@ -26,11 +20,11 @@ export function isNotNullOrUndefined<T>(obj: T | null | undefined): obj is T {
2620
return obj !== null && typeof obj !== "undefined";
2721
}
2822

23+
const strOnly = (x: string | undefined): x is string => typeof x === "string";
24+
2925
export function getDependents(
3026
obj: oas31.ReferenceObject | oas31.SchemaObject,
3127
): string[] {
32-
const strOnly = (x: string | undefined): x is string => typeof x === "string";
33-
3428
if (isReferenceObject(obj)) {
3529
return [getDependency(obj)].filter(strOnly);
3630
}
@@ -61,14 +55,6 @@ export function getDependents(
6155
return [];
6256
}
6357

64-
export function refToName(ref: string): string {
65-
const name = ref.split("/").at(-1);
66-
if (!name) {
67-
throw new Error(`invalid ref: ${ref}`);
68-
}
69-
return name;
70-
}
71-
7258
export function camelCase(...str: string[]): string {
7359
return camelcase(str.flatMap((s) => s.split("/")));
7460
}

0 commit comments

Comments
 (0)