@atscript/typescript 0.1.87 → 0.1.89
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/cli.cjs +427 -389
- package/dist/index.cjs +4 -5
- package/dist/index.mjs +1 -2
- package/dist/{json-schema-3mti4G4J.cjs → json-schema-DXEiCfe1.cjs} +206 -97
- package/dist/{json-schema-B64qclVa.mjs → json-schema-DycaD0Rm.mjs} +207 -98
- package/dist/{plugin-CYzZm3rk.mjs → plugin-BOoquMkD.mjs} +117 -152
- package/dist/{plugin-CZ5g5D9I.cjs → plugin-DMBkdYCf.cjs} +180 -211
- package/dist/test-utils.cjs +16 -9
- package/dist/test-utils.mjs +9 -3
- package/dist/utils.cjs +176 -106
- package/dist/utils.mjs +153 -83
- package/package.json +2 -2
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
//#region packages/typescript/src/runtime/validator.ts
|
|
3
2
|
function _define_property(obj, key, value) {
|
|
4
3
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
@@ -7,11 +6,31 @@ function _define_property(obj, key, value) {
|
|
|
7
6
|
configurable: true,
|
|
8
7
|
writable: true
|
|
9
8
|
});
|
|
10
|
-
else obj[key] = value;
|
|
9
|
+
else obj[key] = value;
|
|
11
10
|
return obj;
|
|
12
11
|
}
|
|
13
|
-
const regexCache = new Map();
|
|
14
|
-
|
|
12
|
+
const regexCache = /* @__PURE__ */ new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Validates values against an {@link TAtscriptAnnotatedType} definition.
|
|
15
|
+
*
|
|
16
|
+
* `DataType` is automatically inferred from the type definition's phantom generic,
|
|
17
|
+
* enabling the {@link validate} method to act as a type guard.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* // From a generated interface class:
|
|
22
|
+
* const validator = new Validator(MyInterface)
|
|
23
|
+
* if (validator.validate(data, true)) {
|
|
24
|
+
* data // narrowed to MyInterface
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // Or use the built-in factory:
|
|
28
|
+
* MyInterface.validator().validate(data)
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @typeParam T - The annotated type definition.
|
|
32
|
+
* @typeParam DataType - The TypeScript type that `validate` narrows to (auto-inferred).
|
|
33
|
+
*/ var Validator = class {
|
|
15
34
|
buildPath() {
|
|
16
35
|
if (this.depth <= 0) return "";
|
|
17
36
|
let path = this.pathSegments[0];
|
|
@@ -26,7 +45,7 @@ var Validator = class {
|
|
|
26
45
|
pop(saveErrors) {
|
|
27
46
|
this.depth--;
|
|
28
47
|
const popped = this.stackErrors.pop();
|
|
29
|
-
if (saveErrors && popped !== null && popped !==
|
|
48
|
+
if (saveErrors && popped !== null && popped !== void 0 && popped.length > 0) for (const err of popped) this.error(err.message, err.path, err.details);
|
|
30
49
|
return popped;
|
|
31
50
|
}
|
|
32
51
|
clear() {
|
|
@@ -67,7 +86,7 @@ var Validator = class {
|
|
|
67
86
|
this.limitExceeded = false;
|
|
68
87
|
this.context = context;
|
|
69
88
|
const passed = this.validateSafe(this.def, value);
|
|
70
|
-
this.context =
|
|
89
|
+
this.context = void 0;
|
|
71
90
|
if (!passed) {
|
|
72
91
|
if (safe) return false;
|
|
73
92
|
this.throw();
|
|
@@ -78,13 +97,13 @@ var Validator = class {
|
|
|
78
97
|
if (this.limitExceeded) return false;
|
|
79
98
|
if (this.hasReplace) {
|
|
80
99
|
let replaced = this.replaceCache.get(def);
|
|
81
|
-
if (replaced ===
|
|
100
|
+
if (replaced === void 0) {
|
|
82
101
|
replaced = this.opts.replace(def, this.buildPath());
|
|
83
102
|
this.replaceCache.set(def, replaced);
|
|
84
103
|
}
|
|
85
104
|
def = replaced;
|
|
86
105
|
}
|
|
87
|
-
if (def.optional && (value ===
|
|
106
|
+
if (def.optional && (value === void 0 || value === null)) return true;
|
|
88
107
|
if (this.hasPlugins) for (const plugin of this.opts.plugins) {
|
|
89
108
|
const result = plugin(this, def, value);
|
|
90
109
|
if (result === false || result === true) return result;
|
|
@@ -96,10 +115,9 @@ var Validator = class {
|
|
|
96
115
|
}
|
|
97
116
|
validateAnnotatedType(def, value) {
|
|
98
117
|
switch (def.type.kind) {
|
|
99
|
-
case "":
|
|
118
|
+
case "":
|
|
100
119
|
if (def.type.designType === "phantom") return true;
|
|
101
120
|
return this.validatePrimitive(def, value);
|
|
102
|
-
}
|
|
103
121
|
case "object": return this.validateObject(def, value);
|
|
104
122
|
case "array": return this.validateArray(def, value);
|
|
105
123
|
case "union": return this.validateUnion(def, value);
|
|
@@ -120,10 +138,10 @@ var Validator = class {
|
|
|
120
138
|
const branchErrors = this.stackErrors.pop();
|
|
121
139
|
if (this.limitExceeded) this.limitExceeded = false;
|
|
122
140
|
if (branchErrors) if (details) for (const err of branchErrors) details.push(err);
|
|
123
|
-
else details = branchErrors;
|
|
141
|
+
else details = branchErrors;
|
|
124
142
|
}
|
|
125
143
|
const expected = items.map((item, i) => `[${item.type.kind || item.type.designType}(${i})]`).join(", ");
|
|
126
|
-
this.error(`Value does not match any of the allowed types: ${expected}`,
|
|
144
|
+
this.error(`Value does not match any of the allowed types: ${expected}`, void 0, details);
|
|
127
145
|
return false;
|
|
128
146
|
}
|
|
129
147
|
validateIntersection(def, value) {
|
|
@@ -173,8 +191,8 @@ else details = branchErrors;
|
|
|
173
191
|
const uniqueItems = def.metadata.get("expect.array.uniqueItems");
|
|
174
192
|
if (uniqueItems) {
|
|
175
193
|
const separator = "▼↩";
|
|
176
|
-
const seen = new Set();
|
|
177
|
-
const keyProps = new Set();
|
|
194
|
+
const seen = /* @__PURE__ */ new Set();
|
|
195
|
+
const keyProps = /* @__PURE__ */ new Set();
|
|
178
196
|
if (def.type.of.type.kind === "object") {
|
|
179
197
|
for (const [key, val] of def.type.of.type.props.entries()) if (val.metadata.get("expect.array.key")) keyProps.add(key);
|
|
180
198
|
}
|
|
@@ -219,7 +237,7 @@ else details = branchErrors;
|
|
|
219
237
|
const path = this.depth > 0 ? `${this.buildPath()}.` : "";
|
|
220
238
|
for (const item of this.opts.skipList) if (item.startsWith(path)) {
|
|
221
239
|
const key = item.slice(path.length);
|
|
222
|
-
if (!skipList) skipList = new Set();
|
|
240
|
+
if (!skipList) skipList = /* @__PURE__ */ new Set();
|
|
223
241
|
skipList.add(key);
|
|
224
242
|
}
|
|
225
243
|
}
|
|
@@ -227,12 +245,12 @@ else details = branchErrors;
|
|
|
227
245
|
if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.buildPath());
|
|
228
246
|
for (const [key, item] of def.type.props.entries()) {
|
|
229
247
|
if (skipList && skipList.has(key) || isPhantomType(item)) continue;
|
|
230
|
-
if (value[key] ===
|
|
248
|
+
if (value[key] === void 0) {
|
|
231
249
|
if (partialFunctionMatched || this.opts.partial === "deep" || this.opts.partial === true && this.depth === 0) continue;
|
|
232
250
|
}
|
|
233
251
|
this.push(key);
|
|
234
252
|
if (this.validateSafe(item, value[key])) this.pop(false);
|
|
235
|
-
else {
|
|
253
|
+
else {
|
|
236
254
|
passed = false;
|
|
237
255
|
this.pop(true);
|
|
238
256
|
if (this.limitExceeded) return false;
|
|
@@ -282,7 +300,7 @@ else {
|
|
|
282
300
|
return passed;
|
|
283
301
|
}
|
|
284
302
|
validatePrimitive(def, value) {
|
|
285
|
-
if (def.type.value !==
|
|
303
|
+
if (def.type.value !== void 0) {
|
|
286
304
|
if (value !== def.type.value) {
|
|
287
305
|
this.error(`Expected ${def.type.value}, got ${value}`);
|
|
288
306
|
return false;
|
|
@@ -291,47 +309,41 @@ else {
|
|
|
291
309
|
}
|
|
292
310
|
const typeOfValue = Array.isArray(value) ? "array" : typeof value;
|
|
293
311
|
switch (def.type.designType) {
|
|
294
|
-
case "never":
|
|
312
|
+
case "never":
|
|
295
313
|
this.error(`This type is impossible, must be an internal problem`);
|
|
296
314
|
return false;
|
|
297
|
-
}
|
|
298
315
|
case "any": return true;
|
|
299
|
-
case "string":
|
|
316
|
+
case "string":
|
|
300
317
|
if (typeOfValue !== def.type.designType) {
|
|
301
318
|
this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
|
|
302
319
|
return false;
|
|
303
320
|
}
|
|
304
321
|
return this.validateString(def, value);
|
|
305
|
-
|
|
306
|
-
case "number": {
|
|
322
|
+
case "number":
|
|
307
323
|
if (typeOfValue !== def.type.designType) {
|
|
308
324
|
this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
|
|
309
325
|
return false;
|
|
310
326
|
}
|
|
311
327
|
return this.validateNumber(def, value);
|
|
312
|
-
|
|
313
|
-
case "boolean": {
|
|
328
|
+
case "boolean":
|
|
314
329
|
if (typeOfValue !== def.type.designType) {
|
|
315
330
|
this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
|
|
316
331
|
return false;
|
|
317
332
|
}
|
|
318
333
|
return this.validateBoolean(def, value);
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (value !== undefined) {
|
|
334
|
+
case "undefined":
|
|
335
|
+
if (value !== void 0) {
|
|
322
336
|
this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
|
|
323
337
|
return false;
|
|
324
338
|
}
|
|
325
339
|
return true;
|
|
326
|
-
|
|
327
|
-
case "null": {
|
|
340
|
+
case "null":
|
|
328
341
|
if (value !== null) {
|
|
329
342
|
this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
|
|
330
343
|
return false;
|
|
331
344
|
}
|
|
332
345
|
return true;
|
|
333
|
-
|
|
334
|
-
case "decimal": {
|
|
346
|
+
case "decimal":
|
|
335
347
|
if (typeOfValue !== "string") {
|
|
336
348
|
this.error(`Expected string (decimal), got ${typeOfValue}`);
|
|
337
349
|
return false;
|
|
@@ -341,7 +353,6 @@ else {
|
|
|
341
353
|
return false;
|
|
342
354
|
}
|
|
343
355
|
return true;
|
|
344
|
-
}
|
|
345
356
|
default: throw new Error(`Unknown type "${def.type.designType}"`);
|
|
346
357
|
}
|
|
347
358
|
}
|
|
@@ -456,10 +467,10 @@ else {
|
|
|
456
467
|
};
|
|
457
468
|
this.hasPlugins = this.opts.plugins.length > 0;
|
|
458
469
|
this.hasReplace = typeof this.opts.replace === "function";
|
|
459
|
-
if (this.hasReplace) this.replaceCache = new WeakMap();
|
|
470
|
+
if (this.hasReplace) this.replaceCache = /* @__PURE__ */ new WeakMap();
|
|
460
471
|
}
|
|
461
472
|
};
|
|
462
|
-
var ValidatorError = class extends Error {
|
|
473
|
+
/** Error thrown by {@link Validator.validate} when validation fails. Contains structured error details. */ var ValidatorError = class extends Error {
|
|
463
474
|
constructor(errors) {
|
|
464
475
|
super(`${errors[0].path ? errors[0].path + ": " : ""}${errors[0].message}`), _define_property(this, "errors", void 0), _define_property(this, "name", void 0), this.errors = errors, this.name = "Validation Error";
|
|
465
476
|
}
|
|
@@ -476,7 +487,10 @@ const NON_PRIMITIVE_KINDS = new Set(["array", "object"]);
|
|
|
476
487
|
/** Shared validator method reused by all annotated type nodes. */ function validatorMethod(opts) {
|
|
477
488
|
return new Validator(this, opts);
|
|
478
489
|
}
|
|
479
|
-
|
|
490
|
+
/**
|
|
491
|
+
* Creates a minimal annotated type node from a type def and metadata map.
|
|
492
|
+
* Centralises the shape so callers never duplicate the boilerplate.
|
|
493
|
+
*/ function createAnnotatedTypeNode(type, metadata, opts) {
|
|
480
494
|
return {
|
|
481
495
|
__is_atscript_annotated_type: true,
|
|
482
496
|
type,
|
|
@@ -487,19 +501,27 @@ function createAnnotatedTypeNode(type, metadata, opts) {
|
|
|
487
501
|
ref: opts?.ref
|
|
488
502
|
};
|
|
489
503
|
}
|
|
490
|
-
|
|
504
|
+
/**
|
|
505
|
+
* Type Guard to check if a type is atscript-annotated
|
|
506
|
+
*/ function isAnnotatedType(type) {
|
|
491
507
|
return type && type.__is_atscript_annotated_type;
|
|
492
508
|
}
|
|
493
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Standalone annotate function that handles both replace and append (array) strategies.
|
|
511
|
+
* Used by the handle's .annotate() method and by generated mutation statements.
|
|
512
|
+
*/ function annotate(metadata, key, value, asArray) {
|
|
494
513
|
if (!metadata) return;
|
|
495
514
|
if (asArray) if (metadata.has(key)) {
|
|
496
515
|
const a = metadata.get(key);
|
|
497
516
|
if (Array.isArray(a)) a.push(value);
|
|
498
|
-
else metadata.set(key, [a, value]);
|
|
517
|
+
else metadata.set(key, [a, value]);
|
|
499
518
|
} else metadata.set(key, [value]);
|
|
500
|
-
else metadata.set(key, value);
|
|
519
|
+
else metadata.set(key, value);
|
|
501
520
|
}
|
|
502
|
-
|
|
521
|
+
/**
|
|
522
|
+
* Clones a property's type tree in-place so mutations don't leak to shared refs.
|
|
523
|
+
* Used by mutating annotate codegen when paths cross ref boundaries.
|
|
524
|
+
*/ function cloneRefProp(parentType, propName) {
|
|
503
525
|
if (parentType.kind !== "object") return;
|
|
504
526
|
const objType = parentType;
|
|
505
527
|
const existing = objType.props.get(propName);
|
|
@@ -513,7 +535,7 @@ function cloneRefProp(parentType, propName) {
|
|
|
513
535
|
function cloneTypeDef(type) {
|
|
514
536
|
if (type.kind === "object") {
|
|
515
537
|
const obj = type;
|
|
516
|
-
const props = new Map();
|
|
538
|
+
const props = /* @__PURE__ */ new Map();
|
|
517
539
|
for (const [k, v] of obj.props) props.set(k, createAnnotatedTypeNode(v.type, new Map(v.metadata), {
|
|
518
540
|
id: v.id,
|
|
519
541
|
optional: v.optional
|
|
@@ -546,18 +568,36 @@ function cloneTypeDef(type) {
|
|
|
546
568
|
tags: new Set(type.tags)
|
|
547
569
|
};
|
|
548
570
|
}
|
|
549
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Creates a builder handle for constructing a {@link TAtscriptAnnotatedType} at runtime.
|
|
573
|
+
*
|
|
574
|
+
* This is primarily used by generated `.as.js` code. The returned handle provides
|
|
575
|
+
* a fluent API for setting the type definition, metadata, and properties.
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* ```ts
|
|
579
|
+
* const handle = defineAnnotatedType('object')
|
|
580
|
+
* .prop('name', defineAnnotatedType().designType('string').$type)
|
|
581
|
+
* .prop('age', defineAnnotatedType().designType('number').$type)
|
|
582
|
+
*
|
|
583
|
+
* handle.$type // the resulting TAtscriptAnnotatedType
|
|
584
|
+
* ```
|
|
585
|
+
*
|
|
586
|
+
* @param _kind - The kind of type to create (e.g. `'object'`, `'array'`, `'union'`). Defaults to `''` (primitive/final).
|
|
587
|
+
* @param base - Optional existing object to augment with annotated type fields.
|
|
588
|
+
* @returns A builder handle for fluent type construction.
|
|
589
|
+
*/ function defineAnnotatedType(_kind, base) {
|
|
550
590
|
const kind = _kind || "";
|
|
551
|
-
const baseProvided = base !==
|
|
591
|
+
const baseProvided = base !== void 0;
|
|
552
592
|
const type = base?.type || {};
|
|
553
593
|
type.kind = kind;
|
|
554
594
|
if (COMPLEX_KINDS.has(kind)) type.items = [];
|
|
555
595
|
if (kind === "object") {
|
|
556
|
-
type.props = new Map();
|
|
596
|
+
type.props = /* @__PURE__ */ new Map();
|
|
557
597
|
type.propsPatterns = [];
|
|
558
598
|
}
|
|
559
|
-
type.tags = new Set();
|
|
560
|
-
const metadata = base?.metadata || new Map();
|
|
599
|
+
type.tags = /* @__PURE__ */ new Set();
|
|
600
|
+
const metadata = base?.metadata || /* @__PURE__ */ new Map();
|
|
561
601
|
const payload = {
|
|
562
602
|
__is_atscript_annotated_type: true,
|
|
563
603
|
metadata,
|
|
@@ -565,7 +605,7 @@ function defineAnnotatedType(_kind, base) {
|
|
|
565
605
|
validator: validatorMethod
|
|
566
606
|
};
|
|
567
607
|
base = base ? Object.assign(base, payload) : payload;
|
|
568
|
-
|
|
608
|
+
return {
|
|
569
609
|
$type: base,
|
|
570
610
|
$def: type,
|
|
571
611
|
$metadata: metadata,
|
|
@@ -608,15 +648,15 @@ function defineAnnotatedType(_kind, base) {
|
|
|
608
648
|
for (const [key, value] of fromMetadata.entries()) if (!ignore || !ignore.has(key)) this.$metadata.set(key, value);
|
|
609
649
|
return this;
|
|
610
650
|
},
|
|
611
|
-
refTo(type
|
|
651
|
+
refTo(type, chain) {
|
|
612
652
|
const node = this.$type;
|
|
613
|
-
if (isAnnotatedType(type
|
|
614
|
-
let newBase = type
|
|
615
|
-
const typeName = type
|
|
653
|
+
if (isAnnotatedType(type)) {
|
|
654
|
+
let newBase = type;
|
|
655
|
+
const typeName = type.name || "Unknown";
|
|
616
656
|
if (chain) for (let i = 0; i < chain.length; i++) {
|
|
617
657
|
const c = chain[i];
|
|
618
658
|
if (newBase.type.kind === "object" && newBase.type.props.has(c)) newBase = newBase.type.props.get(c);
|
|
619
|
-
else {
|
|
659
|
+
else {
|
|
620
660
|
const keys = chain.slice(0, i + 1).map((k) => `["${k}"]`).join("");
|
|
621
661
|
throw new Error(`Can't find prop ${typeName}${keys}`);
|
|
622
662
|
}
|
|
@@ -624,11 +664,11 @@ else {
|
|
|
624
664
|
node.type = baseProvided ? cloneTypeDef(newBase.type) : newBase.type;
|
|
625
665
|
node.id = node.id ?? newBase.id;
|
|
626
666
|
if (chain && chain.length > 0) node.ref = {
|
|
627
|
-
type: () => type
|
|
667
|
+
type: () => type,
|
|
628
668
|
field: chain.join(".")
|
|
629
669
|
};
|
|
630
|
-
} else if (typeof type
|
|
631
|
-
const lazyType = type
|
|
670
|
+
} else if (typeof type === "function") {
|
|
671
|
+
const lazyType = type;
|
|
632
672
|
const ownId = node.id;
|
|
633
673
|
node.ref = {
|
|
634
674
|
type: lazyType,
|
|
@@ -648,7 +688,7 @@ else {
|
|
|
648
688
|
}
|
|
649
689
|
let target = t;
|
|
650
690
|
if (chain) for (const c of chain) if (target.type.kind === "object" && target.type.props.has(c)) target = target.type.props.get(c);
|
|
651
|
-
else return t.type;
|
|
691
|
+
else return t.type;
|
|
652
692
|
node.id = ownId ?? target.id;
|
|
653
693
|
const resolved = baseProvided ? cloneTypeDef(target.type) : target.type;
|
|
654
694
|
Object.defineProperty(node, "type", {
|
|
@@ -660,7 +700,7 @@ else return t.type;
|
|
|
660
700
|
},
|
|
661
701
|
configurable: true
|
|
662
702
|
});
|
|
663
|
-
} else throw new TypeError(`${type
|
|
703
|
+
} else throw new TypeError(`${type} is not annotated type`);
|
|
664
704
|
return this;
|
|
665
705
|
},
|
|
666
706
|
annotate(key, value, asArray) {
|
|
@@ -672,12 +712,21 @@ else return t.type;
|
|
|
672
712
|
return this;
|
|
673
713
|
}
|
|
674
714
|
};
|
|
675
|
-
return handle;
|
|
676
715
|
}
|
|
677
|
-
|
|
716
|
+
/**
|
|
717
|
+
* Checks whether an annotated type is a phantom type.
|
|
718
|
+
*
|
|
719
|
+
* Phantom types do not affect the data type, validation, or schema,
|
|
720
|
+
* but are discoverable via runtime type traversal.
|
|
721
|
+
*/ function isPhantomType(def) {
|
|
678
722
|
return def.type.kind === "" && def.type.designType === "phantom";
|
|
679
723
|
}
|
|
680
|
-
|
|
724
|
+
/**
|
|
725
|
+
* Checks whether an annotated type resolves to a primitive (non-object, non-array) shape.
|
|
726
|
+
*
|
|
727
|
+
* Returns `true` for final types and for unions/intersections/tuples
|
|
728
|
+
* whose members are all primitives.
|
|
729
|
+
*/ function isAnnotatedTypeOfPrimitive(t) {
|
|
681
730
|
if (NON_PRIMITIVE_KINDS.has(t.type.kind)) return false;
|
|
682
731
|
if (!t.type.kind) return true;
|
|
683
732
|
if (COMPLEX_KINDS.has(t.type.kind)) {
|
|
@@ -689,7 +738,17 @@ function isAnnotatedTypeOfPrimitive(t) {
|
|
|
689
738
|
|
|
690
739
|
//#endregion
|
|
691
740
|
//#region packages/typescript/src/runtime/traverse.ts
|
|
692
|
-
|
|
741
|
+
/**
|
|
742
|
+
* Type-safe dispatch over `TAtscriptAnnotatedType` by its `type.kind`.
|
|
743
|
+
*
|
|
744
|
+
* Provides the common `switch (def.type.kind)` pattern used by
|
|
745
|
+
* the validator, JSON-schema builder, and serializer.
|
|
746
|
+
* Each caller supplies its own handlers that control recursion.
|
|
747
|
+
*
|
|
748
|
+
* When a `phantom` handler is provided, phantom types (`designType === 'phantom'`)
|
|
749
|
+
* are dispatched to it instead of `final`. This allows consumers to skip or
|
|
750
|
+
* handle phantom props without polluting their `final` handler.
|
|
751
|
+
*/ function forAnnotatedType(def, handlers) {
|
|
693
752
|
switch (def.type.kind) {
|
|
694
753
|
case "": {
|
|
695
754
|
const typed = def;
|
|
@@ -707,21 +766,27 @@ function forAnnotatedType(def, handlers) {
|
|
|
707
766
|
|
|
708
767
|
//#endregion
|
|
709
768
|
//#region packages/typescript/src/runtime/json-schema.ts
|
|
710
|
-
|
|
769
|
+
/**
|
|
770
|
+
* Detects a discriminator property across union items.
|
|
771
|
+
*
|
|
772
|
+
* Scans all items for object-typed members that share a common property
|
|
773
|
+
* with distinct const/literal values. If exactly one such property exists,
|
|
774
|
+
* it is returned as the discriminator. Returns `null` when no qualifying
|
|
775
|
+
* property exists, or when more than one does (ambiguous).
|
|
776
|
+
*/ function detectDiscriminator(items) {
|
|
711
777
|
if (items.length < 2) return null;
|
|
712
778
|
for (const item of items) if (item.type.kind !== "object") return null;
|
|
713
779
|
const firstObj = items[0].type;
|
|
714
780
|
const candidates = [];
|
|
715
|
-
for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !==
|
|
781
|
+
for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== void 0) candidates.push(propName);
|
|
716
782
|
let result = null;
|
|
717
783
|
for (const candidate of candidates) {
|
|
718
|
-
const values = new Set();
|
|
784
|
+
const values = /* @__PURE__ */ new Set();
|
|
719
785
|
const indexMapping = {};
|
|
720
786
|
let valid = true;
|
|
721
787
|
for (let i = 0; i < items.length; i++) {
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
if (!prop || prop.type.kind !== "" || prop.type.value === undefined) {
|
|
788
|
+
const prop = items[i].type.props.get(candidate);
|
|
789
|
+
if (!prop || prop.type.kind !== "" || prop.type.value === void 0) {
|
|
725
790
|
valid = false;
|
|
726
791
|
break;
|
|
727
792
|
}
|
|
@@ -743,7 +808,23 @@ function detectDiscriminator(items) {
|
|
|
743
808
|
}
|
|
744
809
|
return result;
|
|
745
810
|
}
|
|
746
|
-
|
|
811
|
+
/**
|
|
812
|
+
* Builds a JSON Schema from an {@link TAtscriptAnnotatedType}.
|
|
813
|
+
*
|
|
814
|
+
* Translates the atscript type structure and validation metadata
|
|
815
|
+
* (min/max, patterns, integer constraints, etc.) into a standard JSON Schema.
|
|
816
|
+
*
|
|
817
|
+
* @example
|
|
818
|
+
* ```ts
|
|
819
|
+
* import { buildJsonSchema } from '@atscript/typescript'
|
|
820
|
+
*
|
|
821
|
+
* const schema = buildJsonSchema(MyInterface)
|
|
822
|
+
* // { type: 'object', properties: { ... }, required: [...] }
|
|
823
|
+
* ```
|
|
824
|
+
*
|
|
825
|
+
* @param type - The annotated type to convert.
|
|
826
|
+
* @returns A JSON Schema object.
|
|
827
|
+
*/ function buildJsonSchema(type) {
|
|
747
828
|
const defs = {};
|
|
748
829
|
let hasDefs = false;
|
|
749
830
|
const buildObject = (d) => {
|
|
@@ -754,12 +835,12 @@ function buildJsonSchema(type) {
|
|
|
754
835
|
properties[key] = build(val);
|
|
755
836
|
if (!val.optional) required.push(key);
|
|
756
837
|
}
|
|
757
|
-
const schema
|
|
838
|
+
const schema = {
|
|
758
839
|
type: "object",
|
|
759
840
|
properties
|
|
760
841
|
};
|
|
761
|
-
if (required.length > 0) schema
|
|
762
|
-
return schema
|
|
842
|
+
if (required.length > 0) schema.required = required;
|
|
843
|
+
return schema;
|
|
763
844
|
};
|
|
764
845
|
const build = (def) => {
|
|
765
846
|
if (def.id && def.type.kind === "object" && def !== type) {
|
|
@@ -780,15 +861,15 @@ function buildJsonSchema(type) {
|
|
|
780
861
|
return buildObject(d);
|
|
781
862
|
},
|
|
782
863
|
array(d) {
|
|
783
|
-
const schema
|
|
864
|
+
const schema = {
|
|
784
865
|
type: "array",
|
|
785
866
|
items: build(d.type.of)
|
|
786
867
|
};
|
|
787
868
|
const minLength = meta.get("expect.minLength");
|
|
788
|
-
if (minLength) schema
|
|
869
|
+
if (minLength) schema.minItems = typeof minLength === "number" ? minLength : minLength.length;
|
|
789
870
|
const maxLength = meta.get("expect.maxLength");
|
|
790
|
-
if (maxLength) schema
|
|
791
|
-
return schema
|
|
871
|
+
if (maxLength) schema.maxItems = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
872
|
+
return schema;
|
|
792
873
|
},
|
|
793
874
|
union(d) {
|
|
794
875
|
const disc = detectDiscriminator(d.type.items);
|
|
@@ -820,30 +901,30 @@ function buildJsonSchema(type) {
|
|
|
820
901
|
};
|
|
821
902
|
},
|
|
822
903
|
final(d) {
|
|
823
|
-
const schema
|
|
824
|
-
if (d.type.value !==
|
|
904
|
+
const schema = {};
|
|
905
|
+
if (d.type.value !== void 0) schema.const = d.type.value;
|
|
825
906
|
if (d.type.designType && d.type.designType !== "any") {
|
|
826
907
|
const dt = d.type.designType;
|
|
827
|
-
schema
|
|
828
|
-
if (schema
|
|
908
|
+
schema.type = dt === "undefined" ? "null" : dt === "decimal" ? "string" : dt;
|
|
909
|
+
if (schema.type === "number" && meta.get("expect.int")) schema.type = "integer";
|
|
829
910
|
}
|
|
830
|
-
if (schema
|
|
831
|
-
if (meta.get("meta.required")) schema
|
|
911
|
+
if (schema.type === "string") {
|
|
912
|
+
if (meta.get("meta.required")) schema.minLength = 1;
|
|
832
913
|
const minLength = meta.get("expect.minLength");
|
|
833
|
-
if (minLength) schema
|
|
914
|
+
if (minLength) schema.minLength = typeof minLength === "number" ? minLength : minLength.length;
|
|
834
915
|
const maxLength = meta.get("expect.maxLength");
|
|
835
|
-
if (maxLength) schema
|
|
916
|
+
if (maxLength) schema.maxLength = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
836
917
|
const patterns = meta.get("expect.pattern");
|
|
837
|
-
if (patterns?.length) if (patterns.length === 1) schema
|
|
838
|
-
else schema
|
|
918
|
+
if (patterns?.length) if (patterns.length === 1) schema.pattern = patterns[0].pattern;
|
|
919
|
+
else schema.allOf = (schema.allOf || []).concat(patterns.map((p) => ({ pattern: p.pattern })));
|
|
839
920
|
}
|
|
840
|
-
if (schema
|
|
921
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
841
922
|
const min = meta.get("expect.min");
|
|
842
|
-
if (min) schema
|
|
923
|
+
if (min) schema.minimum = typeof min === "number" ? min : min.minValue;
|
|
843
924
|
const max = meta.get("expect.max");
|
|
844
|
-
if (max) schema
|
|
925
|
+
if (max) schema.maximum = typeof max === "number" ? max : max.maxValue;
|
|
845
926
|
}
|
|
846
|
-
return schema
|
|
927
|
+
return schema;
|
|
847
928
|
}
|
|
848
929
|
});
|
|
849
930
|
};
|
|
@@ -854,9 +935,28 @@ else schema$1.allOf = (schema$1.allOf || []).concat(patterns.map((p) => ({ patte
|
|
|
854
935
|
};
|
|
855
936
|
return schema;
|
|
856
937
|
}
|
|
857
|
-
|
|
938
|
+
/**
|
|
939
|
+
* Converts a JSON Schema object into a {@link TAtscriptAnnotatedType}.
|
|
940
|
+
*
|
|
941
|
+
* This is the inverse of {@link buildJsonSchema}. A round-trip
|
|
942
|
+
* `buildJsonSchema(fromJsonSchema(schema))` preserves structure and constraints.
|
|
943
|
+
*
|
|
944
|
+
* Supports the JSON Schema subset produced by `buildJsonSchema` plus
|
|
945
|
+
* common extensions like `oneOf` (treated as union) and `enum` (union of literals).
|
|
946
|
+
*
|
|
947
|
+
* @example
|
|
948
|
+
* ```ts
|
|
949
|
+
* import { fromJsonSchema } from '@atscript/typescript'
|
|
950
|
+
*
|
|
951
|
+
* const type = fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] })
|
|
952
|
+
* type.validator().validate({ name: 'Alice' }) // passes
|
|
953
|
+
* ```
|
|
954
|
+
*
|
|
955
|
+
* @param schema - A JSON Schema object.
|
|
956
|
+
* @returns An annotated type with full validator support.
|
|
957
|
+
*/ function fromJsonSchema(schema) {
|
|
858
958
|
const defsSource = schema.$defs || schema.definitions || {};
|
|
859
|
-
const resolved = new Map();
|
|
959
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
860
960
|
const convert = (s) => {
|
|
861
961
|
if (!s || Object.keys(s).length === 0) return defineAnnotatedType().designType("any").$type;
|
|
862
962
|
if (s.$ref) {
|
|
@@ -919,9 +1019,9 @@ function fromJsonSchema(schema) {
|
|
|
919
1019
|
}
|
|
920
1020
|
if (s.type === "array") {
|
|
921
1021
|
if (Array.isArray(s.items)) {
|
|
922
|
-
const handle
|
|
923
|
-
for (const item of s.items) handle
|
|
924
|
-
return handle
|
|
1022
|
+
const handle = defineAnnotatedType("tuple");
|
|
1023
|
+
for (const item of s.items) handle.item(convert(item));
|
|
1024
|
+
return handle.$type;
|
|
925
1025
|
}
|
|
926
1026
|
const itemType = s.items ? convert(s.items) : defineAnnotatedType().designType("any").$type;
|
|
927
1027
|
const handle = defineAnnotatedType("array").of(itemType);
|
|
@@ -958,7 +1058,16 @@ function fromJsonSchema(schema) {
|
|
|
958
1058
|
};
|
|
959
1059
|
return convert(schema);
|
|
960
1060
|
}
|
|
961
|
-
|
|
1061
|
+
/**
|
|
1062
|
+
* Merges multiple annotated types into a combined schema map with shared `$defs`.
|
|
1063
|
+
*
|
|
1064
|
+
* Each type must have an `id`. The returned `schemas` object contains individual
|
|
1065
|
+
* schemas keyed by type id, and `$defs` contains all shared type definitions
|
|
1066
|
+
* deduplicated across schemas.
|
|
1067
|
+
*
|
|
1068
|
+
* @param types - Array of annotated types, each with an `id`.
|
|
1069
|
+
* @returns An object with `schemas` (keyed by id) and shared `$defs`.
|
|
1070
|
+
*/ function mergeJsonSchemas(types) {
|
|
962
1071
|
const mergedDefs = {};
|
|
963
1072
|
const schemas = {};
|
|
964
1073
|
for (const type of types) {
|
|
@@ -967,7 +1076,7 @@ function mergeJsonSchemas(types) {
|
|
|
967
1076
|
const schema = buildJsonSchema(type);
|
|
968
1077
|
if (schema.$defs) {
|
|
969
1078
|
for (const [defName, defSchema] of Object.entries(schema.$defs)) if (!mergedDefs[defName]) mergedDefs[defName] = defSchema;
|
|
970
|
-
const { $defs: _
|
|
1079
|
+
const { $defs: _, ...rest } = schema;
|
|
971
1080
|
schemas[name] = rest;
|
|
972
1081
|
} else schemas[name] = schema;
|
|
973
1082
|
}
|
|
@@ -978,4 +1087,4 @@ function mergeJsonSchemas(types) {
|
|
|
978
1087
|
}
|
|
979
1088
|
|
|
980
1089
|
//#endregion
|
|
981
|
-
export {
|
|
1090
|
+
export { forAnnotatedType as a, createAnnotatedTypeNode as c, isAnnotatedTypeOfPrimitive as d, isPhantomType as f, mergeJsonSchemas as i, defineAnnotatedType as l, ValidatorError as m, detectDiscriminator as n, annotate as o, Validator as p, fromJsonSchema as r, cloneRefProp as s, buildJsonSchema as t, isAnnotatedType as u };
|