@amritk/generate-validators 0.3.0 → 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
|
@@ -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
|
}
|
|
@@ -569,29 +570,29 @@ 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
597
|
return `(${baseType}) & ${refToName2(conditionalResult.thenRef)}`;
|
|
597
598
|
}
|
|
@@ -600,20 +601,20 @@ var getUnbrandedType = (schema) => {
|
|
|
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
|
`;
|
|
@@ -1262,34 +1265,50 @@ var generateScalarValidator = (schema, typeName) => {
|
|
|
1262
1265
|
if (t === "string") {
|
|
1263
1266
|
if (hasPattern(schema)) {
|
|
1264
1267
|
constraintLines.push(` if (typeof input === 'string' && !/${schema.pattern}/.test(input)) {`);
|
|
1265
|
-
constraintLines.push(`
|
|
1268
|
+
constraintLines.push(` errors.push({ message: 'must match pattern ${schema.pattern}', path: _path })`);
|
|
1266
1269
|
constraintLines.push(` }`);
|
|
1267
1270
|
}
|
|
1268
1271
|
if (hasMinLength(schema)) {
|
|
1269
1272
|
constraintLines.push(` if (typeof input === 'string' && input.length < ${schema.minLength}) {`);
|
|
1270
|
-
constraintLines.push(`
|
|
1273
|
+
constraintLines.push(` errors.push({ message: 'must have at least ${schema.minLength} characters', path: _path })`);
|
|
1271
1274
|
constraintLines.push(` }`);
|
|
1272
1275
|
}
|
|
1273
1276
|
if (hasMaxLength(schema)) {
|
|
1274
1277
|
constraintLines.push(` if (typeof input === 'string' && input.length > ${schema.maxLength}) {`);
|
|
1275
|
-
constraintLines.push(`
|
|
1278
|
+
constraintLines.push(` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`);
|
|
1276
1279
|
constraintLines.push(` }`);
|
|
1277
1280
|
}
|
|
1278
1281
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
`)
|
|
1282
|
-
`
|
|
1283
|
-
|
|
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 [
|
|
1284
1302
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1285
1303
|
` if (${wrongType}) {`,
|
|
1286
1304
|
` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
|
|
1287
1305
|
` }`,
|
|
1288
|
-
|
|
1289
|
-
`
|
|
1306
|
+
` const errors: ValidationError[] = []`,
|
|
1307
|
+
constraintLines.join(`
|
|
1308
|
+
`),
|
|
1309
|
+
` return errors.length > 0 ? { valid: false, errors } : true`,
|
|
1290
1310
|
`}`
|
|
1291
1311
|
].join(`
|
|
1292
|
-
`) : [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(`
|
|
1293
1312
|
`);
|
|
1294
1313
|
}
|
|
1295
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.
|
|
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",
|
|
@@ -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.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')
|
|
@@ -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(
|