@amritk/generate-validators 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,4 +27,4 @@ export type GeneratedFile = {
27
27
  * // files → [{ filename: 'document.ts', content: '...' }, { filename: 'info.ts', ... }, ...]
28
28
  * ```
29
29
  */
30
- export declare const buildValidatorSchema: (rootSchema: JSONSchema, rootTypeName: string) => Promise<GeneratedFile[]>;
30
+ export declare const buildValidatorSchema: (rootSchema: JSONSchema, rootTypeName: string, typeSuffix?: string) => Promise<GeneratedFile[]>;
@@ -13,6 +13,11 @@ type CollectValidatorImportsOptions = {
13
13
  * are excluded from the import list (they were never generated as files).
14
14
  */
15
15
  readonly rootSchema?: Record<string, unknown> | undefined;
16
+ /**
17
+ * Suffix appended to every type/validator name derived from a `$ref`. Must
18
+ * match the suffix used when generating the referenced files. Defaults to `''`.
19
+ */
20
+ readonly typeSuffix?: string;
16
21
  };
17
22
  /**
18
23
  * Collects import statements for all $ref dependencies of a schema.
@@ -22,7 +27,7 @@ type CollectValidatorImportsOptions = {
22
27
  * ```typescript
23
28
  * const schema = { properties: { contact: { $ref: '#/$defs/contact' } } }
24
29
  * collectValidatorImports(schema)
25
- * // ["import { type ContactObject, validateContactObject } from './contact-object'"]
30
+ * // ["import { type Contact, validateContact } from './contact'"]
26
31
  * ```
27
32
  */
28
33
  export declare const collectValidatorImports: (schema: JSONSchema, options?: CollectValidatorImportsOptions) => string[];
@@ -12,6 +12,11 @@ type GenerateValidatorFileOptions = {
12
12
  * The root schema document. Used to filter out unresolvable refs.
13
13
  */
14
14
  readonly rootSchema?: Record<string, unknown>;
15
+ /**
16
+ * Suffix appended to every type/validator name derived from a `$ref`.
17
+ * Defaults to `''` (no suffix).
18
+ */
19
+ readonly typeSuffix?: string;
15
20
  };
16
21
  /**
17
22
  * Generates a complete TypeScript validator file from a JSON Schema.
@@ -21,4 +21,4 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
21
21
  * // }
22
22
  * ```
23
23
  */
24
- export declare const generateValidatorFunction: (schema: JSONSchema, typeName: string) => string;
24
+ export declare const generateValidatorFunction: (schema: JSONSchema, typeName: string, suffix?: string) => string;
package/dist/index.js CHANGED
@@ -132,15 +132,15 @@ var refToFilename2 = (ref) => {
132
132
  }
133
133
  return filename;
134
134
  };
