diff --git a/examples/mapping-example/ExampleCRM/relationships/Address_Customer.relationship.cm b/examples/mapping-example/ExampleCRM/relationships/Address_Customer.relationship.cm index f7a97c1..a0f8227 100644 --- a/examples/mapping-example/ExampleCRM/relationships/Address_Customer.relationship.cm +++ b/examples/mapping-example/ExampleCRM/relationships/Address_Customer.relationship.cm @@ -2,4 +2,7 @@ relationship: id: Address_Customer parent: Customer child: ExampleCRM.Address - type: "1:1" \ No newline at end of file + type: "1:1" + attributes: + - parent: Customer.Id + child: ExampleCRM.Address.CustomerID \ No newline at end of file diff --git a/examples/mapping-example/ExampleCRM/relationships/Order_Customer.relationship.cm b/examples/mapping-example/ExampleCRM/relationships/Order_Customer.relationship.cm index 76ef9d4..65e20a8 100644 --- a/examples/mapping-example/ExampleCRM/relationships/Order_Customer.relationship.cm +++ b/examples/mapping-example/ExampleCRM/relationships/Order_Customer.relationship.cm @@ -2,4 +2,7 @@ relationship: id: Order_Customer parent: Customer child: Order - type: "1:1" \ No newline at end of file + type: "1:1" + attributes: + - parent: Customer.Id + child: Order.CustomerId \ No newline at end of file diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-completion-provider.ts b/extensions/crossmodel-lang/src/language-server/cross-model-completion-provider.ts index 602edbd..e18b567 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-completion-provider.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-completion-provider.ts @@ -18,6 +18,7 @@ import { CompletionItemKind, InsertTextFormat } from 'vscode-languageserver-prot import type { Range } from 'vscode-languageserver-types'; import { CrossModelServices } from './cross-model-module.js'; import { isExternalDescriptionForLocalPackage } from './cross-model-scope.js'; +import { RelationshipAttribute, isRelationshipAttribute } from './generated/ast.js'; import { fixDocument } from './util/ast-util.js'; /** @@ -26,6 +27,10 @@ import { fixDocument } from './util/ast-util.js'; export class CrossModelCompletionProvider extends DefaultCompletionProvider { protected packageId?: string; + readonly completionOptions = { + triggerCharacters: ['\n', ' '] + }; + constructor( services: CrossModelServices, protected packageManager = services.shared.workspace.PackageManager @@ -52,6 +57,14 @@ export class CrossModelCompletionProvider extends DefaultCompletionProvider { return context; } + protected override filterKeyword(context: CompletionContext, keyword: GrammarAST.Keyword): boolean { + return super.filterKeyword(context, keyword) && this.isUndefinedProperty(context.node, keyword.value); + } + + protected isUndefinedProperty(obj: any, property: string): boolean { + return obj?.[property] === undefined || (Array.isArray(obj[property]) && obj[property].length === 0); + } + protected completionForAssignment( context: CompletionContext, assignment: GrammarAST.Assignment, @@ -182,8 +195,25 @@ export class CrossModelCompletionProvider extends DefaultCompletionProvider { } protected override filterCrossReference(context: CompletionContext, description: AstNodeDescription): boolean { - // we want to keep fully qualified names in the scope so we can do proper linking - // but want to hide it from the user for local options, i.e., if we are in the same project we can skip the project name - return !isExternalDescriptionForLocalPackage(description, this.packageId) && super.filterCrossReference(context, description); + if (isRelationshipAttribute(context.node)) { + return this.filterRelationshipAttribute(context.node, context, description); + } + if (isExternalDescriptionForLocalPackage(description, this.packageId)) { + // we want to keep fully qualified names in the scope so we can do proper linking + // but want to hide it from the user for local options, i.e., if we are in the same project we can skip the project name + return false; + } + return super.filterCrossReference(context, description); + } + + protected filterRelationshipAttribute(node: RelationshipAttribute, context: CompletionContext, desc: AstNodeDescription): boolean { + // only show relevant attributes depending on the parent or child context + if (context.features.find(feature => feature.property === 'child')) { + return desc.name.startsWith(node.$container.child?.$refText + '.'); + } + if (context.features.find(feature => feature.property === 'parent')) { + return desc.name.startsWith(node.$container.parent?.$refText + '.'); + } + return true; } } diff --git a/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts b/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts index 87debcc..cc2b14f 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts @@ -5,7 +5,9 @@ import { AstNode, ValidationAcceptor, ValidationChecks } from 'langium'; import type { CrossModelServices } from './cross-model-module.js'; import { ID_PROPERTY, IdentifiableAstNode } from './cross-model-naming.js'; import { + Attribute, CrossModelAstType, + Relationship, RelationshipEdge, SourceObject, isEntity, @@ -25,7 +27,8 @@ export function registerValidationChecks(services: CrossModelServices): void { const checks: ValidationChecks = { AstNode: validator.checkNode, RelationshipEdge: validator.checkRelationshipEdge, - SourceObject: validator.checkSourceObject + SourceObject: validator.checkSourceObject, + Relationship: validator.checkRelationship }; registry.register(checks, validator); } @@ -83,6 +86,33 @@ export class CrossModelValidator { } } + checkRelationship(relationship: Relationship, accept: ValidationAcceptor): void { + // we check that each attribute actually belongs to their respective entity (parent, child) + // and that each attribute is only used once + const usedParentAttributes: Attribute[] = []; + const usedChildAttributes: Attribute[] = []; + for (const attribute of relationship.attributes) { + if (attribute.parent.ref) { + if (attribute.parent?.ref?.$container !== relationship.parent?.ref) { + accept('error', 'Not a valid parent attribute.', { node: attribute, property: 'parent' }); + } else if (usedParentAttributes.includes(attribute.parent.ref)) { + accept('error', 'Each parent attribute can only be referenced once.', { node: attribute, property: 'parent' }); + } else { + usedParentAttributes.push(attribute.parent.ref); + } + } + if (attribute.child.ref) { + if (attribute.child?.ref?.$container !== relationship.child?.ref) { + accept('error', 'Not a valid child attribute.', { node: attribute, property: 'child' }); + } else if (usedChildAttributes.includes(attribute.child.ref)) { + accept('error', 'Each child attribute can only be referenced once.', { node: attribute, property: 'child' }); + } else { + usedChildAttributes.push(attribute.child.ref); + } + } + } + } + checkRelationshipEdge(edge: RelationshipEdge, accept: ValidationAcceptor): void { if (edge.sourceNode?.ref?.entity?.ref?.$type !== edge.relationship?.ref?.parent?.ref?.$type) { accept('error', 'Source must match type of parent.', { node: edge, property: 'sourceNode' }); diff --git a/extensions/crossmodel-lang/src/language-server/entity.langium b/extensions/crossmodel-lang/src/language-server/entity.langium index d78032d..ac6a5dc 100644 --- a/extensions/crossmodel-lang/src/language-server/entity.langium +++ b/extensions/crossmodel-lang/src/language-server/entity.langium @@ -3,37 +3,29 @@ import 'terminals' // Entity definition Entity: 'entity' ':' - ( INDENT - ( - 'id' ':' id=ID | - 'name' ':' name=STRING | - 'description' ':' description=STRING | - 'attributes' ':' EntityAttributes - )* + 'id' ':' id=ID + ('name' ':' name=STRING)? + ('description' ':' description=STRING)? + ('attributes' ':' + INDENT + ('-' attributes+=EntityAttribute)+ + DEDENT + )? DEDENT - )* ; interface Attribute { id: string; - name?: string; + name: string; + datatype: string; description?: string; - datatype: string; } interface EntityAttribute extends Attribute {} -EntityAttributes infers Entity: - INDENT - (attributes+=EntityAttribute)* - DEDENT; - EntityAttribute returns EntityAttribute: - '-' ( - 'id' ':' id=ID | - 'name' ':' name=STRING | - 'datatype' ':' datatype=STRING | - 'description' ':' description=STRING - )* -; + 'id' ':' id=ID + 'name' ':' name=STRING + 'datatype' ':' datatype=STRING + ('description' ':' description=STRING)?; diff --git a/extensions/crossmodel-lang/src/language-server/generated/ast.ts b/extensions/crossmodel-lang/src/language-server/generated/ast.ts index 0b28a2b..fe2b912 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/ast.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/ast.ts @@ -51,7 +51,7 @@ export interface Attribute extends AstNode { datatype: string description?: string id: string - name?: string + name: string } export const Attribute = 'Attribute'; @@ -104,7 +104,7 @@ export interface Entity extends AstNode { readonly $type: 'Entity'; attributes: Array description?: string - id?: string + id: string name?: string } @@ -118,13 +118,13 @@ export interface EntityNode extends AstNode { readonly $container: SystemDiagram; readonly $type: 'EntityNode'; description?: string - entity?: Reference - height?: number - id?: string + entity: Reference + height: number + id: string name?: string - width?: number - x?: number - y?: number + width: number + x: number + y: number } export const EntityNode = 'EntityNode'; @@ -200,12 +200,13 @@ export function isReferenceSource(item: unknown): item is ReferenceSource { export interface Relationship extends AstNode { readonly $container: CrossModelRoot; readonly $type: 'Relationship'; - child?: Reference + attributes: Array + child: Reference description?: string - id?: string + id: string name?: string - parent?: Reference - type?: string + parent: Reference + type: string } export const Relationship = 'Relationship'; @@ -214,6 +215,19 @@ export function isRelationship(item: unknown): item is Relationship { return reflection.isInstance(item, Relationship); } +export interface RelationshipAttribute extends AstNode { + readonly $container: Relationship; + readonly $type: 'RelationshipAttribute'; + child: Reference + parent: Reference +} + +export const RelationshipAttribute = 'RelationshipAttribute'; + +export function isRelationshipAttribute(item: unknown): item is RelationshipAttribute { + return reflection.isInstance(item, RelationshipAttribute); +} + export interface RelationshipCondition extends AstNode { readonly $container: SourceObjectRelations; readonly $type: 'RelationshipCondition'; @@ -229,10 +243,10 @@ export function isRelationshipCondition(item: unknown): item is RelationshipCond export interface RelationshipEdge extends AstNode { readonly $container: SystemDiagram; readonly $type: 'RelationshipEdge'; - id?: string - relationship?: Reference - sourceNode?: Reference - targetNode?: Reference + id: string + relationship: Reference + sourceNode: Reference + targetNode: Reference } export const RelationshipEdge = 'RelationshipEdge'; @@ -366,6 +380,7 @@ export type CrossModelAstType = { NumberLiteral: NumberLiteral ReferenceSource: ReferenceSource Relationship: Relationship + RelationshipAttribute: RelationshipAttribute RelationshipCondition: RelationshipCondition RelationshipEdge: RelationshipEdge SourceObject: SourceObject @@ -381,7 +396,7 @@ export type CrossModelAstType = { export class CrossModelAstReflection extends AbstractAstReflection { getAllTypes(): string[] { - return ['Attribute', 'AttributeMapping', 'AttributeMappingSource', 'AttributeMappingTarget', 'CrossModelRoot', 'Entity', 'EntityAttribute', 'EntityNode', 'EntityNodeAttribute', 'JoinCondition', 'JoinExpression', 'Mapping', 'NumberLiteral', 'ReferenceSource', 'Relationship', 'RelationshipCondition', 'RelationshipEdge', 'SourceObject', 'SourceObjectAttribute', 'SourceObjectCondition', 'SourceObjectRelations', 'StringLiteral', 'SystemDiagram', 'TargetObject', 'TargetObjectAttribute']; + return ['Attribute', 'AttributeMapping', 'AttributeMappingSource', 'AttributeMappingTarget', 'CrossModelRoot', 'Entity', 'EntityAttribute', 'EntityNode', 'EntityNodeAttribute', 'JoinCondition', 'JoinExpression', 'Mapping', 'NumberLiteral', 'ReferenceSource', 'Relationship', 'RelationshipAttribute', 'RelationshipCondition', 'RelationshipEdge', 'SourceObject', 'SourceObjectAttribute', 'SourceObjectCondition', 'SourceObjectRelations', 'StringLiteral', 'SystemDiagram', 'TargetObject', 'TargetObjectAttribute']; } protected override computeIsSubtype(subtype: string, supertype: string): boolean { @@ -427,6 +442,10 @@ export class CrossModelAstReflection extends AbstractAstReflection { case 'ReferenceSource:value': { return SourceObjectAttribute; } + case 'RelationshipAttribute:child': + case 'RelationshipAttribute:parent': { + return Attribute; + } case 'RelationshipCondition:relationship': case 'RelationshipEdge:relationship': { return Relationship; @@ -462,6 +481,14 @@ export class CrossModelAstReflection extends AbstractAstReflection { ] }; } + case 'Relationship': { + return { + name: 'Relationship', + mandatory: [ + { name: 'attributes', type: 'array' } + ] + }; + } case 'SourceObject': { return { name: 'SourceObject', diff --git a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts index a5430a2..76d9eda 100644 --- a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts +++ b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts @@ -39,7 +39,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@13" + "$ref": "#/rules@12" }, "arguments": [] } @@ -51,7 +51,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@15" + "$ref": "#/rules@14" }, "arguments": [] } @@ -63,7 +63,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@17" }, "arguments": [] } @@ -91,126 +91,141 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "$type": "Keyword", "value": ":" }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + }, + { + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "id", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, { "$type": "Group", "elements": [ + { + "$type": "Keyword", + "value": "attributes" + }, + { + "$type": "Keyword", + "value": ":" + }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, { - "$type": "Alternatives", + "$type": "Group", "elements": [ { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "id", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@12" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ] + "$type": "Keyword", + "value": "-" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "attributes" - }, - { - "$type": "Keyword", - "value": ":" + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@2" }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@2" - }, - "arguments": [] - } - ] + "arguments": [] + } } ], - "cardinality": "*" + "cardinality": "+" }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } ], - "cardinality": "*" + "cardinality": "?" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] } ] }, @@ -223,40 +238,98 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "ParserRule", - "name": "EntityAttributes", - "inferredType": { - "$type": "InferredType", - "name": "Entity" + "name": "EntityAttribute", + "returnType": { + "$ref": "#/interfaces@1" }, "definition": { "$type": "Group", "elements": [ { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@10" - }, - "arguments": [] + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" }, { "$type": "Assignment", - "feature": "attributes", - "operator": "+=", + "feature": "id", + "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@3" + "$ref": "#/rules@11" }, "arguments": [] - }, - "cardinality": "*" + } }, { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "datatype" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "datatype", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } + } + ], + "cardinality": "?" } ] }, @@ -269,143 +342,15 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "ParserRule", - "name": "EntityAttribute", - "returnType": { - "$ref": "#/interfaces@1" - }, - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "-" - }, - { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "id", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@12" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "datatype" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "datatype", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "IDReference", - "dataType": "string", + "name": "IDReference", + "dataType": "string", "definition": { "$type": "Group", "elements": [ { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@11" }, "arguments": [] }, @@ -419,7 +364,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@11" }, "arguments": [] } @@ -546,76 +491,31 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "value": ":" }, { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@10" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@14" - }, - "arguments": [], - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "RelationshipFields", - "inferredType": { - "$type": "InferredType", - "name": "Relationship" - }, - "definition": { - "$type": "Alternatives", - "elements": [ + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" - }, - { - "$type": "Keyword", - "value": ":" + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "id", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" }, - { - "$type": "Assignment", - "feature": "id", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@12" - }, - "arguments": [] - } - } - ] + "arguments": [] + } }, { "$type": "Group", @@ -635,12 +535,13 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@5" + "$ref": "#/rules@4" }, "arguments": [] } } - ] + ], + "cardinality": "?" }, { "$type": "Group", @@ -660,101 +561,144 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@5" + "$ref": "#/rules@4" }, "arguments": [] } } - ] + ], + "cardinality": "?" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "parent" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "parent", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@4" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] + "$type": "Keyword", + "value": "parent" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "child" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "child", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@4" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "parent", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + }, + { + "$type": "Keyword", + "value": "child" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "child", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] + }, + "deprecatedSyntax": false + } + }, + { + "$type": "Keyword", + "value": "type" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "type", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "type" + "value": "attributes" }, { "$type": "Keyword", "value": ":" }, { - "$type": "Assignment", - "feature": "type", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" }, - "arguments": [] - } + { + "$type": "Assignment", + "feature": "attributes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@13" + }, + "arguments": [] + } + } + ], + "cardinality": "+" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] } - ] + ], + "cardinality": "?" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] } ] }, @@ -767,54 +711,63 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "ParserRule", - "name": "SystemDiagram", + "name": "RelationshipAttribute", "definition": { "$type": "Group", "elements": [ { - "$type": "Alternatives", - "elements": [ - { - "$type": "Keyword", - "value": "systemDiagram" - }, - { - "$type": "Keyword", - "value": "diagram" - } - ] + "$type": "Keyword", + "value": "parent" }, { "$type": "Keyword", "value": ":" }, { - "$type": "Group", - "elements": [ - { + "$type": "Assignment", + "feature": "parent", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/interfaces@0" + }, + "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@3" }, "arguments": [] }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@16" - }, - "arguments": [], - "cardinality": "*" + "deprecatedSyntax": false + } + }, + { + "$type": "Keyword", + "value": "child" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "child", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/interfaces@0" }, - { + "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@3" }, "arguments": [] - } - ], - "cardinality": "*" + }, + "deprecatedSyntax": false + } } ] }, @@ -827,57 +780,37 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "ParserRule", - "name": "SystemDiagramFields", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagram" - }, + "name": "SystemDiagram", "definition": { - "$type": "Alternatives", + "$type": "Group", "elements": [ { - "$type": "Group", + "$type": "Alternatives", "elements": [ { "$type": "Keyword", - "value": "nodes" + "value": "systemDiagram" }, { "$type": "Keyword", - "value": ":" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@17" - }, - "arguments": [] + "value": "diagram" } ] }, + { + "$type": "Keyword", + "value": ":" + }, { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "edges" - }, - { - "$type": "Keyword", - "value": ":" - }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@9" }, "arguments": [] - } - ] - }, - { - "$type": "Group", - "elements": [ + }, { "$type": "Keyword", "value": "id" @@ -893,62 +826,172 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@11" }, "arguments": [] } - } - ] - }, - { - "$type": "Group", - "elements": [ + }, { - "$type": "Keyword", - "value": "description" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } + } + ], + "cardinality": "?" }, { - "$type": "Keyword", - "value": ":" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } + } + ], + "cardinality": "?" }, { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "nodes" }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "Assignment", + "feature": "nodes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@15" + }, + "arguments": [] + } + } + ], + "cardinality": "+" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + ], + "cardinality": "?" }, { - "$type": "Keyword", - "value": ":" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "edges" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" + }, + { + "$type": "Assignment", + "feature": "edges", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@16" + }, + "arguments": [] + } + } + ], + "cardinality": "+" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + ], + "cardinality": "?" }, { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] } - ] + ], + "cardinality": "*" } ] }, @@ -961,93 +1004,36 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "ParserRule", - "name": "SystemDiagramNodes", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagram" - }, + "name": "EntityNode", "definition": { "$type": "Group", "elements": [ { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@10" - }, - "arguments": [] + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" }, { "$type": "Assignment", - "feature": "nodes", - "operator": "+=", + "feature": "id", + "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@11" }, "arguments": [] - }, - "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "EntityNode", - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "-" + } }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@19" - }, - "arguments": [], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "EntityNodeFields", - "inferredType": { - "$type": "InferredType", - "name": "EntityNode" - }, - "definition": { - "$type": "Alternatives", - "elements": [ { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "id" + "value": "name" }, { "$type": "Keyword", @@ -1055,56 +1041,25 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "Assignment", - "feature": "id", + "feature": "name", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@4" }, "arguments": [] } } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "entity" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "entity", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@1" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@4" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] + ], + "cardinality": "?" }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "x" + "value": "description" }, { "$type": "Keyword", @@ -1112,188 +1067,125 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "Assignment", - "feature": "x", + "feature": "description", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@4" }, "arguments": [] } } - ] + ], + "cardinality": "?" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "y" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "y", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@6" - }, - "arguments": [] - } - } - ] + "$type": "Keyword", + "value": "entity" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "width" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "width", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@6" - }, - "arguments": [] - } - } - ] + "$type": "Keyword", + "value": ":" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "height" + "$type": "Assignment", + "feature": "entity", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@1" }, - { - "$type": "Keyword", - "value": ":" + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] }, - { - "$type": "Assignment", - "feature": "height", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@6" - }, - "arguments": [] - } - } - ] + "deprecatedSyntax": false + } }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ] + "$type": "Keyword", + "value": "x" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "x", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" }, - { - "$type": "Keyword", - "value": ":" + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "y" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "y", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagramEdge", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagram" - }, - "definition": { - "$type": "Group", - "elements": [ + "arguments": [] + } + }, { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@10" - }, - "arguments": [] + "$type": "Keyword", + "value": "width" + }, + { + "$type": "Keyword", + "value": ":" }, { "$type": "Assignment", - "feature": "edges", - "operator": "+=", + "feature": "width", + "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@21" + "$ref": "#/rules@5" }, "arguments": [] - }, - "cardinality": "*" + } }, { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] + "$type": "Keyword", + "value": "height" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "height", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" + }, + "arguments": [] + } } ] }, @@ -1312,155 +1204,104 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "elements": [ { "$type": "Keyword", - "value": "-" + "value": "id" }, { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@22" - }, - "arguments": [], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "RelationshipEdgeFields", - "inferredType": { - "$type": "InferredType", - "name": "RelationshipEdge" - }, - "definition": { - "$type": "Alternatives", - "elements": [ + "$type": "Keyword", + "value": ":" + }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "relationship" - }, - { - "$type": "Keyword", - "value": ":" + "$type": "Assignment", + "feature": "id", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" }, - { - "$type": "Assignment", - "feature": "relationship", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@13" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@4" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] + "arguments": [] + } }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "sourceNode" + "$type": "Keyword", + "value": "relationship" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "relationship", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@12" }, - { - "$type": "Keyword", - "value": ":" + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] }, - { - "$type": "Assignment", - "feature": "sourceNode", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@18" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@4" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] + "deprecatedSyntax": false + } }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "targetNode" + "$type": "Keyword", + "value": "sourceNode" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "sourceNode", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@15" }, - { - "$type": "Keyword", - "value": ":" + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] }, - { - "$type": "Assignment", - "feature": "targetNode", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@18" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@4" - }, - "arguments": [] - }, - "deprecatedSyntax": false - } - } - ] + "deprecatedSyntax": false + } }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "id" + "$type": "Keyword", + "value": "targetNode" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "targetNode", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@15" }, - { - "$type": "Keyword", - "value": ":" + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] }, - { - "$type": "Assignment", - "feature": "id", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@12" - }, - "arguments": [] - } - } - ] + "deprecatedSyntax": false + } } ] }, @@ -1488,7 +1329,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, @@ -1507,7 +1348,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@11" }, "arguments": [] } @@ -1526,27 +1367,36 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, { - "$type": "Assignment", - "feature": "sources", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@24" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" }, - "arguments": [] - }, - "cardinality": "*" + { + "$type": "Assignment", + "feature": "sources", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + } + } + ], + "cardinality": "+" }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -1568,7 +1418,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@25" }, "arguments": [] } @@ -1576,7 +1426,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -1595,10 +1445,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "id" @@ -1614,7 +1460,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@11" }, "arguments": [] } @@ -1639,7 +1485,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -1661,7 +1507,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@19" }, "arguments": [] } @@ -1680,27 +1526,36 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, { - "$type": "Assignment", - "feature": "relations", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@26" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" }, - "arguments": [] - }, + { + "$type": "Assignment", + "feature": "relations", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@20" + }, + "arguments": [] + } + } + ], "cardinality": "*" }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -1758,10 +1613,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "source" @@ -1777,12 +1628,12 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@24" + "$ref": "#/rules@18" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -1800,27 +1651,36 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, { - "$type": "Assignment", - "feature": "conditions", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@27" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" }, - "arguments": [] - }, + { + "$type": "Assignment", + "feature": "conditions", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@21" + }, + "arguments": [] + } + } + ], "cardinality": "*" }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -1842,14 +1702,14 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@28" + "$ref": "#/rules@22" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@29" + "$ref": "#/rules@23" }, "arguments": [] } @@ -1868,10 +1728,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "relationship" @@ -1887,12 +1743,12 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@13" + "$ref": "#/rules@12" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -1914,10 +1770,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "join" @@ -1933,7 +1785,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@24" }, "arguments": [] } @@ -1965,7 +1817,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -1993,7 +1845,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -2018,7 +1870,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, @@ -2042,7 +1894,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -2063,27 +1915,36 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, { - "$type": "Assignment", - "feature": "mappings", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@32" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "-" }, - "arguments": [] - }, - "cardinality": "*" + { + "$type": "Assignment", + "feature": "mappings", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@26" + }, + "arguments": [] + } + } + ], + "cardinality": "+" }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -2093,7 +1954,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -2112,10 +1973,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "attribute" @@ -2131,7 +1988,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@33" + "$ref": "#/rules@27" }, "arguments": [] } @@ -2151,7 +2008,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@34" + "$ref": "#/rules@28" }, "arguments": [] } @@ -2180,7 +2037,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -2222,7 +2079,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -2248,7 +2105,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@5" }, "arguments": [] } @@ -2272,7 +2129,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@5" + "$ref": "#/rules@4" }, "arguments": [] } @@ -2307,29 +2164,29 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "TypeAttribute", "name": "name", - "isOptional": true, "type": { "$type": "SimpleType", "primitiveType": "string" - } + }, + "isOptional": false }, { "$type": "TypeAttribute", - "name": "description", - "isOptional": true, + "name": "datatype", "type": { "$type": "SimpleType", "primitiveType": "string" - } + }, + "isOptional": false }, { "$type": "TypeAttribute", - "name": "datatype", + "name": "description", + "isOptional": true, "type": { "$type": "SimpleType", "primitiveType": "string" - }, - "isOptional": false + } } ], "name": "Attribute", diff --git a/extensions/crossmodel-lang/src/language-server/mapping.langium b/extensions/crossmodel-lang/src/language-server/mapping.langium index eaafa88..ef02d69 100644 --- a/extensions/crossmodel-lang/src/language-server/mapping.langium +++ b/extensions/crossmodel-lang/src/language-server/mapping.langium @@ -14,40 +14,42 @@ Mapping: 'id' ':' id=ID ('sources' ':' INDENT - sources+=SourceObject* - DEDENT)? + ('-' sources+=SourceObject)+ + DEDENT + )? 'target' ':' target=TargetObject DEDENT ; SourceObject: - '-' 'id' ':' id=ID - 'entity' ':' entity=[Entity:IDReference] // implies attributes through entity - 'join' ':' join=JoinType - ('relations' ':' - INDENT - relations+=SourceObjectRelations* - DEDENT)? + 'id' ':' id=ID + 'entity' ':' entity=[Entity:IDReference] // implies attributes through entity + 'join' ':' join=JoinType + ('relations' ':' + INDENT + ('-' relations+=SourceObjectRelations)* + DEDENT + )? ; JoinType returns string: 'from' | 'inner-join' | 'cross-join' | 'left-join' | 'apply'; SourceObjectRelations: - '-' 'source' ':' source=[SourceObject:IDReference] - 'conditions' ':' - INDENT - conditions+=SourceObjectCondition* - DEDENT + 'source' ':' source=[SourceObject:IDReference] + 'conditions' ':' + INDENT + ('-' conditions+=SourceObjectCondition)* + DEDENT ; SourceObjectCondition: RelationshipCondition | JoinCondition; RelationshipCondition: - '-' 'relationship' ':' relationship=[Relationship:IDReference] + 'relationship' ':' relationship=[Relationship:IDReference] ; JoinCondition: - '-' 'join' ':' expression=JoinExpression + 'join' ':' expression=JoinExpression ; JoinExpression: @@ -59,14 +61,15 @@ TargetObject: 'entity' ':' entity=[Entity:IDReference] // implies attributes through entity ('mappings' ':' INDENT - mappings+=AttributeMapping* - DEDENT)? + ('-' mappings+=AttributeMapping)+ + DEDENT + )? DEDENT ; AttributeMapping: - '-' 'attribute' ':' attribute=AttributeMappingTarget - 'source' ':' source=AttributeMappingSource + 'attribute' ':' attribute=AttributeMappingTarget + 'source' ':' source=AttributeMappingSource ; AttributeMappingTarget: diff --git a/extensions/crossmodel-lang/src/language-server/relationship.langium b/extensions/crossmodel-lang/src/language-server/relationship.langium index fcd6815..be90bf5 100644 --- a/extensions/crossmodel-lang/src/language-server/relationship.langium +++ b/extensions/crossmodel-lang/src/language-server/relationship.langium @@ -4,20 +4,22 @@ import 'entity' // Relationship defintion Relationship: 'relationship' ':' - ( INDENT - RelationshipFields* + 'id' ':' id=ID + ('name' ':' name=STRING)? + ('description' ':' description=STRING)? + 'parent' ':' parent=[Entity:IDReference] + 'child' ':' child=[Entity:IDReference] + 'type' ':' type=STRING + ('attributes' ':' + INDENT + ('-' attributes+=RelationshipAttribute)+ + DEDENT + )? DEDENT - )* ; - -RelationshipFields infers Relationship: - ( - 'id' ':' id=ID | - 'name' ':' name=STRING | - 'description' ':' description=STRING | - 'parent' ':' parent=[Entity:IDReference] | - 'child' ':' child=[Entity:IDReference] | - 'type' ':' type=STRING - ) -; \ No newline at end of file + +RelationshipAttribute: + 'parent' ':' parent=[Attribute:IDReference] + 'child' ':' child=[Attribute:IDReference] +; diff --git a/extensions/crossmodel-lang/src/language-server/system-diagram.langium b/extensions/crossmodel-lang/src/language-server/system-diagram.langium index e5dafb8..1aea257 100644 --- a/extensions/crossmodel-lang/src/language-server/system-diagram.langium +++ b/extensions/crossmodel-lang/src/language-server/system-diagram.langium @@ -7,60 +7,41 @@ SystemDiagram: ('systemDiagram' | 'diagram') ':' ( INDENT - SystemDiagramFields* + 'id' ':' id=ID + ('name' ':' name=STRING)? + ('description' ':' description=STRING)? + ('nodes' ':' + INDENT + ('-' nodes+=EntityNode)+ + DEDENT + )? + ('edges' ':' + INDENT + ('-' edges+=RelationshipEdge)+ + DEDENT + )? DEDENT )* ; -SystemDiagramFields infers SystemDiagram: - ( - 'nodes' ':' SystemDiagramNodes | - 'edges' ':' SystemDiagramEdge | - 'id' ':' id=ID | - 'description' ':' description=STRING | - 'name' ':' name=STRING - ) -; - -SystemDiagramNodes infers SystemDiagram: - INDENT - (nodes+=EntityNode)* - DEDENT -; EntityNode: - '-' EntityNodeFields* -; - -EntityNodeFields infers EntityNode: - 'id' ':' id=ID | - 'entity' ':' entity=[Entity:IDReference] | - 'x' ':' x=NUMBER | - 'y' ':' y=NUMBER | - 'width' ':' width=NUMBER | - 'height' ':' height=NUMBER | - 'name' ':' name=STRING | - 'description' ':' description=STRING + 'id' ':' id=ID + ('name' ':' name=STRING)? + ('description' ':' description=STRING)? + 'entity' ':' entity=[Entity:IDReference] + 'x' ':' x=NUMBER + 'y' ':' y=NUMBER + 'width' ':' width=NUMBER + 'height' ':' height=NUMBER ; interface EntityNodeAttribute extends EntityAttribute { } -SystemDiagramEdge infers SystemDiagram: - INDENT - (edges+=RelationshipEdge)* - DEDENT -; - RelationshipEdge: - '-' RelationshipEdgeFields* -; - -RelationshipEdgeFields infers RelationshipEdge: - ( - 'relationship' ':' relationship=[Relationship:IDReference] | - 'sourceNode' ':' sourceNode=[EntityNode:IDReference] | - 'targetNode' ':' targetNode=[EntityNode:IDReference] | - 'id' ':' id=ID - ) + 'id' ':' id=ID + 'relationship' ':' relationship=[Relationship:IDReference] + 'sourceNode' ':' sourceNode=[EntityNode:IDReference] + 'targetNode' ':' targetNode=[EntityNode:IDReference] ; \ No newline at end of file diff --git a/extensions/crossmodel-lang/src/language-server/util/ast-util.ts b/extensions/crossmodel-lang/src/language-server/util/ast-util.ts index 22f1774..0a3e812 100644 --- a/extensions/crossmodel-lang/src/language-server/util/ast-util.ts +++ b/extensions/crossmodel-lang/src/language-server/util/ast-util.ts @@ -2,7 +2,16 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ import { unquote } from '@crossbreeze/protocol'; -import { AstNode, AstNodeDescription, LangiumDocument, Reference, ReferenceInfo, findRootNode, isAstNodeDescription } from 'langium'; +import { + AstNode, + AstNodeDescription, + LangiumDocument, + Reference, + ReferenceInfo, + findRootNode, + isAstNode, + isAstNodeDescription +} from 'langium'; import { ID_PROPERTY, IdProvider } from '../cross-model-naming.js'; import { getLocalName } from '../cross-model-scope.js'; import { @@ -22,9 +31,16 @@ import { StringLiteral, SystemDiagram, TargetObject, - TargetObjectAttribute + TargetObjectAttribute, + isCrossModelRoot, + isEntity, + isMapping, + isRelationship, + isSystemDiagram } from '../generated/ast.js'; +export type SemanticRoot = Entity | Mapping | Relationship | SystemDiagram; + export const IMPLICIT_ATTRIBUTES_PROPERTY = '$attributes'; export const IMPLICIT_OWNER_PROPERTY = '$owner'; export const IMPLICIT_ID_PROPERTY = '$id'; @@ -195,11 +211,34 @@ export function fixDocument(node: T | undefined, do return node; } -export function findSemanticRoot(document: LangiumDocument): Entity | Mapping | Relationship | SystemDiagram | undefined { - const root = document.parseResult.value; - return root.entity ?? root.mapping ?? root.relationship ?? root.systemDiagram; +export type WithDocument = T & { $document: LangiumDocument }; +export type DocumentContent = LangiumDocument | AstNode; +export type TypeGuard = (item: unknown) => item is T; + +export function findSemanticRoot(input: DocumentContent): SemanticRoot | undefined; +export function findSemanticRoot(input: DocumentContent, guard: TypeGuard): T | undefined; +export function findSemanticRoot(input: DocumentContent, guard?: TypeGuard): SemanticRoot | T | undefined { + const root = isAstNode(input) ? input.$document?.parseResult.value ?? findRootNode(input) : input.parseResult.value; + const semanticRoot = isCrossModelRoot(root) ? root.entity ?? root.mapping ?? root.relationship ?? root.systemDiagram : undefined; + return !semanticRoot ? undefined : !guard ? semanticRoot : guard(semanticRoot) ? semanticRoot : undefined; +} + +export function findEntity(input: DocumentContent): Entity | undefined { + return findSemanticRoot(input, isEntity); +} + +export function findRelationship(input: DocumentContent): Relationship | undefined { + return findSemanticRoot(input, isRelationship); +} + +export function findSystemDiagram(input: DocumentContent): SystemDiagram | undefined { + return findSemanticRoot(input, isSystemDiagram); +} + +export function findMapping(input: DocumentContent): Mapping | undefined { + return findSemanticRoot(input, isMapping); } -export function hasSemanticRoot(document: LangiumDocument, guard: (item: unknown) => item is T): boolean { +export function hasSemanticRoot(document: LangiumDocument, guard: (item: unknown) => item is T): boolean { return guard(findSemanticRoot(document)); } diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts index ca4981e..84d3052 100644 --- a/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-diagram.test.ts @@ -2,54 +2,31 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ import { describe, expect, test } from '@jest/globals'; -import { EmptyFileSystem, isReference } from 'langium'; +import { isReference } from 'langium'; import { diagram1, diagram2, diagram3, diagram4, diagram5, diagram6 } from './test-utils/test-documents/diagram/index.js'; -import { parseDocument } from './test-utils/utils.js'; +import { createCrossModelTestServices, parseSystemDiagram } from './test-utils/utils.js'; -import { createCrossModelServices } from '../../src/language-server/cross-model-module.js'; -import { CrossModelRoot } from '../../src/language-server/generated/ast.js'; - -const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; +const services = createCrossModelTestServices(); describe('CrossModel language Diagram', () => { describe('Diagram without nodes and edges', () => { test('Simple file for diagram', async () => { - const document = diagram1; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - - expect(model).toHaveProperty('systemDiagram'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); - - expect(model.systemDiagram?.id).toBe('Systemdiagram1'); + const systemDiagram = await parseSystemDiagram({ services, text: diagram1 }); + expect(systemDiagram?.id).toBe('Systemdiagram1'); }); test('Diagram with indentation error', async () => { - const document = diagram4; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - - expect(model).toHaveProperty('systemDiagram'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(1); + await parseSystemDiagram({ services, text: diagram4 }, { parserErrors: 1 }); }); }); describe('Diagram with nodes', () => { test('Simple file for diagram and nodes', async () => { - const document = diagram2; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - const node1 = model.systemDiagram?.nodes[0]; - - expect(model).toHaveProperty('systemDiagram'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); - - expect(model.systemDiagram?.nodes.length).toBe(1); + const systemDiagram = await parseSystemDiagram({ services, text: diagram2 }); + expect(systemDiagram?.nodes).toHaveLength(1); + const node1 = systemDiagram?.nodes[0]; expect(node1?.id).toBe('CustomerNode'); expect(isReference(node1?.entity)).toBe(true); expect(node1?.entity?.$refText).toBe('Customer'); @@ -59,17 +36,10 @@ describe('CrossModel language Diagram', () => { describe('Diagram with edges', () => { test('Simple file for diagram and edges', async () => { - const document = diagram3; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - const edge1 = model.systemDiagram?.edges[0]; - - expect(model).toHaveProperty('systemDiagram'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); - - expect(model.systemDiagram?.edges.length).toBe(1); + const systemDiagram = await parseSystemDiagram({ services, text: diagram3 }); + expect(systemDiagram?.edges).toHaveLength(1); + const edge1 = systemDiagram?.edges[0]; expect(edge1?.id).toBe('OrderCustomerEdge'); expect(isReference(edge1?.relationship)).toBe(true); expect(edge1?.relationship?.$refText).toBe('Order_Customer'); @@ -78,53 +48,41 @@ describe('CrossModel language Diagram', () => { describe('Diagram with nodes and edges', () => { test('Simple file for diagram and edges', async () => { - const document = diagram5; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - const node1 = model.systemDiagram?.nodes[0]; - const edge1 = model.systemDiagram?.edges[0]; - - expect(model).toHaveProperty('systemDiagram'); - - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + const systemDiagram = await parseSystemDiagram({ services, text: diagram5 }); - expect(model.systemDiagram?.name).toBe('System diagram 1'); - expect(model.systemDiagram?.description).toBe('This is a basic diagram with nodes and edges'); - expect(model.systemDiagram?.nodes.length).toBe(1); - expect(model.systemDiagram?.edges.length).toBe(1); + expect(systemDiagram?.name).toBe('System diagram 1'); + expect(systemDiagram?.description).toBe('This is a basic diagram with nodes and edges'); + expect(systemDiagram?.nodes).toHaveLength(1); + const node1 = systemDiagram?.nodes[0]; expect(node1?.id).toBe('CustomerNode'); expect(isReference(node1?.entity)).toBe(true); expect(node1?.entity?.$refText).toBe('Customer'); expect(node1?.x).toBe(100); + expect(systemDiagram?.edges).toHaveLength(1); + const edge1 = systemDiagram?.edges[0]; expect(edge1?.id).toBe('OrderCustomerEdge'); expect(isReference(edge1?.relationship)).toBe(true); expect(edge1?.relationship?.$refText).toBe('Order_Customer'); }); test('Simple file for diagram and edges, but description and name coming last', async () => { - const document = diagram6; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - const node1 = model.systemDiagram?.nodes[0]; - const edge1 = model.systemDiagram?.edges[0]; + const systemDiagram = await parseSystemDiagram({ services, text: diagram6 }, { parserErrors: 3 }); - expect(model).toHaveProperty('systemDiagram'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + const node1 = systemDiagram?.nodes[0]; - expect(model.systemDiagram?.name).toBe('System diagram 1'); - expect(model.systemDiagram?.description).toBe('This is a basic diagram with nodes and edges'); - expect(model.systemDiagram?.nodes.length).toBe(1); - expect(model.systemDiagram?.edges.length).toBe(1); + expect(systemDiagram?.name).toBeUndefined(); + expect(systemDiagram?.description).toBeUndefined(); + expect(systemDiagram?.nodes).toHaveLength(1); expect(node1?.id).toBe('CustomerNode'); expect(isReference(node1?.entity)).toBe(true); expect(node1?.entity?.$refText).toBe('Customer'); expect(node1?.x).toBe(100); + expect(systemDiagram?.edges).toHaveLength(1); + const edge1 = systemDiagram?.edges[0]; expect(edge1?.id).toBe('OrderCustomerEdge'); expect(isReference(edge1?.relationship)).toBe(true); expect(edge1?.relationship?.$refText).toBe('Order_Customer'); diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts index 4eb344b..1385859 100644 --- a/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-entity.test.ts @@ -3,77 +3,45 @@ ********************************************************************************/ import { describe, expect, test } from '@jest/globals'; -import { EmptyFileSystem } from 'langium'; import { entity1, entity2, entity3, entity4 } from './test-utils/test-documents/entity/index.js'; -import { parseDocument } from './test-utils/utils.js'; +import { createCrossModelTestServices, parseEntity } from './test-utils/utils.js'; -import { createCrossModelServices } from '../../src/language-server/cross-model-module.js'; -import { CrossModelRoot } from '../../src/language-server/generated/ast.js'; - -const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; +const services = createCrossModelTestServices(); describe('CrossModel language Entity', () => { describe('Without attributes', () => { test('Simple file for entity', async () => { - const document = entity1; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - - expect(model).toHaveProperty('entity'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); - - expect(model.entity?.id).toBe('Customer'); - expect(model.entity?.name).toBe('Customer'); - expect(model.entity?.description).toBe('A customer with whom a transaction has been made.'); + const entity = await parseEntity({ services, text: entity1 }); + expect(entity.id).toBe('Customer'); + expect(entity.name).toBe('Customer'); + expect(entity.description).toBe('A customer with whom a transaction has been made.'); }); }); describe('With attributes', () => { test('entity with attributes', async () => { - const document = entity2; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - - expect(model).toHaveProperty('entity'); - - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); - - expect(model.entity?.attributes.length).toBe(6); - expect(model.entity?.attributes[0].id).toBe('Id'); - expect(model.entity?.attributes[0].name).toBe('Id'); - expect(model.entity?.attributes[0].datatype).toBe('int'); + const entity = await parseEntity({ services, text: entity2 }); + expect(entity.attributes.length).toBe(6); + expect(entity.attributes[0].id).toBe('Id'); + expect(entity.attributes[0].name).toBe('Id'); + expect(entity.attributes[0].datatype).toBe('int'); }); test('entity with attributes coming before the description and name', async () => { - const document = entity4; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - - expect(model).toHaveProperty('entity'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); - - expect(model.entity?.id).toBe('Customer'); - expect(model.entity?.name).toBe('Customer'); - expect(model.entity?.description).toBe('A customer with whom a transaction has been made.'); - - expect(model.entity?.attributes.length).toBe(6); - expect(model.entity?.attributes[0].id).toBe('Id'); - expect(model.entity?.attributes[0].name).toBe('Id'); - expect(model.entity?.attributes[0].datatype).toBe('int'); + const entity = await parseEntity({ services, text: entity4 }, { parserErrors: 2 }); + expect(entity.id).toBe('Customer'); + expect(entity.name).toBeUndefined(); + expect(entity.description).toBeUndefined(); + + expect(entity.attributes.length).toBe(6); + expect(entity.attributes[0].id).toBe('Id'); + expect(entity.attributes[0].name).toBe('Id'); + expect(entity.attributes[0].datatype).toBe('int'); }); test('entity with indentation error', async () => { - const document = entity3; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - - expect(model).toHaveProperty('entity'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(1); + await parseEntity({ services, text: entity3 }, { parserErrors: 1 }); }); }); }); diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts index dc6ed39..93e77c1 100644 --- a/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts +++ b/extensions/crossmodel-lang/test/language-server/cross-model-lang-relationship.test.ts @@ -3,44 +3,60 @@ ********************************************************************************/ import { describe, expect, test } from '@jest/globals'; -import { EmptyFileSystem, isReference } from 'langium'; +import { isReference } from 'langium'; -import { relationship1, relationship2 } from './test-utils/test-documents/relationship/index.js'; -import { parseDocument } from './test-utils/utils.js'; +import { + relationship1, + relationship2, + relationship_with_attribute, + relationship_with_attribute_wrong_entity, + relationship_with_duplicate_attributes +} from './test-utils/test-documents/relationship/index.js'; +import { createCrossModelTestServices, parseDocuments, parseRelationship } from './test-utils/utils.js'; -import { createCrossModelServices } from '../../src/language-server/cross-model-module.js'; -import { CrossModelRoot } from '../../src/language-server/generated/ast.js'; +import { address } from './test-utils/test-documents/entity/address.js'; +import { customer } from './test-utils/test-documents/entity/customer.js'; +import { order } from './test-utils/test-documents/entity/order.js'; -const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; +const services = createCrossModelTestServices(); describe('CrossModel language Relationship', () => { + beforeAll(async () => { + await parseDocuments(services, [order, customer, address]); + }); + test('Simple file for relationship', async () => { - const document = relationship1; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; - - expect(model).toHaveProperty('relationship'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); - - expect(model.relationship?.id).toBe('Order_Customer'); - expect(model.relationship?.name).toBe('Customer Order relationship'); - expect(model.relationship?.type).toBe('1:1'); - expect(model.relationship?.description).toBe('A relationship between a customer and an order.'); - - expect(isReference(model.relationship?.parent)).toBe(true); - expect(isReference(model.relationship?.child)).toBe(true); - expect(model.relationship?.parent?.$refText).toBe('Customer'); - expect(model.relationship?.child?.$refText).toBe('Order'); + const relationship = await parseRelationship({ services, text: relationship1 }); + + expect(relationship.id).toBe('Order_Customer1'); + expect(relationship.name).toBe('Customer Order relationship'); + expect(relationship.type).toBe('1:1'); + expect(relationship.description).toBe('A relationship between a customer and an order.'); + + expect(isReference(relationship.parent)).toBe(true); + expect(isReference(relationship.child)).toBe(true); + expect(relationship.parent.$refText).toBe('Customer'); + expect(relationship.child.$refText).toBe('Order'); }); test('relationship with indentation error', async () => { - const document = relationship2; - const parsedDocument = await parseDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; + await parseRelationship({ services, text: relationship2 }, { parserErrors: 2 }); + }); + + test('relationship with attributes', async () => { + const relationship = await parseRelationship({ services, text: relationship_with_attribute, validation: true }); + + expect(relationship.attributes).toHaveLength(1); + expect(relationship.$document.diagnostics).toHaveLength(0); + }); + + test('relationship with wrong entity', async () => { + const relationship = await parseRelationship({ services, text: relationship_with_attribute_wrong_entity, validation: true }); + expect(relationship.$document.diagnostics).toHaveLength(1); + }); - expect(model).toHaveProperty('relationship'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(1); + test('relationship with duplicates', async () => { + const relationship = await parseRelationship({ services, text: relationship_with_duplicate_attributes, validation: true }); + expect(relationship.$document.diagnostics).toHaveLength(2); }); }); diff --git a/extensions/crossmodel-lang/test/language-server/cross-model-naming.test.ts b/extensions/crossmodel-lang/test/language-server/cross-model-naming.test.ts index eb3a58e..21f7645 100644 --- a/extensions/crossmodel-lang/test/language-server/cross-model-naming.test.ts +++ b/extensions/crossmodel-lang/test/language-server/cross-model-naming.test.ts @@ -2,55 +2,85 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ import { describe, expect, test } from '@jest/globals'; -import { EmptyFileSystem } from 'langium'; -import { createCrossModelServices } from '../../src/language-server/cross-model-module.js'; -import { CrossModelRoot, EntityNode } from '../../src/language-server/generated/ast.js'; -import { parseDocument } from './test-utils/utils.js'; +import { EntityNode } from '../../src/language-server/generated/ast.js'; +import { createCrossModelTestServices, parseSystemDiagram } from './test-utils/utils.js'; -const services = createCrossModelServices({ ...EmptyFileSystem }); -const cmServices = services.CrossModel; +const services = createCrossModelTestServices(); -const ex1 = 'systemDiagram:'; -const ex2 = `diagram: +const ex1 = `systemDiagram: + id: example1`; +const ex2 = `systemDiagram: + id: example2 nodes: - - id: nodeA`; -const ex3 = `diagram: + - id: nodeA + entity: NotExisting + x: 0 + y: 0 + width: 0 + height: 0`; +const ex3 = `systemDiagram: + id: example3 nodes: - id: nodeA - - id: nodeA1`; -const ex4 = `diagram: + entity: NotExisting + x: 0 + y: 0 + width: 0 + height: 0 + - id: nodeA1 + entity: NotExisting + x: 0 + y: 0 + width: 0 + height: 0`; +const ex4 = `systemDiagram: + id: example4 nodes: - id: nodeA + entity: NotExisting + x: 0 + y: 0 + width: 0 + height: 0 - id: nodeA1 + entity: NotExisting + x: 0 + y: 0 + width: 0 + height: 0 - id: nodeA2 - - id: nodeA4`; + entity: NotExisting + x: 0 + y: 0 + width: 0 + height: 0 + - id: nodeA4 + entity: NotExisting + x: 0 + y: 0 + width: 0 + height: 0`; describe('NameUtil', () => { describe('findAvailableNodeName', () => { test('should return given name if unique', async () => { - const document = await parseDocument(cmServices, ex1); - - expect(cmServices.references.IdProvider.findNextId(EntityNode, 'nodeA', document.parseResult.value.systemDiagram!)).toBe('nodeA'); + const diagram = await parseSystemDiagram({ services, text: ex1 }); + expect(services.references.IdProvider.findNextId(EntityNode, 'nodeA', diagram)).toBe('nodeA'); }); test('should return unique name if given is taken', async () => { - const document = await parseDocument(cmServices, ex2); - - const result = cmServices.references.IdProvider.findNextId(EntityNode, 'nodeA', document.parseResult.value.systemDiagram!); - - expect(result).toBe('nodeA1'); + const diagram = await parseSystemDiagram({ services, text: ex2 }); + expect(services.references.IdProvider.findNextId(EntityNode, 'nodeA', diagram)).toBe('nodeA1'); }); test('should properly count up if name is taken', async () => { - const document = await parseDocument(cmServices, ex3); - - expect(cmServices.references.IdProvider.findNextId(EntityNode, 'nodeA', document.parseResult.value.systemDiagram!)).toBe('nodeA2'); + const diagram = await parseSystemDiagram({ services, text: ex3 }); + expect(services.references.IdProvider.findNextId(EntityNode, 'nodeA', diagram)).toBe('nodeA2'); }); test('should find lowest count if multiple are taken', async () => { - const document = await parseDocument(cmServices, ex4); - - expect(cmServices.references.IdProvider.findNextId(EntityNode, 'nodeA', document.parseResult.value.systemDiagram!)).toBe('nodeA3'); + const diagram = await parseSystemDiagram({ services, text: ex4 }); + expect(services.references.IdProvider.findNextId(EntityNode, 'nodeA', diagram)).toBe('nodeA3'); }); }); }); diff --git a/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts b/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts index 633e5cf..4ffa45f 100644 --- a/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts +++ b/extensions/crossmodel-lang/test/language-server/serializer/cross-model-serializer.test.ts @@ -40,8 +40,20 @@ describe('CrossModelLexer', () => { crossModelRootWithoutAttributes = _.cloneDeep(crossModelRoot); crossModelRoot.entity.attributes = [ - { $container: crossModelRoot.entity, $type: 'EntityAttribute', id: 'Attribute1', datatype: 'Datatype Attribute 1' }, - { $container: crossModelRoot.entity, $type: 'EntityAttribute', id: 'Attribute2', datatype: 'Datatype Attribute 2' } + { + $container: crossModelRoot.entity, + $type: 'EntityAttribute', + id: 'Attribute1', + name: 'Attribute1', + datatype: 'Datatype Attribute 1' + }, + { + $container: crossModelRoot.entity, + $type: 'EntityAttribute', + id: 'Attribute2', + name: 'Attribute2', + datatype: 'Datatype Attribute 2' + } ]; crossModelRootWithAttributesDifPlace.entity = { @@ -53,8 +65,20 @@ describe('CrossModelLexer', () => { name: 'test Name' }; crossModelRootWithAttributesDifPlace.entity.attributes = [ - { $container: crossModelRoot.entity, $type: 'EntityAttribute', id: 'Attribute1', datatype: 'Datatype Attribute 1' }, - { $container: crossModelRoot.entity, $type: 'EntityAttribute', id: 'Attribute2', datatype: 'Datatype Attribute 2' } + { + $container: crossModelRoot.entity, + $type: 'EntityAttribute', + id: 'Attribute1', + name: 'Attribute1', + datatype: 'Datatype Attribute 1' + }, + { + $container: crossModelRoot.entity, + $type: 'EntityAttribute', + id: 'Attribute2', + name: 'Attribute2', + datatype: 'Datatype Attribute 2' + } ]; }); @@ -116,7 +140,8 @@ describe('CrossModelLexer', () => { name: 'test Name', parent: ref1, child: ref2, - type: 'n:m' + type: 'n:m', + attributes: [] }; }); @@ -168,7 +193,8 @@ describe('CrossModelLexer', () => { name: 'test Name', parent: ref1, child: ref2, - type: 'n:m' + type: 'n:m', + attributes: [] } }; @@ -212,7 +238,9 @@ describe('CrossModelLexer', () => { $container: crossModelRoot.systemDiagram, $type: 'RelationshipEdge', relationship: ref3, - id: 'Edge1' + id: 'Edge1', + sourceNode: { $refText: 'A' }, + targetNode: { $refText: 'B' } } ]; }); @@ -230,8 +258,10 @@ const expected_result = `entity: description: "Test description" attributes: - id: Attribute1 + name: "Attribute1" datatype: "Datatype Attribute 1" - id: Attribute2 + name: "Attribute2" datatype: "Datatype Attribute 2"`; const expected_result2 = `entity: id: testId @@ -243,8 +273,10 @@ const expected_result3 = `entity: description: "Test description" attributes: - id: Attribute1 + name: "Attribute1" datatype: "Datatype Attribute 1" - id: Attribute2 + name: "Attribute2" datatype: "Datatype Attribute 2"`; const expected_result4 = `relationship: @@ -275,4 +307,6 @@ const expected_result5 = `systemDiagram: height: 102 edges: - id: Edge1 - relationship: Ref3`; + relationship: Ref3 + sourceNode: A + targetNode: B`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram2.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram2.ts index 981a1ae..de66068 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram2.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram2.ts @@ -8,5 +8,5 @@ export const diagram2 = `systemDiagram: entity: Customer x: 100 y: 100 - height: 100 - width: 100`; + width: 100 + height: 100`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram3.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram3.ts index 53fc33f..03d71f1 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram3.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram3.ts @@ -5,4 +5,6 @@ export const diagram3 = `systemDiagram: id: Systemdiagram1 edges: - id: OrderCustomerEdge - relationship: Order_Customer`; + relationship: Order_Customer + sourceNode: Order + targetNode: Customer`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram5.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram5.ts index 3acefcb..aa1242f 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram5.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram5.ts @@ -5,13 +5,15 @@ export const diagram5 = `systemDiagram: id: Systemdiagram1 name: "System diagram 1" description: "This is a basic diagram with nodes and edges" - edges: - - id: OrderCustomerEdge - relationship: Order_Customer nodes: - id: CustomerNode entity: Customer x: 100 y: 100 + width: 100 height: 100 - width: 100`; + edges: + - id: OrderCustomerEdge + relationship: Order_Customer + sourceNode: Anything + targetNode: IsPossible`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram6.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram6.ts index 0817c8f..97ca8c8 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram6.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/diagram/diagram6.ts @@ -3,15 +3,15 @@ ********************************************************************************/ export const diagram6 = `systemDiagram: id: Systemdiagram1 - edges: - - id: OrderCustomerEdge - relationship: Order_Customer nodes: - id: CustomerNode entity: Customer x: 100 y: 100 - height: 100 width: 100 + height: 100 + edges: + - id: OrderCustomerEdge + relationship: Order_Customer name: "System diagram 1" description: "This is a basic diagram with nodes and edges"`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/address.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/address.ts new file mode 100644 index 0000000..e9d2d13 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/address.ts @@ -0,0 +1,16 @@ +/******************************************************************************** + * Copyright (c) 2024 CrossBreeze. + ********************************************************************************/ +export const address = `entity: + id: Address + name: "Address" + attributes: + - id: CustomerID + name: "CustomerID" + datatype: "Integer" + - id: Street + name: "Street" + datatype: "Varchar" + - id: CountryCode + name: "CountryCode" + datatype: "Varchar"`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/customer.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/customer.ts new file mode 100644 index 0000000..1957315 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/customer.ts @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (c) 2024 CrossBreeze. + ********************************************************************************/ +export const customer = `entity: + id: Customer + name: "Customer" + attributes: + - id: Id + name: "Id" + datatype: "Integer" + - id: FirstName + name: "FirstName" + datatype: "Varchar" + - id: LastName + name: "LastName" + datatype: "Varchar" + - id: City + name: "City" + datatype: "Varchar" + - id: Country + name: "Country" + datatype: "Varchar" + - id: Phone + name: "Phone" + datatype: "Varchar" + - id: BirthDate + name: "BirthDate" + datatype: "DateTime"`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/index.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/index.ts index 62de41f..74b2fd5 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/index.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/index.ts @@ -1,7 +1,10 @@ /******************************************************************************** * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ +export * from './address.js'; +export * from './customer.js'; export * from './entity1.js'; export * from './entity2.js'; export * from './entity3.js'; export * from './entity4.js'; +export * from './order.js'; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/order.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/order.ts new file mode 100644 index 0000000..c9d22b9 --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/entity/order.ts @@ -0,0 +1,23 @@ +/******************************************************************************** + * Copyright (c) 2024 CrossBreeze. + ********************************************************************************/ +export const order = `entity: + id: Order + name: "Order" + description: "Order placed by a customer in the Customer table." + attributes: + - id: Id + name: "Id" + datatype: "Integer" + - id: OrderDate + name: "OrderDate" + datatype: "Integer" + - id: OrderNumber + name: "OrderNumber" + datatype: "Varchar" + - id: CustomerId + name: "CustomerId" + datatype: "Integer" + - id: TotalAmount + name: "TotalAmount" + datatype: "Float"`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/index.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/index.ts index eaf440c..456bfc9 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/index.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/index.ts @@ -3,3 +3,4 @@ ********************************************************************************/ export * from './relationship1.js'; export * from './relationship2.js'; +export * from './relationship_attribute.js'; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship1.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship1.ts index ae7ba0e..17494e9 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship1.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship1.ts @@ -2,9 +2,9 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ export const relationship1 = `relationship: - id: Order_Customer + id: Order_Customer1 + name: "Customer Order relationship" + description: "A relationship between a customer and an order." parent: Customer child: Order - type: "1:1" - name: "Customer Order relationship" - description: "A relationship between a customer and an order."`; + type: "1:1"`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship2.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship2.ts index eb27ba2..d28cf1f 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship2.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship2.ts @@ -2,7 +2,7 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ export const relationship2 = `relationship: - id: Order_Customer + id: Order_Customer2 parent: Customer child: Order type: "1:1"`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship_attribute.ts b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship_attribute.ts new file mode 100644 index 0000000..cf81bcb --- /dev/null +++ b/extensions/crossmodel-lang/test/language-server/test-utils/test-documents/relationship/relationship_attribute.ts @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2024 CrossBreeze. + ********************************************************************************/ + +/** Valid relationship with attribute */ +export const relationship_with_attribute = `relationship: + id: Order_CustomerWithAttribute + parent: Customer + child: Order + type: "1:1" + attributes: + - parent: Customer.Id + child: Order.CustomerId`; + +/** Relationship with invalid attribute (wrong entity) */ +export const relationship_with_attribute_wrong_entity = `relationship: + id: Order_CustomerWithAttributeWrongEntity + parent: Customer + child: Order + type: "1:1" + attributes: + - parent: Customer.Id + child: Order.Address`; + +/** Relationship with invalid attribute (duplicates) */ +export const relationship_with_duplicate_attributes = `relationship: + id: Order_CustomerWithDuplicateAttributes + parent: Customer + child: Order + type: "1:1" + attributes: + - parent: Customer.Id + child: Order.CustomerId + - parent: Customer.Id + child: Order.CustomerId`; diff --git a/extensions/crossmodel-lang/test/language-server/test-utils/utils.ts b/extensions/crossmodel-lang/test/language-server/test-utils/utils.ts index 08362a7..74615a3 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/utils.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/utils.ts @@ -1,26 +1,75 @@ /******************************************************************************** * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { AstNode, LangiumDocument, LangiumServices } from 'langium'; -import { URI } from 'vscode-uri'; - -export async function parseDocument(services: LangiumServices, input: string): Promise> { - const document = await parseHelper(services)(input); - if (!document.parseResult) { - throw new Error('Could not parse document'); - } - return document; -} - -export function parseHelper(services: LangiumServices): (input: string) => Promise> { - const metaData = services.LanguageMetaData; - const documentBuilder = services.shared.workspace.DocumentBuilder; - return async input => { - const randomNumber = Math.floor(Math.random() * 10000000) + 1000000; - const uri = URI.parse(`file:///${randomNumber}${metaData.fileExtensions[0]}`); - const document = services.shared.workspace.LangiumDocumentFactory.fromString(input, uri); - services.shared.workspace.LangiumDocuments.addDocument(document); - await documentBuilder.build([document]); - return document; - }; + +import { DefaultLangiumDocuments, EmptyFileSystem, LangiumDocument, LangiumServices } from 'langium'; +import { ParseHelperOptions, parseDocument as langiumParseDocument } from 'langium/test'; +import { CrossModelServices, createCrossModelServices } from '../../../src/language-server/cross-model-module.js'; +import { + CrossModelRoot, + Entity, + Mapping, + Relationship, + SystemDiagram, + isEntity, + isMapping, + isRelationship, + isSystemDiagram +} from '../../../src/language-server/generated/ast.js'; +import { SemanticRoot, TypeGuard, WithDocument, findSemanticRoot } from '../../../src/language-server/util/ast-util.js'; + +export function createCrossModelTestServices(): CrossModelServices { + const services = createCrossModelServices({ ...EmptyFileSystem }).CrossModel; + services.shared.workspace.LangiumDocuments = new DefaultLangiumDocuments(services.shared); + return services; +} + +export const parseDocument = langiumParseDocument; + +export interface ParseInput extends ParseHelperOptions { + services: LangiumServices; + text: T; +} + +export interface ParseAssert { + lexerErrors?: number; + parserErrors?: number; +} + +export async function parseDocuments( + services: LangiumServices, + inputs: string[], + options?: ParseHelperOptions +): Promise[]> { + return Promise.all(inputs.map(input => parseDocument(services, input, options))); +} + +export async function parseSemanticRoot( + input: ParseInput, + assert: ParseAssert, + guard: TypeGuard +): Promise> { + const document = await parseDocument(input.services, input.text, input); + expect(document.parseResult.lexerErrors).toHaveLength(assert.lexerErrors ?? 0); + expect(document.parseResult.parserErrors).toHaveLength(assert.parserErrors ?? 0); + const semanticRoot = findSemanticRoot(document, guard); + expect(semanticRoot).toBeDefined(); + (semanticRoot as any).$document = document; + return semanticRoot as WithDocument; +} + +export async function parseEntity(input: ParseInput, assert: ParseAssert = {}): Promise> { + return parseSemanticRoot(input, assert, isEntity); +} + +export async function parseRelationship(input: ParseInput, assert: ParseAssert = {}): Promise> { + return parseSemanticRoot(input, assert, isRelationship); +} + +export async function parseSystemDiagram(input: ParseInput, assert: ParseAssert = {}): Promise> { + return parseSemanticRoot(input, assert, isSystemDiagram); +} + +export async function parseMapping(input: ParseInput, assert: ParseAssert = {}): Promise> { + return parseSemanticRoot(input, assert, isMapping); } diff --git a/yarn.lock b/yarn.lock index d9f0c26..466bdbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3269,9 +3269,9 @@ undici-types "~5.26.4" "@types/node@^16.11.26": - version "16.18.80" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.80.tgz#9644e2d8acaf8163d46d23e05ce3822e9379dfc3" - integrity sha512-vFxJ1Iyl7A0+xB0uW1r1v504yItKZLdqg/VZELUZ4H02U0bXAgBisSQ8Erf0DMruNFz9ggoiEv6T8Ll9bTg8Jw== + version "16.18.87" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.87.tgz#9473038a28bf2d7ef7e4d23ef693a1011981abdf" + integrity sha512-+IzfhNirR/MDbXz6Om5eHV54D9mQlEMGag6AgEzlju0xH3M8baCXYwqQ6RKgGMpn9wSTx6Ltya/0y4Z8eSfdLw== "@types/node@~16.18.41": version "16.18.64" @@ -13914,7 +13914,12 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" -tmp@^0.2.0, tmp@^0.2.1: +tmp@^0.2.0: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + +tmp@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==