@twin.org/tools-core 0.0.3-next.16 → 0.0.3-next.17
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.
|
@@ -715,7 +715,8 @@ export class JsonSchemaBuilder {
|
|
|
715
715
|
return mappedUnionTypes[0];
|
|
716
716
|
}
|
|
717
717
|
if (JsonSchemaBuilder.isNeverDiscriminatedObjectUnion(context, typeNode.types, mappedUnionTypes) ||
|
|
718
|
-
JsonSchemaBuilder.isLiteralTagDiscriminatedObjectUnion(context, mappedUnionTypes)
|
|
718
|
+
JsonSchemaBuilder.isLiteralTagDiscriminatedObjectUnion(context, mappedUnionTypes) ||
|
|
719
|
+
JsonSchemaBuilder.isDisjointPrimitiveKeywordUnion(typeNode, typeNode.types, mappedUnionTypes)) {
|
|
719
720
|
return {
|
|
720
721
|
oneOf: mappedUnionTypes
|
|
721
722
|
};
|
|
@@ -749,6 +750,52 @@ export class JsonSchemaBuilder {
|
|
|
749
750
|
});
|
|
750
751
|
return undefined;
|
|
751
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* Determine whether a union of primitive keyword branches is pairwise disjoint.
|
|
755
|
+
* @param unionTypeNode The union node being mapped.
|
|
756
|
+
* @param unionTypeNodes The original union branch type nodes.
|
|
757
|
+
* @param unionSchemas The mapped union branch schemas.
|
|
758
|
+
* @returns True if every branch is a primitive keyword schema and no branches overlap.
|
|
759
|
+
*/
|
|
760
|
+
static isDisjointPrimitiveKeywordUnion(unionTypeNode, unionTypeNodes, unionSchemas) {
|
|
761
|
+
if (unionSchemas.length < 2 || unionSchemas.length !== unionTypeNodes.length) {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
let unionContainer = unionTypeNode;
|
|
765
|
+
while (ts.isParenthesizedTypeNode(unionContainer.parent)) {
|
|
766
|
+
unionContainer = unionContainer.parent;
|
|
767
|
+
}
|
|
768
|
+
if (!ts.isTypeAliasDeclaration(unionContainer.parent) ||
|
|
769
|
+
unionContainer.parent.type !== unionContainer) {
|
|
770
|
+
// Keep nested/property unions as anyOf; only top-level alias unions are promoted.
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
const primitiveKinds = new Set([
|
|
774
|
+
ts.SyntaxKind.StringKeyword,
|
|
775
|
+
ts.SyntaxKind.NumberKeyword,
|
|
776
|
+
ts.SyntaxKind.BooleanKeyword,
|
|
777
|
+
ts.SyntaxKind.NullKeyword
|
|
778
|
+
]);
|
|
779
|
+
if (!unionTypeNodes.every(unionBranchType => primitiveKinds.has(unionBranchType.kind))) {
|
|
780
|
+
// Mixed or non-primitive unions can overlap in value space.
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
const schemaTypeDomains = unionSchemas.map(schema => {
|
|
784
|
+
if (schema.type === "integer") {
|
|
785
|
+
return "number";
|
|
786
|
+
}
|
|
787
|
+
return Is.stringValue(schema.type) ? schema.type : undefined;
|
|
788
|
+
});
|
|
789
|
+
if (schemaTypeDomains.some(schemaType => schemaType !== "string" &&
|
|
790
|
+
schemaType !== "number" &&
|
|
791
|
+
schemaType !== "boolean" &&
|
|
792
|
+
schemaType !== "null")) {
|
|
793
|
+
// Only primitive keyword domains are considered disjoint enough for oneOf.
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
// oneOf is safe when every branch maps to a unique primitive domain.
|
|
797
|
+
return new Set(schemaTypeDomains).size === unionSchemas.length;
|
|
798
|
+
}
|
|
752
799
|
/**
|
|
753
800
|
* Determine whether a union of schemas represents mutually exclusive object branches.
|
|
754
801
|
* @param context The generation context.
|
|
@@ -760,38 +807,55 @@ export class JsonSchemaBuilder {
|
|
|
760
807
|
if (unionSchemas.length < 2 || unionSchemas.length !== unionTypeNodes.length) {
|
|
761
808
|
return false;
|
|
762
809
|
}
|
|
810
|
+
const branchInfos = [];
|
|
763
811
|
let hasNeverDiscriminator = false;
|
|
764
812
|
for (const unionTypeNode of unionTypeNodes) {
|
|
765
813
|
const branchMembers = JsonSchemaBuilder.resolveNeverDiscriminatorMembers(context, unionTypeNode);
|
|
766
814
|
if (!branchMembers) {
|
|
767
815
|
return false;
|
|
768
816
|
}
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
817
|
+
const branchInfo = {
|
|
818
|
+
requiredKeys: new Set(),
|
|
819
|
+
forbiddenKeys: new Set()
|
|
820
|
+
};
|
|
821
|
+
for (const member of branchMembers) {
|
|
822
|
+
if (ts.isPropertySignature(member) && member.type && member.name) {
|
|
823
|
+
const propertyName = JsonSchemaBuilder.extractPropertyName(context, member.name);
|
|
824
|
+
if (propertyName) {
|
|
825
|
+
if (member.type.kind === ts.SyntaxKind.NeverKeyword) {
|
|
826
|
+
// never marks a property as impossible in this branch.
|
|
827
|
+
branchInfo.forbiddenKeys.add(propertyName);
|
|
828
|
+
hasNeverDiscriminator = true;
|
|
829
|
+
}
|
|
830
|
+
else if (!member.questionToken) {
|
|
831
|
+
// Required non-never keys are the positive branch signals.
|
|
832
|
+
branchInfo.requiredKeys.add(propertyName);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
772
835
|
}
|
|
773
|
-
return member.type.kind === ts.SyntaxKind.NeverKeyword;
|
|
774
|
-
});
|
|
775
|
-
if (hasNeverProperty) {
|
|
776
|
-
hasNeverDiscriminator = true;
|
|
777
836
|
}
|
|
837
|
+
branchInfos.push(branchInfo);
|
|
778
838
|
}
|
|
779
839
|
if (!hasNeverDiscriminator) {
|
|
780
840
|
return false;
|
|
781
841
|
}
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (resolvedSchema.type !== "object" ||
|
|
786
|
-
!Is.array(resolvedSchema.required) ||
|
|
787
|
-
resolvedSchema.required.length !== 1) {
|
|
842
|
+
for (const branchInfo of branchInfos) {
|
|
843
|
+
if (branchInfo.requiredKeys.size === 0 && branchInfo.forbiddenKeys.size === 0) {
|
|
844
|
+
// Require each branch to contribute at least one discriminating signal.
|
|
788
845
|
return false;
|
|
789
846
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
847
|
+
}
|
|
848
|
+
for (let i = 0; i < branchInfos.length; i++) {
|
|
849
|
+
for (let j = i + 1; j < branchInfos.length; j++) {
|
|
850
|
+
const firstBranch = branchInfos[i];
|
|
851
|
+
const secondBranch = branchInfos[j];
|
|
852
|
+
const firstExcludesSecond = [...firstBranch.requiredKeys].some(requiredKey => secondBranch.forbiddenKeys.has(requiredKey));
|
|
853
|
+
const secondExcludesFirst = [...secondBranch.requiredKeys].some(requiredKey => firstBranch.forbiddenKeys.has(requiredKey));
|
|
854
|
+
if (!firstExcludesSecond && !secondExcludesFirst) {
|
|
855
|
+
// Branch pairs must be mutually exclusive in at least one direction.
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
793
858
|
}
|
|
794
|
-
requiredKeys.add(requiredKey);
|
|
795
859
|
}
|
|
796
860
|
return true;
|
|
797
861
|
}
|
|
@@ -847,6 +911,19 @@ export class JsonSchemaBuilder {
|
|
|
847
911
|
* @returns Type members for the branch when resolvable.
|
|
848
912
|
*/
|
|
849
913
|
static resolveNeverDiscriminatorMembers(context, unionTypeNode) {
|
|
914
|
+
const findTypeMembersInSourceFile = (sourceFile, typeName) => {
|
|
915
|
+
for (const statement of sourceFile.statements) {
|
|
916
|
+
if (ts.isInterfaceDeclaration(statement) && statement.name.text === typeName) {
|
|
917
|
+
return statement.members;
|
|
918
|
+
}
|
|
919
|
+
if (ts.isTypeAliasDeclaration(statement) &&
|
|
920
|
+
statement.name.text === typeName &&
|
|
921
|
+
ts.isTypeLiteralNode(statement.type)) {
|
|
922
|
+
return statement.type.members;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return undefined;
|
|
926
|
+
};
|
|
850
927
|
// { discriminator: "a"; other: string } (inline type literal as union branch)
|
|
851
928
|
if (ts.isTypeLiteralNode(unionTypeNode)) {
|
|
852
929
|
return unionTypeNode.members;
|
|
@@ -863,21 +940,24 @@ export class JsonSchemaBuilder {
|
|
|
863
940
|
const typeName = ts.isIdentifier(unionTypeNode.typeName)
|
|
864
941
|
? unionTypeNode.typeName.text
|
|
865
942
|
: unionTypeNode.typeName.right.text;
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
return statement.members;
|
|
870
|
-
}
|
|
871
|
-
if (
|
|
872
|
-
// type BranchType = { discriminator: "a"; other: string }
|
|
873
|
-
ts.isTypeAliasDeclaration(statement) &&
|
|
874
|
-
statement.name.text === typeName &&
|
|
875
|
-
// { discriminator: "a"; other: string } (must be a type literal alias)
|
|
876
|
-
ts.isTypeLiteralNode(statement.type)) {
|
|
877
|
-
return statement.type.members;
|
|
878
|
-
}
|
|
943
|
+
const localMembers = findTypeMembersInSourceFile(sourceFile, typeName);
|
|
944
|
+
if (localMembers) {
|
|
945
|
+
return localMembers;
|
|
879
946
|
}
|
|
880
|
-
|
|
947
|
+
const importedTypeReference = JsonSchemaBuilder.findImportedTypeReference(context, typeName);
|
|
948
|
+
if (!importedTypeReference?.moduleSpecifier.startsWith(".")) {
|
|
949
|
+
return undefined;
|
|
950
|
+
}
|
|
951
|
+
const resolvedImportPath = JsonSchemaBuilder.resolveImportDeclarationSourceFile(sourceFile.fileName, importedTypeReference.moduleSpecifier);
|
|
952
|
+
if (!resolvedImportPath) {
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
955
|
+
const importedSource = FileUtils.readFile(resolvedImportPath);
|
|
956
|
+
if (!importedSource) {
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
const importedSourceFile = ts.createSourceFile(resolvedImportPath, importedSource, ts.ScriptTarget.Latest, true);
|
|
960
|
+
return findTypeMembersInSourceFile(importedSourceFile, importedTypeReference.candidateTypeName);
|
|
881
961
|
}
|
|
882
962
|
/**
|
|
883
963
|
* Resolve a local $ref schema to its stored schema definition.
|