@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.
- package/dist/generators/build-schema.d.ts +1 -1
- package/dist/generators/collect-validator-imports.d.ts +6 -1
- package/dist/generators/generate-files.d.ts +5 -0
- package/dist/generators/generate-validator-function.d.ts +1 -1
- package/dist/index.js +103 -79
- package/package.json +2 -2
- package/src/generators/build-schema.test.ts +2 -2
- package/src/generators/build-schema.ts +8 -2
- package/src/generators/collect-validator-imports.ts +10 -4
- package/src/generators/generate-files.ts +9 -2
- package/src/generators/generate-validator-function.test.ts +13 -1
- package/src/generators/generate-validator-function.ts +44 -32
|
@@ -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
|
|
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 +
|
|
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 +
|
|
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
|
|
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
|
|
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
|
|
604
|
+
return recordType("string", getBooleanSubSchemaType(schema.additionalProperties), options);
|
|
604
605
|
}
|
|
605
|
-
return
|
|
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 `
|
|
615
|
+
return recordType("`x-${string}`", valueType, options);
|
|
615
616
|
}
|
|
616
|
-
return
|
|
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
|
|
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 `
|
|
703
|
+
return recordType("`x-${string}`", valueType, options);
|
|
702
704
|
}
|
|
703
|
-
return
|
|
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} =
|
|
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(`
|
|
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(`
|
|
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(`
|
|
1279
|
+
constraintLines.push(` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`);
|
|
1276
1280
|
constraintLines.push(` }`);
|
|
1277
1281
|
}
|
|
1278
1282
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
`)
|
|
1282
|
-
`
|
|
1283
|
-
|
|
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
|
-
|
|
1289
|
-
`
|
|
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
|
+
"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.
|
|
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('
|
|
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('
|
|
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 (
|
|
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
|
|
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('
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
}
|