Skip to content

Commit

Permalink
Extend relationship with attributes between parent and child entity
Browse files Browse the repository at this point in the history
- Add relationship attribute support in grammar
- Provide only correct attributes as suggestion to the user
- Validate to only have correct attributes and no duplicates
  • Loading branch information
martin-fleck-at committed Mar 7, 2024
1 parent 3d9eba8 commit b2fa0bc
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 176 deletions.
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 Down Expand Up @@ -182,8 +183,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,37 @@ 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' });
}
if (usedParentAttributes.includes(attribute.parent.ref)) {
// duplicate detected
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' });
}
if (usedChildAttributes.includes(attribute.child.ref)) {
// duplicate detected
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
30 changes: 29 additions & 1 deletion extensions/crossmodel-lang/src/language-server/generated/ast.ts
Expand Up @@ -200,6 +200,7 @@ export function isReferenceSource(item: unknown): item is ReferenceSource {
export interface Relationship extends AstNode {
readonly $container: CrossModelRoot;
readonly $type: 'Relationship';
attributes: Array<RelationshipAttribute>
child?: Reference<Entity>
description?: string
id?: string
Expand All @@ -214,6 +215,20 @@ 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>
description?: string
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 Down Expand Up @@ -366,6 +381,7 @@ export type CrossModelAstType = {
NumberLiteral: NumberLiteral
ReferenceSource: ReferenceSource
Relationship: Relationship
RelationshipAttribute: RelationshipAttribute
RelationshipCondition: RelationshipCondition
RelationshipEdge: RelationshipEdge
SourceObject: SourceObject
Expand All @@ -381,7 +397,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 +443,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 +482,14 @@ export class CrossModelAstReflection extends AbstractAstReflection {
]
};
}
case 'Relationship': {
return {
name: 'Relationship',
mandatory: [
{ name: 'attributes', type: 'array' }
]
};
}
case 'SourceObject': {
return {
name: 'SourceObject',
Expand Down

0 comments on commit b2fa0bc

Please sign in to comment.