@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 getUnbrandedType = (schema) => {
524
+ var recordType = (keyType, valueType, options) => options.readonly ? `Readonly<Record<${keyType}, ${valueType}>>` : `Record<${keyType}, ${valueType}>`;
525
+ var getUnbrandedType = (schema, options = {}) => {
525
526
  if (typeof schema === "boolean") {
526
527
  return getBooleanSubSchemaType(schema);
527
528
  }
@@ -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 `Record<string, ${getBooleanSubSchemaType(schema.additionalProperties)}>`;
604
+ return recordType("string", getBooleanSubSchemaType(schema.additionalProperties), options);
604
605
  }
605
- return `Record<string, ${getTypeScriptType(schema.additionalProperties)}>`;
606
+ return recordType("string", getTypeScriptType(schema.additionalProperties, options), options);
606
607
  }
607
608
  if (schema.patternProperties && typeof schema.patternProperties === "object") {
608
609
  const firstEntry = Object.entries(schema.patternProperties)[0];
609
610
  if (firstEntry) {
610
611
  const [pattern, value] = firstEntry;
611
612
  if (value !== undefined) {
612
- const valueType = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value);
613
+ const valueType = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value, options);
613
614
  if (pattern === "^x-") {
614
- return `Record<\`x-\${string}\`, ${valueType}>`;
615
+ return recordType("`x-${string}`", valueType, options);
615
616
  }
616
- return `Record<string, ${valueType}>`;
617
+ return recordType("string", valueType, options);
617
618
  }
618
619
  }
619
620
  }
@@ -666,41 +667,42 @@ var getUnbrandedType = (schema) => {
666
667
  return "boolean";
667
668
  case "array":
668
669
  if (schema.items) {
669
- const itemType = getTypeScriptType(schema.items);
670
+ const itemType = getTypeScriptType(schema.items, options);
670
671
  const wrappedItemType = itemType.includes(" | ") ? `(${itemType})` : itemType;
671
- return `${wrappedItemType}[]`;
672
+ return options.readonly ? `readonly ${wrappedItemType}[]` : `${wrappedItemType}[]`;
672
673
  }
673
- return "unknown[]";
674
+ return options.readonly ? "readonly unknown[]" : "unknown[]";
674
675
  case "object":
675
676
  if (schema.properties) {
677
+ const readonlyPrefix = options.readonly ? "readonly " : "";
676
678
  let properties = "";
677
679
  let first = true;
678
680
  for (const key in schema.properties) {
679
681
  const propSchema = schema.properties[key];
680
682
  const isRequired = schema.required?.includes(key) ?? false;
681
683
  const optional = isRequired ? "" : "?";
682
- const propType = getTypeScriptType(propSchema);
684
+ const propType = getTypeScriptType(propSchema, options);
683
685
  if (!first)
684
686
  properties += "; ";
685
- properties += safeKey(key) + optional + ": " + propType;
687
+ properties += readonlyPrefix + safeKey(key) + optional + ": " + propType;
686
688
  first = false;
687
689
  }
688
690
  return "{ " + properties + " }";
689
691
  }
690
692
  if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
691
- const additionalPropType = getTypeScriptType(schema.additionalProperties);
692
- return `Record<string, ${additionalPropType}>`;
693
+ const additionalPropType = getTypeScriptType(schema.additionalProperties, options);
694
+ return recordType("string", additionalPropType, options);
693
695
  }
694
696
  if (schema.patternProperties && typeof schema.patternProperties === "object") {
695
697
  const firstEntry = Object.entries(schema.patternProperties)[0];
696
698
  if (firstEntry) {
697
699
  const [pattern, patternVal] = firstEntry;
698
700
  if (patternVal) {
699
- const valueType = getTypeScriptType(patternVal);
701
+ const valueType = getTypeScriptType(patternVal, options);
700
702
  if (pattern === "^x-") {
701
- return `Record<\`x-\${string}\`, ${valueType}>`;
703
+ return recordType("`x-${string}`", valueType, options);
702
704
  }
703
- return `Record<string, ${valueType}>`;
705
+ return recordType("string", valueType, options);
704
706
  }
705
707
  }
706
708
  }
@@ -709,9 +711,10 @@ var getUnbrandedType = (schema) => {
709
711
  return "unknown";
710
712
  }
711
713
  };
