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 2 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 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)) {
martin-fleck-at marked this conversation as resolved.
Show resolved Hide resolved
// 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)) {
martin-fleck-at marked this conversation as resolved.
Show resolved Hide resolved
// 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