135
- var kebabToPascal = (kebab) => {
135
+ var kebabToPascal = (kebab, suffix) => {
136
136
  const words = kebab.split("-");
137
137
  let pascalCase = "";
138
138
  for (const word of words) {
139
139
  pascalCase += word.charAt(0).toUpperCase() + word.slice(1);
140
140
  }
141
- return pascalCase + "Object";
141
+ return pascalCase + suffix;
142
142
  };
143
- var refToName = (ref) => kebabToPascal(refToFilename2(ref));
143
+ var refToName = (ref, suffix = "") => kebabToPascal(refToFilename2(ref), suffix);
144
144
 
145
145
  // ../helpers/dist/resolve-dynamic-refs.js
146
146
  var resolveDynamicRefs = (schema, dynamicRefMap) => {
@@ -415,15 +415,15 @@ var refToFilename4 = (ref) => {
415
415
  }
416
416
  return filename;
417
417
  };
418
- var kebabToPascal2 = (kebab) => {
418
+ var kebabToPascal2 = (kebab, suffix) => {
419
419
  const words = kebab.split("-");
420
420
  let pascalCase = "";
421
421
  for (const word of words) {
422
422
  pascalCase += word.charAt(0).toUpperCase() + word.slice(1);
423
423
  }
424
- return pascalCase + "Object";
424
+ return pascalCase + suffix;
425
425
  };
426
- var refToName2 = (ref) => kebabToPascal2(refToFilename4(ref));
426
+ var refToName2 = (ref, suffix = "") => kebabToPascal2(refToFilename4(ref), suffix);
427
427
  var JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
428
428
  var safeKey = (key) => {
429
429
  if (JS_IDENTIFIER.test(key)) {
@@ -516,12 +516,13 @@ var buildJsDocBlock = (title, description, commentUrl) => {
516
516
  `;
517
517
  return block;
518
518
  };
519
- var getTypeScriptType = (schema) => {
520
- const base = getUnbrandedType(schema);
519
+ var getTypeScriptType = (schema, options = {}) => {
520
+ const base = getUnbrandedType(schema, options);
521
521
  const brand = getMjstBrand(schema);
522
522
  return brand ? `(${base} & { readonly __brand: '${brand}' })` : base;
523
523
  };
524
- var getUnbrandedType = (schema) => {
524
+ var recordType = (keyType, valueType, options) => options.readonly ? `Readonly<Record<${keyType}, ${valueType}>>` : `Record<${keyType}, ${valueType}>`;
525
+ var getUnbrandedType = (schema, options = {}) => {
525
526
  if (typeof schema === "boolean") {
526
527
  return getBooleanSubSchemaType(schema);
527
528
  }
@@ -540,13 +541,13 @@ var getUnbrandedType = (schema) => {
540
541
  if (!schema.$ref.startsWith("#")) {
541
542
  return "unknown";
542
543
  }
543
- return refToName2(schema.$ref);
544
+ return refToName2(schema.$ref, options.typeSuffix);
544
545
  }
545
546
  if (schema.$dynamicRef) {
546
547
  if (schema.$dynamicRef === "#meta") {
547
- return "Schema";
548
+ return `Schema${options.typeSuffix ?? ""}`;
548
549
  }
549
- return refToName2(schema.$dynamicRef);
550
+ return refToName2(schema.$dynamicRef, options.typeSuffix);
550
551
  }
551
552
  if (schema.const !== undefined) {
552
553
  return JSON.stringify(schema.const);
@@ -569,51 +570,51 @@ var getUnbrandedType = (schema) => {
569
570
  return multiEnumUnion;
570
571
  }
571
572
  if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
572
- let oneOfUnion = getTypeScriptType(schema.oneOf[0]);
573
+ let oneOfUnion = getTypeScriptType(schema.oneOf[0], options);
573
574
  for (let i = 1;i < schema.oneOf.length; i++) {
574
- oneOfUnion += " | " + getTypeScriptType(schema.oneOf[i]);
575
+ oneOfUnion += " | " + getTypeScriptType(schema.oneOf[i], options);
575
576
  }
576
577
  return oneOfUnion;
577
578
  }
578
579
  if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
579
- let anyOfUnion = getTypeScriptType(schema.anyOf[0]);
580
+ let anyOfUnion = getTypeScriptType(schema.anyOf[0], options);
580
581
  for (let i = 1;i < schema.anyOf.length; i++) {
581
- anyOfUnion += " | " + getTypeScriptType(schema.anyOf[i]);
582
+ anyOfUnion += " | " + getTypeScriptType(schema.anyOf[i], options);
582
583
  }
583
584
  return anyOfUnion;
584
585
  }
585
586
  if (schema.allOf && Array.isArray(schema.allOf) && schema.allOf.length > 0) {
586
- let intersectionTypes = getTypeScriptType(schema.allOf[0]);
587
+ let intersectionTypes = getTypeScriptType(schema.allOf[0], options);
587
588
  for (let i = 1;i < schema.allOf.length; i++) {
588
- intersectionTypes += " & " + getTypeScriptType(schema.allOf[i]);
589
+ intersectionTypes += " & " + getTypeScriptType(schema.allOf[i], options);
589
590
  }
590
591
  return intersectionTypes;
591
592
  }
592
593
  const conditionalResult = getConditionalObjectSchema(schema);
593
594
  if (conditionalResult) {
594
- const baseType = getTypeScriptType(conditionalResult.schema);
595
+ const baseType = getTypeScriptType(conditionalResult.schema, options);
595
596
  if (conditionalResult.thenRef) {
596
- return `(${baseType}) & ${refToName2(conditionalResult.thenRef)}`;
597
+ return `(${baseType}) & ${refToName2(conditionalResult.thenRef, options.typeSuffix)}`;
597
598
  }
598
599
  return baseType;
599
600
  }
600
601
  if (!schema.type) {
601
602
  if (schema.additionalProperties !== undefined) {
602
603
  if (typeof schema.additionalProperties === "boolean") {
603
- return `Record<string, ${getBooleanSubSchemaType(schema.additionalProperties)}>`;
604
+ return recordType("string", getBooleanSubSchemaType(schema.additionalProperties), options);
604
605
  }
605
- return `Record<string, ${getTypeScriptType(schema.additionalProperties)}>`;
606
+ return recordType("string", getTypeScriptType(schema.additionalProperties, options), options);
606
607
  }
607
608
  if (schema.patternProperties && typeof schema.patternProperties === "object") {
608
609
  const firstEntry = Object.entries(schema.patternProperties)[0];
609
610
  if (firstEntry) {
610
611
  const [pattern, value] = firstEntry;
611
612
  if (value !== undefined) {
612
- const valueType = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value);
613
+ const valueType = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value, options);
613
614
  if (pattern === "^x-") {
614
- return `Record<\`x-\${string}\`, ${valueType}>`;
615
+ return recordType("`x-${string}`", valueType, options);
615
616
  }
616
- return `Record<string, ${valueType}>`;
617
+ return recordType("string", valueType, options);
617
618
  }
618
619
  }
619
620
  }
@@ -666,41 +667,42 @@ var getUnbrandedType = (schema) => {
666
667
  return "boolean";
667
668
  case "array":
668
669
  if (schema.items) {
669
- const itemType = getTypeScriptType(schema.items);
670
+ const itemType = getTypeScriptType(schema.items, options);
670
671
  const wrappedItemType = itemType.includes(" | ") ? `(${itemType})` : itemType;
671
- return `${wrappedItemType}[]`;
672
+ return options.readonly ? `readonly ${wrappedItemType}[]` : `${wrappedItemType}[]`;
672
673
  }
673
- return "unknown[]";
674
+ return options.readonly ? "readonly unknown[]" : "unknown[]";
674
675
  case "object":
675
676
  if (schema.properties) {
677
+ const readonlyPrefix = options.readonly ? "readonly " : "";
676
678
  let properties = "";
677
679
  let first = true;
678
680
  for (const key in schema.properties) {
679
681
  const propSchema = schema.properties[key];
680
682
  const isRequired = schema.required?.includes(key) ?? false;
681
683
  const optional = isRequired ? "" : "?";
682
- const propType = getTypeScriptType(propSchema);
684
+ const propType = getTypeScriptType(propSchema, options);
683
685
  if (!first)
684
686
  properties += "; ";
685
- properties += safeKey(key) + optional + ": " + propType;
687
+ properties += readonlyPrefix + safeKey(key) + optional + ": " + propType;
686
688
  first = false;
687
689
  }
688
690
  return "{ " + properties + " }";
689
691
  }
690
692
  if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
691
- const additionalPropType = getTypeScriptType(schema.additionalProperties);
692
- return `Record<string, ${additionalPropType}>`;
693
+ const additionalPropType = getTypeScriptType(schema.additionalProperties, options);
694
+ return recordType("string", additionalPropType, options);
693
695
  }
694
696
  if (schema.patternProperties && typeof schema.patternProperties === "object") {
695
697
  const firstEntry = Object.entries(schema.patternProperties)[0];
696
698
  if (firstEntry) {
697
699
  const [pattern, patternVal] = firstEntry;
698
700
  if (patternVal) {
699
- const valueType = getTypeScriptType(patternVal);
701
+ const valueType = getTypeScriptType(patternVal, options);
700
702
  if (pattern === "^x-") {
701
- return `Record<\`x-\${string}\`, ${valueType}>`;
703
+ return recordType("`x-${string}`", valueType, options);
702
704
  }
703
- return `Record<string, ${valueType}>`;
705
+ return recordType("string", valueType, options);
704
706
  }
705
707
  }
706
708
  }
@@ -709,9 +711,10 @@ var getUnbrandedType = (schema) => {
709
711
  return "unknown";
710
712
  }
711
713
  };
