@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/index.mjs
CHANGED
|
@@ -944,6 +944,7 @@ else if (!newBase) throw new Error(`"${typeName}" is not annotated type`);
|
|
|
944
944
|
__is_atscript_annotated_type: true,
|
|
945
945
|
type: newBase.type,
|
|
946
946
|
metadata,
|
|
947
|
+
id: newBase.id,
|
|
947
948
|
validator(opts) {
|
|
948
949
|
return new Validator(this, opts);
|
|
949
950
|
}
|
|
@@ -954,6 +955,10 @@ else if (!newBase) throw new Error(`"${typeName}" is not annotated type`);
|
|
|
954
955
|
annotate(key, value, asArray) {
|
|
955
956
|
annotate(this.$metadata, key, value, asArray);
|
|
956
957
|
return this;
|
|
958
|
+
},
|
|
959
|
+
id(value) {
|
|
960
|
+
this.$type.id = value;
|
|
961
|
+
return this;
|
|
957
962
|
}
|
|
958
963
|
};
|
|
959
964
|
return handle;
|
|
@@ -964,40 +969,112 @@ function isPhantomType(def) {
|
|
|
964
969
|
|
|
965
970
|
//#endregion
|
|
966
971
|
//#region packages/typescript/src/json-schema.ts
|
|
972
|
+
/**
|
|
973
|
+
* Detects a discriminator property across union items.
|
|
974
|
+
*
|
|
975
|
+
* Scans all items for object-typed members that share a common property
|
|
976
|
+
* with distinct const/literal values. If exactly one such property exists,
|
|
977
|
+
* it is returned as the discriminator.
|
|
978
|
+
*/ function detectDiscriminator(items) {
|
|
979
|
+
if (items.length < 2) return null;
|
|
980
|
+
for (const item of items) if (item.type.kind !== "object") return null;
|
|
981
|
+
const firstObj = items[0].type;
|
|
982
|
+
const candidates = [];
|
|
983
|
+
for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== undefined) candidates.push(propName);
|
|
984
|
+
const validCandidates = [];
|
|
985
|
+
for (const candidate of candidates) {
|
|
986
|
+
const values = new Set();
|
|
987
|
+
const mapping = {};
|
|
988
|
+
let valid = true;
|
|
989
|
+
for (let i = 0; i < items.length; i++) {
|
|
990
|
+
const obj = items[i].type;
|
|
991
|
+
const prop = obj.props.get(candidate);
|
|
992
|
+
if (!prop || prop.type.kind !== "" || prop.type.value === undefined) {
|
|
993
|
+
valid = false;
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
const val = prop.type.value;
|
|
997
|
+
if (values.has(val)) {
|
|
998
|
+
valid = false;
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
values.add(val);
|
|
1002
|
+
mapping[String(val)] = `#/oneOf/${i}`;
|
|
1003
|
+
}
|
|
1004
|
+
if (valid) validCandidates.push({
|
|
1005
|
+
propertyName: candidate,
|
|
1006
|
+
mapping
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
if (validCandidates.length === 1) return validCandidates[0];
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
967
1012
|
function buildJsonSchema(type) {
|
|
1013
|
+
const defs = {};
|
|
1014
|
+
let isRoot = true;
|
|
1015
|
+
const buildObject = (d) => {
|
|
1016
|
+
const properties = {};
|
|
1017
|
+
const required = [];
|
|
1018
|
+
for (const [key, val] of d.type.props.entries()) {
|
|
1019
|
+
if (isPhantomType(val)) continue;
|
|
1020
|
+
properties[key] = build(val);
|
|
1021
|
+
if (!val.optional) required.push(key);
|
|
1022
|
+
}
|
|
1023
|
+
const schema$1 = {
|
|
1024
|
+
type: "object",
|
|
1025
|
+
properties
|
|
1026
|
+
};
|
|
1027
|
+
if (required.length > 0) schema$1.required = required;
|
|
1028
|
+
return schema$1;
|
|
1029
|
+
};
|
|
968
1030
|
const build = (def) => {
|
|
1031
|
+
if (def.id && def.type.kind === "object" && !isRoot) {
|
|
1032
|
+
const name = def.id;
|
|
1033
|
+
if (!defs[name]) {
|
|
1034
|
+
defs[name] = {};
|
|
1035
|
+
defs[name] = buildObject(def);
|
|
1036
|
+
}
|
|
1037
|
+
return { $ref: `#/$defs/${name}` };
|
|
1038
|
+
}
|
|
1039
|
+
isRoot = false;
|
|
969
1040
|
const meta = def.metadata;
|
|
970
1041
|
return forAnnotatedType(def, {
|
|
971
1042
|
phantom() {
|
|
972
1043
|
return {};
|
|
973
1044
|
},
|
|
974
1045
|
object(d) {
|
|
975
|
-
|
|
976
|
-
const required = [];
|
|
977
|
-
for (const [key, val] of d.type.props.entries()) {
|
|
978
|
-
if (isPhantomType(val)) continue;
|
|
979
|
-
properties[key] = build(val);
|
|
980
|
-
if (!val.optional) required.push(key);
|
|
981
|
-
}
|
|
982
|
-
const schema = {
|
|
983
|
-
type: "object",
|
|
984
|
-
properties
|
|
985
|
-
};
|
|
986
|
-
if (required.length > 0) schema.required = required;
|
|
987
|
-
return schema;
|
|
1046
|
+
return buildObject(d);
|
|
988
1047
|
},
|
|
989
1048
|
array(d) {
|
|
990
|
-
const schema = {
|
|
1049
|
+
const schema$1 = {
|
|
991
1050
|
type: "array",
|
|
992
1051
|
items: build(d.type.of)
|
|
993
1052
|
};
|
|
994
1053
|
const minLength = meta.get("expect.minLength");
|
|
995
|
-
if (minLength) schema.minItems = typeof minLength === "number" ? minLength : minLength.length;
|
|
1054
|
+
if (minLength) schema$1.minItems = typeof minLength === "number" ? minLength : minLength.length;
|
|
996
1055
|
const maxLength = meta.get("expect.maxLength");
|
|
997
|
-
if (maxLength) schema.maxItems = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
998
|
-
return schema;
|
|
1056
|
+
if (maxLength) schema$1.maxItems = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
1057
|
+
return schema$1;
|
|
999
1058
|
},
|
|
1000
1059
|
union(d) {
|
|
1060
|
+
const disc = detectDiscriminator(d.type.items);
|
|
1061
|
+
if (disc) {
|
|
1062
|
+
const oneOf = d.type.items.map(build);
|
|
1063
|
+
const mapping = {};
|
|
1064
|
+
for (const [val, origPath] of Object.entries(disc.mapping)) {
|
|
1065
|
+
const idx = Number.parseInt(origPath.split("/").pop());
|
|
1066
|
+
const item = d.type.items[idx];
|
|
1067
|
+
if (item.id && defs[item.id]) mapping[val] = `#/$defs/${item.id}`;
|
|
1068
|
+
else mapping[val] = origPath;
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
oneOf,
|
|
1072
|
+
discriminator: {
|
|
1073
|
+
propertyName: disc.propertyName,
|
|
1074
|
+
mapping
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1001
1078
|
return { anyOf: d.type.items.map(build) };
|
|
1002
1079
|
},
|
|
1003
1080
|
intersection(d) {
|
|
@@ -1011,33 +1088,38 @@ function buildJsonSchema(type) {
|
|
|
1011
1088
|
};
|
|
1012
1089
|
},
|
|
1013
1090
|
final(d) {
|
|
1014
|
-
const schema = {};
|
|
1015
|
-
if (d.type.value !== undefined) schema.const = d.type.value;
|
|
1091
|
+
const schema$1 = {};
|
|
1092
|
+
if (d.type.value !== undefined) schema$1.const = d.type.value;
|
|
1016
1093
|
if (d.type.designType && d.type.designType !== "any") {
|
|
1017
|
-
schema.type = d.type.designType === "undefined" ? "null" : d.type.designType;
|
|
1018
|
-
if (schema.type === "number" && meta.get("expect.int")) schema.type = "integer";
|
|
1094
|
+
schema$1.type = d.type.designType === "undefined" ? "null" : d.type.designType;
|
|
1095
|
+
if (schema$1.type === "number" && meta.get("expect.int")) schema$1.type = "integer";
|
|
1019
1096
|
}
|
|
1020
|
-
if (schema.type === "string") {
|
|
1021
|
-
if (meta.get("meta.required")) schema.minLength = 1;
|
|
1097
|
+
if (schema$1.type === "string") {
|
|
1098
|
+
if (meta.get("meta.required")) schema$1.minLength = 1;
|
|
1022
1099
|
const minLength = meta.get("expect.minLength");
|
|
1023
|
-
if (minLength) schema.minLength = typeof minLength === "number" ? minLength : minLength.length;
|
|
1100
|
+
if (minLength) schema$1.minLength = typeof minLength === "number" ? minLength : minLength.length;
|
|
1024
1101
|
const maxLength = meta.get("expect.maxLength");
|
|
1025
|
-
if (maxLength) schema.maxLength = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
1102
|
+
if (maxLength) schema$1.maxLength = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
1026
1103
|
const patterns = meta.get("expect.pattern");
|
|
1027
|
-
if (patterns?.length) if (patterns.length === 1) schema.pattern = patterns[0].pattern;
|
|
1028
|
-
else schema.allOf = (schema.allOf || []).concat(patterns.map((p) => ({ pattern: p.pattern })));
|
|
1104
|
+
if (patterns?.length) if (patterns.length === 1) schema$1.pattern = patterns[0].pattern;
|
|
1105
|
+
else schema$1.allOf = (schema$1.allOf || []).concat(patterns.map((p) => ({ pattern: p.pattern })));
|
|
1029
1106
|
}
|
|
1030
|
-
if (schema.type === "number" || schema.type === "integer") {
|
|
1107
|
+
if (schema$1.type === "number" || schema$1.type === "integer") {
|
|
1031
1108
|
const min = meta.get("expect.min");
|
|
1032
|
-
if (min) schema.minimum = typeof min === "number" ? min : min.minValue;
|
|
1109
|
+
if (min) schema$1.minimum = typeof min === "number" ? min : min.minValue;
|
|
1033
1110
|
const max = meta.get("expect.max");
|
|
1034
|
-
if (max) schema.maximum = typeof max === "number" ? max : max.maxValue;
|
|
1111
|
+
if (max) schema$1.maximum = typeof max === "number" ? max : max.maxValue;
|
|
1035
1112
|
}
|
|
1036
|
-
return schema;
|
|
1113
|
+
return schema$1;
|
|
1037
1114
|
}
|
|
1038
1115
|
});
|
|
1039
1116
|
};
|
|
1040
|
-
|
|
1117
|
+
const schema = build(type);
|
|
1118
|
+
if (Object.keys(defs).length > 0) return {
|
|
1119
|
+
...schema,
|
|
1120
|
+
$defs: defs
|
|
1121
|
+
};
|
|
1122
|
+
return schema;
|
|
1041
1123
|
}
|
|
1042
1124
|
|
|
1043
1125
|
//#endregion
|
|
@@ -1058,11 +1140,23 @@ var JsRenderer = class extends BaseRenderer {
|
|
|
1058
1140
|
this.writeln("/* eslint-disable */");
|
|
1059
1141
|
this.writeln("/* oxlint-disable */");
|
|
1060
1142
|
const imports = ["defineAnnotatedType as $", "annotate as $a"];
|
|
1143
|
+
const hasMutatingAnnotate = this.doc.nodes.some((n) => n.entity === "annotate" && n.isMutating);
|
|
1144
|
+
if (hasMutatingAnnotate) imports.push("cloneRefProp as $c");
|
|
1061
1145
|
const jsonSchemaMode = resolveJsonSchemaMode(this.opts);
|
|
1062
1146
|
if (jsonSchemaMode === "lazy") imports.push("buildJsonSchema as $$");
|
|
1063
1147
|
if (this.opts?.exampleData) imports.push("createDataFromAnnotatedType as $e");
|
|
1064
1148
|
if (jsonSchemaMode === false) imports.push("throwFeatureDisabled as $d");
|
|
1065
1149
|
this.writeln(`import { ${imports.join(", ")} } from "@atscript/typescript/utils"`);
|
|
1150
|
+
const nameCounts = new Map();
|
|
1151
|
+
const nodesByName = new Map();
|
|
1152
|
+
for (const node of this.doc.nodes) if (node.__typeId != null && node.id) {
|
|
1153
|
+
const name = node.id;
|
|
1154
|
+
nameCounts.set(name, (nameCounts.get(name) || 0) + 1);
|
|
1155
|
+
if (!nodesByName.has(name)) nodesByName.set(name, []);
|
|
1156
|
+
nodesByName.get(name).push(node);
|
|
1157
|
+
}
|
|
1158
|
+
for (const [name, nodes] of nodesByName) if (nodes.length === 1) this.typeIds.set(nodes[0], name);
|
|
1159
|
+
else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}__${i + 1}`);
|
|
1066
1160
|
}
|
|
1067
1161
|
buildAdHocMap(annotateNodes) {
|
|
1068
1162
|
const map = new Map();
|
|
@@ -1076,6 +1170,15 @@ var JsRenderer = class extends BaseRenderer {
|
|
|
1076
1170
|
}
|
|
1077
1171
|
return map.size > 0 ? map : null;
|
|
1078
1172
|
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Checks if any ad-hoc annotation path extends beyond the current _propPath,
|
|
1175
|
+
* meaning annotations target properties inside a referenced type.
|
|
1176
|
+
*/ hasAdHocAnnotationsThroughRef() {
|
|
1177
|
+
if (!this._adHocAnnotations || this._propPath.length === 0) return false;
|
|
1178
|
+
const prefix = this._propPath.join(".") + ".";
|
|
1179
|
+
for (const key of this._adHocAnnotations.keys()) if (key.startsWith(prefix)) return true;
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1079
1182
|
post() {
|
|
1080
1183
|
for (const node of this.postAnnotate) if (node.entity === "annotate") {
|
|
1081
1184
|
const annotateNode = node;
|
|
@@ -1106,6 +1209,8 @@ else {
|
|
|
1106
1209
|
this.writeln("static __is_atscript_annotated_type = true");
|
|
1107
1210
|
this.writeln("static type = {}");
|
|
1108
1211
|
this.writeln("static metadata = new Map()");
|
|
1212
|
+
const typeId = this.typeIds.get(node);
|
|
1213
|
+
if (typeId) this.writeln(`static id = "${typeId}"`);
|
|
1109
1214
|
this.renderJsonSchemaMethod(node);
|
|
1110
1215
|
this.renderExampleDataMethod(node);
|
|
1111
1216
|
}
|
|
@@ -1184,6 +1289,8 @@ else {
|
|
|
1184
1289
|
case "type": {
|
|
1185
1290
|
const def = node.getDefinition();
|
|
1186
1291
|
const handle = this.toAnnotatedHandle(def, true);
|
|
1292
|
+
const typeId = this.typeIds.get(node) ?? (node.__typeId != null ? node.id : undefined);
|
|
1293
|
+
if (typeId) handle.id(typeId);
|
|
1187
1294
|
return skipAnnotations ? handle : this.applyExpectAnnotations(handle, this.doc.evalAnnotationsForNode(node));
|
|
1188
1295
|
}
|
|
1189
1296
|
case "prop": {
|
|
@@ -1310,6 +1417,14 @@ else handle.prop(prop.id, propHandle.$type);
|
|
|
1310
1417
|
return this;
|
|
1311
1418
|
}
|
|
1312
1419
|
}
|
|
1420
|
+
if (this._adHocAnnotations && this.hasAdHocAnnotationsThroughRef()) {
|
|
1421
|
+
let resolved = decl ? this.doc.mergeIntersection(decl) : undefined;
|
|
1422
|
+
if (resolved && isInterface(resolved)) resolved = resolved.getDefinition() || resolved;
|
|
1423
|
+
if (resolved) {
|
|
1424
|
+
this.annotateType(resolved, name);
|
|
1425
|
+
return this;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1313
1428
|
const chain = ref.hasChain ? `, [${ref.chain.map((c) => `"${escapeQuotes(c.text)}"`).join(", ")}]` : "";
|
|
1314
1429
|
this.writeln(`$(${name ? `"", ${name}` : ""})`).indent().writeln(`.refTo(${ref.id}${chain})`);
|
|
1315
1430
|
if (!ref.hasChain) {
|
|
@@ -1543,11 +1658,29 @@ else targetValue = "true";
|
|
|
1543
1658
|
const targetName = node.targetName;
|
|
1544
1659
|
const targetDef = this.resolveTargetDef(targetName);
|
|
1545
1660
|
this.writeln("// Ad-hoc annotations for ", targetName);
|
|
1661
|
+
const allClones = [];
|
|
1662
|
+
const entryAccessors = [];
|
|
1546
1663
|
for (const entry of node.entries) {
|
|
1547
1664
|
const anns = entry.annotations;
|
|
1548
1665
|
if (!anns || anns.length === 0) continue;
|
|
1549
1666
|
const parts = entry.hasChain ? [entry.id, ...entry.chain.map((c) => c.text)] : [entry.id];
|
|
1550
|
-
const accessors = this.buildMutatingAccessors(targetName, targetDef, parts);
|
|
1667
|
+
const { accessors, clones } = this.buildMutatingAccessors(targetName, targetDef, parts);
|
|
1668
|
+
allClones.push(...clones);
|
|
1669
|
+
entryAccessors.push({
|
|
1670
|
+
entry,
|
|
1671
|
+
accessors
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
const cloneKeys = new Set();
|
|
1675
|
+
for (const clone of allClones) {
|
|
1676
|
+
const key = `${clone.parentPath}|${clone.propName}`;
|
|
1677
|
+
if (!cloneKeys.has(key)) {
|
|
1678
|
+
cloneKeys.add(key);
|
|
1679
|
+
this.writeln(`$c(${clone.parentPath}, "${escapeQuotes(clone.propName)}")`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
for (const { entry, accessors } of entryAccessors) {
|
|
1683
|
+
const anns = entry.annotations;
|
|
1551
1684
|
for (const accessor of accessors) {
|
|
1552
1685
|
const cleared = new Set();
|
|
1553
1686
|
for (const an of anns) {
|
|
@@ -1597,15 +1730,21 @@ else targetValue = "true";
|
|
|
1597
1730
|
prefix: `${targetName}.type`,
|
|
1598
1731
|
def: targetDef
|
|
1599
1732
|
}];
|
|
1733
|
+
const clones = [];
|
|
1600
1734
|
for (let i = 0; i < parts.length; i++) {
|
|
1601
1735
|
const nextAccessors = [];
|
|
1602
1736
|
for (const { prefix, def } of accessors) {
|
|
1603
1737
|
const results = this.buildPropPaths(def, parts[i]);
|
|
1604
|
-
if (results.length > 0) for (const result of results) if (i < parts.length - 1)
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1738
|
+
if (results.length > 0) for (const result of results) if (i < parts.length - 1) {
|
|
1739
|
+
if (result.propDef && isRef(result.propDef)) clones.push({
|
|
1740
|
+
parentPath: prefix,
|
|
1741
|
+
propName: parts[i]
|
|
1742
|
+
});
|
|
1743
|
+
nextAccessors.push({
|
|
1744
|
+
prefix: `${prefix}${result.path}?.type`,
|
|
1745
|
+
def: result.propDef
|
|
1746
|
+
});
|
|
1747
|
+
} else nextAccessors.push({
|
|
1609
1748
|
prefix: `${prefix}${result.path}?`,
|
|
1610
1749
|
def: result.propDef
|
|
1611
1750
|
});
|
|
@@ -1619,7 +1758,10 @@ else {
|
|
|
1619
1758
|
}
|
|
1620
1759
|
accessors = nextAccessors;
|
|
1621
1760
|
}
|
|
1622
|
-
return
|
|
1761
|
+
return {
|
|
1762
|
+
accessors: accessors.map((a) => a.prefix),
|
|
1763
|
+
clones
|
|
1764
|
+
};
|
|
1623
1765
|
}
|
|
1624
1766
|
/**
|
|
1625
1767
|
* Finds a property in a type tree at compile time, returning all
|
|
@@ -1655,7 +1797,7 @@ else {
|
|
|
1655
1797
|
return [];
|
|
1656
1798
|
}
|
|
1657
1799
|
constructor(doc, opts) {
|
|
1658
|
-
super(doc), _define_property(this, "opts", void 0), _define_property(this, "postAnnotate", void 0), _define_property(this, "_adHocAnnotations", void 0), _define_property(this, "_propPath", void 0), this.opts = opts, this.postAnnotate = [], this._adHocAnnotations = null, this._propPath = [];
|
|
1800
|
+
super(doc), _define_property(this, "opts", void 0), _define_property(this, "postAnnotate", void 0), _define_property(this, "_adHocAnnotations", void 0), _define_property(this, "_propPath", void 0), _define_property(this, "typeIds", void 0), this.opts = opts, this.postAnnotate = [], this._adHocAnnotations = null, this._propPath = [], this.typeIds = new Map();
|
|
1659
1801
|
}
|
|
1660
1802
|
};
|
|
1661
1803
|
|
package/dist/utils.cjs
CHANGED
|
@@ -428,6 +428,63 @@ else metadata.set(key, [a, value]);
|
|
|
428
428
|
} else metadata.set(key, [value]);
|
|
429
429
|
else metadata.set(key, value);
|
|
430
430
|
}
|
|
431
|
+
function cloneRefProp(parentType, propName) {
|
|
432
|
+
if (parentType.kind !== "object") return;
|
|
433
|
+
const objType = parentType;
|
|
434
|
+
const existing = objType.props.get(propName);
|
|
435
|
+
if (!existing) return;
|
|
436
|
+
const clonedType = cloneTypeDef(existing.type);
|
|
437
|
+
objType.props.set(propName, {
|
|
438
|
+
__is_atscript_annotated_type: true,
|
|
439
|
+
type: clonedType,
|
|
440
|
+
metadata: new Map(existing.metadata),
|
|
441
|
+
id: existing.id,
|
|
442
|
+
optional: existing.optional,
|
|
443
|
+
validator(opts) {
|
|
444
|
+
return new Validator(this, opts);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function cloneTypeDef(type) {
|
|
449
|
+
if (type.kind === "object") {
|
|
450
|
+
const obj = type;
|
|
451
|
+
return {
|
|
452
|
+
kind: "object",
|
|
453
|
+
props: new Map(Array.from(obj.props.entries()).map(([k, v]) => [k, {
|
|
454
|
+
__is_atscript_annotated_type: true,
|
|
455
|
+
type: v.type,
|
|
456
|
+
metadata: new Map(v.metadata),
|
|
457
|
+
id: v.id,
|
|
458
|
+
optional: v.optional,
|
|
459
|
+
validator(opts) {
|
|
460
|
+
return new Validator(this, opts);
|
|
461
|
+
}
|
|
462
|
+
}])),
|
|
463
|
+
propsPatterns: [...obj.propsPatterns],
|
|
464
|
+
tags: new Set(obj.tags)
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (type.kind === "array") {
|
|
468
|
+
const arr = type;
|
|
469
|
+
return {
|
|
470
|
+
kind: "array",
|
|
471
|
+
of: arr.of,
|
|
472
|
+
tags: new Set(arr.tags)
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (type.kind === "union" || type.kind === "intersection" || type.kind === "tuple") {
|
|
476
|
+
const complex = type;
|
|
477
|
+
return {
|
|
478
|
+
kind: type.kind,
|
|
479
|
+
items: [...complex.items],
|
|
480
|
+
tags: new Set(complex.tags)
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
...type,
|
|
485
|
+
tags: new Set(type.tags)
|
|
486
|
+
};
|
|
487
|
+
}
|
|
431
488
|
function defineAnnotatedType(_kind, base) {
|
|
432
489
|
const kind = _kind || "";
|
|
433
490
|
const type = base?.type || {};
|
|
@@ -519,6 +576,7 @@ else if (!newBase) throw new Error(`"${typeName}" is not annotated type`);
|
|
|
519
576
|
__is_atscript_annotated_type: true,
|
|
520
577
|
type: newBase.type,
|
|
521
578
|
metadata,
|
|
579
|
+
id: newBase.id,
|
|
522
580
|
validator(opts) {
|
|
523
581
|
return new Validator(this, opts);
|
|
524
582
|
}
|
|
@@ -529,6 +587,10 @@ else if (!newBase) throw new Error(`"${typeName}" is not annotated type`);
|
|
|
529
587
|
annotate(key, value, asArray) {
|
|
530
588
|
annotate(this.$metadata, key, value, asArray);
|
|
531
589
|
return this;
|
|
590
|
+
},
|
|
591
|
+
id(value) {
|
|
592
|
+
this.$type.id = value;
|
|
593
|
+
return this;
|
|
532
594
|
}
|
|
533
595
|
};
|
|
534
596
|
return handle;
|
|
@@ -552,40 +614,112 @@ function isAnnotatedTypeOfPrimitive(t) {
|
|
|
552
614
|
|
|
553
615
|
//#endregion
|
|
554
616
|
//#region packages/typescript/src/json-schema.ts
|
|
617
|
+
/**
|
|
618
|
+
* Detects a discriminator property across union items.
|
|
619
|
+
*
|
|
620
|
+
* Scans all items for object-typed members that share a common property
|
|
621
|
+
* with distinct const/literal values. If exactly one such property exists,
|
|
622
|
+
* it is returned as the discriminator.
|
|
623
|
+
*/ function detectDiscriminator(items) {
|
|
624
|
+
if (items.length < 2) return null;
|
|
625
|
+
for (const item of items) if (item.type.kind !== "object") return null;
|
|
626
|
+
const firstObj = items[0].type;
|
|
627
|
+
const candidates = [];
|
|
628
|
+
for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== undefined) candidates.push(propName);
|
|
629
|
+
const validCandidates = [];
|
|
630
|
+
for (const candidate of candidates) {
|
|
631
|
+
const values = new Set();
|
|
632
|
+
const mapping = {};
|
|
633
|
+
let valid = true;
|
|
634
|
+
for (let i = 0; i < items.length; i++) {
|
|
635
|
+
const obj = items[i].type;
|
|
636
|
+
const prop = obj.props.get(candidate);
|
|
637
|
+
if (!prop || prop.type.kind !== "" || prop.type.value === undefined) {
|
|
638
|
+
valid = false;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
const val = prop.type.value;
|
|
642
|
+
if (values.has(val)) {
|
|
643
|
+
valid = false;
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
values.add(val);
|
|
647
|
+
mapping[String(val)] = `#/oneOf/${i}`;
|
|
648
|
+
}
|
|
649
|
+
if (valid) validCandidates.push({
|
|
650
|
+
propertyName: candidate,
|
|
651
|
+
mapping
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
if (validCandidates.length === 1) return validCandidates[0];
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
555
657
|
function buildJsonSchema(type) {
|
|
658
|
+
const defs = {};
|
|
659
|
+
let isRoot = true;
|
|
660
|
+
const buildObject = (d) => {
|
|
661
|
+
const properties = {};
|
|
662
|
+
const required = [];
|
|
663
|
+
for (const [key, val] of d.type.props.entries()) {
|
|
664
|
+
if (isPhantomType(val)) continue;
|
|
665
|
+
properties[key] = build$1(val);
|
|
666
|
+
if (!val.optional) required.push(key);
|
|
667
|
+
}
|
|
668
|
+
const schema$1 = {
|
|
669
|
+
type: "object",
|
|
670
|
+
properties
|
|
671
|
+
};
|
|
672
|
+
if (required.length > 0) schema$1.required = required;
|
|
673
|
+
return schema$1;
|
|
674
|
+
};
|
|
556
675
|
const build$1 = (def) => {
|
|
676
|
+
if (def.id && def.type.kind === "object" && !isRoot) {
|
|
677
|
+
const name = def.id;
|
|
678
|
+
if (!defs[name]) {
|
|
679
|
+
defs[name] = {};
|
|
680
|
+
defs[name] = buildObject(def);
|
|
681
|
+
}
|
|
682
|
+
return { $ref: `#/$defs/${name}` };
|
|
683
|
+
}
|
|
684
|
+
isRoot = false;
|
|
557
685
|
const meta = def.metadata;
|
|
558
686
|
return forAnnotatedType(def, {
|
|
559
687
|
phantom() {
|
|
560
688
|
return {};
|
|
561
689
|
},
|
|
562
690
|
object(d) {
|
|
563
|
-
|
|
564
|
-
const required = [];
|
|
565
|
-
for (const [key, val] of d.type.props.entries()) {
|
|
566
|
-
if (isPhantomType(val)) continue;
|
|
567
|
-
properties[key] = build$1(val);
|
|
568
|
-
if (!val.optional) required.push(key);
|
|
569
|
-
}
|
|
570
|
-
const schema = {
|
|
571
|
-
type: "object",
|
|
572
|
-
properties
|
|
573
|
-
};
|
|
574
|
-
if (required.length > 0) schema.required = required;
|
|
575
|
-
return schema;
|
|
691
|
+
return buildObject(d);
|
|
576
692
|
},
|
|
577
693
|
array(d) {
|
|
578
|
-
const schema = {
|
|
694
|
+
const schema$1 = {
|
|
579
695
|
type: "array",
|
|
580
696
|
items: build$1(d.type.of)
|
|
581
697
|
};
|
|
582
698
|
const minLength = meta.get("expect.minLength");
|
|
583
|
-
if (minLength) schema.minItems = typeof minLength === "number" ? minLength : minLength.length;
|
|
699
|
+
if (minLength) schema$1.minItems = typeof minLength === "number" ? minLength : minLength.length;
|
|
584
700
|
const maxLength = meta.get("expect.maxLength");
|
|
585
|
-
if (maxLength) schema.maxItems = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
586
|
-
return schema;
|
|
701
|
+
if (maxLength) schema$1.maxItems = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
702
|
+
return schema$1;
|
|
587
703
|
},
|
|
588
704
|
union(d) {
|
|
705
|
+
const disc = detectDiscriminator(d.type.items);
|
|
706
|
+
if (disc) {
|
|
707
|
+
const oneOf = d.type.items.map(build$1);
|
|
708
|
+
const mapping = {};
|
|
709
|
+
for (const [val, origPath] of Object.entries(disc.mapping)) {
|
|
710
|
+
const idx = Number.parseInt(origPath.split("/").pop());
|
|
711
|
+
const item = d.type.items[idx];
|
|
712
|
+
if (item.id && defs[item.id]) mapping[val] = `#/$defs/${item.id}`;
|
|
713
|
+
else mapping[val] = origPath;
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
oneOf,
|
|
717
|
+
discriminator: {
|
|
718
|
+
propertyName: disc.propertyName,
|
|
719
|
+
mapping
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
}
|
|
589
723
|
return { anyOf: d.type.items.map(build$1) };
|
|
590
724
|
},
|
|
591
725
|
intersection(d) {
|
|
@@ -599,38 +733,54 @@ function buildJsonSchema(type) {
|
|
|
599
733
|
};
|
|
600
734
|
},
|
|
601
735
|
final(d) {
|
|
602
|
-
const schema = {};
|
|
603
|
-
if (d.type.value !== undefined) schema.const = d.type.value;
|
|
736
|
+
const schema$1 = {};
|
|
737
|
+
if (d.type.value !== undefined) schema$1.const = d.type.value;
|
|
604
738
|
if (d.type.designType && d.type.designType !== "any") {
|
|
605
|
-
schema.type = d.type.designType === "undefined" ? "null" : d.type.designType;
|
|
606
|
-
if (schema.type === "number" && meta.get("expect.int")) schema.type = "integer";
|
|
739
|
+
schema$1.type = d.type.designType === "undefined" ? "null" : d.type.designType;
|
|
740
|
+
if (schema$1.type === "number" && meta.get("expect.int")) schema$1.type = "integer";
|
|
607
741
|
}
|
|
608
|
-
if (schema.type === "string") {
|
|
609
|
-
if (meta.get("meta.required")) schema.minLength = 1;
|
|
742
|
+
if (schema$1.type === "string") {
|
|
743
|
+
if (meta.get("meta.required")) schema$1.minLength = 1;
|
|
610
744
|
const minLength = meta.get("expect.minLength");
|
|
611
|
-
if (minLength) schema.minLength = typeof minLength === "number" ? minLength : minLength.length;
|
|
745
|
+
if (minLength) schema$1.minLength = typeof minLength === "number" ? minLength : minLength.length;
|
|
612
746
|
const maxLength = meta.get("expect.maxLength");
|
|
613
|
-
if (maxLength) schema.maxLength = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
747
|
+
if (maxLength) schema$1.maxLength = typeof maxLength === "number" ? maxLength : maxLength.length;
|
|
614
748
|
const patterns = meta.get("expect.pattern");
|
|
615
|
-
if (patterns?.length) if (patterns.length === 1) schema.pattern = patterns[0].pattern;
|
|
616
|
-
else schema.allOf = (schema.allOf || []).concat(patterns.map((p) => ({ pattern: p.pattern })));
|
|
749
|
+
if (patterns?.length) if (patterns.length === 1) schema$1.pattern = patterns[0].pattern;
|
|
750
|
+
else schema$1.allOf = (schema$1.allOf || []).concat(patterns.map((p) => ({ pattern: p.pattern })));
|
|
617
751
|
}
|
|
618
|
-
if (schema.type === "number" || schema.type === "integer") {
|
|
752
|
+
if (schema$1.type === "number" || schema$1.type === "integer") {
|
|
619
753
|
const min = meta.get("expect.min");
|
|
620
|
-
if (min) schema.minimum = typeof min === "number" ? min : min.minValue;
|
|
754
|
+
if (min) schema$1.minimum = typeof min === "number" ? min : min.minValue;
|
|
621
755
|
const max = meta.get("expect.max");
|
|
622
|
-
if (max) schema.maximum = typeof max === "number" ? max : max.maxValue;
|
|
756
|
+
if (max) schema$1.maximum = typeof max === "number" ? max : max.maxValue;
|
|
623
757
|
}
|
|
624
|
-
return schema;
|
|
758
|
+
return schema$1;
|
|
625
759
|
}
|
|
626
760
|
});
|
|
627
761
|
};
|
|
628
|
-
|
|
762
|
+
const schema = build$1(type);
|
|
763
|
+
if (Object.keys(defs).length > 0) return {
|
|
764
|
+
...schema,
|
|
765
|
+
$defs: defs
|
|
766
|
+
};
|
|
767
|
+
return schema;
|
|
629
768
|
}
|
|
630
769
|
function fromJsonSchema(schema) {
|
|
770
|
+
const defsSource = schema.$defs || schema.definitions || {};
|
|
771
|
+
const resolved = new Map();
|
|
631
772
|
const convert = (s) => {
|
|
632
773
|
if (!s || Object.keys(s).length === 0) return defineAnnotatedType().designType("any").$type;
|
|
633
|
-
if (s.$ref)
|
|
774
|
+
if (s.$ref) {
|
|
775
|
+
const refName = s.$ref.replace(/^#\/(\$defs|definitions)\//, "");
|
|
776
|
+
if (resolved.has(refName)) return resolved.get(refName);
|
|
777
|
+
if (defsSource[refName]) {
|
|
778
|
+
const type = convert(defsSource[refName]);
|
|
779
|
+
resolved.set(refName, type);
|
|
780
|
+
return type;
|
|
781
|
+
}
|
|
782
|
+
throw new Error(`Unresolvable $ref: ${s.$ref}`);
|
|
783
|
+
}
|
|
634
784
|
if ("const" in s) {
|
|
635
785
|
const val = s.const;
|
|
636
786
|
const dt = val === null ? "null" : typeof val;
|
|
@@ -718,6 +868,24 @@ function fromJsonSchema(schema) {
|
|
|
718
868
|
};
|
|
719
869
|
return convert(schema);
|
|
720
870
|
}
|
|
871
|
+
function mergeJsonSchemas(types) {
|
|
872
|
+
const mergedDefs = {};
|
|
873
|
+
const schemas = {};
|
|
874
|
+
for (const type of types) {
|
|
875
|
+
const name = type.id;
|
|
876
|
+
if (!name) throw new Error("mergeJsonSchemas: all types must have an id");
|
|
877
|
+
const schema = buildJsonSchema(type);
|
|
878
|
+
if (schema.$defs) {
|
|
879
|
+
for (const [defName, defSchema] of Object.entries(schema.$defs)) if (!mergedDefs[defName]) mergedDefs[defName] = defSchema;
|
|
880
|
+
const { $defs: _,...rest } = schema;
|
|
881
|
+
schemas[name] = rest;
|
|
882
|
+
} else schemas[name] = schema;
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
schemas,
|
|
886
|
+
$defs: mergedDefs
|
|
887
|
+
};
|
|
888
|
+
}
|
|
721
889
|
|
|
722
890
|
//#endregion
|
|
723
891
|
//#region packages/typescript/src/default-value.ts
|
|
@@ -911,6 +1079,7 @@ function serializeNode(def, path, options) {
|
|
|
911
1079
|
metadata: serializeMetadata(def.metadata, path, def.type.kind, options)
|
|
912
1080
|
};
|
|
913
1081
|
if (def.optional) result.optional = true;
|
|
1082
|
+
if (def.id) result.id = def.id;
|
|
914
1083
|
return result;
|
|
915
1084
|
}
|
|
916
1085
|
function serializeTypeDef(def, path, options) {
|
|
@@ -1014,6 +1183,7 @@ function deserializeNode(data) {
|
|
|
1014
1183
|
}
|
|
1015
1184
|
};
|
|
1016
1185
|
if (data.optional) result.optional = true;
|
|
1186
|
+
if (data.id) result.id = data.id;
|
|
1017
1187
|
return result;
|
|
1018
1188
|
}
|
|
1019
1189
|
function deserializeTypeDef(t) {
|
|
@@ -1064,6 +1234,7 @@ exports.Validator = Validator
|
|
|
1064
1234
|
exports.ValidatorError = ValidatorError
|
|
1065
1235
|
exports.annotate = annotate
|
|
1066
1236
|
exports.buildJsonSchema = buildJsonSchema
|
|
1237
|
+
exports.cloneRefProp = cloneRefProp
|
|
1067
1238
|
exports.createDataFromAnnotatedType = createDataFromAnnotatedType
|
|
1068
1239
|
exports.defineAnnotatedType = defineAnnotatedType
|
|
1069
1240
|
exports.deserializeAnnotatedType = deserializeAnnotatedType
|
|
@@ -1073,5 +1244,6 @@ exports.fromJsonSchema = fromJsonSchema
|
|
|
1073
1244
|
exports.isAnnotatedType = isAnnotatedType
|
|
1074
1245
|
exports.isAnnotatedTypeOfPrimitive = isAnnotatedTypeOfPrimitive
|
|
1075
1246
|
exports.isPhantomType = isPhantomType
|
|
1247
|
+
exports.mergeJsonSchemas = mergeJsonSchemas
|
|
1076
1248
|
exports.serializeAnnotatedType = serializeAnnotatedType
|
|
1077
1249
|
exports.throwFeatureDisabled = throwFeatureDisabled
|