@amritk/generate-parsers 0.3.0 → 0.4.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 +119 -0
- package/package.json +4 -4
- package/src/generators/generate-schema-checks.test.ts +8 -0
- package/src/generators/generate-schema-checks.ts +13 -0
- package/src/generators/generate-strict-assertion.test.ts +67 -0
- package/src/generators/generate-strict-assertion.ts +39 -0
- package/src/generators/generate-validation-expression.test.ts +37 -0
- package/src/generators/generate-validation-expression.ts +40 -0
- package/src/helpers/get-default-value.test.ts +4 -0
- package/src/helpers/get-default-value.ts +6 -0
package/dist/index.js
CHANGED
|
@@ -375,6 +375,31 @@ var isSchemaObject2 = (schema) => {
|
|
|
375
375
|
var isObjectSchema = (schema) => {
|
|
376
376
|
return isSchemaObject2(schema) && (("type" in schema) && schema.type === "object" || ("properties" in schema));
|
|
377
377
|
};
|
|
378
|
+
var MJST_EXTENSION_KEY = "x-mjst";
|
|
379
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
380
|
+
var SUPPORTED_PRIMITIVES = new Set(["bigint"]);
|
|
381
|
+
var SAFE_BRAND = /^[\w$ -]+$/;
|
|
382
|
+
var readExtensionString = (schema, field) => {
|
|
383
|
+
if (!isSchemaObject2(schema))
|
|
384
|
+
return;
|
|
385
|
+
const extension = schema[MJST_EXTENSION_KEY];
|
|
386
|
+
if (typeof extension !== "object" || extension === null)
|
|
387
|
+
return;
|
|
388
|
+
const value = extension[field];
|
|
389
|
+
return typeof value === "string" ? value : undefined;
|
|
390
|
+
};
|
|
391
|
+
var getMjstInstanceOf = (schema) => {
|
|
392
|
+
const instanceOf = readExtensionString(schema, "instanceOf");
|
|
393
|
+
return instanceOf !== undefined && IDENTIFIER.test(instanceOf) ? instanceOf : undefined;
|
|
394
|
+
};
|
|
395
|
+
var getMjstPrimitive = (schema) => {
|
|
396
|
+
const primitive = readExtensionString(schema, "primitive");
|
|
397
|
+
return primitive !== undefined && SUPPORTED_PRIMITIVES.has(primitive) ? primitive : undefined;
|
|
398
|
+
};
|
|
399
|
+
var getMjstBrand = (schema) => {
|
|
400
|
+
const brand = readExtensionString(schema, "brand");
|
|
401
|
+
return brand !== undefined && SAFE_BRAND.test(brand) ? brand : undefined;
|
|
402
|
+
};
|
|
378
403
|
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();
|
|
379
404
|
var uriRefToFilename4 = (uri) => {
|
|
380
405
|
const hashIndex = uri.indexOf("#");
|
|
@@ -516,12 +541,25 @@ var buildJsDocBlock = (title, description, commentUrl) => {
|
|
|
516
541
|
return block;
|
|
517
542
|
};
|
|
518
543
|
var getTypeScriptType = (schema) => {
|
|
544
|
+
const base = getUnbrandedType(schema);
|
|
545
|
+
const brand = getMjstBrand(schema);
|
|
546
|
+
return brand ? `(${base} & { readonly __brand: '${brand}' })` : base;
|
|
547
|
+
};
|
|
548
|
+
var getUnbrandedType = (schema) => {
|
|
519
549
|
if (typeof schema === "boolean") {
|
|
520
550
|
return getBooleanSubSchemaType(schema);
|
|
521
551
|
}
|
|
522
552
|
if (typeof schema !== "object" || schema === null) {
|
|
523
553
|
return "unknown";
|
|
524
554
|
}
|
|
555
|
+
const instanceOf = getMjstInstanceOf(schema);
|
|
556
|
+
if (instanceOf) {
|
|
557
|
+
return instanceOf;
|
|
558
|
+
}
|
|
559
|
+
const primitive = getMjstPrimitive(schema);
|
|
560
|
+
if (primitive) {
|
|
561
|
+
return primitive;
|
|
562
|
+
}
|
|
525
563
|
if (schema.$ref) {
|
|
526
564
|
if (!schema.$ref.startsWith("#")) {
|
|
527
565
|
return "unknown";
|
|
@@ -1082,6 +1120,31 @@ var safeKey2 = (key) => {
|
|
|
1082
1120
|
return `'${key}'`;
|
|
1083
1121
|
};
|
|
1084
1122
|
|
|
1123
|
+
// ../helpers/dist/mjst-extension.js
|
|
1124
|
+
var isSchemaObject4 = (schema) => {
|
|
1125
|
+
return typeof schema === "object" && schema !== null && typeof schema !== "boolean";
|
|
1126
|
+
};
|
|
1127
|
+
var MJST_EXTENSION_KEY2 = "x-mjst";
|
|
1128
|
+
var IDENTIFIER2 = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1129
|
+
var SUPPORTED_PRIMITIVES2 = new Set(["bigint"]);
|
|
1130
|
+
var readExtensionString2 = (schema, field) => {
|
|
1131
|
+
if (!isSchemaObject4(schema))
|
|
1132
|
+
return;
|
|
1133
|
+
const extension = schema[MJST_EXTENSION_KEY2];
|
|
1134
|
+
if (typeof extension !== "object" || extension === null)
|
|
1135
|
+
return;
|
|
1136
|
+
const value = extension[field];
|
|
1137
|
+
return typeof value === "string" ? value : undefined;
|
|
1138
|
+
};
|
|
1139
|
+
var getMjstInstanceOf2 = (schema) => {
|
|
1140
|
+
const instanceOf = readExtensionString2(schema, "instanceOf");
|
|
1141
|
+
return instanceOf !== undefined && IDENTIFIER2.test(instanceOf) ? instanceOf : undefined;
|
|
1142
|
+
};
|
|
1143
|
+
var getMjstPrimitive2 = (schema) => {
|
|
1144
|
+
const primitive = readExtensionString2(schema, "primitive");
|
|
1145
|
+
return primitive !== undefined && SUPPORTED_PRIMITIVES2.has(primitive) ? primitive : undefined;
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1085
1148
|
// src/generators/generate-default-from-pattern.ts
|
|
1086
1149
|
var generateDefaultFromPattern = (pattern) => {
|
|
1087
1150
|
if (pattern.includes("@") || pattern.includes("[a-zA-Z0-9]") && pattern.includes("\\.")) {
|
|
@@ -1139,6 +1202,9 @@ var getDefaultValue = (schema) => {
|
|
|
1139
1202
|
return getDefaultValue(firstSchema);
|
|
1140
1203
|
}
|
|
1141
1204
|
}
|
|
1205
|
+
if (getMjstPrimitive2(schema) === "bigint") {
|
|
1206
|
+
return "0n";
|
|
1207
|
+
}
|
|
1142
1208
|
if (!hasType(schema)) {
|
|
1143
1209
|
return "undefined";
|
|
1144
1210
|
}
|
|
@@ -1234,6 +1300,24 @@ var generatePropertyAssertion = (key, propSchema, isRequired, typeName) => {
|
|
|
1234
1300
|
return lines;
|
|
1235
1301
|
if (hasRef(propSchema))
|
|
1236
1302
|
return lines;
|
|
1303
|
+
const instanceOf = getMjstInstanceOf2(propSchema);
|
|
1304
|
+
if (instanceOf) {
|
|
1305
|
+
if (isRequired) {
|
|
1306
|
+
lines.push(` if (!(${acc} instanceof ${instanceOf})) throw new Error('[${typeName}] field "${key}" must be ${instanceOf}');`);
|
|
1307
|
+
} else {
|
|
1308
|
+
lines.push(` if (${acc} !== undefined && !(${acc} instanceof ${instanceOf})) throw new Error('[${typeName}] field "${key}" must be ${instanceOf}');`);
|
|
1309
|
+
}
|
|
1310
|
+
return lines;
|
|
1311
|
+
}
|
|
1312
|
+
const primitive = getMjstPrimitive2(propSchema);
|
|
1313
|
+
if (primitive) {
|
|
1314
|
+
if (isRequired) {
|
|
1315
|
+
lines.push(` if (typeof ${acc} !== "${primitive}") throw new Error('[${typeName}] field "${key}" must be ${primitive}');`);
|
|
1316
|
+
} else {
|
|
1317
|
+
lines.push(` if (${acc} !== undefined && typeof ${acc} !== "${primitive}") throw new Error('[${typeName}] field "${key}" must be ${primitive}');`);
|
|
1318
|
+
}
|
|
1319
|
+
return lines;
|
|
1320
|
+
}
|
|
1237
1321
|
if (hasEnum(propSchema)) {
|
|
1238
1322
|
const allowed = JSON.stringify(propSchema.enum);
|
|
1239
1323
|
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
@@ -1270,6 +1354,14 @@ var generateObjectStrictAssertion = (schema, typeName) => {
|
|
|
1270
1354
|
return lines;
|
|
1271
1355
|
};
|
|
1272
1356
|
var generateScalarStrictAssertion = (schema, typeName) => {
|
|
1357
|
+
const instanceOf = getMjstInstanceOf2(schema);
|
|
1358
|
+
if (instanceOf) {
|
|
1359
|
+
return ` if (!(input instanceof ${instanceOf})) throw new Error(\`[${typeName}] expected ${instanceOf}, got \${input === null ? "null" : typeof input}\`);`;
|
|
1360
|
+
}
|
|
1361
|
+
const primitive = getMjstPrimitive2(schema);
|
|
1362
|
+
if (primitive) {
|
|
1363
|
+
return ` if (typeof input !== "${primitive}") throw new Error(\`[${typeName}] expected ${primitive}, got \${input === null ? "null" : typeof input}\`);`;
|
|
1364
|
+
}
|
|
1273
1365
|
if (!isSchemaObject3(schema) || !hasType(schema))
|
|
1274
1366
|
return null;
|
|
1275
1367
|
const t = schema.type;
|
|
@@ -1329,6 +1421,14 @@ var generateEnumCheck = (accessor, enumValues) => {
|
|
|
1329
1421
|
// src/generators/generate-schema-checks.ts
|
|
1330
1422
|
var generateSchemaChecks = (accessor, schema) => {
|
|
1331
1423
|
const checks = [];
|
|
1424
|
+
const instanceOf = getMjstInstanceOf2(schema);
|
|
1425
|
+
if (instanceOf) {
|
|
1426
|
+
return [`${accessor} instanceof ${instanceOf}`];
|
|
1427
|
+
}
|
|
1428
|
+
const primitive = getMjstPrimitive2(schema);
|
|
1429
|
+
if (primitive) {
|
|
1430
|
+
return [`typeof ${accessor} === "${primitive}"`];
|
|
1431
|
+
}
|
|
1332
1432
|
if (!hasType(schema)) {
|
|
1333
1433
|
return checks;
|
|
1334
1434
|
}
|
|
@@ -1461,6 +1561,11 @@ var generateNonDiscriminatedUnionValidation = (accessor, schemas, defaultValue,
|
|
|
1461
1561
|
};
|
|
1462
1562
|
|
|
1463
1563
|
// src/generators/generate-validation-expression.ts
|
|
1564
|
+
var getInstanceCoercion = (accessor, instanceOf) => {
|
|
1565
|
+
if (instanceOf === "Date")
|
|
1566
|
+
return `new Date(${accessor} as string | number | Date)`;
|
|
1567
|
+
return null;
|
|
1568
|
+
};
|
|
1464
1569
|
var getTypeCoercion = (accessor, schema, defaultValue) => {
|
|
1465
1570
|
if (!hasType(schema)) {
|
|
1466
1571
|
return null;
|
|
@@ -1489,6 +1594,20 @@ var generateValidationExpression = (key, schema, defaultValue, isRequired, rootS
|
|
|
1489
1594
|
if (!isSchemaObject3(schema)) {
|
|
1490
1595
|
return isRequired ? `${accessor} ?? ${defaultValue}` : accessor;
|
|
1491
1596
|
}
|
|
1597
|
+
const instanceOf = getMjstInstanceOf2(schema);
|
|
1598
|
+
if (instanceOf) {
|
|
1599
|
+
const valid = `${accessor} instanceof ${instanceOf}`;
|
|
1600
|
+
const coercion = getInstanceCoercion(accessor, instanceOf);
|
|
1601
|
+
if (isRequired) {
|
|
1602
|
+
return coercion ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : ${defaultValue})` : `${valid} ? ${accessor} : ${defaultValue}`;
|
|
1603
|
+
}
|
|
1604
|
+
return coercion ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : undefined)` : `${valid} ? ${accessor} : undefined`;
|
|
1605
|
+
}
|
|
1606
|
+
const primitive = getMjstPrimitive2(schema);
|
|
1607
|
+
if (primitive) {
|
|
1608
|
+
const valid = `typeof ${accessor} === "${primitive}"`;
|
|
1609
|
+
return isRequired ? `${valid} ? ${accessor} : ${defaultValue}` : `${valid} ? ${accessor} : undefined`;
|
|
1610
|
+
}
|
|
1492
1611
|
if (hasRef(schema) && rootSchema) {
|
|
1493
1612
|
const ref = schema.$ref;
|
|
1494
1613
|
if (visitedRefs?.has(ref)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-parsers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"json-schema-typed": "
|
|
52
|
-
"@amritk/helpers": "
|
|
53
|
-
"@amritk/generate-markdown": "
|
|
51
|
+
"json-schema-typed": "^8.0.1",
|
|
52
|
+
"@amritk/helpers": "0.4.0",
|
|
53
|
+
"@amritk/generate-markdown": "0.2.1"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -319,4 +319,12 @@ describe('generate-schema-checks', () => {
|
|
|
319
319
|
const result = generateSchemaChecks('input?.name', { type: 'string', minLength: 1 })
|
|
320
320
|
expect(result).toEqual(['typeof input?.name === "string"', 'input?.name.length >= 1'])
|
|
321
321
|
})
|
|
322
|
+
|
|
323
|
+
it('returns an instanceof check for an x-mjst instanceOf node', () => {
|
|
324
|
+
expect(generateSchemaChecks('value', { 'x-mjst': { instanceOf: 'Date' } })).toEqual(['value instanceof Date'])
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('returns a typeof check for an x-mjst bigint node', () => {
|
|
328
|
+
expect(generateSchemaChecks('value', { 'x-mjst': { primitive: 'bigint' } })).toEqual(['typeof value === "bigint"'])
|
|
329
|
+
})
|
|
322
330
|
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
|
|
1
2
|
import {
|
|
2
3
|
hasAdditionalProperties,
|
|
3
4
|
hasEnum,
|
|
@@ -28,6 +29,18 @@ import { generateEnumCheck } from './generate-enum-check'
|
|
|
28
29
|
export const generateSchemaChecks = (accessor: string, schema: JSONSchema): string[] => {
|
|
29
30
|
const checks: string[] = []
|
|
30
31
|
|
|
32
|
+
// x-mjst instanceOf nodes validate purely by instance check (no native type).
|
|
33
|
+
const instanceOf = getMjstInstanceOf(schema)
|
|
34
|
+
if (instanceOf) {
|
|
35
|
+
return [`${accessor} instanceof ${instanceOf}`]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// x-mjst primitive nodes (e.g. bigint) validate purely by typeof check.
|
|
39
|
+
const primitive = getMjstPrimitive(schema)
|
|
40
|
+
if (primitive) {
|
|
41
|
+
return [`typeof ${accessor} === "${primitive}"`]
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
if (!hasType(schema)) {
|
|
32
45
|
return checks
|
|
33
46
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion'
|
|
4
|
+
|
|
5
|
+
describe('generate-strict-assertion x-mjst instanceOf', () => {
|
|
6
|
+
it('asserts instanceof for a required Date property', () => {
|
|
7
|
+
const schema = {
|
|
8
|
+
type: 'object' as const,
|
|
9
|
+
properties: { createdAt: { 'x-mjst': { instanceOf: 'Date' } } },
|
|
10
|
+
required: ['createdAt'],
|
|
11
|
+
}
|
|
12
|
+
const lines = generateObjectStrictAssertion(schema, 'Event').join('\n')
|
|
13
|
+
|
|
14
|
+
expect(lines).toContain('missing required property "createdAt"')
|
|
15
|
+
expect(lines).toContain('!(input.createdAt instanceof Date)')
|
|
16
|
+
expect(lines).toContain('field "createdAt" must be Date')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('guards undefined before asserting instanceof for an optional Date property', () => {
|
|
20
|
+
const schema = {
|
|
21
|
+
type: 'object' as const,
|
|
22
|
+
properties: { createdAt: { 'x-mjst': { instanceOf: 'Date' } } },
|
|
23
|
+
}
|
|
24
|
+
const lines = generateObjectStrictAssertion(schema, 'Event').join('\n')
|
|
25
|
+
|
|
26
|
+
expect(lines).toContain('input.createdAt !== undefined && !(input.createdAt instanceof Date)')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('asserts instanceof for a top-level Date schema', () => {
|
|
30
|
+
const line = generateScalarStrictAssertion({ 'x-mjst': { instanceOf: 'Date' } }, 'When')
|
|
31
|
+
|
|
32
|
+
expect(line).toContain('!(input instanceof Date)')
|
|
33
|
+
expect(line).toContain('expected Date')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('generate-strict-assertion x-mjst primitive', () => {
|
|
38
|
+
it('asserts typeof bigint for a required property', () => {
|
|
39
|
+
const schema = {
|
|
40
|
+
type: 'object' as const,
|
|
41
|
+
properties: { balance: { 'x-mjst': { primitive: 'bigint' } } },
|
|
42
|
+
required: ['balance'],
|
|
43
|
+
}
|
|
44
|
+
const lines = generateObjectStrictAssertion(schema, 'Account').join('\n')
|
|
45
|
+
|
|
46
|
+
expect(lines).toContain('missing required property "balance"')
|
|
47
|
+
expect(lines).toContain('typeof input.balance !== "bigint"')
|
|
48
|
+
expect(lines).toContain('field "balance" must be bigint')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('guards undefined before asserting typeof for an optional bigint property', () => {
|
|
52
|
+
const schema = {
|
|
53
|
+
type: 'object' as const,
|
|
54
|
+
properties: { balance: { 'x-mjst': { primitive: 'bigint' } } },
|
|
55
|
+
}
|
|
56
|
+
const lines = generateObjectStrictAssertion(schema, 'Account').join('\n')
|
|
57
|
+
|
|
58
|
+
expect(lines).toContain('input.balance !== undefined && typeof input.balance !== "bigint"')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('asserts typeof for a top-level bigint schema', () => {
|
|
62
|
+
const line = generateScalarStrictAssertion({ 'x-mjst': { primitive: 'bigint' } }, 'Big')
|
|
63
|
+
|
|
64
|
+
expect(line).toContain('typeof input !== "bigint"')
|
|
65
|
+
expect(line).toContain('expected bigint')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
|
|
1
2
|
import { safeAccessor } from '@amritk/helpers/safe-accessor'
|
|
2
3
|
import {
|
|
3
4
|
hasEnum,
|
|
@@ -134,6 +135,34 @@ const generatePropertyAssertion = (
|
|
|
134
135
|
if (!isSchemaObject(propSchema)) return lines
|
|
135
136
|
if (hasRef(propSchema)) return lines
|
|
136
137
|
|
|
138
|
+
const instanceOf = getMjstInstanceOf(propSchema)
|
|
139
|
+
if (instanceOf) {
|
|
140
|
+
if (isRequired) {
|
|
141
|
+
lines.push(
|
|
142
|
+
` if (!(${acc} instanceof ${instanceOf})) throw new Error('[${typeName}] field "${key}" must be ${instanceOf}');`,
|
|
143
|
+
)
|
|
144
|
+
} else {
|
|
145
|
+
lines.push(
|
|
146
|
+
` if (${acc} !== undefined && !(${acc} instanceof ${instanceOf})) throw new Error('[${typeName}] field "${key}" must be ${instanceOf}');`,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
return lines
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const primitive = getMjstPrimitive(propSchema)
|
|
153
|
+
if (primitive) {
|
|
154
|
+
if (isRequired) {
|
|
155
|
+
lines.push(
|
|
156
|
+
` if (typeof ${acc} !== "${primitive}") throw new Error('[${typeName}] field "${key}" must be ${primitive}');`,
|
|
157
|
+
)
|
|
158
|
+
} else {
|
|
159
|
+
lines.push(
|
|
160
|
+
` if (${acc} !== undefined && typeof ${acc} !== "${primitive}") throw new Error('[${typeName}] field "${key}" must be ${primitive}');`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
return lines
|
|
164
|
+
}
|
|
165
|
+
|
|
137
166
|
if (hasEnum(propSchema)) {
|
|
138
167
|
const allowed = JSON.stringify(propSchema.enum)
|
|
139
168
|
const label = (propSchema.enum as unknown[]).map((v) => JSON.stringify(v)).join(', ')
|
|
@@ -202,6 +231,16 @@ export const generateObjectStrictAssertion = (schema: JSONSchema, typeName: stri
|
|
|
202
231
|
* Returns null when the schema has no type information to assert on.
|
|
203
232
|
*/
|
|
204
233
|
export const generateScalarStrictAssertion = (schema: JSONSchema, typeName: string): string | null => {
|
|
234
|
+
const instanceOf = getMjstInstanceOf(schema)
|
|
235
|
+
if (instanceOf) {
|
|
236
|
+
return ` if (!(input instanceof ${instanceOf})) throw new Error(\`[${typeName}] expected ${instanceOf}, got \${input === null ? "null" : typeof input}\`);`
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const primitive = getMjstPrimitive(schema)
|
|
240
|
+
if (primitive) {
|
|
241
|
+
return ` if (typeof input !== "${primitive}") throw new Error(\`[${typeName}] expected ${primitive}, got \${input === null ? "null" : typeof input}\`);`
|
|
242
|
+
}
|
|
243
|
+
|
|
205
244
|
if (!isSchemaObject(schema) || !hasType(schema)) return null
|
|
206
245
|
const t = schema.type as string
|
|
207
246
|
const wrongType = wrongTypeCondition('input', t)
|
|
@@ -791,4 +791,41 @@ describe('generate-validation-expression', () => {
|
|
|
791
791
|
|
|
792
792
|
expect(result).toContain('? input?.status : undefined')
|
|
793
793
|
})
|
|
794
|
+
|
|
795
|
+
it('checks instanceof and coerces a required x-mjst Date field', () => {
|
|
796
|
+
const schema = { 'x-mjst': { instanceOf: 'Date' } }
|
|
797
|
+
const result = generateValidationExpression('createdAt', schema, '{}', true)
|
|
798
|
+
|
|
799
|
+
expect(result).toContain('input?.createdAt instanceof Date')
|
|
800
|
+
expect(result).toContain('new Date(input?.createdAt as string | number | Date)')
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
it('checks instanceof and coerces an optional x-mjst Date field', () => {
|
|
804
|
+
const schema = { 'x-mjst': { instanceOf: 'Date' } }
|
|
805
|
+
const result = generateValidationExpression('createdAt', schema, 'undefined', false)
|
|
806
|
+
|
|
807
|
+
expect(result).toContain('input?.createdAt instanceof Date')
|
|
808
|
+
expect(result).toContain('? input?.createdAt : (input?.createdAt !== undefined ? new Date')
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
it('falls back to the default for an unknown instanceOf with no coercion', () => {
|
|
812
|
+
const schema = { 'x-mjst': { instanceOf: 'CustomThing' } }
|
|
813
|
+
const result = generateValidationExpression('value', schema, '{}', true)
|
|
814
|
+
|
|
815
|
+
expect(result).toBe('input?.value instanceof CustomThing ? input?.value : {}')
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
it('checks typeof bigint for a required x-mjst primitive field', () => {
|
|
819
|
+
const schema = { 'x-mjst': { primitive: 'bigint' } }
|
|
820
|
+
const result = generateValidationExpression('balance', schema, '0n', true)
|
|
821
|
+
|
|
822
|
+
expect(result).toBe('typeof input?.balance === "bigint" ? input?.balance : 0n')
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
it('checks typeof bigint for an optional x-mjst primitive field', () => {
|
|
826
|
+
const schema = { 'x-mjst': { primitive: 'bigint' } }
|
|
827
|
+
const result = generateValidationExpression('balance', schema, '0n', false)
|
|
828
|
+
|
|
829
|
+
expect(result).toBe('typeof input?.balance === "bigint" ? input?.balance : undefined')
|
|
830
|
+
})
|
|
794
831
|
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'
|
|
1
2
|
import { resolveRef } from '@amritk/helpers/resolve-ref'
|
|
2
3
|
import { safeAccessor } from '@amritk/helpers/safe-accessor'
|
|
3
4
|
import {
|
|
@@ -34,6 +35,17 @@ import { generateEnumCheck } from './generate-enum-check'
|
|
|
34
35
|
import { generateNonDiscriminatedUnionValidation } from './generate-non-discriminated-union-validation'
|
|
35
36
|
import { generateSchemaChecks } from './generate-schema-checks'
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Coercion expression for an `x-mjst` instanceOf value, when one is known.
|
|
40
|
+
* `Date` can be reconstructed from the common JSON encodings (ISO string or
|
|
41
|
+
* epoch number); other classes have no safe generic coercion, so we return null
|
|
42
|
+
* and the caller falls back to the default.
|
|
43
|
+
*/
|
|
44
|
+
const getInstanceCoercion = (accessor: string, instanceOf: string): string | null => {
|
|
45
|
+
if (instanceOf === 'Date') return `new Date(${accessor} as string | number | Date)`
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
/**
|
|
38
50
|
* Generates a type coercion expression for converting a value to the expected type.
|
|
39
51
|
*/
|
|
@@ -85,6 +97,34 @@ export const generateValidationExpression = (
|
|
|
85
97
|
return isRequired ? `${accessor} ?? ${defaultValue}` : accessor
|
|
86
98
|
}
|
|
87
99
|
|
|
100
|
+
// Handle x-mjst instanceOf (e.g. Date): the value must be an instance of the
|
|
101
|
+
// named class. Invalid values are coerced when a coercer is known, otherwise
|
|
102
|
+
// they fall back to the default (required) or undefined (optional).
|
|
103
|
+
const instanceOf = getMjstInstanceOf(schema)
|
|
104
|
+
if (instanceOf) {
|
|
105
|
+
const valid = `${accessor} instanceof ${instanceOf}`
|
|
106
|
+
const coercion = getInstanceCoercion(accessor, instanceOf)
|
|
107
|
+
|
|
108
|
+
if (isRequired) {
|
|
109
|
+
return coercion
|
|
110
|
+
? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : ${defaultValue})`
|
|
111
|
+
: `${valid} ? ${accessor} : ${defaultValue}`
|
|
112
|
+
}
|
|
113
|
+
return coercion
|
|
114
|
+
? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : undefined)`
|
|
115
|
+
: `${valid} ? ${accessor} : undefined`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle x-mjst primitive (e.g. bigint): validate with typeof. We do not
|
|
119
|
+
// coerce — BigInt() throws on invalid input, which a pure coercion expression
|
|
120
|
+
// cannot guard — so invalid values fall back to the default (required) or
|
|
121
|
+
// undefined (optional).
|
|
122
|
+
const primitive = getMjstPrimitive(schema)
|
|
123
|
+
if (primitive) {
|
|
124
|
+
const valid = `typeof ${accessor} === "${primitive}"`
|
|
125
|
+
return isRequired ? `${valid} ? ${accessor} : ${defaultValue}` : `${valid} ? ${accessor} : undefined`
|
|
126
|
+
}
|
|
127
|
+
|
|
88
128
|
// Handle $ref - resolve and validate against the referenced schema
|
|
89
129
|
if (hasRef(schema) && rootSchema) {
|
|
90
130
|
const ref = schema.$ref
|
|
@@ -132,4 +132,8 @@ describe('get-default-value', () => {
|
|
|
132
132
|
it('handles empty oneOf array and falls back', () => {
|
|
133
133
|
expect(getDefaultValue({ oneOf: [], type: 'boolean' })).toBe('false')
|
|
134
134
|
})
|
|
135
|
+
|
|
136
|
+
it('returns 0n for an x-mjst bigint schema', () => {
|
|
137
|
+
expect(getDefaultValue({ 'x-mjst': { primitive: 'bigint' } })).toBe('0n')
|
|
138
|
+
})
|
|
135
139
|
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getMjstPrimitive } from '@amritk/helpers/mjst-extension'
|
|
1
2
|
import {
|
|
2
3
|
hasAllOf,
|
|
3
4
|
hasAnyOf,
|
|
@@ -60,6 +61,11 @@ export const getDefaultValue = (schema: JSONSchema): string => {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
// x-mjst bigint has no native JSON type but a sensible zero default.
|
|
65
|
+
if (getMjstPrimitive(schema) === 'bigint') {
|
|
66
|
+
return '0n'
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
if (!hasType(schema)) {
|
|
64
70
|
return 'undefined'
|
|
65
71
|
}
|