@amritk/generate-validators 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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("#");
@@ -492,12 +517,25 @@ var buildJsDocBlock = (title, description, commentUrl) => {
492
517
  return block;
493
518
  };
494
519
  var getTypeScriptType = (schema) => {
520
+ const base = getUnbrandedType(schema);
521
+ const brand = getMjstBrand(schema);
522
+ return brand ? `(${base} & { readonly __brand: '${brand}' })` : base;
523
+ };
524
+ var getUnbrandedType = (schema) => {
495
525
  if (typeof schema === "boolean") {
496
526
  return getBooleanSubSchemaType(schema);
497
527
  }
498
528
  if (typeof schema !== "object" || schema === null) {
499
529
  return "unknown";
500
530
  }
531
+ const instanceOf = getMjstInstanceOf(schema);
532
+ if (instanceOf) {
533
+ return instanceOf;
534
+ }
535
+ const primitive = getMjstPrimitive(schema);
536
+ if (primitive) {
537
+ return primitive;
538
+ }
501
539
  if (schema.$ref) {
502
540
  if (!schema.$ref.startsWith("#")) {
503
541
  return "unknown";
@@ -903,6 +941,31 @@ var collectValidatorImports = (schema, options) => {
903
941
  return imports;
904
942
  };
905
943
 
944
+ // ../helpers/dist/mjst-extension.js
945
+ var isSchemaObject4 = (schema) => {
946
+ return typeof schema === "object" && schema !== null && typeof schema !== "boolean";
947
+ };
948
+ var MJST_EXTENSION_KEY2 = "x-mjst";
949
+ var IDENTIFIER2 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
950
+ var SUPPORTED_PRIMITIVES2 = new Set(["bigint"]);
951
+ var readExtensionString2 = (schema, field) => {
952
+ if (!isSchemaObject4(schema))
953
+ return;
954
+ const extension = schema[MJST_EXTENSION_KEY2];
955
+ if (typeof extension !== "object" || extension === null)
956
+ return;
957
+ const value = extension[field];
958
+ return typeof value === "string" ? value : undefined;
959
+ };
960
+ var getMjstInstanceOf2 = (schema) => {
961
+ const instanceOf = readExtensionString2(schema, "instanceOf");
962
+ return instanceOf !== undefined && IDENTIFIER2.test(instanceOf) ? instanceOf : undefined;
963
+ };
964
+ var getMjstPrimitive2 = (schema) => {
965
+ const primitive = readExtensionString2(schema, "primitive");
966
+ return primitive !== undefined && SUPPORTED_PRIMITIVES2.has(primitive) ? primitive : undefined;
967
+ };
968
+
906
969
  // src/generators/generate-validator-function.ts
907
970
  var validatorName = (typeName) => `validate${typeName}`;
908
971
  var typeofString = (type) => {
@@ -951,6 +1014,36 @@ var generatePropertyChecks = (key, propSchema, isRequired) => {
951
1014
  }
952
1015
  return lines;
953
1016
  }
1017
+ const instanceOf = getMjstInstanceOf2(propSchema);
1018
+ if (instanceOf) {
1019
+ if (isRequired) {
1020
+ lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
1021
+ lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
1022
+ lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
1023
+ lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
1024
+ lines.push(` }`);
1025
+ } else {
1026
+ lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`);
1027
+ lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
1028
+ lines.push(` }`);
1029
+ }
1030
+ return lines;
1031
+ }
1032
+ const primitive = getMjstPrimitive2(propSchema);
1033
+ if (primitive) {
1034
+ if (isRequired) {
1035
+ lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
1036
+ lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
1037
+ lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
1038
+ lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
1039
+ lines.push(` }`);
1040
+ } else {
1041
+ lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`);
1042
+ lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
1043
+ lines.push(` }`);
1044
+ }
1045
+ return lines;
1046
+ }
954
1047
  if (hasEnum(propSchema)) {
955
1048
  const allowed = JSON.stringify(propSchema.enum);
956
1049
  const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
@@ -1105,6 +1198,30 @@ var generateScalarValidator = (schema, typeName) => {
1105
1198
  ` return ${delegateName}(input, _path)`,
1106
1199
  `}`
1107
1200
  ].join(`
1201
+ `);
1202
+ }
1203
+ const instanceOf = getMjstInstanceOf2(schema);
1204
+ if (instanceOf) {
1205
+ return [
1206
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1207
+ ` if (!(input instanceof ${instanceOf})) {`,
1208
+ ` return { valid: false, errors: [{ message: 'must be ${instanceOf}', path: _path }] }`,
1209
+ ` }`,
1210
+ ` return true`,
1211
+ `}`
1212
+ ].join(`
1213
+ `);
1214
+ }
1215
+ const primitive = getMjstPrimitive2(schema);
1216
+ if (primitive) {
1217
+ return [
1218
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1219
+ ` if (typeof input !== "${primitive}") {`,
1220
+ ` return { valid: false, errors: [{ message: 'must be ${primitive}', path: _path }] }`,
1221
+ ` }`,
1222
+ ` return true`,
1223
+ `}`
1224
+ ].join(`
1108
1225
  `);
1109
1226
  }
