Ran Wei/MDE Series
中文
Model-Driven Engineering with Eclipse Epsilon — Ran Wei

Model-to-model transformation

Map one metamodel onto another with ETL rules, guards, equivalents and lazy rules.

4 hours4 sessions8 runnable examplesEpsilon Playground

By the end you can

  • Map components to a graph with ETL.
  • Use equivalent, guards, lazy and primary rules deliberately.
  • Inspect the generated target model and its omissions.

Before you start

Modules 1–6 are recommended. Examples run in the Playground unless noted.

Download all examples of this module (zip)

Contents

Study plan

4 hours

Times are estimates and include running the examples. Progress is saved in this browser and shared by both language editions.

Session 160 min
Define source and target mappings
Session 260 min
Trace, guards and lazy rules
Session 360 min
Primary rules and target checks
Session 460 min
Practice
1

Two metamodels and one mapping

The component model describes ports and wiring. A graph model needs only nodes and directed edges. Model-to-model transformation translates between these languages: the source model conforms to the component metamodel, and the target model conforms to a separate graph metamodel. The transformation must decide what to preserve, what to omit and how to reconnect references.

Here, each Architecture becomes a Graph, each Component becomes a Node, and each Connector becomes an Edge. Port names disappear in the simplest version. This is deliberate: a graph algorithm can work at component level without knowing the component language’s port details. ETL, the Epsilon Transformation Language, defines the mapping in rules. Its rule bodies use EOL expressions from Module 4.

2

Write ETL rules

The first program has three rules. Architecture2Graph creates the root and populates its contained nodes and edges. Component2Node copies a component name. Connector2Edge links the graph nodes corresponding to the components at its two endpoint ports.

ExampleComponents become a graphOpen in Playground ↗
transform.etl
rule Architecture2Graph
  transform a : Source!Architecture
  to g : Target!Graph {
  g.name = a.name;
  g.nodes ::= a.components;
  g.edges ::= a.connectors;
}
rule Component2Node
  transform c : Source!Component
  to n : Target!Node {
  n.name = c.name;
}
rule Connector2Edge
  transform c : Source!Connector
  to e : Target!Edge {
  e.source ::= c.source.eContainer();
  e.target ::= c.target.eContainer();
}
graph.emf
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
  attr String name;
  val Node[*] nodes;
  val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
  attr String label;
  ref Node source;
  ref Node target;
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Graph name="Alarm"
  Node name="OrGate"
  Node name="AndGate"
  Node name="Siren"
  Edge source->OrGate target->AndGate
  Edge source->AndGate target->Siren

Read the captured target tree. It has a graph named Alarm, three nodes and two edges. The target metamodel is visible under the ETL program. Source!Component and Target!Node specify types in different models; they avoid mistaking a source component for a target node. g.nodes ::= a.components uses ETL’s special assignment: assign each source component’s transformed counterpart, not the source component itself.

A transformation is not just an export. It creates another model with its own types and relationships, ready for another model-management task. The local runner prints the target model’s containment tree so you can inspect it; it does not persist the target as an XMI file for this lesson.

3

Resolve transformed counterparts

ETL keeps a transformation trace connecting source elements to the target elements produced for them. equivalent() asks for a source element’s transformed counterpart. The next program performs the same mapping as the first, but calls equivalent() explicitly when it fills the target graph and links edge endpoints.

ExampleResolve counterparts explicitlyOpen in Playground ↗
transform.etl
rule Architecture2Graph
  transform a : Source!Architecture
  to g : Target!Graph {
  g.name = a.name;
  for (c in a.components) { g.nodes.add(c.equivalent()); }
  for (wire in a.connectors) { g.edges.add(wire.equivalent()); }
}
rule Component2Node
  transform c : Source!Component
  to n : Target!Node { n.name = c.name; }
