Integrated Workflows

Cross-Format Integration

Master cross-format model integration by connecting XMI, JSON, and native Swift data sources.

In this tutorial, you’ll integrate models from different sources and formats, create unified workflows that span multiple platforms, and build APIs that bridge between model-driven and traditional development approaches.

60 mins Estimated Time

Section 1

Understanding Format Integration Challenges

Modern systems often involve multiple data formats and platforms. XMI from Java EMF tools, JSON from web APIs, and native Swift data structures each have different characteristics and constraints.

Cross-format integration enables Swift Modelling tools to work seamlessly with existing toolchains while providing the benefits of model-driven development.

Multi-Format Ecosystem

Step 1

Set up the integration scenario.

This scenario involves integrating a Java EMF-based design tool, a web-based model editor that uses JSON, and a Swift application that needs native data structures.

integration-overview.md
1# Cross-Format Integration Scenario
2
3## Overview
4
5This tutorial demonstrates integrating models across three different platforms:
6
71. **Java EMF Tool** - Enterprise design tool using XMI format
82. **Web Editor** - Browser-based model editor using JSON format
93. **Swift Application** - Native iOS/macOS app using Swift structs
10
11## Business Context
12
13Acme Organisation uses different tools for project management:
14
15- **Enterprise Architects** use a Java EMF-based tool for project planning
16- **Team Members** use a web-based interface for task updates
17- **Mobile Users** access projects through a native Swift app
18
19## Integration Requirements
20
21| Source | Format | Direction | Consumer |
22|--------|--------|-----------|----------|
23| Java EMF Tool | XMI | Export | Web Editor, Swift App |
24| Web Editor | JSON | Import/Export | Java EMF Tool, Swift App |
25| Swift App | Swift | Import | Local storage |
26
27## Key Challenges
28
291. **Format Translation** - XMI references vs JSON IDs vs Swift optionals
302. **Schema Evolution** - Different tools may have different versions
313. **Conflict Resolution** - Concurrent edits from multiple sources
324. **Type Safety** - Preserving type information across formats
33
34## Success Criteria
35
36- [ ] Bidirectional XMI <-> JSON conversion
37- [ ] Type-safe Swift code generation
38- [ ] Automated synchronisation pipeline
39- [ ] Conflict detection and resolution
40- [ ] Complete audit trail of changes
41
42## Tools Used
43
44- `swift-ecore` - Metamodel operations
45- `swift-atl` - Model transformations
46- `swift-mtl` - Code generation
47- `swift-sync` - Synchronisation management

Step 2

Create the shared metamodel.

This metamodel defines project management concepts that will be represented in multiple formats across different tools and platforms.

ProjectManagement.ecore
1<?xml version="1.0" encoding="UTF-8"?>
2<!-- Project Management Metamodel -->
3<!-- Shared across XMI, JSON, and Swift representations -->
4<!-- Version: 1.0 -->
5<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
6 xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="ProjectManagement"
7 nsURI="http://www.example.org/projectmanagement/1.0" nsPrefix="pm">
8
9 <!-- Enumerations for type safety across all formats -->
10 <eClassifiers xsi:type="ecore:EEnum" name="ProjectStatus">
11 <eLiterals name="PLANNING" value="0"/>
12 <eLiterals name="ACTIVE" value="1"/>
13 <eLiterals name="ON_HOLD" value="2"/>
14 <eLiterals name="COMPLETED" value="3"/>
15 <eLiterals name="CANCELLED" value="4"/>
16 </eClassifiers>
17
18 <eClassifiers xsi:type="ecore:EEnum" name="TaskPriority">
19 <eLiterals name="LOW" value="0"/>
20 <eLiterals name="MEDIUM" value="1"/>
21 <eLiterals name="HIGH" value="2"/>
22 <eLiterals name="CRITICAL" value="3"/>
23 </eClassifiers>
24
25 <eClassifiers xsi:type="ecore:EEnum" name="TaskStatus">
26 <eLiterals name="NOT_STARTED" value="0"/>
27 <eLiterals name="IN_PROGRESS" value="1"/>
28 <eLiterals name="BLOCKED" value="2"/>
29 <eLiterals name="IN_REVIEW" value="3"/>
30 <eLiterals name="COMPLETED" value="4"/>
31 </eClassifiers>
32
33 <!-- Root container for organisation -->
34 <eClassifiers xsi:type="ecore:EClass" name="Organisation">
35 <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
36 <eStructuralFeatures xsi:type="ecore:EAttribute" name="domain" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
37 <eStructuralFeatures xsi:type="ecore:EReference" name="departments" upperBound="-1" eType="#//Department" containment="true"/>
38 <eStructuralFeatures xsi:type="ecore:EReference" name="projects" upperBound="-1" eType="#//Project" containment="true"/>
39 <eStructuralFeatures xsi:type="ecore:EReference" name="members" upperBound="-1" eType="#//TeamMember" containment="true"/>
40 </eClassifiers>
41
42 <!-- Department for team organisation -->
43 <eClassifiers xsi:type="ecore:EClass" name="Department">
44 <eStructuralFeatures xsi:type="ecore:EAttribute" name="id" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString">
45 <eAnnotations source="http://www.eclipse.org/emf/2002/GenModel">
46 <details key="documentation" value="Unique identifier for cross-format references"/>
47 </eAnnotations>
48 </eStructuralFeatures>
49 <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
50 <eStructuralFeatures xsi:type="ecore:EAttribute" name="code" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
51 <eStructuralFeatures xsi:type="ecore:EReference" name="manager" eType="#//TeamMember"/>
52 <eStructuralFeatures xsi:type="ecore:EReference" name="members" upperBound="-1" eType="#//TeamMember" eOpposite="#//TeamMember/department"/>
53 </eClassifiers>
54
55 <!-- Team member with contact information -->
56 <eClassifiers xsi:type="ecore:EClass" name="TeamMember">
57 <eStructuralFeatures xsi:type="ecore:EAttribute" name="id" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
58 <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
59 <eStructuralFeatures xsi:type="ecore:EAttribute" name="email" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
60 <eStructuralFeatures xsi:type="ecore:EAttribute" name="role" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
61 <eStructuralFeatures xsi:type="ecore:EReference" name="department" eType="#//Department" eOpposite="#//Department/members"/>
62 <eStructuralFeatures xsi:type="ecore:EReference" name="assignedTasks" upperBound="-1" eType="#//Task" eOpposite="#//Task/assignee"/>
63 </eClassifiers>
64
65 <!-- Project with milestones and tasks -->
66 <eClassifiers xsi:type="ecore:EClass" name="Project">
67 <eStructuralFeatures xsi:type="ecore:EAttribute" name="id" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
68 <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
69 <eStructuralFeatures xsi:type="ecore:EAttribute" name="description" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
70 <eStructuralFeatures xsi:type="ecore:EAttribute" name="status" eType="#//ProjectStatus"/>
71 <eStructuralFeatures xsi:type="ecore:EAttribute" name="startDate" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EDate"/>
72 <eStructuralFeatures xsi:type="ecore:EAttribute" name="targetEndDate" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EDate"/>
73 <eStructuralFeatures xsi:type="ecore:EAttribute" name="budget" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EDouble"/>
74 <eStructuralFeatures xsi:type="ecore:EReference" name="owner" eType="#//TeamMember"/>
75 <eStructuralFeatures xsi:type="ecore:EReference" name="department" eType="#//Department"/>
76 <eStructuralFeatures xsi:type="ecore:EReference" name="milestones" upperBound="-1" eType="#//Milestone" containment="true"/>
77 <eStructuralFeatures xsi:type="ecore:EReference" name="tasks" upperBound="-1" eType="#//Task" containment="true"/>
78 <eStructuralFeatures xsi:type="ecore:EReference" name="teamMembers" upperBound="-1" eType="#//TeamMember"/>
79 </eClassifiers>
80
81 <!-- Milestone for tracking project progress -->
82 <eClassifiers xsi:type="ecore:EClass" name="Milestone">
83 <eStructuralFeatures xsi:type="ecore:EAttribute" name="id" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
84 <eStructuralFeatures xsi:type="ecore:EAttribute" name="name" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
85 <eStructuralFeatures xsi:type="ecore:EAttribute" name="dueDate" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EDate"/>
86 <eStructuralFeatures xsi:type="ecore:EAttribute" name="completed" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EBoolean"/>
87 <eStructuralFeatures xsi:type="ecore:EReference" name="deliverables" upperBound="-1" eType="#//Task"/>
88 </eClassifiers>
89
90 <!-- Task with dependencies and assignments -->
91 <eClassifiers xsi:type="ecore:EClass" name="Task">
92 <eStructuralFeatures xsi:type="ecore:EAttribute" name="id" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
93 <eStructuralFeatures xsi:type="ecore:EAttribute" name="title" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
94 <eStructuralFeatures xsi:type="ecore:EAttribute" name="description" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
95 <eStructuralFeatures xsi:type="ecore:EAttribute" name="priority" eType="#//TaskPriority"/>
96 <eStructuralFeatures xsi:type="ecore:EAttribute" name="status" eType="#//TaskStatus"/>
97 <eStructuralFeatures xsi:type="ecore:EAttribute" name="estimatedHours" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EDouble"/>
98 <eStructuralFeatures xsi:type="ecore:EAttribute" name="actualHours" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EDouble"/>
99 <eStructuralFeatures xsi:type="ecore:EAttribute" name="dueDate" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EDate"/>
100 <eStructuralFeatures xsi:type="ecore:EReference" name="assignee" eType="#//TeamMember" eOpposite="#//TeamMember/assignedTasks"/>
101 <eStructuralFeatures xsi:type="ecore:EReference" name="dependencies" upperBound="-1" eType="#//Task"/>
102 <eStructuralFeatures xsi:type="ecore:EReference" name="comments" upperBound="-1" eType="#//Comment" containment="true"/>
103 </eClassifiers>
104
105 <!-- Comment for task discussions -->
106 <eClassifiers xsi:type="ecore:EClass" name="Comment">
107 <eStructuralFeatures xsi:type="ecore:EAttribute" name="id" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
108 <eStructuralFeatures xsi:type="ecore:EAttribute" name="text" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
109 <eStructuralFeatures xsi:type="ecore:EAttribute" name="timestamp" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EDate"/>
110 <eStructuralFeatures xsi:type="ecore:EReference" name="author" eType="#//TeamMember"/>
111 </eClassifiers>
112</ecore:EPackage>

Step 3

Identify format-specific constraints.

Document the specific limitations and features of each format to understand integration challenges and design appropriate bridging strategies.

format-constraints.md
1# Format-Specific Constraints
2
3## XMI (XML Metadata Interchange)
4
5### Strengths
6- Full metamodel information preserved
7- Rich reference semantics (containment, opposites)
8- Standardised by OMG (Object Management Group)
9- Supports cross-file references via hrefs
10
11### Constraints
12- Verbose XML syntax increases file size
13- Requires XMI namespace declarations
14- Reference resolution depends on resource sets
15- Not directly consumable by web browsers
16
17### Example Reference Handling
18```xml
19<!-- Containment reference -->
20<tasks xsi:type="pm:Task" id="TASK-001">
21 <assignee href="#MEMBER-001"/>
22</tasks>
23
24<!-- Cross-file reference -->
25<owner href="team.xmi#//@members.0"/>
26```
27
28---
29
30## JSON (JavaScript Object Notation)
31
32### Strengths
33- Native to web browsers and JavaScript
34- Compact representation
35- Easy to parse and generate
36- Well-supported by REST APIs
37
38### Constraints
39- No built-in type information
40- Flat reference model (IDs only)
41- No containment semantics
42- Circular references require special handling
43
44### Example Reference Handling
45```json
46{
47 "id": "TASK-001",
48 "title": "Implement feature",
49 "assigneeId": "MEMBER-001"
50}
51```
52
53---
54
55## Swift (Native Structures)
56
57### Strengths
58- Full type safety with compiler checks
59- Optional handling for nullable values
60- Value types vs reference types
61- Protocol-based abstraction
62
63### Constraints
64- No direct serialisation format
65- Requires Codable conformance for JSON
66- Reference cycles need weak/unowned
67- Metamodel changes require recompilation
68
69### Example Type Definition
70```swift
71struct Task: Identifiable, Codable {
72 let id: String
73 var title: String
74 var assigneeId: String?
75 var status: TaskStatus
76}
77```
78
79---
80
81## Translation Matrix
82
83| Feature | XMI | JSON | Swift |
84|---------|-----|------|-------|
85| Type Information | Full | None | Compile-time |
86| Containment | Native | Manual | Manual |
87| Opposites | Native | Manual | Manual |
88| Null Handling | xsi:nil | null | Optional |
89| Collections | Ordered | Array | Array/Set |
90| Dates | ISO 8601 | ISO 8601 | Date |
91| Enums | Literal names | Strings | Swift enum |
92
93---
94
95## Integration Considerations
96
97### XMI -> JSON
981. Flatten containment hierarchy to ID references
992. Include type discriminator for polymorphism
1003. Convert XMI references to simple IDs
1014. Serialise dates as ISO 8601 strings
102
103### JSON -> XMI
1041. Reconstruct containment from type information
1052. Resolve ID references to XMI hrefs
1063. Validate against metamodel constraints
1074. Handle missing optional values
108
109### JSON -> Swift
1101. Map JSON types to Swift types
1112. Generate Codable conformance
1123. Handle optional vs required fields
1134. Create custom date decoders if needed

Step 4

Plan the integration architecture.

Design the overall architecture that connects different format sources while maintaining data integrity and enabling bidirectional synchronisation.

integration-architecture.md
1# Integration Architecture
2
3## System Overview
4
5```
6+------------------+ +------------------+ +------------------+
7| Java EMF Tool | | Web Editor | | Swift App |
8| (Enterprise) | | (Browser) | | (iOS/macOS) |
9+--------+---------+ +--------+---------+ +--------+---------+
10 | | |
11 | XMI | JSON | Swift
12 | | |
13+--------v------------------------v------------------------v---------+
14| Integration Gateway |
15| +-------------+ +-------------+ +-------------+ +-------------+ |
16| | XMI Parser | | JSON Parser | | Validator | | Sync Engine | |
17| +-------------+ +-------------+ +-------------+ +-------------+ |
18+--------+------------------------+------------------------+---------+
19 | | |
20 v v v
21+--------+------------------------+------------------------+---------+
22| Canonical Model |
23| (In-memory Ecore representation) |
24+--------------------------------------------------------------------+
25```
26
27## Component Responsibilities
28
29### XMI Parser
30- Parse XMI 2.0/2.1 documents
31- Resolve cross-file references
32- Validate against Ecore metamodel
33- Generate XMI output with proper namespaces
34
35### JSON Parser
36- Parse/generate JSON documents
37- Map IDs to model references
38- Handle embedded vs referenced objects
39- Support JSON Schema validation
40
41### Validator
42- Enforce metamodel constraints
43- Validate multiplicity bounds
44- Check referential integrity
45- Report validation errors
46
47### Sync Engine
48- Track change timestamps
49- Detect concurrent modifications
50- Apply conflict resolution policies
51- Maintain audit log
52
53## Data Flow Patterns
54
55### Pattern 1: XMI to JSON Export
56```
571. Java EMF Tool exports XMI
582. Gateway parses XMI to canonical model
593. Validator checks model integrity
604. JSON serialiser flattens references
615. Web Editor receives JSON
62```
63
64### Pattern 2: JSON to XMI Import
65```
661. Web Editor sends modified JSON
672. Gateway parses JSON
683. Validator checks against metamodel
694. Sync Engine detects changes
705. XMI serialiser generates output
716. Java EMF Tool imports XMI
72```
73
74### Pattern 3: Real-time Synchronisation
75```
761. Any client makes change
772. Change event sent to Gateway
783. Conflict detection runs
794. If no conflict: broadcast to all clients
805. If conflict: apply resolution policy
81```
82
83## Synchronisation Strategies
84
85### Strategy A: Last-Write-Wins
86- Simplest approach
87- Based on timestamps
88- May lose concurrent changes
89- Suitable for single-user scenarios
90
91### Strategy B: Merge-on-Conflict
92- Attempts automatic merge
93- Field-level granularity
94- Falls back to manual resolution
95- Better for team collaboration
96
97### Strategy C: Operational Transform
98- Real-time collaboration
99- Transform concurrent operations
100- Preserves all user intent
101- Most complex to implement
102
103## API Endpoints
104
105| Endpoint | Method | Description |
106|----------|--------|-------------|
107| `/api/projects` | GET | List all projects |
108| `/api/projects/{id}` | GET | Get project by ID |
109| `/api/projects` | POST | Create new project |
110| `/api/projects/{id}` | PUT | Update project |
111| `/api/sync/status` | GET | Get sync status |
112| `/api/sync/push` | POST | Push local changes |
113| `/api/sync/pull` | GET | Pull remote changes |
114| `/api/export/xmi` | GET | Export as XMI |
115| `/api/export/json` | GET | Export as JSON |

Section 2

XMI to JSON Bridge

The first integration challenge is converting between XMI (used by Java EMF tools) and JSON (used by web applications). This bridge enables model data to flow between enterprise tools and web interfaces.

XMI contains rich metamodel information and references, while JSON provides simple, web-friendly data structures that are easy to consume in modern web applications.

Step 1

Create sample XMI data from Java EMF.

This XMI file represents project data created by a Java EMF-based tool. It includes complex references and metamodel-specific information typical of enterprise modeling tools.

project-data.xmi
1<?xml version="1.0" encoding="UTF-8"?>
2<!-- Project Management Model from Java EMF Tool -->
3<!-- Created: 2024-01-15 by Enterprise Architect -->
4<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5 xmlns:pm="http://www.example.org/projectmanagement/1.0"
6 xsi:schemaLocation="http://www.example.org/projectmanagement/1.0 ProjectManagement.ecore">
7
8 <pm:Organisation name="Acme Software Solutions" domain="acme.com.au">
9 <!-- Departments -->
10 <departments id="DEPT-ENG" name="Engineering" code="ENG">
11 <manager href="#MEMBER-001"/>
12 </departments>
13 <departments id="DEPT-DESIGN" name="Design" code="DES">
14 <manager href="#MEMBER-003"/>
15 </departments>
16 <departments id="DEPT-QA" name="Quality Assurance" code="QA">
17 <manager href="#MEMBER-005"/>
18 </departments>
19
20 <!-- Team Members -->
21 <members id="MEMBER-001" name="Alice Chen" email="alice.chen@acme.com.au" role="Engineering Lead">
22 <department href="#DEPT-ENG"/>
23 </members>
24 <members id="MEMBER-002" name="Bob Williams" email="bob.williams@acme.com.au" role="Senior Developer">
25 <department href="#DEPT-ENG"/>
26 </members>
27 <members id="MEMBER-003" name="Carol Thompson" email="carol.thompson@acme.com.au" role="Design Lead">
28 <department href="#DEPT-DESIGN"/>
29 </members>
30 <members id="MEMBER-004" name="David Kim" email="david.kim@acme.com.au" role="UX Designer">
31 <department href="#DEPT-DESIGN"/>
32 </members>
33 <members id="MEMBER-005" name="Emma Wilson" email="emma.wilson@acme.com.au" role="QA Lead">
34 <department href="#DEPT-QA"/>
35 </members>
36 <members id="MEMBER-006" name="Frank Brown" email="frank.brown@acme.com.au" role="Test Engineer">
37 <department href="#DEPT-QA"/>
38 </members>
39
40 <!-- Projects -->
41 <projects id="PROJ-001" name="Customer Portal Redesign"
42 description="Modernise the customer-facing portal with improved UX and performance"
43 status="ACTIVE" startDate="2024-01-01" targetEndDate="2024-06-30" budget="250000.00">
44 <owner href="#MEMBER-001"/>
45 <department href="#DEPT-ENG"/>
46 <teamMembers href="#MEMBER-001"/>
47 <teamMembers href="#MEMBER-002"/>
48 <teamMembers href="#MEMBER-003"/>
49 <teamMembers href="#MEMBER-004"/>
50
51 <milestones id="MS-001" name="Design Phase Complete" dueDate="2024-02-15" completed="true">
52 <deliverables href="#TASK-001"/>
53 <deliverables href="#TASK-002"/>
54 </milestones>
55 <milestones id="MS-002" name="Backend API Ready" dueDate="2024-04-01" completed="false">
56 <deliverables href="#TASK-003"/>
57 <deliverables href="#TASK-004"/>
58 </milestones>
59 <milestones id="MS-003" name="Beta Release" dueDate="2024-05-15" completed="false">
60 <deliverables href="#TASK-005"/>
61 </milestones>
62
63 <tasks id="TASK-001" title="Create wireframes"
64 description="Design wireframes for all major user flows"
65 priority="HIGH" status="COMPLETED" estimatedHours="40.0" actualHours="45.0" dueDate="2024-01-31">
66 <assignee href="#MEMBER-004"/>
67 <comments id="CMT-001" text="Initial wireframes completed and approved by stakeholders" timestamp="2024-01-28">
68 <author href="#MEMBER-004"/>
69 </comments>
70 </tasks>
71 <tasks id="TASK-002" title="Design system components"
72 description="Create reusable UI component library based on wireframes"
73 priority="HIGH" status="COMPLETED" estimatedHours="60.0" actualHours="55.0" dueDate="2024-02-15">
74 <assignee href="#MEMBER-003"/>
75 <dependencies href="#TASK-001"/>
76 <comments id="CMT-002" text="Component library ready for development handoff" timestamp="2024-02-14">
77 <author href="#MEMBER-003"/>
78 </comments>
79 </tasks>
80 <tasks id="TASK-003" title="Implement authentication API"
81 description="Build OAuth 2.0 authentication endpoints with MFA support"
82 priority="CRITICAL" status="IN_PROGRESS" estimatedHours="80.0" actualHours="35.0" dueDate="2024-03-15">
83 <assignee href="#MEMBER-002"/>
84 <comments id="CMT-003" text="OAuth flow working, starting MFA integration" timestamp="2024-02-28">
85 <author href="#MEMBER-002"/>
86 </comments>
87 </tasks>
88 <tasks id="TASK-004" title="Implement customer data API"
89 description="RESTful API for customer profile and preferences"
90 priority="HIGH" status="NOT_STARTED" estimatedHours="60.0" dueDate="2024-03-30">
91 <assignee href="#MEMBER-002"/>
92 <dependencies href="#TASK-003"/>
93 </tasks>
94 <tasks id="TASK-005" title="Frontend implementation"
95 description="Implement React frontend with design system components"
96 priority="HIGH" status="NOT_STARTED" estimatedHours="120.0" dueDate="2024-05-01">
97 <assignee href="#MEMBER-001"/>
98 <dependencies href="#TASK-002"/>
99 <dependencies href="#TASK-003"/>
100 <dependencies href="#TASK-004"/>
101 </tasks>
102 </projects>
103
104 <projects id="PROJ-002" name="Mobile App Testing Framework"
105 description="Automated testing framework for iOS and Android applications"
106 status="PLANNING" startDate="2024-03-01" targetEndDate="2024-08-31" budget="150000.00">
107 <owner href="#MEMBER-005"/>
108 <department href="#DEPT-QA"/>
109 <teamMembers href="#MEMBER-005"/>
110 <teamMembers href="#MEMBER-006"/>
111
112 <milestones id="MS-004" name="Framework Architecture" dueDate="2024-03-31" completed="false">
113 <deliverables href="#TASK-006"/>
114 </milestones>
115
116 <tasks id="TASK-006" title="Design test framework architecture"
117 description="Define architecture for cross-platform test automation"
118 priority="HIGH" status="NOT_STARTED" estimatedHours="40.0" dueDate="2024-03-15">
119 <assignee href="#MEMBER-005"/>
120 </tasks>
121 <tasks id="TASK-007" title="Evaluate testing tools"
122 description="Compare XCUITest, Espresso, Appium, and Detox"
123 priority="MEDIUM" status="NOT_STARTED" estimatedHours="24.0" dueDate="2024-03-20">
124 <assignee href="#MEMBER-006"/>
125 </tasks>
126 </projects>
127 </pm:Organisation>
128</xmi:XMI>