1110
1227
  if (hasEnum(schema)) {
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.0",
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.4.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@scalar/openapi-parser": "^0.26.1"
@@ -164,4 +164,63 @@ describe('generate-validator-function', () => {
164
164
  expect(code).toContain('Array.isArray(input)')
165
165
  expect(code).toContain('must be object')
166
166
  })
167
+
168
+ it('generates an instanceof check for a required x-mjst Date property', () => {
169
+ const schema = {
170
+ type: 'object' as const,
171
+ properties: { createdAt: { 'x-mjst': { instanceOf: 'Date' } } },
172
+ required: ['createdAt'],
173
+ }
174
+ const code = generateValidatorFunction(schema, 'Event')
175
+
176
+ expect(code).toContain('"createdAt" in obj')
177
+ expect(code).toContain('!(obj["createdAt"] instanceof Date)')
178
+ expect(code).toContain('must be Date')
179
+ })
180
+
181
+ it('generates an instanceof check for an optional x-mjst Date property', () => {
182
+ const schema = {
183
+ type: 'object' as const,
184
+ properties: { createdAt: { 'x-mjst': { instanceOf: 'Date' } } },
185
+ }
186
+ const code = generateValidatorFunction(schema, 'Event')
187
+
188
+ expect(code).toContain('obj["createdAt"] !== undefined && !(obj["createdAt"] instanceof Date)')
189
+ })
190
+
191
+ it('generates an instanceof check for a top-level x-mjst Date schema', () => {
192
+ const code = generateValidatorFunction({ 'x-mjst': { instanceOf: 'Date' } }, 'When')
193
+
194
+ expect(code).toContain('!(input instanceof Date)')
195
+ expect(code).toContain('must be Date')
196
+ })
197
+
198
+ it('generates a typeof check for a required x-mjst bigint property', () => {
199
+ const schema = {
200
+ type: 'object' as const,
201
+ properties: { balance: { 'x-mjst': { primitive: 'bigint' } } },
202
+ required: ['balance'],
203
+ }
204
+ const code = generateValidatorFunction(schema, 'Account')
205
+
206
+ expect(code).toContain('typeof obj["balance"] !== "bigint"')
207
+ expect(code).toContain('must be bigint')
208
+ })
209
+
210
+ it('guards undefined for an optional x-mjst bigint property', () => {
211
+ const schema = {
212
+ type: 'object' as const,
213
+ properties: { balance: { 'x-mjst': { primitive: 'bigint' } } },
214
+ }
215
+ const code = generateValidatorFunction(schema, 'Account')
216
+
217
+ expect(code).toContain('obj["balance"] !== undefined && typeof obj["balance"] !== "bigint"')
218
+ })
219
+
220
+ it('generates a typeof check for a top-level x-mjst bigint schema', () => {
221
+ const code = generateValidatorFunction({ 'x-mjst': { primitive: 'bigint' } }, 'Big')
222
+
223
+ expect(code).toContain('typeof input !== "bigint"')
224
+ expect(code).toContain('must be bigint')
225
+ })
167
226
  })
@@ -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)