rule Connector2Edge
  transform c : Source!Connector
  to e : Target!Edge {
  e.source = c.source.eContainer().equivalent();
  e.target = c.target.eContainer().equivalent();
}
graph.emf
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
  attr String name;
  val Node[*] nodes;
  val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
  attr String label;
  ref Node source;
  ref Node target;
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Graph name="Alarm"
  Node name="OrGate"
  Node name="AndGate"
  Node name="Siren"
  Edge source->OrGate target->AndGate
  Edge source->AndGate target->Siren

Compare the two outputs: they are the same graph. ::= is shorthand for the common case in which a target feature receives an equivalent target element. An explicit equivalent() is useful when you want to calculate, select or combine counterparts in a larger expression. Neither form is a lookup by matching names: the trace records which rule created which result.

4

Guards and lazy rules

A guard restricts which source elements a rule transforms. In the next source model, the second connector joins a digital output to an analog input. The guarded connector rule creates an edge only when the two port types match. All three components still become nodes, but only the compatible connection becomes an edge.

ExampleSkip an incompatible connectionOpen in Playground ↗
transform.etl
rule Architecture2Graph
  transform a : Source!Architecture
  to g : Target!Graph {
  g.name = a.name;
  g.nodes ::= a.components;
  g.edges ::= a.connectors;
}
rule Component2Node
  transform c : Source!Component
  to n : Target!Node {
  n.name = c.name;
}
rule Connector2Edge
  transform c : Source!Connector
  to e : Target!Edge {
  guard: c.source.type = c.target.type
  e.source ::= c.source.eContainer();
  e.target ::= c.target.eContainer();
}
graph.emf
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
  attr String name;
  val Node[*] nodes;
  val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
  attr String label;
  ref Node source;
  ref Node target;
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="ANALOG"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Graph name="Alarm"
  Node name="OrGate"
  Node name="AndGate"
  Node name="Siren"
  Edge source->OrGate target->AndGate

This is a policy choice, not a replacement for validation. Silently dropping an invalid connector may be dangerous for a downstream task. A project can first run EVL to reject the model, or deliberately transform only valid connections while reporting omissions. The example isolates the guard’s effect so you can see it.

A lazy rule runs only when an equivalent is requested. The next example maps ports, rather than components, to graph nodes. Architecture2Graph requests Port2Node for each port. Because Port is abstract and the instances are InPort and OutPort, the rule uses @greedy to match subclasses as well as the exact type.

ExampleCreate port nodes on demandOpen in Playground ↗
transform.etl
rule Architecture2Graph
  transform a : Source!Architecture
  to g : Target!Graph {
  g.name = a.name;
  for (p in Port.all) { g.nodes.add(p.equivalent("Port2Node")); }
}
@lazy
@greedy
rule Port2Node
  transform p : Source!Port
  to n : Target!Node {
  n.name = p.eContainer().name + "." + p.name;
}
graph.emf
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
  attr String name;
  val Node[*] nodes;
  val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
  attr String label;
  ref Node source;
  ref Node target;
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Graph name="Alarm"
  Node name="OrGate.door"
  Node name="OrGate.window"
  Node name="OrGate.open"
  Node name="AndGate.open"
  Node name="AndGate.armed"
  Node name="AndGate.sound"
  Node name="Siren.sound"

The result has seven nodes labelled with both the owning component and port name. Without @greedy, this rule would not apply to the concrete port subclasses in Epsilon 2.8.0; the requested counterparts would be missing. @lazy avoids creating port nodes unless a rule asks for them. Use it for results that are conditional or expensive, not to conceal rules whose products should always exist.

5

Primary rules and trace

Sometimes more than one rule transforms the same source type. In the next example, Component2Node makes the ordinary node and Component2Audit makes a second, audit node. @primary puts the ordinary node first when equivalent() chooses one counterpart.