Step 2

Design JSON schema for web consumption.

This JSON schema defines a web-friendly representation of project data that’s optimised for REST APIs and JavaScript consumption while preserving essential model information.

project-schema.json
1{
2 "$schema": "http://json-schema.org/draft-07/schema#",
3 "$id": "https://api.acme.com.au/schemas/project-management/v1",
4 "title": "Project Management API Schema",
5 "description": "JSON Schema for web-based project management interface",
6
7 "definitions": {
8 "ProjectStatus": {
9 "type": "string",
10 "enum": ["PLANNING", "ACTIVE", "ON_HOLD", "COMPLETED", "CANCELLED"]
11 },
12
13 "TaskPriority": {
14 "type": "string",
15 "enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
16 },
17
18 "TaskStatus": {
19 "type": "string",
20 "enum": ["NOT_STARTED", "IN_PROGRESS", "BLOCKED", "IN_REVIEW", "COMPLETED"]
21 },
22
23 "Department": {
24 "type": "object",
25 "properties": {
26 "id": { "type": "string", "pattern": "^DEPT-[A-Z0-9]+$" },
27 "name": { "type": "string", "minLength": 1, "maxLength": 100 },
28 "code": { "type": "string", "pattern": "^[A-Z]{2,5}$" },
29 "managerId": { "type": "string" },
30 "memberIds": { "type": "array", "items": { "type": "string" } }
31 },
32 "required": ["id", "name", "code"],
33 "additionalProperties": false
34 },
35
36 "TeamMember": {
37 "type": "object",
38 "properties": {
39 "id": { "type": "string", "pattern": "^MEMBER-[0-9]+$" },
40 "name": { "type": "string", "minLength": 1, "maxLength": 100 },
41 "email": { "type": "string", "format": "email" },
42 "role": { "type": "string" },
43 "departmentId": { "type": "string" },
44 "avatarUrl": { "type": "string", "format": "uri" }
45 },
46 "required": ["id", "name", "email"],
47 "additionalProperties": false
48 },
49
50 "Comment": {
51 "type": "object",
52 "properties": {
53 "id": { "type": "string", "pattern": "^CMT-[0-9]+$" },
54 "text": { "type": "string", "minLength": 1, "maxLength": 4000 },
55 "timestamp": { "type": "string", "format": "date-time" },
56 "authorId": { "type": "string" }
57 },
58 "required": ["id", "text", "timestamp", "authorId"],
59 "additionalProperties": false
60 },
61
62 "Task": {
63 "type": "object",
64 "properties": {
65 "id": { "type": "string", "pattern": "^TASK-[0-9]+$" },
66 "title": { "type": "string", "minLength": 1, "maxLength": 200 },
67 "description": { "type": "string", "maxLength": 4000 },
68 "priority": { "$ref": "#/definitions/TaskPriority" },
69 "status": { "$ref": "#/definitions/TaskStatus" },
70 "estimatedHours": { "type": "number", "minimum": 0 },
71 "actualHours": { "type": "number", "minimum": 0 },
72 "dueDate": { "type": "string", "format": "date" },
73 "assigneeId": { "type": "string" },
74 "dependencyIds": { "type": "array", "items": { "type": "string" } },
75 "comments": { "type": "array", "items": { "$ref": "#/definitions/Comment" } }
76 },
77 "required": ["id", "title", "priority", "status"],
78 "additionalProperties": false
79 },
80
81 "Milestone": {
82 "type": "object",
83 "properties": {
84 "id": { "type": "string", "pattern": "^MS-[0-9]+$" },
85 "name": { "type": "string", "minLength": 1, "maxLength": 100 },
86 "dueDate": { "type": "string", "format": "date" },
87 "completed": { "type": "boolean", "default": false },
88 "deliverableIds": { "type": "array", "items": { "type": "string" } }
89 },
90 "required": ["id", "name", "dueDate"],
91 "additionalProperties": false
92 },
93
94 "Project": {
95 "type": "object",
96 "properties": {
97 "id": { "type": "string", "pattern": "^PROJ-[0-9]+$" },
98 "name": { "type": "string", "minLength": 1, "maxLength": 200 },
99 "description": { "type": "string", "maxLength": 4000 },
100 "status": { "$ref": "#/definitions/ProjectStatus" },
101 "startDate": { "type": "string", "format": "date" },
102 "targetEndDate": { "type": "string", "format": "date" },
103 "budget": { "type": "number", "minimum": 0 },
104 "ownerId": { "type": "string" },
105 "departmentId": { "type": "string" },
106 "teamMemberIds": { "type": "array", "items": { "type": "string" } },
107 "milestones": { "type": "array", "items": { "$ref": "#/definitions/Milestone" } },
108 "tasks": { "type": "array", "items": { "$ref": "#/definitions/Task" } }
109 },
110 "required": ["id", "name", "status"],
111 "additionalProperties": false
112 },
113
114 "Organisation": {
115 "type": "object",
116 "properties": {
117 "name": { "type": "string", "minLength": 1, "maxLength": 200 },
118 "domain": { "type": "string", "format": "hostname" },
119 "departments": { "type": "array", "items": { "$ref": "#/definitions/Department" } },
120 "members": { "type": "array", "items": { "$ref": "#/definitions/TeamMember" } },
121 "projects": { "type": "array", "items": { "$ref": "#/definitions/Project" } }
122 },
123 "required": ["name"],
124 "additionalProperties": false
125 }
126 },
127
128 "type": "object",
129 "$ref": "#/definitions/Organisation"
130}

Step 3

Create XMI-to-JSON transformation.

This ATL transformation converts XMI project data into JSON format, handling reference resolution and adapting the structure for web consumption.

XMI2JSON.atl
1-- ATL Transformation: XMI to JSON Format
2-- Converts Project Management XMI models to web-friendly JSON structure
3-- Part of the Cross-Format Integration tutorial
4-- @path PM=/ProjectManagement/ProjectManagement.ecore
5-- @path JSON=/JSON/JsonModel.ecore
6
7module XMI2JSON;
8create OUT: JSON from IN: PM;
9
10-- ===========================================================================
11-- Helper: Format date as ISO 8601 string
12-- ===========================================================================
13helper context OclAny def: formatDate(): String =
14 if self.oclIsUndefined() then
15 ''
16 else
17 self.toString()
18 endif;
19
20-- ===========================================================================
21-- Helper: Convert ProjectStatus enum to string
22-- ===========================================================================
23helper context PM!ProjectStatus def: toJsonString(): String =
24 if self = #PLANNING then 'PLANNING'
25 else if self = #ACTIVE then 'ACTIVE'
26 else if self = #ON_HOLD then 'ON_HOLD'
27 else if self = #COMPLETED then 'COMPLETED'
28 else if self = #CANCELLED then 'CANCELLED'
29 else 'PLANNING'
30 endif endif endif endif endif;
31
32-- ===========================================================================
33-- Helper: Convert TaskPriority enum to string
34-- ===========================================================================
35helper context PM!TaskPriority def: toJsonString(): String =
36 if self = #LOW then 'LOW'
37 else if self = #MEDIUM then 'MEDIUM'
38 else if self = #HIGH then 'HIGH'
39 else if self = #CRITICAL then 'CRITICAL'
40 else 'MEDIUM'
41 endif endif endif endif;
42
43-- ===========================================================================
44-- Helper: Convert TaskStatus enum to string
45-- ===========================================================================
46helper context PM!TaskStatus def: toJsonString(): String =
47 if self = #NOT_STARTED then 'NOT_STARTED'
48 else if self = #IN_PROGRESS then 'IN_PROGRESS'
49 else if self = #BLOCKED then 'BLOCKED'
50 else if self = #IN_REVIEW then 'IN_REVIEW'
51 else if self = #COMPLETED then 'COMPLETED'
52 else 'NOT_STARTED'
53 endif endif endif endif endif;
54
55-- ===========================================================================
56-- Helper: Get ID reference from element
57-- ===========================================================================
58helper context PM!TeamMember def: idRef(): String =
59 if self.oclIsUndefined() then '' else self.id endif;
60
61helper context PM!Department def: idRef(): String =
62 if self.oclIsUndefined() then '' else self.id endif;
63
64helper context PM!Task def: idRef(): String =
65 if self.oclIsUndefined() then '' else self.id endif;
66
67-- ===========================================================================
68-- Rule: Organisation -> JsonObject (root)
69-- ===========================================================================
70rule Organisation2Json {
71 from
72 org: PM!Organisation
73 to
74 json: JSON!JsonObject (
75 properties <- Sequence{
76 nameProp, domainProp, departmentsProp, membersProp, projectsProp
77 }
78 ),
79 nameProp: JSON!JsonProperty (
80 key <- 'name',
81 value <- nameVal
82 ),
83 nameVal: JSON!JsonString (
84 value <- org.name
85 ),
86 domainProp: JSON!JsonProperty (
87 key <- 'domain',
88 value <- domainVal
89 ),
90 domainVal: JSON!JsonString (
91 value <- org.domain
92 ),
93 departmentsProp: JSON!JsonProperty (
94 key <- 'departments',
95 value <- departmentsArr
96 ),
97 departmentsArr: JSON!JsonArray (
98 elements <- org.departments
99 ),
100 membersProp: JSON!JsonProperty (
101 key <- 'members',
102 value <- membersArr
103 ),
104 membersArr: JSON!JsonArray (
105 elements <- org.members
106 ),
107 projectsProp: JSON!JsonProperty (
108 key <- 'projects',
109 value <- projectsArr
110 ),
111 projectsArr: JSON!JsonArray (
112 elements <- org.projects
113 )
114}
115
116-- ===========================================================================
117-- Rule: Department -> JsonObject
118-- Flattens manager reference to ID
119-- ===========================================================================
120rule Department2Json {
121 from
122 dept: PM!Department
123 to
124 json: JSON!JsonObject (
125 properties <- Sequence{idProp, nameProp, codeProp, managerIdProp, memberIdsProp}
126 ),
127 idProp: JSON!JsonProperty (
128 key <- 'id',
129 value <- idVal
130 ),
131 idVal: JSON!JsonString (
132 value <- dept.id
133 ),
134 nameProp: JSON!JsonProperty (
135 key <- 'name',
136 value <- nameVal
137 ),
138 nameVal: JSON!JsonString (
139 value <- dept.name
140 ),
141 codeProp: JSON!JsonProperty (
142 key <- 'code',
143 value <- codeVal
144 ),
145 codeVal: JSON!JsonString (
146 value <- dept.code
147 ),
148 managerIdProp: JSON!JsonProperty (
149 key <- 'managerId',
150 value <- managerIdVal
151 ),
152 managerIdVal: JSON!JsonString (
153 value <- dept.manager.idRef()
154 ),
155 memberIdsProp: JSON!JsonProperty (
156 key <- 'memberIds',
157 value <- memberIdsArr
158 ),
159 memberIdsArr: JSON!JsonArray (
160 elements <- dept.members->collect(m | thisModule.createIdString(m.id))
161 )
162}
163
164-- ===========================================================================
165-- Rule: TeamMember -> JsonObject
166-- ===========================================================================
167rule TeamMember2Json {
168 from
169 member: PM!TeamMember
170 to
171 json: JSON!JsonObject (
172 properties <- Sequence{idProp, nameProp, emailProp, roleProp, deptIdProp}
173 ),
174 idProp: JSON!JsonProperty (
175 key <- 'id',
176 value <- idVal
177 ),
178 idVal: JSON!JsonString (
179 value <- member.id
180 ),
181 nameProp: JSON!JsonProperty (
182 key <- 'name',
183 value <- nameVal
184 ),
185 nameVal: JSON!JsonString (
186 value <- member.name
187 ),
188 emailProp: JSON!JsonProperty (
189 key <- 'email',
190 value <- emailVal
191 ),
192 emailVal: JSON!JsonString (
193 value <- member.email
194 ),
195 roleProp: JSON!JsonProperty (
196 key <- 'role',
197 value <- roleVal
198 ),
199 roleVal: JSON!JsonString (
200 value <- member.role
201 ),
202 deptIdProp: JSON!JsonProperty (
203 key <- 'departmentId',
204 value <- deptIdVal
205 ),
206 deptIdVal: JSON!JsonString (
207 value <- member.department.idRef()
208 )
209}
210
211-- ===========================================================================
212-- Rule: Project -> JsonObject
213-- ===========================================================================
214rule Project2Json {
215 from
216 proj: PM!Project
217 to
218 json: JSON!JsonObject (
219 properties <- Sequence{
220 idProp, nameProp, descProp, statusProp, startDateProp,
221 endDateProp, budgetProp, ownerIdProp, deptIdProp,
222 teamIdsProp, milestonesProp, tasksProp
223 }
224 ),
225 idProp: JSON!JsonProperty (key <- 'id', value <- idVal),
226 idVal: JSON!JsonString (value <- proj.id),
227 nameProp: JSON!JsonProperty (key <- 'name', value <- nameVal),
228 nameVal: JSON!JsonString (value <- proj.name),
229 descProp: JSON!JsonProperty (key <- 'description', value <- descVal),
230 descVal: JSON!JsonString (value <- proj.description),
231 statusProp: JSON!JsonProperty (key <- 'status', value <- statusVal),
232 statusVal: JSON!JsonString (value <- proj.status.toJsonString()),
233 startDateProp: JSON!JsonProperty (key <- 'startDate', value <- startDateVal),
234 startDateVal: JSON!JsonString (value <- proj.startDate.formatDate()),
235 endDateProp: JSON!JsonProperty (key <- 'targetEndDate', value <- endDateVal),
236 endDateVal: JSON!JsonString (value <- proj.targetEndDate.formatDate()),
237 budgetProp: JSON!JsonProperty (key <- 'budget', value <- budgetVal),
238 budgetVal: JSON!JsonNumber (value <- proj.budget),
239 ownerIdProp: JSON!JsonProperty (key <- 'ownerId', value <- ownerIdVal),
240 ownerIdVal: JSON!JsonString (value <- proj.owner.idRef()),
241 deptIdProp: JSON!JsonProperty (key <- 'departmentId', value <- deptIdVal),
242 deptIdVal: JSON!JsonString (value <- proj.department.idRef()),
243 teamIdsProp: JSON!JsonProperty (key <- 'teamMemberIds', value <- teamIdsArr),
244 teamIdsArr: JSON!JsonArray (
245 elements <- proj.teamMembers->collect(m | thisModule.createIdString(m.id))
246 ),
247 milestonesProp: JSON!JsonProperty (key <- 'milestones', value <- milestonesArr),
248 milestonesArr: JSON!JsonArray (elements <- proj.milestones),
249 tasksProp: JSON!JsonProperty (key <- 'tasks', value <- tasksArr),
250 tasksArr: JSON!JsonArray (elements <- proj.tasks)
251}
252
253-- ===========================================================================
254-- Rule: Milestone -> JsonObject
255-- ===========================================================================
256rule Milestone2Json {
257 from
258 ms: PM!Milestone
259 to
260 json: JSON!JsonObject (
261 properties <- Sequence{idProp, nameProp, dueDateProp, completedProp, deliverableIdsProp}
262 ),
263 idProp: JSON!JsonProperty (key <- 'id', value <- idVal),
264 idVal: JSON!JsonString (value <- ms.id),
265 nameProp: JSON!JsonProperty (key <- 'name', value <- nameVal),
266 nameVal: JSON!JsonString (value <- ms.name),
267 dueDateProp: JSON!JsonProperty (key <- 'dueDate', value <- dueDateVal),
268 dueDateVal: JSON!JsonString (value <- ms.dueDate.formatDate()),
269 completedProp: JSON!JsonProperty (key <- 'completed', value <- completedVal),
270 completedVal: JSON!JsonBoolean (value <- ms.completed),
271 deliverableIdsProp: JSON!JsonProperty (key <- 'deliverableIds', value <- deliverableIdsArr),
272 deliverableIdsArr: JSON!JsonArray (
273 elements <- ms.deliverables->collect(t | thisModule.createIdString(t.id))
274 )
275}
276
277-- ===========================================================================
278-- Rule: Task -> JsonObject
279-- ===========================================================================
280rule Task2Json {
281 from
282 task: PM!Task
283 to
284 json: JSON!JsonObject (
285 properties <- Sequence{
286 idProp, titleProp, descProp, priorityProp, statusProp,
287 estHoursProp, actHoursProp, dueDateProp, assigneeIdProp,
288 depIdsProp, commentsProp
289 }
290 ),
291 idProp: JSON!JsonProperty (key <- 'id', value <- idVal),
292 idVal: JSON!JsonString (value <- task.id),
293 titleProp: JSON!JsonProperty (key <- 'title', value <- titleVal),
294 titleVal: JSON!JsonString (value <- task.title),
295 descProp: JSON!JsonProperty (key <- 'description', value <- descVal),
296 descVal: JSON!JsonString (value <- task.description),
297 priorityProp: JSON!JsonProperty (key <- 'priority', value <- priorityVal),
298 priorityVal: JSON!JsonString (value <- task.priority.toJsonString()),
299 statusProp: JSON!JsonProperty (key <- 'status', value <- statusVal),
300 statusVal: JSON!JsonString (value <- task.status.toJsonString()),
301 estHoursProp: JSON!JsonProperty (key <- 'estimatedHours', value <- estHoursVal),
302 estHoursVal: JSON!JsonNumber (value <- task.estimatedHours),
303 actHoursProp: JSON!JsonProperty (key <- 'actualHours', value <- actHoursVal),
304 actHoursVal: JSON!JsonNumber (value <- task.actualHours),
305 dueDateProp: JSON!JsonProperty (key <- 'dueDate', value <- dueDateVal),
306 dueDateVal: JSON!JsonString (value <- task.dueDate.formatDate()),
307 assigneeIdProp: JSON!JsonProperty (key <- 'assigneeId', value <- assigneeIdVal),
308 assigneeIdVal: JSON!JsonString (value <- task.assignee.idRef()),
309 depIdsProp: JSON!JsonProperty (key <- 'dependencyIds', value <- depIdsArr),
310 depIdsArr: JSON!JsonArray (
311 elements <- task.dependencies->collect(t | thisModule.createIdString(t.id))
312 ),
313 commentsProp: JSON!JsonProperty (key <- 'comments', value <- commentsArr),
314 commentsArr: JSON!JsonArray (elements <- task.comments)
315}
316
317-- ===========================================================================
318-- Rule: Comment -> JsonObject
319-- ===========================================================================
320rule Comment2Json {
321 from
322 cmt: PM!Comment
323 to
324 json: JSON!JsonObject (
325 properties <- Sequence{idProp, textProp, timestampProp, authorIdProp}
326 ),
327 idProp: JSON!JsonProperty (key <- 'id', value <- idVal),
328 idVal: JSON!JsonString (value <- cmt.id),
329 textProp: JSON!JsonProperty (key <- 'text', value <- textVal),
330 textVal: JSON!JsonString (value <- cmt.text),
331 timestampProp: JSON!JsonProperty (key <- 'timestamp', value <- timestampVal),
332 timestampVal: JSON!JsonString (value <- cmt.timestamp.formatDate()),
333 authorIdProp: JSON!JsonProperty (key <- 'authorId', value <- authorIdVal),
334 authorIdVal: JSON!JsonString (value <- cmt.author.idRef())
335}
336
337-- ===========================================================================
338-- Called Rule: Create ID string reference
339-- ===========================================================================
340rule createIdString(id: String) {
341 to
342 s: JSON!JsonString (
343 value <- id
344 )
345 do {
346 s;
347 }
348}

Step 4

Execute the XMI-to-JSON conversion.

Run the transformation to convert XMI data to JSON format, demonstrating how model data can flow from enterprise tools to web applications.

Terminal
1# Run the XMI to JSON transformation
2# Converts Java EMF project data to web-friendly JSON format
3
4# Execute the ATL transformation
5swift-atl transform XMI2JSON.atl \
6 --source project-data.xmi \
7 --source-metamodel ProjectManagement.ecore \
8 --target project-data.json \
9 --target-metamodel JsonModel.ecore \
10 --verbose
11
12# Output:
13# [INFO] Loading source metamodel: ProjectManagement.ecore
14# [INFO] Loading target metamodel: JsonModel.ecore
15# [INFO] Loading source model: project-data.xmi
16# [INFO] Compiling transformation: XMI2JSON.atl
17# [INFO] Executing transformation...
18#
19# [PROGRESS] Processing Organisation: 1
20# [PROGRESS] Processing Department: 3
21# [PROGRESS] Processing TeamMember: 6
22# [PROGRESS] Processing Project: 2
23# [PROGRESS] Processing Milestone: 4
24# [PROGRESS] Processing Task: 7
25# [PROGRESS] Processing Comment: 3
26#
27# [INFO] Created JSON elements:
28# - Root object: 1
29# - Nested objects: 25
30# - Arrays: 18
31# - Properties: 142
32#
33# [INFO] Transformation completed in 0.23 seconds
34# [INFO] Output saved to: project-data.json
35
36# Validate the JSON output against schema
37swift-json validate project-data.json \
38 --schema project-schema.json
39
40# Output:
41# Validation successful. JSON conforms to schema.
42
43# Pretty-print a sample of the output
44swift-json query project-data.json \
45 --path '$.projects[0]' \
46 --format pretty
47
48# Output:
49# {
50# "id": "PROJ-001",
51# "name": "Customer Portal Redesign",
52# "description": "Modernise the customer-facing portal...",
53# "status": "ACTIVE",
54# "startDate": "2024-01-01",
55# "targetEndDate": "2024-06-30",
56# "budget": 250000.0,
57# "ownerId": "MEMBER-001",
58# "departmentId": "DEPT-ENG",
59# "teamMemberIds": ["MEMBER-001", "MEMBER-002", "MEMBER-003", "MEMBER-004"],
60# "milestones": [...],
61# "tasks": [...]
62# }
63
64# Generate statistics
65echo "Conversion Statistics:"
66swift-json stats project-data.json
67
68# Output:
69# Total objects: 26
70# Total arrays: 18
71# Total properties: 142
72# File size: 12.4 KB (vs 18.2 KB XMI = 32% reduction)

Section 3

JSON to Swift Integration

Web applications often modify model data through JSON APIs. These changes need to be integrated back into the Swift modeling environment and potentially synchronized with other tools.

This integration enables web-based model editing while maintaining the benefits of strong typing and validation available in Swift Modelling tools.

Step 1

Create modified JSON data from web interface.

This JSON represents project data modified through a web interface. It includes new projects, updated tasks, and changed relationships that need to be integrated.

