@amritk/generate-validators 0.2.3 → 0.3.1
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/index.js
CHANGED
|
@@ -351,6 +351,31 @@ var isSchemaObject2 = (schema) => {
|
|
|
351
351
|
var isObjectSchema = (schema) => {
|
|
352
352
|
return isSchemaObject2(schema) && (("type" in schema) && schema.type === "object" || ("properties" in schema));
|
|
353
353
|
};
|
|
354
|
+
var MJST_EXTENSION_KEY = "x-mjst";
|
|
355
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
356
|
+
var SUPPORTED_PRIMITIVES = new Set(["bigint"]);
|
|
357
|
+
var SAFE_BRAND = /^[\w$ -]+$/;
|
|
358
|
+
var readExtensionString = (schema, field) => {
|
|
359
|
+
if (!isSchemaObject2(schema))
|
|
360
|
+
return;
|
|
361
|
+
const extension = schema[MJST_EXTENSION_KEY];
|
|
362
|
+
if (typeof extension !== "object" || extension === null)
|
|
363
|
+
return;
|
|
364
|
+
const value = extension[field];
|
|
365
|
+
return typeof value === "string" ? value : undefined;
|
|
366
|
+
};
|
|
367
|
+
var getMjstInstanceOf = (schema) => {
|
|
368
|
+
const instanceOf = readExtensionString(schema, "instanceOf");
|
|
369
|
+
return instanceOf !== undefined && IDENTIFIER.test(instanceOf) ? instanceOf : undefined;
|
|
370
|
+
};
|
|
371
|
+
var getMjstPrimitive = (schema) => {
|
|
372
|
+
const primitive = readExtensionString(schema, "primitive");
|
|
373
|
+
return primitive !== undefined && SUPPORTED_PRIMITIVES.has(primitive) ? primitive : undefined;
|
|
374
|
+
};
|
|
375
|
+
var getMjstBrand = (schema) => {
|
|
376
|
+
const brand = readExtensionString(schema, "brand");
|
|
377
|
+
return brand !== undefined && SAFE_BRAND.test(brand) ? brand : undefined;
|
|
378
|
+
};
|
|
354
379
|
var toKebabCase4 = (value) => value.replace(/OAuth/g, "Oauth").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase();
|
|
355
380
|
var uriRefToFilename4 = (uri) => {
|
|
356
381
|
const hashIndex = uri.indexOf("#");
|
|
@@ -491,13 +516,27 @@ var buildJsDocBlock = (title, description, commentUrl) => {
|
|
|
491
516
|
`;
|
|
492
517
|
return block;
|
|
493
518
|
};
|
|
494
|
-
var getTypeScriptType = (schema) => {
|
|
519
|
+
var getTypeScriptType = (schema, options = {}) => {
|
|
520
|
+
const base = getUnbrandedType(schema, options);
|
|
521
|
+
const brand = getMjstBrand(schema);
|
|
522
|
+
return brand ? `(${base} & { readonly __brand: '${brand}' })` : base;
|
|
523
|
+
};
|
|
524
|
+
var recordType = (keyType, valueType, options) => options.readonly ? `Readonly<Record<${keyType}, ${valueType}>>` : `Record<${keyType}, ${valueType}>`;
|
|
525
|
+
var getUnbrandedType = (schema, options = {}) => {
|
|
495
526
|
if (typeof schema === "boolean") {
|
|
496
527
|
return getBooleanSubSchemaType(schema);
|
|
497
528
|
}
|
|
498
529
|
if (typeof schema !== "object" || schema === null) {
|
|
499
530
|
return "unknown";
|
|
500
531
|
}
|
|
532
|
+
const instanceOf = getMjstInstanceOf(schema);
|
|
533
|
+
if (instanceOf) {
|
|
534
|
+
return instanceOf;
|
|
535
|
+
}
|
|
536
|
+
const primitive = getMjstPrimitive(schema);
|
|
537
|
+
if (primitive) {
|
|
538
|
+
return primitive;
|
|
539
|
+
}
|
|
501
540
|
if (schema.$ref) {
|
|
502
541
|
if (!schema.$ref.startsWith("#")) {
|
|
503
542
|
return "unknown";
|
|
@@ -531,29 +570,29 @@ var getTypeScriptType = (schema) => {
|
|
|
531
570
|
return multiEnumUnion;
|
|
532
571
|
}
|
|
533
572
|
if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
|
|
534
|
-
let oneOfUnion = getTypeScriptType(schema.oneOf[0]);
|
|
573
|
+
let oneOfUnion = getTypeScriptType(schema.oneOf[0], options);
|
|
535
574
|
for (let i = 1;i < schema.oneOf.length; i++) {
|
|
536
|
-
oneOfUnion += " | " + getTypeScriptType(schema.oneOf[i]);
|
|
575
|
+
oneOfUnion += " | " + getTypeScriptType(schema.oneOf[i], options);
|
|
537
576
|
}
|
|
538
577
|
return oneOfUnion;
|
|
539
578
|
}
|
|
540
579
|
if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
|
|
541
|
-
let anyOfUnion = getTypeScriptType(schema.anyOf[0]);
|
|
580
|
+
let anyOfUnion = getTypeScriptType(schema.anyOf[0], options);
|
|
542
581
|
for (let i = 1;i < schema.anyOf.length; i++) {
|
|
543
|
-
anyOfUnion += " | " + getTypeScriptType(schema.anyOf[i]);
|
|
582
|
+
anyOfUnion += " | " + getTypeScriptType(schema.anyOf[i], options);
|
|
544
583
|
}
|
|
545
584
|
return anyOfUnion;
|
|
546
585
|
}
|
|
547
586
|
if (schema.allOf && Array.isArray(schema.allOf) && schema.allOf.length > 0) {
|
|
548
|
-
let intersectionTypes = getTypeScriptType(schema.allOf[0]);
|
|
587
|
+
let intersectionTypes = getTypeScriptType(schema.allOf[0], options);
|
|
549
588
|
for (let i = 1;i < schema.allOf.length; i++) {
|
|
550
|
-
intersectionTypes += " & " + getTypeScriptType(schema.allOf[i]);
|
|
589
|
+
intersectionTypes += " & " + getTypeScriptType(schema.allOf[i], options);
|
|
551
590
|
}
|
|
552
591
|
return intersectionTypes;
|
|
553
592
|
}
|
|
554
593
|
const conditionalResult = getConditionalObjectSchema(schema);
|
|
555
594
|
if (conditionalResult) {
|
|
556
|
-
const baseType = getTypeScriptType(conditionalResult.schema);
|
|
595
|
+
const baseType = getTypeScriptType(conditionalResult.schema, options);
|
|
557
596
|
if (conditionalResult.thenRef) {
|
|
558
597
|
return `(${baseType}) & ${refToName2(conditionalResult.thenRef)}`;
|
|
559
598
|
}
|
|
@@ -562,20 +601,20 @@ var getTypeScriptType = (schema) => {
|
|
|
562
601
|
if (!schema.type) {
|
|
563
602
|
if (schema.additionalProperties !== undefined) {
|
|
564
603
|
if (typeof schema.additionalProperties === "boolean") {
|
|
565
|
-
return
|
|
604
|
+
return recordType("string", getBooleanSubSchemaType(schema.additionalProperties), options);
|
|
566
605
|
}
|
|
567
|
-
return
|
|
606
|
+
return recordType("string", getTypeScriptType(schema.additionalProperties, options), options);
|
|
568
607
|
}
|
|
569
608
|
if (schema.patternProperties && typeof schema.patternProperties === "object") {
|
|
570
609
|
const firstEntry = Object.entries(schema.patternProperties)[0];
|
|
571
610
|
if (firstEntry) {
|
|
572
611
|
const [pattern, value] = firstEntry;
|
|
573
612
|
if (value !== undefined) {
|
|
574
|
-
const valueType = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value);
|
|
613
|
+
const valueType = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value, options);
|
|
575
614
|
if (pattern === "^x-") {
|
|
576
|
-
return `
|
|
615
|
+
return recordType("`x-${string}`", valueType, options);
|
|
577
616
|
}
|
|
578
|
-
return
|
|
617
|
+
return recordType("string", valueType, options);
|
|
579
618
|
}
|
|
580
619
|
}
|
|
581
620
|
}
|
|
@@ -628,41 +667,42 @@ var getTypeScriptType = (schema) => {
|
|
|
628
667
|
return "boolean";
|
|
629
668
|
case "array":
|
|
630
669
|
if (schema.items) {
|
|
631
|
-
const itemType = getTypeScriptType(schema.items);
|
|
670
|
+
const itemType = getTypeScriptType(schema.items, options);
|
|
632
671
|
const wrappedItemType = itemType.includes(" | ") ? `(${itemType})` : itemType;
|
|
633
|
-
return `${wrappedItemType}[]`;
|
|
672
|
+
return options.readonly ? `readonly ${wrappedItemType}[]` : `${wrappedItemType}[]`;
|
|
634
673
|
}
|
|
635
|
-
return "unknown[]";
|
|
674
|
+
return options.readonly ? "readonly unknown[]" : "unknown[]";
|
|
636
675
|
case "object":
|
|
637
676
|
if (schema.properties) {
|
|
677
|
+
const readonlyPrefix = options.readonly ? "readonly " : "";
|
|
638
678
|
let properties = "";
|
|
639
679
|
let first = true;
|
|
640
680
|
for (const key in schema.properties) {
|
|
641
681
|
const propSchema = schema.properties[key];
|
|
642
682
|
const isRequired = schema.required?.includes(key) ?? false;
|
|
643
683
|
const optional = isRequired ? "" : "?";
|
|
644
|
-
const propType = getTypeScriptType(propSchema);
|
|
684
|
+
const propType = getTypeScriptType(propSchema, options);
|
|
645
685
|
if (!first)
|
|
646
686
|
properties += "; ";
|
|
647
|
-
properties += safeKey(key) + optional + ": " + propType;
|
|
687
|
+
properties += readonlyPrefix + safeKey(key) + optional + ": " + propType;
|
|
648
688
|
first = false;
|
|
649
689
|
}
|
|
650
690
|
return "{ " + properties + " }";
|
|
651
691
|
}
|
|
652
692
|
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
653
|
-
const additionalPropType = getTypeScriptType(schema.additionalProperties);
|
|
654
|
-
return
|
|
693
|
+
const additionalPropType = getTypeScriptType(schema.additionalProperties, options);
|
|
694
|
+
return recordType("string", additionalPropType, options);
|
|
655
695
|
}
|
|
656
696
|
if (schema.patternProperties && typeof schema.patternProperties === "object") {
|
|
657
697
|
const firstEntry = Object.entries(schema.patternProperties)[0];
|
|
658
698
|
if (firstEntry) {
|
|
659
699
|
const [pattern, patternVal] = firstEntry;
|
|
660
700
|
if (patternVal) {
|
|
661
|
-
const valueType = getTypeScriptType(patternVal);
|
|
701
|
+
const valueType = getTypeScriptType(patternVal, options);
|
|
662
702
|
if (pattern === "^x-") {
|
|
663
|
-
return `
|
|
703
|
+
return recordType("`x-${string}`", valueType, options);
|
|
664
704
|
}
|
|
665
|
-
return
|
|
705
|
+
return recordType("string", valueType, options);
|
|
666
706
|
}
|
|
667
707
|
}
|
|
668
708
|
}
|
|
@@ -671,9 +711,10 @@ var getTypeScriptType = (schema) => {
|
|
|
671
711
|
return "unknown";
|
|
672
712
|
}
|
|
673
713
|
};
|
|
674
|
-
var generateTypeDefinition = (schema, typeName) => {
|
|
714
|
+
var generateTypeDefinition = (schema, typeName, options = {}) => {
|
|
715
|
+
const readonlyPrefix = options.readonly ? "readonly " : "";
|
|
675
716
|
if (!isObjectLikeSchema(schema)) {
|
|
676
|
-
const tsType = getTypeScriptType(schema);
|
|
717
|
+
const tsType = getTypeScriptType(schema, options);
|
|
677
718
|
let result = "";
|
|
678
719
|
if (isSchemaObject2(schema) && schema.$comment && typeof schema.$comment === "string") {
|
|
679
720
|
result += buildJsDocBlock(typeName, schema.$comment);
|
|
@@ -701,23 +742,23 @@ var generateTypeDefinition = (schema, typeName) => {
|
|
|
701
742
|
if (firstPatternProperty === undefined) {
|
|
702
743
|
return `export type ${typeName} = Record<string, unknown>;`;
|
|
703
744
|
}
|
|
704
|
-
const patternPropType = typeof firstPatternProperty === "boolean" ? getBooleanSubSchemaType(firstPatternProperty) : getTypeScriptType(firstPatternProperty);
|
|
745
|
+
const patternPropType = typeof firstPatternProperty === "boolean" ? getBooleanSubSchemaType(firstPatternProperty) : getTypeScriptType(firstPatternProperty, options);
|
|
705
746
|
const keyType = firstPattern === "^x-" ? "`x-${string}`" : "string";
|
|
706
747
|
let result2 = "";
|
|
707
748
|
if (jsDocTitle && jsDocDescription) {
|
|
708
749
|
result2 += buildJsDocBlock(jsDocTitle, jsDocDescription);
|
|
709
750
|
}
|
|
710
|
-
result2 += `export type ${typeName} =
|
|
751
|
+
result2 += `export type ${typeName} = ${recordType(keyType, patternPropType, options)};`;
|
|
711
752
|
return result2;
|
|
712
753
|
}
|
|
713
754
|
if (!hasProperties2 && hasAdditionalProperties2 && normalizedSchema.additionalProperties) {
|
|
714
|
-
const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties);
|
|
755
|
+
const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties, options);
|
|
715
756
|
let result2 = "";
|
|
716
757
|
if (jsDocTitle && jsDocDescription) {
|
|
717
758
|
result2 += buildJsDocBlock(jsDocTitle, jsDocDescription);
|
|
718
759
|
}
|
|
719
760
|
result2 += `export type ${typeName} = {
|
|
720
|
-
[key: string]: ${additionalPropType};
|
|
761
|
+
${readonlyPrefix}[key: string]: ${additionalPropType};
|
|
721
762
|
};`;
|
|
722
763
|
return result2;
|
|
723
764
|
}
|
|
@@ -728,8 +769,8 @@ var generateTypeDefinition = (schema, typeName) => {
|
|
|
728
769
|
const propSchema = schemaProps[key];
|
|
729
770
|
const isRequired = normalizedSchema.required?.includes(key) ?? false;
|
|
730
771
|
const optional = isRequired ? "" : "?";
|
|
731
|
-
const propType = getTypeScriptType(propSchema);
|
|
732
|
-
const quotedKey = safeKey(key);
|
|
772
|
+
const propType = getTypeScriptType(propSchema, options);
|
|
773
|
+
const quotedKey = readonlyPrefix + safeKey(key);
|
|
733
774
|
if (!isFirstProp)
|
|
734
775
|
properties += `
|
|
735
776
|
`;
|
|
@@ -903,6 +944,31 @@ var collectValidatorImports = (schema, options) => {
|
|
|
903
944
|
return imports;
|
|
904
945
|
};
|
|
905
946
|
|
|
947
|
+
// ../helpers/dist/mjst-extension.js
|
|
948
|
+
var isSchemaObject4 = (schema) => {
|
|
949
|
+
return typeof schema === "object" && schema !== null && typeof schema !== "boolean";
|
|
950
|
+
};
|
|
951
|
+
var MJST_EXTENSION_KEY2 = "x-mjst";
|
|
952
|
+
var IDENTIFIER2 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
953
|
+
var SUPPORTED_PRIMITIVES2 = new Set(["bigint"]);
|
|
954
|
+
var readExtensionString2 = (schema, field) => {
|
|
955
|
+
if (!isSchemaObject4(schema))
|
|
956
|
+
return;
|
|
957
|
+
const extension = schema[MJST_EXTENSION_KEY2];
|
|
958
|
+
if (typeof extension !== "object" || extension === null)
|
|
959
|
+
return;
|
|
960
|
+
const value = extension[field];
|
|
961
|
+
return typeof value === "string" ? value : undefined;
|
|
962
|
+
};
|
|
963
|
+
var getMjstInstanceOf2 = (schema) => {
|
|
964
|
+
const instanceOf = readExtensionString2(schema, "instanceOf");
|
|
965
|
+
return instanceOf !== undefined && IDENTIFIER2.test(instanceOf) ? instanceOf : undefined;
|
|
966
|
+
};
|
|
967
|
+
var getMjstPrimitive2 = (schema) => {
|
|
968
|
+
const primitive = readExtensionString2(schema, "primitive");
|
|
969
|
+
return primitive !== undefined && SUPPORTED_PRIMITIVES2.has(primitive) ? primitive : undefined;
|
|
970
|
+
};
|
|
971
|
+
|
|
906
972
|
// src/generators/generate-validator-function.ts
|
|
907
973
|
var validatorName = (typeName) => `validate${typeName}`;
|
|
908
974
|
var typeofString = (type) => {
|
|
@@ -951,6 +1017,36 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
|
|
|
951
1017
|
}
|
|
952
1018
|
return lines;
|
|
953
1019
|
}
|
|
1020
|
+
const instanceOf = getMjstInstanceOf2(propSchema);
|
|
1021
|
+
if (instanceOf) {
|
|
1022
|
+
if (isRequired) {
|
|
1023
|
+
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
|
|
1024
|
+
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
|
|
1025
|
+
lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
|
|
1026
|
+
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
1027
|
+
lines.push(` }`);
|
|
1028
|
+
} else {
|
|
1029
|
+
lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`);
|
|
1030
|
+
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
1031
|
+
lines.push(` }`);
|
|
1032
|
+
}
|
|
1033
|
+
return lines;
|
|
1034
|
+
}
|
|
1035
|
+
const primitive = getMjstPrimitive2(propSchema);
|
|
1036
|
+
if (primitive) {
|
|
1037
|
+
if (isRequired) {
|
|
1038
|
+
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
|
|
1039
|
+
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
|
|
1040
|
+
lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
|
|
1041
|
+
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
1042
|
+
lines.push(` }`);
|
|
1043
|
+
} else {
|
|
1044
|
+
lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`);
|
|
1045
|
+
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
1046
|
+
lines.push(` }`);
|
|
1047
|
+
}
|
|
1048
|
+
return lines;
|
|
1049
|
+
}
|
|
954
1050
|
if (hasEnum(propSchema)) {
|
|
955
1051
|
const allowed = JSON.stringify(propSchema.enum);
|
|
956
1052
|
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
@@ -1105,6 +1201,30 @@ var generateScalarValidator = (schema, typeName) => {
|
|
|
1105
1201
|
` return ${delegateName}(input, _path)`,
|
|
1106
1202
|
`}`
|
|
1107
1203
|
].join(`
|
|
1204
|
+
`);
|
|
1205
|
+
}
|
|
1206
|
+
const instanceOf = getMjstInstanceOf2(schema);
|
|
1207
|
+
if (instanceOf) {
|
|
1208
|
+
return [
|
|
1209
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1210
|
+
` if (!(input instanceof ${instanceOf})) {`,
|
|
1211
|
+
` return { valid: false, errors: [{ message: 'must be ${instanceOf}', path: _path }] }`,
|
|
1212
|
+
` }`,
|
|
1213
|
+
` return true`,
|
|
1214
|
+
`}`
|
|
1215
|
+
].join(`
|
|
1216
|
+
`);
|
|
1217
|
+
}
|
|
1218
|
+
const primitive = getMjstPrimitive2(schema);
|
|
1219
|
+
if (primitive) {
|
|
1220
|
+
return [
|
|
1221
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1222
|
+
` if (typeof input !== "${primitive}") {`,
|
|
1223
|
+
` return { valid: false, errors: [{ message: 'must be ${primitive}', path: _path }] }`,
|
|
1224
|
+
` }`,
|
|
1225
|
+
` return true`,
|
|
1226
|
+
`}`
|
|
1227
|
+
].join(`
|
|
1108
1228
|
`);
|
|
1109
1229
|
}
|
|
1110
1230
|
if (hasEnum(schema)) {
|
|
@@ -1145,34 +1265,50 @@ var generateScalarValidator = (schema, typeName) => {
|
|
|
1145
1265
|
if (t === "string") {
|
|
1146
1266
|
if (hasPattern(schema)) {
|
|
1147
1267
|
constraintLines.push(` if (typeof input === 'string' && !/${schema.pattern}/.test(input)) {`);
|
|
1148
|
-
constraintLines.push(`
|
|
1268
|
+
constraintLines.push(` errors.push({ message: 'must match pattern ${schema.pattern}', path: _path })`);
|
|
1149
1269
|
constraintLines.push(` }`);
|
|
1150
1270
|
}
|
|
1151
1271
|
if (hasMinLength(schema)) {
|
|
1152
1272
|
constraintLines.push(` if (typeof input === 'string' && input.length < ${schema.minLength}) {`);
|
|
1153
|
-
constraintLines.push(`
|
|
1273
|
+
constraintLines.push(` errors.push({ message: 'must have at least ${schema.minLength} characters', path: _path })`);
|
|
1154
1274
|
constraintLines.push(` }`);
|
|
1155
1275
|
}
|
|
1156
1276
|
if (hasMaxLength(schema)) {
|
|
1157
1277
|
constraintLines.push(` if (typeof input === 'string' && input.length > ${schema.maxLength}) {`);
|
|
1158
|
-
constraintLines.push(`
|
|
1278
|
+
constraintLines.push(` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`);
|
|
1159
1279
|
constraintLines.push(` }`);
|
|
1160
1280
|
}
|
|
1161
1281
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
`)
|
|
1165
|
-
`
|
|
1166
|
-
|
|
1282
|
+
if (!wrongType) {
|
|
1283
|
+
return [
|
|
1284
|
+
`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
|
|
1285
|
+
` return true`,
|
|
1286
|
+
`}`
|
|
1287
|
+
].join(`
|
|
1288
|
+
`);
|
|
1289
|
+
}
|
|
1290
|
+
if (constraintLines.length === 0) {
|
|
1291
|
+
return [
|
|
1292
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1293
|
+
` if (${wrongType}) {`,
|
|
1294
|
+
` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
|
|
1295
|
+
` }`,
|
|
1296
|
+
` return true`,
|
|
1297
|
+
`}`
|
|
1298
|
+
].join(`
|
|
1299
|
+
`);
|
|
1300
|
+
}
|
|
1301
|
+
return [
|
|
1167
1302
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1168
1303
|
` if (${wrongType}) {`,
|
|
1169
1304
|
` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
|
|
1170
1305
|
` }`,
|
|
1171
|
-
|
|
1172
|
-
`
|
|
1306
|
+
` const errors: ValidationError[] = []`,
|
|
1307
|
+
constraintLines.join(`
|
|
1308
|
+
`),
|
|
1309
|
+
` return errors.length > 0 ? { valid: false, errors } : true`,
|
|
1173
1310
|
`}`
|
|
1174
1311
|
].join(`
|
|
1175
|
-
`) : [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
|
|
1176
1312
|
`);
|
|
1177
1313
|
}
|
|
1178
1314
|
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-validators",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Generate TypeScript validation functions from JSON Schemas.",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"json-schema-typed": "
|
|
50
|
-
"@amritk/helpers": "
|
|
49
|
+
"json-schema-typed": "^8.0.1",
|
|
50
|
+
"@amritk/helpers": "0.5.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@scalar/openapi-parser": "^0.26.1"
|
|
@@ -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')
|
|
@@ -164,4 +176,63 @@ describe('generate-validator-function', () => {
|
|
|
164
176
|
expect(code).toContain('Array.isArray(input)')
|
|
165
177
|
expect(code).toContain('must be object')
|
|
166
178
|
})
|
|
179
|
+
|
|
180
|
+
it('generates an instanceof check for a required x-mjst Date property', () => {
|
|
181
|
+
const schema = {
|
|
182
|
+
type: 'object' as const,
|
|
183
|
+
properties: { createdAt: { 'x-mjst': { instanceOf: 'Date' } } },
|
|
184
|
+
required: ['createdAt'],
|
|
185
|
+
}
|
|
186
|
+
const code = generateValidatorFunction(schema, 'Event')
|
|
187
|
+
|
|
188
|
+
expect(code).toContain('"createdAt" in obj')
|
|
189
|
+
expect(code).toContain('!(obj["createdAt"] instanceof Date)')
|
|
190
|
+
expect(code).toContain('must be Date')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('generates an instanceof check for an optional x-mjst Date property', () => {
|
|
194
|
+
const schema = {
|
|
195
|
+
type: 'object' as const,
|
|
196
|
+
properties: { createdAt: { 'x-mjst': { instanceOf: 'Date' } } },
|
|
197
|
+
}
|
|
198
|
+
const code = generateValidatorFunction(schema, 'Event')
|
|
199
|
+
|
|
200
|
+
expect(code).toContain('obj["createdAt"] !== undefined && !(obj["createdAt"] instanceof Date)')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('generates an instanceof check for a top-level x-mjst Date schema', () => {
|
|
204
|
+
const code = generateValidatorFunction({ 'x-mjst': { instanceOf: 'Date' } }, 'When')
|
|
205
|
+
|
|
206
|
+
expect(code).toContain('!(input instanceof Date)')
|
|
207
|
+
expect(code).toContain('must be Date')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('generates a typeof check for a required x-mjst bigint property', () => {
|
|
211
|
+
const schema = {
|
|
212
|
+
type: 'object' as const,
|
|
213
|
+
properties: { balance: { 'x-mjst': { primitive: 'bigint' } } },
|
|
214
|
+
required: ['balance'],
|
|
215
|
+
}
|
|
216
|
+
const code = generateValidatorFunction(schema, 'Account')
|
|
217
|
+
|
|
218
|
+
expect(code).toContain('typeof obj["balance"] !== "bigint"')
|
|
219
|
+
expect(code).toContain('must be bigint')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('guards undefined for an optional x-mjst bigint property', () => {
|
|
223
|
+
const schema = {
|
|
224
|
+
type: 'object' as const,
|
|
225
|
+
properties: { balance: { 'x-mjst': { primitive: 'bigint' } } },
|
|
226
|
+
}
|
|
227
|
+
const code = generateValidatorFunction(schema, 'Account')
|
|
228
|
+
|
|
229
|
+
expect(code).toContain('obj["balance"] !== undefined && typeof obj["balance"] !== "bigint"')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('generates a typeof check for a top-level x-mjst bigint schema', () => {
|
|
233
|
+
const code = generateValidatorFunction({ 'x-mjst': { primitive: 'bigint' } }, 'Big')
|
|
234
|
+
|
|
235
|
+
expect(code).toContain('typeof input !== "bigint"')
|
|
236
|
+
expect(code).toContain('must be bigint')
|
|
237
|
+
})
|
|
167
238
|
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
|
|
1
2
|
import { refToName } from '@amritk/helpers/ref-to-name'
|
|
2
3
|
import {
|
|
3
4
|
hasAdditionalProperties,
|
|
@@ -88,6 +89,40 @@ const generatePropertyChecks = (key: string, propSchema: JSONSchema, isRequired:
|
|
|
88
89
|
return lines
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// x-mjst instanceOf (e.g. Date) — value must be an instance of the class
|
|
93
|
+
const instanceOf = getMjstInstanceOf(propSchema)
|
|
94
|
+
if (instanceOf) {
|
|
95
|
+
if (isRequired) {
|
|
96
|
+
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
|
|
97
|
+
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`)
|
|
98
|
+
lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`)
|
|
99
|
+
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`)
|
|
100
|
+
lines.push(` }`)
|
|
101
|
+
} else {
|
|
102
|
+
lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`)
|
|
103
|
+
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`)
|
|
104
|
+
lines.push(` }`)
|
|
105
|
+
}
|
|
106
|
+
return lines
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// x-mjst primitive (e.g. bigint) — value must satisfy a typeof check
|
|
110
|
+
const primitive = getMjstPrimitive(propSchema)
|
|
111
|
+
if (primitive) {
|
|
112
|
+
if (isRequired) {
|
|
113
|
+
lines.push(` if (!(${JSON.stringify(key)} in obj)) {`)
|
|
114
|
+
lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`)
|
|
115
|
+
lines.push(` } else if (typeof ${raw} !== "${primitive}") {`)
|
|
116
|
+
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`)
|
|
117
|
+
lines.push(` }`)
|
|
118
|
+
} else {
|
|
119
|
+
lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`)
|
|
120
|
+
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`)
|
|
121
|
+
lines.push(` }`)
|
|
122
|
+
}
|
|
123
|
+
return lines
|
|
124
|
+
}
|
|
125
|
+
|
|
91
126
|
// enum
|
|
92
127
|
if (hasEnum(propSchema)) {
|
|
93
128
|
const allowed = JSON.stringify(propSchema.enum)
|
|
@@ -281,6 +316,32 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
|
|
|
281
316
|
].join('\n')
|
|
282
317
|
}
|
|
283
318
|
|
|
319
|
+
// Top-level x-mjst instanceOf (e.g. a schema that is itself a Date)
|
|
320
|
+
const instanceOf = getMjstInstanceOf(schema)
|
|
321
|
+
if (instanceOf) {
|
|
322
|
+
return [
|
|
323
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
324
|
+
` if (!(input instanceof ${instanceOf})) {`,
|
|
325
|
+
` return { valid: false, errors: [{ message: 'must be ${instanceOf}', path: _path }] }`,
|
|
326
|
+
` }`,
|
|
327
|
+
` return true`,
|
|
328
|
+
`}`,
|
|
329
|
+
].join('\n')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Top-level x-mjst primitive (e.g. a schema that is itself a bigint)
|
|
333
|
+
const primitive = getMjstPrimitive(schema)
|
|
334
|
+
if (primitive) {
|
|
335
|
+
return [
|
|
336
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
337
|
+
` if (typeof input !== "${primitive}") {`,
|
|
338
|
+
` return { valid: false, errors: [{ message: 'must be ${primitive}', path: _path }] }`,
|
|
339
|
+
` }`,
|
|
340
|
+
` return true`,
|
|
341
|
+
`}`,
|
|
342
|
+
].join('\n')
|
|
343
|
+
}
|
|
344
|
+
|
|
284
345
|
// Top-level enum
|
|
285
346
|
if (hasEnum(schema)) {
|
|
286
347
|
const allowed = JSON.stringify(schema.enum)
|
|
@@ -325,42 +386,54 @@ const generateScalarValidator = (schema: JSONSchema, typeName: string): string =
|
|
|
325
386
|
if (t === 'string') {
|
|
326
387
|
if (hasPattern(schema)) {
|
|
327
388
|
constraintLines.push(` if (typeof input === 'string' && !/${schema.pattern}/.test(input)) {`)
|
|
328
|
-
constraintLines.push(
|
|
329
|
-
` return { valid: false, errors: [{ message: 'must match pattern ${schema.pattern}', path: _path }] }`,
|
|
330
|
-
)
|
|
389
|
+
constraintLines.push(` errors.push({ message: 'must match pattern ${schema.pattern}', path: _path })`)
|
|
331
390
|
constraintLines.push(` }`)
|
|
332
391
|
}
|
|
333
392
|
if (hasMinLength(schema)) {
|
|
334
393
|
constraintLines.push(` if (typeof input === 'string' && input.length < ${schema.minLength}) {`)
|
|
335
394
|
constraintLines.push(
|
|
336
|
-
`
|
|
395
|
+
` errors.push({ message: 'must have at least ${schema.minLength} characters', path: _path })`,
|
|
337
396
|
)
|
|
338
397
|
constraintLines.push(` }`)
|
|
339
398
|
}
|
|
340
399
|
if (hasMaxLength(schema)) {
|
|
341
400
|
constraintLines.push(` if (typeof input === 'string' && input.length > ${schema.maxLength}) {`)
|
|
342
401
|
constraintLines.push(
|
|
343
|
-
`
|
|
402
|
+
` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`,
|
|
344
403
|
)
|
|
345
404
|
constraintLines.push(` }`)
|
|
346
405
|
}
|
|
347
406
|
}
|
|
348
407
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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')
|
|
364
437
|
}
|
|
365
438
|
|
|
366
439
|
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(
|