diff --git a/CHANGELOG.md b/CHANGELOG.md index 51afc5b..6be0304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ This is a lockstep workspace: all packages share the same version and release no ## [Unreleased] +### Fixed + +- Fixed handler type matching to support inheritance hierarchies (sealed classes, abstract base classes). + - Handlers registered for a base class now correctly receive events of subclass types. + - Previously, publishing `SubclassEvent` would not trigger handlers registered for `BaseClass`. + - This is critical for freezed/sealed class patterns where events have multiple subtypes. + ## [3.0.1] - 2026-01-22 ### Changed diff --git a/packages/raiser/lib/src/bus/error_strategy.dart b/packages/raiser/lib/src/bus/error_strategy.dart index 009a5ff..8941014 100644 --- a/packages/raiser/lib/src/bus/error_strategy.dart +++ b/packages/raiser/lib/src/bus/error_strategy.dart @@ -34,6 +34,5 @@ class AggregateException implements Exception { AggregateException(this.errors, this.stackTraces); @override - String toString() => - 'AggregateException: ${errors.length} error${errors.length == 1 ? '' : 's'} occurred'; + String toString() => 'AggregateException: ${errors.length} error${errors.length == 1 ? '' : 's'} occurred'; } diff --git a/packages/raiser/lib/src/bus/event_bus.dart b/packages/raiser/lib/src/bus/event_bus.dart index 6b6f2a2..2f6909d 100644 --- a/packages/raiser/lib/src/bus/event_bus.dart +++ b/packages/raiser/lib/src/bus/event_bus.dart @@ -14,9 +14,17 @@ typedef ErrorCallback = void Function(Object error, StackTrace stackTrace); typedef Middleware = Future Function(dynamic event, Future Function() next); /// Internal class for storing handler registrations with metadata. -class _HandlerEntry { - /// The handler function to invoke. - final Future Function(T) handler; +/// +/// Stores handlers in a type-erased manner to enable inheritance-aware +/// type matching. The [canHandle] predicate checks if an event is assignable +/// to the handler's expected type, and [invoke] wraps the typed handler +/// to allow invocation with a dynamic event. +class _HandlerEntry { + /// Type-erased handler invocation. + /// + /// Wraps the original typed handler and performs the cast internally. + /// This is safe because [canHandle] guarantees type compatibility. + final Future Function(dynamic) invoke; /// Priority for ordering (higher = earlier execution). final int priority; @@ -24,7 +32,14 @@ class _HandlerEntry { /// Registration order for stable sorting within same priority. final int registrationOrder; - _HandlerEntry(this.handler, this.priority, this.registrationOrder); + /// Type predicate to check if an event is assignable to this handler's type. + /// + /// Uses runtime type checking to support inheritance and sealed class + /// hierarchies. This allows a handler registered for `BaseEvent` to receive + /// events of type `SubclassEvent`. + final bool Function(dynamic) canHandle; + + _HandlerEntry(this.invoke, this.priority, this.registrationOrder, this.canHandle); } /// Internal class for storing middleware registrations with metadata. @@ -66,9 +81,12 @@ class EventBus { /// Counter for assigning registration order to middleware. int _middlewareCounter = 0; - /// Type-based handler storage. - /// Maps runtime types to lists of handler entries. - final Map>> _handlers = {}; + /// Flat list of all registered handler entries. + /// + /// We use a flat list instead of a type-keyed map to support + /// inheritance-aware type matching. Each entry contains a type predicate + /// that checks if an event is assignable to the handler's expected type. + final List<_HandlerEntry> _handlers = []; /// Middleware pipeline. final List<_MiddlewareEntry> _middleware = []; @@ -138,14 +156,26 @@ class EventBus { } /// Internal method to add a handler to the registry. + /// + /// Creates a handler entry with a type predicate that uses runtime type + /// checking to support inheritance hierarchies (e.g., sealed classes with + /// multiple subclasses). The handler is wrapped in a type-erased invocation + /// function that performs the cast internally. Subscription _addHandler(Future Function(T) handler, int priority) { - final entry = _HandlerEntry(handler, priority, _registrationCounter++); + // Create a type predicate that checks if an event is assignable to T. + // This enables inheritance-aware matching where a handler for BaseEvent + // will receive events of SubclassEvent. + bool canHandle(dynamic event) => event is T; - _handlers.putIfAbsent(T, () => []); - _handlers[T]!.add(entry); + // Wrap the typed handler in a type-erased function that performs the cast. + // This is safe because canHandle guarantees the event is of type T. + Future invoke(dynamic event) => handler(event as T); + + final entry = _HandlerEntry(invoke, priority, _registrationCounter++, canHandle); + _handlers.add(entry); return Subscription(() { - _handlers[T]?.remove(entry); + _handlers.remove(entry); }); } @@ -196,26 +226,34 @@ class EventBus { } /// Executes handlers for the given event. + /// + /// Uses inheritance-aware type matching to find all handlers that can + /// process the event. This supports sealed class hierarchies where a + /// handler registered for a base class receives subclass events. Future _executeHandlers(T event) async { - final entries = _handlers[T]; - if (entries == null || entries.isEmpty) { + // Find all handlers whose type predicate matches the event. + // This enables inheritance: a handler for BaseEvent will match SubclassEvent. + final matchingEntries = _handlers.where((entry) => entry.canHandle(event)).toList(); + + if (matchingEntries.isEmpty) { return; } // Sort by priority (descending) then by registration order (ascending) - final sortedEntries = List<_HandlerEntry>.from(entries) - ..sort((a, b) { - final priorityCompare = b.priority.compareTo(a.priority); - if (priorityCompare != 0) return priorityCompare; - return a.registrationOrder.compareTo(b.registrationOrder); - }); + matchingEntries.sort((a, b) { + final priorityCompare = b.priority.compareTo(a.priority); + if (priorityCompare != 0) return priorityCompare; + return a.registrationOrder.compareTo(b.registrationOrder); + }); final errors = []; final stackTraces = []; - for (final entry in sortedEntries) { + for (final entry in matchingEntries) { try { - await (entry as _HandlerEntry).handler(event); + // The canHandle predicate guarantees the event is assignable to the + // handler's type, so the internal cast in invoke is safe. + await entry.invoke(event); } catch (error, stackTrace) { // Always invoke the error callback if configured onError?.call(error, stackTrace); diff --git a/packages/raiser/test/edge_cases_test.dart b/packages/raiser/test/edge_cases_test.dart index 3b77247..e9a0c35 100644 --- a/packages/raiser/test/edge_cases_test.dart +++ b/packages/raiser/test/edge_cases_test.dart @@ -242,6 +242,107 @@ void main() { expect(invoked, isFalse); }); + + test('handler for base class receives subclass events', () async { + // Verifies inheritance-aware type matching for sealed class hierarchies. + // A handler registered for the sealed base class should receive events + // from any of its subclasses. + final bus = EventBus(); + final received = []; + + bus.on((event) async { + received.add(event); + }); + + // Publish subclass events - they should be received by the base handler + await bus.publish(SubEventA('first')); + await bus.publish(SubEventB(42)); + + expect(received.length, equals(2)); + expect(received[0], isA()); + expect(received[1], isA()); + }); + + test('handler for specific subclass ignores other subtypes', () async { + // A handler for a specific subclass should not receive events from + // sibling subclasses. + final bus = EventBus(); + final receivedA = []; + final receivedB = []; + + bus.on((event) async { + receivedA.add(event); + }); + bus.on((event) async { + receivedB.add(event); + }); + + await bus.publish(SubEventA('test')); + + // Only SubEventA handler should have received the event + expect(receivedA.length, equals(1)); + expect(receivedB.length, equals(0)); + }); + + test('both base and subclass handlers receive subclass events', () async { + // When handlers are registered for both base and subclass, publishing a + // subclass event should trigger both handlers. + final bus = EventBus(); + final baseReceived = []; + final subReceived = []; + + bus.on((event) async { + baseReceived.add(event); + }); + bus.on((event) async { + subReceived.add(event); + }); + + await bus.publish(SubEventA('test')); + + // Both handlers should receive the event + expect(baseReceived.length, equals(1)); + expect(subReceived.length, equals(1)); + }); + + test('handler priority works correctly with inheritance', () async { + // Priority ordering should work correctly when multiple handlers at + // different inheritance levels receive the same event. + final bus = EventBus(); + final order = []; + + // Register handlers with explicit priorities + bus.on((event) async { + order.add('sub:low'); + }, priority: 0); + bus.on((event) async { + order.add('base:high'); + }, priority: 10); + bus.on((event) async { + order.add('sub:high'); + }, priority: 5); + + await bus.publish(SubEventA('test')); + + // Should execute in priority order: base:high (10), sub:high (5), sub:low (0) + expect(order, equals(['base:high', 'sub:high', 'sub:low'])); + }); + + test('class-based handler receives subclass events', () async { + // Verifies that EventHandler receives subclass events, + // matching the pattern described in the user's issue. + final bus = EventBus(); + final handler = BaseEventHandler(); + + bus.register(handler); + + await bus.publish(SubEventA('test')); + await bus.publish(SubEventB(123)); + + expect(handler.received.length, equals(2)); + expect(handler.received[0], isA()); + expect(handler.received[1], isA()); + }); }); group('Memory and Cleanup Edge Cases', () { @@ -386,3 +487,62 @@ final class OtherEvent implements RaiserEvent { Map toMetadataMap() => {'value': value}; } + +/// Base event class for testing inheritance-aware type matching. +/// +/// Simulates a sealed class hierarchy where handlers can be registered for +/// the base type and receive subclass events. +sealed class BaseEvent implements RaiserEvent { + const BaseEvent({required this.id, required this.occurredOn, required this.metadata}); + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +/// First subclass of [BaseEvent] for testing inheritance. +final class SubEventA extends BaseEvent { + SubEventA( + this.value, { + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : super( + id: eventId ?? EventId.fromUlid(), + occurredOn: occurredOn ?? DateTime.now(), + metadata: Map.unmodifiable(metadata), + ); + + final String value; +} + +/// Second subclass of [BaseEvent] for testing inheritance. +final class SubEventB extends BaseEvent { + SubEventB( + this.number, { + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : super( + id: eventId ?? EventId.fromUlid(), + occurredOn: occurredOn ?? DateTime.now(), + metadata: Map.unmodifiable(metadata), + ); + + final int number; +} + +/// Class-based handler for [BaseEvent] to test inheritance with register(). +class BaseEventHandler implements EventHandler { + final List received = []; + + @override + Future handle(BaseEvent event) async { + received.add(event); + } +} diff --git a/packages/raiser/test/event_bus_test.dart b/packages/raiser/test/event_bus_test.dart index 474006a..3b53ed7 100644 --- a/packages/raiser/test/event_bus_test.dart +++ b/packages/raiser/test/event_bus_test.dart @@ -305,8 +305,7 @@ void main() { expect( executionOrder, equals(expectedOrder), - reason: - 'Priorities $priorities should execute in descending priority order', + reason: 'Priorities $priorities should execute in descending priority order', ); } }); @@ -343,8 +342,7 @@ void main() { expect( executionOrder, equals(expectedOrder), - reason: - 'Equal priority handlers should execute in registration order', + reason: 'Equal priority handlers should execute in registration order', ); } }, diff --git a/packages/raiser/test/inheritance_type_matching_test.dart b/packages/raiser/test/inheritance_type_matching_test.dart new file mode 100644 index 0000000..d3efe80 --- /dev/null +++ b/packages/raiser/test/inheritance_type_matching_test.dart @@ -0,0 +1,717 @@ +import 'package:raiser/raiser.dart'; +import 'package:test/test.dart'; +import 'package:zooper_flutter_core/zooper_flutter_core.dart'; + +/// Tests for inheritance-aware type matching in the EventBus. +/// +/// Verifies that handlers registered for base types correctly receive +/// events of subclass types. This is critical for sealed class patterns +/// (e.g., freezed) where domain events have multiple subtypes. +void main() { + group('Inheritance-Aware Type Matching', () { + group('Sealed Class Hierarchies', () { + test('handler for sealed base class receives all subclass events', () async { + // Simulates the real-world pattern where a handler is registered for + // a sealed base class but events are published as concrete subtypes. + final bus = EventBus(); + final received = []; + + bus.on((event) async { + received.add(event); + }); + + // Publish different subtypes - all should be received + await bus.publish(ConcreteEventA(valueA: 'data-a')); + await bus.publish(ConcreteEventB(valueB: 42)); + + expect(received.length, equals(2)); + expect(received[0], isA()); + expect(received[1], isA()); + }); + + test('class-based EventHandler for sealed base receives all subtypes', () async { + // Demonstrates the pattern: EventHandler receives all subtypes. + // Typical usage: @RaiserHandler() class MyHandler extends EventHandler + final bus = EventBus(); + final handler = SealedBaseEventHandler(); + + bus.register(handler); + + await bus.publish(ConcreteEventA(valueA: 'test')); + await bus.publish(ConcreteEventB(valueB: 100)); + + expect(handler.handledEvents.length, equals(2)); + expect(handler.handledEvents[0], isA()); + expect(handler.handledEvents[1], isA()); + }); + + test('handler for specific sealed subtype only receives that subtype', () async { + // Ensure type specificity still works - a handler for ConcreteEventA + // should NOT receive ConcreteEventB. + final bus = EventBus(); + final eventsA = []; + final eventsB = []; + + bus.on((event) async { + eventsA.add(event); + }); + bus.on((event) async { + eventsB.add(event); + }); + + await bus.publish(ConcreteEventA(valueA: 'first')); + await bus.publish(ConcreteEventB(valueB: 10)); + await bus.publish(ConcreteEventA(valueA: 'second')); + + expect(eventsA.length, equals(2)); + expect(eventsB.length, equals(1)); + }); + + test('both base and subtype handlers receive subtype events', () async { + // When handlers exist for both SealedBaseEvent and ConcreteEventA, + // publishing a ConcreteEventA should trigger BOTH handlers. + final bus = EventBus(); + final baseReceived = []; + final subtypeReceived = []; + + bus.on((event) async { + baseReceived.add(event); + }); + bus.on((event) async { + subtypeReceived.add(event); + }); + + await bus.publish(ConcreteEventA(valueA: 'test')); + + // Both handlers should fire + expect(baseReceived.length, equals(1)); + expect(subtypeReceived.length, equals(1)); + + // Base handler would NOT fire for base-only publish + // (if we could instantiate SealedBaseEvent, but sealed prevents that) + }); + + test('sealed class with three or more subtypes works correctly', () async { + // Test with a sealed class that has multiple subtypes + final bus = EventBus(); + final allEvents = []; + final creditCardEvents = []; + final bankTransferEvents = []; + final cryptoEvents = []; + + bus.on((event) async => allEvents.add(event)); + bus.on((event) async => creditCardEvents.add(event)); + bus.on((event) async => bankTransferEvents.add(event)); + bus.on((event) async => cryptoEvents.add(event)); + + await bus.publish(CreditCardPaymentEvent(amount: 100.0, cardLast4: '1234')); + await bus.publish(BankTransferPaymentEvent(amount: 500.0, iban: 'DE89370400440532013000')); + await bus.publish(CryptoPaymentEvent(amount: 0.5, walletAddress: '0x123...')); + await bus.publish(CreditCardPaymentEvent(amount: 25.0, cardLast4: '5678')); + + // Base handler should receive all 4 events + expect(allEvents.length, equals(4)); + + // Specific handlers should only receive their type + expect(creditCardEvents.length, equals(2)); + expect(bankTransferEvents.length, equals(1)); + expect(cryptoEvents.length, equals(1)); + }); + }); + + group('Abstract Base Class Hierarchies', () { + test('handler for abstract base class receives concrete implementations', () async { + final bus = EventBus(); + final received = []; + + bus.on((event) async { + received.add(event); + }); + + await bus.publish(EmailNotification(to: 'user@example.com', subject: 'Hello')); + await bus.publish(PushNotification(deviceToken: 'abc123', title: 'Alert')); + await bus.publish(SmsNotification(phoneNumber: '+1234567890', body: 'Code: 1234')); + + expect(received.length, equals(3)); + expect(received[0], isA()); + expect(received[1], isA()); + expect(received[2], isA()); + }); + }); + + group('Multi-Level Inheritance', () { + test('handlers at each level of hierarchy receive appropriate events', () async { + // Given: Animal > Mammal > Dog hierarchy + final bus = EventBus(); + final animalEvents = []; + final mammalEvents = []; + final dogEvents = []; + + bus.on((event) async => animalEvents.add(event)); + bus.on((event) async => mammalEvents.add(event)); + bus.on((event) async => dogEvents.add(event)); + + // When: Publishing a DogEvent + await bus.publish(DogEvent(name: 'Buddy', breed: 'Golden Retriever')); + + // Then: All three handlers should receive it + expect(animalEvents.length, equals(1)); + expect(mammalEvents.length, equals(1)); + expect(dogEvents.length, equals(1)); + }); + + test('mid-level handler receives all descendants but not ancestors', () async { + final bus = EventBus(); + final mammalEvents = []; + + bus.on((event) async => mammalEvents.add(event)); + + await bus.publish(DogEvent(name: 'Rex', breed: 'German Shepherd')); + await bus.publish(CatEvent(name: 'Whiskers', indoor: true)); + + // MammalEvent handler receives both Dog and Cat + expect(mammalEvents.length, equals(2)); + }); + }); + + group('Interface Implementation', () { + test('handler for interface receives all implementers', () async { + final bus = EventBus(); + final serializableEvents = []; + + bus.on((event) async { + serializableEvents.add(event); + }); + + await bus.publish(JsonSerializableEvent(data: {'key': 'value'})); + await bus.publish(XmlSerializableEvent(rootElement: 'config')); + + expect(serializableEvents.length, equals(2)); + }); + }); + + group('Priority Ordering with Inheritance', () { + test('priority is respected across inheritance levels', () async { + final bus = EventBus(); + final executionOrder = []; + + // Register handlers with explicit priorities + bus.on((e) async => executionOrder.add('base:priority-0'), priority: 0); + bus.on((e) async => executionOrder.add('subtype:priority-10'), priority: 10); + bus.on((e) async => executionOrder.add('base:priority-5'), priority: 5); + + await bus.publish(ConcreteEventA(valueA: 'test')); + + // Should execute in priority order: 10 > 5 > 0 + expect(executionOrder, equals(['subtype:priority-10', 'base:priority-5', 'base:priority-0'])); + }); + + test('registration order is tiebreaker for same priority', () async { + final bus = EventBus(); + final executionOrder = []; + + // All same priority, should execute in registration order + bus.on((e) async => executionOrder.add('first')); + bus.on((e) async => executionOrder.add('second')); + bus.on((e) async => executionOrder.add('third')); + + await bus.publish(ConcreteEventA(valueA: 'test')); + + expect(executionOrder, equals(['first', 'second', 'third'])); + }); + }); + + group('Error Handling with Inheritance', () { + test('error in base handler does not prevent subtype handler (continueOnError)', () async { + final bus = EventBus(errorStrategy: ErrorStrategy.continueOnError); + final subtypeHandlerCalled = []; + + bus.on((e) async { + throw Exception('Base handler error'); + }, priority: 10); + bus.on((e) async { + subtypeHandlerCalled.add(true); + }, priority: 0); + + // Should throw aggregate but both handlers execute + await expectLater( + () => bus.publish(ConcreteEventA(valueA: 'test')), + throwsA(isA()), + ); + + expect(subtypeHandlerCalled, equals([true])); + }); + + test('error callback receives errors from all matching handlers', () async { + final bus = EventBus( + errorStrategy: ErrorStrategy.continueOnError, + onError: (error, _) {}, + ); + final errors = []; + + bus.on((e) async { + throw ArgumentError('Base error'); + }); + bus.on((e) async { + throw StateError('Subtype error'); + }); + + try { + await bus.publish(ConcreteEventA(valueA: 'test')); + } on AggregateException catch (e) { + errors.addAll(e.errors); + } + + expect(errors.length, equals(2)); + expect(errors[0], isA()); + expect(errors[1], isA()); + }); + }); + + group('Subscription Cancellation with Inheritance', () { + test('cancelling base handler stops receiving subtype events', () async { + final bus = EventBus(); + final received = []; + + final subscription = bus.on((event) async { + received.add(event); + }); + + await bus.publish(ConcreteEventA(valueA: 'first')); + expect(received.length, equals(1)); + + subscription.cancel(); + + await bus.publish(ConcreteEventA(valueA: 'second')); + expect(received.length, equals(1)); // Still 1, no new events + }); + + test('cancelling subtype handler still receives via base handler', () async { + final bus = EventBus(); + final baseReceived = []; + final subtypeReceived = []; + + bus.on((e) async => baseReceived.add(e)); + final subtypeSub = bus.on((e) async => subtypeReceived.add(e)); + + await bus.publish(ConcreteEventA(valueA: 'test')); + expect(baseReceived.length, equals(1)); + expect(subtypeReceived.length, equals(1)); + + subtypeSub.cancel(); + + await bus.publish(ConcreteEventA(valueA: 'test2')); + expect(baseReceived.length, equals(2)); // Base still receives + expect(subtypeReceived.length, equals(1)); // Subtype cancelled + }); + }); + + group('Middleware with Inheritance', () { + test('middleware sees subtype events when handler is for base type', () async { + final bus = EventBus(); + final middlewareEvents = []; + final handlerEvents = []; + + bus.addMiddleware((event, next) async { + middlewareEvents.add(event); + // ignore: avoid_dynamic_calls + await next(); + }); + + bus.on((e) async => handlerEvents.add(e)); + + final event = ConcreteEventA(valueA: 'test'); + await bus.publish(event); + + // Middleware should see the actual runtime type + expect(middlewareEvents.length, equals(1)); + expect(middlewareEvents[0], isA()); + expect(identical(middlewareEvents[0], event), isTrue); + + // Handler should also receive it + expect(handlerEvents.length, equals(1)); + }); + }); + + group('Edge Cases', () { + test('publishing exact base type (non-sealed) works', () async { + // For non-sealed hierarchies where you CAN instantiate the base + final bus = EventBus(); + final received = []; + + bus.on((e) async => received.add(e)); + + await bus.publish(ConcreteBaseEvent(value: 'base')); + await bus.publish(ConcreteChildEvent(value: 'child', extra: 42)); + + expect(received.length, equals(2)); + }); + + test('no handlers for type completes without error', () async { + final bus = EventBus(); + final received = []; + + // Only register for EventB, not EventA + bus.on((e) async => received.add(e)); + + // Publishing EventA should complete without error + await expectLater( + bus.publish(ConcreteEventA(valueA: 'test')), + completes, + ); + + expect(received.length, equals(0)); + }); + + test('handler registered multiple times receives event multiple times', () async { + final bus = EventBus(); + var callCount = 0; + + Future handler(SealedBaseEvent e) async { + callCount++; + } + + bus.on(handler); + bus.on(handler); + + await bus.publish(ConcreteEventA(valueA: 'test')); + + expect(callCount, equals(2)); + }); + }); + }); +} + +// ============================================================================= +// Test Event Hierarchies +// ============================================================================= + +/// Generic sealed base event for testing inheritance-aware type matching. +/// +/// Demonstrates the pattern where a sealed class has multiple concrete subtypes. +sealed class SealedBaseEvent implements RaiserEvent { + @override + EventId get id; + + @override + DateTime get occurredOn; + + @override + Map get metadata; +} + +/// First concrete implementation of sealed base event. +final class ConcreteEventA extends SealedBaseEvent { + ConcreteEventA({required this.valueA, EventId? id, DateTime? occurredOn}) + : id = id ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = const {}; + + final String valueA; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +/// Second concrete implementation of sealed base event. +final class ConcreteEventB extends SealedBaseEvent { + ConcreteEventB({required this.valueB, EventId? id, DateTime? occurredOn}) + : id = id ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = const {}; + + final int valueB; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +/// Class-based handler for sealed base type. +class SealedBaseEventHandler implements EventHandler { + final List handledEvents = []; + + @override + Future handle(SealedBaseEvent event) async { + handledEvents.add(event); + } +} + +// ----------------------------------------------------------------------------- +// Payment Event Hierarchy (sealed with 3+ subtypes) +// ----------------------------------------------------------------------------- + +sealed class PaymentEvent implements RaiserEvent { + double get amount; + + @override + EventId get id; + + @override + DateTime get occurredOn; + + @override + Map get metadata; +} + +final class CreditCardPaymentEvent extends PaymentEvent { + CreditCardPaymentEvent({required this.amount, required this.cardLast4}) + : id = EventId.fromUlid(), + occurredOn = DateTime.now(), + metadata = const {}; + + @override + final double amount; + + final String cardLast4; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +final class BankTransferPaymentEvent extends PaymentEvent { + BankTransferPaymentEvent({required this.amount, required this.iban}) + : id = EventId.fromUlid(), + occurredOn = DateTime.now(), + metadata = const {}; + + @override + final double amount; + + final String iban; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +final class CryptoPaymentEvent extends PaymentEvent { + CryptoPaymentEvent({required this.amount, required this.walletAddress}) + : id = EventId.fromUlid(), + occurredOn = DateTime.now(), + metadata = const {}; + + @override + final double amount; + + final String walletAddress; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +// ----------------------------------------------------------------------------- +// Abstract Base Class Hierarchy +// ----------------------------------------------------------------------------- + +abstract class AbstractNotification implements RaiserEvent { + @override + EventId get id; + + @override + DateTime get occurredOn; + + @override + Map get metadata; +} + +final class EmailNotification extends AbstractNotification { + EmailNotification({required this.to, required this.subject}) + : id = EventId.fromUlid(), + occurredOn = DateTime.now(), + metadata = const {}; + + final String to; + final String subject; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +final class PushNotification extends AbstractNotification { + PushNotification({required this.deviceToken, required this.title}) + : id = EventId.fromUlid(), + occurredOn = DateTime.now(), + metadata = const {}; + + final String deviceToken; + final String title; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +final class SmsNotification extends AbstractNotification { + SmsNotification({required this.phoneNumber, required this.body}) + : id = EventId.fromUlid(), + occurredOn = DateTime.now(), + metadata = const {}; + + final String phoneNumber; + final String body; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +// ----------------------------------------------------------------------------- +// Multi-Level Inheritance Hierarchy +// ----------------------------------------------------------------------------- + +class AnimalEvent implements RaiserEvent { + AnimalEvent({required this.name}) + : id = EventId.fromUlid(), + occurredOn = DateTime.now(), + metadata = const {}; + + final String name; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +class MammalEvent extends AnimalEvent { + MammalEvent({required super.name}); +} + +class DogEvent extends MammalEvent { + DogEvent({required super.name, required this.breed}); + + final String breed; +} + +class CatEvent extends MammalEvent { + CatEvent({required super.name, required this.indoor}); + + final bool indoor; +} + +// ----------------------------------------------------------------------------- +// Interface-Based Hierarchy +// ----------------------------------------------------------------------------- + +abstract interface class SerializableEvent implements RaiserEvent { + String serialize(); +} + +final class JsonSerializableEvent implements SerializableEvent { + JsonSerializableEvent({required this.data}) + : id = EventId.fromUlid(), + occurredOn = DateTime.now(), + metadata = const {}; + + final Map data; + + @override + String serialize() => data.toString(); + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +final class XmlSerializableEvent implements SerializableEvent { + XmlSerializableEvent({required this.rootElement}) + : id = EventId.fromUlid(), + occurredOn = DateTime.now(), + metadata = const {}; + + final String rootElement; + + @override + String serialize() => '<$rootElement />'; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +// ----------------------------------------------------------------------------- +// Concrete Base (non-sealed) for edge case testing +// ----------------------------------------------------------------------------- + +class ConcreteBaseEvent implements RaiserEvent { + ConcreteBaseEvent({required this.value}) + : id = EventId.fromUlid(), + occurredOn = DateTime.now(), + metadata = const {}; + + final String value; + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} + +class ConcreteChildEvent extends ConcreteBaseEvent { + ConcreteChildEvent({required super.value, required this.extra}); + + final int extra; +}