@atscript/typescript 0.1.24 → 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 +182 -40
- package/dist/index.cjs +182 -40
- package/dist/index.mjs +182 -40
- package/dist/utils.cjs +205 -33
- package/dist/utils.d.ts +24 -2
- package/dist/utils.mjs +204 -34
- 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 +48 -6
package/dist/utils.d.ts
CHANGED
|
@@ -181,6 +181,7 @@ interface TAtscriptAnnotatedType<T extends TAtscriptTypeDef = TAtscriptTypeDef,
|
|
|
181
181
|
validator(opts?: Partial<TValidatorOptions>): Validator<this, DataType>;
|
|
182
182
|
metadata: TMetadataMap<AtscriptMetadata>;
|
|
183
183
|
optional?: boolean;
|
|
184
|
+
id?: string;
|
|
184
185
|
}
|
|
185
186
|
/** An annotated type that is also a class constructor (i.e. a generated interface class). */
|
|
186
187
|
type TAtscriptAnnotatedTypeConstructor = TAtscriptAnnotatedType & (new (...args: any[]) => any);
|
|
@@ -193,6 +194,11 @@ declare function isAnnotatedType(type: any): type is TAtscriptAnnotatedType;
|
|
|
193
194
|
* Used by the handle's .annotate() method and by generated mutation statements.
|
|
194
195
|
*/
|
|
195
196
|
declare function annotate<K extends keyof AtscriptMetadata>(metadata: TMetadataMap<AtscriptMetadata> | undefined, key: K, value: AtscriptMetadata[K] extends Array<infer E> ? E : AtscriptMetadata[K], asArray?: boolean): void;
|
|
197
|
+
/**
|
|
198
|
+
* Clones a property's type tree in-place so mutations don't leak to shared refs.
|
|
199
|
+
* Used by mutating annotate codegen when paths cross ref boundaries.
|
|
200
|
+
*/
|
|
201
|
+
declare function cloneRefProp(parentType: TAtscriptTypeDef, propName: string): void;
|
|
196
202
|
type TKind = '' | 'array' | 'object' | 'union' | 'intersection' | 'tuple';
|
|
197
203
|
/**
|
|
198
204
|
* Creates a builder handle for constructing a {@link TAtscriptAnnotatedType} at runtime.
|
|
@@ -242,6 +248,7 @@ interface TAnnotatedTypeHandle {
|
|
|
242
248
|
name?: string;
|
|
243
249
|
}, chain?: string[]): TAnnotatedTypeHandle;
|
|
244
250
|
annotate(key: keyof AtscriptMetadata, value: any, asArray?: boolean): TAnnotatedTypeHandle;
|
|
251
|
+
id(value: string): TAnnotatedTypeHandle;
|
|
245
252
|
}
|
|
246
253
|
/**
|
|
247
254
|
* Checks whether an annotated type is a phantom type.
|
|
@@ -299,6 +306,20 @@ declare function buildJsonSchema(type: TAtscriptAnnotatedType): TJsonSchema;
|
|
|
299
306
|
* @returns An annotated type with full validator support.
|
|
300
307
|
*/
|
|
301
308
|
declare function fromJsonSchema(schema: TJsonSchema): TAtscriptAnnotatedType;
|
|
309
|
+
/**
|
|
310
|
+
* Merges multiple annotated types into a combined schema map with shared `$defs`.
|
|
311
|
+
*
|
|
312
|
+
* Each type must have an `id`. The returned `schemas` object contains individual
|
|
313
|
+
* schemas keyed by type id, and `$defs` contains all shared type definitions
|
|
314
|
+
* deduplicated across schemas.
|
|
315
|
+
*
|
|
316
|
+
* @param types - Array of annotated types, each with an `id`.
|
|
317
|
+
* @returns An object with `schemas` (keyed by id) and shared `$defs`.
|
|
318
|
+
*/
|
|
319
|
+
declare function mergeJsonSchemas(types: TAtscriptAnnotatedType[]): {
|
|
320
|
+
schemas: Record<string, TJsonSchema>;
|
|
321
|
+
$defs: Record<string, TJsonSchema>;
|
|
322
|
+
};
|
|
302
323
|
|
|
303
324
|
/**
|
|
304
325
|
* Type-safe dispatch over `TAtscriptAnnotatedType` by its `type.kind`.
|
|
@@ -417,6 +438,7 @@ interface TSerializedAnnotatedTypeInner {
|
|
|
417
438
|
type: TSerializedTypeDef;
|
|
418
439
|
metadata: Record<string, unknown>;
|
|
419
440
|
optional?: boolean;
|
|
441
|
+
id?: string;
|
|
420
442
|
}
|
|
421
443
|
interface TSerializedTypeFinal {
|
|
422
444
|
kind: '';
|
|
@@ -511,5 +533,5 @@ declare function serializeAnnotatedType(type: TAtscriptAnnotatedType, options?:
|
|
|
511
533
|
*/
|
|
512
534
|
declare function deserializeAnnotatedType(data: TSerializedAnnotatedType): TAtscriptAnnotatedType;
|
|
513
535
|
|
|
514
|
-
export { SERIALIZE_VERSION, Validator, ValidatorError, annotate, buildJsonSchema, createDataFromAnnotatedType, defineAnnotatedType, deserializeAnnotatedType, flattenAnnotatedType, forAnnotatedType, fromJsonSchema, isAnnotatedType, isAnnotatedTypeOfPrimitive, isPhantomType, serializeAnnotatedType, throwFeatureDisabled };
|
|
515
|
-
export type { InferDataType, TAnnotatedTypeHandle, TAtscriptAnnotatedType, TAtscriptAnnotatedTypeConstructor, TAtscriptDataType, TAtscriptTypeArray, TAtscriptTypeComplex, TAtscriptTypeDef, TAtscriptTypeFinal, TAtscriptTypeObject, TCreateDataOptions, TFlattenOptions, TMetadataMap, TProcessAnnotationContext, TSerializeOptions, TSerializedAnnotatedType, TSerializedAnnotatedTypeInner, TSerializedTypeArray, TSerializedTypeComplex, TSerializedTypeDef, TSerializedTypeFinal, TSerializedTypeObject, TValidatorOptions, TValidatorPlugin, TValidatorPluginContext, TValueResolver };
|
|
536
|
+
export { SERIALIZE_VERSION, Validator, ValidatorError, annotate, buildJsonSchema, cloneRefProp, createDataFromAnnotatedType, defineAnnotatedType, deserializeAnnotatedType, flattenAnnotatedType, forAnnotatedType, fromJsonSchema, isAnnotatedType, isAnnotatedTypeOfPrimitive, isPhantomType, mergeJsonSchemas, serializeAnnotatedType, throwFeatureDisabled };
|
|
537
|
+
export type { InferDataType, TAnnotatedTypeHandle, TAtscriptAnnotatedType, TAtscriptAnnotatedTypeConstructor, TAtscriptDataType, TAtscriptTypeArray, TAtscriptTypeComplex, TAtscriptTypeDef, TAtscriptTypeFinal, TAtscriptTypeObject, TCreateDataOptions, TFlattenOptions, TJsonSchema, TMetadataMap, TProcessAnnotationContext, TSerializeOptions, TSerializedAnnotatedType, TSerializedAnnotatedTypeInner, TSerializedTypeArray, TSerializedTypeComplex, TSerializedTypeDef, TSerializedTypeFinal, TSerializedTypeObject, TValidatorOptions, TValidatorPlugin, TValidatorPluginContext, TValueResolver };
|
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;
|
|
@@ -551,40 +613,112 @@ function isAnnotatedTypeOfPrimitive(t) {
|
|
|
551
613
|
|
|
552
614
|
//#endregion
|
|
553
615
|
//#region packages/typescript/src/json-schema.ts
|
|
616
|
+
/**
|
|
617
|
+
* Detects a discriminator property across union items.
|
|
618
|
+
*
|
|
619
|
+
* Scans all items for object-typed members that share a common property
|
|
620
|
+
* with distinct const/literal values. If exactly one such property exists,
|
|
621
|
+
* it is returned as the discriminator.
|
|
622
|
+
*/ function detectDiscriminator(items) {
|
|
623
|
+
if (items.length < 2) return null;
|
|
624
|
+
for (const item of items) if (item.type.kind !== "object") return null;
|
|
625
|
+
const firstObj = items[0].type;
|
|
626
|
+
const candidates = [];
|
|
627
|
+
for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== undefined) candidates.push(propName);
|
|
628
|
+
const validCandidates = [];
|
|
629
|
+
for (const candidate of candidates) {
|
|
630
|
+
const values = new Set();
|
|
631
|
+
const mapping = {};
|
|
632
|
+
let valid = true;
|
|
633
|
+
for (let i = 0; i < items.length; i++) {
|
|
634
|
+
const obj = items[i].type;
|
|
635
|
+
const prop = obj.props.get(candidate);
|
|
636
|
+
if (!prop || prop.type.kind !== "" || prop.type.value === undefined) {
|
|
637
|
+
valid = false;
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
const val = prop.type.value;
|
|
641
|
+
if (values.has(val)) {
|
|
642
|
+
valid = false;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
values.add(val);
|
|
646
|
+
mapping[String(val)] = `#/oneOf/${i}`;
|
|
647
|
+
}
|
|
648
|
+
if (valid) validCandidates.push({
|
|
649
|
+
propertyName: candidate,
|
|
650
|
+
mapping
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (validCandidates.length === 1) return validCandidates[0];
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
554
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
|
+
};
|
|
555
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;
|
|
556
684
|
const meta = def.metadata;
|
|
557
685
|
return forAnnotatedType(def, {
|
|
558
686
|
phantom() {
|
|
559
687
|
return {};
|
|
560
688
|
},
|
|
561
689
|
object(d) {
|
|
562
|
-
|
|
563
|
-
const required = [];
|
|
564
|
-
for (const [key, val] of d.type.props.entries()) {
|
|
565
|
-
if (isPhantomType(val)) continue;
|
|
566
|
-
properties[key] = build$1(val);
|
|
567
|
-
if (!val.optional) required.push(key);
|
|
568
|
-
}
|
|
569
|
-
const schema = {
|
|
570
|
-
type: "object",
|
|
571
|
-
properties
|
|
572
|
-
};
|
|
573
|
-
if (required.length > 0) schema.required = required;
|
|
574
|
-
return schema;
|
|
690
|
+
return buildObject(d);
|
|
575
691
|
},
|
|
576
692
|
array(d) {
|
|
577
|
-
const schema = {
|
|
693
|
+
const schema$1 = {
|
|
578
694
|
type: "array",
|
|
579
695
|
items: build$1(d.type.of)
|
|
580
696
|
};
|
|
581
697
|
const minLength = meta.get("expect.minLength");
|
|
582
|
-
if (minLength) schema.minItems = typeof minLength === "number" ? minLength : minLength.length;
|
|
698
|
+
if (minLength) schema$1.minItems = typeof minLength === "number" ? minLength : minLength.length;
|
|
583
699
|
const maxLength = meta.get("expect.maxLength");
|
|
584
|
-
if (maxLength) schema.maxItems = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
585
|
-
return schema;
|
|
700
|
+
if (maxLength) schema$1.maxItems = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
701
|
+
return schema$1;
|
|
586
702
|
},
|
|
587
703
|
union(d) {
|
|
704
|
+
const disc = detectDiscriminator(d.type.items);
|
|
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;
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
oneOf,
|
|
716
|
+
discriminator: {
|
|
717
|
+
propertyName: disc.propertyName,
|
|
718
|
+
mapping
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
588
722
|
return { anyOf: d.type.items.map(build$1) };
|
|
589
723
|
},
|
|
590
724
|
intersection(d) {
|
|
@@ -598,38 +732,54 @@ function buildJsonSchema(type) {
|
|
|
598
732
|
};
|
|
599
733
|
},
|
|
600
734
|
final(d) {
|
|
601
|
-
const schema = {};
|
|
602
|
-
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;
|
|
603
737
|
if (d.type.designType && d.type.designType !== "any") {
|
|
604
|
-
schema.type = d.type.designType === "undefined" ? "null" : d.type.designType;
|
|
605
|
-
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";
|
|
606
740
|
}
|
|
607
|
-
if (schema.type === "string") {
|
|
608
|
-
if (meta.get("meta.required")) schema.minLength = 1;
|
|
741
|
+
if (schema$1.type === "string") {
|
|
742
|
+
if (meta.get("meta.required")) schema$1.minLength = 1;
|
|
609
743
|
const minLength = meta.get("expect.minLength");
|
|
610
|
-
if (minLength) schema.minLength = typeof minLength === "number" ? minLength : minLength.length;
|
|
744
|
+
if (minLength) schema$1.minLength = typeof minLength === "number" ? minLength : minLength.length;
|
|
611
745
|
const maxLength = meta.get("expect.maxLength");
|
|
612
|
-
if (maxLength) schema.maxLength = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
746
|
+
if (maxLength) schema$1.maxLength = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
613
747
|
const patterns = meta.get("expect.pattern");
|
|
614
|
-
if (patterns?.length) if (patterns.length === 1) schema.pattern = patterns[0].pattern;
|
|
615
|
-
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 })));
|
|
616
750
|
}
|
|
617
|
-
if (schema.type === "number" || schema.type === "integer") {
|
|
751
|
+
if (schema$1.type === "number" || schema$1.type === "integer") {
|
|
618
752
|
const min = meta.get("expect.min");
|
|
619
|
-
if (min) schema.minimum = typeof min === "number" ? min : min.minValue;
|
|
753
|
+
if (min) schema$1.minimum = typeof min === "number" ? min : min.minValue;
|
|
620
754
|
const max = meta.get("expect.max");
|
|
621
|
-
if (max) schema.maximum = typeof max === "number" ? max : max.maxValue;
|
|
755
|
+
if (max) schema$1.maximum = typeof max === "number" ? max : max.maxValue;
|
|
622
756
|
}
|
|
623
|
-
return schema;
|
|
757
|
+
return schema$1;
|
|
624
758
|
}
|
|
625
759
|
});
|
|
626
760
|
};
|
|
627
|
-
|
|
761
|
+
const schema = build$1(type);
|
|
762
|
+
if (Object.keys(defs).length > 0) return {
|
|
763
|
+
...schema,
|
|
764
|
+
$defs: defs
|
|
765
|
+
};
|
|
766
|
+
return schema;
|
|
628
767
|
}
|
|
629
768
|
function fromJsonSchema(schema) {
|
|
769
|
+
const defsSource = schema.$defs || schema.definitions || {};
|
|
770
|
+
const resolved = new Map();
|
|
630
771
|
const convert = (s) => {
|
|
631
772
|
if (!s || Object.keys(s).length === 0) return defineAnnotatedType().designType("any").$type;
|
|
632
|
-
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
|
+
}
|
|
633
783
|
if ("const" in s) {
|
|
634
784
|
const val = s.const;
|
|
635
785
|
const dt = val === null ? "null" : typeof val;
|
|
@@ -717,6 +867,24 @@ function fromJsonSchema(schema) {
|
|
|
717
867
|
};
|
|
718
868
|
return convert(schema);
|
|
719
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
|
+
}
|
|
720
888
|
|
|
721
889
|
//#endregion
|
|
722
890
|
//#region packages/typescript/src/default-value.ts
|
|
@@ -910,6 +1078,7 @@ function serializeNode(def, path, options) {
|
|
|
910
1078
|
metadata: serializeMetadata(def.metadata, path, def.type.kind, options)
|
|
911
1079
|
};
|
|
912
1080
|
if (def.optional) result.optional = true;
|
|
1081
|
+
if (def.id) result.id = def.id;
|
|
913
1082
|
return result;
|
|
914
1083
|
}
|
|
915
1084
|
function serializeTypeDef(def, path, options) {
|
|
@@ -1013,6 +1182,7 @@ function deserializeNode(data) {
|
|
|
1013
1182
|
}
|
|
1014
1183
|
};
|
|
1015
1184
|
if (data.optional) result.optional = true;
|
|
1185
|
+
if (data.id) result.id = data.id;
|
|
1016
1186
|
return result;
|
|
1017
1187
|
}
|
|
1018
1188
|
function deserializeTypeDef(t) {
|
|
@@ -1058,4 +1228,4 @@ function deserializeTypeDef(t) {
|
|
|
1058
1228
|
}
|
|
1059
1229
|
|
|
1060
1230
|
//#endregion
|
|
1061
|
-
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 |
|
|
@@ -88,11 +109,15 @@ const schema = buildJsonSchema(User)
|
|
|
88
109
|
| `@expect.pattern` (multiple) | `allOf: [{ pattern }, ...]` |
|
|
89
110
|
| `@meta.required` on string | `minLength: 1` |
|
|
90
111
|
| optional property | not in `required` array |
|
|
91
|
-
| union | `anyOf` |
|
|
112
|
+
| union | `anyOf` (or `oneOf` + `discriminator` for discriminated unions) |
|
|
92
113
|
| intersection | `allOf` |
|
|
93
114
|
| tuple | `items` as array |
|
|
94
115
|
| phantom | empty object `{}` (excluded) |
|
|
95
116
|
|
|
117
|
+
### Discriminated Unions
|
|
118
|
+
|
|
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.
|
|
120
|
+
|
|
96
121
|
## `fromJsonSchema(schema)` — JSON Schema → Annotated Type
|
|
97
122
|
|
|
98
123
|
The inverse of `buildJsonSchema`. Creates a fully functional annotated type from a JSON Schema:
|
|
@@ -113,9 +138,26 @@ const type = fromJsonSchema({
|
|
|
113
138
|
type.validator().validate({ name: 'Alice', age: 30 }) // passes
|
|
114
139
|
```
|
|
115
140
|
|
|
116
|
-
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
|
+
```
|
|
117
159
|
|
|
118
|
-
|
|
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.
|
|
119
161
|
|
|
120
162
|
## `serializeAnnotatedType(type, options?)` — Serialize to JSON
|
|
121
163
|
|
|
@@ -167,7 +209,7 @@ type.validator().validate(someData)
|
|
|
167
209
|
type.metadata.get('meta.label')
|
|
168
210
|
```
|
|
169
211
|
|
|
170
|
-
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.
|
|
171
213
|
|
|
172
214
|
### `SERIALIZE_VERSION`
|
|
173
215
|
|