Skip to content

Commit

Permalink
Add, edit and delete entity inheritances in the text representation (#77
Browse files Browse the repository at this point in the history
)

- Extend grammar with 'inherits' keyword and super entities
- Ensure proper serialization and cycle detection validation
- Add test cases
- Created example entity inheritance structure with system diagram.

Co-authored-by: Harmen Wessels <[email protected]>
  • Loading branch information
martin-fleck-at and harmen-xb authored Dec 13, 2024
1 parent ff0669f commit 890a051
Show file tree
Hide file tree
Showing 24 changed files with 651 additions and 332 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
systemDiagram:
id: ProductInheritance
name: "ProductInheritance"
nodes:
- id: ExampleDWH_ProductNode
entity: ExampleDWH.Product
x: 242
y: 33
width: 154.649658203125
height: 116
- id: ExampleDWH_NewEntityNode
entity: ExampleDWH.DigitalProduct
x: 187
y: 275
width: 154.22557544708252
height: 73.93824672698975
- id: PhysicalProductNode
entity: PhysicalProduct
x: 429
y: 275
width: 156.57902002334595
height: 136
- id: PerishableProductNode
entity: PerishableProduct
x: 429
y: 495
width: 155.22567415237427
height: 73.93824672698975
- id: SomeProductNode
entity: ExampleOtherModel.SomeProduct
x: 671
y: 352
width: 207.14583730697632
height: 55.33333396911621
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
entity:
id: DigitalProduct
name: "DigitalProduct"
inherits:
- Product
attributes:
- id: Attribute
name: "ProductID"
datatype: "Integer"
identifier: true
- id: SizeInKB
name: "SizeInKB"
datatype: "Float"
- id: DownloadURL
name: "DownloadURL"
datatype: "Varchar"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
entity:
id: PerishableProduct
name: "PerishableProduct"
description: "A product with an expiration date"
inherits:
- PhysicalProduct
- ExampleOtherModel.SomeProduct
attributes:
- id: ProductID
name: "ProductID"
datatype: "Integer"
identifier: true
- id: ExpirationDate
name: "ExpirationDate"
datatype: "Varchar"
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
entity:
id: PhysicalProduct
name: "PhysicalProduct"
inherits:
- Product
attributes:
- id: ProductID
name: "ProductID"
datatype: "Integer"
identifier: true
- id: WeightInKG
name: "WeightInKG"
datatype: "Float"
- id: Width
name: "Width"
datatype: "Integer"
- id: Attribute12
name: "Height"
datatype: "Integer"
- id: Depth
name: "Depth"
datatype: "Integer"
17 changes: 17 additions & 0 deletions examples/mapping-example/ExampleDWH/entities/Product.entity.cm
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
entity:
id: Product
name: "Product"
attributes:
- id: Attribute
name: "ProductID"
datatype: "Integer"
identifier: true
- id: Attribute1
name: "Name"
datatype: "Varchar"
- id: Attribute12
name: "Description"
datatype: "Varchar"
- id: Attribute123
name: "Price"
datatype: "Float"
3 changes: 2 additions & 1 deletion examples/mapping-example/ExampleDWH/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "1.0.0" ,
"dependencies": {
"ExampleCRM": "1.0.0",
"ExampleMasterdata": "1.0.0"
"ExampleMasterdata": "1.0.0",
"ExampleOtherModel": "1.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
entity:
id: SomeProduct
name: "SomeProduct"
attributes:
- id: SomeProductAttribute
name: "SomeProductAttribute"
datatype: "Varchar"
4 changes: 4 additions & 0 deletions examples/mapping-example/ExampleOtherModel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "ExampleOtherModel",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ export class CrossModelSerializer implements Serializer<CrossModelRoot> {
}
if (
key === 'id' ||
(key === 'superEntities' && Array.isArray(parent)) ||
propertyOf(parent, key, isCustomProperty, 'name') ||
propertyOf(parent, key, isSourceObject, 'join') ||
this.isValidReference(parent, key, value)
(!Array.isArray(value) && this.isValidReference(parent, key, value))
) {
// values that we do not want to quote because they are ids or references
return value;
Expand Down Expand Up @@ -103,7 +104,7 @@ export class CrossModelSerializer implements Serializer<CrossModelRoot> {
return undefined;
}
const separator = onNewLine ? CrossModelSerializer.CHAR_NEWLINE : ' ';
const serializedProp = `${prop}:${separator}${serializedPropValue}`;
const serializedProp = `${this.toKeyword(prop)}:${separator}${serializedPropValue}`;
const serialized = isFirstNested ? this.indent(serializedProp, indentationLevel) : serializedProp;
isFirstNested = false;
return serialized;
Expand All @@ -123,6 +124,13 @@ export class CrossModelSerializer implements Serializer<CrossModelRoot> {
return JSON.stringify(value);
}

protected toKeyword(prop: string): string {
if (prop === 'superEntities') {
return 'inherits';
}
return prop;
}

protected indent(text: string, level: number): string {
return `${CrossModelSerializer.CHAR_INDENTATION.repeat(level * CrossModelSerializer.INDENTATION_AMOUNT_OBJECT)}${text}`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Attribute,
AttributeMapping,
CrossModelAstType,
Entity,
isEntity,
isEntityAttribute,
isMapping,
Expand Down Expand Up @@ -52,14 +53,15 @@ export function registerValidationChecks(services: CrossModelServices): void {

const checks: ValidationChecks<CrossModelAstType> = {
AstNode: validator.checkNode,
RelationshipEdge: validator.checkRelationshipEdge,
AttributeMapping: validator.checkAttributeMapping,
Entity: validator.checkEntity,
Mapping: validator.checkMapping,
SourceObject: validator.checkSourceObject,
Relationship: validator.checkRelationship,
AttributeMapping: validator.checkAttributeMapping,
TargetObject: validator.checkTargetObject,
RelationshipEdge: validator.checkRelationshipEdge,
SourceObject: validator.checkSourceObject,
SourceObjectCondition: validator.checkSourceObjectCondition,
SourceObjectDependency: validator.checkSourceObjectDependency,
SourceObjectCondition: validator.checkSourceObjectCondition
TargetObject: validator.checkTargetObject
};
registry.register(checks, validator);
}
Expand Down Expand Up @@ -142,6 +144,55 @@ export class CrossModelValidator {
}
}

checkEntity(entity: Entity, accept: ValidationAcceptor): void {
const cycle = this.findInheritanceCycle(entity);
if (cycle.length > 0) {
accept('error', `Inheritance cycle detected: ${cycle.join(' -> ')}.`, { node: entity, property: 'superEntities' });
}
}

protected findInheritanceCycle(entity: Entity): string[] {
const visited: Set<string> = new Set();
const recursionStack: Set<string> = new Set();
const path: string[] = [];

function depthFirst(current: Entity): string[] {
const currentId = current.id;

// Mark the current node as visited and add to recursion stack
visited.add(currentId);
recursionStack.add(currentId);
path.push(currentId);

for (const superEntityRef of current.superEntities) {
const superEntity = superEntityRef.ref;
if (!superEntity) {
continue; // Ignore unresolved references
}
const superId = superEntity.id;
if (!visited.has(superId)) {
const cycle = depthFirst(superEntity);
if (cycle.length > 0) {
return cycle; // Propagate the detected cycle up the recursion
}
} else if (recursionStack.has(superId)) {
// Cycle detected
const cycleStartIndex = path.indexOf(superId);
const cycle = path.slice(cycleStartIndex);
cycle.push(superId);
return cycle;
}
}

// Backtrack: remove the current node from recursion stack and path
recursionStack.delete(currentId);
path.pop();
return []; // No cycle detected in this path
}

return depthFirst(entity);
}

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
Expand Down
5 changes: 5 additions & 0 deletions extensions/crossmodel-lang/src/language-server/entity.langium
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ Entity:
'id' ':' id=ID
('name' ':' name=STRING)?
('description' ':' description=STRING)?
('inherits' ':'
INDENT
(LIST_ITEM superEntities+=[Entity:IDReference])+
DEDENT
)?
('attributes' ':'
INDENT
(LIST_ITEM attributes+=EntityAttribute)+
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export type CrossModelKeywordNames =
| "height"
| "id"
| "identifier"
| "inherits"
| "inner-join"
| "join"
| "left-join"
Expand Down Expand Up @@ -202,6 +203,7 @@ export interface Entity extends AstNode {
description?: string;
id: string;
name?: string;
superEntities: Array<Reference<Entity>>;
}

export const Entity = 'Entity';
Expand Down Expand Up @@ -377,7 +379,7 @@ export interface SystemDiagram extends AstNode {
customProperties: Array<CustomProperty>;
description?: string;
edges: Array<RelationshipEdge>;
id?: string;
id: string;
name?: string;
nodes: Array<EntityNode>;
}
Expand Down Expand Up @@ -525,6 +527,7 @@ export class CrossModelAstReflection extends AbstractAstReflection {
case 'AttributeMappingTarget:value': {
return TargetObjectAttribute;
}
case 'Entity:superEntities':
case 'EntityNode:entity':
case 'Relationship:child':
case 'Relationship:parent':
Expand Down Expand Up @@ -630,7 +633,8 @@ export class CrossModelAstReflection extends AbstractAstReflection {
{ name: 'customProperties', defaultValue: [] },
{ name: 'description' },
{ name: 'id' },
{ name: 'name' }
{ name: 'name' },
{ name: 'superEntities', defaultValue: [] }
]
};
}
Expand Down
Loading

0 comments on commit 890a051

Please sign in to comment.