712
- var generateTypeDefinition = (schema, typeName) => {
714
+ var generateTypeDefinition = (schema, typeName, options = {}) => {
715
+ const readonlyPrefix = options.readonly ? "readonly " : "";
713
716
  if (!isObjectLikeSchema(schema)) {
714
- const tsType = getTypeScriptType(schema);
717
+ const tsType = getTypeScriptType(schema, options);
715
718
  let result = "";
716
719
  if (isSchemaObject2(schema) && schema.$comment && typeof schema.$comment === "string") {
717
720
  result += buildJsDocBlock(typeName, schema.$comment);
@@ -739,23 +742,23 @@ var generateTypeDefinition = (schema, typeName) => {
739
742
  if (firstPatternProperty === undefined) {
740
743
  return `export type ${typeName} = Record<string, unknown>;`;
741
744
  }
742
- const patternPropType = typeof firstPatternProperty === "boolean" ? getBooleanSubSchemaType(firstPatternProperty) : getTypeScriptType(firstPatternProperty);
745
+ const patternPropType = typeof firstPatternProperty === "boolean" ? getBooleanSubSchemaType(firstPatternProperty) : getTypeScriptType(firstPatternProperty, options);
743
746
  const keyType = firstPattern === "^x-" ? "`x-${string}`" : "string";
744
747
  let result2 = "";
745
748
  if (jsDocTitle && jsDocDescription) {
746
749
  result2 += buildJsDocBlock(jsDocTitle, jsDocDescription);
747
750
  }
748
- result2 += `export type ${typeName} = Record<${keyType}, ${patternPropType}>;`;
751
+ result2 += `export type ${typeName} = ${recordType(keyType, patternPropType, options)};`;
749
752
  return result2;
750
753
  }
751
754
  if (!hasProperties2 && hasAdditionalProperties2 && normalizedSchema.additionalProperties) {
752
- const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties);
755
+ const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties, options);
753
756
  let result2 = "";
754
757
  if (jsDocTitle && jsDocDescription) {
755
758
  result2 += buildJsDocBlock(jsDocTitle, jsDocDescription);
756
759
  }
757
760
  result2 += `export type ${typeName} = {
758
- [key: string]: ${additionalPropType};
761
+ ${readonlyPrefix}[key: string]: ${additionalPropType};
759
762
  };`;
760
763
  return result2;
761
764
  }
@@ -766,8 +769,8 @@ var generateTypeDefinition = (schema, typeName) => {
766
769
  const propSchema = schemaProps[key];
767
770
  const isRequired = normalizedSchema.required?.includes(key) ?? false;
768
771
  const optional = isRequired ? "" : "?";
769
- const propType = getTypeScriptType(propSchema);
770
- const quotedKey = safeKey(key);
772
+ const propType = getTypeScriptType(propSchema, options);
773
+ const quotedKey = readonlyPrefix + safeKey(key);
771
774
  if (!isFirstProp)
772
775
  properties += `
773
776
  `;
@@ -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(` return { valid: false, errors: [{ message: 'must match pattern ${schema.pattern}', path: _path }] }`);
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(` return { valid: false, errors: [{ message: 'must have at least ${schema.minLength} characters', path: _path }] }`);
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(` return { valid: false, errors: [{ message: 'must have at most ${schema.maxLength} characters', path: _path }] }`);
1278
+ constraintLines.push(` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`);
1276
1279
  constraintLines.push(` }`);
1277
1280
  }
1278
1281
  }
1279
- const body = constraintLines.length > 0 ? `
1280
- ` + constraintLines.join(`
1281
- `) + `
1282
- ` : "";
1283
- return wrongType ? [
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
- body,
1289
- ` return true`,
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.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",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "json-schema-typed": "^8.0.1",
50
- "@amritk/helpers": "0.4.0"
50
+ "@amritk/helpers": "0.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
- ` return { valid: false, errors: [{ message: 'must have at least ${schema.minLength} characters', path: _path }] }`,
395
+ ` errors.push({ message: 'must have at least ${schema.minLength} characters', path: _path })`,
398
396
  )
399
397
  constraintLines.push(` }`)
400
398
  }
401
399
  if (hasMaxLength(schema)) {
402
400
  constraintLines.push(` if (typeof input === 'string' && input.length > ${schema.maxLength}) {`)
403
401
  constraintLines.push(
404
- ` return { valid: false, errors: [{ message: 'must have at most ${schema.maxLength} characters', path: _path }] }`,
402
+ ` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`,
405
403
  )
406
404
  constraintLines.push(` }`)
407
405
  }
408
406
  }
409
407
 
410
- const body = constraintLines.length > 0 ? '\n' + constraintLines.join('\n') + '\n' : ''
411
-
412
- return wrongType
413
- ? [
414
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
415
- ` if (${wrongType}) {`,
416
- ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
417
- ` }`,
418
- body,
419
- ` return true`,
420
- `}`,
421
- ].join('\n')
422
- : [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(
423
- '\n',
424
- )
408
+ if (!wrongType) {
409
+ return [
410
+ `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
411
+ ` return true`,
412
+ `}`,
413
+ ].join('\n')
414
+ }
415
+
416
+ if (constraintLines.length === 0) {
417
+ return [
418
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
419
+ ` if (${wrongType}) {`,
420
+ ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
421
+ ` }`,
422
+ ` return true`,
423
+ `}`,
424
+ ].join('\n')
425
+ }
426
+
427
+ return [
428
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
429
+ ` if (${wrongType}) {`,
430
+ ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
431
+ ` }`,
432
+ ` const errors: ValidationError[] = []`,
433
+ constraintLines.join('\n'),
434
+ ` return errors.length > 0 ? { valid: false, errors } : true`,
435
+ `}`,
436
+ ].join('\n')
425
437
  }
426
438
 
427
439
  return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(