updated-project-data.json
1{
2 "name": "Acme Software Solutions",
3 "domain": "acme.com.au",
4 "departments": [
5 {
6 "id": "DEPT-ENG",
7 "name": "Engineering",
8 "code": "ENG",
9 "managerId": "MEMBER-001",
10 "memberIds": ["MEMBER-001", "MEMBER-002"]
11 },
12 {
13 "id": "DEPT-DESIGN",
14 "name": "Design",
15 "code": "DES",
16 "managerId": "MEMBER-003",
17 "memberIds": ["MEMBER-003", "MEMBER-004"]
18 },
19 {
20 "id": "DEPT-QA",
21 "name": "Quality Assurance",
22 "code": "QA",
23 "managerId": "MEMBER-005",
24 "memberIds": ["MEMBER-005", "MEMBER-006"]
25 }
26 ],
27 "members": [
28 {
29 "id": "MEMBER-001",
30 "name": "Alice Chen",
31 "email": "alice.chen@acme.com.au",
32 "role": "Engineering Lead",
33 "departmentId": "DEPT-ENG"
34 },
35 {
36 "id": "MEMBER-002",
37 "name": "Bob Williams",
38 "email": "bob.williams@acme.com.au",
39 "role": "Senior Developer",
40 "departmentId": "DEPT-ENG"
41 },
42 {
43 "id": "MEMBER-003",
44 "name": "Carol Thompson",
45 "email": "carol.thompson@acme.com.au",
46 "role": "Design Lead",
47 "departmentId": "DEPT-DESIGN"
48 },
49 {
50 "id": "MEMBER-004",
51 "name": "David Kim",
52 "email": "david.kim@acme.com.au",
53 "role": "UX Designer",
54 "departmentId": "DEPT-DESIGN"
55 },
56 {
57 "id": "MEMBER-005",
58 "name": "Emma Wilson",
59 "email": "emma.wilson@acme.com.au",
60 "role": "QA Lead",
61 "departmentId": "DEPT-QA"
62 },
63 {
64 "id": "MEMBER-006",
65 "name": "Frank Brown",
66 "email": "frank.brown@acme.com.au",
67 "role": "Test Engineer",
68 "departmentId": "DEPT-QA"
69 },
70 {
71 "id": "MEMBER-007",
72 "name": "Grace Lee",
73 "email": "grace.lee@acme.com.au",
74 "role": "Junior Developer",
75 "departmentId": "DEPT-ENG"
76 }
77 ],
78 "projects": [
79 {
80 "id": "PROJ-001",
81 "name": "Customer Portal Redesign",
82 "description": "Modernise the customer-facing portal with improved UX and performance",
83 "status": "ACTIVE",
84 "startDate": "2024-01-01",
85 "targetEndDate": "2024-06-30",
86 "budget": 250000.00,
87 "ownerId": "MEMBER-001",
88 "departmentId": "DEPT-ENG",
89 "teamMemberIds": ["MEMBER-001", "MEMBER-002", "MEMBER-003", "MEMBER-004", "MEMBER-007"],
90 "milestones": [
91 {
92 "id": "MS-001",
93 "name": "Design Phase Complete",
94 "dueDate": "2024-02-15",
95 "completed": true,
96 "deliverableIds": ["TASK-001", "TASK-002"]
97 },
98 {
99 "id": "MS-002",
100 "name": "Backend API Ready",
101 "dueDate": "2024-04-01",
102 "completed": false,
103 "deliverableIds": ["TASK-003", "TASK-004"]
104 },
105 {
106 "id": "MS-003",
107 "name": "Beta Release",
108 "dueDate": "2024-05-15",
109 "completed": false,
110 "deliverableIds": ["TASK-005"]
111 }
112 ],
113 "tasks": [
114 {
115 "id": "TASK-001",
116 "title": "Create wireframes",
117 "description": "Design wireframes for all major user flows",
118 "priority": "HIGH",
119 "status": "COMPLETED",
120 "estimatedHours": 40.0,
121 "actualHours": 45.0,
122 "dueDate": "2024-01-31",
123 "assigneeId": "MEMBER-004",
124 "dependencyIds": [],
125 "comments": [
126 {
127 "id": "CMT-001",
128 "text": "Initial wireframes completed and approved by stakeholders",
129 "timestamp": "2024-01-28T14:30:00Z",
130 "authorId": "MEMBER-004"
131 }
132 ]
133 },
134 {
135 "id": "TASK-002",
136 "title": "Design system components",
137 "description": "Create reusable UI component library based on wireframes",
138 "priority": "HIGH",
139 "status": "COMPLETED",
140 "estimatedHours": 60.0,
141 "actualHours": 55.0,
142 "dueDate": "2024-02-15",
143 "assigneeId": "MEMBER-003",
144 "dependencyIds": ["TASK-001"],
145 "comments": [
146 {
147 "id": "CMT-002",
148 "text": "Component library ready for development handoff",
149 "timestamp": "2024-02-14T16:45:00Z",
150 "authorId": "MEMBER-003"
151 }
152 ]
153 },
154 {
155 "id": "TASK-003",
156 "title": "Implement authentication API",
157 "description": "Build OAuth 2.0 authentication endpoints with MFA support",
158 "priority": "CRITICAL",
159 "status": "COMPLETED",
160 "estimatedHours": 80.0,
161 "actualHours": 75.0,
162 "dueDate": "2024-03-15",
163 "assigneeId": "MEMBER-002",
164 "dependencyIds": [],
165 "comments": [
166 {
167 "id": "CMT-003",
168 "text": "OAuth flow working, starting MFA integration",
169 "timestamp": "2024-02-28T10:00:00Z",
170 "authorId": "MEMBER-002"
171 },
172 {
173 "id": "CMT-004",
174 "text": "MFA integration complete. Ready for review.",
175 "timestamp": "2024-03-10T15:20:00Z",
176 "authorId": "MEMBER-002"
177 }
178 ]
179 },
180 {
181 "id": "TASK-004",
182 "title": "Implement customer data API",
183 "description": "RESTful API for customer profile and preferences",
184 "priority": "HIGH",
185 "status": "IN_PROGRESS",
186 "estimatedHours": 60.0,
187 "actualHours": 25.0,
188 "dueDate": "2024-03-30",
189 "assigneeId": "MEMBER-002",
190 "dependencyIds": ["TASK-003"],
191 "comments": [
192 {
193 "id": "CMT-005",
194 "text": "Started implementation. Profile endpoints 50% complete.",
195 "timestamp": "2024-03-12T09:15:00Z",
196 "authorId": "MEMBER-002"
197 }
198 ]
199 },
200 {
201 "id": "TASK-005",
202 "title": "Frontend implementation",
203 "description": "Implement React frontend with design system components",
204 "priority": "HIGH",
205 "status": "IN_PROGRESS",
206 "estimatedHours": 120.0,
207 "actualHours": 40.0,
208 "dueDate": "2024-05-01",
209 "assigneeId": "MEMBER-001",
210 "dependencyIds": ["TASK-002", "TASK-003", "TASK-004"],
211 "comments": [
212 {
213 "id": "CMT-006",
214 "text": "Started early work on UI shell and routing",
215 "timestamp": "2024-03-05T11:30:00Z",
216 "authorId": "MEMBER-001"
217 }
218 ]
219 },
220 {
221 "id": "TASK-008",
222 "title": "Setup CI/CD pipeline",
223 "description": "Configure GitHub Actions for automated testing and deployment",
224 "priority": "MEDIUM",
225 "status": "NOT_STARTED",
226 "estimatedHours": 16.0,
227 "dueDate": "2024-03-20",
228 "assigneeId": "MEMBER-007",
229 "dependencyIds": [],
230 "comments": []
231 }
232 ]
233 },
234 {
235 "id": "PROJ-002",
236 "name": "Mobile App Testing Framework",
237 "description": "Automated testing framework for iOS and Android applications",
238 "status": "ACTIVE",
239 "startDate": "2024-03-01",
240 "targetEndDate": "2024-08-31",
241 "budget": 150000.00,
242 "ownerId": "MEMBER-005",
243 "departmentId": "DEPT-QA",
244 "teamMemberIds": ["MEMBER-005", "MEMBER-006"],
245 "milestones": [
246 {
247 "id": "MS-004",
248 "name": "Framework Architecture",
249 "dueDate": "2024-03-31",
250 "completed": true,
251 "deliverableIds": ["TASK-006"]
252 },
253 {
254 "id": "MS-005",
255 "name": "Core Framework Implementation",
256 "dueDate": "2024-05-31",
257 "completed": false,
258 "deliverableIds": ["TASK-009"]
259 }
260 ],
261 "tasks": [
262 {
263 "id": "TASK-006",
264 "title": "Design test framework architecture",
265 "description": "Define architecture for cross-platform test automation",
266 "priority": "HIGH",
267 "status": "COMPLETED",
268 "estimatedHours": 40.0,
269 "actualHours": 38.0,
270 "dueDate": "2024-03-15",
271 "assigneeId": "MEMBER-005",
272 "dependencyIds": [],
273 "comments": [
274 {
275 "id": "CMT-007",
276 "text": "Architecture document approved. Using XCUITest and Espresso with shared test specifications.",
277 "timestamp": "2024-03-14T13:00:00Z",
278 "authorId": "MEMBER-005"
279 }
280 ]
281 },
282 {
283 "id": "TASK-007",
284 "title": "Evaluate testing tools",
285 "description": "Compare XCUITest, Espresso, Appium, and Detox",
286 "priority": "MEDIUM",
287 "status": "COMPLETED",
288 "estimatedHours": 24.0,
289 "actualHours": 20.0,
290 "dueDate": "2024-03-20",
291 "assigneeId": "MEMBER-006",
292 "dependencyIds": [],
293 "comments": [
294 {
295 "id": "CMT-008",
296 "text": "Evaluation complete. Recommendation: native tools (XCUITest/Espresso) for reliability.",
297 "timestamp": "2024-03-18T16:30:00Z",
298 "authorId": "MEMBER-006"
299 }
300 ]
301 },
302 {
303 "id": "TASK-009",
304 "title": "Implement core test runner",
305 "description": "Build the cross-platform test runner with reporting capabilities",
306 "priority": "HIGH",
307 "status": "IN_PROGRESS",
308 "estimatedHours": 80.0,
309 "actualHours": 15.0,
310 "dueDate": "2024-05-15",
311 "assigneeId": "MEMBER-005",
312 "dependencyIds": ["TASK-006", "TASK-007"],
313 "comments": []
314 }
315 ]
316 }
317 ]
318}

Step 2

Create JSON-to-XMI reverse transformation.

This transformation converts JSON data back to XMI format, handling data validation and ensuring the result conforms to the original metamodel constraints.

JSON2XMI.atl
1-- ATL Transformation: JSON to XMI Format
2-- Converts web JSON data back to Project Management XMI format
3-- Part of the Cross-Format Integration tutorial
4-- @path JSON=/JSON/JsonModel.ecore
5-- @path PM=/ProjectManagement/ProjectManagement.ecore
6
7module JSON2XMI;
8create OUT: PM from IN: JSON;
9
10-- ===========================================================================
11-- Helpers: Extract property values from JSON objects
12-- ===========================================================================
13helper context JSON!JsonObject def: getProperty(key: String): JSON!JsonValue =
14 self.properties->any(p | p.key = key).value;
15
16helper context JSON!JsonObject def: getString(key: String): String =
17 let prop: JSON!JsonProperty = self.properties->any(p | p.key = key) in
18 if prop.oclIsUndefined() then ''
19 else prop.value.oclAsType(JSON!JsonString).value
20 endif;
21
22helper context JSON!JsonObject def: getNumber(key: String): Real =
23 let prop: JSON!JsonProperty = self.properties->any(p | p.key = key) in
24 if prop.oclIsUndefined() then 0.0
25 else prop.value.oclAsType(JSON!JsonNumber).value
26 endif;
27
28helper context JSON!JsonObject def: getBoolean(key: String): Boolean =
29 let prop: JSON!JsonProperty = self.properties->any(p | p.key = key) in
30 if prop.oclIsUndefined() then false
31 else prop.value.oclAsType(JSON!JsonBoolean).value
32 endif;
33
34helper context JSON!JsonObject def: getArray(key: String): Sequence(JSON!JsonValue) =
35 let prop: JSON!JsonProperty = self.properties->any(p | p.key = key) in
36 if prop.oclIsUndefined() then Sequence{}
37 else prop.value.oclAsType(JSON!JsonArray).elements
38 endif;
39
40-- ===========================================================================
41-- Helpers: Parse string values to enums
42-- ===========================================================================
43helper def: parseProjectStatus(s: String): PM!ProjectStatus =
44 if s = 'PLANNING' then #PLANNING
45 else if s = 'ACTIVE' then #ACTIVE
46 else if s = 'ON_HOLD' then #ON_HOLD
47 else if s = 'COMPLETED' then #COMPLETED
48 else if s = 'CANCELLED' then #CANCELLED
49 else #PLANNING
50 endif endif endif endif endif;
51
52helper def: parseTaskPriority(s: String): PM!TaskPriority =
53 if s = 'LOW' then #LOW
54 else if s = 'MEDIUM' then #MEDIUM
55 else if s = 'HIGH' then #HIGH
56 else if s = 'CRITICAL' then #CRITICAL
57 else #MEDIUM
58 endif endif endif endif;
59
60helper def: parseTaskStatus(s: String): PM!TaskStatus =
61 if s = 'NOT_STARTED' then #NOT_STARTED
62 else if s = 'IN_PROGRESS' then #IN_PROGRESS
63 else if s = 'BLOCKED' then #BLOCKED
64 else if s = 'IN_REVIEW' then #IN_REVIEW
65 else if s = 'COMPLETED' then #COMPLETED
66 else #NOT_STARTED
67 endif endif endif endif endif;
68
69-- ===========================================================================
70-- Helper: Parse ISO date string to Date
71-- ===========================================================================
72helper def: parseDate(s: String): OclAny =
73 if s.oclIsUndefined() or s = '' then OclUndefined
74 else s.toDate()
75 endif;
76
77-- ===========================================================================
78-- Helper: Lookup elements by ID for reference resolution
79-- ===========================================================================
80helper def: allMembers: Map(String, JSON!JsonObject) =
81 JSON!JsonObject.allInstances()
82 ->select(o | o.properties->exists(p | p.key = 'email'))
83 ->iterate(m; acc: Map(String, JSON!JsonObject) = Map{} |
84 acc.including(m.getString('id'), m)
85 );
86
87helper def: allDepartments: Map(String, JSON!JsonObject) =
88 JSON!JsonObject.allInstances()
89 ->select(o | o.properties->exists(p | p.key = 'code') and
90 o.properties->exists(p | p.key = 'managerId'))
91 ->iterate(d; acc: Map(String, JSON!JsonObject) = Map{} |
92 acc.including(d.getString('id'), d)
93 );
94
95helper def: allTasks: Map(String, JSON!JsonObject) =
96 JSON!JsonObject.allInstances()
97 ->select(o | o.properties->exists(p | p.key = 'title') and
98 o.properties->exists(p | p.key = 'priority'))
99 ->iterate(t; acc: Map(String, JSON!JsonObject) = Map{} |
100 acc.including(t.getString('id'), t)
101 );
102
103-- ===========================================================================
104-- Rule: Root Organisation
105-- ===========================================================================
106rule Organisation {
107 from
108 json: JSON!JsonObject (
109 json.properties->exists(p | p.key = 'domain') and
110 json.properties->exists(p | p.key = 'departments')
111 )
112 to
113 org: PM!Organisation (
114 name <- json.getString('name'),
115 domain <- json.getString('domain'),
116 departments <- json.getArray('departments')
117 ->collect(d | thisModule.Department(d.oclAsType(JSON!JsonObject))),
118 members <- json.getArray('members')
119 ->collect(m | thisModule.TeamMember(m.oclAsType(JSON!JsonObject))),
120 projects <- json.getArray('projects')
121 ->collect(p | thisModule.Project(p.oclAsType(JSON!JsonObject)))
122 )
123}
124
125-- ===========================================================================
126-- Lazy Rule: Department
127-- ===========================================================================
128lazy rule Department {
129 from
130 json: JSON!JsonObject
131 to
132 dept: PM!Department (
133 id <- json.getString('id'),
134 name <- json.getString('name'),
135 code <- json.getString('code')
136 )
137}
138
139-- ===========================================================================
140-- Lazy Rule: TeamMember
141-- ===========================================================================
142lazy rule TeamMember {
143 from
144 json: JSON!JsonObject
145 to
146 member: PM!TeamMember (
147 id <- json.getString('id'),
148 name <- json.getString('name'),
149 email <- json.getString('email'),
150 role <- json.getString('role')
151 )
152}
153
154-- ===========================================================================
155-- Lazy Rule: Project
156-- ===========================================================================
157lazy rule Project {
158 from
159 json: JSON!JsonObject
160 to
161 proj: PM!Project (
162 id <- json.getString('id'),
163 name <- json.getString('name'),
164 description <- json.getString('description'),
165 status <- thisModule.parseProjectStatus(json.getString('status')),
166 startDate <- thisModule.parseDate(json.getString('startDate')),
167 targetEndDate <- thisModule.parseDate(json.getString('targetEndDate')),
168 budget <- json.getNumber('budget'),
169 milestones <- json.getArray('milestones')
170 ->collect(m | thisModule.Milestone(m.oclAsType(JSON!JsonObject))),
171 tasks <- json.getArray('tasks')
172 ->collect(t | thisModule.Task(t.oclAsType(JSON!JsonObject)))
173 )
174}
175
176-- ===========================================================================
177-- Lazy Rule: Milestone
178-- ===========================================================================
179lazy rule Milestone {
180 from
181 json: JSON!JsonObject
182 to
183 ms: PM!Milestone (
184 id <- json.getString('id'),
185 name <- json.getString('name'),
186 dueDate <- thisModule.parseDate(json.getString('dueDate')),
187 completed <- json.getBoolean('completed')
188 )
189}
190
191-- ===========================================================================
192-- Lazy Rule: Task
193-- ===========================================================================
194lazy rule Task {
195 from
196 json: JSON!JsonObject
197 to
198 task: PM!Task (
199 id <- json.getString('id'),
200 title <- json.getString('title'),
201 description <- json.getString('description'),
202 priority <- thisModule.parseTaskPriority(json.getString('priority')),
203 status <- thisModule.parseTaskStatus(json.getString('status')),
204 estimatedHours <- json.getNumber('estimatedHours'),
205 actualHours <- json.getNumber('actualHours'),
206 dueDate <- thisModule.parseDate(json.getString('dueDate')),
207 comments <- json.getArray('comments')
208 ->collect(c | thisModule.Comment(c.oclAsType(JSON!JsonObject)))
209 )
210}
211
212-- ===========================================================================
213-- Lazy Rule: Comment
214-- ===========================================================================
215lazy rule Comment {
216 from
217 json: JSON!JsonObject
218 to
219 cmt: PM!Comment (
220 id <- json.getString('id'),
221 text <- json.getString('text'),
222 timestamp <- thisModule.parseDate(json.getString('timestamp'))
223 )
224}
225
226-- ===========================================================================
227-- Endpoint: Resolve references after main transformation
228-- ===========================================================================
229endpoint def: resolveReferences() : OclAny =
230 -- Resolve Department.manager references
231 PM!Department.allInstances()->collect(d |
232 let jsonDept: JSON!JsonObject = thisModule.allDepartments.get(d.id) in
233 let managerId: String = jsonDept.getString('managerId') in
234 d.manager <- PM!TeamMember.allInstances()->any(m | m.id = managerId)
235 );
236
237 -- Resolve TeamMember.department references
238 PM!TeamMember.allInstances()->collect(m |
239 let jsonMember: JSON!JsonObject = thisModule.allMembers.get(m.id) in
240 let deptId: String = jsonMember.getString('departmentId') in
241 m.department <- PM!Department.allInstances()->any(d | d.id = deptId)
242 );
243
244 -- Resolve Project references
245 PM!Project.allInstances()->collect(p |
246 let jsonProj: JSON!JsonObject = JSON!JsonObject.allInstances()
247 ->any(o | o.getString('id') = p.id and o.properties->exists(pr | pr.key = 'ownerId')) in
248 p.owner <- PM!TeamMember.allInstances()->any(m | m.id = jsonProj.getString('ownerId'));
249 p.department <- PM!Department.allInstances()->any(d | d.id = jsonProj.getString('departmentId'));
250 p.teamMembers <- jsonProj.getArray('teamMemberIds')
251 ->collect(id | PM!TeamMember.allInstances()->any(m | m.id = id.oclAsType(JSON!JsonString).value))
252 );
253
254 -- Resolve Task.assignee and Task.dependencies
255 PM!Task.allInstances()->collect(t |
256 let jsonTask: JSON!JsonObject = thisModule.allTasks.get(t.id) in
257 t.assignee <- PM!TeamMember.allInstances()->any(m | m.id = jsonTask.getString('assigneeId'));
258 t.dependencies <- jsonTask.getArray('dependencyIds')
259 ->collect(id | PM!Task.allInstances()->any(task | task.id = id.oclAsType(JSON!JsonString).value))
260 );
261
262 -- Resolve Comment.author
263 PM!Comment.allInstances()->collect(c |
264 let jsonComments: Sequence(JSON!JsonObject) = JSON!JsonObject.allInstances()
265 ->select(o | o.properties->exists(p | p.key = 'authorId')) in
266 let jsonCmt: JSON!JsonObject = jsonComments->any(o | o.getString('id') = c.id) in
267 c.author <- PM!TeamMember.allInstances()->any(m | m.id = jsonCmt.getString('authorId'))
268 );
269
270 -- Resolve Milestone.deliverables
271 PM!Milestone.allInstances()->collect(ms |
272 let jsonMilestones: Sequence(JSON!JsonObject) = JSON!JsonObject.allInstances()
273 ->select(o | o.properties->exists(p | p.key = 'deliverableIds')) in
274 let jsonMs: JSON!JsonObject = jsonMilestones->any(o | o.getString('id') = ms.id) in
275 ms.deliverables <- jsonMs.getArray('deliverableIds')
276 ->collect(id | PM!Task.allInstances()->any(t | t.id = id.oclAsType(JSON!JsonString).value))
277 );
278
279 OclUndefined;

Step 3

Generate Swift data structures.

MTL templates generate Swift data structures from the model, creating native Swift classes that provide type safety and integrate seamlessly with Swift applications.

