@formspec/build 0.1.0-alpha.13 → 0.1.0-alpha.15
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/README.md +20 -20
- package/dist/__tests__/alias-chain-propagation.test.d.ts +9 -0
- package/dist/__tests__/alias-chain-propagation.test.d.ts.map +1 -0
- package/dist/__tests__/extension-runtime.integration.test.d.ts +2 -0
- package/dist/__tests__/extension-runtime.integration.test.d.ts.map +1 -0
- package/dist/__tests__/fixtures/alias-chains.d.ts +37 -0
- package/dist/__tests__/fixtures/alias-chains.d.ts.map +1 -0
- package/dist/__tests__/fixtures/edge-cases.d.ts +11 -0
- package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
- package/dist/__tests__/fixtures/example-a-builtins.d.ts +13 -13
- package/dist/__tests__/fixtures/example-interface-types.d.ts +33 -33
- package/dist/__tests__/fixtures/example-interface-types.d.ts.map +1 -1
- package/dist/__tests__/jsdoc-constraints.test.d.ts +4 -5
- package/dist/__tests__/jsdoc-constraints.test.d.ts.map +1 -1
- package/dist/__tests__/json-utils.test.d.ts +5 -0
- package/dist/__tests__/json-utils.test.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts +19 -0
- package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts +6 -0
- package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts +17 -0
- package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts +9 -0
- package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts +6 -0
- package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts +19 -0
- package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts.map +1 -0
- package/dist/__tests__/parity/utils.d.ts +6 -1
- package/dist/__tests__/parity/utils.d.ts.map +1 -1
- package/dist/__tests__/path-target-parser.test.d.ts +9 -0
- package/dist/__tests__/path-target-parser.test.d.ts.map +1 -0
- package/dist/analyzer/class-analyzer.d.ts +1 -1
- package/dist/analyzer/class-analyzer.d.ts.map +1 -1
- package/dist/analyzer/jsdoc-constraints.d.ts +8 -52
- package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
- package/dist/analyzer/json-utils.d.ts +22 -0
- package/dist/analyzer/json-utils.d.ts.map +1 -0
- package/dist/analyzer/tsdoc-parser.d.ts +24 -12
- package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
- package/dist/browser.cjs +452 -94
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +15 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +450 -94
- package/dist/browser.js.map +1 -1
- package/dist/build.d.ts +132 -5
- package/dist/canonicalize/tsdoc-canonicalizer.d.ts +3 -3
- package/dist/cli.cjs +406 -104
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +407 -104
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +386 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +20 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +386 -104
- package/dist/index.js.map +1 -1
- package/dist/internals.cjs +597 -172
- package/dist/internals.cjs.map +1 -1
- package/dist/internals.js +597 -172
- package/dist/internals.js.map +1 -1
- package/dist/json-schema/generator.d.ts +8 -2
- package/dist/json-schema/generator.d.ts.map +1 -1
- package/dist/json-schema/ir-generator.d.ts +25 -2
- package/dist/json-schema/ir-generator.d.ts.map +1 -1
- package/dist/json-schema/types.d.ts +1 -1
- package/dist/json-schema/types.d.ts.map +1 -1
- package/dist/validate/constraint-validator.d.ts +3 -7
- package/dist/validate/constraint-validator.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/internals.cjs
CHANGED
|
@@ -228,7 +228,7 @@ function canonicalizeArrayField(field) {
|
|
|
228
228
|
const itemsType = {
|
|
229
229
|
kind: "object",
|
|
230
230
|
properties: itemProperties,
|
|
231
|
-
additionalProperties:
|
|
231
|
+
additionalProperties: true
|
|
232
232
|
};
|
|
233
233
|
const type = { kind: "array", items: itemsType };
|
|
234
234
|
const constraints = [];
|
|
@@ -263,7 +263,7 @@ function canonicalizeObjectField(field) {
|
|
|
263
263
|
const type = {
|
|
264
264
|
kind: "object",
|
|
265
265
|
properties,
|
|
266
|
-
additionalProperties:
|
|
266
|
+
additionalProperties: true
|
|
267
267
|
};
|
|
268
268
|
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
269
269
|
}
|
|
@@ -511,26 +511,36 @@ var ts4 = __toESM(require("typescript"), 1);
|
|
|
511
511
|
|
|
512
512
|
// src/analyzer/jsdoc-constraints.ts
|
|
513
513
|
var ts3 = __toESM(require("typescript"), 1);
|
|
514
|
-
var import_core4 = require("@formspec/core");
|
|
515
514
|
|
|
516
515
|
// src/analyzer/tsdoc-parser.ts
|
|
517
516
|
var ts2 = __toESM(require("typescript"), 1);
|
|
518
517
|
var import_tsdoc = require("@microsoft/tsdoc");
|
|
519
518
|
var import_core3 = require("@formspec/core");
|
|
519
|
+
|
|
520
|
+
// src/analyzer/json-utils.ts
|
|
521
|
+
function tryParseJson(text) {
|
|
522
|
+
try {
|
|
523
|
+
return JSON.parse(text);
|
|
524
|
+
} catch {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/analyzer/tsdoc-parser.ts
|
|
520
530
|
var NUMERIC_CONSTRAINT_MAP = {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
531
|
+
minimum: "minimum",
|
|
532
|
+
maximum: "maximum",
|
|
533
|
+
exclusiveMinimum: "exclusiveMinimum",
|
|
534
|
+
exclusiveMaximum: "exclusiveMaximum",
|
|
535
|
+
multipleOf: "multipleOf"
|
|
525
536
|
};
|
|
526
537
|
var LENGTH_CONSTRAINT_MAP = {
|
|
527
|
-
|
|
528
|
-
|
|
538
|
+
minLength: "minLength",
|
|
539
|
+
maxLength: "maxLength",
|
|
540
|
+
minItems: "minItems",
|
|
541
|
+
maxItems: "maxItems"
|
|
529
542
|
};
|
|
530
|
-
var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["
|
|
531
|
-
function isBuiltinConstraintName(tagName) {
|
|
532
|
-
return tagName in import_core3.BUILTIN_CONSTRAINT_DEFINITIONS;
|
|
533
|
-
}
|
|
543
|
+
var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
|
|
534
544
|
function createFormSpecTSDocConfig() {
|
|
535
545
|
const config = new import_tsdoc.TSDocConfiguration();
|
|
536
546
|
for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
|
|
@@ -542,6 +552,15 @@ function createFormSpecTSDocConfig() {
|
|
|
542
552
|
})
|
|
543
553
|
);
|
|
544
554
|
}
|
|
555
|
+
for (const tagName of ["displayName", "description"]) {
|
|
556
|
+
config.addTagDefinition(
|
|
557
|
+
new import_tsdoc.TSDocTagDefinition({
|
|
558
|
+
tagName: "@" + tagName,
|
|
559
|
+
syntaxKind: import_tsdoc.TSDocTagSyntaxKind.BlockTag,
|
|
560
|
+
allowMultiple: true
|
|
561
|
+
})
|
|
562
|
+
);
|
|
563
|
+
}
|
|
545
564
|
return config;
|
|
546
565
|
}
|
|
547
566
|
var sharedParser;
|
|
@@ -570,7 +589,28 @@ function parseTSDocTags(node, file = "") {
|
|
|
570
589
|
);
|
|
571
590
|
const docComment = parserContext.docComment;
|
|
572
591
|
for (const block of docComment.customBlocks) {
|
|
573
|
-
const tagName = block.blockTag.tagName.substring(1);
|
|
592
|
+
const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
|
|
593
|
+
if (tagName === "displayName" || tagName === "description") {
|
|
594
|
+
const text2 = extractBlockText(block).trim();
|
|
595
|
+
if (text2 === "") continue;
|
|
596
|
+
const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
|
|
597
|
+
if (tagName === "displayName") {
|
|
598
|
+
annotations.push({
|
|
599
|
+
kind: "annotation",
|
|
600
|
+
annotationKind: "displayName",
|
|
601
|
+
value: text2,
|
|
602
|
+
provenance: provenance2
|
|
603
|
+
});
|
|
604
|
+
} else {
|
|
605
|
+
annotations.push({
|
|
606
|
+
kind: "annotation",
|
|
607
|
+
annotationKind: "description",
|
|
608
|
+
value: text2,
|
|
609
|
+
provenance: provenance2
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
574
614
|
if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
575
615
|
const text = extractBlockText(block).trim();
|
|
576
616
|
if (text === "") continue;
|
|
@@ -591,7 +631,7 @@ function parseTSDocTags(node, file = "") {
|
|
|
591
631
|
}
|
|
592
632
|
const jsDocTagsAll = ts2.getJSDocTags(node);
|
|
593
633
|
for (const tag of jsDocTagsAll) {
|
|
594
|
-
const tagName = tag.tagName.text;
|
|
634
|
+
const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
|
|
595
635
|
if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
596
636
|
const commentText = getTagCommentText(tag);
|
|
597
637
|
if (commentText === void 0 || commentText.trim() === "") continue;
|
|
@@ -602,43 +642,17 @@ function parseTSDocTags(node, file = "") {
|
|
|
602
642
|
constraints.push(constraintNode);
|
|
603
643
|
}
|
|
604
644
|
}
|
|
605
|
-
let displayName;
|
|
606
|
-
let description;
|
|
607
|
-
let displayNameTag;
|
|
608
|
-
let descriptionTag;
|
|
609
|
-
for (const tag of jsDocTagsAll) {
|
|
610
|
-
const tagName = tag.tagName.text;
|
|
611
|
-
const commentText = getTagCommentText(tag);
|
|
612
|
-
if (commentText === void 0 || commentText.trim() === "") {
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
const trimmed = commentText.trim();
|
|
616
|
-
if (tagName === "Field_displayName") {
|
|
617
|
-
displayName = trimmed;
|
|
618
|
-
displayNameTag = tag;
|
|
619
|
-
} else if (tagName === "Field_description") {
|
|
620
|
-
description = trimmed;
|
|
621
|
-
descriptionTag = tag;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
if (displayName !== void 0 && displayNameTag) {
|
|
625
|
-
annotations.push({
|
|
626
|
-
kind: "annotation",
|
|
627
|
-
annotationKind: "displayName",
|
|
628
|
-
value: displayName,
|
|
629
|
-
provenance: provenanceForJSDocTag(displayNameTag, file)
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
if (description !== void 0 && descriptionTag) {
|
|
633
|
-
annotations.push({
|
|
634
|
-
kind: "annotation",
|
|
635
|
-
annotationKind: "description",
|
|
636
|
-
value: description,
|
|
637
|
-
provenance: provenanceForJSDocTag(descriptionTag, file)
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
645
|
return { constraints, annotations };
|
|
641
646
|
}
|
|
647
|
+
function extractPathTarget(text) {
|
|
648
|
+
const trimmed = text.trimStart();
|
|
649
|
+
const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
|
|
650
|
+
if (!match?.[1] || !match[2]) return null;
|
|
651
|
+
return {
|
|
652
|
+
path: { segments: [match[1]] },
|
|
653
|
+
remainingText: match[2]
|
|
654
|
+
};
|
|
655
|
+
}
|
|
642
656
|
function extractBlockText(block) {
|
|
643
657
|
return extractPlainText(block.content);
|
|
644
658
|
}
|
|
@@ -658,12 +672,15 @@ function extractPlainText(node) {
|
|
|
658
672
|
return result;
|
|
659
673
|
}
|
|
660
674
|
function parseConstraintValue(tagName, text, provenance) {
|
|
661
|
-
if (!isBuiltinConstraintName(tagName)) {
|
|
675
|
+
if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
|
|
662
676
|
return null;
|
|
663
677
|
}
|
|
678
|
+
const pathResult = extractPathTarget(text);
|
|
679
|
+
const effectiveText = pathResult ? pathResult.remainingText : text;
|
|
680
|
+
const path2 = pathResult?.path;
|
|
664
681
|
const expectedType = import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
|
|
665
682
|
if (expectedType === "number") {
|
|
666
|
-
const value = Number(
|
|
683
|
+
const value = Number(effectiveText);
|
|
667
684
|
if (Number.isNaN(value)) {
|
|
668
685
|
return null;
|
|
669
686
|
}
|
|
@@ -673,6 +690,7 @@ function parseConstraintValue(tagName, text, provenance) {
|
|
|
673
690
|
kind: "constraint",
|
|
674
691
|
constraintKind: numericKind,
|
|
675
692
|
value,
|
|
693
|
+
...path2 && { path: path2 },
|
|
676
694
|
provenance
|
|
677
695
|
};
|
|
678
696
|
}
|
|
@@ -682,42 +700,41 @@ function parseConstraintValue(tagName, text, provenance) {
|
|
|
682
700
|
kind: "constraint",
|
|
683
701
|
constraintKind: lengthKind,
|
|
684
702
|
value,
|
|
703
|
+
...path2 && { path: path2 },
|
|
685
704
|
provenance
|
|
686
705
|
};
|
|
687
706
|
}
|
|
688
707
|
return null;
|
|
689
708
|
}
|
|
690
709
|
if (expectedType === "json") {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
members.push(id);
|
|
704
|
-
}
|
|
710
|
+
const parsed = tryParseJson(effectiveText);
|
|
711
|
+
if (!Array.isArray(parsed)) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
const members = [];
|
|
715
|
+
for (const item of parsed) {
|
|
716
|
+
if (typeof item === "string" || typeof item === "number") {
|
|
717
|
+
members.push(item);
|
|
718
|
+
} else if (typeof item === "object" && item !== null && "id" in item) {
|
|
719
|
+
const id = item["id"];
|
|
720
|
+
if (typeof id === "string" || typeof id === "number") {
|
|
721
|
+
members.push(id);
|
|
705
722
|
}
|
|
706
723
|
}
|
|
707
|
-
return {
|
|
708
|
-
kind: "constraint",
|
|
709
|
-
constraintKind: "allowedMembers",
|
|
710
|
-
members,
|
|
711
|
-
provenance
|
|
712
|
-
};
|
|
713
|
-
} catch {
|
|
714
|
-
return null;
|
|
715
724
|
}
|
|
725
|
+
return {
|
|
726
|
+
kind: "constraint",
|
|
727
|
+
constraintKind: "allowedMembers",
|
|
728
|
+
members,
|
|
729
|
+
...path2 && { path: path2 },
|
|
730
|
+
provenance
|
|
731
|
+
};
|
|
716
732
|
}
|
|
717
733
|
return {
|
|
718
734
|
kind: "constraint",
|
|
719
735
|
constraintKind: "pattern",
|
|
720
|
-
pattern:
|
|
736
|
+
pattern: effectiveText,
|
|
737
|
+
...path2 && { path: path2 },
|
|
721
738
|
provenance
|
|
722
739
|
};
|
|
723
740
|
}
|
|
@@ -889,18 +906,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
|
|
|
889
906
|
const tsType = checker.getTypeAtLocation(prop);
|
|
890
907
|
const optional = prop.questionToken !== void 0;
|
|
891
908
|
const provenance = provenanceForNode(prop, file);
|
|
892
|
-
|
|
909
|
+
let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
|
|
893
910
|
const constraints = [];
|
|
894
911
|
if (prop.type) {
|
|
895
912
|
constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
|
|
896
913
|
}
|
|
897
914
|
constraints.push(...extractJSDocConstraintNodes(prop, file));
|
|
898
|
-
|
|
915
|
+
let annotations = [];
|
|
899
916
|
annotations.push(...extractJSDocAnnotationNodes(prop, file));
|
|
900
917
|
const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
|
|
901
918
|
if (defaultAnnotation) {
|
|
902
919
|
annotations.push(defaultAnnotation);
|
|
903
920
|
}
|
|
921
|
+
({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
|
|
904
922
|
return {
|
|
905
923
|
kind: "field",
|
|
906
924
|
name,
|
|
@@ -919,14 +937,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
|
|
|
919
937
|
const tsType = checker.getTypeAtLocation(prop);
|
|
920
938
|
const optional = prop.questionToken !== void 0;
|
|
921
939
|
const provenance = provenanceForNode(prop, file);
|
|
922
|
-
|
|
940
|
+
let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
|
|
923
941
|
const constraints = [];
|
|
924
942
|
if (prop.type) {
|
|
925
943
|
constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
|
|
926
944
|
}
|
|
927
945
|
constraints.push(...extractJSDocConstraintNodes(prop, file));
|
|
928
|
-
|
|
946
|
+
let annotations = [];
|
|
929
947
|
annotations.push(...extractJSDocAnnotationNodes(prop, file));
|
|
948
|
+
({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
|
|
930
949
|
return {
|
|
931
950
|
kind: "field",
|
|
932
951
|
name,
|
|
@@ -937,6 +956,68 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
|
|
|
937
956
|
provenance
|
|
938
957
|
};
|
|
939
958
|
}
|
|
959
|
+
function applyEnumMemberDisplayNames(type, annotations) {
|
|
960
|
+
if (!annotations.some(
|
|
961
|
+
(annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
|
|
962
|
+
)) {
|
|
963
|
+
return { type, annotations: [...annotations] };
|
|
964
|
+
}
|
|
965
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
966
|
+
const nextType = rewriteEnumDisplayNames(type, annotations, consumed);
|
|
967
|
+
if (consumed.size === 0) {
|
|
968
|
+
return { type, annotations: [...annotations] };
|
|
969
|
+
}
|
|
970
|
+
return {
|
|
971
|
+
type: nextType,
|
|
972
|
+
annotations: annotations.filter((annotation) => !consumed.has(annotation))
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function rewriteEnumDisplayNames(type, annotations, consumed) {
|
|
976
|
+
switch (type.kind) {
|
|
977
|
+
case "enum":
|
|
978
|
+
return applyEnumMemberDisplayNamesToEnum(type, annotations, consumed);
|
|
979
|
+
case "union": {
|
|
980
|
+
return {
|
|
981
|
+
...type,
|
|
982
|
+
members: type.members.map(
|
|
983
|
+
(member) => rewriteEnumDisplayNames(member, annotations, consumed)
|
|
984
|
+
)
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
default:
|
|
988
|
+
return type;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function applyEnumMemberDisplayNamesToEnum(type, annotations, consumed) {
|
|
992
|
+
const displayNames = /* @__PURE__ */ new Map();
|
|
993
|
+
for (const annotation of annotations) {
|
|
994
|
+
if (annotation.annotationKind !== "displayName") continue;
|
|
995
|
+
const parsed = parseEnumMemberDisplayName(annotation.value);
|
|
996
|
+
if (!parsed) continue;
|
|
997
|
+
consumed.add(annotation);
|
|
998
|
+
const member = type.members.find((m) => String(m.value) === parsed.value);
|
|
999
|
+
if (!member) continue;
|
|
1000
|
+
displayNames.set(String(member.value), parsed.label);
|
|
1001
|
+
}
|
|
1002
|
+
if (displayNames.size === 0) {
|
|
1003
|
+
return type;
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
...type,
|
|
1007
|
+
members: type.members.map((member) => {
|
|
1008
|
+
const displayName = displayNames.get(String(member.value));
|
|
1009
|
+
return displayName !== void 0 ? { ...member, displayName } : member;
|
|
1010
|
+
})
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function parseEnumMemberDisplayName(value) {
|
|
1014
|
+
const trimmed = value.trim();
|
|
1015
|
+
const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(trimmed);
|
|
1016
|
+
if (!match?.[1] || !match[2]) return null;
|
|
1017
|
+
const label = match[2].trim();
|
|
1018
|
+
if (label === "") return null;
|
|
1019
|
+
return { value: match[1], label };
|
|
1020
|
+
}
|
|
940
1021
|
function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
|
|
941
1022
|
if (type.flags & ts4.TypeFlags.String) {
|
|
942
1023
|
return { kind: "primitive", primitiveKind: "string" };
|
|
@@ -1047,7 +1128,30 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
|
|
|
1047
1128
|
const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
|
|
1048
1129
|
return { kind: "array", items };
|
|
1049
1130
|
}
|
|
1131
|
+
function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
|
|
1132
|
+
if (type.getProperties().length > 0) {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
const indexInfo = checker.getIndexInfoOfType(type, ts4.IndexKind.String);
|
|
1136
|
+
if (!indexInfo) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
if (visiting.has(type)) {
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
visiting.add(type);
|
|
1143
|
+
try {
|
|
1144
|
+
const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
|
|
1145
|
+
return { kind: "record", valueType };
|
|
1146
|
+
} finally {
|
|
1147
|
+
visiting.delete(type);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1050
1150
|
function resolveObjectType(type, checker, file, typeRegistry, visiting) {
|
|
1151
|
+
const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
|
|
1152
|
+
if (recordNode) {
|
|
1153
|
+
return recordNode;
|
|
1154
|
+
}
|
|
1051
1155
|
if (visiting.has(type)) {
|
|
1052
1156
|
return { kind: "object", properties: [], additionalProperties: false };
|
|
1053
1157
|
}
|
|
@@ -1079,7 +1183,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
|
|
|
1079
1183
|
const objectNode = {
|
|
1080
1184
|
kind: "object",
|
|
1081
1185
|
properties,
|
|
1082
|
-
additionalProperties:
|
|
1186
|
+
additionalProperties: true
|
|
1083
1187
|
};
|
|
1084
1188
|
if (typeName) {
|
|
1085
1189
|
typeRegistry[typeName] = {
|
|
@@ -1148,14 +1252,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
|
|
|
1148
1252
|
}
|
|
1149
1253
|
return map;
|
|
1150
1254
|
}
|
|
1151
|
-
|
|
1255
|
+
var MAX_ALIAS_CHAIN_DEPTH = 8;
|
|
1256
|
+
function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
|
|
1152
1257
|
if (!ts4.isTypeReferenceNode(typeNode)) return [];
|
|
1258
|
+
if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
|
|
1259
|
+
const aliasName = typeNode.typeName.getText();
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
`Type alias chain exceeds maximum depth of ${String(MAX_ALIAS_CHAIN_DEPTH)} at alias "${aliasName}" in ${file}. Simplify the alias chain or check for circular references.`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1153
1264
|
const symbol = checker.getSymbolAtLocation(typeNode.typeName);
|
|
1154
1265
|
if (!symbol?.declarations) return [];
|
|
1155
1266
|
const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
|
|
1156
1267
|
if (!aliasDecl) return [];
|
|
1157
1268
|
if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
|
|
1158
|
-
|
|
1269
|
+
const constraints = extractJSDocConstraintNodes(aliasDecl, file);
|
|
1270
|
+
constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
|
|
1271
|
+
return constraints;
|
|
1159
1272
|
}
|
|
1160
1273
|
function provenanceForNode(node, file) {
|
|
1161
1274
|
const sourceFile = node.getSourceFile();
|
|
@@ -1230,11 +1343,21 @@ function detectFormSpecReference(typeNode) {
|
|
|
1230
1343
|
}
|
|
1231
1344
|
|
|
1232
1345
|
// src/json-schema/ir-generator.ts
|
|
1233
|
-
function makeContext() {
|
|
1234
|
-
|
|
1346
|
+
function makeContext(options) {
|
|
1347
|
+
const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
|
|
1348
|
+
if (!vendorPrefix.startsWith("x-")) {
|
|
1349
|
+
throw new Error(
|
|
1350
|
+
`Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
defs: {},
|
|
1355
|
+
extensionRegistry: options?.extensionRegistry,
|
|
1356
|
+
vendorPrefix
|
|
1357
|
+
};
|
|
1235
1358
|
}
|
|
1236
|
-
function generateJsonSchemaFromIR(ir) {
|
|
1237
|
-
const ctx = makeContext();
|
|
1359
|
+
function generateJsonSchemaFromIR(ir, options) {
|
|
1360
|
+
const ctx = makeContext(options);
|
|
1238
1361
|
for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
|
|
1239
1362
|
ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
|
|
1240
1363
|
}
|
|
@@ -1277,8 +1400,70 @@ function collectFields(elements, properties, required, ctx) {
|
|
|
1277
1400
|
}
|
|
1278
1401
|
function generateFieldSchema(field, ctx) {
|
|
1279
1402
|
const schema = generateTypeNode(field.type, ctx);
|
|
1280
|
-
|
|
1281
|
-
|
|
1403
|
+
const directConstraints = [];
|
|
1404
|
+
const pathConstraints = [];
|
|
1405
|
+
for (const c of field.constraints) {
|
|
1406
|
+
if (c.path) {
|
|
1407
|
+
pathConstraints.push(c);
|
|
1408
|
+
} else {
|
|
1409
|
+
directConstraints.push(c);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
applyConstraints(schema, directConstraints, ctx);
|
|
1413
|
+
applyAnnotations(schema, field.annotations, ctx);
|
|
1414
|
+
if (pathConstraints.length === 0) {
|
|
1415
|
+
return schema;
|
|
1416
|
+
}
|
|
1417
|
+
return applyPathTargetedConstraints(schema, pathConstraints, ctx);
|
|
1418
|
+
}
|
|
1419
|
+
function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
|
|
1420
|
+
if (schema.type === "array" && schema.items) {
|
|
1421
|
+
schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
|
|
1422
|
+
return schema;
|
|
1423
|
+
}
|
|
1424
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
1425
|
+
for (const c of pathConstraints) {
|
|
1426
|
+
const target = c.path?.segments[0];
|
|
1427
|
+
if (!target) continue;
|
|
1428
|
+
const group = byTarget.get(target) ?? [];
|
|
1429
|
+
group.push(c);
|
|
1430
|
+
byTarget.set(target, group);
|
|
1431
|
+
}
|
|
1432
|
+
const propertyOverrides = {};
|
|
1433
|
+
for (const [target, constraints] of byTarget) {
|
|
1434
|
+
const subSchema = {};
|
|
1435
|
+
applyConstraints(subSchema, constraints, ctx);
|
|
1436
|
+
propertyOverrides[target] = subSchema;
|
|
1437
|
+
}
|
|
1438
|
+
if (schema.$ref) {
|
|
1439
|
+
const { $ref, ...rest } = schema;
|
|
1440
|
+
const refPart = { $ref };
|
|
1441
|
+
const overridePart = {
|
|
1442
|
+
properties: propertyOverrides,
|
|
1443
|
+
...rest
|
|
1444
|
+
};
|
|
1445
|
+
return { allOf: [refPart, overridePart] };
|
|
1446
|
+
}
|
|
1447
|
+
if (schema.type === "object" && schema.properties) {
|
|
1448
|
+
const missingOverrides = {};
|
|
1449
|
+
for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
|
|
1450
|
+
if (schema.properties[target]) {
|
|
1451
|
+
Object.assign(schema.properties[target], overrideSchema);
|
|
1452
|
+
} else {
|
|
1453
|
+
missingOverrides[target] = overrideSchema;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (Object.keys(missingOverrides).length === 0) {
|
|
1457
|
+
return schema;
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
allOf: [schema, { properties: missingOverrides }]
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
if (schema.allOf) {
|
|
1464
|
+
schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
|
|
1465
|
+
return schema;
|
|
1466
|
+
}
|
|
1282
1467
|
return schema;
|
|
1283
1468
|
}
|
|
1284
1469
|
function generateTypeNode(type, ctx) {
|
|
@@ -1291,6 +1476,8 @@ function generateTypeNode(type, ctx) {
|
|
|
1291
1476
|
return generateArrayType(type, ctx);
|
|
1292
1477
|
case "object":
|
|
1293
1478
|
return generateObjectType(type, ctx);
|
|
1479
|
+
case "record":
|
|
1480
|
+
return generateRecordType(type, ctx);
|
|
1294
1481
|
case "union":
|
|
1295
1482
|
return generateUnionType(type, ctx);
|
|
1296
1483
|
case "reference":
|
|
@@ -1298,7 +1485,7 @@ function generateTypeNode(type, ctx) {
|
|
|
1298
1485
|
case "dynamic":
|
|
1299
1486
|
return generateDynamicType(type);
|
|
1300
1487
|
case "custom":
|
|
1301
|
-
return generateCustomType(type);
|
|
1488
|
+
return generateCustomType(type, ctx);
|
|
1302
1489
|
default: {
|
|
1303
1490
|
const _exhaustive = type;
|
|
1304
1491
|
return _exhaustive;
|
|
@@ -1347,16 +1534,27 @@ function generateObjectType(type, ctx) {
|
|
|
1347
1534
|
}
|
|
1348
1535
|
return schema;
|
|
1349
1536
|
}
|
|
1537
|
+
function generateRecordType(type, ctx) {
|
|
1538
|
+
return {
|
|
1539
|
+
type: "object",
|
|
1540
|
+
additionalProperties: generateTypeNode(type.valueType, ctx)
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1350
1543
|
function generatePropertySchema(prop, ctx) {
|
|
1351
1544
|
const schema = generateTypeNode(prop.type, ctx);
|
|
1352
|
-
applyConstraints(schema, prop.constraints);
|
|
1353
|
-
applyAnnotations(schema, prop.annotations);
|
|
1545
|
+
applyConstraints(schema, prop.constraints, ctx);
|
|
1546
|
+
applyAnnotations(schema, prop.annotations, ctx);
|
|
1354
1547
|
return schema;
|
|
1355
1548
|
}
|
|
1356
1549
|
function generateUnionType(type, ctx) {
|
|
1357
1550
|
if (isBooleanUnion(type)) {
|
|
1358
1551
|
return { type: "boolean" };
|
|
1359
1552
|
}
|
|
1553
|
+
if (isNullableUnion(type)) {
|
|
1554
|
+
return {
|
|
1555
|
+
oneOf: type.members.map((m) => generateTypeNode(m, ctx))
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1360
1558
|
return {
|
|
1361
1559
|
anyOf: type.members.map((m) => generateTypeNode(m, ctx))
|
|
1362
1560
|
};
|
|
@@ -1366,6 +1564,13 @@ function isBooleanUnion(type) {
|
|
|
1366
1564
|
const kinds = type.members.map((m) => m.kind);
|
|
1367
1565
|
return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
|
|
1368
1566
|
}
|
|
1567
|
+
function isNullableUnion(type) {
|
|
1568
|
+
if (type.members.length !== 2) return false;
|
|
1569
|
+
const nullCount = type.members.filter(
|
|
1570
|
+
(m) => m.kind === "primitive" && m.primitiveKind === "null"
|
|
1571
|
+
).length;
|
|
1572
|
+
return nullCount === 1;
|
|
1573
|
+
}
|
|
1369
1574
|
function generateReferenceType(type) {
|
|
1370
1575
|
return { $ref: `#/$defs/${type.name}` };
|
|
1371
1576
|
}
|
|
@@ -1386,10 +1591,7 @@ function generateDynamicType(type) {
|
|
|
1386
1591
|
"x-formspec-schemaSource": type.sourceKey
|
|
1387
1592
|
};
|
|
1388
1593
|
}
|
|
1389
|
-
function
|
|
1390
|
-
return { type: "object" };
|
|
1391
|
-
}
|
|
1392
|
-
function applyConstraints(schema, constraints) {
|
|
1594
|
+
function applyConstraints(schema, constraints, ctx) {
|
|
1393
1595
|
for (const constraint of constraints) {
|
|
1394
1596
|
switch (constraint.constraintKind) {
|
|
1395
1597
|
case "minimum":
|
|
@@ -1434,6 +1636,7 @@ function applyConstraints(schema, constraints) {
|
|
|
1434
1636
|
case "allowedMembers":
|
|
1435
1637
|
break;
|
|
1436
1638
|
case "custom":
|
|
1639
|
+
applyCustomConstraint(schema, constraint, ctx);
|
|
1437
1640
|
break;
|
|
1438
1641
|
default: {
|
|
1439
1642
|
const _exhaustive = constraint;
|
|
@@ -1442,7 +1645,7 @@ function applyConstraints(schema, constraints) {
|
|
|
1442
1645
|
}
|
|
1443
1646
|
}
|
|
1444
1647
|
}
|
|
1445
|
-
function applyAnnotations(schema, annotations) {
|
|
1648
|
+
function applyAnnotations(schema, annotations, ctx) {
|
|
1446
1649
|
for (const annotation of annotations) {
|
|
1447
1650
|
switch (annotation.annotationKind) {
|
|
1448
1651
|
case "displayName":
|
|
@@ -1462,6 +1665,7 @@ function applyAnnotations(schema, annotations) {
|
|
|
1462
1665
|
case "formatHint":
|
|
1463
1666
|
break;
|
|
1464
1667
|
case "custom":
|
|
1668
|
+
applyCustomAnnotation(schema, annotation, ctx);
|
|
1465
1669
|
break;
|
|
1466
1670
|
default: {
|
|
1467
1671
|
const _exhaustive = annotation;
|
|
@@ -1470,6 +1674,36 @@ function applyAnnotations(schema, annotations) {
|
|
|
1470
1674
|
}
|
|
1471
1675
|
}
|
|
1472
1676
|
}
|
|
1677
|
+
function generateCustomType(type, ctx) {
|
|
1678
|
+
const registration = ctx.extensionRegistry?.findType(type.typeId);
|
|
1679
|
+
if (registration === void 0) {
|
|
1680
|
+
throw new Error(
|
|
1681
|
+
`Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
|
|
1685
|
+
}
|
|
1686
|
+
function applyCustomConstraint(schema, constraint, ctx) {
|
|
1687
|
+
const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
|
|
1688
|
+
if (registration === void 0) {
|
|
1689
|
+
throw new Error(
|
|
1690
|
+
`Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
|
|
1694
|
+
}
|
|
1695
|
+
function applyCustomAnnotation(schema, annotation, ctx) {
|
|
1696
|
+
const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
|
|
1697
|
+
if (registration === void 0) {
|
|
1698
|
+
throw new Error(
|
|
1699
|
+
`Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
if (registration.toJsonSchema === void 0) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
|
|
1706
|
+
}
|
|
1473
1707
|
|
|
1474
1708
|
// src/ui-schema/schema.ts
|
|
1475
1709
|
var import_zod = require("zod");
|
|
@@ -1694,12 +1928,9 @@ function generateClassSchemas(analysis, source) {
|
|
|
1694
1928
|
}
|
|
1695
1929
|
|
|
1696
1930
|
// src/validate/constraint-validator.ts
|
|
1697
|
-
function makeCode(ctx, category, number) {
|
|
1698
|
-
return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
|
|
1699
|
-
}
|
|
1700
1931
|
function addContradiction(ctx, message, primary, related) {
|
|
1701
1932
|
ctx.diagnostics.push({
|
|
1702
|
-
code:
|
|
1933
|
+
code: "CONTRADICTING_CONSTRAINTS",
|
|
1703
1934
|
message,
|
|
1704
1935
|
severity: "error",
|
|
1705
1936
|
primaryLocation: primary,
|
|
@@ -1708,7 +1939,7 @@ function addContradiction(ctx, message, primary, related) {
|
|
|
1708
1939
|
}
|
|
1709
1940
|
function addTypeMismatch(ctx, message, primary) {
|
|
1710
1941
|
ctx.diagnostics.push({
|
|
1711
|
-
code:
|
|
1942
|
+
code: "TYPE_MISMATCH",
|
|
1712
1943
|
message,
|
|
1713
1944
|
severity: "error",
|
|
1714
1945
|
primaryLocation: primary,
|
|
@@ -1717,28 +1948,153 @@ function addTypeMismatch(ctx, message, primary) {
|
|
|
1717
1948
|
}
|
|
1718
1949
|
function addUnknownExtension(ctx, message, primary) {
|
|
1719
1950
|
ctx.diagnostics.push({
|
|
1720
|
-
code:
|
|
1951
|
+
code: "UNKNOWN_EXTENSION",
|
|
1721
1952
|
message,
|
|
1722
1953
|
severity: "warning",
|
|
1723
1954
|
primaryLocation: primary,
|
|
1724
1955
|
relatedLocations: []
|
|
1725
1956
|
});
|
|
1726
1957
|
}
|
|
1958
|
+
function addConstraintBroadening(ctx, message, primary, related) {
|
|
1959
|
+
ctx.diagnostics.push({
|
|
1960
|
+
code: "CONSTRAINT_BROADENING",
|
|
1961
|
+
message,
|
|
1962
|
+
severity: "error",
|
|
1963
|
+
primaryLocation: primary,
|
|
1964
|
+
relatedLocations: [related]
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1727
1967
|
function findNumeric(constraints, constraintKind) {
|
|
1728
|
-
return constraints.find(
|
|
1729
|
-
(c) => c.constraintKind === constraintKind
|
|
1730
|
-
);
|
|
1968
|
+
return constraints.find((c) => c.constraintKind === constraintKind);
|
|
1731
1969
|
}
|
|
1732
1970
|
function findLength(constraints, constraintKind) {
|
|
1733
|
-
return constraints.find(
|
|
1734
|
-
(c) => c.constraintKind === constraintKind
|
|
1735
|
-
);
|
|
1971
|
+
return constraints.find((c) => c.constraintKind === constraintKind);
|
|
1736
1972
|
}
|
|
1737
1973
|
function findAllowedMembers(constraints) {
|
|
1738
1974
|
return constraints.filter(
|
|
1739
1975
|
(c) => c.constraintKind === "allowedMembers"
|
|
1740
1976
|
);
|
|
1741
1977
|
}
|
|
1978
|
+
function isOrderedBoundConstraint(constraint) {
|
|
1979
|
+
return constraint.constraintKind === "minimum" || constraint.constraintKind === "exclusiveMinimum" || constraint.constraintKind === "minLength" || constraint.constraintKind === "minItems" || constraint.constraintKind === "maximum" || constraint.constraintKind === "exclusiveMaximum" || constraint.constraintKind === "maxLength" || constraint.constraintKind === "maxItems";
|
|
1980
|
+
}
|
|
1981
|
+
function pathKey(constraint) {
|
|
1982
|
+
return constraint.path?.segments.join(".") ?? "";
|
|
1983
|
+
}
|
|
1984
|
+
function orderedBoundFamily(kind) {
|
|
1985
|
+
switch (kind) {
|
|
1986
|
+
case "minimum":
|
|
1987
|
+
case "exclusiveMinimum":
|
|
1988
|
+
return "numeric-lower";
|
|
1989
|
+
case "maximum":
|
|
1990
|
+
case "exclusiveMaximum":
|
|
1991
|
+
return "numeric-upper";
|
|
1992
|
+
case "minLength":
|
|
1993
|
+
return "minLength";
|
|
1994
|
+
case "minItems":
|
|
1995
|
+
return "minItems";
|
|
1996
|
+
case "maxLength":
|
|
1997
|
+
return "maxLength";
|
|
1998
|
+
case "maxItems":
|
|
1999
|
+
return "maxItems";
|
|
2000
|
+
default: {
|
|
2001
|
+
const _exhaustive = kind;
|
|
2002
|
+
return _exhaustive;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function isNumericLowerKind(kind) {
|
|
2007
|
+
return kind === "minimum" || kind === "exclusiveMinimum";
|
|
2008
|
+
}
|
|
2009
|
+
function isNumericUpperKind(kind) {
|
|
2010
|
+
return kind === "maximum" || kind === "exclusiveMaximum";
|
|
2011
|
+
}
|
|
2012
|
+
function describeConstraintTag(constraint) {
|
|
2013
|
+
return `@${constraint.constraintKind}`;
|
|
2014
|
+
}
|
|
2015
|
+
function compareConstraintStrength(current, previous) {
|
|
2016
|
+
const family = orderedBoundFamily(current.constraintKind);
|
|
2017
|
+
if (family === "numeric-lower") {
|
|
2018
|
+
if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
|
|
2019
|
+
throw new Error("numeric-lower family received non-numeric lower-bound constraint");
|
|
2020
|
+
}
|
|
2021
|
+
if (current.value !== previous.value) {
|
|
2022
|
+
return current.value > previous.value ? 1 : -1;
|
|
2023
|
+
}
|
|
2024
|
+
if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
|
|
2025
|
+
return 1;
|
|
2026
|
+
}
|
|
2027
|
+
if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
|
|
2028
|
+
return -1;
|
|
2029
|
+
}
|
|
2030
|
+
return 0;
|
|
2031
|
+
}
|
|
2032
|
+
if (family === "numeric-upper") {
|
|
2033
|
+
if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
|
|
2034
|
+
throw new Error("numeric-upper family received non-numeric upper-bound constraint");
|
|
2035
|
+
}
|
|
2036
|
+
if (current.value !== previous.value) {
|
|
2037
|
+
return current.value < previous.value ? 1 : -1;
|
|
2038
|
+
}
|
|
2039
|
+
if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
|
|
2040
|
+
return 1;
|
|
2041
|
+
}
|
|
2042
|
+
if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
|
|
2043
|
+
return -1;
|
|
2044
|
+
}
|
|
2045
|
+
return 0;
|
|
2046
|
+
}
|
|
2047
|
+
switch (family) {
|
|
2048
|
+
case "minLength":
|
|
2049
|
+
case "minItems":
|
|
2050
|
+
if (current.value === previous.value) {
|
|
2051
|
+
return 0;
|
|
2052
|
+
}
|
|
2053
|
+
return current.value > previous.value ? 1 : -1;
|
|
2054
|
+
case "maxLength":
|
|
2055
|
+
case "maxItems":
|
|
2056
|
+
if (current.value === previous.value) {
|
|
2057
|
+
return 0;
|
|
2058
|
+
}
|
|
2059
|
+
return current.value < previous.value ? 1 : -1;
|
|
2060
|
+
default: {
|
|
2061
|
+
const _exhaustive = family;
|
|
2062
|
+
return _exhaustive;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
function checkConstraintBroadening(ctx, fieldName, constraints) {
|
|
2067
|
+
const strongestByKey = /* @__PURE__ */ new Map();
|
|
2068
|
+
for (const constraint of constraints) {
|
|
2069
|
+
if (!isOrderedBoundConstraint(constraint)) {
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
|
|
2073
|
+
const previous = strongestByKey.get(key);
|
|
2074
|
+
if (previous === void 0) {
|
|
2075
|
+
strongestByKey.set(key, constraint);
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
2078
|
+
const strength = compareConstraintStrength(constraint, previous);
|
|
2079
|
+
if (strength < 0) {
|
|
2080
|
+
const displayFieldName = formatPathTargetFieldName(
|
|
2081
|
+
fieldName,
|
|
2082
|
+
constraint.path?.segments ?? []
|
|
2083
|
+
);
|
|
2084
|
+
addConstraintBroadening(
|
|
2085
|
+
ctx,
|
|
2086
|
+
`Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
|
|
2087
|
+
constraint.provenance,
|
|
2088
|
+
previous.provenance
|
|
2089
|
+
);
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
if (strength <= 0) {
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
strongestByKey.set(key, constraint);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
1742
2098
|
function checkNumericContradictions(ctx, fieldName, constraints) {
|
|
1743
2099
|
const min = findNumeric(constraints, "minimum");
|
|
1744
2100
|
const max = findNumeric(constraints, "maximum");
|
|
@@ -1835,6 +2191,8 @@ function typeLabel(type) {
|
|
|
1835
2191
|
return "array";
|
|
1836
2192
|
case "object":
|
|
1837
2193
|
return "object";
|
|
2194
|
+
case "record":
|
|
2195
|
+
return "record";
|
|
1838
2196
|
case "union":
|
|
1839
2197
|
return "union";
|
|
1840
2198
|
case "reference":
|
|
@@ -1849,74 +2207,140 @@ function typeLabel(type) {
|
|
|
1849
2207
|
}
|
|
1850
2208
|
}
|
|
1851
2209
|
}
|
|
1852
|
-
function
|
|
1853
|
-
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2210
|
+
function dereferenceType(ctx, type) {
|
|
2211
|
+
let current = type;
|
|
2212
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2213
|
+
while (current.kind === "reference") {
|
|
2214
|
+
if (seen.has(current.name)) {
|
|
2215
|
+
return current;
|
|
2216
|
+
}
|
|
2217
|
+
seen.add(current.name);
|
|
2218
|
+
const definition = ctx.typeRegistry[current.name];
|
|
2219
|
+
if (definition === void 0) {
|
|
2220
|
+
return current;
|
|
2221
|
+
}
|
|
2222
|
+
current = definition.type;
|
|
2223
|
+
}
|
|
2224
|
+
return current;
|
|
2225
|
+
}
|
|
2226
|
+
function resolvePathTargetType(ctx, type, segments) {
|
|
2227
|
+
const effectiveType = dereferenceType(ctx, type);
|
|
2228
|
+
if (segments.length === 0) {
|
|
2229
|
+
return { kind: "resolved", type: effectiveType };
|
|
2230
|
+
}
|
|
2231
|
+
if (effectiveType.kind === "array") {
|
|
2232
|
+
return resolvePathTargetType(ctx, effectiveType.items, segments);
|
|
2233
|
+
}
|
|
2234
|
+
if (effectiveType.kind === "object") {
|
|
2235
|
+
const [segment, ...rest] = segments;
|
|
2236
|
+
if (segment === void 0) {
|
|
2237
|
+
throw new Error("Invariant violation: object path traversal requires a segment");
|
|
2238
|
+
}
|
|
2239
|
+
const property = effectiveType.properties.find((prop) => prop.name === segment);
|
|
2240
|
+
if (property === void 0) {
|
|
2241
|
+
return { kind: "missing-property", segment };
|
|
2242
|
+
}
|
|
2243
|
+
return resolvePathTargetType(ctx, property.type, rest);
|
|
2244
|
+
}
|
|
2245
|
+
return { kind: "unresolvable", type: effectiveType };
|
|
2246
|
+
}
|
|
2247
|
+
function formatPathTargetFieldName(fieldName, path2) {
|
|
2248
|
+
return path2.length === 0 ? fieldName : `${fieldName}.${path2.join(".")}`;
|
|
2249
|
+
}
|
|
2250
|
+
function checkConstraintOnType(ctx, fieldName, type, constraint) {
|
|
2251
|
+
const effectiveType = dereferenceType(ctx, type);
|
|
2252
|
+
const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
|
|
2253
|
+
const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
|
|
2254
|
+
const isArray = effectiveType.kind === "array";
|
|
2255
|
+
const isEnum = effectiveType.kind === "enum";
|
|
2256
|
+
const label = typeLabel(effectiveType);
|
|
2257
|
+
const ck = constraint.constraintKind;
|
|
2258
|
+
switch (ck) {
|
|
2259
|
+
case "minimum":
|
|
2260
|
+
case "maximum":
|
|
2261
|
+
case "exclusiveMinimum":
|
|
2262
|
+
case "exclusiveMaximum":
|
|
2263
|
+
case "multipleOf": {
|
|
2264
|
+
if (!isNumber) {
|
|
2265
|
+
addTypeMismatch(
|
|
2266
|
+
ctx,
|
|
2267
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
|
|
2268
|
+
constraint.provenance
|
|
2269
|
+
);
|
|
1874
2270
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2271
|
+
break;
|
|
2272
|
+
}
|
|
2273
|
+
case "minLength":
|
|
2274
|
+
case "maxLength":
|
|
2275
|
+
case "pattern": {
|
|
2276
|
+
if (!isString) {
|
|
2277
|
+
addTypeMismatch(
|
|
2278
|
+
ctx,
|
|
2279
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
|
|
2280
|
+
constraint.provenance
|
|
2281
|
+
);
|
|
1886
2282
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2283
|
+
break;
|
|
2284
|
+
}
|
|
2285
|
+
case "minItems":
|
|
2286
|
+
case "maxItems":
|
|
2287
|
+
case "uniqueItems": {
|
|
2288
|
+
if (!isArray) {
|
|
2289
|
+
addTypeMismatch(
|
|
2290
|
+
ctx,
|
|
2291
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
|
|
2292
|
+
constraint.provenance
|
|
2293
|
+
);
|
|
1898
2294
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2295
|
+
break;
|
|
2296
|
+
}
|
|
2297
|
+
case "allowedMembers": {
|
|
2298
|
+
if (!isEnum) {
|
|
2299
|
+
addTypeMismatch(
|
|
2300
|
+
ctx,
|
|
2301
|
+
`Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
|
|
2302
|
+
constraint.provenance
|
|
2303
|
+
);
|
|
1908
2304
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2307
|
+
case "custom": {
|
|
2308
|
+
checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
|
|
2309
|
+
break;
|
|
2310
|
+
}
|
|
2311
|
+
default: {
|
|
2312
|
+
const _exhaustive = constraint;
|
|
2313
|
+
throw new Error(
|
|
2314
|
+
`Unhandled constraint kind: ${_exhaustive.constraintKind}`
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
function checkTypeApplicability(ctx, fieldName, type, constraints) {
|
|
2320
|
+
for (const constraint of constraints) {
|
|
2321
|
+
if (constraint.path) {
|
|
2322
|
+
const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
|
|
2323
|
+
const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
|
|
2324
|
+
if (resolution.kind === "missing-property") {
|
|
2325
|
+
addTypeMismatch(
|
|
2326
|
+
ctx,
|
|
2327
|
+
`Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
|
|
2328
|
+
constraint.provenance
|
|
2329
|
+
);
|
|
2330
|
+
continue;
|
|
1912
2331
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
`
|
|
2332
|
+
if (resolution.kind === "unresolvable") {
|
|
2333
|
+
addTypeMismatch(
|
|
2334
|
+
ctx,
|
|
2335
|
+
`Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
|
|
2336
|
+
constraint.provenance
|
|
1917
2337
|
);
|
|
2338
|
+
continue;
|
|
1918
2339
|
}
|
|
2340
|
+
checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
|
|
2341
|
+
continue;
|
|
1919
2342
|
}
|
|
2343
|
+
checkConstraintOnType(ctx, fieldName, type, constraint);
|
|
1920
2344
|
}
|
|
1921
2345
|
}
|
|
1922
2346
|
function checkCustomConstraint(ctx, fieldName, type, constraint) {
|
|
@@ -1960,6 +2384,7 @@ function validateConstraints(ctx, name, type, constraints) {
|
|
|
1960
2384
|
checkNumericContradictions(ctx, name, constraints);
|
|
1961
2385
|
checkLengthContradictions(ctx, name, constraints);
|
|
1962
2386
|
checkAllowedMembersContradiction(ctx, name, constraints);
|
|
2387
|
+
checkConstraintBroadening(ctx, name, constraints);
|
|
1963
2388
|
checkTypeApplicability(ctx, name, type, constraints);
|
|
1964
2389
|
}
|
|
1965
2390
|
function validateElement(ctx, element) {
|
|
@@ -1986,8 +2411,8 @@ function validateElement(ctx, element) {
|
|
|
1986
2411
|
function validateIR(ir, options) {
|
|
1987
2412
|
const ctx = {
|
|
1988
2413
|
diagnostics: [],
|
|
1989
|
-
|
|
1990
|
-
|
|
2414
|
+
extensionRegistry: options?.extensionRegistry,
|
|
2415
|
+
typeRegistry: ir.typeRegistry
|
|
1991
2416
|
};
|
|
1992
2417
|
for (const element of ir.elements) {
|
|
1993
2418
|
validateElement(ctx, element);
|
|
@@ -2041,7 +2466,7 @@ function createExtensionRegistry(extensions) {
|
|
|
2041
2466
|
}
|
|
2042
2467
|
|
|
2043
2468
|
// src/generators/method-schema.ts
|
|
2044
|
-
var
|
|
2469
|
+
var import_core4 = require("@formspec/core");
|
|
2045
2470
|
function typeToJsonSchema(type, checker) {
|
|
2046
2471
|
const typeRegistry = {};
|
|
2047
2472
|
const visiting = /* @__PURE__ */ new Set();
|
|
@@ -2049,7 +2474,7 @@ function typeToJsonSchema(type, checker) {
|
|
|
2049
2474
|
const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
|
|
2050
2475
|
const ir = {
|
|
2051
2476
|
kind: "form-ir",
|
|
2052
|
-
irVersion:
|
|
2477
|
+
irVersion: import_core4.IR_VERSION,
|
|
2053
2478
|
elements: [
|
|
2054
2479
|
{
|
|
2055
2480
|
kind: "field",
|