@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 `Record<string, ${getBooleanSubSchemaType(schema.additionalProperties)}>`;
604
+ return recordType("string", getBooleanSubSchemaType(schema.additionalProperties), options);
566
605
  }
567
- return `Record<string, ${getTypeScriptType(schema.additionalProperties)}>`;
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 `Record<\`x-\${string}\`, ${valueType}>`;
615
+ return recordType("`x-${string}`", valueType, options);
577
616
  }
578
- return `Record<string, ${valueType}>`;
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 `Record<string, ${additionalPropType}>`;
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 `Record<\`x-\${string}\`, ${valueType}>`;
703
+ return recordType("`x-${string}`", valueType, options);
664
704
  }
665
- return `Record<string, ${valueType}>`;
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} = Record<${keyType}, ${patternPropType}>;`;
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(` 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 })`);
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(` 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 })`);
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(` 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 })`);
1159
1279
  constraintLines.push(` }`);
1160
1280
  }
1161
1281
  }
1162
- const body = constraintLines.length > 0 ? `
1163
- ` + constraintLines.join(`
1164
- `) + `
1165
- ` : "";
1166
- 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 [
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
- body,
1172
- ` return true`,
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.2.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",
@@ -46,8 +46,8 @@
46
46
  }
47
47
  },
48
48
  "dependencies": {
49
- "json-schema-typed": "catalog:",
50
- "@amritk/helpers": "workspace:*"
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
- ` 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 })`,
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
- ` 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 })`,
344
403
  )
345
404
  constraintLines.push(` }`)
346
405
  }
347
406
  }
348
407
 
349
- const body = constraintLines.length > 0 ? '\n' + constraintLines.join('\n') + '\n' : ''
350
-
351
- return wrongType
352
- ? [
353
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
354
- ` if (${wrongType}) {`,
355
- ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
356
- ` }`,
357
- body,
358
- ` return true`,
359
- `}`,
360
- ].join('\n')
361
- : [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join(
362
- '\n',
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(