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]