Skip to content

Commit

Permalink
Enhance grammar to support attribute mapping from sources to target
Browse files Browse the repository at this point in the history
- Provide mapping grammar for source objects, joins and target mapping
- Add new element creation for mappings in toolbar and context menu
- Adapt custom serializer for new attributes
- Add example for mapping

Refactorings:
- Split grammar into multiple files
- Replace id and id references with unquoted text
- Fix auto completion in text files through completion lexer and parser
- Enhance auto-completion for string, number, and boolean
  • Loading branch information
martin-fleck-at authored and harmen-xb committed Dec 8, 2023
1 parent bf4c2c0 commit f4dd129
Show file tree
Hide file tree
Showing 50 changed files with 2,286 additions and 652 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"--log-level=debug",
"--hostname=localhost",
"--no-cluster",
"--root-dir=${workspaceRoot}/examples/yaml-example",
"--root-dir=${workspaceRoot}/examples/mapping-example",
"--app-project-path=${workspaceFolder}/applications/electron-app",
"--remote-debugging-port=9222",
"--no-app-auto-install",
Expand Down
22 changes: 22 additions & 0 deletions examples/mapping-example/ExampleCRM/diagrams/CRM.diagram.cm
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
diagram:
id: CRM
name: "CRM"
description: "Shows the complete CRM"
nodes:
- id: CustomerNode
entity: Customer
x: 325.5893891316664
y: 261.8195919791379
width: 122.22364807128906
height: 151
- id: OrderNode
entity: Order
x: 649.4416344093675
y: 274.85224527770106
width: 139.6079559326172
height: 132
edges:
- id: CustomerToOrder
relationship: Order_Customer
sourceNode: CustomerNode
targetNode: OrderNode
13 changes: 13 additions & 0 deletions examples/mapping-example/ExampleCRM/entities/Address.entity.cm
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
entity:
id: Address
name: "Address"
attributes:
- id: CustomerID
name: "CustomerID"
datatype: "Integer"
- id: Street
name: "Street"
datatype: "Varchar"
- id: CountryCode
name: "CountryCode"
datatype: "Varchar"
22 changes: 22 additions & 0 deletions examples/mapping-example/ExampleCRM/entities/Customer.entity.cm
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
entity:
id: Customer
name: "Customer"
attributes:
- id: Id
name: "Id"
datatype: "Integer"
- id: FirstName
name: "FirstName"
datatype: "Varchar"
- id: LastName
name: "LastName"
datatype: "Varchar"
- id: City
name: "City"
datatype: "Varchar"
- id: Country
name: "Country"
datatype: "Varchar"
- id: Phone
name: "Phone"
datatype: "Varchar"
20 changes: 20 additions & 0 deletions examples/mapping-example/ExampleCRM/entities/Order.entity.cm
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
entity:
id: Order
name: "Order"
description: "Order placed by a customer in the Customer table."
attributes:
- id: Id
name: "Id"
datatype: "Integer"
- id: OrderDate
name: "OrderDate"
datatype: "Integer"
- id: OrderNumber
name: "OrderNumber"
datatype: "Varchar"
- id: CustomerId
name: "CustomerId"
datatype: "Integer"
- id: TotalAmount
name: "TotalAmount"
datatype: "Float"
4 changes: 4 additions & 0 deletions examples/mapping-example/ExampleCRM/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "ExampleCRM",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
relationship:
id: Order_Customer
parent: Customer
child: Order
type: '1:1'
16 changes: 16 additions & 0 deletions examples/mapping-example/ExampleDWH/CompleteCustomer.entity.cm
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
entity:
id: CompleteCustomer
name: "CompleteCustomer"
attributes:
- id: Name
name: "Name"
datatype: "Varchar"
- id: Country
name: "Country"
datatype: "Varchar"
- id: FixedNumber
name: "FixedNumber"
datatype: "Integer"
- id: FixedString
name: "FixedString"
datatype: "Varchar"
31 changes: 31 additions & 0 deletions examples/mapping-example/ExampleDWH/DWH.mapping.cm
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
mapping:
id: CompleteCustomer_Example
sources:
- id: Customer
object: ExampleCRM.Customer
join: from
- id: Address
object: ExampleCRM.Address
join: left-join
relations:
- source: Customer
conditions:
- join: Address.CustomerID = Customer.Id
- id: Country
object: ExampleMasterdata.Country
join: left-join
relations:
- source: Address
conditions:
- join: Country.Code = Address.CountryCode
target:
entity: CompleteCustomer
attributes:
- attribute: Name
source: Customer.LastName
- attribute: Country
source: Country.Code
- attribute: FixedNumber
source: 1337
- attribute: FixedString
source: "Fixed String"
8 changes: 8 additions & 0 deletions examples/mapping-example/ExampleDWH/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "ExampleDWH",
"version": "1.0.0" ,
"dependencies": {
"ExampleCRM": "1.0.0",
"ExampleMasterdata": "1.0.0"
}
}
10 changes: 10 additions & 0 deletions examples/mapping-example/ExampleMasterdata/Country.entity.cm
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
entity:
id: Country
name: "Country"
attributes:
- id: Code
name: "Code"
datatype: "Varchar"
- id: Name
name: "name"
datatype: "Varchar"
4 changes: 4 additions & 0 deletions examples/mapping-example/ExampleMasterdata/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "ExampleMasterdata",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
/********************************************************************************
* Copyright (c) 2023 CrossBreeze.
********************************************************************************/
import { quote } from '@crossbreeze/protocol';
import {
AstNodeDescription,
CompletionAcceptor,
CompletionContext,
DefaultCompletionProvider,
GrammarAST,
MaybePromise,
NextFeature
NextFeature,
getContainerOfType
} from 'langium';
import { getExplicitRuleType } from 'langium/internal';
import { v4 as uuid } from 'uuid';
import { CompletionItemKind, InsertTextFormat } from 'vscode-languageserver-protocol';
import type { Range } from 'vscode-languageserver-types';
import { CrossModelServices } from './cross-model-module.js';
import { isExternalDescriptionForLocalPackage } from './cross-model-scope.js';