ExampleChoose the primary counterpartOpen in Playground ↗
transform.etl
rule Architecture2Graph
  transform a : Source!Architecture
  to g : Target!Graph {
  g.name = a.name;
  for (c in a.components) { g.nodes.add(c.equivalent()); }
}
@primary
rule Component2Node
  transform c : Source!Component
  to n : Target!Node { n.name = c.name; }
rule Component2Audit
  transform c : Source!Component
  to n : Target!Node { n.name = "audit:" + c.name; }
graph.emf
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
  attr String name;
  val Node[*] nodes;
  val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
  attr String label;
  ref Node source;
  ref Node target;
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Graph name="Alarm"
  Node name="OrGate"
  Node name="AndGate"
  Node name="Siren"
Node name="audit:OrGate"
Node name="audit:AndGate"
Node name="audit:Siren"

The graph contains the three ordinary nodes. The audit nodes are also produced by their non-lazy rule; the local target printer shows them as separate roots because no graph contains them. Primary does not suppress other rules. It controls which counterpart comes first for a single-element equivalent() lookup. If a target model should contain only graph-owned nodes, remove the audit rule or give its results a deliberate home. The ETL reference describes rule scheduling, equivalent() and equivalents().

6

Check the target model

A transformation can run successfully yet lose information you needed. The basic graph drops port names and signal types. The guarded graph drops an incompatible connection. The primary-rule graph has extra uncontained audit nodes. Read the target metamodel and the printed target tree together to see exactly what exists after transformation.

For a real mapping, write down the intended invariants: the number of nodes expected, which connectors become edges, whether names remain unique, and whether every edge endpoint belongs to the target graph. Validate the source before transforming when the mapping assumes well-formed wiring, then inspect or validate the target too. Module 10 applies this pattern to a second domain.

7

Exercises

Exercise 1★★★conceptual5 min

Map the types. List the source type that becomes each of Graph, Node and Edge. Which source information does the basic target omit?

Show solution

Architecture becomes Graph, Component becomes Node, and Connector becomes Edge. The basic target omits ports, their names and signal types; it keeps only component names and component-level connections.

Exercise 2★★★conceptual5 min

Read ::=. Why is g.nodes ::= a.components meaningful while assigning a.components directly to target Node containment would be wrong?

Show solution

The right-hand side contains source Component elements. ETL’s ::= resolves their transformed Node counterparts and assigns those to g.nodes. Direct assignment would mix elements from incompatible metamodels.

Exercise 3★★★conceptual5 min

Interpret primary. In the primary example, why are audit nodes printed even though the graph’s nodes contain only ordinary nodes?

Show solution

Component2Audit is non-lazy, so it still runs for every component. @primary makes ordinary nodes the first counterpart selected by equivalent(); it does not disable the audit rule. The audit nodes remain separate target roots because nothing contains them.

Exercise 4★★★coding10 min

Keep an edge label. Extend the target Edge with a label attribute and set it to <source port> to <target port> in Connector2Edge. Check that two labelled edges are present.

Show solution

The target metamodel already has the label feature in this module’s examples. The rule assigns it while retaining both endpoint mappings:

SolutionGive graph edges labelsOpen in Playground ↗
transform.etl
rule Architecture2Graph
  transform a : Source!Architecture
  to g : Target!Graph {
  g.name = a.name;
  g.nodes ::= a.components;
  g.edges ::= a.connectors;
}
rule Component2Node
  transform c : Source!Component
  to n : Target!Node {
  n.name = c.name;
}
rule Connector2Edge
  transform c : Source!Connector
  to e : Target!Edge {
  e.label = c.source.name + " to " + c.target.name;
  e.source ::= c.source.eContainer();
  e.target ::= c.target.eContainer();
}
graph.emf
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
  attr String name;
  val Node[*] nodes;
  val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
  attr String label;
  ref Node source;
  ref Node target;
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Graph name="Alarm"
  Node name="OrGate"
  Node name="AndGate"
  Node name="Siren"
  Edge label="open to open" source->OrGate target->AndGate
  Edge label="sound to sound" source->AndGate target->Siren
