@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/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
- const properties = {};
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) return {
630
- oneOf: d.type.items.map(build$1),
631
- discriminator: {
632
- propertyName: disc.propertyName,
633
- mapping: disc.mapping
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
- return build$1(type);
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) throw new Error("$ref is not supported by fromJsonSchema. Dereference the schema first.");
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.25",
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.25"
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
- | `.refTo(type, chain?)` | Reference another annotated type's definition |
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
- Does **not** support `$ref` dereference schemas first.
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