Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend relationship with attributes between parent and child entity #52

Merged
merged 3 commits into from Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -2,4 +2,7 @@ relationship:
id: Address_Customer
parent: Customer
child: ExampleCRM.Address
type: "1:1"
type: "1:1"
attributes:
- parent: Customer.Id
child: ExampleCRM.Address.CustomerID
Expand Up @@ -2,4 +2,7 @@ relationship:
id: Order_Customer
parent: Customer
child: Order
type: "1:1"
type: "1:1"
attributes:
- parent: Customer.Id
child: Order.CustomerId
Expand Up @@ -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';

/**
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
}
Expand Up @@ -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,
Expand All @@ -25,7 +27,8 @@ export function registerValidationChecks(services: CrossModelServices): void {
const checks: ValidationChecks<CrossModelAstType> = {
AstNode: validator.checkNode,
RelationshipEdge: validator.checkRelationshipEdge,
SourceObject: validator.checkSourceObject
SourceObject: validator.checkSourceObject,
Relationship: validator.checkRelationship
};
registry.register(checks, validator);
}
Expand Down Expand Up @@ -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' });
Expand Down
36 changes: 14 additions & 22 deletions extensions/crossmodel-lang/src/language-server/entity.langium
Expand Up @@ -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)?;
61 changes: 44 additions & 17 deletions extensions/crossmodel-lang/src/language-server/generated/ast.ts
Expand Up @@ -51,7 +51,7 @@ export interface Attribute extends AstNode {
datatype: string
description?: string
id: string
name?: string
name: string
}

export const Attribute = 'Attribute';
Expand Down Expand Up @@ -104,7 +104,7 @@ export interface Entity extends AstNode {
readonly $type: 'Entity';
attributes: Array<EntityAttribute>
description?: string
id?: string
id: string
name?: string
}

Expand All @@ -118,13 +118,13 @@ export interface EntityNode extends AstNode {
readonly $container: SystemDiagram;
readonly $type: 'EntityNode';
description?: string
entity?: Reference<Entity>
height?: number
id?: string
entity: Reference<Entity>
height: number
id: string
name?: string
width?: number
x?: number
y?: number
width: number
x: number
y: number
}

export const EntityNode = 'EntityNode';
Expand Down Expand Up @@ -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<Entity>
attributes: Array<RelationshipAttribute>
child: Reference<Entity>
description?: string
id?: string
id: string
name?: string
parent?: Reference<Entity>
type?: string
parent: Reference<Entity>
type: string
}

export const Relationship = 'Relationship';
Expand All @@ -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<Attribute>
parent: Reference<Attribute>
}

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';
Expand All @@ -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<Relationship>
sourceNode?: Reference<EntityNode>
targetNode?: Reference<EntityNode>
id: string
relationship: Reference<Relationship>
sourceNode: Reference<EntityNode>
targetNode: Reference<EntityNode>
}

export const RelationshipEdge = 'RelationshipEdge';
Expand Down Expand Up @@ -366,6 +380,7 @@ export type CrossModelAstType = {
NumberLiteral: NumberLiteral
ReferenceSource: ReferenceSource
Relationship: Relationship
RelationshipAttribute: RelationshipAttribute
RelationshipCondition: RelationshipCondition
RelationshipEdge: RelationshipEdge
SourceObject: SourceObject
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -462,6 +481,14 @@ export class CrossModelAstReflection extends AbstractAstReflection {
]
};
}
case 'Relationship': {
return {
name: 'Relationship',
mandatory: [
{ name: 'attributes', type: 'array' }
]
};
}
case 'SourceObject': {
return {
name: 'SourceObject',
Expand Down