@atscript/typescript 0.1.23 → 0.1.25

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 CHANGED
@@ -991,6 +991,46 @@ function isPhantomType(def) {
991
991
 
992
992
  //#endregion
993
993
  //#region packages/typescript/src/json-schema.ts
994
+ /**
995
+ * Detects a discriminator property across union items.
996
+ *
997
+ * Scans all items for object-typed members that share a common property
998
+ * with distinct const/literal values. If exactly one such property exists,
999
+ * it is returned as the discriminator.
1000
+ */ function detectDiscriminator(items) {
1001
+ if (items.length < 2) return null;
1002
+ for (const item of items) if (item.type.kind !== "object") return null;
1003
+ const firstObj = items[0].type;
1004
+ const candidates = [];
1005
+ for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== undefined) candidates.push(propName);
1006
+ const validCandidates = [];
1007
+ for (const candidate of candidates) {
1008
+ const values = new Set();
1009
+ const mapping = {};
1010
+ let valid = true;
1011
+ for (let i = 0; i < items.length; i++) {
1012
+ const obj = items[i].type;
1013
+ const prop = obj.props.get(candidate);
1014
+ if (!prop || prop.type.kind !== "" || prop.type.value === undefined) {
1015
+ valid = false;
1016
+ break;
1017
+ }
1018
+ const val = prop.type.value;
1019
+ if (values.has(val)) {
1020
+ valid = false;
1021
+ break;
1022
+ }
1023
+ values.add(val);
1024
+ mapping[String(val)] = `#/oneOf/${i}`;
1025
+ }
1026
+ if (valid) validCandidates.push({
1027
+ propertyName: candidate,
1028
+ mapping
1029
+ });
1030
+ }
1031
+ if (validCandidates.length === 1) return validCandidates[0];
1032
+ return null;
1033
+ }
994
1034
  function buildJsonSchema(type) {
995
1035
  const build$1 = (def) => {
996
1036
  const meta = def.metadata;
@@ -1025,6 +1065,14 @@ function buildJsonSchema(type) {
1025
1065
  return schema;
1026
1066
  },
1027
1067
  union(d) {
1068
+ const disc = detectDiscriminator(d.type.items);
1069
+ if (disc) return {
1070
+ oneOf: d.type.items.map(build$1),
1071
+ discriminator: {
1072
+ propertyName: disc.propertyName,
1073
+ mapping: disc.mapping
1074
+ }
1075
+ };
1028
1076
  return { anyOf: d.type.items.map(build$1) };
1029
1077
  },
1030
1078
  intersection(d) {
package/dist/index.cjs CHANGED
@@ -988,6 +988,46 @@ function isPhantomType(def) {
988
988
 
989
989
  //#endregion
990
990
  //#region packages/typescript/src/json-schema.ts
991
+ /**
992
+ * Detects a discriminator property across union items.
993
+ *
994
+ * Scans all items for object-typed members that share a common property
995
+ * with distinct const/literal values. If exactly one such property exists,
996
+ * it is returned as the discriminator.
997
+ */ function detectDiscriminator(items) {
998
+ if (items.length < 2) return null;
999
+ for (const item of items) if (item.type.kind !== "object") return null;
1000
+ const firstObj = items[0].type;
1001
+ const candidates = [];
1002
+ for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== undefined) candidates.push(propName);
1003
+ const validCandidates = [];
1004
+ for (const candidate of candidates) {
1005
+ const values = new Set();
1006
+ const mapping = {};
1007
+ let valid = true;
1008
+ for (let i = 0; i < items.length; i++) {
1009
+ const obj = items[i].type;
1010
+ const prop = obj.props.get(candidate);
1011
+ if (!prop || prop.type.kind !== "" || prop.type.value === undefined) {
1012
+ valid = false;
1013
+ break;
1014
+ }
1015
+ const val = prop.type.value;
1016
+ if (values.has(val)) {
1017
+ valid = false;
1018
+ break;
1019
+ }
1020
+ values.add(val);
1021
+ mapping[String(val)] = `#/oneOf/${i}`;
1022
+ }
1023
+ if (valid) validCandidates.push({
1024
+ propertyName: candidate,
1025
+ mapping
1026
+ });
1027
+ }
1028
+ if (validCandidates.length === 1) return validCandidates[0];
1029
+ return null;
1030
+ }
991
1031
  function buildJsonSchema(type) {
992
1032
  const build = (def) => {
993
1033
  const meta = def.metadata;
@@ -1022,6 +1062,14 @@ function buildJsonSchema(type) {
1022
1062
  return schema;
1023
1063
  },
1024
1064
  union(d) {
1065
+ const disc = detectDiscriminator(d.type.items);
1066
+ if (disc) return {
1067
+ oneOf: d.type.items.map(build),
1068
+ discriminator: {
1069
+ propertyName: disc.propertyName,
1070
+ mapping: disc.mapping
1071
+ }
1072
+ };
1025
1073
  return { anyOf: d.type.items.map(build) };
1026
1074
  },
1027
1075
  intersection(d) {
package/dist/index.mjs CHANGED
@@ -964,6 +964,46 @@ function isPhantomType(def) {
964
964
 
965
965
  //#endregion
966
966
  //#region packages/typescript/src/json-schema.ts
967
+ /**
968
+ * Detects a discriminator property across union items.
969
+ *
970
+ * Scans all items for object-typed members that share a common property
971
+ * with distinct const/literal values. If exactly one such property exists,
972
+ * it is returned as the discriminator.
973
+ */ function detectDiscriminator(items) {
974
+ if (items.length < 2) return null;
975
+ for (const item of items) if (item.type.kind !== "object") return null;
976
+ const firstObj = items[0].type;
977
+ const candidates = [];
978
+ for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== undefined) candidates.push(propName);
979
+ const validCandidates = [];
980
+ for (const candidate of candidates) {
981
+ const values = new Set();
982
+ const mapping = {};
983
+ let valid = true;
984
+ for (let i = 0; i < items.length; i++) {
985
+ const obj = items[i].type;
986
+ const prop = obj.props.get(candidate);
987
+ if (!prop || prop.type.kind !== "" || prop.type.value === undefined) {
988
+ valid = false;
989
+ break;
990
+ }
991
+ const val = prop.type.value;
992
+ if (values.has(val)) {
993
+ valid = false;
994
+ break;
995
+ }
996
+ values.add(val);
997
+ mapping[String(val)] = `#/oneOf/${i}`;
998
+ }
999
+ if (valid) validCandidates.push({
1000
+ propertyName: candidate,
1001
+ mapping
1002
+ });
1003
+ }
1004
+ if (validCandidates.length === 1) return validCandidates[0];
1005
+ return null;
1006
+ }
967
1007
  function buildJsonSchema(type) {
968
1008
  const build = (def) => {
969
1009
  const meta = def.metadata;
@@ -998,6 +1038,14 @@ function buildJsonSchema(type) {
998
1038
  return schema;
999
1039
  },
1000
1040
  union(d) {
1041
+ const disc = detectDiscriminator(d.type.items);
1042
+ if (disc) return {
1043
+ oneOf: d.type.items.map(build),
1044
+ discriminator: {
1045
+ propertyName: disc.propertyName,
1046
+ mapping: disc.mapping
1047
+ }
1048
+ };
1001
1049
  return { anyOf: d.type.items.map(build) };
1002
1050
  },
1003
1051
  intersection(d) {
package/dist/utils.cjs CHANGED
@@ -552,6 +552,46 @@ function isAnnotatedTypeOfPrimitive(t) {
552
552
 
553
553
  //#endregion
554
554
  //#region packages/typescript/src/json-schema.ts
555
+ /**
556
+ * Detects a discriminator property across union items.
557
+ *
558
+ * Scans all items for object-typed members that share a common property
559
+ * with distinct const/literal values. If exactly one such property exists,
560
+ * it is returned as the discriminator.
561
+ */ function detectDiscriminator(items) {
562
+ if (items.length < 2) return null;
563
+ for (const item of items) if (item.type.kind !== "object") return null;
564
+ const firstObj = items[0].type;
565
+ const candidates = [];
566
+ for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== undefined) candidates.push(propName);
567
+ const validCandidates = [];
568
+ for (const candidate of candidates) {
569
+ const values = new Set();
570
+ const mapping = {};
571
+ let valid = true;
572
+ for (let i = 0; i < items.length; i++) {
573
+ const obj = items[i].type;
574
+ const prop = obj.props.get(candidate);
575
+ if (!prop || prop.type.kind !== "" || prop.type.value === undefined) {
576
+ valid = false;
577
+ break;
578
+ }
579
+ const val = prop.type.value;
580
+ if (values.has(val)) {
581
+ valid = false;
582
+ break;
583
+ }
584
+ values.add(val);
585
+ mapping[String(val)] = `#/oneOf/${i}`;
586
+ }
587
+ if (valid) validCandidates.push({
588
+ propertyName: candidate,
589
+ mapping
590
+ });
591
+ }
592
+ if (validCandidates.length === 1) return validCandidates[0];
593
+ return null;
594
+ }
555
595
  function buildJsonSchema(type) {
556
596
  const build$1 = (def) => {
557
597
  const meta = def.metadata;
@@ -586,6 +626,14 @@ function buildJsonSchema(type) {
586
626
  return schema;
587
627
  },
588
628
  union(d) {
629
+ const disc = detectDiscriminator(d.type.items);
630
+ if (disc) return {
631
+ oneOf: d.type.items.map(build$1),
632
+ discriminator: {
633
+ propertyName: disc.propertyName,
634
+ mapping: disc.mapping
635
+ }
636
+ };
589
637
  return { anyOf: d.type.items.map(build$1) };
590
638
  },
591
639
  intersection(d) {
@@ -778,15 +826,24 @@ function build(def, path, mode) {
778
826
  if (isPhantomType(prop)) continue;
779
827
  const childPath = path ? `${path}.${key}` : key;
780
828
  if (prop.optional) {
781
- const childResolved = resolveValue(prop, childPath, mode);
782
- if (childResolved !== undefined) data[key] = childResolved.value;
829
+ if (mode === "example") data[key] = build(prop, childPath, mode);
830
+ else {
831
+ const childResolved = resolveValue(prop, childPath, mode);
832
+ if (childResolved !== undefined) data[key] = childResolved.value;
833
+ }
783
834
  continue;
784
835
  }
785
836
  data[key] = build(prop, childPath, mode);
786
837
  }
787
838
  return data;
788
839
  },
789
- array: () => [],
840
+ array: (d) => {
841
+ if (mode === "example") {
842
+ const item = build(d.type.of, `${path}.0`, mode);
843
+ return item !== undefined ? [item] : [];
844
+ }
845
+ return [];
846
+ },
790
847
  tuple: (d) => d.type.items.map((item, i) => build(item, `${path}.${i}`, mode)),
791
848
  union: (d) => {
792
849
  const first = d.type.items[0];
package/dist/utils.d.ts CHANGED
@@ -332,7 +332,7 @@ interface TCreateDataOptions {
332
332
  * How to resolve values:
333
333
  * - `'empty'` — structural defaults only (`''`, `0`, `false`, `[]`, `{}`); optional props skipped
334
334
  * - `'default'` — use `@meta.default` annotations; optional props skipped unless annotated
335
- * - `'example'` — use `@meta.example` annotations; optional props skipped unless annotated
335
+ * - `'example'` — use `@meta.example` annotations; optional props always included; arrays get one sample item
336
336
  * - `function` — custom resolver per field; optional props skipped unless resolver returns a value
337
337
  *
338
338
  * @default 'empty'
@@ -345,7 +345,7 @@ interface TCreateDataOptions {
345
345
  * Supports four modes:
346
346
  * - `'empty'` — structural defaults only; optional props omitted
347
347
  * - `'default'` — uses `@meta.default` annotations; optional props omitted unless annotated
348
- * - `'example'` — uses `@meta.example` annotations; optional props omitted unless annotated
348
+ * - `'example'` — uses `@meta.example` annotations; optional props always included; arrays get one sample item
349
349
  * - `function` — custom resolver; optional props omitted unless resolver returns a value
350
350
  *
351
351
  * When a `@meta.default` / `@meta.example` value is set on a complex type (object, array)
package/dist/utils.mjs CHANGED
@@ -551,6 +551,46 @@ function isAnnotatedTypeOfPrimitive(t) {
551
551
 
552
552
  //#endregion
553
553
  //#region packages/typescript/src/json-schema.ts
554
+ /**
555
+ * Detects a discriminator property across union items.
556
+ *
557
+ * Scans all items for object-typed members that share a common property
558
+ * with distinct const/literal values. If exactly one such property exists,
559
+ * it is returned as the discriminator.
560
+ */ function detectDiscriminator(items) {
561
+ if (items.length < 2) return null;
562
+ for (const item of items) if (item.type.kind !== "object") return null;
563
+ const firstObj = items[0].type;
564
+ const candidates = [];
565
+ for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== undefined) candidates.push(propName);
566
+ const validCandidates = [];
567
+ for (const candidate of candidates) {
568
+ const values = new Set();
569
+ const mapping = {};
570
+ let valid = true;
571
+ for (let i = 0; i < items.length; i++) {
572
+ const obj = items[i].type;
573
+ const prop = obj.props.get(candidate);
574
+ if (!prop || prop.type.kind !== "" || prop.type.value === undefined) {
575
+ valid = false;
576
+ break;
577
+ }
578
+ const val = prop.type.value;
579
+ if (values.has(val)) {
580
+ valid = false;
581
+ break;
582
+ }
583
+ values.add(val);
584
+ mapping[String(val)] = `#/oneOf/${i}`;
585
+ }
586
+ if (valid) validCandidates.push({
587
+ propertyName: candidate,
588
+ mapping
589
+ });
590
+ }
591
+ if (validCandidates.length === 1) return validCandidates[0];
592
+ return null;
593
+ }
554
594
  function buildJsonSchema(type) {
555
595
  const build$1 = (def) => {
556
596
  const meta = def.metadata;
@@ -585,6 +625,14 @@ function buildJsonSchema(type) {
585
625
  return schema;
586
626
  },
587
627
  union(d) {
628
+ 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
634
+ }
635
+ };
588
636
  return { anyOf: d.type.items.map(build$1) };
589
637
  },
590
638
  intersection(d) {
@@ -777,15 +825,24 @@ function build(def, path, mode) {
777
825
  if (isPhantomType(prop)) continue;
778
826
  const childPath = path ? `${path}.${key}` : key;
779
827
  if (prop.optional) {
780
- const childResolved = resolveValue(prop, childPath, mode);
781
- if (childResolved !== undefined) data[key] = childResolved.value;
828
+ if (mode === "example") data[key] = build(prop, childPath, mode);
829
+ else {
830
+ const childResolved = resolveValue(prop, childPath, mode);
831
+ if (childResolved !== undefined) data[key] = childResolved.value;
832
+ }
782
833
  continue;
783
834
  }
784
835
  data[key] = build(prop, childPath, mode);
785
836
  }
786
837
  return data;
787
838
  },
788
- array: () => [],
839
+ array: (d) => {
840
+ if (mode === "example") {
841
+ const item = build(d.type.of, `${path}.0`, mode);
842
+ return item !== undefined ? [item] : [];
843
+ }
844
+ return [];
845
+ },
789
846
  tuple: (d) => d.type.items.map((item, i) => build(item, `${path}.${i}`, mode)),
790
847
  union: (d) => {
791
848
  const first = d.type.items[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/typescript",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
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.23"
67
+ "@atscript/core": "^0.1.25"
68
68
  },
69
69
  "build": [
70
70
  {},
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: atscript-typescript
3
- description: Atscript TypeScript language extension — .as file syntax, code generation, runtime type system, validation, and utilities for the Atscript metadata description language.
3
+ description: Use when working with Atscript (.as files) in TypeScript projectswriting or editing .as interfaces/types/annotations, configuring atscript.config.ts or tsPlugin, understanding generated .d.ts/.js output, using runtime APIs (TAtscriptAnnotatedType, metadata, validators), generating JSON Schema or example data from types, serializing/deserializing annotated types, or running the asc CLI.
4
4
  ---
5
5
 
6
6
  # @atscript/typescript
@@ -88,11 +88,15 @@ const schema = buildJsonSchema(User)
88
88
  | `@expect.pattern` (multiple) | `allOf: [{ pattern }, ...]` |
89
89
  | `@meta.required` on string | `minLength: 1` |
90
90
  | optional property | not in `required` array |
91
- | union | `anyOf` |
91
+ | union | `anyOf` (or `oneOf` + `discriminator` for discriminated unions) |
92
92
  | intersection | `allOf` |
93
93
  | tuple | `items` as array |
94
94
  | phantom | empty object `{}` (excluded) |
95
95
 
96
+ ### Discriminated Unions
97
+
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.
99
+
96
100
  ## `fromJsonSchema(schema)` — JSON Schema → Annotated Type
97
101
 
98
102
  The inverse of `buildJsonSchema`. Creates a fully functional annotated type from a JSON Schema:
@@ -257,12 +261,13 @@ const custom = createDataFromAnnotatedType(User, {
257
261
  |------|----------|
258
262
  | `'empty'` (default) | Structural defaults: `''`, `0`, `false`, `[]`, `{}`. Optional props omitted |
259
263
  | `'default'` | Uses `@meta.default` annotations. Optional props only included if annotated |
260
- | `'example'` | Uses `@meta.example` annotations. Optional props only included if annotated |
264
+ | `'example'` | Uses `@meta.example` annotations. Optional props always included. Arrays get one sample item |
261
265
  | `function` | Custom resolver per field. Return `undefined` to fall through |
262
266
 
263
267
  ### Behavior Notes
264
268
 
265
- - **Optional properties** are omitted unless the mode provides a value for them
269
+ - **Optional properties** are omitted unless the mode provides a value for them (exception: `'example'` mode always includes all optional props)
270
+ - **Arrays** in `'example'` mode generate one sample item from the element type instead of an empty array
266
271
  - **Complex types** (object, array): if a `@meta.default`/`@meta.example` annotation is set and passes validation, the entire subtree is replaced (no recursion into inner props)
267
272
  - **Annotation values**: strings are used as-is for string types; everything else is parsed via `JSON.parse`
268
273
  - **Unions/Intersections**: defaults to first item's value