GenerateSwiftModels.mtl
1[comment encoding = UTF-8 /]
2[comment]
3 MTL Template: Generate Swift Models from Ecore Metamodel
4 Creates type-safe Swift structures with Codable conformance
5 Part of the Cross-Format Integration tutorial
6[/comment]
7
8[module GenerateSwiftModels('http://www.eclipse.org/emf/2002/Ecore')]
9
10[comment Main entry point /]
11[template public generate(pkg : EPackage)]
12[comment Generate all enum types /]
13[for (enum : EEnum | pkg.eClassifiers->filter(EEnum))]
14[file (enum.name.concat('.swift'), false, 'UTF-8')]
15[generateEnum(enum)/]
16[/file]
17[/for]
18
19[comment Generate all class types /]
20[for (cls : EClass | pkg.eClassifiers->filter(EClass))]
21[file (cls.name.concat('.swift'), false, 'UTF-8')]
22[generateClass(cls)/]
23[/file]
24[/for]
25
26[comment Generate main models file /]
27[file ('ProjectManagementModels.swift', false, 'UTF-8')]
28[generateModelsFile(pkg)/]
29[/file]
30[/template]
31
32[comment Generate Swift enum from EEnum /]
33[template private generateEnum(enum : EEnum)]
34// [enum.name/].swift
35// Generated from ProjectManagement.ecore
36// Do not edit manually - changes will be overwritten
37
38import Foundation
39
40/// [enum.name/] enumeration
41/// Maps to XMI enum and JSON string values
42public enum [enum.name/]: String, Codable, CaseIterable, Sendable {
43[for (literal : EEnumLiteral | enum.eLiterals)]
44 case [literal.name.toLowerFirst()/] = "[literal.name/]"
45[/for]
46
47 /// Human-readable display name
48 public var displayName: String {
49 switch self {
50[for (literal : EEnumLiteral | enum.eLiterals)]
51 case .[literal.name.toLowerFirst()/]:
52 return "[literal.name.replace('_', ' ').toLowerFirst().toUpperFirst()/]"
53[/for]
54 }
55 }
56}
57[/template]
58
59[comment Generate Swift struct from EClass /]
60[template private generateClass(cls : EClass)]
61// [cls.name/].swift
62// Generated from ProjectManagement.ecore
63// Do not edit manually - changes will be overwritten
64
65import Foundation
66
67/// [cls.name/] model
68/// Represents [cls.name.toLowerFirst()/] data from XMI/JSON sources
69public struct [cls.name/]: Identifiable, Codable, Equatable, Sendable {
70[comment Generate properties /]
71[for (attr : EAttribute | cls.eAllAttributes)]
72 [generateAttribute(attr)/]
73[/for]
74[for (ref : EReference | cls.eAllReferences->select(r | not r.containment))]
75 [generateReference(ref)/]
76[/for]
77[for (ref : EReference | cls.eAllReferences->select(r | r.containment))]
78 [generateContainment(ref)/]
79[/for]
80
81 // MARK: - Coding Keys
82
83 private enum CodingKeys: String, CodingKey {
84[for (attr : EAttribute | cls.eAllAttributes)]
85 case [attr.name.toLowerCamel()/][if (attr.name <> attr.name.toLowerCamel())] = "[attr.name/]"[/if]
86[/for]
87[for (ref : EReference | cls.eAllReferences->select(r | not r.containment))]
88 case [ref.name.toLowerCamel()/]Id[if (ref.upperBound <> 1)]s[/if] = "[ref.name/]Id[if (ref.upperBound <> 1)]s[/if]"
89[/for]
90[for (ref : EReference | cls.eAllReferences->select(r | r.containment))]
91 case [ref.name.toLowerCamel()/]
92[/for]
93 }
94
95 // MARK: - Initialiser
96
97 public init(
98[for (attr : EAttribute | cls.eAllAttributes) separator(',\n')]
99 [attr.name.toLowerCamel()/]: [attr.swiftType()/][if (not attr.required)] = [attr.swiftDefault()/][/if][/for][if (cls.eAllReferences->notEmpty())],[/if]
100[for (ref : EReference | cls.eAllReferences) separator(',\n')]
101 [ref.name.toLowerCamel()/][if (not ref.containment)]Id[if (ref.upperBound <> 1)]s[/if][/if]: [ref.swiftType()/][if (not ref.required)] = [ref.swiftDefault()/][/if][/for]
102 ) {
103[for (attr : EAttribute | cls.eAllAttributes)]
104 self.[attr.name.toLowerCamel()/] = [attr.name.toLowerCamel()/]
105[/for]
106[for (ref : EReference | cls.eAllReferences)]
107 self.[ref.name.toLowerCamel()/][if (not ref.containment)]Id[if (ref.upperBound <> 1)]s[/if][/if] = [ref.name.toLowerCamel()/][if (not ref.containment)]Id[if (ref.upperBound <> 1)]s[/if][/if]
108[/for]
109 }
110}
111
112[if (cls.name = 'Organisation')]
113// MARK: - Convenience Extensions
114
115extension [cls.name/] {
116 /// Find a member by ID
117 public func member(withId id: String) -> TeamMember? {
118 members.first { $0.id == id }
119 }
120
121 /// Find a department by ID
122 public func department(withId id: String) -> Department? {
123 departments.first { $0.id == id }
124 }
125
126 /// Find a project by ID
127 public func project(withId id: String) -> Project? {
128 projects.first { $0.id == id }
129 }
130}
131[/if]
132[/template]
133
134[comment Generate attribute property /]
135[template private generateAttribute(attr : EAttribute)]
136 /// [attr.name.toUpperFirst()/] attribute
137 public [if (attr.changeable)]var[else]let[/if] [attr.name.toLowerCamel()/]: [attr.swiftType()/]
138[/template]
139
140[comment Generate reference property (as ID) /]
141[template private generateReference(ref : EReference)]
142 /// Reference to [ref.eType.name/] by ID
143 public [if (ref.changeable)]var[else]let[/if] [ref.name.toLowerCamel()/]Id[if (ref.upperBound <> 1)]s[/if]: [ref.swiftIdType()/]
144[/template]
145
146[comment Generate containment property /]
147[template private generateContainment(ref : EReference)]
148 /// Contained [ref.eType.name/] elements
149 public [if (ref.changeable)]var[else]let[/if] [ref.name.toLowerCamel()/]: [ref.swiftType()/]
150[/template]
151
152[comment Map Ecore type to Swift type /]
153[query private swiftType(attr : EAttribute) : String =
154 if attr.eType.name = 'EString' then
155 if attr.required then 'String' else 'String?' endif
156 else if attr.eType.name = 'EInt' then
157 if attr.required then 'Int' else 'Int?' endif
158 else if attr.eType.name = 'EDouble' then
159 if attr.required then 'Double' else 'Double?' endif
160 else if attr.eType.name = 'EBoolean' then
161 if attr.required then 'Bool' else 'Bool?' endif
162 else if attr.eType.name = 'EDate' then
163 if attr.required then 'Date' else 'Date?' endif
164 else if attr.eType.oclIsKindOf(EEnum) then
165 if attr.required then attr.eType.name else attr.eType.name + '?' endif
166 else
167 'Any'
168 endif endif endif endif endif endif
169/]
170
171[comment Map reference to Swift type /]
172[query private swiftType(ref : EReference) : String =
173 if ref.upperBound = 1 then
174 if ref.required then ref.eType.name else ref.eType.name + '?' endif
175 else
176 '[' + ref.eType.name + ']'
177 endif
178/]
179
180[comment Map reference to Swift ID type /]
181[query private swiftIdType(ref : EReference) : String =
182 if ref.upperBound = 1 then
183 if ref.required then 'String' else 'String?' endif
184 else
185 '[String]'
186 endif
187/]
188
189[comment Get default value for attribute /]
190[query private swiftDefault(attr : EAttribute) : String =
191 if attr.eType.name = 'EString' then 'nil'
192 else if attr.eType.name = 'EInt' then 'nil'
193 else if attr.eType.name = 'EDouble' then 'nil'
194 else if attr.eType.name = 'EBoolean' then 'false'
195 else if attr.eType.name = 'EDate' then 'nil'
196 else 'nil'
197 endif endif endif endif endif
198/]
199
200[comment Get default value for reference /]
201[query private swiftDefault(ref : EReference) : String =
202 if ref.upperBound = 1 then 'nil'
203 else '[]'
204 endif
205/]
206
207[comment Check if attribute is required /]
208[query private required(attr : EAttribute) : Boolean =
209 attr.lowerBound > 0
210/]
211
212[comment Check if reference is required /]
213[query private required(ref : EReference) : Boolean =
214 ref.lowerBound > 0
215/]
216
217[comment Convert to lowerCamelCase /]
218[query private toLowerCamel(s : String) : String =
219 s.substring(1, 1).toLower() + s.substring(2)
220/]
221
222[comment Generate combined models file /]
223[template private generateModelsFile(pkg : EPackage)]
224// ProjectManagementModels.swift
225// Generated from ProjectManagement.ecore
226// Unified import file for all Project Management models
227
228import Foundation
229
230// MARK: - Type Aliases
231
232/// Identifier type used across all models
233public typealias ModelID = String
234
235// MARK: - Date Formatting
236
237extension DateFormatter {
238 /// ISO 8601 formatter for JSON serialisation
239 static let iso8601: DateFormatter = {
240 let formatter = DateFormatter()
241 formatter.dateFormat = "yyyy-MM-dd"
242 formatter.calendar = Calendar(identifier: .iso8601)
243 formatter.timeZone = TimeZone(secondsFromGMT: 0)
244 formatter.locale = Locale(identifier: "en_US_POSIX")
245 return formatter
246 }()
247
248 /// ISO 8601 formatter with time for timestamps
249 static let iso8601WithTime: DateFormatter = {
250 let formatter = DateFormatter()
251 formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
252 formatter.calendar = Calendar(identifier: .iso8601)
253 formatter.timeZone = TimeZone(secondsFromGMT: 0)
254 formatter.locale = Locale(identifier: "en_US_POSIX")
255 return formatter
256 }()
257}
258
259// MARK: - JSON Decoder Configuration
260
261extension JSONDecoder {
262 /// Configured decoder for Project Management JSON
263 static let projectManagement: JSONDecoder = {
264 let decoder = JSONDecoder()
265 decoder.dateDecodingStrategy = .formatted(.iso8601)
266 return decoder
267 }()
268}
269
270// MARK: - JSON Encoder Configuration
271
272extension JSONEncoder {
273 /// Configured encoder for Project Management JSON
274 static let projectManagement: JSONEncoder = {
275 let encoder = JSONEncoder()
276 encoder.dateEncodingStrategy = .formatted(.iso8601)
277 encoder.outputFormatting = ['.prettyPrinted', .sortedKeys]
278 return encoder
279 }()
280}
281
282// MARK: - Model Loading
283
284/// Errors that can occur during model loading
285public enum ModelLoadingError: Error, LocalizedError {
286 case fileNotFound(String)
287 case invalidFormat(String)
288 case decodingFailed(Error)
289
290 public var errorDescription: String? {
291 switch self {
292 case .fileNotFound(let path):
293 return "Model file not found: \(path)"
294 case .invalidFormat(let message):
295 return "Invalid model format: \(message)"
296 case .decodingFailed(let error):
297 return "Failed to decode model: \(error.localizedDescription)"
298 }
299 }
300}
301
302/// Load organisation from JSON file
303public func loadOrganisation(from url: URL) throws -> Organisation {
304 let data = try Data(contentsOf: url)
305 return try JSONDecoder.projectManagement.decode(Organisation.self, from: data)
306}
307
308/// Save organisation to JSON file
309public func saveOrganisation(_ organisation: Organisation, to url: URL) throws {
310 let data = try JSONEncoder.projectManagement.encode(organisation)
311 try data.write(to: url)
312}
313[/template]

Step 4

Create the complete JSON-to-Swift pipeline.

Execute the complete pipeline that takes JSON input, validates it against the metamodel, and generates type-safe Swift code for native application use.

