@amritk/generate-validators 0.2.2 → 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.
|
|
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": "
|
|
50
|
-
"@amritk/helpers": "
|
|
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)
|