Expand All @@ -26,6 +32,147 @@ export class CrossModelCompletionProvider extends DefaultCompletionProvider {
super(services);
}

protected override completionFor(
context: CompletionContext,
next: NextFeature<GrammarAST.AbstractElement>,
acceptor: CompletionAcceptor
): MaybePromise<void> {
this.fixCompletionNode(context);
const assignment = getContainerOfType(next.feature, GrammarAST.isAssignment);
if (!GrammarAST.isCrossReference(next.feature) && assignment) {
return this.completionForAssignment(context, assignment, acceptor);
}
return super.completionFor(context, next, acceptor);
}

protected fixCompletionNode(context: CompletionContext): CompletionContext {
// for some reason the document is not always properly set on the node
let node = context.node;
while (node) {
if (!node.$document) {
(context.node as any).$document = context.document;
}
node = node.$container;
}
return context;
}

protected completionForAssignment(
context: CompletionContext,
assignment: GrammarAST.Assignment,
acceptor: CompletionAcceptor
): MaybePromise<void> {
if (assignment.feature === 'id') {
return this.completionForId(context, assignment, acceptor);
}
if (GrammarAST.isRuleCall(assignment.terminal) && assignment.terminal.rule.ref) {
const type = this.getRuleType(assignment.terminal.rule.ref);
switch (type) {
case 'string':
return this.completionForString(context, assignment, acceptor);
case 'number':
return this.completionForNumber(context, assignment, acceptor);
case 'boolean':
return this.completionForBoolean(context, assignment, acceptor);
}
}
}

protected getRuleType(rule: GrammarAST.AbstractRule): string | undefined {
if (GrammarAST.isTerminalRule(rule)) {
return rule.type?.name ?? 'string';
}
const explicitType = getExplicitRuleType(rule);
return explicitType ?? rule.name;
}

protected completionForId(
context: CompletionContext,
_assignment: GrammarAST.Assignment,
acceptor: CompletionAcceptor
): MaybePromise<void> {
const generatedId = 'id_' + uuid();
acceptor(context, {
label: 'Generated ID: ' + generatedId,
textEdit: {
newText: generatedId,
range: this.getCompletionRange(context)
},
kind: CompletionItemKind.Value,
sortText: '0'
});
}

protected completionForString(
context: CompletionContext,
assignment: GrammarAST.Assignment,
acceptor: CompletionAcceptor
): MaybePromise<void> {
acceptor(context, {
label: 'String Value',
textEdit: {
newText: quote('${1:' + assignment.feature + '}'),
range: this.getCompletionRange(context)
},
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Snippet,
sortText: '0'
});
}

protected completionForNumber(
context: CompletionContext,
_assignment: GrammarAST.Assignment,
acceptor: CompletionAcceptor
): MaybePromise<void> {
acceptor(context, {
label: 'Number Value',
textEdit: {
newText: '${1:0}',
range: this.getCompletionRange(context)
},
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Snippet,
sortText: '0'
});
}

protected completionForBoolean(
context: CompletionContext,
_assignment: GrammarAST.Assignment,
acceptor: CompletionAcceptor
): MaybePromise<void> {
acceptor(context, {
label: 'Boolean Value',
textEdit: {
newText: '${true:0}',
range: this.getCompletionRange(context)
},
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Snippet,
sortText: '0'
});
}

protected getCompletionRange(context: CompletionContext): Range {
const text = context.textDocument.getText();
const existingText = text.substring(context.tokenOffset, context.offset);
let range: Range = {
start: context.position,
end: context.position
};
if (existingText.length > 0) {
// FIXME: Completely replace the current token
const start = context.textDocument.positionAt(context.tokenOffset + 1);
const end = context.textDocument.positionAt(context.tokenEndOffset - 1);
range = {
start,
end
};
}
return range;
}

protected override completionForCrossReference(
context: CompletionContext,
crossRef: NextFeature<GrammarAST.CrossReference>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { CrossModelSerializer } from './cross-model-serializer.js';
import { CrossModelValidator, registerValidationChecks } from './cross-model-validator.js';
import { CrossModelWorkspaceManager } from './cross-model-workspace-manager.js';
import { CrossModelGeneratedModule, CrossModelGeneratedSharedModule } from './generated/module.js';
import { createCrossModelCompletionParser } from './lexer/cross-model-completion-parser.js';
import { CrossModelLexer } from './lexer/cross-model-lexer.js';
import { CrossModelTokenBuilder } from './lexer/cross-model-token-generator.js';

Expand Down Expand Up @@ -168,7 +169,8 @@ export function createCrossModelModule(
},
parser: {
TokenBuilder: () => new CrossModelTokenBuilder(),
Lexer: services => new CrossModelLexer(services)
Lexer: services => new CrossModelLexer(services),
CompletionParser: services => createCrossModelCompletionParser(services)
},
shared: () => context.shared
};
Expand Down
Loading

0 comments on commit f4dd129

Please sign in to comment.