712
- var generateTypeDefinition = (schema, typeName) => {
714
+ var generateTypeDefinition = (schema, typeName, options = {}) => {
715
+ const readonlyPrefix = options.readonly ? "readonly " : "";
713
716
  if (!isObjectLikeSchema(schema)) {
714
- const tsType = getTypeScriptType(schema);
717
+ const tsType = getTypeScriptType(schema, options);
715
718
  let result = "";
716
719
  if (isSchemaObject2(schema) && schema.$comment && typeof schema.$comment === "string") {
717
720
  result += buildJsDocBlock(typeName, schema.$comment);
@@ -739,23 +742,23 @@ var generateTypeDefinition = (schema, typeName) => {
739
742
  if (firstPatternProperty === undefined) {
740
743
  return `export type ${typeName} = Record<string, unknown>;`;
741
744
  }
742
- const patternPropType = typeof firstPatternProperty === "boolean" ? getBooleanSubSchemaType(firstPatternProperty) : getTypeScriptType(firstPatternProperty);
745
+ const patternPropType = typeof firstPatternProperty === "boolean" ? getBooleanSubSchemaType(firstPatternProperty) : getTypeScriptType(firstPatternProperty, options);
743
746
  const keyType = firstPattern === "^x-" ? "`x-${string}`" : "string";
744
747
  let result2 = "";
745
748
  if (jsDocTitle && jsDocDescription) {
746
749
  result2 += buildJsDocBlock(jsDocTitle, jsDocDescription);
747
750
  }
748
- result2 += `export type ${typeName} = Record<${keyType}, ${patternPropType}>;`;
751
+ result2 += `export type ${typeName} = ${recordType(keyType, patternPropType, options)};`;
749
752
  return result2;
750
753
  }
751
754
  if (!hasProperties2 && hasAdditionalProperties2 && normalizedSchema.additionalProperties) {
752
- const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties);
755
+ const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties, options);
753
756
  let result2 = "";
754
757
  if (jsDocTitle && jsDocDescription) {
755
758
  result2 += buildJsDocBlock(jsDocTitle, jsDocDescription);
756
759
  }
757
760
  result2 += `export type ${typeName} = {
758
- [key: string]: ${additionalPropType};
761
+ ${readonlyPrefix}[key: string]: ${additionalPropType};
759
762
  };`;
760
763
  return result2;
761
764
  }
@@ -766,8 +769,8 @@ var generateTypeDefinition = (schema, typeName) => {
766
769
  const propSchema = schemaProps[key];
767
770
  const isRequired = normalizedSchema.required?.includes(key) ?? false;
768
771
  const optional = isRequired ? "" : "?";
769
- const propType = getTypeScriptType(propSchema);
770
- const quotedKey = safeKey(key);
772
+ const propType = getTypeScriptType(propSchema, options);
773
+ const quotedKey = readonlyPrefix + safeKey(key);
771
774
  if (!isFirstProp)
772
775
  properties += `
773
776
  `;
@@ -784,12 +787,12 @@ var generateTypeDefinition = (schema, typeName) => {
784
787
  if (isSchemaObject2(schema) && Array.isArray(schema.allOf)) {
785
788
  for (const entry of schema.allOf) {
786
789
  if (isSchemaObject2(entry) && entry.$ref) {
787
- allOfIntersections.push(refToName2(entry.$ref));
790
+ allOfIntersections.push(refToName2(entry.$ref, options.typeSuffix));
788
791
  }
789
792
  }
790
793
  }
791
794
  if (isSchemaObject2(schema) && typeof schema.$ref === "string" && schema.$ref.startsWith("#")) {
792
- allOfIntersections.push(refToName2(schema.$ref));
795
+ allOfIntersections.push(refToName2(schema.$ref, options.typeSuffix));
793
796
  }
794
797
  let result = "";
795
798
  if (jsDocTitle && jsDocDescription) {
@@ -799,7 +802,7 @@ var generateTypeDefinition = (schema, typeName) => {
799
802
  ` + properties + `
800
803
  }`;
801
804
  if (conditionalThenRef) {
802
- typeBody += " & " + refToName2(conditionalThenRef);
805
+ typeBody += " & " + refToName2(conditionalThenRef, options.typeSuffix);
803
806
  }
804
807
  for (const intersectionType of allOfIntersections) {
805
808
  typeBody += " & " + intersectionType;
@@ -873,9 +876,9 @@ var hasMultipleOf = (schema) => {
873
876
  };
874
877
 
875
878
  // src/generators/collect-validator-imports.ts
876
- var buildImport = (ref) => {
879
+ var buildImport = (ref, suffix) => {
877
880
  const filename = refToFilename(ref);
878
- const typeName = refToName(ref);
881
+ const typeName = refToName(ref, suffix);
879
882
  const validatorName = `validate${typeName}`;
880
883
  return `import { type ${typeName}, ${validatorName} } from './${filename}'`;
881
884
  };
@@ -920,6 +923,7 @@ var collectDirectRefs = (schema) => {
920
923
  var collectValidatorImports = (schema, options) => {
921
924
  const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
922
925
  const rootSchema = options?.rootSchema;
926
+ const typeSuffix = options?.typeSuffix ?? "";
923
927
  const refs = collectDirectRefs(schema);
924
928
  const seen = new Set;
925
929
  const imports = [];
@@ -936,7 +940,7 @@ var collectValidatorImports = (schema, options) => {
936
940
  }
937
941
  seen.add(filename);
938
942
  const importRef = ref.endsWith("-or-reference") ? ref.replace("-or-reference", "") : ref;
939
- imports.push(buildImport(importRef));
943
+ imports.push(buildImport(importRef, typeSuffix));
940
944
  }
941
945
  return imports;
942
946
  };
@@ -990,7 +994,7 @@ var wrongTypeCondition = (accessor, type) => {
990
994
  return "";
991
995
  }
992
996
  };
993
- var generatePropertyChecks = (key, propSchema, isRequired) => {
997
+ var generatePropertyChecks = (key, propSchema, isRequired, suffix) => {
994
998
  if (!isSchemaObject3(propSchema))
995
999
  return [];
996
1000
  const raw = `obj[${JSON.stringify(key)}]`;
@@ -998,7 +1002,7 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
998
1002
  const lines = [];
999
1003
  if (hasRef(propSchema)) {
1000
1004
  const ref = propSchema.$ref;
1001
- const vName = validatorName(refToName(ref));
1005
+ const vName = validatorName(refToName(ref, suffix));
1002
1006
  if (isRequired) {
1003
1007
  lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
1004
1008
  lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
@@ -1124,7 +1128,7 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
1124
1128
  if (t === "array" && hasItems(propSchema)) {
1125
1129
  const itemSchema = propSchema.items;
1126
1130
  if (hasRef(itemSchema)) {
1127
- const vName = validatorName(refToName(itemSchema.$ref));
1131
+ const vName = validatorName(refToName(itemSchema.$ref, suffix));
1128
1132
  lines.push(` if (Array.isArray(${raw})) {`);
1129
1133
  lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`);
1130
1134
  lines.push(` const _ir = ${vName}(${raw}[_i], \`${path.slice(1, -1)}/${key}/\${_i}\`)`);
@@ -1148,19 +1152,19 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
1148
1152
  }
1149
1153
  return lines;
1150
1154
  };
1151
- var generateObjectValidator = (schema, typeName) => {
1155
+ var generateObjectValidator = (schema, typeName, suffix) => {
1152
1156
  const vName = validatorName(typeName);
1153
1157
  const required = new Set(hasRequired(schema) ? schema.required : []);
1154
1158
  const properties = hasProperties(schema) ? schema.properties : {};
1155
1159
  const propertyLines = [];
1156
1160
  for (const [key, propSchema] of Object.entries(properties)) {
1157
- const checks = generatePropertyChecks(key, propSchema, required.has(key));
1161
+ const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix);
1158
1162
  if (checks.length > 0) {
1159
1163
  propertyLines.push(...checks);
1160
1164
  }
1161
1165
  }
1162
1166
  if (hasAdditionalProperties(schema) && isSchemaObject3(schema.additionalProperties) && hasRef(schema.additionalProperties)) {
1163
- const vRefName = validatorName(refToName(schema.additionalProperties.$ref));
1167
+ const vRefName = validatorName(refToName(schema.additionalProperties.$ref, suffix));
1164
1168
  propertyLines.push(` for (const _key of Object.keys(obj)) {`);
1165
1169
  propertyLines.push(` if (${JSON.stringify(Object.keys(properties))}.includes(_key)) continue`);
1166
1170
  propertyLines.push(` const _r = ${vRefName}(obj[_key as keyof typeof obj], \`\${_path}/\${_key}\`)`);
@@ -1185,14 +1189,14 @@ var generateObjectValidator = (schema, typeName) => {
1185
1189
  ].join(`
1186
1190
  `);
1187
1191
  };
1188
- var generateScalarValidator = (schema, typeName) => {
1192
+ var generateScalarValidator = (schema, typeName, suffix) => {
1189
1193
  const vName = validatorName(typeName);
1190
1194
  if (!isSchemaObject3(schema)) {
1191
1195
  return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
1192
1196
  `);
1193
1197
  }
1194
1198
  if (hasRef(schema)) {
1195
- const delegateName = validatorName(refToName(schema.$ref));
1199
+ const delegateName = validatorName(refToName(schema.$ref, suffix));
1196
1200
  return [
1197
1201
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1198
1202
  ` return ${delegateName}(input, _path)`,
@@ -1241,7 +1245,7 @@ var generateScalarValidator = (schema, typeName) => {
1241
1245
  const branches = schema.oneOf.map((branch, i) => {
1242
1246
  if (!hasRef(branch))
1243
1247
  return null;
1244
- const bName = validatorName(refToName(branch.$ref));
1248
+ const bName = validatorName(refToName(branch.$ref, suffix));
1245
1249
  return ` const _r${i} = ${bName}(input, _path)
1246
1250
  if (_r${i} === true) return true`;
1247
1251
  }).filter(Boolean).join(`
@@ -1262,54 +1266,72 @@ var generateScalarValidator = (schema, typeName) => {
1262
1266
  if (t === "string") {
1263
1267
  if (hasPattern(schema)) {
1264
1268
  constraintLines.push(` if (typeof input === 'string' && !/${schema.pattern}/.test(input)) {`);
1265
- constraintLines.push(` return { valid: false, errors: [{ message: 'must match pattern ${schema.pattern}', path: _path }] }`);
1269
+ constraintLines.push(` errors.push({ message: 'must match pattern ${schema.pattern}', path: _path })`);
1266
1270
  constraintLines.push(` }`);
1267
1271
  }
1268
1272
  if (hasMinLength(schema)) {
1269
1273
  constraintLines.push(` if (typeof input === 'string' && input.length < ${schema.minLength}) {`);
1270
- constraintLines.push(` return { valid: false, errors: [{ message: 'must have at least ${schema.minLength} characters', path: _path }] }`);
1274
+ constraintLines.push(` errors.push({ message: 'must have at least ${schema.minLength} characters', path: _path })`);
1271
1275
  constraintLines.push(` }`);
1272
1276
  }
1273
1277
  if (hasMaxLength(schema)) {
1274
1278
  constraintLines.push(` if (typeof input === 'string' && input.length > ${schema.maxLength}) {`);
1275
- constraintLines.push(` return { valid: false, errors: [{ message: 'must have at most ${schema.maxLength} characters', path: _path }] }`);
1279
+ constraintLines.push(` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`);
1276
1280
  constraintLines.push(` }`);
1277
1281
  }
1278
1282
  }
1279
- const body = constraintLines.length > 0 ? `
1280
- ` + constraintLines.join(`
1281
- `) + `
1282
- ` : "";
1283
- return wrongType ? [
1283
+ if (!wrongType) {
1284
+ return [
1285
+ `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
1286
+ ` return true`,
1287
+ `}`
1288
+ ].join(`
1289
+ `);
1290
+ }
1291
+ if (constraintLines.length === 0) {
1292
+ return [
1293
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1294
+ ` if (${wrongType}) {`,
1295
+ ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
1296
+ ` }`,
1297
+ ` return true`,
1298
+ `}`
1299
+ ].join(`
1300
+ `);
1301
+ }
1302
+ return [
1284
1303
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1285
1304
  ` if (${wrongType}) {`,
1286
1305
  ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
1287
1306
  ` }`,
1288
- body,
1289
- ` return true`,
1307
+ ` const errors: ValidationError[] = []`,
1308
+ constraintLines.join(`
1309
+ `),
1310
+ ` return errors.length > 0 ? { valid: false, errors } : true`,
1290
1311
  `}`
1291
1312
  ].join(`
1292
- `) : [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
1293
1313
  `);
1294
1314
  }
1295
1315
  return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
1296
1316
  `);
1297
1317
  };
1298
- var generateValidatorFunction = (schema, typeName) => {
1318
+ var generateValidatorFunction = (schema, typeName, suffix = "") => {
1299
1319
  if (isObjectSchema2(schema)) {
1300
- return generateObjectValidator(schema, typeName);
1320
+ return generateObjectValidator(schema, typeName, suffix);
1301
1321
  }
1302
- return generateScalarValidator(schema, typeName);
1322
+ return generateScalarValidator(schema, typeName, suffix);
1303
1323
  };
1304
1324
 
1305
1325
  // src/generators/generate-files.ts
1306
1326
  var generateValidatorFile = (schema, typeName, options) => {
1327
+ const typeSuffix = options?.typeSuffix ?? "";
1307
1328
  const refImports = collectValidatorImports(schema, {
1308
1329
  selfRef: options?.selfRef,
1309
- rootSchema: options?.rootSchema
1330
+ rootSchema: options?.rootSchema,
1331
+ typeSuffix
1310
1332
  });
1311
- const typeDefinition = generateTypeDefinition(schema, typeName);
1312
- const validatorFunction = generateValidatorFunction(schema, typeName);
1333
+ const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
1334
+ const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix);
1313
1335
  let result = `import type { ValidationResult, ValidationError } from './validation-result'
1314
1336
  `;
1315
1337
  for (const imp of refImports) {
@@ -1346,7 +1368,7 @@ export type ValidationError = {
1346
1368
  */
1347
1369
  export type ValidationResult = true | { valid: false; errors: ValidationError[] }
1348
1370
  `;
1349
- var buildValidatorSchema = async (rootSchema, rootTypeName) => {
1371
+ var buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = "") => {
1350
1372
  rootSchema = upgradeDraft07Schema(rootSchema);
1351
1373
  const files = [];
1352
1374
  const processedRefs = new Set;
@@ -1355,7 +1377,8 @@ var buildValidatorSchema = async (rootSchema, rootTypeName) => {
1355
1377
  const dynamicRefMap = buildDynamicRefMap(rootSchema);
1356
1378
  const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap);
1357
1379
  const rootContent = generateValidatorFile(processedRootSchema, rootTypeName, {
1358
- rootSchema
1380
+ rootSchema,
1381
+ typeSuffix
1359
1382
  });
1360
1383
  const rootFilename = rootTypeName.toLowerCase();
1361
1384
  if (rootFilename !== "validation-result") {
@@ -1374,12 +1397,13 @@ var buildValidatorSchema = async (rootSchema, rootTypeName) => {
1374
1397
  console.warn(`Warning: Could not resolve ref: ${ref}`);
1375
1398
  continue;
1376
1399
  }
1377
- const typeName = refToName(ref);
1400
+ const typeName = refToName(ref, typeSuffix);
1378
1401
  const filename = refToFilename(ref);
1379
1402
  const processedSchema = resolveDynamicRefs(resolvedSchema, dynamicRefMap);
1380
1403
  const content = generateValidatorFile(processedSchema, typeName, {
1381
1404
  selfRef: ref,
1382
- rootSchema
1405
+ rootSchema,
1406
+ typeSuffix
1383
1407
  });
1384
1408
  if (filename !== "validation-result" && !processedFilenames.has(filename)) {
1385
1409
  processedFilenames.add(filename);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-validators",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Generate TypeScript validation functions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "json-schema-typed": "^8.0.1",
50
- "@amritk/helpers": "0.4.0"
50
+ "@amritk/helpers": "0.6.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@scalar/openapi-parser": "^0.26.1"
@@ -74,7 +74,7 @@ describe('build-schema', () => {
74
74
  const documentFile = files.find((f) => f.filename === 'document.ts')
75
75
 
76
76
  expect(documentFile?.content).toContain("from './info'")
77
- expect(documentFile?.content).toContain('validateInfoObject')
77
+ expect(documentFile?.content).toContain('validateInfo')
78
78
  })
79
79
 
80
80
  it('generates a valid index.ts with re-exports', async () => {
@@ -136,7 +136,7 @@ describe('build-schema', () => {
136
136
  const infoFile = files.find((f) => f.filename === 'info.ts')
137
137
 
138
138
  expect(documentFile?.content).toContain('validateDocument')
139
- expect(infoFile?.content).toContain('validateInfoObject')
139
+ expect(infoFile?.content).toContain('validateInfo')
140
140
 
141
141
  // Cross-check: @scalar/openapi-parser says a complete document is valid
142
142
  const validDoc = { openapi: '3.1.0', info: { title: 'API', version: '1.0' }, paths: {} }
@@ -55,7 +55,11 @@ export type ValidationResult = true | { valid: false; errors: ValidationError[]
55
55
  * // files → [{ filename: 'document.ts', content: '...' }, { filename: 'info.ts', ... }, ...]
56
56
  * ```
57
57
  */
58
- export const buildValidatorSchema = async (rootSchema: JSONSchema, rootTypeName: string): Promise<GeneratedFile[]> => {
58
+ export const buildValidatorSchema = async (
59
+ rootSchema: JSONSchema,
60
+ rootTypeName: string,
61
+ typeSuffix = '',
62
+ ): Promise<GeneratedFile[]> => {
59
63
  rootSchema = upgradeDraft07Schema(rootSchema as Record<string, unknown>) as JSONSchema
60
64
 
61
65
  const files: GeneratedFile[] = []
@@ -69,6 +73,7 @@ export const buildValidatorSchema = async (rootSchema: JSONSchema, rootTypeName:
69
73
  const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap)
70
74
  const rootContent = generateValidatorFile(processedRootSchema, rootTypeName, {
71
75
  rootSchema: rootSchema as Record<string, unknown>,
76
+ typeSuffix,
72
77
  })
73
78
  const rootFilename = rootTypeName.toLowerCase()
74
79
 
@@ -91,12 +96,13 @@ export const buildValidatorSchema = async (rootSchema: JSONSchema, rootTypeName:
91
96
  continue
92
97
  }
93
98
 
94
- const typeName = refToName(ref)
99
+ const typeName = refToName(ref, typeSuffix)
95
100
  const filename = refToFilename(ref)
96
101
  const processedSchema = resolveDynamicRefs(resolvedSchema as JSONSchema, dynamicRefMap)
97
102
  const content = generateValidatorFile(processedSchema, typeName, {
98
103
  selfRef: ref,
99
104
  rootSchema: rootSchema as Record<string, unknown>,
105
+ typeSuffix,
100
106
  })
101
107
 
102
108
  if (filename !== 'validation-result' && !processedFilenames.has(filename)) {
@@ -18,15 +18,20 @@ type CollectValidatorImportsOptions = {
18
18
  * are excluded from the import list (they were never generated as files).
19
19
  */
20
20
  readonly rootSchema?: Record<string, unknown> | undefined
21
+ /**
22
+ * Suffix appended to every type/validator name derived from a `$ref`. Must
23
+ * match the suffix used when generating the referenced files. Defaults to `''`.
24
+ */
25
+ readonly typeSuffix?: string
21
26
  }
22
27
 
23
28
  /**
24
29
  * Generates an import statement for a single $ref, importing both the type
25
30
  * and the validator function from the ref's generated file.
26
31
  */
27
- const buildImport = (ref: string): string => {
32
+ const buildImport = (ref: string, suffix: string): string => {
28
33
  const filename = refToFilename(ref)
29
- const typeName = refToName(ref)
34
+ const typeName = refToName(ref, suffix)
30
35
  const validatorName = `validate${typeName}`
31
36
  return `import { type ${typeName}, ${validatorName} } from './${filename}'`
32
37
  }
@@ -94,12 +99,13 @@ const collectDirectRefs = (schema: JSONSchema): string[] => {
94
99
  * ```typescript
95
100
  * const schema = { properties: { contact: { $ref: '#/$defs/contact' } } }
96
101
  * collectValidatorImports(schema)
97
- * // ["import { type ContactObject, validateContactObject } from './contact-object'"]
102
+ * // ["import { type Contact, validateContact } from './contact'"]
98
103
  * ```
99
104
  */
100
105
  export const collectValidatorImports = (schema: JSONSchema, options?: CollectValidatorImportsOptions): string[] => {
101
106
  const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null
102
107
  const rootSchema = options?.rootSchema
108
+ const typeSuffix = options?.typeSuffix ?? ''
103
109
 
104
110
  const refs = collectDirectRefs(schema)
105
111
  const seen = new Set<string>()
@@ -121,7 +127,7 @@ export const collectValidatorImports = (schema: JSONSchema, options?: CollectVal
121
127
 
122
128
  // -or-reference unions import the base type's validator
123
129
  const importRef = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref
124
- imports.push(buildImport(importRef))
130
+ imports.push(buildImport(importRef, typeSuffix))
125
131
  }
126
132
 
127
133
  return imports
@@ -17,6 +17,11 @@ type GenerateValidatorFileOptions = {
17
17
  * The root schema document. Used to filter out unresolvable refs.
18
18
  */
19
19
  readonly rootSchema?: Record<string, unknown>
20
+ /**
21
+ * Suffix appended to every type/validator name derived from a `$ref`.
22
+ * Defaults to `''` (no suffix).
23
+ */
24
+ readonly typeSuffix?: string
20
25
  }
21
26
 
22
27
  /**
@@ -46,13 +51,15 @@ export const generateValidatorFile = (
46
51
  typeName: string,
47
52
  options?: GenerateValidatorFileOptions,
48
53
  ): string => {
54
+ const typeSuffix = options?.typeSuffix ?? ''
49
55
  const refImports = collectValidatorImports(schema, {
50
56
  selfRef: options?.selfRef,
51
57
  rootSchema: options?.rootSchema,
58
+ typeSuffix,
52
59
  })
53
60
 
54
- const typeDefinition = generateTypeDefinition(schema, typeName)
55
- const validatorFunction = generateValidatorFunction(schema, typeName)
61
+ const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix })
62
+ const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix)
56
63
 
57
64
  let result = `import type { ValidationResult, ValidationError } from './validation-result'\n`
58
65
 
@@ -135,7 +135,7 @@ describe('generate-validator-function', () => {
135
135
  }
136
136
  const code = generateValidatorFunction(schema, 'Document')
137
137
 
138
- expect(code).toContain('validateInfoObject(')
138
+ expect(code).toContain('validateInfo(')
139
139
  expect(code).toContain('"info" in obj')
140
140
  })
141
141
 
@@ -147,6 +147,18 @@ describe('generate-validator-function', () => {
147
147
  expect(code).toContain('must be string')
148
148
  })
149
149
 
150
+ it('accumulates all constraint errors for a scalar string schema', () => {
151
+ const schema = { type: 'string' as const, pattern: '^\\d+$', minLength: 2, maxLength: 4 }
152
+ const code = generateValidatorFunction(schema, 'Code')
153
+
154
+ // All three constraints push onto a shared errors array instead of returning early
155
+ expect(code).toContain('const errors: ValidationError[] = []')
156
+ expect(code).toContain("errors.push({ message: 'must match pattern")
157
+ expect(code).toContain("errors.push({ message: 'must have at least 2 characters'")
158
+ expect(code).toContain("errors.push({ message: 'must have at most 4 characters'")
159
+ expect(code).toContain('return errors.length > 0 ? { valid: false, errors } : true')
160
+ })
161
+
150
162
  it('returns true for empty object schemas', () => {
151
163
  const schema = { type: 'object' as const }
152
164
  const code = generateValidatorFunction(schema, 'Empty')
@@ -61,7 +61,7 @@ const wrongTypeCondition = (accessor: string, type: string): string => {
61
61
  * Generates validation lines for a single property in an object schema.
62
62
  * Handles $ref delegation, enum checks, type checks, and string/number constraints.
63
63
  */
64
- const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired: boolean): string[] => {
64
+ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired: boolean, suffix: string): string[] => {
65
65
  if (!isSchemaObject(propSchema)) return []
66
66
 
67
67
  const raw = `obj[${JSON.stringify(key)}]`
@@ -71,7 +71,7 @@ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired:
71
71
  // $ref — delegate to the imported validator
72
72
  if (hasRef(propSchema)) {
73
73
  const ref = propSchema.$ref
74
- const vName = validatorName(refToName(ref))
74
+ const vName = validatorName(refToName(ref, suffix))
75
75
 
76
76
  if (isRequired) {
77
77
  lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
@@ -218,7 +218,7 @@ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired:
218
218
  if (t === 'array' && hasItems(propSchema)) {
219
219
  const itemSchema = propSchema.items
220
220
  if (hasRef(itemSchema)) {
221
- const vName = validatorName(refToName(itemSchema.$ref))
221
+ const vName = validatorName(refToName(itemSchema.$ref, suffix))
222
222
  lines.push(` if (Array.isArray(${raw})) {`)
223
223
  lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`)
224
224
  lines.push(` const _ir = ${vName}(${raw}[_i], \`${path.slice(1, -1)}/${key}/\${_i}\`)`)
@@ -250,7 +250,7 @@ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired:
250
250
  * Generates a validator function body for an object schema, checking each
251
251
  * property's presence and type and collecting all errors.
252
252
  */
253
- const generateObjectValidator = (schema: JSONSchema, typeName: string): string => {
253
+ const generateObjectValidator = (schema: JSONSchema, typeName: string, suffix: string): string => {
254
254
  const vName = validatorName(typeName)
255
255
  const required = new Set(hasRequired(schema) ? schema.required : [])
256
256
  const properties = hasProperties(schema) ? schema.properties : {}
@@ -258,7 +258,7 @@ const generateObjectValidator = (schema: JSONSchema, typeName: string): string =
258
258
  const propertyLines: string[] = []
259
259
 
260
260
  for (const [key, propSchema] of Object.entries(properties)) {
261
- const checks = generatePropertyChecks(key, propSchema as JSONSchema, required.has(key))
261
+ const checks = generatePropertyChecks(key, propSchema as JSONSchema, required.has(key), suffix)
262
262
  if (checks.length > 0) {
263
263
  propertyLines.push(...checks)
264
264
  }
@@ -270,7 +270,7 @@ const generateObjectValidator = (schema: JSONSchema, typeName: string): string =
270
270
  isSchemaObject(schema.additionalProperties) &&
271
271
  hasRef(schema.additionalProperties)
272
272
  ) {
273
- const vRefName = validatorName(refToName(schema.additionalProperties.$ref))
273
+ const vRefName = validatorName(refToName(schema.additionalProperties.$ref, suffix))
274
274
  propertyLines.push(` for (const _key of Object.keys(obj)) {`)
275
275
  propertyLines.push(` if (${JSON.stringify(Object.keys(properties))}.includes(_key)) continue`)
276
276
  propertyLines.push(` const _r = ${vRefName}(obj[_key as keyof typeof obj], \`\${_path}/\${_key}\`)`)
@@ -297,7 +297,7 @@ const generateObjectValidator = (schema: JSONSchema, typeName: string): string =
297
297
  /**
298
298
  * Generates a validator function for a non-object schema (primitive, array, enum, $ref).
299
299
  */
300
- const generateScalarValidator = (schema: JSONSchema, typeName: string): string => {
300
+ const generateScalarValidator = (schema: JSONSchema, typeName: string, suffix: string): string => {
301
301
  const vName = validatorName(typeName)
302
302
 
303
303
  if (!isSchemaObject(schema)) {
@@ -308,7 +308,7 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
308
308
 
309
309
  // Top-level $ref — delegate entirely
310
310
  if (hasRef(schema)) {
311
- const delegateName = validatorName(refToName(schema.$ref))
311
+ const delegateName = validatorName(refToName(schema.$ref, suffix))
312
312
  return [
313
313
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
314
314
  ` return ${delegateName}(input, _path)`,
@@ -361,7 +361,7 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
361
361
  const branches = schema.oneOf
362
362
  .map((branch, i) => {
363
363
  if (!hasRef(branch)) return null
364
- const bName = validatorName(refToName((branch as { $ref: string }).$ref))
364
+ const bName = validatorName(refToName((branch as { $ref: string }).$ref, suffix))
365
365
  return ` const _r${i} = ${bName}(input, _path)\n if (_r${i} === true) return true`
366
366
  })
367
367
  .filter(Boolean)
@@ -386,42 +386,54 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
386
386
  if (t === 'string') {
387
387
  if (hasPattern(schema)) {
388
388
  constraintLines.push(` if (typeof input === 'string' && !/${schema.pattern}/.test(input)) {`)
389
- constraintLines.push(
390
- ` return { valid: false, errors: [{ message: 'must match pattern ${schema.pattern}', path: _path }] }`,
391
- )
389
+ constraintLines.push(` errors.push({ message: 'must match pattern ${schema.pattern}', path: _path })`)
392
390
  constraintLines.push(` }`)
393
391
  }
394
392
  if (hasMinLength(schema)) {
395
393
  constraintLines.push(` if (typeof input === 'string' && input.length < ${schema.minLength}) {`)
396
394
  constraintLines.push(
397
- ` return { valid: false, errors: [{ message: 'must have at least ${schema.minLength} characters', path: _path }] }`,
395
+ ` errors.push({ message: 'must have at least ${schema.minLength} characters', path: _path })`,
398
396
  )
399
397
  constraintLines.push(` }`)
400
398
  }
401
399
  if (hasMaxLength(schema)) {
402
400
  constraintLines.push(` if (typeof input === 'string' && input.length > ${schema.maxLength}) {`)
403
401
  constraintLines.push(
404
- ` return { valid: false, errors: [{ message: 'must have at most ${schema.maxLength} characters', path: _path }] }`,
402
+ ` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`,
405
403
  )
406
404
  constraintLines.push(` }`)
407
405
  }
408
406
  }
409
407
 
410
- const body = constraintLines.length > 0 ? '\n' + constraintLines.join('\n') + '\n' : ''
411
-
412
- return wrongType
413
- ? [
414
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
415
- ` if (${wrongType}) {`,
416
- ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
417
- ` }`,
418
- body,
419
- ` return true`,
420
- `}`,
421
- ].join('\n')
422
- : [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(
423
- '\n',
424
- )
408
+ if (!wrongType) {
409
+ return [
410
+ `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
411
+ ` return true`,
412
+ `}`,
413
+ ].join('\n')
414
+ }
415
+
416
+ if (constraintLines.length === 0) {
417
+ return [
418
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
419
+ ` if (${wrongType}) {`,
420
+ ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
421
+ ` }`,
422
+ ` return true`,
423
+ `}`,
424
+ ].join('\n')
425
+ }
426
+
427
+ return [
428
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
429
+ ` if (${wrongType}) {`,
430
+ ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
431
+ ` }`,
432
+ ` const errors: ValidationError[] = []`,
433
+ constraintLines.join('\n'),
434
+ ` return errors.length > 0 ? { valid: false, errors } : true`,
435
+ `}`,
436
+ ].join('\n')
425
437
  }
426
438
 
427
439
  return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(
@@ -451,10 +463,10 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
451
463
  * // }
452
464
  * ```
453
465
  */
454
- export const generateValidatorFunction = (schema: JSONSchema, typeName: string): string => {
466
+ export const generateValidatorFunction = (schema: JSONSchema, typeName: string, suffix = ''): string => {
455
467
  if (isObjectSchema(schema)) {
456
- return generateObjectValidator(schema, typeName)
468
+ return generateObjectValidator(schema, typeName, suffix)
457
469
  }
458
470
 
459
- return generateScalarValidator(schema, typeName)
471
+ return generateScalarValidator(schema, typeName, suffix)
460
472
  }