Terminal
1# Complete JSON to Swift pipeline
2# Takes JSON input, validates, and generates type-safe Swift code
3
4# Step 1: Validate JSON against schema
5echo "=== Step 1: Validating JSON ==="
6swift-json validate updated-project-data.json \
7 --schema project-schema.json \
8 --verbose
9
10# Output:
11# [INFO] Loading schema: project-schema.json
12# [INFO] Validating: updated-project-data.json
13# [INFO] Validation successful
14# [INFO] Objects validated: 35
15# [INFO] Arrays validated: 22
16# [INFO] Properties validated: 186
17
18# Step 2: Convert JSON to XMI for metamodel validation
19echo ""
20echo "=== Step 2: Converting JSON to XMI ==="
21swift-atl transform JSON2XMI.atl \
22 --source updated-project-data.json \
23 --source-metamodel JsonModel.ecore \
24 --target updated-project-data.xmi \
25 --target-metamodel ProjectManagement.ecore \
26 --verbose
27
28# Output:
29# [INFO] Loading source metamodel: JsonModel.ecore
30# [INFO] Loading target metamodel: ProjectManagement.ecore
31# [INFO] Compiling transformation: JSON2XMI.atl
32# [INFO] Executing transformation...
33# [PROGRESS] Processing root Organisation
34# [PROGRESS] Creating 3 departments
35# [PROGRESS] Creating 7 team members (1 new)
36# [PROGRESS] Creating 2 projects
37# [PROGRESS] Creating 5 milestones
38# [PROGRESS] Creating 9 tasks (2 new)
39# [PROGRESS] Creating 8 comments
40# [INFO] Resolving cross-references...
41# [INFO] Transformation completed in 0.31 seconds
42
43# Step 3: Validate XMI against metamodel
44echo ""
45echo "=== Step 3: Validating XMI ==="
46swift-ecore validate updated-project-data.xmi \
47 --metamodel ProjectManagement.ecore
48
49# Output:
50# Validation successful. Model conforms to ProjectManagement metamodel.
51# Elements: 35, References resolved: 48
52
53# Step 4: Generate Swift code from metamodel
54echo ""
55echo "=== Step 4: Generating Swift Code ==="
56swift-mtl generate GenerateSwiftModels.mtl \
57 --metamodel ProjectManagement.ecore \
58 --output ./Generated/
59
60# Output:
61# [INFO] Loading template: GenerateSwiftModels.mtl
62# [INFO] Loading metamodel: ProjectManagement.ecore
63# [INFO] Generating Swift code...
64# [GENERATED] ProjectStatus.swift
65# [GENERATED] TaskPriority.swift
66# [GENERATED] TaskStatus.swift
67# [GENERATED] Organisation.swift
68# [GENERATED] Department.swift
69# [GENERATED] TeamMember.swift
70# [GENERATED] Project.swift
71# [GENERATED] Milestone.swift
72# [GENERATED] Task.swift
73# [GENERATED] Comment.swift
74# [GENERATED] ProjectManagementModels.swift
75# [INFO] Generated 11 Swift files in ./Generated/
76
77# Step 5: Compile and validate generated Swift
78echo ""
79echo "=== Step 5: Compiling Swift Code ==="
80swiftc -parse ./Generated/*.swift
81
82# Output:
83# (no output means successful compilation)
84
85# Step 6: Test loading JSON into generated types
86echo ""
87echo "=== Step 6: Testing Model Loading ==="
88swift test-model-loading.swift \
89 --json updated-project-data.json \
90 --types ./Generated/
91
92# Output:
93# [TEST] Loading organisation from JSON...
94# [TEST] Organisation loaded: Acme Software Solutions
95# [TEST] Departments: 3
96# [TEST] Members: 7
97# [TEST] Projects: 2
98# [TEST] All references resolved successfully
99# [TEST] Model loading test PASSED
100
101echo ""
102echo "=== Pipeline Complete ==="
103echo "Generated Swift files are ready in ./Generated/"
104echo "Use 'import ProjectManagementModels' in your Swift project"

Section 4

Bidirectional Synchronisation

Real integration scenarios require bidirectional synchronisation where changes in any format can be propagated to all other formats while maintaining data consistency.

This requires careful change detection, conflict resolution, and synchronisation protocols that preserve data integrity across the entire ecosystem.

Step 1

Design change detection mechanisms.

Document strategies for detecting changes in different formats and identifying conflicts that require resolution during synchronisation.

change-detection.md
1# Change Detection Mechanisms
2
3## Overview
4
5Bidirectional synchronisation requires detecting changes made in different formats
6and tools. This document describes strategies for identifying and tracking changes
7across XMI, JSON, and Swift representations.
8
9## Change Types
10
11### Structural Changes
12- **Addition**: New elements added to the model
13- **Deletion**: Elements removed from the model
14- **Move**: Elements relocated in containment hierarchy
15
16### Property Changes
17- **Value Update**: Attribute value modified
18- **Reference Update**: Reference target changed
19- **Collection Modification**: Items added/removed from multi-valued features
20
21## Detection Strategies
22
23### Strategy 1: Timestamp-Based
24
25Track modification timestamps for each element.
26
27```
28Element Metadata:
29- id: "TASK-001"
30- createdAt: "2024-01-15T09:00:00Z"
31- modifiedAt: "2024-03-10T15:20:00Z"
32- modifiedBy: "MEMBER-002"
33- version: 3
34```
35
36**Pros**: Simple, low overhead
37**Cons**: Timestamp drift, concurrent edits
38
39### Strategy 2: Version Vectors
40
41Maintain version counters per source.
42
43```
44Element Metadata:
45- id: "TASK-001"
46- versions:
47 xmi: 5
48 json: 7
49 swift: 4
50```
51
52**Pros**: Detects concurrent modifications
53**Cons**: More storage, complex merge
54
55### Strategy 3: Content Hashing
56
57Compute hash of element content.
58
59```
60Element Metadata:
61- id: "TASK-001"
62- contentHash: "sha256:a1b2c3..."
63- previousHash: "sha256:x9y8z7..."
64```
65
66**Pros**: Detects any change, integrity verification
67**Cons**: Recomputation overhead, no semantic understanding
68
69---
70
71## Change Detection Algorithm
72
73```
74function detectChanges(oldModel, newModel):
75 changes = []
76
77 # Find new elements
78 for element in newModel.elements:
79 if element.id not in oldModel.elementIds:
80 changes.add(Addition(element))
81
82 # Find deleted elements
83 for element in oldModel.elements:
84 if element.id not in newModel.elementIds:
85 changes.add(Deletion(element))
86
87 # Find modified elements
88 for element in newModel.elements:
89 if element.id in oldModel.elementIds:
90 oldElement = oldModel.getElement(element.id)
91 if element.contentHash != oldElement.contentHash:
92 propertyChanges = detectPropertyChanges(oldElement, element)
93 changes.add(Modification(element, propertyChanges))
94
95 return changes
96```
97
98---
99
100## Conflict Detection
101
102### Definition
103A conflict occurs when the same element is modified differently
104in multiple sources between synchronisation points.
105
106### Conflict Scenarios
107
108| Scenario | Source A | Source B | Conflict Type |
109|----------|----------|----------|---------------|
110| 1 | Update field X | Update field X (different value) | Value Conflict |
111| 2 | Update field X | Update field Y | No Conflict (mergeable) |
112| 3 | Delete element | Update element | Update-Delete Conflict |
113| 4 | Add child | Add different child | No Conflict (both added) |
114| 5 | Move element | Delete element | Move-Delete Conflict |
115
116### Conflict Detection Algorithm
117
118```
119function detectConflicts(changesA, changesB):
120 conflicts = []
121
122 for changeA in changesA:
123 for changeB in changesB:
124 if changeA.elementId == changeB.elementId:
125 if isConflicting(changeA, changeB):
126 conflicts.add(Conflict(changeA, changeB))
127
128 return conflicts
129
130function isConflicting(changeA, changeB):
131 # Same element, both modified
132 if changeA.type == MODIFY and changeB.type == MODIFY:
133 # Check if same properties modified with different values
134 for propA in changeA.properties:
135 for propB in changeB.properties:
136 if propA.name == propB.name:
137 if propA.newValue != propB.newValue:
138 return true
139 return false
140
141 # One deleted, one modified
142 if (changeA.type == DELETE and changeB.type == MODIFY) or
143 (changeA.type == MODIFY and changeB.type == DELETE):
144 return true
145
146 return false
147```
148
149---
150
151## Change Tracking Implementation
152
153### XMI Format
154- Use EMF Change Recorder
155- Store change descriptions in separate resource
156- Track containment path for each change
157
158### JSON Format
159- JSON Patch (RFC 6902) for change representation
160- Store patches with timestamps
161- Support operational transformation
162
163### Swift Format
164- Combine pattern with change notification
165- SwiftUI @Observable for reactive updates
166- Store change log in CoreData or file
167
168---
169
170## Synchronisation Checkpoints
171
172Regular checkpoints ensure recovery and audit trail:
173
174```
175Checkpoint Structure:
176- checkpointId: "CP-2024-03-10-001"
177- timestamp: "2024-03-10T18:00:00Z"
178- sources:
179 xmi: { version: 12, hash: "sha256:..." }
180 json: { version: 15, hash: "sha256:..." }
181 swift: { version: 10, hash: "sha256:..." }
182- pendingChanges: []
183- lastConflictResolution: "2024-03-09T14:30:00Z"
184```

Step 2

Create synchronisation transformations.

These transformations handle bidirectional synchronisation by comparing model states and applying changes while resolving conflicts according to defined policies.

SynchroniseFormats.atl
1-- ATL Transformation: Bidirectional Synchronisation
2-- Handles merging changes from multiple format sources
3-- Part of the Cross-Format Integration tutorial
4-- @path PM=/ProjectManagement/ProjectManagement.ecore
5-- @path SYNC=/Sync/SyncModel.ecore
6
7module SynchroniseFormats;
8create OUT: PM from BASE: PM, CHANGES_A: SYNC, CHANGES_B: SYNC;
9
10-- ===========================================================================
11-- Synchronisation Metadata Helpers
12-- ===========================================================================
13
14-- Get the latest modification timestamp
15helper context SYNC!ChangeRecord def: isNewerThan(other: SYNC!ChangeRecord): Boolean =
16 self.timestamp > other.timestamp;
17
18-- Check if change affects given element
19helper context SYNC!ChangeRecord def: affectsElement(elementId: String): Boolean =
20 self.elementId = elementId;
21
22-- Get all changes for a specific element
23helper def: changesForElement(elementId: String): Sequence(SYNC!ChangeRecord) =
24 SYNC!ChangeRecord.allInstances()->select(c | c.elementId = elementId);
25
26-- ===========================================================================
27-- Change Type Helpers
28-- ===========================================================================
29
30helper context SYNC!ChangeRecord def: isAddition(): Boolean =
31 self.changeType = #ADDITION;
32
33helper context SYNC!ChangeRecord def: isDeletion(): Boolean =
34 self.changeType = #DELETION;
35
36helper context SYNC!ChangeRecord def: isModification(): Boolean =
37 self.changeType = #MODIFICATION;
38
39-- ===========================================================================
40-- Conflict Detection Helpers
41-- ===========================================================================
42
43-- Check if two changes conflict
44helper def: areConflicting(changeA: SYNC!ChangeRecord, changeB: SYNC!ChangeRecord): Boolean =
45 changeA.elementId = changeB.elementId and (
46 -- Both modify same property with different values
47 (changeA.isModification() and changeB.isModification() and
48 changeA.modifiedProperties->exists(pA |
49 changeB.modifiedProperties->exists(pB |
50 pA.propertyName = pB.propertyName and pA.newValue <> pB.newValue
51 )
52 )) or
53 -- One deletes, other modifies
54 (changeA.isDeletion() and changeB.isModification()) or
55 (changeA.isModification() and changeB.isDeletion())
56 );
57
58-- Get all conflicts between two change sets
59helper def: detectConflicts(
60 changesA: Sequence(SYNC!ChangeRecord),
61 changesB: Sequence(SYNC!ChangeRecord)
62): Sequence(TupleType(changeA: SYNC!ChangeRecord, changeB: SYNC!ChangeRecord)) =
63 changesA->collect(cA |
64 changesB->select(cB | thisModule.areConflicting(cA, cB))
65 ->collect(cB | Tuple{changeA = cA, changeB = cB})
66 )->flatten();
67
68-- ===========================================================================
69-- Merge Strategy Helpers
70-- ===========================================================================
71
72-- Last-write-wins merge for property
73helper def: mergePropertyLastWriteWins(
74 changeA: SYNC!PropertyChange,
75 changeB: SYNC!PropertyChange
76): OclAny =
77 if changeA.timestamp > changeB.timestamp then changeA.newValue
78 else changeB.newValue
79 endif;
80
81-- Get non-conflicting changes for automatic merge
82helper def: getNonConflictingChanges(
83 changesA: Sequence(SYNC!ChangeRecord),
84 changesB: Sequence(SYNC!ChangeRecord)
85): Sequence(SYNC!ChangeRecord) =
86 let conflicts: Sequence(TupleType(changeA: SYNC!ChangeRecord, changeB: SYNC!ChangeRecord)) =
87 thisModule.detectConflicts(changesA, changesB) in
88 let conflictingIds: Set(String) = conflicts->collect(c | c.changeA.elementId)->asSet() in
89 changesA->union(changesB)->select(c | not conflictingIds->includes(c.elementId));
90
91-- ===========================================================================
92-- Rule: Synchronise Organisation (root element)
93-- ===========================================================================
94rule SyncOrganisation {
95 from
96 base: PM!Organisation
97 to
98 synced: PM!Organisation (
99 name <- thisModule.syncAttribute(base, 'name'),
100 domain <- thisModule.syncAttribute(base, 'domain'),
101 departments <- base.departments->collect(d |
102 thisModule.syncDepartment(d)
103 )->union(
104 thisModule.getAdditions('Department')->collect(a |
105 thisModule.createDepartmentFromChange(a)
106 )
107 ),
108 members <- base.members->collect(m |
109 thisModule.syncTeamMember(m)
110 )->union(
111 thisModule.getAdditions('TeamMember')->collect(a |
112 thisModule.createTeamMemberFromChange(a)
113 )
114 ),
115 projects <- base.projects->collect(p |
116 thisModule.syncProject(p)
117 )->union(
118 thisModule.getAdditions('Project')->collect(a |
119 thisModule.createProjectFromChange(a)
120 )
121 )
122 )
123}
124
125-- ===========================================================================
126-- Lazy Rule: Synchronise Department
127-- ===========================================================================
128lazy rule syncDepartment {
129 from
130 base: PM!Department (
131 not thisModule.isDeleted(base.id)
132 )
133 to
134 synced: PM!Department (
135 id <- base.id,
136 name <- thisModule.syncAttribute(base, 'name'),
137 code <- thisModule.syncAttribute(base, 'code')
138 )
139}
140
141-- ===========================================================================
142-- Lazy Rule: Synchronise TeamMember
143-- ===========================================================================
144lazy rule syncTeamMember {
145 from
146 base: PM!TeamMember (
147 not thisModule.isDeleted(base.id)
148 )
149 to
150 synced: PM!TeamMember (
151 id <- base.id,
152 name <- thisModule.syncAttribute(base, 'name'),
153 email <- thisModule.syncAttribute(base, 'email'),
154 role <- thisModule.syncAttribute(base, 'role')
155 )
156}
157
158-- ===========================================================================
159-- Lazy Rule: Synchronise Project
160-- ===========================================================================
161lazy rule syncProject {
162 from
163 base: PM!Project (
164 not thisModule.isDeleted(base.id)
165 )
166 to
167 synced: PM!Project (
168 id <- base.id,
169 name <- thisModule.syncAttribute(base, 'name'),
170 description <- thisModule.syncAttribute(base, 'description'),
171 status <- thisModule.syncEnumAttribute(base, 'status'),
172 startDate <- thisModule.syncAttribute(base, 'startDate'),
173 targetEndDate <- thisModule.syncAttribute(base, 'targetEndDate'),
174 budget <- thisModule.syncAttribute(base, 'budget'),
175 milestones <- base.milestones->collect(m |
176 thisModule.syncMilestone(m)
177 ),
178 tasks <- base.tasks->collect(t |
179 thisModule.syncTask(t)
180 )->union(
181 thisModule.getAdditionsForContainer(base.id, 'Task')->collect(a |
182 thisModule.createTaskFromChange(a)
183 )
184 )
185 )
186}
187
188-- ===========================================================================
189-- Lazy Rule: Synchronise Milestone
190-- ===========================================================================
191lazy rule syncMilestone {
192 from
193 base: PM!Milestone (
194 not thisModule.isDeleted(base.id)
195 )
196 to
197 synced: PM!Milestone (
198 id <- base.id,
199 name <- thisModule.syncAttribute(base, 'name'),
200 dueDate <- thisModule.syncAttribute(base, 'dueDate'),
201 completed <- thisModule.syncAttribute(base, 'completed')
202 )
203}
204
205-- ===========================================================================
206-- Lazy Rule: Synchronise Task
207-- ===========================================================================
208lazy rule syncTask {
209 from
210 base: PM!Task (
211 not thisModule.isDeleted(base.id)
212 )
213 to
214 synced: PM!Task (
215 id <- base.id,
216 title <- thisModule.syncAttribute(base, 'title'),
217 description <- thisModule.syncAttribute(base, 'description'),
218 priority <- thisModule.syncEnumAttribute(base, 'priority'),
219 status <- thisModule.syncEnumAttribute(base, 'status'),
220 estimatedHours <- thisModule.syncAttribute(base, 'estimatedHours'),
221 actualHours <- thisModule.syncAttribute(base, 'actualHours'),
222 dueDate <- thisModule.syncAttribute(base, 'dueDate'),
223 comments <- base.comments->collect(c |
224 thisModule.syncComment(c)
225 )->union(
226 thisModule.getAdditionsForContainer(base.id, 'Comment')->collect(a |
227 thisModule.createCommentFromChange(a)
228 )
229 )
230 )
231}
232
233-- ===========================================================================
234-- Lazy Rule: Synchronise Comment
235-- ===========================================================================
236lazy rule syncComment {
237 from
238 base: PM!Comment (
239 not thisModule.isDeleted(base.id)
240 )
241 to
242 synced: PM!Comment (
243 id <- base.id,
244 text <- thisModule.syncAttribute(base, 'text'),
245 timestamp <- thisModule.syncAttribute(base, 'timestamp')
246 )
247}
248
249-- ===========================================================================
250-- Helper: Synchronise attribute with change merging
251-- ===========================================================================
252helper def: syncAttribute(element: OclAny, attrName: String): OclAny =
253 let changes: Sequence(SYNC!ChangeRecord) = thisModule.changesForElement(element.id) in
254 let propChanges: Sequence(SYNC!PropertyChange) = changes
255 ->collect(c | c.modifiedProperties)
256 ->flatten()
257 ->select(p | p.propertyName = attrName) in
258 if propChanges->isEmpty() then
259 element.refGetValue(attrName)
260 else
261 propChanges->sortedBy(p | p.timestamp)->last().newValue
262 endif;
263
264-- ===========================================================================
265-- Helper: Synchronise enum attribute
266-- ===========================================================================
267helper def: syncEnumAttribute(element: OclAny, attrName: String): OclAny =
268 let rawValue: OclAny = thisModule.syncAttribute(element, attrName) in
269 if rawValue.oclIsKindOf(String) then
270 rawValue.toEnum(element.refGetValue(attrName).oclType())
271 else
272 rawValue
273 endif;
274
275-- ===========================================================================
276-- Helper: Check if element is deleted
277-- ===========================================================================
278helper def: isDeleted(elementId: String): Boolean =
279 SYNC!ChangeRecord.allInstances()
280 ->exists(c | c.elementId = elementId and c.isDeletion());
281
282-- ===========================================================================
283-- Helper: Get addition changes for type
284-- ===========================================================================
285helper def: getAdditions(typeName: String): Sequence(SYNC!ChangeRecord) =
286 SYNC!ChangeRecord.allInstances()
287 ->select(c | c.isAddition() and c.elementType = typeName);
288
289-- ===========================================================================
290-- Helper: Get additions for a specific container
291-- ===========================================================================
292helper def: getAdditionsForContainer(containerId: String, typeName: String): Sequence(SYNC!ChangeRecord) =
293 SYNC!ChangeRecord.allInstances()
294 ->select(c | c.isAddition() and c.elementType = typeName and c.containerId = containerId);
295
296-- ===========================================================================
297-- Called Rule: Create new Department from change record
298-- ===========================================================================
299rule createDepartmentFromChange(change: SYNC!ChangeRecord) {
300 to
301 dept: PM!Department (
302 id <- change.elementId,
303 name <- change.getPropertyValue('name'),
304 code <- change.getPropertyValue('code')
305 )
306 do {
307 dept;
308 }
309}
310
311-- ===========================================================================
312-- Called Rule: Create new TeamMember from change record
313-- ===========================================================================
314rule createTeamMemberFromChange(change: SYNC!ChangeRecord) {
315 to
316 member: PM!TeamMember (
317 id <- change.elementId,
318 name <- change.getPropertyValue('name'),
319 email <- change.getPropertyValue('email'),
320 role <- change.getPropertyValue('role')
321 )
322 do {
323 member;
324 }
325}
326
327-- ===========================================================================
328-- Called Rule: Create new Project from change record
329-- ===========================================================================
330rule createProjectFromChange(change: SYNC!ChangeRecord) {
331 to
332 proj: PM!Project (
333 id <- change.elementId,
334 name <- change.getPropertyValue('name'),
335 description <- change.getPropertyValue('description'),
336 status <- change.getPropertyValue('status').toEnum(#ProjectStatus),
337 startDate <- change.getPropertyValue('startDate').toDate(),
338 targetEndDate <- change.getPropertyValue('targetEndDate').toDate(),
339 budget <- change.getPropertyValue('budget').toReal()
340 )
341 do {
342 proj;
343 }
344}
345
346-- ===========================================================================
347-- Called Rule: Create new Task from change record
348-- ===========================================================================
349rule createTaskFromChange(change: SYNC!ChangeRecord) {
350 to
351 task: PM!Task (
352 id <- change.elementId,
353 title <- change.getPropertyValue('title'),
354 description <- change.getPropertyValue('description'),
355 priority <- change.getPropertyValue('priority').toEnum(#TaskPriority),
356 status <- change.getPropertyValue('status').toEnum(#TaskStatus),
357 estimatedHours <- change.getPropertyValue('estimatedHours').toReal(),
358 dueDate <- change.getPropertyValue('dueDate').toDate()
359 )
360 do {
361 task;
362 }
363}
364
365-- ===========================================================================
366-- Called Rule: Create new Comment from change record
367-- ===========================================================================
368rule createCommentFromChange(change: SYNC!ChangeRecord) {
369 to
370 cmt: PM!Comment (
371 id <- change.elementId,
372 text <- change.getPropertyValue('text'),
373 timestamp <- change.getPropertyValue('timestamp').toDate()
374 )
375 do {
376 cmt;
377 }
378}

Step 3

Build conflict resolution logic.

Implement conflict resolution strategies that handle simultaneous changes in multiple formats, ensuring consistent resolution across the entire system.

ConflictResolution.atl
1-- ATL Transformation: Conflict Resolution
2-- Implements policies for resolving synchronisation conflicts
3-- Part of the Cross-Format Integration tutorial
4-- @path PM=/ProjectManagement/ProjectManagement.ecore
5-- @path SYNC=/Sync/SyncModel.ecore
6-- @path RESOLVED=/Resolved/ResolvedModel.ecore
7
8module ConflictResolution;
9create OUT: RESOLVED from CONFLICTS: SYNC, POLICY: SYNC;
10
11-- ===========================================================================
12-- Resolution Policy Enumeration
13-- ===========================================================================
14
15-- Resolution policies:
16-- LAST_WRITE_WINS: Most recent timestamp wins
17-- SOURCE_PRIORITY: Predefined source priority order
18-- MERGE_FIELDS: Merge non-conflicting fields, prompt for others
19-- MANUAL: All conflicts require manual resolution
20-- PRESERVE_BOTH: Keep both versions with conflict markers
21
22-- ===========================================================================
23-- Helper: Get active resolution policy
24-- ===========================================================================
25helper def: resolutionPolicy: String =
26 SYNC!ResolutionPolicy.allInstances()->first().policy;
27
28-- ===========================================================================
29-- Helper: Get source priority order
30-- ===========================================================================
31helper def: sourcePriority: Sequence(String) =
32 SYNC!ResolutionPolicy.allInstances()->first().sourcePriorityOrder;
33
34-- ===========================================================================
35-- Conflict Analysis Helpers
36-- ===========================================================================
37
38-- Classify conflict severity
39helper context SYNC!Conflict def: severity: String =
40 if self.changeA.isDeletion() or self.changeB.isDeletion() then
41 'CRITICAL' -- Deletion conflicts are critical
42 else if self.conflictingProperties->size() > 3 then
43 'HIGH' -- Many conflicting properties
44 else if self.conflictingProperties->exists(p | p.isKeyProperty) then
45 'HIGH' -- Key property conflict
46 else
47 'MEDIUM'
48 endif endif endif;
49
50-- Check if conflict can be auto-resolved
51helper context SYNC!Conflict def: canAutoResolve: Boolean =
52 thisModule.resolutionPolicy <> 'MANUAL' and
53 self.severity <> 'CRITICAL';
54
55-- Get conflicting property names
56helper context SYNC!Conflict def: conflictingPropertyNames: Set(String) =
57 self.conflictingProperties->collect(p | p.propertyName)->asSet();
58
59-- ===========================================================================
60-- Resolution Strategy Helpers
61-- ===========================================================================
62
63-- Resolve using last-write-wins
64helper def: resolveLastWriteWins(conflict: SYNC!Conflict): SYNC!ChangeRecord =
65 if conflict.changeA.timestamp > conflict.changeB.timestamp then
66 conflict.changeA
67 else
68 conflict.changeB
69 endif;
70
71-- Resolve using source priority
72helper def: resolveSourcePriority(conflict: SYNC!Conflict): SYNC!ChangeRecord =
73 let priorityA: Integer = thisModule.sourcePriority->indexOf(conflict.changeA.source) in
74 let priorityB: Integer = thisModule.sourcePriority->indexOf(conflict.changeB.source) in
75 if priorityA < priorityB then
76 conflict.changeA
77 else
78 conflict.changeB
79 endif;
80
81-- Create merged change from non-conflicting parts
82helper def: createMergedChange(conflict: SYNC!Conflict): SYNC!ChangeRecord =
83 let nonConflictingA: Sequence(SYNC!PropertyChange) =
84 conflict.changeA.modifiedProperties
85 ->select(p | not conflict.conflictingPropertyNames->includes(p.propertyName)) in
86 let nonConflictingB: Sequence(SYNC!PropertyChange) =
87 conflict.changeB.modifiedProperties
88 ->select(p | not conflict.conflictingPropertyNames->includes(p.propertyName)) in
89 -- Return merged properties (conflicting ones need manual resolution)
90 conflict.changeA; -- Placeholder - actual merge would combine properties
91
92-- ===========================================================================
93-- Rule: Process Conflict
94-- ===========================================================================
95rule ProcessConflict {
96 from
97 conflict: SYNC!Conflict
98 to
99 resolution: RESOLVED!Resolution (
100 conflictId <- conflict.id,
101 elementId <- conflict.changeA.elementId,
102 elementType <- conflict.changeA.elementType,
103 severity <- conflict.severity,
104 autoResolved <- conflict.canAutoResolve,
105 resolutionStrategy <- thisModule.resolutionPolicy,
106 resolvedChange <- if conflict.canAutoResolve then
107 thisModule.applyResolutionStrategy(conflict)
108 else
109 OclUndefined
110 endif,
111 pendingConflicts <- if not conflict.canAutoResolve then
112 conflict.conflictingProperties
113 else
114 Sequence{}
115 endif,
116 sourceA <- sourceInfoA,
117 sourceB <- sourceInfoB
118 ),
119 sourceInfoA: RESOLVED!SourceInfo (
120 source <- conflict.changeA.source,
121 timestamp <- conflict.changeA.timestamp,
122 user <- conflict.changeA.modifiedBy,
123 changes <- conflict.changeA.modifiedProperties->collect(p |
124 thisModule.createPropertyInfo(p)
125 )
126 ),
127 sourceInfoB: RESOLVED!SourceInfo (
128 source <- conflict.changeB.source,
129 timestamp <- conflict.changeB.timestamp,
130 user <- conflict.changeB.modifiedBy,
131 changes <- conflict.changeB.modifiedProperties->collect(p |
132 thisModule.createPropertyInfo(p)
133 )
134 )
135}
136
137-- ===========================================================================
138-- Helper: Apply resolution strategy
139-- ===========================================================================
140helper def: applyResolutionStrategy(conflict: SYNC!Conflict): SYNC!ChangeRecord =
141 if thisModule.resolutionPolicy = 'LAST_WRITE_WINS' then
142 thisModule.resolveLastWriteWins(conflict)
143 else if thisModule.resolutionPolicy = 'SOURCE_PRIORITY' then
144 thisModule.resolveSourcePriority(conflict)
145 else if thisModule.resolutionPolicy = 'MERGE_FIELDS' then
146 thisModule.createMergedChange(conflict)
147 else
148 OclUndefined
149 endif endif endif;
150
151-- ===========================================================================
152-- Called Rule: Create property info for resolution report
153-- ===========================================================================
154rule createPropertyInfo(prop: SYNC!PropertyChange) {
155 to
156 info: RESOLVED!PropertyInfo (
157 name <- prop.propertyName,
158 oldValue <- prop.oldValue.toString(),
159 newValue <- prop.newValue.toString(),
160 isKey <- prop.isKeyProperty
161 )
162 do {
163 info;
164 }
165}
166
167-- ===========================================================================
168-- Rule: Generate Resolution Report
169-- ===========================================================================
170rule GenerateResolutionReport {
171 from
172 policy: SYNC!ResolutionPolicy
173 to
174 report: RESOLVED!ResolutionReport (
175 timestamp <- thisModule.currentTimestamp(),
176 policy <- policy.policy,
177 totalConflicts <- SYNC!Conflict.allInstances()->size(),
178 autoResolved <- SYNC!Conflict.allInstances()->select(c | c.canAutoResolve)->size(),
179 pendingManual <- SYNC!Conflict.allInstances()->select(c | not c.canAutoResolve)->size(),
180 resolutions <- SYNC!Conflict.allInstances()->collect(c |
181 thisModule.ProcessConflict(c)
182 )
183 )
184}
185
186-- ===========================================================================
187-- Helper: Get current timestamp
188-- ===========================================================================
189helper def: currentTimestamp(): String =
190 '2024-03-15T10:30:00Z'; -- In production, use actual system time
191
192-- ===========================================================================
193-- Rule: Create Manual Resolution Request
194-- ===========================================================================
195rule CreateManualResolutionRequest {
196 from
197 conflict: SYNC!Conflict (
198 not conflict.canAutoResolve
199 )
200 to
201 request: RESOLVED!ManualResolutionRequest (
202 conflictId <- conflict.id,
203 elementId <- conflict.changeA.elementId,
204 elementType <- conflict.changeA.elementType,
205 urgency <- if conflict.severity = 'CRITICAL' then 'IMMEDIATE'
206 else 'NORMAL' endif,
207 description <- 'Conflict detected between ' + conflict.changeA.source +
208 ' and ' + conflict.changeB.source +
209 ' for ' + conflict.changeA.elementType +
210 ' [' + conflict.changeA.elementId + ']',
211 affectedProperties <- conflict.conflictingPropertyNames->asSequence(),
212 optionA <- optionA,
213 optionB <- optionB,
214 suggestedResolution <- if conflict.changeA.timestamp > conflict.changeB.timestamp then
215 'OPTION_A'
216 else
217 'OPTION_B'
218 endif
219 ),
220 optionA: RESOLVED!ResolutionOption (
221 label <- 'Keep changes from ' + conflict.changeA.source,
222 description <- 'Accept all changes from ' + conflict.changeA.source +
223 ' (modified by ' + conflict.changeA.modifiedBy +
224 ' at ' + conflict.changeA.timestamp + ')',
225 preview <- conflict.changeA.modifiedProperties->collect(p |
226 p.propertyName + ': ' + p.newValue.toString()
227 )->iterate(s; acc: String = '' | acc + s + '\n')
228 ),
229 optionB: RESOLVED!ResolutionOption (
230 label <- 'Keep changes from ' + conflict.changeB.source,
231 description <- 'Accept all changes from ' + conflict.changeB.source +
232 ' (modified by ' + conflict.changeB.modifiedBy +
233 ' at ' + conflict.changeB.timestamp + ')',
234 preview <- conflict.changeB.modifiedProperties->collect(p |
235 p.propertyName + ': ' + p.newValue.toString()
236 )->iterate(s; acc: String = '' | acc + s + '\n')
237 )
238}
239
240-- ===========================================================================
241-- Endpoint: Generate Resolution Summary
242-- ===========================================================================
243endpoint def: generateSummary(): String =
244 let total: Integer = SYNC!Conflict.allInstances()->size() in
245 let auto: Integer = SYNC!Conflict.allInstances()->select(c | c.canAutoResolve)->size() in
246 let manual: Integer = total - auto in
247 'Resolution Summary\n' +
248 '==================\n' +
249 'Total Conflicts: ' + total.toString() + '\n' +
250 'Auto-Resolved: ' + auto.toString() + '\n' +
251 'Pending Manual: ' + manual.toString() + '\n' +
252 'Policy: ' + thisModule.resolutionPolicy;

Step 4

Test synchronisation scenarios.

Execute comprehensive tests that validate synchronisation behaviour under various scenarios including conflicts, partial updates, and error conditions.

Terminal
1# Test synchronisation scenarios
2# Validates bidirectional sync and conflict resolution
3
4echo "=== Synchronisation Test Suite ==="
5echo ""
6
7# Test 1: Basic sync without conflicts
8echo "--- Test 1: Basic Synchronisation (No Conflicts) ---"
9swift-sync test \
10 --base base-model.xmi \
11 --changes-a xmi-changes.json \
12 --changes-b json-changes.json \
13 --no-conflicts
14
15# Output:
16# [TEST] Loading base model: base-model.xmi
17# [TEST] Loading changes from source A (XMI): 5 changes
18# [TEST] Loading changes from source B (JSON): 3 changes
19# [TEST] Detecting conflicts...
20# [TEST] No conflicts detected
21# [TEST] Merging changes...
22# [TEST] Merged model created: 8 changes applied
23# [TEST] PASSED: Basic synchronisation without conflicts
24
25echo ""
26
27# Test 2: Sync with auto-resolvable conflicts
28echo "--- Test 2: Auto-Resolvable Conflicts ---"
29swift-sync test \
30 --base base-model.xmi \
31 --changes-a xmi-changes-conflict.json \
32 --changes-b json-changes-conflict.json \
33 --policy last-write-wins
34
35# Output:
36# [TEST] Loading base model: base-model.xmi
37# [TEST] Loading changes from source A (XMI): 4 changes
38# [TEST] Loading changes from source B (JSON): 4 changes
39# [TEST] Detecting conflicts...
40# [TEST] Found 2 conflicts:
41# - TASK-003: status (IN_PROGRESS vs COMPLETED)
42# - TASK-004: actualHours (25.0 vs 30.0)
43# [TEST] Applying resolution policy: LAST_WRITE_WINS
44# [TEST] Resolved conflict for TASK-003: using JSON value (newer timestamp)
45# [TEST] Resolved conflict for TASK-004: using XMI value (newer timestamp)
46# [TEST] Merged model created: 6 changes applied
47# [TEST] PASSED: Auto-resolvable conflicts
48
49echo ""
50
51# Test 3: Sync with manual resolution required
52echo "--- Test 3: Manual Resolution Required ---"
53swift-sync test \
54 --base base-model.xmi \
55 --changes-a xmi-delete-task.json \
56 --changes-b json-modify-task.json \
57 --policy merge-fields
58
59# Output:
60# [TEST] Loading base model: base-model.xmi
61# [TEST] Loading changes from source A (XMI): 1 deletion
62# [TEST] Loading changes from source B (JSON): 3 modifications
63# [TEST] Detecting conflicts...
64# [TEST] Found 1 critical conflict:
65# - TASK-005: DELETE vs MODIFY conflict
66# [TEST] Applying resolution policy: MERGE_FIELDS
67# [TEST] Cannot auto-resolve deletion conflict
68# [TEST] Manual resolution required for 1 conflict
69# [TEST] Resolution request generated: manual-resolution-001.json
70# [TEST] PASSED: Manual resolution correctly identified
71
72echo ""
73
74# Test 4: Three-way merge scenario
75echo "--- Test 4: Three-Way Merge ---"
76swift-sync test \
77 --base base-model.xmi \
78 --changes-a xmi-changes.json \
79 --changes-b json-changes.json \
80 --changes-c swift-changes.json \
81 --policy source-priority \
82 --priority "xmi,json,swift"
83
84# Output:
85# [TEST] Loading base model: base-model.xmi
86# [TEST] Loading changes from 3 sources:
87# - XMI: 4 changes
88# - JSON: 3 changes
89# - Swift: 2 changes
90# [TEST] Detecting conflicts...
91# [TEST] Found 3 conflicts across sources
92# [TEST] Applying resolution policy: SOURCE_PRIORITY
93# [TEST] Priority order: XMI > JSON > Swift
94# [TEST] Resolved 3 conflicts using source priority
95# [TEST] Merged model created: 7 changes applied
96# [TEST] PASSED: Three-way merge with source priority
97
98echo ""
99
100# Test 5: Roundtrip verification
101echo "--- Test 5: Roundtrip Verification ---"
102swift-sync roundtrip \
103 --input original-model.xmi \
104 --format-chain "xmi,json,swift,json,xmi"
105
106# Output:
107# [TEST] Starting roundtrip verification
108# [TEST] Original: original-model.xmi (sha256: a1b2c3...)
109# [TEST] Step 1: XMI -> JSON
110# [TEST] Step 2: JSON -> Swift
111# [TEST] Step 3: Swift -> JSON
112# [TEST] Step 4: JSON -> XMI
113# [TEST] Final: roundtrip-result.xmi (sha256: a1b2c3...)
114# [TEST] Comparing original and final...
115# [TEST] Models are semantically equivalent
116# [TEST] PASSED: Roundtrip verification
117
118echo ""
119
120# Test 6: Performance test
121echo "--- Test 6: Performance Test ---"
122swift-sync benchmark \
123 --model large-model.xmi \
124 --changes 1000 \
125 --conflicts 100
126
127# Output:
128# [BENCHMARK] Model size: 5000 elements
129# [BENCHMARK] Changes to process: 1000
130# [BENCHMARK] Conflicts to resolve: 100
131# [BENCHMARK] Running synchronisation...
132# [BENCHMARK] Change detection: 45ms
133# [BENCHMARK] Conflict detection: 12ms
134# [BENCHMARK] Conflict resolution: 28ms
135# [BENCHMARK] Model merge: 156ms
136# [BENCHMARK] Total time: 241ms
137# [BENCHMARK] Throughput: 4149 changes/second
138# [BENCHMARK] PASSED: Performance within acceptable limits
139
140echo ""
141echo "=== All Synchronisation Tests Complete ==="
142echo ""
143echo "Summary:"
144echo " - Basic sync: PASSED"
145echo " - Auto-resolve: PASSED"
146echo " - Manual resolution: PASSED"
147echo " - Three-way merge: PASSED"
148echo " - Roundtrip: PASSED"
149echo " - Performance: PASSED"

Section 5

API Integration Layer

Production integration requires robust APIs that handle format conversion, validation, and synchronisation seamlessly. These APIs abstract the complexity from client applications while ensuring data integrity.

The integration layer provides REST APIs for web clients, native Swift APIs for iOS/macOS apps, and event-driven synchronisation for real-time collaboration.

Step 1

Generate REST API definitions.

These templates generate OpenAPI specifications and server stubs from the metamodel, creating consistent APIs that support all format conversions and validation.

GenerateRestAPI.mtl
1[comment encoding = UTF-8 /]
2[comment]
3 MTL Template: Generate REST API from Ecore Metamodel
4 Creates OpenAPI specification and server stubs
5 Part of the Cross-Format Integration tutorial
6[/comment]
7
8[module GenerateRestAPI('http://www.eclipse.org/emf/2002/Ecore')]
9
10[comment Main entry point /]
11[template public generate(pkg : EPackage)]
12[comment Generate OpenAPI specification /]
13[file ('openapi.yaml', false, 'UTF-8')]
14[generateOpenAPISpec(pkg)/]
15[/file]
16
17[comment Generate server routes /]
18[file ('routes.swift', false, 'UTF-8')]
19[generateServerRoutes(pkg)/]
20[/file]
21
22[comment Generate request/response DTOs /]
23[file ('DTOs.swift', false, 'UTF-8')]
24[generateDTOs(pkg)/]
25[/file]
26[/template]
27
28[comment Generate OpenAPI 3.0 specification /]
29[template private generateOpenAPISpec(pkg : EPackage)]
30# OpenAPI Specification for [pkg.name/] API
31# Generated from [pkg.name/].ecore
32# Do not edit manually - changes will be overwritten
33
34openapi: "3.0.3"
35info:
36 title: [pkg.name/] API
37 description: REST API for [pkg.name/] model management
38 version: "1.0.0"
39 contact:
40 name: API Support
41 email: api-support@acme.com.au
42
43servers:
44 - url: https://api.acme.com.au/v1
45 description: Production server
46 - url: https://api-staging.acme.com.au/v1
47 description: Staging server
48
49paths:
50[for (cls : EClass | pkg.eClassifiers->filter(EClass))]
51[generatePathsForClass(cls)/]
52[/for]
53
54 /sync/status:
55 get:
56 summary: Get synchronisation status
57 operationId: getSyncStatus
58 tags: ['['/]Synchronisation[']'/]
59 responses:
60 '200':
61 description: Current synchronisation status
62 content:
63 application/json:
64 schema:
65 $ref: '#/components/schemas/SyncStatus'
66
67 /sync/push:
68 post:
69 summary: Push local changes
70 operationId: pushChanges
71 tags: ['['/]Synchronisation[']'/]
72 requestBody:
73 required: true
74 content:
75 application/json:
76 schema:
77 $ref: '#/components/schemas/ChangeSet'
78 responses:
79 '200':
80 description: Changes pushed successfully
81 content:
82 application/json:
83 schema:
84 $ref: '#/components/schemas/PushResult'
85 '409':
86 description: Conflict detected
87 content:
88 application/json:
89 schema:
90 $ref: '#/components/schemas/ConflictReport'
91
92 /sync/pull:
93 get:
94 summary: Pull remote changes
95 operationId: pullChanges
96 tags: ['['/]Synchronisation[']'/]
97 parameters:
98 - name: since
99 in: query
100 description: Timestamp of last sync
101 schema:
102 type: string
103 format: date-time
104 responses:
105 '200':
106 description: Remote changes retrieved
107 content:
108 application/json:
109 schema:
110 $ref: '#/components/schemas/ChangeSet'
111
112 /export/xmi:
113 get:
114 summary: Export model as XMI
115 operationId: exportXMI
116 tags: ['['/]Export[']'/]
117 responses:
118 '200':
119 description: XMI export
120 content:
121 application/xml:
122 schema:
123 type: string
124
125 /export/json:
126 get:
127 summary: Export model as JSON
128 operationId: exportJSON
129 tags: ['['/]Export[']'/]
130 responses:
131 '200':
132 description: JSON export
133 content:
134 application/json:
135 schema:
136 $ref: '#/components/schemas/Organisation'
137
138components:
139 schemas:
140[for (cls : EClass | pkg.eClassifiers->filter(EClass))]
141[generateSchemaForClass(cls)/]
142[/for]
143[for (enum : EEnum | pkg.eClassifiers->filter(EEnum))]
144[generateSchemaForEnum(enum)/]
145[/for]
146
147 SyncStatus:
148 type: object
149 properties:
150 lastSync:
151 type: string
152 format: date-time
153 pendingChanges:
154 type: integer
155 conflicts:
156 type: integer
157 status:
158 type: string
159 enum: ['['/]SYNCED[']'/], ['['/]PENDING[']'/], ['['/]CONFLICT[']'/]
160
161 ChangeSet:
162 type: object
163 properties:
164 timestamp:
165 type: string
166 format: date-time
167 source:
168 type: string
169 changes:
170 type: array
171 items:
172 $ref: '#/components/schemas/Change'
173
174 Change:
175 type: object
176 properties:
177 id:
178 type: string
179 type:
180 type: string
181 enum: ['['/]ADD[']'/], ['['/]UPDATE[']'/], ['['/]DELETE[']'/]
182 elementType:
183 type: string
184 elementId:
185 type: string
186 properties:
187 type: object
188 additionalProperties: true
189
190 PushResult:
191 type: object
192 properties:
193 success:
194 type: boolean
195 changesApplied:
196 type: integer
197 newVersion:
198 type: string
199
200 ConflictReport:
201 type: object
202 properties:
203 conflicts:
204 type: array
205 items:
206 $ref: '#/components/schemas/Conflict'
207
208 Conflict:
209 type: object
210 properties:
211 elementId:
212 type: string
213 elementType:
214 type: string
215 localChange:
216 $ref: '#/components/schemas/Change'
217 remoteChange:
218 $ref: '#/components/schemas/Change'
219
220 securitySchemes:
221 bearerAuth:
222 type: http
223 scheme: bearer
224 bearerFormat: JWT
225
226security:
227 - bearerAuth: ['['/][']'/]
228[/template]
229
230[comment Generate paths for a class /]
231[template private generatePathsForClass(cls : EClass)]
232 /[cls.name.toLower()/]s:
233 get:
234 summary: List all [cls.name.toLower()/]s
235 operationId: list[cls.name/]s
236 tags: ['['/][cls.name/][']'/]
237 parameters:
238 - name: limit
239 in: query
240 schema:
241 type: integer
242 default: 20
243 - name: offset
244 in: query
245 schema:
246 type: integer
247 default: 0
248 responses:
249 '200':
250 description: List of [cls.name.toLower()/]s
251 content:
252 application/json:
253 schema:
254 type: object
255 properties:
256 items:
257 type: array
258 items:
259 $ref: '#/components/schemas/[cls.name/]'
260 total:
261 type: integer
262 post:
263 summary: Create a new [cls.name.toLower()/]
264 operationId: create[cls.name/]
265 tags: ['['/][cls.name/][']'/]
266 requestBody:
267 required: true
268 content:
269 application/json:
270 schema:
271 $ref: '#/components/schemas/[cls.name/]Input'
272 responses:
273 '201':
274 description: [cls.name/] created
275 content:
276 application/json:
277 schema:
278 $ref: '#/components/schemas/[cls.name/]'
279
280 /[cls.name.toLower()/]s/{id}:
281 get:
282 summary: Get [cls.name.toLower()/] by ID
283 operationId: get[cls.name/]
284 tags: ['['/][cls.name/][']'/]
285 parameters:
286 - name: id
287 in: path
288 required: true
289 schema:
290 type: string
291 responses:
292 '200':
293 description: [cls.name/] details
294 content:
295 application/json:
296 schema:
297 $ref: '#/components/schemas/[cls.name/]'
298 '404':
299 description: [cls.name/] not found
300 put:
301 summary: Update [cls.name.toLower()/]
302 operationId: update[cls.name/]
303 tags: ['['/][cls.name/][']'/]
304 parameters:
305 - name: id
306 in: path
307 required: true
308 schema:
309 type: string
310 requestBody:
311 required: true
312 content:
313 application/json:
314 schema:
315 $ref: '#/components/schemas/[cls.name/]Input'
316 responses:
317 '200':
318 description: [cls.name/] updated
319 content:
320 application/json:
321 schema:
322 $ref: '#/components/schemas/[cls.name/]'
323 delete:
324 summary: Delete [cls.name.toLower()/]
325 operationId: delete[cls.name/]
326 tags: ['['/][cls.name/][']'/]
327 parameters:
328 - name: id
329 in: path
330 required: true
331 schema:
332 type: string
333 responses:
334 '204':
335 description: [cls.name/] deleted
336
337[/template]
338
339[comment Generate schema for a class /]
340[template private generateSchemaForClass(cls : EClass)]
341 [cls.name/]:
342 type: object
343 properties:
344[for (attr : EAttribute | cls.eAllAttributes)]
345 [attr.name/]:
346 [attr.openAPIType()/]
347[/for]
348[for (ref : EReference | cls.eAllReferences->select(r | not r.containment))]
349 [ref.name/]Id[if (ref.upperBound <> 1)]s[/if]:
350 [ref.openAPIRefType()/]
351[/for]
352[for (ref : EReference | cls.eAllReferences->select(r | r.containment))]
353 [ref.name/]:
354 [ref.openAPIContainmentType()/]
355[/for]
356
357 [cls.name/]Input:
358 type: object
359 properties:
360[for (attr : EAttribute | cls.eAllAttributes->select(a | a.name <> 'id'))]
361 [attr.name/]:
362 [attr.openAPIType()/]
363[/for]
364[for (ref : EReference | cls.eAllReferences->select(r | not r.containment))]
365 [ref.name/]Id[if (ref.upperBound <> 1)]s[/if]:
366 [ref.openAPIRefType()/]
367[/for]
368 required:
369[for (attr : EAttribute | cls.eAllAttributes->select(a | a.lowerBound > 0 and a.name <> 'id'))]
370 - [attr.name/]
371[/for]
372
373[/template]
374
375[comment Generate schema for an enum /]
376[template private generateSchemaForEnum(enum : EEnum)]
377 [enum.name/]:
378 type: string
379 enum:
380[for (literal : EEnumLiteral | enum.eLiterals)]
381 - [literal.name/]
382[/for]
383
384[/template]
385
386[comment Map attribute to OpenAPI type /]
387[query private openAPIType(attr : EAttribute) : String =
388 if attr.eType.name = 'EString' then 'type: string'
389 else if attr.eType.name = 'EInt' then 'type: integer'
390 else if attr.eType.name = 'EDouble' then 'type: number\n format: double'
391 else if attr.eType.name = 'EBoolean' then 'type: boolean'
392 else if attr.eType.name = 'EDate' then 'type: string\n format: date'
393 else if attr.eType.oclIsKindOf(EEnum) then '$ref: \'#/components/schemas/' + attr.eType.name + '\''
394 else 'type: string'
395 endif endif endif endif endif endif
396/]
397
398[comment Map reference to OpenAPI type /]
399[query private openAPIRefType(ref : EReference) : String =
400 if ref.upperBound = 1 then 'type: string'
401 else 'type: array\n items:\n type: string'
402 endif
403/]
404
405[comment Map containment to OpenAPI type /]
406[query private openAPIContainmentType(ref : EReference) : String =
407 if ref.upperBound = 1 then '$ref: \'#/components/schemas/' + ref.eType.name + '\''
408 else 'type: array\n items:\n $ref: \'#/components/schemas/' + ref.eType.name + '\''
409 endif
410/]
411
412[comment Generate server routes /]
413[template private generateServerRoutes(pkg : EPackage)]
414// routes.swift
415// Generated from [pkg.name/].ecore
416// Vapor server routes for [pkg.name/] API
417
418import Vapor
419
420/// Configure all routes for the [pkg.name/] API
421func configureRoutes(_ app: Application) throws {
422 // API version prefix
423 let api = app.grouped("v1")
424
425 // Configure CORS
426 let cors = CORSMiddleware(configuration: .default())
427 api.middleware.use(cors)
428
429 // Authentication middleware
430 let protected = api.grouped(JWTAuthMiddleware())
431
432[for (cls : EClass | pkg.eClassifiers->filter(EClass))]
433 // [cls.name/] routes
434 configure[cls.name/]Routes(protected)
435
436[/for]
437 // Synchronisation routes
438 configureSyncRoutes(protected)
439
440 // Export routes
441 configureExportRoutes(protected)
442}
443
444[for (cls : EClass | pkg.eClassifiers->filter(EClass))]
445[generateRoutesForClass(cls)/]
446[/for]
447
448/// Configure synchronisation routes
449func configureSyncRoutes(_ routes: RoutesBuilder) {
450 let sync = routes.grouped("sync")
451
452 sync.get("status") { req async throws -> SyncStatusDTO in
453 let service = try req.make(SyncService.self)
454 return try await service.getStatus()
455 }
456
457 sync.post("push") { req async throws -> PushResultDTO in
458 let changeSet = try req.content.decode(ChangeSetDTO.self)
459 let service = try req.make(SyncService.self)
460 return try await service.pushChanges(changeSet)
461 }
462
463 sync.get("pull") { req async throws -> ChangeSetDTO in
464 let since = req.query[String.self, at: "since"]
465 let service = try req.make(SyncService.self)
466 return try await service.pullChanges(since: since)
467 }
468}
469
470/// Configure export routes
471func configureExportRoutes(_ routes: RoutesBuilder) {
472 let export = routes.grouped("export")
473
474 export.get("xmi") { req async throws -> Response in
475 let service = try req.make(ExportService.self)
476 let xmi = try await service.exportXMI()
477 return Response(
478 status: .ok,
479 headers: ["Content-Type": "application/xml"],
480 body: .init(string: xmi)
481 )
482 }
483
484 export.get("json") { req async throws -> OrganisationDTO in
485 let service = try req.make(ExportService.self)
486 return try await service.exportJSON()
487 }
488}
489[/template]
490
491[comment Generate routes for a class /]
492[template private generateRoutesForClass(cls : EClass)]
493/// Configure [cls.name/] routes
494func configure[cls.name/]Routes(_ routes: RoutesBuilder) {
495 let [cls.name.toLowerFirst()/]s = routes.grouped("[cls.name.toLower()/]s")
496
497 // List all [cls.name.toLower()/]s
498 [cls.name.toLowerFirst()/]s.get { req async throws -> Page<[cls.name/]DTO> in
499 let service = try req.make([cls.name/]Service.self)
500 let limit = req.query[Int.self, at: "limit"] ?? 20
501 let offset = req.query[Int.self, at: "offset"] ?? 0
502 return try await service.list(limit: limit, offset: offset)
503 }
504
505 // Get [cls.name.toLower()/] by ID
506 [cls.name.toLowerFirst()/]s.get(":id") { req async throws -> [cls.name/]DTO in
507 guard let id = req.parameters.get("id") else {
508 throw Abort(.badRequest, reason: "Missing ID parameter")
509 }
510 let service = try req.make([cls.name/]Service.self)
511 guard let [cls.name.toLowerFirst()/] = try await service.find(id: id) else {
512 throw Abort(.notFound, reason: "[cls.name/] not found")
513 }
514 return [cls.name.toLowerFirst()/]
515 }
516
517 // Create new [cls.name.toLower()/]
518 [cls.name.toLowerFirst()/]s.post { req async throws -> [cls.name/]DTO in
519 let input = try req.content.decode([cls.name/]InputDTO.self)
520 let service = try req.make([cls.name/]Service.self)
521 return try await service.create(input)
522 }
523
524 // Update [cls.name.toLower()/]
525 [cls.name.toLowerFirst()/]s.put(":id") { req async throws -> [cls.name/]DTO in
526 guard let id = req.parameters.get("id") else {
527 throw Abort(.badRequest, reason: "Missing ID parameter")
528 }
529 let input = try req.content.decode([cls.name/]InputDTO.self)
530 let service = try req.make([cls.name/]Service.self)
531 return try await service.update(id: id, input: input)
532 }
533
534 // Delete [cls.name.toLower()/]
535 [cls.name.toLowerFirst()/]s.delete(":id") { req async throws -> HTTPStatus in
536 guard let id = req.parameters.get("id") else {
537 throw Abort(.badRequest, reason: "Missing ID parameter")
538 }
539 let service = try req.make([cls.name/]Service.self)
540 try await service.delete(id: id)
541 return .noContent
542 }
543}
544
545[/template]
546
547[comment Generate DTOs /]
548[template private generateDTOs(pkg : EPackage)]
549// DTOs.swift
550// Generated from [pkg.name/].ecore
551// Data Transfer Objects for [pkg.name/] API
552
553import Vapor
554
555[for (cls : EClass | pkg.eClassifiers->filter(EClass))]
556[generateDTOForClass(cls)/]
557[/for]
558
559// MARK: - Synchronisation DTOs
560
561struct SyncStatusDTO: Content {
562 let lastSync: Date?
563 let pendingChanges: Int
564 let conflicts: Int
565 let status: SyncState
566}
567
568enum SyncState: String, Content {
569 case synced = "SYNCED"
570 case pending = "PENDING"
571 case conflict = "CONFLICT"
572}
573
574struct ChangeSetDTO: Content {
575 let timestamp: Date
576 let source: String
577 let changes: [ChangeDTO]
578}
579
580struct ChangeDTO: Content {
581 let id: String
582 let type: ChangeType
583 let elementType: String
584 let elementId: String
585 let properties: [String: AnyCodable]
586}
587
588enum ChangeType: String, Content {
589 case add = "ADD"
590 case update = "UPDATE"
591 case delete = "DELETE"
592}
593
594struct PushResultDTO: Content {
595 let success: Bool
596 let changesApplied: Int
597 let newVersion: String
598}
599
600struct ConflictReportDTO: Content {
601 let conflicts: [ConflictDTO]
602}
603
604struct ConflictDTO: Content {
605 let elementId: String
606 let elementType: String
607 let localChange: ChangeDTO
608 let remoteChange: ChangeDTO
609}
610
611// MARK: - Pagination
612
613struct Page<T: Content>: Content {
614 let items: [T]
615 let total: Int
616}
617
618// MARK: - AnyCodable for dynamic properties
619
620struct AnyCodable: Codable {
621 let value: Any
622
623 init(_ value: Any) {
624 self.value = value
625 }
626
627 init(from decoder: Decoder) throws {
628 let container = try decoder.singleValueContainer()
629 if let string = try? container.decode(String.self) {
630 value = string
631 } else if let int = try? container.decode(Int.self) {
632 value = int
633 } else if let double = try? container.decode(Double.self) {
634 value = double
635 } else if let bool = try? container.decode(Bool.self) {
636 value = bool
637 } else {
638 value = ""
639 }
640 }
641
642 func encode(to encoder: Encoder) throws {
643 var container = encoder.singleValueContainer()
644 if let string = value as? String {
645 try container.encode(string)
646 } else if let int = value as? Int {
647 try container.encode(int)
648 } else if let double = value as? Double {
649 try container.encode(double)
650 } else if let bool = value as? Bool {
651 try container.encode(bool)
652 }
653 }
654}
655[/template]
656
657[comment Generate DTO for a class /]
658[template private generateDTOForClass(cls : EClass)]
659// MARK: - [cls.name/] DTOs
660
661struct [cls.name/]DTO: Content, Identifiable {
662[for (attr : EAttribute | cls.eAllAttributes)]
663 let [attr.name/]: [attr.swiftDTOType()/]
664[/for]
665[for (ref : EReference | cls.eAllReferences->select(r | not r.containment))]
666 let [ref.name/]Id[if (ref.upperBound <> 1)]s[/if]: [ref.swiftRefDTOType()/]
667[/for]
668[for (ref : EReference | cls.eAllReferences->select(r | r.containment))]
669 let [ref.name/]: [ref.swiftContainmentDTOType()/]
670[/for]
671}
672
673struct [cls.name/]InputDTO: Content {
674[for (attr : EAttribute | cls.eAllAttributes->select(a | a.name <> 'id'))]
675 [if (attr.lowerBound > 0)]let[else]var[/if] [attr.name/]: [attr.swiftDTOType()/]
676[/for]
677[for (ref : EReference | cls.eAllReferences->select(r | not r.containment))]
678 var [ref.name/]Id[if (ref.upperBound <> 1)]s[/if]: [ref.swiftRefDTOType()/]
679[/for]
680}
681
682[/template]
683
684[comment Map attribute to Swift DTO type /]
685[query private swiftDTOType(attr : EAttribute) : String =
686 if attr.eType.name = 'EString' then
687 if attr.lowerBound > 0 then 'String' else 'String?' endif
688 else if attr.eType.name = 'EInt' then
689 if attr.lowerBound > 0 then 'Int' else 'Int?' endif
690 else if attr.eType.name = 'EDouble' then
691 if attr.lowerBound > 0 then 'Double' else 'Double?' endif
692 else if attr.eType.name = 'EBoolean' then
693 if attr.lowerBound > 0 then 'Bool' else 'Bool?' endif
694 else if attr.eType.name = 'EDate' then
695 if attr.lowerBound > 0 then 'Date' else 'Date?' endif
696 else if attr.eType.oclIsKindOf(EEnum) then
697 if attr.lowerBound > 0 then attr.eType.name else attr.eType.name + '?' endif
698 else 'String'
699 endif endif endif endif endif endif
700/]
701
702[comment Map reference to Swift DTO type /]
703[query private swiftRefDTOType(ref : EReference) : String =
704 if ref.upperBound = 1 then
705 if ref.lowerBound > 0 then 'String' else 'String?' endif
706 else '[String]'
707 endif
708/]
709
710[comment Map containment to Swift DTO type /]
711[query private swiftContainmentDTOType(ref : EReference) : String =
712 if ref.upperBound = 1 then
713 if ref.lowerBound > 0 then ref.eType.name + 'DTO' else ref.eType.name + 'DTO?' endif
714 else '[' + ref.eType.name + 'DTO]'
715 endif
716/]

Step 2

Create Swift API client libraries.

Generate Swift client libraries that provide type-safe access to the integration APIs, handling format conversion transparently for Swift applications.

GenerateSwiftAPI.mtl
1[comment encoding = UTF-8 /]
2[comment]
3 MTL Template: Generate Swift API Client
4 Creates type-safe client library for consuming the REST API
5 Part of the Cross-Format Integration tutorial
6[/comment]
7
8[module GenerateSwiftAPI('http://www.eclipse.org/emf/2002/Ecore')]
9
10[comment Main entry point /]
11[template public generate(pkg : EPackage)]
12[comment Generate API client /]
13[file ('ProjectManagementAPI.swift', false, 'UTF-8')]
14[generateAPIClient(pkg)/]
15[/file]
16
17[comment Generate model protocols /]
18[file ('APIProtocols.swift', false, 'UTF-8')]
19[generateProtocols(pkg)/]
20[/file]
21
22[comment Generate async/await extensions /]
23[file ('APIExtensions.swift', false, 'UTF-8')]
24[generateExtensions(pkg)/]
25[/file]
26[/template]
27
28[comment Generate main API client /]
29[template private generateAPIClient(pkg : EPackage)]
30// ProjectManagementAPI.swift
31// Generated from [pkg.name/].ecore
32// Swift API Client for [pkg.name/] REST API
33
34import Foundation
35
36// MARK: - API Configuration
37
38/// Configuration for the Project Management API client
39public struct APIConfiguration: Sendable {
40 /// Base URL for the API
41 public let baseURL: URL
42
43 /// Authentication token
44 public var authToken: String?
45
46 /// Request timeout interval
47 public let timeoutInterval: TimeInterval
48
49 /// Default configuration for production
50 public static let production = APIConfiguration(
51 baseURL: URL(string: "https://api.acme.com.au/v1")!,
52 timeoutInterval: 30
53 )
54
55 /// Configuration for staging environment
56 public static let staging = APIConfiguration(
57 baseURL: URL(string: "https://api-staging.acme.com.au/v1")!,
58 timeoutInterval: 30
59 )
60
61 public init(baseURL: URL, authToken: String? = nil, timeoutInterval: TimeInterval = 30) {
62 self.baseURL = baseURL
63 self.authToken = authToken
64 self.timeoutInterval = timeoutInterval
65 }
66}
67
68// MARK: - API Errors
69
70/// Errors that can occur during API operations
71public enum APIError: Error, LocalizedError {
72 case invalidURL
73 case networkError(Error)
74 case httpError(statusCode: Int, message: String?)
75 case decodingError(Error)
76 case encodingError(Error)
77 case unauthorised
78 case notFound
79 case conflict(ConflictReport)
80 case serverError(String)
81
82 public var errorDescription: String? {
83 switch self {
84 case .invalidURL:
85 return "Invalid URL"
86 case .networkError(let error):
87 return "Network error: \(error.localizedDescription)"
88 case .httpError(let code, let message):
89 return "HTTP error \(code): \(message ?? "Unknown error")"
90 case .decodingError(let error):
91 return "Failed to decode response: \(error.localizedDescription)"
92 case .encodingError(let error):
93 return "Failed to encode request: \(error.localizedDescription)"
94 case .unauthorised:
95 return "Unauthorised - please check your authentication token"
96 case .notFound:
97 return "Resource not found"
98 case .conflict(let report):
99 return "Conflict detected: \(report.conflicts.count) conflicts"
100 case .serverError(let message):
101 return "Server error: \(message)"
102 }
103 }
104}
105
106// MARK: - API Client
107
108/// Main API client for Project Management operations
109@MainActor
110public final class ProjectManagementAPI: ObservableObject {
111 /// Current configuration
112 public let configuration: APIConfiguration
113
114 /// URL session for network requests
115 private let session: URLSession
116
117 /// JSON decoder configured for the API
118 private let decoder: JSONDecoder = {
119 let decoder = JSONDecoder()
120 decoder.dateDecodingStrategy = .iso8601
121 return decoder
122 }()
123
124 /// JSON encoder configured for the API
125 private let encoder: JSONEncoder = {
126 let encoder = JSONEncoder()
127 encoder.dateEncodingStrategy = .iso8601
128 return encoder
129 }()
130
131 /// Published sync status for reactive updates
132 @Published public private(set) var syncStatus: SyncStatus?
133
134 /// Initialise the API client
135 public init(configuration: APIConfiguration) {
136 self.configuration = configuration
137
138 let sessionConfig = URLSessionConfiguration.default
139 sessionConfig.timeoutIntervalForRequest = configuration.timeoutInterval
140 self.session = URLSession(configuration: sessionConfig)
141 }
142
143[for (cls : EClass | pkg.eClassifiers->filter(EClass))]
144 // MARK: - [cls.name/] Operations
145
146[generateClassOperations(cls)/]
147[/for]
148
149 // MARK: - Synchronisation Operations
150
151 /// Get current synchronisation status
152 public func getSyncStatus() async throws -> SyncStatus {
153 let status: SyncStatus = try await get(path: "sync/status")
154 await MainActor.run { self.syncStatus = status }
155 return status
156 }
157
158 /// Push local changes to the server
159 public func pushChanges(_ changeSet: ChangeSet) async throws -> PushResult {
160 do {
161 return try await post(path: "sync/push", body: changeSet)
162 } catch APIError.httpError(409, _) {
163 // Handle conflict response
164 let conflictReport: ConflictReport = try await get(path: "sync/conflicts")
165 throw APIError.conflict(conflictReport)
166 }
167 }
168
169 /// Pull remote changes since last sync
170 public func pullChanges(since: Date? = nil) async throws -> ChangeSet {
171 var queryItems: [URLQueryItem] = []
172 if let since = since {
173 let formatter = ISO8601DateFormatter()
174 queryItems.append(URLQueryItem(name: "since", value: formatter.string(from: since)))
175 }
176 return try await get(path: "sync/pull", queryItems: queryItems)
177 }
178
179 // MARK: - Export Operations
180
181 /// Export model as XMI
182 public func exportXMI() async throws -> String {
183 try await getString(path: "export/xmi")
184 }
185
186 /// Export model as JSON
187 public func exportJSON() async throws -> Organisation {
188 try await get(path: "export/json")
189 }
190
191 // MARK: - Network Helpers
192
193 /// Perform GET request
194 private func get<T: Decodable>(path: String, queryItems: [URLQueryItem] = []) async throws -> T {
195 let request = try buildRequest(path: path, method: "GET", queryItems: queryItems)
196 return try await perform(request)
197 }
198
199 /// Perform GET request returning string
200 private func getString(path: String) async throws -> String {
201 let request = try buildRequest(path: path, method: "GET")
202 let (data, response) = try await session.data(for: request)
203 try validateResponse(response)
204 guard let string = String(data: data, encoding: .utf8) else {
205 throw APIError.decodingError(NSError(domain: "APIError", code: 0))
206 }
207 return string
208 }
209
210 /// Perform POST request
211 private func post<T: Decodable, B: Encodable>(path: String, body: B) async throws -> T {
212 var request = try buildRequest(path: path, method: "POST")
213 request.httpBody = try encoder.encode(body)
214 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
215 return try await perform(request)
216 }
217
218 /// Perform PUT request
219 private func put<T: Decodable, B: Encodable>(path: String, body: B) async throws -> T {
220 var request = try buildRequest(path: path, method: "PUT")
221 request.httpBody = try encoder.encode(body)
222 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
223 return try await perform(request)
224 }
225
226 /// Perform DELETE request
227 private func delete(path: String) async throws {
228 let request = try buildRequest(path: path, method: "DELETE")
229 let (_, response) = try await session.data(for: request)
230 try validateResponse(response)
231 }
232
233 /// Build URL request
234 private func buildRequest(path: String, method: String, queryItems: [URLQueryItem] = []) throws -> URLRequest {
235 var components = URLComponents(url: configuration.baseURL.appendingPathComponent(path), resolvingAgainstBaseURL: true)
236 if !queryItems.isEmpty {
237 components?.queryItems = queryItems
238 }
239 guard let url = components?.url else {
240 throw APIError.invalidURL
241 }
242 var request = URLRequest(url: url)
243 request.httpMethod = method
244 if let token = configuration.authToken {
245 request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorisation")
246 }
247 return request
248 }
249
250 /// Perform request and decode response
251 private func perform<T: Decodable>(_ request: URLRequest) async throws -> T {
252 do {
253 let (data, response) = try await session.data(for: request)
254 try validateResponse(response)
255 return try decoder.decode(T.self, from: data)
256 } catch let error as APIError {
257 throw error
258 } catch let error as DecodingError {
259 throw APIError.decodingError(error)
260 } catch {
261 throw APIError.networkError(error)
262 }
263 }
264
265 /// Validate HTTP response
266 private func validateResponse(_ response: URLResponse) throws {
267 guard let httpResponse = response as? HTTPURLResponse else {
268 throw APIError.networkError(NSError(domain: "APIError", code: 0))
269 }
270 switch httpResponse.statusCode {
271 case 200...299:
272 return
273 case 401:
274 throw APIError.unauthorised
275 case 404:
276 throw APIError.notFound
277 case 409:
278 throw APIError.httpError(statusCode: 409, message: "Conflict")
279 case 500...599:
280 throw APIError.serverError("Internal server error")
281 default:
282 throw APIError.httpError(statusCode: httpResponse.statusCode, message: nil)
283 }
284 }
285}
286[/template]
287
288[comment Generate operations for a class /]
289[template private generateClassOperations(cls : EClass)]
290 /// List all [cls.name.toLower()/]s
291 public func list[cls.name/]s(limit: Int = 20, offset: Int = 0) async throws -> PagedResult<[cls.name/]> {
292 let queryItems = [
293 URLQueryItem(name: "limit", value: String(limit)),
294 URLQueryItem(name: "offset", value: String(offset))
295 ]
296 return try await get(path: "[cls.name.toLower()/]s", queryItems: queryItems)
297 }
298
299 /// Get [cls.name.toLower()/] by ID
300 public func get[cls.name/](id: String) async throws -> [cls.name/] {
301 try await get(path: "[cls.name.toLower()/]s/\(id)")
302 }
303
304 /// Create new [cls.name.toLower()/]
305 public func create[cls.name/](_ input: [cls.name/]Input) async throws -> [cls.name/] {
306 try await post(path: "[cls.name.toLower()/]s", body: input)
307 }
308
309 /// Update [cls.name.toLower()/]
310 public func update[cls.name/](id: String, _ input: [cls.name/]Input) async throws -> [cls.name/] {
311 try await put(path: "[cls.name.toLower()/]s/\(id)", body: input)
312 }
313
314 /// Delete [cls.name.toLower()/]
315 public func delete[cls.name/](id: String) async throws {
316 try await delete(path: "[cls.name.toLower()/]s/\(id)")
317 }
318
319[/template]
320
321[comment Generate protocols /]
322[template private generateProtocols(pkg : EPackage)]
323// APIProtocols.swift
324// Generated from [pkg.name/].ecore
325// Protocols for [pkg.name/] API models
326
327import Foundation
328
329// MARK: - Identifiable Protocol
330
331/// Protocol for models with unique identifiers
332public protocol APIIdentifiable {
333 var id: String { get }
334}
335
336// MARK: - Syncable Protocol
337
338/// Protocol for models that support synchronisation
339public protocol Syncable: APIIdentifiable {
340 var lastModified: Date? { get }
341 var version: Int { get }
342}
343
344// MARK: - Input Protocol
345
346/// Protocol for input DTOs used in create/update operations
347public protocol APIInput: Encodable {
348 associatedtype Model: APIIdentifiable
349}
350
351// MARK: - Paged Result
352
353/// Wrapper for paginated results
354public struct PagedResult<T: Decodable>: Decodable {
355 public let items: [T]
356 public let total: Int
357
358 public var hasMore: Bool {
359 items.count < total
360 }
361}
362
363// MARK: - Sync Types
364
365/// Synchronisation status
366public struct SyncStatus: Decodable {
367 public let lastSync: Date?
368 public let pendingChanges: Int
369 public let conflicts: Int
370 public let status: SyncState
371}
372
373/// Synchronisation state
374public enum SyncState: String, Decodable {
375 case synced = "SYNCED"
376 case pending = "PENDING"
377 case conflict = "CONFLICT"
378}
379
380/// Set of changes for synchronisation
381public struct ChangeSet: Codable {
382 public let timestamp: Date
383 public let source: String
384 public let changes: [Change]
385
386 public init(timestamp: Date, source: String, changes: [Change]) {
387 self.timestamp = timestamp
388 self.source = source
389 self.changes = changes
390 }
391}
392
393/// Individual change record
394public struct Change: Codable {
395 public let id: String
396 public let type: ChangeType
397 public let elementType: String
398 public let elementId: String
399 public let properties: [String: String]
400
401 public init(id: String, type: ChangeType, elementType: String, elementId: String, properties: [String: String]) {
402 self.id = id
403 self.type = type
404 self.elementType = elementType
405 self.elementId = elementId
406 self.properties = properties
407 }
408}
409
410/// Type of change
411public enum ChangeType: String, Codable {
412 case add = "ADD"
413 case update = "UPDATE"
414 case delete = "DELETE"
415}
416
417/// Result of pushing changes
418public struct PushResult: Decodable {
419 public let success: Bool
420 public let changesApplied: Int
421 public let newVersion: String
422}
423
424/// Report of conflicts during sync
425public struct ConflictReport: Decodable {
426 public let conflicts: [Conflict]
427}
428
429/// Individual conflict
430public struct Conflict: Decodable {
431 public let elementId: String
432 public let elementType: String
433 public let localChange: Change
434 public let remoteChange: Change
435}
436[/template]
437
438[comment Generate extensions /]
439[template private generateExtensions(pkg : EPackage)]
440// APIExtensions.swift
441// Generated from [pkg.name/].ecore
442// Convenience extensions for [pkg.name/] API
443
444import Foundation
445
446// MARK: - Combine Support
447
448#if canImport(Combine)
449import Combine
450
451extension ProjectManagementAPI {
452[for (cls : EClass | pkg.eClassifiers->filter(EClass))]
453 /// Publisher for listing [cls.name.toLower()/]s
454 public func list[cls.name/]sPublisher(limit: Int = 20, offset: Int = 0) -> AnyPublisher<PagedResult<[cls.name/]>, APIError> {
455 Future { promise in
456 Task {
457 do {
458 let result = try await self.list[cls.name/]s(limit: limit, offset: offset)
459 promise(.success(result))
460 } catch let error as APIError {
461 promise(.failure(error))
462 } catch {
463 promise(.failure(.networkError(error)))
464 }
465 }
466 }
467 .eraseToAnyPublisher()
468 }
469
470[/for]
471}
472#endif
473
474// MARK: - SwiftUI Support
475
476#if canImport(SwiftUI)
477import SwiftUI
478
479/// Environment key for API client
480private struct APIClientKey: EnvironmentKey {
481 static let defaultValue: ProjectManagementAPI? = nil
482}
483
484extension EnvironmentValues {
485 /// The Project Management API client
486 public var projectManagementAPI: ProjectManagementAPI? {
487 get { self[APIClientKey.self] }
488 set { self[APIClientKey.self] = newValue }
489 }
490}
491
492extension View {
493 /// Inject the API client into the environment
494 public func projectManagementAPI(_ api: ProjectManagementAPI) -> some View {
495 environment(\.projectManagementAPI, api)
496 }
497}
498#endif
499
500// MARK: - Convenience Initialisers
501
502extension ProjectManagementAPI {
503 /// Create client with authentication token
504 public convenience init(token: String, environment: APIConfiguration = .production) {
505 var config = environment
506 config = APIConfiguration(
507 baseURL: config.baseURL,
508 authToken: token,
509 timeoutInterval: config.timeoutInterval
510 )
511 self.init(configuration: config)
512 }
513}
514
515// MARK: - Batch Operations
516
517extension ProjectManagementAPI {
518 /// Fetch multiple resources in parallel
519 public func fetchAll<T: Decodable>(
520 _ requests: [() async throws -> T]
521 ) async throws -> [T] {
522 try await withThrowingTaskGroup(of: T.self) { group in
523 for request in requests {
524 group.addTask {
525 try await request()
526 }
527 }
528
529 var results: [T] = []
530 for try await result in group {
531 results.append(result)
532 }
533 return results
534 }
535 }
536}
537[/template]

Step 3

Build real-time synchronisation support.

Create event-driven APIs that support real-time synchronisation between different clients and formats, enabling collaborative modeling scenarios.

GenerateEventAPI.mtl
1[comment encoding = UTF-8 /]
2[comment]
3 MTL Template: Generate Event-Driven API
4 Creates WebSocket and Server-Sent Events support for real-time sync
5 Part of the Cross-Format Integration tutorial
6[/comment]
7
8[module GenerateEventAPI('http://www.eclipse.org/emf/2002/Ecore')]
9
10[comment Main entry point /]
11[template public generate(pkg : EPackage)]
12[comment Generate event types /]
13[file ('Events.swift', false, 'UTF-8')]
14[generateEventTypes(pkg)/]
15[/file]
16
17[comment Generate WebSocket client /]
18[file ('WebSocketClient.swift', false, 'UTF-8')]
19[generateWebSocketClient(pkg)/]
20[/file]
21
22[comment Generate real-time sync manager /]
23[file ('RealTimeSyncManager.swift', false, 'UTF-8')]
24[generateSyncManager(pkg)/]
25[/file]
26[/template]
27
28[comment Generate event types /]
29[template private generateEventTypes(pkg : EPackage)]
30// Events.swift
31// Generated from [pkg.name/].ecore
32// Event types for real-time synchronisation
33
34import Foundation
35
36// MARK: - Event Protocol
37
38/// Protocol for all synchronisation events
39public protocol SyncEvent: Codable, Sendable {
40 /// Unique event identifier
41 var eventId: String { get }
42
43 /// Timestamp when event was created
44 var timestamp: Date { get }
45
46 /// Source that generated the event
47 var source: String { get }
48
49 /// Type identifier for the event
50 static var eventType: String { get }
51}
52
53// MARK: - Event Types
54
55/// Event indicating a model element was created
56public struct ElementCreatedEvent: SyncEvent {
57 public static let eventType = "element.created"
58
59 public let eventId: String
60 public let timestamp: Date
61 public let source: String
62 public let elementType: String
63 public let elementId: String
64 public let containerId: String?
65 public let properties: [String: String]
66
67 public init(
68 eventId: String = UUID().uuidString,
69 timestamp: Date = Date(),
70 source: String,
71 elementType: String,
72 elementId: String,
73 containerId: String? = nil,
74 properties: [String: String]
75 ) {
76 self.eventId = eventId
77 self.timestamp = timestamp
78 self.source = source
79 self.elementType = elementType
80 self.elementId = elementId
81 self.containerId = containerId
82 self.properties = properties
83 }
84}
85
86/// Event indicating a model element was updated
87public struct ElementUpdatedEvent: SyncEvent {
88 public static let eventType = "element.updated"
89
90 public let eventId: String
91 public let timestamp: Date
92 public let source: String
93 public let elementType: String
94 public let elementId: String
95 public let changedProperties: [PropertyChange]
96
97 public init(
98 eventId: String = UUID().uuidString,
99 timestamp: Date = Date(),
100 source: String,
101 elementType: String,
102 elementId: String,
103 changedProperties: [PropertyChange]
104 ) {
105 self.eventId = eventId
106 self.timestamp = timestamp
107 self.source = source
108 self.elementType = elementType
109 self.elementId = elementId
110 self.changedProperties = changedProperties
111 }
112}
113
114/// Event indicating a model element was deleted
115public struct ElementDeletedEvent: SyncEvent {
116 public static let eventType = "element.deleted"
117
118 public let eventId: String
119 public let timestamp: Date
120 public let source: String
121 public let elementType: String
122 public let elementId: String
123
124 public init(
125 eventId: String = UUID().uuidString,
126 timestamp: Date = Date(),
127 source: String,
128 elementType: String,
129 elementId: String
130 ) {
131 self.eventId = eventId
132 self.timestamp = timestamp
133 self.source = source
134 self.elementType = elementType
135 self.elementId = elementId
136 }
137}
138
139/// Event indicating a conflict was detected
140public struct ConflictDetectedEvent: SyncEvent {
141 public static let eventType = "sync.conflict"
142
143 public let eventId: String
144 public let timestamp: Date
145 public let source: String
146 public let elementId: String
147 public let elementType: String
148 public let conflictingSource: String
149 public let requiresResolution: Bool
150
151 public init(
152 eventId: String = UUID().uuidString,
153 timestamp: Date = Date(),
154 source: String,
155 elementId: String,
156 elementType: String,
157 conflictingSource: String,
158 requiresResolution: Bool
159 ) {
160 self.eventId = eventId
161 self.timestamp = timestamp
162 self.source = source
163 self.elementId = elementId
164 self.elementType = elementType
165 self.conflictingSource = conflictingSource
166 self.requiresResolution = requiresResolution
167 }
168}
169
170/// Event indicating synchronisation completed
171public struct SyncCompletedEvent: SyncEvent {
172 public static let eventType = "sync.completed"
173
174 public let eventId: String
175 public let timestamp: Date
176 public let source: String
177 public let changesApplied: Int
178 public let newVersion: String
179
180 public init(
181 eventId: String = UUID().uuidString,
182 timestamp: Date = Date(),
183 source: String,
184 changesApplied: Int,
185 newVersion: String
186 ) {
187 self.eventId = eventId
188 self.timestamp = timestamp
189 self.source = source
190 self.changesApplied = changesApplied
191 self.newVersion = newVersion
192 }
193}
194
195// MARK: - Property Change
196
197/// Represents a change to a single property
198public struct PropertyChange: Codable, Sendable {
199 public let propertyName: String
200 public let oldValue: String?
201 public let newValue: String
202
203 public init(propertyName: String, oldValue: String?, newValue: String) {
204 self.propertyName = propertyName
205 self.oldValue = oldValue
206 self.newValue = newValue
207 }
208}
209
210// MARK: - Event Envelope
211
212/// Wrapper for events with type information
213public struct EventEnvelope: Codable, Sendable {
214 public let type: String
215 public let payload: Data
216
217 public init<E: SyncEvent>(_ event: E) throws {
218 self.type = E.eventType
219 self.payload = try JSONEncoder().encode(event)
220 }
221
222 public func decode<E: SyncEvent>(_ eventType: E.Type) throws -> E {
223 try JSONDecoder().decode(E.self, from: payload)
224 }
225}
226
227// MARK: - Event Type Registry
228
229[for (cls : EClass | pkg.eClassifiers->filter(EClass))]
230/// Event for [cls.name/] creation
231public typealias [cls.name/]CreatedEvent = ElementCreatedEvent
232
233/// Event for [cls.name/] update
234public typealias [cls.name/]UpdatedEvent = ElementUpdatedEvent
235
236/// Event for [cls.name/] deletion
237public typealias [cls.name/]DeletedEvent = ElementDeletedEvent
238
239[/for]
240[/template]
241
242[comment Generate WebSocket client /]
243[template private generateWebSocketClient(pkg : EPackage)]
244// WebSocketClient.swift
245// Generated from [pkg.name/].ecore
246// WebSocket client for real-time event streaming
247
248import Foundation
249
250// MARK: - WebSocket Configuration
251
252/// Configuration for WebSocket connection
253public struct WebSocketConfiguration: Sendable {
254 public let url: URL
255 public let authToken: String?
256 public let reconnectDelay: TimeInterval
257 public let maxReconnectAttempts: Int
258
259 public static let production = WebSocketConfiguration(
260 url: URL(string: "wss://api.acme.com.au/v1/events")!,
261 authToken: nil,
262 reconnectDelay: 1.0,
263 maxReconnectAttempts: 5
264 )
265
266 public init(
267 url: URL,
268 authToken: String? = nil,
269 reconnectDelay: TimeInterval = 1.0,
270 maxReconnectAttempts: Int = 5
271 ) {
272 self.url = url
273 self.authToken = authToken
274 self.reconnectDelay = reconnectDelay
275 self.maxReconnectAttempts = maxReconnectAttempts
276 }
277}
278
279// MARK: - Connection State
280
281/// State of the WebSocket connection
282public enum ConnectionState: Sendable {
283 case disconnected
284 case connecting
285 case connected
286 case reconnecting(attempt: Int)
287 case failed(Error)
288}
289
290// MARK: - WebSocket Client
291
292/// Client for receiving real-time sync events
293@MainActor
294public final class SyncWebSocketClient: ObservableObject {
295 /// Current connection state
296 @Published public private(set) var connectionState: ConnectionState = .disconnected
297
298 /// Configuration
299 private let configuration: WebSocketConfiguration
300
301 /// WebSocket task
302 private var webSocketTask: URLSessionWebSocketTask?
303
304 /// URL session
305 private let session: URLSession
306
307 /// Event handlers
308 private var eventHandlers: [String: [(Data) -> Void]] = ['['/]:['/]]
309
310 /// Reconnect attempt counter
311 private var reconnectAttempts = 0
312
313 /// Initialise with configuration
314 public init(configuration: WebSocketConfiguration) {
315 self.configuration = configuration
316 self.session = URLSession(configuration: .default)
317 }
318
319 // MARK: - Connection Management
320
321 /// Connect to the WebSocket server
322 public func connect() async {
323 guard case .disconnected = connectionState else { return }
324
325 connectionState = .connecting
326
327 var request = URLRequest(url: configuration.url)
328 if let token = configuration.authToken {
329 request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorisation")
330 }
331
332 webSocketTask = session.webSocketTask(with: request)
333 webSocketTask?.resume()
334
335 connectionState = .connected
336 reconnectAttempts = 0
337
338 await receiveMessages()
339 }
340
341 /// Disconnect from the WebSocket server
342 public func disconnect() {
343 webSocketTask?.cancel(with: .normalClosure, reason: nil)
344 webSocketTask = nil
345 connectionState = .disconnected
346 }
347
348 /// Receive messages in a loop
349 private func receiveMessages() async {
350 guard let task = webSocketTask else { return }
351
352 do {
353 while case .connected = connectionState {
354 let message = try await task.receive()
355
356 switch message {
357 case .string(let text):
358 if let data = text.data(using: .utf8) {
359 handleMessage(data)
360 }
361 case .data(let data):
362 handleMessage(data)
363 @unknown default:
364 break
365 }
366 }
367 } catch {
368 handleConnectionError(error)
369 }
370 }
371
372 /// Handle incoming message
373 private func handleMessage(_ data: Data) {
374 do {
375 let envelope = try JSONDecoder().decode(EventEnvelope.self, from: data)
376 if let handlers = eventHandlers['['/]envelope.type[']'/] {
377 for handler in handlers {
378 handler(envelope.payload)
379 }
380 }
381 } catch {
382 print("Failed to decode event: \(error)")
383 }
384 }
385
386 /// Handle connection error
387 private func handleConnectionError(_ error: Error) {
388 if reconnectAttempts < configuration.maxReconnectAttempts {
389 reconnectAttempts += 1
390 connectionState = .reconnecting(attempt: reconnectAttempts)
391
392 Task {
393 try? await Task.sleep(nanoseconds: UInt64(configuration.reconnectDelay * 1_000_000_000))
394 await connect()
395 }
396 } else {
397 connectionState = .failed(error)
398 }
399 }
400
401 // MARK: - Event Subscription
402
403 /// Subscribe to events of a specific type
404 public func subscribe<E: SyncEvent>(
405 to eventType: E.Type,
406 handler: @escaping (E) -> Void
407 ) {
408 let typeKey = E.eventType
409 let wrapper: (Data) -> Void = { data in
410 do {
411 let event = try JSONDecoder().decode(E.self, from: data)
412 handler(event)
413 } catch {
414 print("Failed to decode event of type \(typeKey): \(error)")
415 }
416 }
417
418 if eventHandlers['['/]typeKey[']'/] == nil {
419 eventHandlers['['/]typeKey[']'/] = ['['/][']'/]
420 }
421 eventHandlers['['/]typeKey[']'/]?.append(wrapper)
422 }
423
424 /// Subscribe to all element events for a specific type
425 public func subscribeToElementEvents(
426 elementType: String,
427 onCreate: ((ElementCreatedEvent) -> Void)? = nil,
428 onUpdate: ((ElementUpdatedEvent) -> Void)? = nil,
429 onDelete: ((ElementDeletedEvent) -> Void)? = nil
430 ) {
431 if let onCreate = onCreate {
432 subscribe(to: ElementCreatedEvent.self) { event in
433 if event.elementType == elementType {
434 onCreate(event)
435 }
436 }
437 }
438 if let onUpdate = onUpdate {
439 subscribe(to: ElementUpdatedEvent.self) { event in
440 if event.elementType == elementType {
441 onUpdate(event)
442 }
443 }
444 }
445 if let onDelete = onDelete {
446 subscribe(to: ElementDeletedEvent.self) { event in
447 if event.elementType == elementType {
448 onDelete(event)
449 }
450 }
451 }
452 }
453
454 // MARK: - Event Publishing
455
456 /// Send an event to the server
457 public func send<E: SyncEvent>(_ event: E) async throws {
458 guard let task = webSocketTask else {
459 throw APIError.networkError(NSError(domain: "WebSocket", code: -1))
460 }
461
462 let envelope = try EventEnvelope(event)
463 let data = try JSONEncoder().encode(envelope)
464 let message = URLSessionWebSocketTask.Message.data(data)
465
466 try await task.send(message)
467 }
468}
469[/template]
470
471[comment Generate sync manager /]
472[template private generateSyncManager(pkg : EPackage)]
473// RealTimeSyncManager.swift
474// Generated from [pkg.name/].ecore
475// Manager for real-time model synchronisation
476
477import Foundation
478import Combine
479
480// MARK: - Sync Manager
481
482/// Manages real-time synchronisation between local and remote models
483@MainActor
484public final class RealTimeSyncManager: ObservableObject {
485 /// The API client for REST operations
486 private let apiClient: ProjectManagementAPI
487
488 /// The WebSocket client for real-time events
489 private let webSocketClient: SyncWebSocketClient
490
491 /// Local change buffer
492 @Published public private(set) var pendingChanges: [Change] = ['['/][']'/]
493
494 /// Conflict queue
495 @Published public private(set) var unresolvedConflicts: [Conflict] = ['['/][']'/]
496
497 /// Sync status
498 @Published public private(set) var isSyncing = false
499
500 /// Source identifier for this client
501 public let sourceId: String
502
503 /// Combine subscriptions
504 private var cancellables = Set<AnyCancellable>()
505
506 /// Initialise sync manager
507 public init(
508 apiClient: ProjectManagementAPI,
509 webSocketClient: SyncWebSocketClient,
510 sourceId: String = "swift-\(UUID().uuidString.prefix(8))"
511 ) {
512 self.apiClient = apiClient
513 self.webSocketClient = webSocketClient
514 self.sourceId = sourceId
515
516 setupEventHandlers()
517 }
518
519 // MARK: - Setup
520
521 /// Configure event handlers
522 private func setupEventHandlers() {
523 // Handle incoming create events
524 webSocketClient.subscribe(to: ElementCreatedEvent.self) { ['['/]weak self[']'/] event in
525 guard let self = self, event.source != self.sourceId else { return }
526 Task { @MainActor in
527 await self.handleRemoteCreate(event)
528 }
529 }
530
531 // Handle incoming update events
532 webSocketClient.subscribe(to: ElementUpdatedEvent.self) { ['['/]weak self[']'/] event in
533 guard let self = self, event.source != self.sourceId else { return }
534 Task { @MainActor in
535 await self.handleRemoteUpdate(event)
536 }
537 }
538
539 // Handle incoming delete events
540 webSocketClient.subscribe(to: ElementDeletedEvent.self) { ['['/]weak self[']'/] event in
541 guard let self = self, event.source != self.sourceId else { return }
542 Task { @MainActor in
543 await self.handleRemoteDelete(event)
544 }
545 }
546
547 // Handle conflict events
548 webSocketClient.subscribe(to: ConflictDetectedEvent.self) { ['['/]weak self[']'/] event in
549 guard let self = self else { return }
550 Task { @MainActor in
551 await self.handleConflict(event)
552 }
553 }
554 }
555
556 // MARK: - Connection
557
558 /// Start real-time synchronisation
559 public func start() async {
560 await webSocketClient.connect()
561
562 // Pull any changes since last sync
563 do {
564 let changeSet = try await apiClient.pullChanges()
565 await applyRemoteChanges(changeSet)
566 } catch {
567 print("Failed to pull initial changes: \(error)")
568 }
569 }
570
571 /// Stop real-time synchronisation
572 public func stop() {
573 webSocketClient.disconnect()
574 }
575
576 // MARK: - Local Changes
577
578 /// Record a local element creation
579 public func recordCreate(elementType: String, elementId: String, properties: [String: String]) {
580 let change = Change(
581 id: UUID().uuidString,
582 type: .add,
583 elementType: elementType,
584 elementId: elementId,
585 properties: properties
586 )
587 pendingChanges.append(change)
588
589 // Broadcast to other clients
590 let event = ElementCreatedEvent(
591 source: sourceId,
592 elementType: elementType,
593 elementId: elementId,
594 properties: properties
595 )
596 Task {
597 try? await webSocketClient.send(event)
598 }
599 }
600
601 /// Record a local element update
602 public func recordUpdate(elementType: String, elementId: String, changes: [PropertyChange]) {
603 var properties: [String: String] = [:]
604 for change in changes {
605 properties['['/]change.propertyName[']'/] = change.newValue
606 }
607
608 let change = Change(
609 id: UUID().uuidString,
610 type: .update,
611 elementType: elementType,
612 elementId: elementId,
613 properties: properties
614 )
615 pendingChanges.append(change)
616
617 // Broadcast to other clients
618 let event = ElementUpdatedEvent(
619 source: sourceId,
620 elementType: elementType,
621 elementId: elementId,
622 changedProperties: changes
623 )
624 Task {
625 try? await webSocketClient.send(event)
626 }
627 }
628
629 /// Record a local element deletion
630 public func recordDelete(elementType: String, elementId: String) {
631 let change = Change(
632 id: UUID().uuidString,
633 type: .delete,
634 elementType: elementType,
635 elementId: elementId,
636 properties: [:]
637 )
638 pendingChanges.append(change)
639
640 // Broadcast to other clients
641 let event = ElementDeletedEvent(
642 source: sourceId,
643 elementType: elementType,
644 elementId: elementId
645 )
646 Task {
647 try? await webSocketClient.send(event)
648 }
649 }
650
651 // MARK: - Sync Operations
652
653 /// Push pending changes to server
654 public func pushChanges() async throws {
655 guard !pendingChanges.isEmpty else { return }
656
657 isSyncing = true
658 defer { isSyncing = false }
659
660 let changeSet = ChangeSet(
661 timestamp: Date(),
662 source: sourceId,
663 changes: pendingChanges
664 )
665
666 do {
667 let result = try await apiClient.pushChanges(changeSet)
668 if result.success {
669 pendingChanges.removeAll()
670 }
671 } catch APIError.conflict(let report) {
672 unresolvedConflicts.append(contentsOf: report.conflicts)
673 throw APIError.conflict(report)
674 }
675 }
676
677 // MARK: - Remote Event Handlers
678
679 private func handleRemoteCreate(_ event: ElementCreatedEvent) async {
680 // Notify observers about remote creation
681 NotificationCenter.default.post(
682 name: .remoteElementCreated,
683 object: nil,
684 userInfo: ["event": event]
685 )
686 }
687
688 private func handleRemoteUpdate(_ event: ElementUpdatedEvent) async {
689 // Check for conflicts with pending changes
690 let conflicts = pendingChanges.filter { change in
691 change.elementId == event.elementId && change.type == .update
692 }
693
694 if !conflicts.isEmpty {
695 // Local change conflicts with remote
696 let conflictEvent = ConflictDetectedEvent(
697 source: sourceId,
698 elementId: event.elementId,
699 elementType: event.elementType,
700 conflictingSource: event.source,
701 requiresResolution: true
702 )
703 await handleConflict(conflictEvent)
704 } else {
705 // No conflict, notify observers
706 NotificationCenter.default.post(
707 name: .remoteElementUpdated,
708 object: nil,
709 userInfo: ["event": event]
710 )
711 }
712 }
713
714 private func handleRemoteDelete(_ event: ElementDeletedEvent) async {
715 // Remove from pending changes if we have updates for this element
716 pendingChanges.removeAll { $0.elementId == event.elementId }
717
718 NotificationCenter.default.post(
719 name: .remoteElementDeleted,
720 object: nil,
721 userInfo: ["event": event]
722 )
723 }
724
725 private func handleConflict(_ event: ConflictDetectedEvent) async {
726 // Add to unresolved conflicts
727 // In a real app, this would trigger UI for conflict resolution
728 print("Conflict detected for \(event.elementType) [\(event.elementId)]")
729 }
730
731 private func applyRemoteChanges(_ changeSet: ChangeSet) async {
732 for change in changeSet.changes {
733 switch change.type {
734 case .add:
735 NotificationCenter.default.post(
736 name: .remoteElementCreated,
737 object: nil,
738 userInfo: ["change": change]
739 )
740 case .update:
741 NotificationCenter.default.post(
742 name: .remoteElementUpdated,
743 object: nil,
744 userInfo: ["change": change]
745 )
746 case .delete:
747 NotificationCenter.default.post(
748 name: .remoteElementDeleted,
749 object: nil,
750 userInfo: ["change": change]
751 )
752 }
753 }
754 }
755}
756
757// MARK: - Notification Names
758
759extension Notification.Name {
760 static let remoteElementCreated = Notification.Name("remoteElementCreated")
761 static let remoteElementUpdated = Notification.Name("remoteElementUpdated")
762 static let remoteElementDeleted = Notification.Name("remoteElementDeleted")
763}
764[/template]

Step 4

Deploy and test the complete integration system.

Deploy the complete cross-format integration system and run comprehensive tests that validate end-to-end functionality across all supported formats and clients.

Terminal
1# Deploy and test the complete cross-format integration system
2# Final step of the Cross-Format Integration tutorial
3
4echo "========================================"
5echo "Cross-Format Integration Deployment"
6echo "========================================"
7echo ""
8
9# Step 1: Generate all code artefacts
10echo "=== Step 1: Generate Code Artefacts ==="
11
12# Generate REST API server
13swift-mtl generate GenerateRestAPI.mtl \
14 --metamodel ProjectManagement.ecore \
15 --output ./Server/
16
17# Output:
18# [GENERATED] openapi.yaml
19# [GENERATED] routes.swift
20# [GENERATED] DTOs.swift
21
22# Generate Swift API client
23swift-mtl generate GenerateSwiftAPI.mtl \
24 --metamodel ProjectManagement.ecore \
25 --output ./Client/
26
27# Output:
28# [GENERATED] ProjectManagementAPI.swift
29# [GENERATED] APIProtocols.swift
30# [GENERATED] APIExtensions.swift
31
32# Generate event API
33swift-mtl generate GenerateEventAPI.mtl \
34 --metamodel ProjectManagement.ecore \
35 --output ./Events/
36
37# Output:
38# [GENERATED] Events.swift
39# [GENERATED] WebSocketClient.swift
40# [GENERATED] RealTimeSyncManager.swift
41
42echo ""
43echo "=== Step 2: Build Server ==="
44
45# Build server application
46cd ./Server
47swift build --configuration release
48
49# Output:
50# Building for production...
51# Build complete!
52
53echo ""
54echo "=== Step 3: Deploy to Staging ==="
55
56# Deploy to staging environment
57deploy-tool deploy \
58 --environment staging \
59 --app project-management-api \
60 --version 1.0.0
61
62# Output:
63# [DEPLOY] Uploading artefacts to staging...
64# [DEPLOY] Configuring environment variables...
65# [DEPLOY] Starting application...
66# [DEPLOY] Health check: PASSED
67# [DEPLOY] Deployment complete: https://api-staging.acme.com.au/v1
68
69echo ""
70echo "=== Step 4: Run Integration Tests ==="
71
72# Test XMI -> JSON conversion
73echo "Testing XMI to JSON conversion..."
74swift-integration-test \
75 --scenario xmi-to-json \
76 --input test-data/sample-project.xmi \
77 --expected test-data/expected-project.json
78
79# Output:
80# [TEST] Loading XMI: test-data/sample-project.xmi
81# [TEST] Converting to JSON...
82# [TEST] Comparing with expected output...
83# [TEST] PASSED: XMI to JSON conversion
84
85# Test JSON -> XMI conversion
86echo "Testing JSON to XMI conversion..."
87swift-integration-test \
88 --scenario json-to-xmi \
89 --input test-data/sample-project.json \
90 --expected test-data/expected-project.xmi
91
92# Output:
93# [TEST] Loading JSON: test-data/sample-project.json
94# [TEST] Converting to XMI...
95# [TEST] Comparing with expected output...
96# [TEST] PASSED: JSON to XMI conversion
97
98# Test roundtrip integrity
99echo "Testing roundtrip integrity..."
100swift-integration-test \
101 --scenario roundtrip \
102 --input test-data/sample-project.xmi \
103 --format-chain "xmi,json,swift,json,xmi"
104
105# Output:
106# [TEST] Starting roundtrip test...
107# [TEST] XMI -> JSON: OK
108# [TEST] JSON -> Swift: OK
109# [TEST] Swift -> JSON: OK
110# [TEST] JSON -> XMI: OK
111# [TEST] Comparing original and result...
112# [TEST] PASSED: Roundtrip integrity maintained
113
114# Test synchronisation
115echo "Testing bidirectional synchronisation..."
116swift-integration-test \
117 --scenario sync \
118 --base test-data/base-model.xmi \
119 --changes-xmi test-data/xmi-changes.json \
120 --changes-json test-data/json-changes.json
121
122# Output:
123# [TEST] Loading base model...
124# [TEST] Applying XMI changes...
125# [TEST] Applying JSON changes...
126# [TEST] Detecting conflicts...
127# [TEST] Resolving conflicts...
128# [TEST] Verifying merged model...
129# [TEST] PASSED: Bidirectional synchronisation
130
131# Test real-time events
132echo "Testing real-time event streaming..."
133swift-integration-test \
134 --scenario realtime \
135 --clients 3 \
136 --duration 10s
137
138# Output:
139# [TEST] Spawning 3 test clients...
140# [TEST] Client 1: Connected
141# [TEST] Client 2: Connected
142# [TEST] Client 3: Connected
143# [TEST] Client 1: Creating element...
144# [TEST] Client 2: Received create event
145# [TEST] Client 3: Received create event
146# [TEST] Client 2: Updating element...
147# [TEST] Client 1: Received update event
148# [TEST] Client 3: Received update event
149# [TEST] Event propagation time: avg 45ms
150# [TEST] PASSED: Real-time event streaming
151
152echo ""
153echo "=== Step 5: Performance Benchmarks ==="
154
155swift-benchmark \
156 --scenarios all \
157 --iterations 100
158
159# Output:
160# Benchmark Results:
161# +--------------------------+----------+----------+----------+
162# | Scenario | Min | Avg | Max |
163# +--------------------------+----------+----------+----------+
164# | XMI Parse (1000 elems) | 12ms | 15ms | 22ms |
165# | JSON Parse (1000 elems) | 8ms | 10ms | 14ms |
166# | XMI to JSON Transform | 25ms | 32ms | 48ms |
167# | JSON to XMI Transform | 28ms | 35ms | 52ms |
168# | Conflict Detection | 5ms | 8ms | 12ms |
169# | Conflict Resolution | 10ms | 15ms | 25ms |
170# | Full Sync Cycle | 85ms | 110ms | 145ms |
171# | WebSocket Event | 2ms | 5ms | 10ms |
172# +--------------------------+----------+----------+----------+
173# All benchmarks within acceptable limits.
174
175echo ""
176echo "=== Step 6: Deploy to Production ==="
177
178# Confirm deployment
179read -p "Deploy to production? (y/n) " confirm
180if [ "$confirm" = "y" ]; then
181 deploy-tool deploy \
182 --environment production \
183 --app project-management-api \
184 --version 1.0.0 \
185 --rollback-on-failure
186
187 # Output:
188 # [DEPLOY] Creating production backup...
189 # [DEPLOY] Uploading artefacts to production...
190 # [DEPLOY] Configuring environment variables...
191 # [DEPLOY] Starting blue-green deployment...
192 # [DEPLOY] Routing traffic to new version...
193 # [DEPLOY] Health check: PASSED
194 # [DEPLOY] Deployment complete: https://api.acme.com.au/v1
195fi
196
197echo ""
198echo "========================================"
199echo "Deployment Summary"
200echo "========================================"
201echo ""
202echo "Environments:"
203echo " - Staging: https://api-staging.acme.com.au/v1"
204echo " - Production: https://api.acme.com.au/v1"
205echo ""
206echo "API Documentation:"
207echo " - OpenAPI Spec: https://api.acme.com.au/v1/docs/openapi.yaml"
208echo " - Interactive Docs: https://api.acme.com.au/v1/docs"
209echo ""
210echo "WebSocket Endpoints:"
211echo " - Staging: wss://api-staging.acme.com.au/v1/events"
212echo " - Production: wss://api.acme.com.au/v1/events"
213echo ""
214echo "Generated Client Libraries:"
215echo " - Swift Package: ./Client/ProjectManagementAPI.swift"
216echo " - Event Types: ./Events/Events.swift"
217echo " - Sync Manager: ./Events/RealTimeSyncManager.swift"
218echo ""
219echo "Integration Test Results: ALL PASSED"
220echo "Performance Benchmarks: WITHIN LIMITS"
221echo ""
222echo "Cross-Format Integration deployment complete!"

Check Your Understanding

Question 1 of 4

What is the main challenge in cross-format integration?

Question 2 of 4

Why might JSON representation differ from XMI representation?

Question 3 of 4

What is bidirectional synchronisation?

Question 4 of 4

How should conflicts be handled during synchronisation?

Class to Relational Transformation

Master the classic Class2Relational transformation benchmark, a foundational example in model-driven engineering.