From 1ebe9c5018987639c8e7b912a869c7943d90d2d3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 10 Jan 2026 23:51:36 +0800 Subject: [PATCH] Refactors RaiserEvent to be an interface Changes `RaiserEvent` from an abstract base class to an interface to provide more flexibility in event implementation. Removes the automatic metadata generation and `aggregateId` property, encouraging explicit implementation and storage of aggregate identifiers in the metadata map. Updates the example and tests to reflect these changes and adds new style guide documentation. --- .github/instructions/dart.instructions.md | 76 ++++++ .../instructions/dart.styling.instructions.md | 28 +++ .../instructions/dart.tests.instructions.md | 43 ++++ .github/instructions/flutter.instructions.md | 17 ++ packages/raiser/CHANGELOG.md | 29 +++ packages/raiser/README.md | 91 ++++--- packages/raiser/analysis_options.yaml | 94 +++++-- packages/raiser/example/advanced_example.dart | 168 ++++++++----- packages/raiser/example/analysis_options.yaml | 76 ++++++ packages/raiser/example/pubspec.yaml | 22 ++ .../raiser/example/pubspec_overrides.yaml | 8 + packages/raiser/example/raiser_example.dart | 116 +++++---- packages/raiser/lib/raiser.dart | 12 +- .../raiser/lib/src/bus/error_strategy.dart | 6 - packages/raiser/lib/src/bus/event_bus.dart | 23 +- .../raiser/lib/src/events/raiser_event.dart | 82 +----- .../lib/src/handlers/event_handler.dart | 7 - .../raiser/lib/src/handlers/subscription.dart | 6 - packages/raiser/pubspec.yaml | 3 + packages/raiser/test/domain_event_test.dart | 97 ++++---- packages/raiser/test/edge_cases_test.dart | 81 +++--- packages/raiser/test/error_handling_test.dart | 8 +- packages/raiser/test/event_bus_test.dart | 4 +- packages/raiser/test/middleware_test.dart | 234 +++++++----------- packages/raiser/test/raiser_test.dart | 29 ++- .../example/analysis_options.yaml | 76 ++++++ .../raiser_generator/example/lib/events.dart | 118 +++++---- .../example/lib/handlers.dart | 9 - .../example/lib/middleware.dart | 9 - .../example/lib/raiser.g.dart | 7 +- .../example/lib/services.dart | 5 - .../raiser_generator/example/pubspec.yaml | 10 +- .../example/raiser_generator_example.dart | 2 +- 33 files changed, 992 insertions(+), 604 deletions(-) create mode 100644 .github/instructions/dart.instructions.md create mode 100644 .github/instructions/dart.styling.instructions.md create mode 100644 .github/instructions/dart.tests.instructions.md create mode 100644 .github/instructions/flutter.instructions.md create mode 100644 packages/raiser/example/analysis_options.yaml create mode 100644 packages/raiser/example/pubspec.yaml create mode 100644 packages/raiser/example/pubspec_overrides.yaml create mode 100644 packages/raiser_generator/example/analysis_options.yaml diff --git a/.github/instructions/dart.instructions.md b/.github/instructions/dart.instructions.md new file mode 100644 index 0000000..1af83c1 --- /dev/null +++ b/.github/instructions/dart.instructions.md @@ -0,0 +1,76 @@ +--- +applyTo: '**/*.dart' +--- + +# General +- ALWAYS write clean, readable, maintainable, explicit code. +- ALWAYS write code that is easy to refactor and reason about. +- NEVER assume context or generate code that I did not explicitly request. +- ALWAYS name files after the primary class or functionality they contain. + +# Documentation +- WHEN a CHANGELOG.md file is present in the project root, ALWAYS add a changelog entry for any non-trivial change.Minimal flat vector icon. Multiple small dots flowing left-to-right into one larger dot in a single line. Clean geometric style, blue palette, no text, transparent background. + +- ALWAYS place a `///` library-level documentation block (before imports) ONLY on: + - lib/.dart (the main public entrypoint) + - a small number of intentionally exposed public sub-libraries +- NEVER add library file-docs on internal files inside `src/` +- ALWAYS keep package-surface documentation concise, stable, and user-facing +- ALWAYS write Dart-doc (`///`) for: + - every class + - every constructor + - every public and private method + - every important field/property +- ALWAYS add inline comments INSIDE methods explaining **why** something is done (preferred) or **what** it does if unclear. +- NEVER generate README / docs / summary files unless explicitly asked. +- NEVER document example usage. + +# Code Style +- ALWAYS use long, descriptive variable and method names. NEVER use abbreviations. +- ALWAYS use explicit return types — NEVER rely on type inference for public API surfaces. +- ALWAYS avoid hidden behavior or magic — explain reasons in comments. +- NEVER use `dynamic` unless explicitly requested. +- NEVER swallow exceptions — failures must be explicit and documented. + +# Package Modularity +- ALWAYS organize code by feature or concept, NOT by layers (domain/app/infrastructure/etc.). +- ALWAYS keep related classes in the same folder to avoid unnecessary cross-navigation. +- ALWAYS aim for package-internal cohesion: a feature should be usable independently of others. +- NEVER introduce folders like `domain`, `application`, `infrastructure`, `presentation` inside a package unless explicitly asked. +- ALWAYS design APIs as small, composable, orthogonal units that can be imported independently. +- ALWAYS hide internal details using file-private symbols or exports from a single public interface file. +- ALWAYS expose only few careful public entrypoints through `package_name.dart`. +- NEVER expose cluttered API surfaces; keep users' imports short and predictable. + +# Asynchronous / IO +- ALWAYS suffix async methods with `Async`. +- NEVER do IO inside constructors. +- ALWAYS document async side-effects. + +# Constants +- NEVER implement magic values. +- ALWAYS elevate numbers, strings, durations, etc. to named constants. + +# Assumptions +- IF details are missing, ALWAYS state assumptions **above the code** before writing it. +- NEVER introduce global state unless explicitly required. + +# API Design +- ALWAYS think in terms of public API surface: every public symbol must be intentionally exposed and supported long-term. +- ALWAYS hide implementation details behind internal files. +- ALWAYS consider whether adding a type forces future backwards-compatibility. +- ALWAYS design for testability (stateless helpers, pure functions, injectable dependencies). + +# Folder Hygiene +- NEVER create folders "just in case." +- ALWAYS delete dead code aggressively. +- ALWAYS keep `src/` readable even after 2 years of growth. + +# Code Hygiene +- NEVER implement barrel export files. +- ALWAYS write code that compiles with ZERO warnings, errors, or analyzer hints. +- ALWAYS remove unused imports, unused variables, unused private fields, and unreachable code. +- ALWAYS prefer explicit typing to avoid inference warnings. +- ALWAYS mark classes, methods, or variables as `@visibleForTesting` or private when they are not part of the public API. +- NEVER ignore analyzer warnings with `// ignore:` unless explicitly asked. +- ALWAYS keep lint and style problems in VSCode Problems panel at ZERO, unless unavoidable and explicitly justified in comments. diff --git a/.github/instructions/dart.styling.instructions.md b/.github/instructions/dart.styling.instructions.md new file mode 100644 index 0000000..e094df3 --- /dev/null +++ b/.github/instructions/dart.styling.instructions.md @@ -0,0 +1,28 @@ +--- +applyTo: '**/*.dart' +--- + +# File Ordering (top → bottom) +- library documentation (ONLY when allowed) +- imports (dart: → package: → relative), alphabetical +- exports, alphabetical +- top-level constants +- top-level typedefs, aliases +- top-level public enums +- top-level public classes / mixins / extensions +- top-level private enums +- top-level private classes / mixins / extensions (ALWAYS LAST) + +# Class Member Ordering +1. static fields (public → private) +2. instance fields (public → private) +3. constructors (public → named → private) +4. factory constructors +5. public getters +6. public setters +7. public methods +8. operator overloads +9. protected methods +10. private getters / setters +11. private methods +12. static methods (public → private) \ No newline at end of file diff --git a/.github/instructions/dart.tests.instructions.md b/.github/instructions/dart.tests.instructions.md new file mode 100644 index 0000000..eb9f77f --- /dev/null +++ b/.github/instructions/dart.tests.instructions.md @@ -0,0 +1,43 @@ +--- +applyTo: '**/*.dart' +--- + +# Tests +- ALWAYS write tests for EVERY publicly accessible class, function, and method. +- ALWAYS write tests with the primary goal of exposing possible bugs — NOT simply making tests pass. +- ALWAYS test failure cases, invalid input, unexpected state, and edge conditions. +- ALWAYS create exactly one unit test file per class being tested. +- ALWAYS name the test file `_test.dart` or `_test.dart`. + +# Unit Tests +- ALWAYS use Arrange–Act–Assert pattern with clear separation. +- ALWAYS write descriptive test names that explain expected behavior. +- ALWAYS add inline comments inside tests explaining WHY assertions matter. +- ALWAYS include tests for: + - Happy path behavior + - Error cases and thrown exceptions + - Boundary conditions + - Null / empty values where applicable + - Timing and concurrency behavior if async +- NEVER skip tests for private methods if they contain complex logic. + (If a private method is trivial, call it indirectly through public API instead.) +- WHEN a class depends on collaborators, ALWAYS use fakes or stubs — NEVER use real infrastructure in unit tests. + +# Integration Tests (only when applicable) +- ALWAYS write integration tests to verify whole workflows that span multiple public classes. +- ALWAYS cover multi-step flows, IO boundaries, and dependency wiring. +- NEVER write integration tests when a unit test is sufficient. +- ALWAYS isolate integration tests into `test/integration/` and name according to workflow. + +# Test Hygiene +- NEVER use random sleeps or timing hacks — use proper async waiting or dependency injection. +- NEVER rely on global order of test execution. +- ALWAYS ensure tests remain readable after years — avoid clever tricks or meta test logic. + +# Mocks +- ALWAYS use Mockito for mocking dependencies. +- ALWAYS mock collaborators instead of creating real implementations in unit tests. +- ALWAYS generate mock classes via `build_runner` when needed. +- NEVER use real data sources, HTTP calls, or platform channels in unit tests. +- ALWAYS verify interactions on mocks when behavior depends on method-call side effects. +- ALWAYS keep mock usage minimal and focused — tests should assert behavior, not implementation details. diff --git a/.github/instructions/flutter.instructions.md b/.github/instructions/flutter.instructions.md new file mode 100644 index 0000000..97acbad --- /dev/null +++ b/.github/instructions/flutter.instructions.md @@ -0,0 +1,17 @@ +--- +applyTo: '**/*.dart' +--- + +# Clean Architecture for Flutter apps +- ALWAYS separate responsibilities: + - domain/: entities, value objects, business rules + - application/: services, use-cases, orchestrators + - infrastructure/: concrete implementations, IO, APIs + - presentation/: Flutter widgets, controllers, adapters +- NEVER mix domain logic inside UI or infrastructure. +- NEVER inject `WidgetRef` or `Ref` into domain/application classes — ONLY resolve dependencies at provider boundaries. + +# Flutter Widgets +- ALWAYS explain the purpose of a widget in Dart-doc. +- ALWAYS extract callbacks into named functions when possible. +- NEVER override themes or text styles unless explicitly requested. \ No newline at end of file diff --git a/packages/raiser/CHANGELOG.md b/packages/raiser/CHANGELOG.md index 7506bd7..b3edf75 100644 --- a/packages/raiser/CHANGELOG.md +++ b/packages/raiser/CHANGELOG.md @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- **BREAKING**: `RaiserEvent` is now a pure `abstract interface class` instead of a base class with implementation + - **Note: Using `RaiserEvent` is completely optional - `EventBus` works with ANY type** + - If you choose to use `RaiserEvent`, you must explicitly implement all three required properties: `id`, `occurredOn`, and `metadata` + - No more automatic property initialization - implement the interface explicitly + - This change allows events to use composition and multiple interfaces without inheritance constraints +- `RaiserEvent` now implements `ZooperDomainEvent` from `zooper_flutter_core` package + - Added `zooper_flutter_core: ^1.0.2` as an optional dependency (only needed if you use `RaiserEvent`) + - Ensures consistency across domain events in the Zooper ecosystem +- Event IDs in `RaiserEvent` now use `EventId` type (ULID-based) instead of `String` + - IDs are generated via `EventId.fromUlid()` for better uniqueness guarantees + - Access raw string value via `event.id.value` when needed +- Renamed `RaiserEvent.timestamp` property to `occurredOn` for clarity and DDD alignment +- Removed `aggregateId` as a direct property from `RaiserEvent` + - Store aggregate identifiers in the `metadata` map instead: `metadata: {'aggregateId': 'user-123'}` + - Access via `event.metadata['aggregateId'] as String?` +- Removed all helper mixins and convenience extensions + - No more `StandardRaiserEvent` mixin + - No more extension methods for `aggregateId` or `timestamp` accessors + - Events implementing `RaiserEvent` must be implemented explicitly following the interface pattern + +### Important Note + +**`EventBus` is fully generic and does not require `RaiserEvent`!** You can publish and subscribe to any type. The `RaiserEvent` interface is purely optional for users who want standardized domain event metadata. + + ## [2.0.1] - 2026-01-08 ### Changed diff --git a/packages/raiser/README.md b/packages/raiser/README.md index 99dcef4..dcf74a6 100644 --- a/packages/raiser/README.md +++ b/packages/raiser/README.md @@ -21,27 +21,25 @@ A type-safe, async-first domain event library for Dart. Raiser provides a clean ```yaml dependencies: - raiser: ^1.0.0 + raiser: ^2.0.1 ``` ## Quick Start +**EventBus works with ANY type** - you don't need to use `RaiserEvent` at all: + ```dart import 'package:raiser/raiser.dart'; -// Define an event -class UserCreated extends RaiserEvent { +// Define an event - just a simple class! +final class UserCreated { + UserCreated({ + required this.userId, + required this.email, + }); + final String userId; final String email; - - UserCreated({required this.userId, required this.email}); - - @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'userId': userId, - 'email': email, - }; } void main() async { @@ -57,32 +55,63 @@ void main() async { } ``` -## Domain Events +## RaiserEvent (Optional) + +**You don't have to use `RaiserEvent`!** The `EventBus` is fully generic and works with any type. + +However, if you want standardized domain event metadata, you can optionally implement `RaiserEvent`, which is an interface extending `ZooperDomainEvent` from [zooper_flutter_core](https://pub.dev/packages/zooper_flutter_core). This provides: -All events extend `RaiserEvent`, which provides automatic metadata: +| Property | Type | Description | +|----------|------|-------------| +| `id` | `EventId` | Unique identifier (ULID-based) | +| `occurredOn` | `DateTime` | When the event occurred | +| `metadata` | `Map` | Additional event context | -| Property | Description | -|----------|-------------| -| `id` | Unique identifier (auto-generated) | -| `timestamp` | Creation time (auto-captured) | -| `aggregateId` | Optional link to a domain aggregate | +`RaiserEvent` is intentionally an **interface** (not a base class) to avoid forcing single inheritance. If you choose to use it, implement it explicitly: ```dart -class OrderPlaced extends RaiserEvent { +import 'package:zooper_flutter_core/zooper_flutter_core.dart'; + +final class OrderPlaced implements RaiserEvent { + OrderPlaced({ + required this.orderId, + required this.amount, + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + final String orderId; final double amount; - OrderPlaced({required this.orderId, required this.amount, super.aggregateId}); + @override + final EventId id; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'orderId': orderId, - 'amount': amount, - }; + final DateTime occurredOn; + + @override + final Map metadata; } ``` +### Aggregate IDs + +Store aggregate identifiers in the `metadata` map: + +```dart +final event = OrderPlaced( + orderId: 'order-123', + amount: 99.99, + metadata: {'aggregateId': 'user-456'}, +); + +// Access via metadata +final aggregateId = event.metadata['aggregateId'] as String?; +``` + ## Event Handlers ### Function Handlers @@ -126,14 +155,14 @@ Wrap handler execution with cross-cutting concerns like logging, timing, or vali ```dart // Add middleware that wraps all handler execution -bus.addMiddleware((event, next) async { +bus.addMiddleware((Object event, Future Function() next) async { print('Before: ${event.runtimeType}'); await next(); print('After: ${event.runtimeType}'); }, priority: 100); // Middleware with higher priority wraps those with lower priority -bus.addMiddleware((event, next) async { +bus.addMiddleware((Object event, Future Function() next) async { final stopwatch = Stopwatch()..start(); await next(); print('Took ${stopwatch.elapsedMilliseconds}ms'); @@ -198,12 +227,12 @@ For automatic handler discovery and registration, use the companion packages: ```yaml dependencies: - raiser: ^1.0.0 - raiser_annotation: ^1.0.0 + raiser: ^2.0.1 + raiser_annotation: ^2.0.1 dev_dependencies: build_runner: ^2.4.0 - raiser_generator: ^1.0.0 + raiser_generator: ^2.0.1 ``` See [raiser_generator](https://pub.dev/packages/raiser_generator) for details. diff --git a/packages/raiser/analysis_options.yaml b/packages/raiser/analysis_options.yaml index dee8927..bad2694 100644 --- a/packages/raiser/analysis_options.yaml +++ b/packages/raiser/analysis_options.yaml @@ -1,30 +1,76 @@ -# This file configures the static analysis results for your project (errors, -# warnings, and lints). -# -# This enables the 'recommended' set of lints from `package:lints`. -# This set helps identify many issues that may lead to problems when running -# or consuming Dart code, and enforces writing Dart using a single, idiomatic -# style and format. -# -# If you want a smaller set of lints you can change this to specify -# 'package:lints/core.yaml'. These are just the most critical lints -# (the recommended set includes the core lints). -# The core lints are also what is used by pub.dev for scoring packages. - include: package:lints/recommended.yaml -# Uncomment the following section to specify additional rules. +formatter: + # Keep trailing commas. + trailing_commas: preserve + + # Set the maximum line length. + page_width: 160 + +# Customize additional linter rules. +linter: + # Specify rules to be enabled or disabled. + rules: + # **Style Rules** + # Enforce using const constructors where possible. + prefer_const_constructors: true + prefer_const_literals_to_create_immutables: true + + # Prefer single quotes over double quotes where possible. + prefer_single_quotes: true + + # Enforce a consistent type definition for variables. + always_specify_types: false + + # **Best Practices** + # Avoid using dynamic types. + avoid_dynamic_calls: true + + # Avoid using print statements in production code. In example code this is acceptable. + avoid_print: false + + # Prefer using the `final` keyword for variables that are not reassigned. + prefer_final_fields: true + prefer_final_locals: true + + # **Error Prevention** + + # Enforce non-nullable types where possible. + always_require_non_null_named_parameters: true + + # **Documentation** + # Require documentation for public members. + public_member_api_docs: false + + # **Other Useful Rules** + # Enforce sorting of directives (e.g., imports). + directives_ordering: true + + # Prefer using `isEmpty` and `isNotEmpty` over `length` comparisons. + prefer_is_empty: true + prefer_is_not_empty: true -# linter: -# rules: -# - camel_case_types + # **Disable Rules (if necessary)** + # Uncomment the following lines to disable specific lints. + # avoid_unused_constructor_parameters: false + # unnecessary_null_in_if_null_operators: false -# analyzer: -# exclude: -# - path/to/excluded/files/** +# Analyzer settings. +analyzer: + # Exclude certain files or directories from analysis. + exclude: + - "**/*.g.dart" + - "**/build/**" + - "**/generated/**" + - "**/mocks/**" + - "**/*.freezed.dart/**" -# For more information about the core and recommended set of lints, see -# https://dart.dev/go/core-lints + # Language-specific analyzer strictness options. + language: + strict-casts: false + strict-inference: false + strict-raw-types: false -# For additional information about configuring this file, see -# https://dart.dev/guides/language/analysis-options + strong-mode: + implicit-casts: false + implicit-dynamic: false \ No newline at end of file diff --git a/packages/raiser/example/advanced_example.dart b/packages/raiser/example/advanced_example.dart index 9bfa7f7..5001cd1 100644 --- a/packages/raiser/example/advanced_example.dart +++ b/packages/raiser/example/advanced_example.dart @@ -8,84 +8,126 @@ // - Testing patterns import 'package:raiser/raiser.dart'; +import 'package:zooper_flutter_core/zooper_flutter_core.dart'; // ============================================================================ // DOMAIN EVENTS FOR E-COMMERCE SCENARIO // ============================================================================ -class CartItemAdded extends RaiserEvent { - final String productId; - final int quantity; - final double price; - +final class CartItemAdded implements RaiserEvent { CartItemAdded({ required this.productId, required this.quantity, required this.price, - super.aggregateId, - }); + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + + final String productId; + final int quantity; + final double price; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'productId': productId, - 'quantity': quantity, - 'price': price, - }; + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; } -class CartItemRemoved extends RaiserEvent { +final class CartItemRemoved implements RaiserEvent { + CartItemRemoved({ + required this.productId, + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + final String productId; - CartItemRemoved({required this.productId, super.aggregateId}); + @override + final EventId id; + + @override + final DateTime occurredOn; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'productId': productId, - }; + final Map metadata; } -class CheckoutStarted extends RaiserEvent { +final class CheckoutStarted implements RaiserEvent { + CheckoutStarted({ + required this.totalAmount, + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + final double totalAmount; - CheckoutStarted({required this.totalAmount, super.aggregateId}); + @override + final EventId id; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'totalAmount': totalAmount, - }; -} + final DateTime occurredOn; -class PaymentReceived extends RaiserEvent { - final String transactionId; - final double amount; + @override + final Map metadata; +} +final class PaymentReceived implements RaiserEvent { PaymentReceived({ required this.transactionId, required this.amount, - super.aggregateId, - }); + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + + final String transactionId; + final double amount; + + @override + final EventId id; + + @override + final DateTime occurredOn; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'transactionId': transactionId, - 'amount': amount, - }; + final Map metadata; } -class OrderShipped extends RaiserEvent { +final class OrderShipped implements RaiserEvent { + OrderShipped({ + required this.trackingNumber, + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + final String trackingNumber; - OrderShipped({required this.trackingNumber, super.aggregateId}); + @override + final EventId id; + + @override + final DateTime occurredOn; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'trackingNumber': trackingNumber, - }; + final Map metadata; } // ============================================================================ @@ -106,7 +148,7 @@ class EventStore { /// Get all events for an aggregate List getEventsForAggregate(String aggregateId) { - return _events.where((e) => e.aggregateId == aggregateId).toList(); + return _events.where((e) => e.metadata['aggregateId'] == aggregateId).toList(); } /// Get all events of a specific type @@ -123,15 +165,15 @@ class EventStore { Future _publishTyped(EventBus bus, RaiserEvent event) async { switch (event) { - case CartItemAdded e: + case final CartItemAdded e: await bus.publish(e); - case CartItemRemoved e: + case final CartItemRemoved e: await bus.publish(e); - case CheckoutStarted e: + case final CheckoutStarted e: await bus.publish(e); - case PaymentReceived e: + case final PaymentReceived e: await bus.publish(e); - case OrderShipped e: + case final OrderShipped e: await bus.publish(e); } } @@ -154,7 +196,7 @@ class CartProjection implements EventHandler { @override Future handle(CartItemAdded event) async { - final cartId = event.aggregateId ?? 'default'; + final cartId = event.metadata['aggregateId'] as String? ?? 'default'; _carts.putIfAbsent(cartId, () => {}); final existing = _carts[cartId]![event.productId]; @@ -174,7 +216,7 @@ class CartProjection implements EventHandler { } void handleRemoval(CartItemRemoved event) { - final cartId = event.aggregateId ?? 'default'; + final cartId = event.metadata['aggregateId'] as String? ?? 'default'; _carts[cartId]?.remove(event.productId); } } @@ -209,7 +251,7 @@ class OrderFulfillmentSaga { } Future _onCheckoutStarted(CheckoutStarted event) async { - final orderId = event.aggregateId ?? event.id; + final orderId = event.metadata['aggregateId'] as String? ?? event.id.value; _orderStates[orderId] = OrderState( orderId: orderId, status: 'awaiting_payment', @@ -219,7 +261,7 @@ class OrderFulfillmentSaga { } Future _onPaymentReceived(PaymentReceived event) async { - final orderId = event.aggregateId; + final orderId = event.metadata['aggregateId'] as String?; if (orderId != null && _orderStates.containsKey(orderId)) { _orderStates[orderId] = _orderStates[orderId]!.copyWith( status: 'paid', @@ -230,7 +272,7 @@ class OrderFulfillmentSaga { } Future _onOrderShipped(OrderShipped event) async { - final orderId = event.aggregateId; + final orderId = event.metadata['aggregateId'] as String?; if (orderId != null && _orderStates.containsKey(orderId)) { _orderStates[orderId] = _orderStates[orderId]!.copyWith( status: 'shipped', @@ -339,7 +381,7 @@ Future eventSourcingExample() async { productId: 'SKU-A', quantity: 2, price: 29.99, - aggregateId: cartId, + metadata: {'aggregateId': cartId}, ), ); @@ -348,17 +390,17 @@ Future eventSourcingExample() async { productId: 'SKU-B', quantity: 1, price: 49.99, - aggregateId: cartId, + metadata: {'aggregateId': cartId}, ), ); - await store.append(CartItemRemoved(productId: 'SKU-A', aggregateId: cartId)); + await store.append(CartItemRemoved(productId: 'SKU-A', metadata: {'aggregateId': cartId})); // Query event history final cartEvents = store.getEventsForAggregate(cartId); print(' Events for $cartId: ${cartEvents.length}'); for (final event in cartEvents) { - print(' - ${event.runtimeType} at ${event.timestamp}'); + print(' - ${event.runtimeType} at ${event.occurredOn}'); } } @@ -372,18 +414,18 @@ Future sagaExample() async { const orderId = 'order-500'; // Simulate order flow - await bus.publish(CheckoutStarted(totalAmount: 199.99, aggregateId: orderId)); + await bus.publish(CheckoutStarted(totalAmount: 199.99, metadata: {'aggregateId': orderId})); await bus.publish( PaymentReceived( transactionId: 'txn-12345', amount: 199.99, - aggregateId: orderId, + metadata: {'aggregateId': orderId}, ), ); await bus.publish( - OrderShipped(trackingNumber: 'TRACK-ABC123', aggregateId: orderId), + OrderShipped(trackingNumber: 'TRACK-ABC123', metadata: {'aggregateId': orderId}), ); // Check final state @@ -413,7 +455,7 @@ Future projectionReplayExample() async { productId: 'ITEM-1', quantity: 3, price: 10.00, - aggregateId: cartId, + metadata: {'aggregateId': cartId}, ), ); @@ -422,7 +464,7 @@ Future projectionReplayExample() async { productId: 'ITEM-2', quantity: 2, price: 25.00, - aggregateId: cartId, + metadata: {'aggregateId': cartId}, ), ); @@ -467,7 +509,7 @@ Future multiBusExample() async { PaymentReceived( transactionId: 'multi-bus-txn', amount: 500.00, - aggregateId: 'order-multi', + metadata: {'aggregateId': 'order-multi'}, ), ); } diff --git a/packages/raiser/example/analysis_options.yaml b/packages/raiser/example/analysis_options.yaml new file mode 100644 index 0000000..c4ae387 --- /dev/null +++ b/packages/raiser/example/analysis_options.yaml @@ -0,0 +1,76 @@ +include: package:lints/recommended.yaml + +formatter: + # Keep trailing commas. + trailing_commas: preserve + + # Set the maximum line length. + page_width: 160 + +# Customize additional linter rules. +linter: + # Specify rules to be enabled or disabled. + rules: + # **Style Rules** + # Enforce using const constructors where possible. + prefer_const_constructors: true + prefer_const_literals_to_create_immutables: true + + # Prefer single quotes over double quotes where possible. + prefer_single_quotes: true + + # Enforce a consistent type definition for variables. + always_specify_types: false + + # **Best Practices** + # Avoid using dynamic types. + avoid_dynamic_calls: true + + # Avoid using print statements in production code. In example code this is fine though. + avoid_print: false + + # Prefer using the `final` keyword for variables that are not reassigned. + prefer_final_fields: true + prefer_final_locals: true + + # **Error Prevention** + + # Enforce non-nullable types where possible. + always_require_non_null_named_parameters: true + + # **Documentation** + # Require documentation for public members. + public_member_api_docs: false + + # **Other Useful Rules** + # Enforce sorting of directives (e.g., imports). + directives_ordering: true + + # Prefer using `isEmpty` and `isNotEmpty` over `length` comparisons. + prefer_is_empty: true + prefer_is_not_empty: true + + # **Disable Rules (if necessary)** + # Uncomment the following lines to disable specific lints. + # avoid_unused_constructor_parameters: false + # unnecessary_null_in_if_null_operators: false + +# Analyzer settings. +analyzer: + # Exclude certain files or directories from analysis. + exclude: + - "**/*.g.dart" + - "**/build/**" + - "**/generated/**" + - "**/mocks/**" + - "**/*.freezed.dart/**" + + # Language-specific analyzer strictness options. + language: + strict-casts: false + strict-inference: false + strict-raw-types: false + + strong-mode: + implicit-casts: false + implicit-dynamic: false diff --git a/packages/raiser/example/pubspec.yaml b/packages/raiser/example/pubspec.yaml new file mode 100644 index 0000000..2fcfa55 --- /dev/null +++ b/packages/raiser/example/pubspec.yaml @@ -0,0 +1,22 @@ +name: raiser_example +description: Example usage of the Raiser package +publish_to: none + +environment: + sdk: ^3.10.4 + +dependencies: + + zooper_flutter_core: ^1.0.2 + + raiser: + path: ../ + raiser_annotation: + path: ../../raiser_annotation + +dev_dependencies: + lints: ^6.0.0 + build_runner: ^2.4.0 + raiser_generator: + path: ../../raiser_generator + diff --git a/packages/raiser/example/pubspec_overrides.yaml b/packages/raiser/example/pubspec_overrides.yaml new file mode 100644 index 0000000..82bfd22 --- /dev/null +++ b/packages/raiser/example/pubspec_overrides.yaml @@ -0,0 +1,8 @@ +# melos_managed_dependency_overrides: raiser,raiser_annotation,raiser_generator +dependency_overrides: + raiser: + path: ../ + raiser_annotation: + path: ../../raiser_annotation + raiser_generator: + path: ../../raiser_generator diff --git a/packages/raiser/example/raiser_example.dart b/packages/raiser/example/raiser_example.dart index a4fc886..ba764c8 100644 --- a/packages/raiser/example/raiser_example.dart +++ b/packages/raiser/example/raiser_example.dart @@ -4,67 +4,89 @@ // from basic usage to advanced patterns. import 'package:raiser/raiser.dart'; +import 'package:zooper_flutter_core/zooper_flutter_core.dart'; // ============================================================================ // DOMAIN EVENTS // ============================================================================ /// Basic domain event for user creation -class UserCreated extends RaiserEvent { +final class UserCreated implements RaiserEvent { + UserCreated({ + required this.userId, + required this.email, + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + final String userId; final String email; - UserCreated({required this.userId, required this.email, super.aggregateId}); + @override + final EventId id; + + @override + final DateTime occurredOn; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'userId': userId, - 'email': email, - }; + final Map metadata; } /// Event for order placement with aggregate ID -class OrderPlaced extends RaiserEvent { - final String orderId; - final double amount; - final List items; - +final class OrderPlaced implements RaiserEvent { OrderPlaced({ required this.orderId, required this.amount, required this.items, - super.aggregateId, - }); + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + + final String orderId; + final double amount; + final List items; + + @override + final EventId id; + + @override + final DateTime occurredOn; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'orderId': orderId, - 'amount': amount, - 'items': items, - }; + final Map metadata; } /// Event for payment processing -class PaymentProcessed extends RaiserEvent { - final String paymentId; - final String orderId; - final bool success; - +final class PaymentProcessed implements RaiserEvent { PaymentProcessed({ required this.paymentId, required this.orderId, required this.success, - }); + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + + final String paymentId; + final String orderId; + final bool success; + + @override + final EventId id; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'paymentId': paymentId, - 'orderId': orderId, - 'success': success, - }; + final DateTime occurredOn; + + @override + final Map metadata; } // ============================================================================ @@ -76,7 +98,7 @@ class WelcomeEmailHandler implements EventHandler { @override Future handle(UserCreated event) async { print(' 📧 Sending welcome email to ${event.email}'); - await Future.delayed(Duration(milliseconds: 50)); // Simulate async work + await Future.delayed(const Duration(milliseconds: 50)); // Simulate async work } } @@ -152,7 +174,7 @@ Future classBasedHandlerExample() async { orderId: 'order-001', amount: 99.99, items: ['Widget A', 'Widget B'], - aggregateId: 'user-002', + metadata: {'aggregateId': 'user-002'}, ), ); } @@ -330,21 +352,17 @@ Future aggregateIdExample() async { // Track all events for a specific user aggregate bus.on((event) async { - if (event.aggregateId != null) { + if (event.metadata['aggregateId'] != null) { userEvents.add(event); } - print( - ' 👤 User ${event.userId} created (aggregate: ${event.aggregateId})', - ); + print(' 👤 User ${event.userId} created (aggregate: ${event.metadata['aggregateId']})'); }); bus.on((event) async { - if (event.aggregateId != null) { + if (event.metadata['aggregateId'] != null) { userEvents.add(event); } - print( - ' 🛒 Order ${event.orderId} placed (aggregate: ${event.aggregateId})', - ); + print(' 🛒 Order ${event.orderId} placed (aggregate: ${event.metadata['aggregateId']})'); }); // Events linked to user aggregate @@ -354,7 +372,7 @@ Future aggregateIdExample() async { UserCreated( userId: userId, email: 'customer@shop.com', - aggregateId: userId, + metadata: {'aggregateId': userId}, ), ); @@ -363,7 +381,7 @@ Future aggregateIdExample() async { orderId: 'order-A', amount: 150.00, items: ['Product X'], - aggregateId: userId, + metadata: {'aggregateId': userId}, ), ); @@ -372,7 +390,7 @@ Future aggregateIdExample() async { orderId: 'order-B', amount: 75.50, items: ['Product Y', 'Product Z'], - aggregateId: userId, + metadata: {'aggregateId': userId}, ), ); @@ -387,15 +405,15 @@ Future metadataExample() async { orderId: 'order-999', amount: 299.99, items: ['Premium Widget', 'Deluxe Gadget'], - aggregateId: 'user-500', + metadata: {'aggregateId': 'user-500'}, ); print(' Event ID: ${event.id}'); - print(' Timestamp: ${event.timestamp}'); - print(' Aggregate ID: ${event.aggregateId}'); + print(' Occurred On: ${event.occurredOn}'); + print(' Aggregate ID: ${event.metadata['aggregateId']}'); print(' Metadata Map:'); - final metadata = event.toMetadataMap(); + final metadata = event.metadata; metadata.forEach((key, value) { print(' $key: $value'); }); diff --git a/packages/raiser/lib/raiser.dart b/packages/raiser/lib/raiser.dart index 1e204e9..e25b6cc 100644 --- a/packages/raiser/lib/raiser.dart +++ b/packages/raiser/lib/raiser.dart @@ -43,13 +43,17 @@ /// ``` library; +import 'src/events/raiser_event.dart'; +import 'src/handlers/event_handler.dart'; +import 'src/handlers/subscription.dart'; + +// Event bus and error handling +export 'src/bus/error_strategy.dart'; +export 'src/bus/event_bus.dart'; + // Core event types export 'src/events/raiser_event.dart'; // Handler interfaces export 'src/handlers/event_handler.dart'; export 'src/handlers/subscription.dart'; - -// Event bus and error handling -export 'src/bus/event_bus.dart'; -export 'src/bus/error_strategy.dart'; diff --git a/packages/raiser/lib/src/bus/error_strategy.dart b/packages/raiser/lib/src/bus/error_strategy.dart index 3477914..009a5ff 100644 --- a/packages/raiser/lib/src/bus/error_strategy.dart +++ b/packages/raiser/lib/src/bus/error_strategy.dart @@ -1,9 +1,3 @@ -/// Error handling strategies for the EventBus. -/// -/// Configures how the EventBus handles exceptions thrown by handlers -/// during event publication. -library; - /// Defines how the EventBus handles handler exceptions. enum ErrorStrategy { /// Stop propagation on first error, rethrow immediately. diff --git a/packages/raiser/lib/src/bus/event_bus.dart b/packages/raiser/lib/src/bus/event_bus.dart index 34106a6..6b6f2a2 100644 --- a/packages/raiser/lib/src/bus/event_bus.dart +++ b/packages/raiser/lib/src/bus/event_bus.dart @@ -1,12 +1,3 @@ -/// EventBus - Central dispatcher for publishing events and managing handlers. -/// -/// Provides type-safe event routing with support for: -/// - Class-based and function-based handlers -/// - Priority-based handler ordering -/// - Middleware pipeline for cross-cutting concerns -/// - Configurable error handling strategies -library; - import '../handlers/event_handler.dart'; import '../handlers/subscription.dart'; import 'error_strategy.dart'; @@ -20,8 +11,7 @@ typedef ErrorCallback = void Function(Object error, StackTrace stackTrace); /// /// Middleware receives the event and a `next` function to call the next /// middleware or handler in the pipeline. -typedef Middleware = - Future Function(dynamic event, Future Function() next); +typedef Middleware = Future Function(dynamic event, Future Function() next); /// Internal class for storing handler registrations with metadata. class _HandlerEntry { @@ -119,11 +109,7 @@ class EventBus { }; } - final entry = _MiddlewareEntry( - middlewareFunc, - priority, - _middlewareCounter++, - ); + final entry = _MiddlewareEntry(middlewareFunc, priority, _middlewareCounter++); _middleware.add(entry); return Subscription(() { @@ -147,10 +133,7 @@ class EventBus { /// Higher priority handlers execute first. /// /// Returns a [Subscription] that can be used to cancel the registration. - Subscription on( - Future Function(T event) handler, { - int priority = 0, - }) { + Subscription on(Future Function(T event) handler, {int priority = 0}) { return _addHandler(handler, priority); } diff --git a/packages/raiser/lib/src/events/raiser_event.dart b/packages/raiser/lib/src/events/raiser_event.dart index eb5ef4f..9c26e9a 100644 --- a/packages/raiser/lib/src/events/raiser_event.dart +++ b/packages/raiser/lib/src/events/raiser_event.dart @@ -1,79 +1,7 @@ -/// Domain event base class providing consistent metadata for all events. -/// -/// This abstract class provides: -/// - Automatic unique ID generation -/// - Automatic timestamp capture -/// - Optional aggregate ID for DDD patterns -/// - Serialization support via metadata maps -library; +import 'package:zooper_flutter_core/zooper_flutter_core.dart'; -/// Abstract base class for domain events with consistent metadata. -/// -/// Events are immutable after construction. Each event automatically -/// receives a unique ID and timestamp unless explicitly provided. -/// -/// Example: -/// ```dart -/// class UserCreated extends RaiserEvent { -/// final String userId; -/// final String email; +/// A raiser event is a Zooper domain event with an [EventId] identifier. /// -/// UserCreated({required this.userId, required this.email, super.aggregateId}); -/// -/// @override -/// Map toMetadataMap() => { -/// ...super.toMetadataMap(), -/// 'userId': userId, -/// 'email': email, -/// }; -/// } -/// ``` -abstract class RaiserEvent { - /// Unique identifier for this event instance. - final String id; - - /// Timestamp when the event was created. - final DateTime timestamp; - - /// Optional aggregate ID for DDD patterns. - final String? aggregateId; - - /// Counter for generating unique IDs within the same process. - static int _idCounter = 0; - - /// Creates a new domain event. - /// - /// If [id] is not provided, a unique ID is automatically generated. - /// If [timestamp] is not provided, the current time is used. - /// [aggregateId] is optional and links the event to a domain aggregate. - RaiserEvent({String? id, DateTime? timestamp, this.aggregateId}) - : id = id ?? _generateId(), - timestamp = timestamp ?? DateTime.now(); - - /// Generates a unique ID for an event. - /// - /// Uses a combination of timestamp and counter to ensure uniqueness - /// within the same process without external dependencies. - static String _generateId() { - final now = DateTime.now(); - final counter = _idCounter++; - return '${now.microsecondsSinceEpoch}-$counter'; - } - - /// Converts event metadata to a Map for serialization. - /// - /// Subclasses should override this method and include their own - /// fields in addition to the base metadata: - /// ```dart - /// @override - /// Map toMetadataMap() => { - /// ...super.toMetadataMap(), - /// 'myField': myField, - /// }; - /// ``` - Map toMetadataMap() => { - 'id': id, - 'timestamp': timestamp.toIso8601String(), - 'aggregateId': aggregateId, - }; -} +/// This type is intentionally an interface so applications are not forced to +/// extend a specific base class (Dart supports only single inheritance). +abstract interface class RaiserEvent implements ZooperDomainEvent {} diff --git a/packages/raiser/lib/src/handlers/event_handler.dart b/packages/raiser/lib/src/handlers/event_handler.dart index fd290cb..e85b5fa 100644 --- a/packages/raiser/lib/src/handlers/event_handler.dart +++ b/packages/raiser/lib/src/handlers/event_handler.dart @@ -1,10 +1,3 @@ -/// Event handler interface for type-safe event handling. -/// -/// Provides a generic interface for handling events of a specific type. -/// Handlers are invoked asynchronously by the EventBus when matching -/// events are published. -library; - /// Abstract interface for type-safe event handlers. /// /// Implement this interface to create handlers that process specific diff --git a/packages/raiser/lib/src/handlers/subscription.dart b/packages/raiser/lib/src/handlers/subscription.dart index 08bc680..3fa49f3 100644 --- a/packages/raiser/lib/src/handlers/subscription.dart +++ b/packages/raiser/lib/src/handlers/subscription.dart @@ -1,9 +1,3 @@ -/// Subscription class for managing handler registrations. -/// -/// Represents a handler registration that can be cancelled to stop -/// receiving events. -library; - /// Represents a handler registration that can be cancelled. /// /// When a handler is registered with the EventBus, a Subscription is diff --git a/packages/raiser/pubspec.yaml b/packages/raiser/pubspec.yaml index 40f2d97..da22ae4 100644 --- a/packages/raiser/pubspec.yaml +++ b/packages/raiser/pubspec.yaml @@ -16,6 +16,9 @@ topics: - clean-architecture - pub-sub +dependencies: + zooper_flutter_core: ^1.0.2 + dev_dependencies: lints: ^6.0.0 test: ^1.25.6 diff --git a/packages/raiser/test/domain_event_test.dart b/packages/raiser/test/domain_event_test.dart index 3da9f7f..883bd41 100644 --- a/packages/raiser/test/domain_event_test.dart +++ b/packages/raiser/test/domain_event_test.dart @@ -1,24 +1,44 @@ import 'package:raiser/raiser.dart'; import 'package:test/test.dart'; +import 'package:zooper_flutter_core/zooper_flutter_core.dart'; /// Simple concrete RaiserEvent for testing purposes. -class TestEvent extends RaiserEvent { - final String name; +class TestEvent implements RaiserEvent { + @override + final EventId id; - TestEvent({required this.name, super.id, super.timestamp, super.aggregateId}); + @override + final DateTime occurredOn; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'name': name, - }; + final Map metadata; + + final String name; + + TestEvent({ + required this.name, + EventId? id, + DateTime? occurredOn, + Map? metadata, + }) : id = id ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata ?? const {}); + + Map toMetadataMap() { + return { + 'id': id.value, + 'occurredOn': occurredOn.toIso8601String(), + 'metadata': metadata, + 'name': name, + }; + } /// Reconstructs a TestEvent from a metadata map. static TestEvent fromMetadataMap(Map map) { return TestEvent( - id: map['id'] as String, - timestamp: DateTime.parse(map['timestamp'] as String), - aggregateId: map['aggregateId'] as String?, + id: EventId.fromJson(map['id'] as String), + occurredOn: DateTime.parse(map['occurredOn'] as String), + metadata: Map.from(map['metadata'] as Map), name: map['name'] as String, ); } @@ -33,52 +53,39 @@ void main() { expect(ids.length, equals(events.length)); }); - // **Feature: core-event-system, Property 3: Aggregate ID Preservation** - test('Property 3: aggregate ID is preserved exactly as provided', () { - final testIds = ['agg-1', 'agg-123', 'user-abc', 'order-xyz-999']; - for (final aggregateId in testIds) { - final event = TestEvent(name: 'test', aggregateId: aggregateId); - expect(event.aggregateId, equals(aggregateId)); - } - }); + // **Feature: core-event-system, Property 3: Metadata Preservation** + test('Property 3: metadata map preserves provided keys and values', () { + final TestEvent event = TestEvent(name: 'test', metadata: {'aggregateId': 'agg-123', 'correlationId': 'corr-1'}); - test('Property 3: null aggregate ID is preserved', () { - final event = TestEvent(name: 'test'); - expect(event.aggregateId, isNull); + expect(event.metadata['aggregateId'], equals('agg-123')); + expect(event.metadata['correlationId'], equals('corr-1')); }); // **Feature: core-event-system, Property 2: Event Metadata Round-Trip** - test( - 'Property 2: serializing and deserializing preserves event metadata', - () { - final testCases = [ - TestEvent(name: 'simple'), - TestEvent(name: 'with-aggregate', aggregateId: 'agg-123'), - TestEvent(name: 'special chars !@#'), - ]; + test('Property 2: serializing and deserializing preserves event metadata', () { + final testCases = [ + TestEvent(name: 'simple'), + TestEvent(name: 'with-aggregate', metadata: {'aggregateId': 'agg-123'}), + TestEvent(name: 'special chars !@#'), + ]; - for (final original in testCases) { - final map = original.toMetadataMap(); - final reconstructed = TestEvent.fromMetadataMap(map); + for (final original in testCases) { + final Map map = original.toMetadataMap(); + final TestEvent reconstructed = TestEvent.fromMetadataMap(Map.from(map)); - expect(reconstructed.id, equals(original.id)); - expect(reconstructed.timestamp, equals(original.timestamp)); - expect(reconstructed.aggregateId, equals(original.aggregateId)); - expect(reconstructed.name, equals(original.name)); - } - }, - ); + expect(reconstructed.id, equals(original.id)); + expect(reconstructed.occurredOn, equals(original.occurredOn)); + expect(reconstructed.name, equals(original.name)); + } + }); - test('timestamp is automatically set', () { + test('occurredOn is automatically set', () { final before = DateTime.now(); final event = TestEvent(name: 'test'); final after = DateTime.now(); - expect( - event.timestamp.isAfter(before.subtract(Duration(seconds: 1))), - isTrue, - ); - expect(event.timestamp.isBefore(after.add(Duration(seconds: 1))), isTrue); + expect(event.occurredOn.isAfter(before.subtract(const Duration(seconds: 1))), isTrue); + expect(event.occurredOn.isBefore(after.add(const Duration(seconds: 1))), isTrue); }); }); } diff --git a/packages/raiser/test/edge_cases_test.dart b/packages/raiser/test/edge_cases_test.dart index ce0dd5b..3b77247 100644 --- a/packages/raiser/test/edge_cases_test.dart +++ b/packages/raiser/test/edge_cases_test.dart @@ -1,14 +1,31 @@ import 'package:raiser/raiser.dart'; import 'package:test/test.dart'; +import 'package:zooper_flutter_core/zooper_flutter_core.dart'; /// Edge case and stress tests for the EventBus. /// /// These tests verify behavior in unusual or extreme scenarios. -class TestEvent extends RaiserEvent { +final class TestEvent implements RaiserEvent { + TestEvent( + this.value, { + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + final String value; - TestEvent(this.value); @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; + Map toMetadataMap() => {'value': value}; } @@ -85,14 +102,8 @@ void main() { final order = []; bus.on((e) async => order.add(0), priority: 0); - bus.on( - (e) async => order.add(-2147483648), - priority: -2147483648, - ); - bus.on( - (e) async => order.add(2147483647), - priority: 2147483647, - ); + bus.on((e) async => order.add(-2147483648), priority: -2147483648); + bus.on((e) async => order.add(2147483647), priority: 2147483647); await bus.publish(TestEvent('test')); @@ -131,10 +142,7 @@ void main() { await bus.publish(TestEvent('initial')); - expect( - received, - equals(['initial', 'nested-1', 'nested-2', 'nested-3']), - ); + expect(received, equals(['initial', 'nested-1', 'nested-2', 'nested-3'])); }); test('many concurrent publishes complete correctly', () async { @@ -142,13 +150,11 @@ void main() { final received = {}; bus.on((event) async { - await Future.delayed(Duration(milliseconds: 1)); + await Future.delayed(const Duration(milliseconds: 1)); received.add(event.value); }); - await Future.wait( - List.generate(100, (i) => bus.publish(TestEvent('event-$i'))), - ); + await Future.wait(List.generate(100, (i) => bus.publish(TestEvent('event-$i')))); expect(received.length, equals(100)); }); @@ -218,16 +224,7 @@ void main() { throw Exception('error 3'); }); - expect( - () => bus.publish(TestEvent('test')), - throwsA( - isA().having( - (e) => e.errors.length, - 'error count', - equals(3), - ), - ), - ); + expect(() => bus.publish(TestEvent('test')), throwsA(isA().having((e) => e.errors.length, 'error count', equals(3)))); }); }); @@ -279,10 +276,10 @@ void main() { final bus = EventBus(); final order = []; - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { order.add('outer:before'); // Add another middleware during execution - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { order.add('dynamic:before'); await next(); order.add('dynamic:after'); @@ -312,7 +309,7 @@ void main() { final order = []; late Subscription middlewareSub; - middlewareSub = bus.addMiddleware((event, next) async { + middlewareSub = bus.addMiddleware((RaiserEvent event, Future Function() next) async { order.add('middleware:before'); middlewareSub.cancel(); // Cancel self await next(); @@ -343,7 +340,7 @@ void main() { for (var i = 0; i < 50; i++) { final index = i; - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { order.add('m$index:before'); await next(); order.add('m$index:after'); @@ -366,10 +363,26 @@ void main() { } /// Another event type for type isolation tests. -class OtherEvent extends RaiserEvent { +final class OtherEvent implements RaiserEvent { + OtherEvent( + this.value, { + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + final int value; - OtherEvent(this.value); @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; + Map toMetadataMap() => {'value': value}; } diff --git a/packages/raiser/test/error_handling_test.dart b/packages/raiser/test/error_handling_test.dart index 8dff23c..2e94f78 100644 --- a/packages/raiser/test/error_handling_test.dart +++ b/packages/raiser/test/error_handling_test.dart @@ -189,8 +189,7 @@ void main() { expect( executionOrder, equals(List.generate(totalHandlers, (i) => i)), - reason: - 'All $totalHandlers handlers should execute with $failCount failures', + reason: 'All $totalHandlers handlers should execute with $failCount failures', ); } }); @@ -294,8 +293,7 @@ void main() { expect( executionOrder, equals(List.generate(totalHandlers, (i) => i)), - reason: - 'All $totalHandlers handlers should execute with $failCount failures', + reason: 'All $totalHandlers handlers should execute with $failCount failures', ); } }); @@ -422,7 +420,7 @@ void main() { }, priority: 20); bus.on((event) async { - throw FormatException('Error 3'); + throw const FormatException('Error 3'); }, priority: 10); // Should complete without throwing diff --git a/packages/raiser/test/event_bus_test.dart b/packages/raiser/test/event_bus_test.dart index 46dabdb..474006a 100644 --- a/packages/raiser/test/event_bus_test.dart +++ b/packages/raiser/test/event_bus_test.dart @@ -106,12 +106,12 @@ void main() { var handler2Completed = false; bus.on((event) async { - await Future.delayed(Duration(milliseconds: 50)); + await Future.delayed(const Duration(milliseconds: 50)); handler1Completed = true; }); bus.on((event) async { - await Future.delayed(Duration(milliseconds: 30)); + await Future.delayed(const Duration(milliseconds: 30)); handler2Completed = true; }); diff --git a/packages/raiser/test/middleware_test.dart b/packages/raiser/test/middleware_test.dart index 8c89e03..b35a512 100644 --- a/packages/raiser/test/middleware_test.dart +++ b/packages/raiser/test/middleware_test.dart @@ -1,21 +1,54 @@ import 'package:raiser/raiser.dart'; import 'package:test/test.dart'; +import 'package:zooper_flutter_core/zooper_flutter_core.dart'; /// Simple event for middleware testing. -class TestEvent extends RaiserEvent { +final class TestEvent implements RaiserEvent { + TestEvent( + this.message, { + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + final String message; - TestEvent(this.message); @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; + Map toMetadataMap() => {'message': message}; } /// Another event type for testing type-specific behavior. -class OtherEvent extends RaiserEvent { +final class OtherEvent implements RaiserEvent { + OtherEvent( + this.value, { + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + final int value; - OtherEvent(this.value); @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; + Map toMetadataMap() => {'value': value}; } @@ -112,7 +145,7 @@ void main() { final bus = EventBus(); final logs = []; - final subscription = bus.addMiddleware((event, next) async { + final subscription = bus.addMiddleware((RaiserEvent event, Future Function() next) async { logs.add('before'); await next(); logs.add('after'); @@ -159,10 +192,7 @@ void main() { await bus.publish(TestEvent('hello')); - expect( - middleware.logs, - equals(['before:TestEvent', 'after:TestEvent']), - ); + expect(middleware.logs, equals(['before:TestEvent', 'after:TestEvent'])); expect(handler.received, equals(['hello'])); }); @@ -173,10 +203,7 @@ void main() { bus.addMiddleware(middleware); await bus.publish(TestEvent('hello')); - expect( - middleware.logs, - equals(['before:TestEvent', 'after:TestEvent']), - ); + expect(middleware.logs, equals(['before:TestEvent', 'after:TestEvent'])); }); test('multiple middleware form a pipeline', () async { @@ -196,21 +223,15 @@ void main() { // Default priority is 0, so execution order depends on registration order // All middleware should wrap the handler expect(executionLog, contains('handler')); - expect( - executionLog.where((e) => e.contains('before')).length, - equals(3), - ); - expect( - executionLog.where((e) => e.contains('after')).length, - equals(3), - ); + expect(executionLog.where((e) => e.contains('before')).length, equals(3)); + expect(executionLog.where((e) => e.contains('after')).length, equals(3)); }); test('function middleware works correctly', () async { final bus = EventBus(); final logs = []; - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { logs.add('before'); await next(); logs.add('after'); @@ -231,18 +252,9 @@ void main() { final bus = EventBus(); final executionLog = []; - bus.addMiddleware( - OrderTrackingMiddleware('low', executionLog), - priority: -10, - ); - bus.addMiddleware( - OrderTrackingMiddleware('high', executionLog), - priority: 100, - ); - bus.addMiddleware( - OrderTrackingMiddleware('mid', executionLog), - priority: 50, - ); + bus.addMiddleware(OrderTrackingMiddleware('low', executionLog), priority: -10); + bus.addMiddleware(OrderTrackingMiddleware('high', executionLog), priority: 100); + bus.addMiddleware(OrderTrackingMiddleware('mid', executionLog), priority: 50); bus.on((event) async { executionLog.add('handler'); @@ -264,18 +276,9 @@ void main() { final bus = EventBus(); final executionLog = []; - bus.addMiddleware( - OrderTrackingMiddleware('first', executionLog), - priority: 0, - ); - bus.addMiddleware( - OrderTrackingMiddleware('second', executionLog), - priority: 0, - ); - bus.addMiddleware( - OrderTrackingMiddleware('third', executionLog), - priority: 0, - ); + bus.addMiddleware(OrderTrackingMiddleware('first', executionLog), priority: 0); + bus.addMiddleware(OrderTrackingMiddleware('second', executionLog), priority: 0); + bus.addMiddleware(OrderTrackingMiddleware('third', executionLog), priority: 0); bus.on((event) async { executionLog.add('handler'); @@ -294,14 +297,8 @@ void main() { final bus = EventBus(); final executionLog = []; - bus.addMiddleware( - OrderTrackingMiddleware('negative', executionLog), - priority: -50, - ); - bus.addMiddleware( - OrderTrackingMiddleware('zero', executionLog), - priority: 0, - ); + bus.addMiddleware(OrderTrackingMiddleware('negative', executionLog), priority: -50); + bus.addMiddleware(OrderTrackingMiddleware('zero', executionLog), priority: 0); bus.on((event) async { executionLog.add('handler'); @@ -347,18 +344,9 @@ void main() { final bus = EventBus(); final executionLog = []; - bus.addMiddleware( - OrderTrackingMiddleware('outer', executionLog), - priority: 100, - ); - bus.addMiddleware( - ShortCircuitMiddleware(shouldShortCircuit: true), - priority: 50, - ); - bus.addMiddleware( - OrderTrackingMiddleware('inner', executionLog), - priority: 0, - ); + bus.addMiddleware(OrderTrackingMiddleware('outer', executionLog), priority: 100); + bus.addMiddleware(ShortCircuitMiddleware(shouldShortCircuit: true), priority: 50); + bus.addMiddleware(OrderTrackingMiddleware('inner', executionLog), priority: 0); bus.on((event) async { executionLog.add('handler'); @@ -376,16 +364,7 @@ void main() { final bus = EventBus(errorStrategy: ErrorStrategy.stop); bus.addMiddleware(ErrorMiddleware('middleware error')); - expect( - () => bus.publish(TestEvent('test')), - throwsA( - isA().having( - (e) => e.toString(), - 'message', - contains('middleware error'), - ), - ), - ); + expect(() => bus.publish(TestEvent('test')), throwsA(isA().having((e) => e.toString(), 'message', contains('middleware error')))); }); test('middleware error propagates even with swallow strategy', () async { @@ -394,16 +373,7 @@ void main() { final bus = EventBus(errorStrategy: ErrorStrategy.swallow); bus.addMiddleware(ErrorMiddleware('middleware error')); - expect( - () => bus.publish(TestEvent('test')), - throwsA( - isA().having( - (e) => e.toString(), - 'message', - contains('middleware error'), - ), - ), - ); + expect(() => bus.publish(TestEvent('test')), throwsA(isA().having((e) => e.toString(), 'message', contains('middleware error')))); }); test('handler errors are caught by error strategy', () async { @@ -417,7 +387,7 @@ void main() { // Middleware that passes through final logs = []; - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { logs.add('before'); await next(); logs.add('after'); @@ -443,7 +413,7 @@ void main() { final bus = EventBus(); final receivedTypes = []; - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { receivedTypes.add(event.runtimeType); await next(); }); @@ -461,7 +431,7 @@ void main() { final bus = EventBus(); final processedEvents = []; - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { if (event is TestEvent) { processedEvents.add('processed:${event.message}'); } @@ -484,14 +454,8 @@ void main() { final bus = EventBus(); final state = {}; - bus.addMiddleware( - StateModifyingMiddleware(state, 'step1', 'done'), - priority: 100, - ); - bus.addMiddleware( - StateModifyingMiddleware(state, 'step2', 'done'), - priority: 50, - ); + bus.addMiddleware(StateModifyingMiddleware(state, 'step1', 'done'), priority: 100); + bus.addMiddleware(StateModifyingMiddleware(state, 'step2', 'done'), priority: 50); bus.on((event) async { state['handler'] = 'executed'; @@ -499,35 +463,29 @@ void main() { await bus.publish(TestEvent('test')); - expect( - state, - equals({'step1': 'done', 'step2': 'done', 'handler': 'executed'}), - ); + expect(state, equals({'step1': 'done', 'step2': 'done', 'handler': 'executed'})); }); - test( - 'middleware state changes are visible to inner middleware', - () async { - final bus = EventBus(); - final state = {}; - var step2SawStep1 = false; - - bus.addMiddleware((event, next) async { - state['step1'] = true; - await next(); - }, priority: 100); - - bus.addMiddleware((event, next) async { - step2SawStep1 = state['step1'] == true; - await next(); - }, priority: 50); - - bus.on((event) async {}); - await bus.publish(TestEvent('test')); - - expect(step2SawStep1, isTrue); - }, - ); + test('middleware state changes are visible to inner middleware', () async { + final bus = EventBus(); + final state = {}; + var step2SawStep1 = false; + + bus.addMiddleware((RaiserEvent event, Future Function() next) async { + state['step1'] = true; + await next(); + }, priority: 100); + + bus.addMiddleware((RaiserEvent event, Future Function() next) async { + step2SawStep1 = state['step1'] == true; + await next(); + }, priority: 50); + + bus.on((event) async {}); + await bus.publish(TestEvent('test')); + + expect(step2SawStep1, isTrue); + }); }); group('Middleware and Handler Interaction', () { @@ -536,7 +494,7 @@ void main() { var handlerCount = 0; var middlewareCount = 0; - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { middlewareCount++; await next(); }); @@ -561,7 +519,7 @@ void main() { final bus = EventBus(); Duration? executionTime; - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { final stopwatch = Stopwatch()..start(); await next(); stopwatch.stop(); @@ -569,7 +527,7 @@ void main() { }); bus.on((event) async { - await Future.delayed(Duration(milliseconds: 50)); + await Future.delayed(const Duration(milliseconds: 50)); }); await bus.publish(TestEvent('test')); @@ -594,7 +552,7 @@ void main() { final bus = EventBus(); final logs = []; - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { logs.add('before'); await next(); logs.add('after'); @@ -623,31 +581,21 @@ void main() { final bus = EventBus(); final invocations = []; - bus.addMiddleware((event, next) async { + bus.addMiddleware((RaiserEvent event, Future Function() next) async { invocations.add('start:${(event as TestEvent).message}'); await next(); invocations.add('end:${(event).message}'); }); bus.on((event) async { - await Future.delayed(Duration(milliseconds: 10)); + await Future.delayed(const Duration(milliseconds: 10)); }); - await Future.wait([ - bus.publish(TestEvent('A')), - bus.publish(TestEvent('B')), - bus.publish(TestEvent('C')), - ]); + await Future.wait([bus.publish(TestEvent('A')), bus.publish(TestEvent('B')), bus.publish(TestEvent('C'))]); // All events should have start and end - expect( - invocations.where((s) => s.startsWith('start:')).length, - equals(3), - ); - expect( - invocations.where((s) => s.startsWith('end:')).length, - equals(3), - ); + expect(invocations.where((s) => s.startsWith('start:')).length, equals(3)); + expect(invocations.where((s) => s.startsWith('end:')).length, equals(3)); }); }); }); diff --git a/packages/raiser/test/raiser_test.dart b/packages/raiser/test/raiser_test.dart index f038983..6228ab2 100644 --- a/packages/raiser/test/raiser_test.dart +++ b/packages/raiser/test/raiser_test.dart @@ -1,13 +1,14 @@ import 'package:raiser/raiser.dart'; import 'package:test/test.dart'; +import 'package:zooper_flutter_core/zooper_flutter_core.dart'; void main() { group('Public API exports', () { test('RaiserEvent is exported and usable', () { - // Verify RaiserEvent can be extended + // Verify RaiserEvent can be implemented via a mixin. final event = _TestEvent(); expect(event.id, isNotEmpty); - expect(event.timestamp, isA()); + expect(event.occurredOn, isA()); }); test('EventBus is exported and usable', () { @@ -34,17 +35,31 @@ void main() { }); test('AggregateException is exported and usable', () { - final exception = AggregateException( - [Exception('test')], - [StackTrace.current], - ); + final exception = AggregateException([Exception('test')], [StackTrace.current]); expect(exception, isA()); expect(exception.errors.length, equals(1)); }); }); } -class _TestEvent extends RaiserEvent {} +final class _TestEvent implements RaiserEvent { + _TestEvent({ + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} class _TestHandler implements EventHandler<_TestEvent> { @override diff --git a/packages/raiser_generator/example/analysis_options.yaml b/packages/raiser_generator/example/analysis_options.yaml new file mode 100644 index 0000000..c4ae387 --- /dev/null +++ b/packages/raiser_generator/example/analysis_options.yaml @@ -0,0 +1,76 @@ +include: package:lints/recommended.yaml + +formatter: + # Keep trailing commas. + trailing_commas: preserve + + # Set the maximum line length. + page_width: 160 + +# Customize additional linter rules. +linter: + # Specify rules to be enabled or disabled. + rules: + # **Style Rules** + # Enforce using const constructors where possible. + prefer_const_constructors: true + prefer_const_literals_to_create_immutables: true + + # Prefer single quotes over double quotes where possible. + prefer_single_quotes: true + + # Enforce a consistent type definition for variables. + always_specify_types: false + + # **Best Practices** + # Avoid using dynamic types. + avoid_dynamic_calls: true + + # Avoid using print statements in production code. In example code this is fine though. + avoid_print: false + + # Prefer using the `final` keyword for variables that are not reassigned. + prefer_final_fields: true + prefer_final_locals: true + + # **Error Prevention** + + # Enforce non-nullable types where possible. + always_require_non_null_named_parameters: true + + # **Documentation** + # Require documentation for public members. + public_member_api_docs: false + + # **Other Useful Rules** + # Enforce sorting of directives (e.g., imports). + directives_ordering: true + + # Prefer using `isEmpty` and `isNotEmpty` over `length` comparisons. + prefer_is_empty: true + prefer_is_not_empty: true + + # **Disable Rules (if necessary)** + # Uncomment the following lines to disable specific lints. + # avoid_unused_constructor_parameters: false + # unnecessary_null_in_if_null_operators: false + +# Analyzer settings. +analyzer: + # Exclude certain files or directories from analysis. + exclude: + - "**/*.g.dart" + - "**/build/**" + - "**/generated/**" + - "**/mocks/**" + - "**/*.freezed.dart/**" + + # Language-specific analyzer strictness options. + language: + strict-casts: false + strict-inference: false + strict-raw-types: false + + strong-mode: + implicit-casts: false + implicit-dynamic: false diff --git a/packages/raiser_generator/example/lib/events.dart b/packages/raiser_generator/example/lib/events.dart index 28a2353..3d33461 100644 --- a/packages/raiser_generator/example/lib/events.dart +++ b/packages/raiser_generator/example/lib/events.dart @@ -1,82 +1,102 @@ -/// Example domain events for testing the Raiser code generator. -/// -/// These events are used by the example handlers and middleware -/// to demonstrate various generator configurations. - import 'package:raiser/raiser.dart'; +import 'package:zooper_flutter_core/zooper_flutter_core.dart'; /// Event fired when a user is created. -class UserCreatedEvent extends RaiserEvent { - final String userId; - final String email; - +final class UserCreatedEvent implements RaiserEvent { UserCreatedEvent({ required this.userId, required this.email, - super.aggregateId, - }); + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + + final String userId; + final String email; + + @override + final EventId id; + + @override + final DateTime occurredOn; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'userId': userId, - 'email': email, - }; + final Map metadata; } /// Event fired when an order is placed. -class OrderPlacedEvent extends RaiserEvent { - final String orderId; - final double amount; - +final class OrderPlacedEvent implements RaiserEvent { OrderPlacedEvent({ required this.orderId, required this.amount, - super.aggregateId, - }); + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + + final String orderId; + final double amount; + + @override + final EventId id; + + @override + final DateTime occurredOn; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'orderId': orderId, - 'amount': amount, - }; + final Map metadata; } /// Event fired when a payment is processed. -class PaymentProcessedEvent extends RaiserEvent { - final String paymentId; - final bool success; - +final class PaymentProcessedEvent implements RaiserEvent { PaymentProcessedEvent({ required this.paymentId, required this.success, - super.aggregateId, - }); + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + + final String paymentId; + final bool success; + + @override + final EventId id; + + @override + final DateTime occurredOn; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'paymentId': paymentId, - 'success': success, - }; + final Map metadata; } /// Event fired when inventory is updated. -class InventoryUpdatedEvent extends RaiserEvent { - final String productId; - final int quantity; - +final class InventoryUpdatedEvent implements RaiserEvent { InventoryUpdatedEvent({ required this.productId, required this.quantity, - super.aggregateId, - }); + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime.now(), + metadata = Map.unmodifiable(metadata); + + final String productId; + final int quantity; + + @override + final EventId id; + + @override + final DateTime occurredOn; @override - Map toMetadataMap() => { - ...super.toMetadataMap(), - 'productId': productId, - 'quantity': quantity, - }; + final Map metadata; } diff --git a/packages/raiser_generator/example/lib/handlers.dart b/packages/raiser_generator/example/lib/handlers.dart index 9076803..8ba271f 100644 --- a/packages/raiser_generator/example/lib/handlers.dart +++ b/packages/raiser_generator/example/lib/handlers.dart @@ -1,12 +1,3 @@ -/// Example handlers demonstrating various @RaiserHandler configurations. -/// -/// This file contains handlers with different configurations: -/// - Basic handler with default settings -/// - Handler with priority -/// - Handler with named bus -/// - Handler with dependency injection -/// - Multiple handlers for the same event type - import 'package:raiser/raiser.dart'; import 'package:raiser_annotation/raiser_annotation.dart'; diff --git a/packages/raiser_generator/example/lib/middleware.dart b/packages/raiser_generator/example/lib/middleware.dart index 66a641b..0fc837e 100644 --- a/packages/raiser_generator/example/lib/middleware.dart +++ b/packages/raiser_generator/example/lib/middleware.dart @@ -1,13 +1,4 @@ -/// Example middleware demonstrating various @RaiserMiddleware configurations. -/// -/// This file contains middleware with different configurations: -/// - Basic middleware with default settings -/// - Middleware with priority -/// - Middleware with named bus -/// - Middleware with dependency injection - import 'package:raiser_annotation/raiser_annotation.dart'; - import 'services.dart'; // ============================================================================ diff --git a/packages/raiser_generator/example/lib/raiser.g.dart b/packages/raiser_generator/example/lib/raiser.g.dart index 2f19544..cf0724e 100644 --- a/packages/raiser_generator/example/lib/raiser.g.dart +++ b/packages/raiser_generator/example/lib/raiser.g.dart @@ -44,8 +44,7 @@ void initRaiser(EventBus bus) { typedef LoggingUserHandlerFactory = LoggingUserHandler Function(); typedef OrderProcessingHandlerFactory = OrderProcessingHandler Function(); -typedef StructuredLoggingMiddlewareFactory = - StructuredLoggingMiddleware Function(); +typedef StructuredLoggingMiddlewareFactory = StructuredLoggingMiddleware Function(); typedef AuthorizationMiddlewareFactory = AuthorizationMiddleware Function(); /// Initializes Raiser with factory functions for dependency injection. @@ -119,8 +118,7 @@ void initRaiserPaymentsBus(EventBus bus) { bus.register(PaymentAuditHandler(), priority: 50); } -typedef ConfigurablePaymentHandlerFactory = - ConfigurablePaymentHandler Function(); +typedef ConfigurablePaymentHandlerFactory = ConfigurablePaymentHandler Function(); typedef RateLimitingMiddlewareFactory = RateLimitingMiddleware Function(); /// Initializes Raiser with factory functions for dependency injection. @@ -165,3 +163,4 @@ void initRaiserInventoryBus(EventBus bus) { // Priority: 0 bus.register(InventoryHandler()); } + diff --git a/packages/raiser_generator/example/lib/services.dart b/packages/raiser_generator/example/lib/services.dart index 0aa4c79..3ab5e06 100644 --- a/packages/raiser_generator/example/lib/services.dart +++ b/packages/raiser_generator/example/lib/services.dart @@ -1,8 +1,3 @@ -/// Example service interfaces for dependency injection examples. -/// -/// These interfaces represent typical dependencies that handlers -/// and middleware might require. - /// Simple logging service interface. abstract class Logger { void log(String message); diff --git a/packages/raiser_generator/example/pubspec.yaml b/packages/raiser_generator/example/pubspec.yaml index 28ea83b..be8f633 100644 --- a/packages/raiser_generator/example/pubspec.yaml +++ b/packages/raiser_generator/example/pubspec.yaml @@ -1,20 +1,22 @@ name: raiser_generator_example -description: Example usage of the Raiser code generator -version: 1.0.0 +description: Example usage of the Raiser package publish_to: none environment: sdk: ^3.10.4 dependencies: + + zooper_flutter_core: ^1.0.2 + raiser: path: ../../raiser raiser_annotation: path: ../../raiser_annotation dev_dependencies: + lints: ^6.0.0 build_runner: ^2.4.0 raiser_generator: - path: .. - source_gen: ^4.0.0 + path: ../ diff --git a/packages/raiser_generator/example/raiser_generator_example.dart b/packages/raiser_generator/example/raiser_generator_example.dart index f7d6a7c..4465428 100644 --- a/packages/raiser_generator/example/raiser_generator_example.dart +++ b/packages/raiser_generator/example/raiser_generator_example.dart @@ -8,7 +8,7 @@ /// cd example /// dart run build_runner build /// ``` -library raiser_generator_example; +library; import 'package:raiser/raiser.dart'; import 'package:raiser_generator_example/raiser.g.dart';