@atscript/typescript 0.1.25 → 0.1.26
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 +140 -46
- package/dist/index.cjs +140 -46
- package/dist/index.mjs +140 -46
- package/dist/utils.cjs +163 -39
- package/dist/utils.d.ts +24 -2
- package/dist/utils.mjs +162 -40
- package/package.json +2 -2
- package/skills/atscript-typescript/SKILL.md +1 -1
- package/skills/atscript-typescript/codegen.md +1 -0
- package/skills/atscript-typescript/runtime.md +3 -1
- package/skills/atscript-typescript/utilities.md +44 -6
package/dist/utils.mjs
CHANGED
|
@@ -427,6 +427,63 @@ else metadata.set(key, [a, value]);
|
|
|
427
427
|
} else metadata.set(key, [value]);
|
|
428
428
|
else metadata.set(key, value);
|
|
429
429
|
}
|
|
430
|
+
function cloneRefProp(parentType, propName) {
|
|
431
|
+
if (parentType.kind !== "object") return;
|
|
432
|
+
const objType = parentType;
|
|
433
|
+
const existing = objType.props.get(propName);
|
|
434
|
+
if (!existing) return;
|
|
435
|
+
const clonedType = cloneTypeDef(existing.type);
|
|
436
|
+
objType.props.set(propName, {
|
|
437
|
+
__is_atscript_annotated_type: true,
|
|
438
|
+
type: clonedType,
|
|
439
|
+
metadata: new Map(existing.metadata),
|
|
440
|
+
id: existing.id,
|
|
441
|
+
optional: existing.optional,
|
|
442
|
+
validator(opts) {
|
|
443
|
+
return new Validator(this, opts);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
function cloneTypeDef(type) {
|
|
448
|
+
if (type.kind === "object") {
|
|
449
|
+
const obj = type;
|
|
450
|
+
return {
|
|
451
|
+
kind: "object",
|
|
452
|
+
props: new Map(Array.from(obj.props.entries()).map(([k, v]) => [k, {
|
|
453
|
+
__is_atscript_annotated_type: true,
|
|
454
|
+
type: v.type,
|
|
455
|
+
metadata: new Map(v.metadata),
|
|
456
|
+
id: v.id,
|
|
457
|
+
optional: v.optional,
|
|
458
|
+
validator(opts) {
|
|
459
|
+
return new Validator(this, opts);
|
|
460
|
+
}
|
|
461
|
+
}])),
|
|
462
|
+
propsPatterns: [...obj.propsPatterns],
|
|
463
|
+
tags: new Set(obj.tags)
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (type.kind === "array") {
|
|
467
|
+
const arr = type;
|
|
468
|
+
return {
|
|
469
|
+
kind: "array",
|
|
470
|
+
of: arr.of,
|
|
471
|
+
tags: new Set(arr.tags)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (type.kind === "union" || type.kind === "intersection" || type.kind === "tuple") {
|
|
475
|
+
const complex = type;
|
|
476
|
+
return {
|
|
477
|
+
kind: type.kind,
|
|
478
|
+
items: [...complex.items],
|
|
479
|
+
tags: new Set(complex.tags)
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
...type,
|
|
484
|
+
tags: new Set(type.tags)
|
|
485
|
+
};
|
|
486
|
+
}
|
|
430
487
|
function defineAnnotatedType(_kind, base) {
|
|
431
488
|
const kind = _kind || "";
|
|
432
489
|
const type = base?.type || {};
|
|
@@ -518,6 +575,7 @@ else if (!newBase) throw new Error(`"${typeName}" is not annotated type`);
|
|
|
518
575
|
__is_atscript_annotated_type: true,
|
|
519
576
|
type: newBase.type,
|
|
520
577
|
metadata,
|
|
578
|
+
id: newBase.id,
|
|
521
579
|
validator(opts) {
|
|
522
580
|
return new Validator(this, opts);
|
|
523
581
|
}
|
|
@@ -528,6 +586,10 @@ else if (!newBase) throw new Error(`"${typeName}" is not annotated type`);
|
|
|
528
586
|
annotate(key, value, asArray) {
|
|
529
587
|
annotate(this.$metadata, key, value, asArray);
|
|
530
588
|
return this;
|
|
589
|
+
},
|
|
590
|
+
id(value) {
|
|
591
|
+
this.$type.id = value;
|
|
592
|
+
return this;
|
|
531
593
|
}
|
|
532
594
|
};
|
|
533
595
|
return handle;
|
|
@@ -592,47 +654,71 @@ function isAnnotatedTypeOfPrimitive(t) {
|
|
|
592
654
|
return null;
|
|
593
655
|
}
|
|
594
656
|
function buildJsonSchema(type) {
|
|
657
|
+
const defs = {};
|
|
658
|
+
let isRoot = true;
|
|
659
|
+
const buildObject = (d) => {
|
|
660
|
+
const properties = {};
|
|
661
|
+
const required = [];
|
|
662
|
+
for (const [key, val] of d.type.props.entries()) {
|
|
663
|
+
if (isPhantomType(val)) continue;
|
|
664
|
+
properties[key] = build$1(val);
|
|
665
|
+
if (!val.optional) required.push(key);
|
|
666
|
+
}
|
|
667
|
+
const schema$1 = {
|
|
668
|
+
type: "object",
|
|
669
|
+
properties
|
|
670
|
+
};
|
|
671
|
+
if (required.length > 0) schema$1.required = required;
|
|
672
|
+
return schema$1;
|
|
673
|
+
};
|
|
595
674
|
const build$1 = (def) => {
|
|
675
|
+
if (def.id && def.type.kind === "object" && !isRoot) {
|
|
676
|
+
const name = def.id;
|
|
677
|
+
if (!defs[name]) {
|
|
678
|
+
defs[name] = {};
|
|
679
|
+
defs[name] = buildObject(def);
|
|
680
|
+
}
|
|
681
|
+
return { $ref: `#/$defs/${name}` };
|
|
682
|
+
}
|
|
683
|
+
isRoot = false;
|
|
596
684
|
const meta = def.metadata;
|
|
597
685
|
return forAnnotatedType(def, {
|
|
598
686
|
phantom() {
|
|
599
687
|
return {};
|
|
600
688
|
},
|
|
601
689
|
object(d) {
|
|
602
|
-
|
|
603
|
-
const required = [];
|
|
604
|
-
for (const [key, val] of d.type.props.entries()) {
|
|
605
|
-
if (isPhantomType(val)) continue;
|
|
606
|
-
properties[key] = build$1(val);
|
|
607
|
-
if (!val.optional) required.push(key);
|
|
608
|
-
}
|
|
609
|
-
const schema = {
|
|
610
|
-
type: "object",
|
|
611
|
-
properties
|
|
612
|
-
};
|
|
613
|
-
if (required.length > 0) schema.required = required;
|
|
614
|
-
return schema;
|
|
690
|
+
return buildObject(d);
|
|
615
691
|
},
|
|
616
692
|
array(d) {
|
|
617
|
-
const schema = {
|
|
693
|
+
const schema$1 = {
|
|
618
694
|
type: "array",
|
|
619
695
|
items: build$1(d.type.of)
|
|
620
696
|
};
|
|
621
697
|
const minLength = meta.get("expect.minLength");
|
|
622
|
-
if (minLength) schema.minItems = typeof minLength === "number" ? minLength : minLength.length;
|
|
698
|
+
if (minLength) schema$1.minItems = typeof minLength === "number" ? minLength : minLength.length;
|
|
623
699
|
const maxLength = meta.get("expect.maxLength");
|
|
624
|
-
if (maxLength) schema.maxItems = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
625
|
-
return schema;
|
|
700
|
+
if (maxLength) schema$1.maxItems = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
701
|
+
return schema$1;
|
|
626
702
|
},
|
|
627
703
|
union(d) {
|
|
628
704
|
const disc = detectDiscriminator(d.type.items);
|
|
629
|
-
if (disc)
|
|
630
|
-
oneOf
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
705
|
+
if (disc) {
|
|
706
|
+
const oneOf = d.type.items.map(build$1);
|
|
707
|
+
const mapping = {};
|
|
708
|
+
for (const [val, origPath] of Object.entries(disc.mapping)) {
|
|
709
|
+
const idx = Number.parseInt(origPath.split("/").pop());
|
|
710
|
+
const item = d.type.items[idx];
|
|
711
|
+
if (item.id && defs[item.id]) mapping[val] = `#/$defs/${item.id}`;
|
|
712
|
+
else mapping[val] = origPath;
|
|
634
713
|
}
|
|
635
|
-
|
|
714
|
+
return {
|
|
715
|
+
oneOf,
|
|
716
|
+
discriminator: {
|
|
717
|
+
propertyName: disc.propertyName,
|
|
718
|
+
mapping
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
636
722
|
return { anyOf: d.type.items.map(build$1) };
|
|
637
723
|
},
|
|
638
724
|
intersection(d) {
|
|
@@ -646,38 +732,54 @@ function buildJsonSchema(type) {
|
|
|
646
732
|
};
|
|
647
733
|
},
|
|
648
734
|
final(d) {
|
|
649
|
-
const schema = {};
|
|
650
|
-
if (d.type.value !== undefined) schema.const = d.type.value;
|
|
735
|
+
const schema$1 = {};
|
|
736
|
+
if (d.type.value !== undefined) schema$1.const = d.type.value;
|
|
651
737
|
if (d.type.designType && d.type.designType !== "any") {
|
|
652
|
-
schema.type = d.type.designType === "undefined" ? "null" : d.type.designType;
|
|
653
|
-
if (schema.type === "number" && meta.get("expect.int")) schema.type = "integer";
|
|
738
|
+
schema$1.type = d.type.designType === "undefined" ? "null" : d.type.designType;
|
|
739
|
+
if (schema$1.type === "number" && meta.get("expect.int")) schema$1.type = "integer";
|
|
654
740
|
}
|
|
655
|
-
if (schema.type === "string") {
|
|
656
|
-
if (meta.get("meta.required")) schema.minLength = 1;
|
|
741
|
+
if (schema$1.type === "string") {
|
|
742
|
+
if (meta.get("meta.required")) schema$1.minLength = 1;
|
|
657
743
|
const minLength = meta.get("expect.minLength");
|
|
658
|
-
if (minLength) schema.minLength = typeof minLength === "number" ? minLength : minLength.length;
|
|
744
|
+
if (minLength) schema$1.minLength = typeof minLength === "number" ? minLength : minLength.length;
|
|
659
745
|
const maxLength = meta.get("expect.maxLength");
|
|
660
|
-
if (maxLength) schema.maxLength = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
746
|
+
if (maxLength) schema$1.maxLength = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
661
747
|
const patterns = meta.get("expect.pattern");
|
|
662
|
-
if (patterns?.length) if (patterns.length === 1) schema.pattern = patterns[0].pattern;
|
|
663
|
-
else schema.allOf = (schema.allOf || []).concat(patterns.map((p) => ({ pattern: p.pattern })));
|
|
748
|
+
if (patterns?.length) if (patterns.length === 1) schema$1.pattern = patterns[0].pattern;
|
|
749
|
+
else schema$1.allOf = (schema$1.allOf || []).concat(patterns.map((p) => ({ pattern: p.pattern })));
|
|
664
750
|
}
|
|
665
|
-
if (schema.type === "number" || schema.type === "integer") {
|
|
751
|
+
if (schema$1.type === "number" || schema$1.type === "integer") {
|
|
666
752
|
const min = meta.get("expect.min");
|
|
667
|
-
if (min) schema.minimum = typeof min === "number" ? min : min.minValue;
|
|
753
|
+
if (min) schema$1.minimum = typeof min === "number" ? min : min.minValue;
|
|
668
754
|
const max = meta.get("expect.max");
|
|
669
|
-
if (max) schema.maximum = typeof max === "number" ? max : max.maxValue;
|
|
755
|
+
if (max) schema$1.maximum = typeof max === "number" ? max : max.maxValue;
|
|
670
756
|
}
|
|
671
|
-
return schema;
|
|
757
|
+
return schema$1;
|
|
672
758
|
}
|
|
673
759
|
});
|
|
674
760
|
};
|
|
675
|
-
|
|
761
|
+
const schema = build$1(type);
|
|
762
|
+
if (Object.keys(defs).length > 0) return {
|
|
763
|
+
...schema,
|
|
764
|
+
$defs: defs
|
|
765
|
+
};
|
|
766
|
+
return schema;
|
|
676
767
|
}
|
|
677
768
|
function fromJsonSchema(schema) {
|
|
769
|
+
const defsSource = schema.$defs || schema.definitions || {};
|
|
770
|
+
const resolved = new Map();
|
|
678
771
|
const convert = (s) => {
|
|
679
772
|
if (!s || Object.keys(s).length === 0) return defineAnnotatedType().designType("any").$type;
|
|
680
|
-
if (s.$ref)
|
|
773
|
+
if (s.$ref) {
|
|
774
|
+
const refName = s.$ref.replace(/^#\/(\$defs|definitions)\//, "");
|
|
775
|
+
if (resolved.has(refName)) return resolved.get(refName);
|
|
776
|
+
if (defsSource[refName]) {
|
|
777
|
+
const type = convert(defsSource[refName]);
|
|
778
|
+
resolved.set(refName, type);
|
|
779
|
+
return type;
|
|
780
|
+
}
|
|
781
|
+
throw new Error(`Unresolvable $ref: ${s.$ref}`);
|
|
782
|
+
}
|
|
681
783
|
if ("const" in s) {
|
|
682
784
|
const val = s.const;
|
|
683
785
|
const dt = val === null ? "null" : typeof val;
|
|
@@ -765,6 +867,24 @@ function fromJsonSchema(schema) {
|
|
|
765
867
|
};
|
|
766
868
|
return convert(schema);
|
|
767
869
|
}
|
|
870
|
+
function mergeJsonSchemas(types) {
|
|
871
|
+
const mergedDefs = {};
|
|
872
|
+
const schemas = {};
|
|
873
|
+
for (const type of types) {
|
|
874
|
+
const name = type.id;
|
|
875
|
+
if (!name) throw new Error("mergeJsonSchemas: all types must have an id");
|
|
876
|
+
const schema = buildJsonSchema(type);
|
|
877
|
+
if (schema.$defs) {
|
|
878
|
+
for (const [defName, defSchema] of Object.entries(schema.$defs)) if (!mergedDefs[defName]) mergedDefs[defName] = defSchema;
|
|
879
|
+
const { $defs: _,...rest } = schema;
|
|
880
|
+
schemas[name] = rest;
|
|
881
|
+
} else schemas[name] = schema;
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
schemas,
|
|
885
|
+
$defs: mergedDefs
|
|
886
|
+
};
|
|
887
|
+
}
|
|
768
888
|
|
|
769
889
|
//#endregion
|
|
770
890
|
//#region packages/typescript/src/default-value.ts
|
|
@@ -958,6 +1078,7 @@ function serializeNode(def, path, options) {
|
|
|
958
1078
|
metadata: serializeMetadata(def.metadata, path, def.type.kind, options)
|
|
959
1079
|
};
|
|
960
1080
|
if (def.optional) result.optional = true;
|
|
1081
|
+
if (def.id) result.id = def.id;
|
|
961
1082
|
return result;
|
|
962
1083
|
}
|
|
963
1084
|
function serializeTypeDef(def, path, options) {
|
|
@@ -1061,6 +1182,7 @@ function deserializeNode(data) {
|
|
|
1061
1182
|
}
|
|
1062
1183
|
};
|
|
1063
1184
|
if (data.optional) result.optional = true;
|
|
1185
|
+
if (data.id) result.id = data.id;
|
|
1064
1186
|
return result;
|
|
1065
1187
|
}
|
|
1066
1188
|
function deserializeTypeDef(t) {
|
|
@@ -1106,4 +1228,4 @@ function deserializeTypeDef(t) {
|
|
|
1106
1228
|
}
|
|
1107
1229
|
|
|
1108
1230
|
//#endregion
|
|
1109
|
-
export { SERIALIZE_VERSION, Validator, ValidatorError, annotate, buildJsonSchema, createDataFromAnnotatedType, defineAnnotatedType, deserializeAnnotatedType, flattenAnnotatedType, forAnnotatedType, fromJsonSchema, isAnnotatedType, isAnnotatedTypeOfPrimitive, isPhantomType, serializeAnnotatedType, throwFeatureDisabled };
|
|
1231
|
+
export { SERIALIZE_VERSION, Validator, ValidatorError, annotate, buildJsonSchema, cloneRefProp, createDataFromAnnotatedType, defineAnnotatedType, deserializeAnnotatedType, flattenAnnotatedType, forAnnotatedType, fromJsonSchema, isAnnotatedType, isAnnotatedTypeOfPrimitive, isPhantomType, mergeJsonSchemas, serializeAnnotatedType, throwFeatureDisabled };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/typescript",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "Atscript: typescript-gen support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"annotations",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"vitest": "3.2.4"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@atscript/core": "^0.1.
|
|
67
|
+
"@atscript/core": "^0.1.26"
|
|
68
68
|
},
|
|
69
69
|
"build": [
|
|
70
70
|
{},
|
|
@@ -31,7 +31,7 @@ import tsPlugin from '@atscript/typescript'
|
|
|
31
31
|
import {
|
|
32
32
|
defineAnnotatedType, isAnnotatedType, annotate,
|
|
33
33
|
Validator, ValidatorError,
|
|
34
|
-
buildJsonSchema, fromJsonSchema,
|
|
34
|
+
buildJsonSchema, fromJsonSchema, mergeJsonSchemas,
|
|
35
35
|
serializeAnnotatedType, deserializeAnnotatedType,
|
|
36
36
|
flattenAnnotatedType, createDataFromAnnotatedType,
|
|
37
37
|
forAnnotatedType, throwFeatureDisabled,
|
|
@@ -70,6 +70,7 @@ Key points:
|
|
|
70
70
|
The JS module creates actual classes with runtime type definitions and metadata:
|
|
71
71
|
|
|
72
72
|
- Uses `defineAnnotatedType` (aliased as `$`) to build the type tree
|
|
73
|
+
- Each class gets a `static id` field with the stable type name (collision-safe via `__N` suffix)
|
|
73
74
|
- Populates metadata maps with all annotation values
|
|
74
75
|
- Wires up `validator()` and `toJsonSchema()` methods
|
|
75
76
|
- When `exampleData: true`, adds `toExampleData()` that calls `createDataFromAnnotatedType(this, { mode: 'example' })` (aliased as `$e`)
|
|
@@ -12,6 +12,7 @@ interface TAtscriptAnnotatedType<T extends TAtscriptTypeDef = TAtscriptTypeDef>
|
|
|
12
12
|
type: T // the type definition (shape)
|
|
13
13
|
metadata: TMetadataMap<AtscriptMetadata> // annotation metadata
|
|
14
14
|
optional?: boolean // whether this type is optional
|
|
15
|
+
id?: string // stable type name (set by codegen or .id() builder)
|
|
15
16
|
validator(opts?): Validator // create a validator instance
|
|
16
17
|
}
|
|
17
18
|
```
|
|
@@ -272,5 +273,6 @@ const labeledType = defineAnnotatedType().designType('string')
|
|
|
272
273
|
| `.optional(flag?)` | Mark as optional |
|
|
273
274
|
| `.annotate(key, value, asArray?)` | Set metadata annotation |
|
|
274
275
|
| `.copyMetadata(from, ignore?)` | Copy metadata from another type |
|
|
275
|
-
| `.
|
|
276
|
+
| `.id(name)` | Set a stable type name (used by `buildJsonSchema` for `$defs`/`$ref`) |
|
|
277
|
+
| `.refTo(type, chain?)` | Reference another annotated type's definition (carries `id`) |
|
|
276
278
|
| `.$type` | Get the final `TAtscriptAnnotatedType` |
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
// Validation
|
|
18
18
|
Validator, ValidatorError,
|
|
19
19
|
// JSON Schema
|
|
20
|
-
buildJsonSchema, fromJsonSchema,
|
|
20
|
+
buildJsonSchema, fromJsonSchema, mergeJsonSchemas,
|
|
21
21
|
// Serialization
|
|
22
22
|
serializeAnnotatedType, deserializeAnnotatedType, SERIALIZE_VERSION,
|
|
23
23
|
// Flattening
|
|
@@ -55,7 +55,7 @@ All handlers except `phantom` are required. Each handler receives the type with
|
|
|
55
55
|
|
|
56
56
|
## `buildJsonSchema(type)` — Annotated Type → JSON Schema
|
|
57
57
|
|
|
58
|
-
Converts an annotated type into a standard JSON Schema object, translating validation metadata
|
|
58
|
+
Converts an annotated type into a standard JSON Schema object, translating validation metadata. Named object types (those with an `id`) are automatically extracted into `$defs` and referenced via `$ref`:
|
|
59
59
|
|
|
60
60
|
```ts
|
|
61
61
|
import { buildJsonSchema } from '@atscript/typescript/utils'
|
|
@@ -73,6 +73,27 @@ const schema = buildJsonSchema(User)
|
|
|
73
73
|
// }
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
### `$defs` and `$ref`
|
|
77
|
+
|
|
78
|
+
Types compiled from `.as` files carry a stable `id` (the type name). When `buildJsonSchema` encounters named object types nested inside other types (unions, properties), it extracts them into `$defs` and references via `$ref`:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { CatOrDog } from './pets.as'
|
|
82
|
+
const schema = buildJsonSchema(CatOrDog)
|
|
83
|
+
// {
|
|
84
|
+
// $defs: { Cat: { type: 'object', ... }, Dog: { type: 'object', ... } },
|
|
85
|
+
// oneOf: [{ $ref: '#/$defs/Cat' }, { $ref: '#/$defs/Dog' }],
|
|
86
|
+
// discriminator: { propertyName: 'petType', mapping: { cat: '#/$defs/Cat', dog: '#/$defs/Dog' } }
|
|
87
|
+
// }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Key behaviors:
|
|
91
|
+
- Only **named object types** (with `id`) are extracted to `$defs`. Primitives, unions, arrays stay inline.
|
|
92
|
+
- The **root type** is never extracted — it IS the schema.
|
|
93
|
+
- Same `id` referenced multiple times → one `$defs` entry, all occurrences become `$ref`.
|
|
94
|
+
- Types without `id` (inline/anonymous) produce inline schemas.
|
|
95
|
+
- For programmatic types, use `.id('Name')` on the builder to enable `$defs` extraction.
|
|
96
|
+
|
|
76
97
|
### Metadata → JSON Schema Mapping
|
|
77
98
|
|
|
78
99
|
| Annotation | JSON Schema |
|
|
@@ -95,7 +116,7 @@ const schema = buildJsonSchema(User)
|
|
|
95
116
|
|
|
96
117
|
### Discriminated Unions
|
|
97
118
|
|
|
98
|
-
When all union items are objects sharing exactly one property with distinct const/literal values, `buildJsonSchema` auto-detects it and emits `oneOf` with a `discriminator` object (including `propertyName` and `mapping`) instead of `anyOf`. No annotations needed — detection is automatic.
|
|
119
|
+
When all union items are objects sharing exactly one property with distinct const/literal values, `buildJsonSchema` auto-detects it and emits `oneOf` with a `discriminator` object (including `propertyName` and `mapping`) instead of `anyOf`. When items have `id`, the mapping uses `$ref` paths into `$defs`. No annotations needed — detection is automatic.
|
|
99
120
|
|
|
100
121
|
## `fromJsonSchema(schema)` — JSON Schema → Annotated Type
|
|
101
122
|
|
|
@@ -117,9 +138,26 @@ const type = fromJsonSchema({
|
|
|
117
138
|
type.validator().validate({ name: 'Alice', age: 30 }) // passes
|
|
118
139
|
```
|
|
119
140
|
|
|
120
|
-
Supports: `type`, `properties`, `required`, `items`, `anyOf`, `oneOf`, `allOf`, `enum`, `const`, `minLength`, `maxLength`, `minimum`, `maximum`, `pattern`, `minItems`, `maxItems`.
|
|
141
|
+
Supports: `type`, `properties`, `required`, `items`, `anyOf`, `oneOf`, `allOf`, `enum`, `const`, `minLength`, `maxLength`, `minimum`, `maximum`, `pattern`, `minItems`, `maxItems`, `$ref`/`$defs`.
|
|
142
|
+
|
|
143
|
+
`$ref` paths are automatically resolved from `$defs` or `definitions` in the schema. Unresolvable `$ref` throws an error.
|
|
144
|
+
|
|
145
|
+
## `mergeJsonSchemas(types)` — Combine Schemas for OpenAPI
|
|
146
|
+
|
|
147
|
+
Combines multiple annotated types into a single schema map with shared `$defs` — useful for building OpenAPI `components/schemas`:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { mergeJsonSchemas } from '@atscript/typescript/utils'
|
|
151
|
+
import { CatOrDog } from './pets.as'
|
|
152
|
+
import { Order } from './orders.as'
|
|
153
|
+
|
|
154
|
+
const merged = mergeJsonSchemas([CatOrDog, Order])
|
|
155
|
+
// merged.schemas.CatOrDog — the CatOrDog schema (oneOf with $ref)
|
|
156
|
+
// merged.schemas.Order — the Order schema
|
|
157
|
+
// merged.$defs: { Cat, Dog, ... } — shared definitions, deduplicated
|
|
158
|
+
```
|
|
121
159
|
|
|
122
|
-
|
|
160
|
+
All types must have an `id` (all types compiled from `.as` files do). The function calls `buildJsonSchema` on each, hoists `$defs` into a shared pool, and returns individual schemas alongside merged definitions.
|
|
123
161
|
|
|
124
162
|
## `serializeAnnotatedType(type, options?)` — Serialize to JSON
|
|
125
163
|
|
|
@@ -171,7 +209,7 @@ type.validator().validate(someData)
|
|
|
171
209
|
type.metadata.get('meta.label')
|
|
172
210
|
```
|
|
173
211
|
|
|
174
|
-
Throws if the serialized version doesn't match `SERIALIZE_VERSION`.
|
|
212
|
+
Throws if the serialized version doesn't match `SERIALIZE_VERSION`. The `id` field is preserved through serialization/deserialization.
|
|
175
213
|
|
|
176
214
|
### `SERIALIZE_VERSION`
|
|
177
215
|
|