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 5ab060b..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 @@ -27,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 @@ -53,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, 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 3c2a07d..cc2b14f 100644 --- a/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts +++ b/extensions/crossmodel-lang/src/language-server/cross-model-validator.ts @@ -95,9 +95,7 @@ export class CrossModelValidator { if (attribute.parent.ref) { if (attribute.parent?.ref?.$container !== relationship.parent?.ref) { accept('error', 'Not a valid parent attribute.', { node: attribute, property: 'parent' }); - } - if (usedParentAttributes.includes(attribute.parent.ref)) { - // duplicate detected + } 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); @@ -106,9 +104,7 @@ export class CrossModelValidator { if (attribute.child.ref) { if (attribute.child?.ref?.$container !== relationship.child?.ref) { accept('error', 'Not a valid child attribute.', { node: attribute, property: 'child' }); - } - if (usedChildAttributes.includes(attribute.child.ref)) { - // duplicate detected + } 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); 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 87584a3..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'; @@ -201,12 +201,12 @@ export interface Relationship extends AstNode { readonly $container: CrossModelRoot; readonly $type: 'Relationship'; attributes: Array - child?: Reference + child: Reference description?: string - id?: string + id: string name?: string - parent?: Reference - type?: string + parent: Reference + type: string } export const Relationship = 'Relationship'; @@ -219,7 +219,6 @@ export interface RelationshipAttribute extends AstNode { readonly $container: Relationship; readonly $type: 'RelationshipAttribute'; child: Reference - description?: string parent: Reference } @@ -244,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'; diff --git a/extensions/crossmodel-lang/src/language-server/generated/grammar.ts b/extensions/crossmodel-lang/src/language-server/generated/grammar.ts index ceb1881..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": [] } @@ -548,194 +493,180 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, { - "$type": "Alternatives", + "$type": "Keyword", + "value": "id" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "id", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" + }, + "arguments": [] + } + }, + { + "$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": "Keyword", + "value": "name" }, { - "$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": ":" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":" + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ] - }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ { - "$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": "description" }, { - "$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": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "type" - }, - { - "$type": "Keyword", - "value": ":" + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" }, - { - "$type": "Assignment", - "feature": "type", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ] + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$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@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": "attributes" + }, + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "attributes" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@10" - }, - "arguments": [] + "value": "-" }, { "$type": "Assignment", @@ -744,28 +675,28 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@14" + "$ref": "#/rules@13" }, "arguments": [] - }, - "cardinality": "+" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] + } } - ] + ], + "cardinality": "+" + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] } ], - "cardinality": "*" + "cardinality": "?" }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -784,10 +715,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "parent" @@ -808,7 +735,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -835,38 +762,12 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, "deprecatedSyntax": false } - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ], - "cardinality": "?" } ] }, @@ -903,93 +804,13 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "Group", "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@10" - }, - "arguments": [] - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@16" - }, - "arguments": [], - "cardinality": "*" - }, { "$type": "RuleCall", "rule": { "$ref": "#/rules@9" }, "arguments": [] - } - ], - "cardinality": "*" - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagramFields", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagram" - }, - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "nodes" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@17" - }, - "arguments": [] - } - ] - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "edges" - }, - { - "$type": "Keyword", - "value": ":" }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@20" - }, - "arguments": [] - } - ] - }, - { - "$type": "Group", - "elements": [ { "$type": "Keyword", "value": "id" @@ -1005,108 +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": "Keyword", - "value": ":" }, { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "name" }, - "arguments": [] - } - } - ] - }, - { - "$type": "Group", - "elements": [ + { + "$type": "Keyword", + "value": ":" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, { - "$type": "Keyword", - "value": "name" + "$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": "Keyword", - "value": ":" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "nodes" + }, + { + "$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": "Assignment", - "feature": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "edges" }, - "arguments": [] - } - } - ] - } - ] - }, - "definesHiddenTokens": false, - "entry": false, - "fragment": false, - "hiddenTokens": [], - "parameters": [], - "wildcard": false - }, - { - "$type": "ParserRule", - "name": "SystemDiagramNodes", - "inferredType": { - "$type": "InferredType", - "name": "SystemDiagram" - }, - "definition": { - "$type": "Group", - "elements": [ - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@10" - }, - "arguments": [] - }, - { - "$type": "Assignment", - "feature": "nodes", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@18" + { + "$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": "?" }, - "arguments": [] - }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + ], "cardinality": "*" - }, - { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@9" - }, - "arguments": [] } ] }, @@ -1125,123 +1010,30 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "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" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "id", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@12" - }, - "arguments": [] - } - } - ] + "value": "id" }, { - "$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 - } - } - ] + "$type": "Keyword", + "value": ":" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "x" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "x", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@6" - }, - "arguments": [] - } - } - ] + "$type": "Assignment", + "feature": "id", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" + }, + "arguments": [] + } }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "y" + "value": "name" }, { "$type": "Keyword", @@ -1249,24 +1041,25 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "Assignment", - "feature": "y", + "feature": "name", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@4" }, "arguments": [] } } - ] + ], + "cardinality": "?" }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "width" + "value": "description" }, { "$type": "Keyword", @@ -1274,138 +1067,125 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load }, { "$type": "Assignment", - "feature": "width", + "feature": "description", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@4" }, "arguments": [] } } - ] + ], + "cardinality": "?" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "height" - }, - { - "$type": "Keyword", - "value": ":" - }, - { - "$type": "Assignment", - "feature": "height", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@6" - }, - "arguments": [] - } - } - ] + "$type": "Keyword", + "value": "entity" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "name" + "$type": "Keyword", + "value": ":" + }, + { + "$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": "name", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" - }, - "arguments": [] - } - } - ] + "deprecatedSyntax": false + } }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" + "$type": "Keyword", + "value": "x" + }, + { + "$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": [] + } } ] }, @@ -1424,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 + } } ] }, @@ -1600,7 +1329,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, @@ -1619,7 +1348,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@11" }, "arguments": [] } @@ -1638,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": [] } @@ -1680,7 +1418,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@25" }, "arguments": [] } @@ -1688,7 +1426,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -1707,10 +1445,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "id" @@ -1726,7 +1460,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@11" }, "arguments": [] } @@ -1751,7 +1485,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -1773,7 +1507,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@19" }, "arguments": [] } @@ -1792,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": [] } @@ -1870,10 +1613,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "source" @@ -1889,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": [] }, @@ -1912,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": [] } @@ -1954,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": [] } @@ -1980,10 +1728,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "relationship" @@ -1999,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": [] }, @@ -2026,10 +1770,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "join" @@ -2045,7 +1785,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@24" }, "arguments": [] } @@ -2077,7 +1817,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -2105,7 +1845,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -2130,7 +1870,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@9" }, "arguments": [] }, @@ -2154,7 +1894,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -2175,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": [] } @@ -2205,7 +1954,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@8" }, "arguments": [] } @@ -2224,10 +1973,6 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "definition": { "$type": "Group", "elements": [ - { - "$type": "Keyword", - "value": "-" - }, { "$type": "Keyword", "value": "attribute" @@ -2243,7 +1988,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@33" + "$ref": "#/rules@27" }, "arguments": [] } @@ -2263,7 +2008,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@34" + "$ref": "#/rules@28" }, "arguments": [] } @@ -2292,7 +2037,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -2334,7 +2079,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@3" }, "arguments": [] }, @@ -2360,7 +2105,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@5" }, "arguments": [] } @@ -2384,7 +2129,7 @@ export const CrossModelGrammar = (): Grammar => loadedCrossModelGrammar ?? (load "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@5" + "$ref": "#/rules@4" }, "arguments": [] } @@ -2419,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 0d86e54..be90bf5 100644 --- a/extensions/crossmodel-lang/src/language-server/relationship.langium +++ b/extensions/crossmodel-lang/src/language-server/relationship.langium @@ -5,23 +5,21 @@ import 'entity' Relationship: 'relationship' ':' INDENT - ( - 'id' ':' id=ID | - 'name' ':' name=STRING | - 'description' ':' description=STRING | - 'parent' ':' parent=[Entity:IDReference] | - 'child' ':' child=[Entity:IDReference] | - 'type' ':' type=STRING | + '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) - )* + ('-' attributes+=RelationshipAttribute)+ + DEDENT + )? DEDENT ; RelationshipAttribute: - '-' 'parent' ':' parent=[Attribute:IDReference] - 'child' ':' child=[Attribute:IDReference] - ('description' ':' description=STRING)? + '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 dab62b4..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 @@ -12,9 +12,8 @@ import { relationship_with_attribute_wrong_entity, relationship_with_duplicate_attributes } from './test-utils/test-documents/relationship/index.js'; -import { createCrossModelTestServices, parseAndForgetDocument, parseDocuments } from './test-utils/utils.js'; +import { createCrossModelTestServices, parseDocuments, parseRelationship } from './test-utils/utils.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'; @@ -22,73 +21,42 @@ import { order } from './test-utils/test-documents/entity/order.js'; const services = createCrossModelTestServices(); describe('CrossModel language Relationship', () => { - beforeAll(() => { - parseDocuments(services, [order, customer, address]); + beforeAll(async () => { + await parseDocuments(services, [order, customer, address]); }); test('Simple file for relationship', async () => { - const document = relationship1; - const parsedDocument = await parseAndForgetDocument(services, document); - const model = parsedDocument.parseResult.value as CrossModelRoot; + const relationship = await parseRelationship({ services, text: relationship1 }); - expect(model).toHaveProperty('relationship'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); + 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(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'); + 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 parseAndForgetDocument(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(1); + await parseRelationship({ services, text: relationship2 }, { parserErrors: 2 }); }); test('relationship with attributes', async () => { - const parsedDocument = await parseAndForgetDocument(services, relationship_with_attribute, { - validation: true - }); - const model = parsedDocument.parseResult.value as CrossModelRoot; + const relationship = await parseRelationship({ services, text: relationship_with_attribute, validation: true }); - expect(model).toHaveProperty('relationship'); - expect(parsedDocument.parseResult.lexerErrors.length).toBe(0); - expect(parsedDocument.parseResult.parserErrors.length).toBe(0); - expect(parsedDocument.diagnostics).toHaveLength(0); + expect(relationship.attributes).toHaveLength(1); + expect(relationship.$document.diagnostics).toHaveLength(0); }); test('relationship with wrong entity', async () => { - const parsedDocument = await parseAndForgetDocument(services, relationship_with_attribute_wrong_entity, { - validation: true - }); - 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(parsedDocument.diagnostics).toHaveLength(1); + const relationship = await parseRelationship({ services, text: relationship_with_attribute_wrong_entity, validation: true }); + expect(relationship.$document.diagnostics).toHaveLength(1); }); test('relationship with duplicates', async () => { - const parsedDocument = await parseAndForgetDocument(services, relationship_with_duplicate_attributes, { - validation: true - }); - 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(parsedDocument.diagnostics).toHaveLength(2); + 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 6d5a5f3..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' + } ]; }); @@ -214,7 +238,9 @@ describe('CrossModelLexer', () => { $container: crossModelRoot.systemDiagram, $type: 'RelationshipEdge', relationship: ref3, - id: 'Edge1' + id: 'Edge1', + sourceNode: { $refText: 'A' }, + targetNode: { $refText: 'B' } } ]; }); @@ -232,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 @@ -245,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: @@ -277,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/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 index 2330adf..cf81bcb 100644 --- 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 @@ -4,7 +4,7 @@ /** Valid relationship with attribute */ export const relationship_with_attribute = `relationship: - id: Order_Customer + id: Order_CustomerWithAttribute parent: Customer child: Order type: "1:1" @@ -14,7 +14,7 @@ export const relationship_with_attribute = `relationship: /** Relationship with invalid attribute (wrong entity) */ export const relationship_with_attribute_wrong_entity = `relationship: - id: Order_Customer + id: Order_CustomerWithAttributeWrongEntity parent: Customer child: Order type: "1:1" @@ -24,7 +24,7 @@ export const relationship_with_attribute_wrong_entity = `relationship: /** Relationship with invalid attribute (duplicates) */ export const relationship_with_duplicate_attributes = `relationship: - id: Order_Customer + id: Order_CustomerWithDuplicateAttributes parent: Customer child: Order type: "1:1" 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 e3b9c9f..74615a3 100644 --- a/extensions/crossmodel-lang/test/language-server/test-utils/utils.ts +++ b/extensions/crossmodel-lang/test/language-server/test-utils/utils.ts @@ -2,11 +2,21 @@ * Copyright (c) 2023 CrossBreeze. ********************************************************************************/ -import { AstNode, DefaultLangiumDocuments, EmptyFileSystem, LangiumDocument, LangiumServices } from 'langium'; -import { ParseHelperOptions, clearDocuments, parseDocument } from 'langium/test'; +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'; - -export { parseDocument } from 'langium/test'; +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; @@ -14,20 +24,52 @@ export function createCrossModelTestServices(): CrossModelServices { return services; } -export async function parseDocuments( +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))); +): Promise[]> { + return Promise.all(inputs.map(input => parseDocument(services, input, options))); } -export async function parseAndForgetDocument( - services: LangiumServices, - input: string, - options?: ParseHelperOptions -): Promise> { - const document = await parseDocument(services, input, options); - await clearDocuments(services, [document]); - return document; +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); }