Exercise 5★★★coding10 min

Test a guarded model. Run the guarded transformation on the correctly typed alarm. How many edges should the result contain, and why?

Show solution

Both connectors pass the type guard, so both become edges:

SolutionKeep only valid connectionsOpen in Playground ↗
transform.etl
rule Architecture2Graph
  transform a : Source!Architecture
  to g : Target!Graph {
  g.name = a.name;
  g.nodes ::= a.components;
  g.edges ::= a.connectors;
}
rule Component2Node
  transform c : Source!Component
  to n : Target!Node {
  n.name = c.name;
}
rule Connector2Edge
  transform c : Source!Connector
  to e : Target!Edge {
  guard: c.source.type = c.target.type
  e.source ::= c.source.eContainer();
  e.target ::= c.target.eContainer();
}
graph.emf
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
  attr String name;
  val Node[*] nodes;
  val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
  attr String label;
  ref Node source;
  ref Node target;
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Graph name="Alarm"
  Node name="OrGate"
  Node name="AndGate"
  Node name="Siren"
  Edge source->OrGate target->AndGate
  Edge source->AndGate target->Siren
Exercise 6★★★coding10 min

Request only output ports. Adapt the lazy example so the graph receives nodes only for OutPort instances. Use their full Component.Port labels. Do you still need @greedy?

Show solution

The rule now matches the exact concrete OutPort type, so @greedy is unnecessary. The graph contains the two output ports:

SolutionLazy output-port nodesOpen in Playground ↗
transform.etl
rule Architecture2Graph
  transform a : Source!Architecture
  to g : Target!Graph {
  g.name = a.name;
  for (p in OutPort.all) { g.nodes.add(p.equivalent("Port2Node")); }
}
@lazy
rule Port2Node
  transform p : Source!OutPort
  to n : Target!Node {
  n.name = p.eContainer().name + "." + p.name;
}
graph.emf
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
  attr String name;
  val Node[*] nodes;
  val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
  attr String label;
  ref Node source;
  ref Node target;
}
More files: alarm.flexmi, components.emf
alarm.flexmi
<?nsuri components?>
<architecture name="Alarm">
  <component name="OrGate">
    <inPort name="door" type="DIGITAL"/>
    <inPort name="window" type="DIGITAL"/>
    <outPort name="open" type="DIGITAL"/>
  </component>
  <component name="AndGate">
    <inPort name="open" type="DIGITAL"/>
    <inPort name="armed" type="DIGITAL"/>
    <outPort name="sound" type="DIGITAL"/>
  </component>
  <component name="Siren">
    <inPort name="sound" type="DIGITAL"/>
  </component>
  <connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
  <connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
components.emf
@namespace(uri="components", prefix="components")
package components;

enum PortType {
  DIGITAL;
  ANALOG;
}

class Architecture {
  attr String[1] name;
  val Component[*]#architecture components;
  val Connector[*] connectors;
}

class Component {
  attr String[1] name;
  ref Architecture#components architecture;
  val Port[*] ports;
}

abstract class Port {
  attr String[1] name;
  attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}

class Connector {
  ref OutPort source;
  ref InPort target;
}
Output captured from Epsilon 2.8.0 when this page was built. The Playground may word messages differently and does not show warnings about the model.
Output
Graph name="Alarm"
  Node name="OrGate.open"
  Node name="AndGate.sound"
8

Self-check quiz

1
What does an ETL transformation produce?
2
What does Source!Component identify?
3
What does ETL’s ::= do here?
4
What does a guard on Connector2Edge control?
5
Why does the lazy Port2Node rule use @greedy?
6
What does @primary guarantee in the example?
7
What should be checked after a transformation?
9

Further reading

  • The ETL reference documents rules, guards, traces, lazy and primary behaviour.
  • The EOL reference explains expressions used inside rule bodies.