Skip to content

Commit d584b2a

Browse files
authored
Merge pull request #4 from zooper-lib/feature/base-classes
Refactors RaiserEvent to be an interface
2 parents 0749f7f + 1ebe9c5 commit d584b2a

33 files changed

+992
-604
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
applyTo: '**/*.dart'
3+
---
4+
5+
# General
6+
- ALWAYS write clean, readable, maintainable, explicit code.
7+
- ALWAYS write code that is easy to refactor and reason about.
8+
- NEVER assume context or generate code that I did not explicitly request.
9+
- ALWAYS name files after the primary class or functionality they contain.
10+
11+
# Documentation
12+
- 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.
13+
14+
- ALWAYS place a `///` library-level documentation block (before imports) ONLY on:
15+
- lib/<package>.dart (the main public entrypoint)
16+
- a small number of intentionally exposed public sub-libraries
17+
- NEVER add library file-docs on internal files inside `src/`
18+
- ALWAYS keep package-surface documentation concise, stable, and user-facing
19+
- ALWAYS write Dart-doc (`///`) for:
20+
- every class
21+
- every constructor
22+
- every public and private method
23+
- every important field/property
24+
- ALWAYS add inline comments INSIDE methods explaining **why** something is done (preferred) or **what** it does if unclear.
25+
- NEVER generate README / docs / summary files unless explicitly asked.
26+
- NEVER document example usage.
27+
28+
# Code Style
29+
- ALWAYS use long, descriptive variable and method names. NEVER use abbreviations.
30+
- ALWAYS use explicit return types — NEVER rely on type inference for public API surfaces.
31+
- ALWAYS avoid hidden behavior or magic — explain reasons in comments.
32+
- NEVER use `dynamic` unless explicitly requested.
33+
- NEVER swallow exceptions — failures must be explicit and documented.
34+
35+
# Package Modularity
36+
- ALWAYS organize code by feature or concept, NOT by layers (domain/app/infrastructure/etc.).
37+
- ALWAYS keep related classes in the same folder to avoid unnecessary cross-navigation.
38+
- ALWAYS aim for package-internal cohesion: a feature should be usable independently of others.
39+
- NEVER introduce folders like `domain`, `application`, `infrastructure`, `presentation` inside a package unless explicitly asked.
40+
- ALWAYS design APIs as small, composable, orthogonal units that can be imported independently.
41+
- ALWAYS hide internal details using file-private symbols or exports from a single public interface file.
42+
- ALWAYS expose only few careful public entrypoints through `package_name.dart`.
43+
- NEVER expose cluttered API surfaces; keep users' imports short and predictable.
44+
45+
# Asynchronous / IO
46+
- ALWAYS suffix async methods with `Async`.
47+
- NEVER do IO inside constructors.
48+
- ALWAYS document async side-effects.
49+
50+
# Constants
51+
- NEVER implement magic values.
52+
- ALWAYS elevate numbers, strings, durations, etc. to named constants.
53+
54+
# Assumptions
55+
- IF details are missing, ALWAYS state assumptions **above the code** before writing it.
56+
- NEVER introduce global state unless explicitly required.
57+
58+
# API Design
59+
- ALWAYS think in terms of public API surface: every public symbol must be intentionally exposed and supported long-term.
60+
- ALWAYS hide implementation details behind internal files.
61+
- ALWAYS consider whether adding a type forces future backwards-compatibility.
62+
- ALWAYS design for testability (stateless helpers, pure functions, injectable dependencies).
63+
64+
# Folder Hygiene
65+
- NEVER create folders "just in case."
66+
- ALWAYS delete dead code aggressively.
67+
- ALWAYS keep `src/` readable even after 2 years of growth.
68+
69+
# Code Hygiene
70+
- NEVER implement barrel export files.
71+
- ALWAYS write code that compiles with ZERO warnings, errors, or analyzer hints.
72+
- ALWAYS remove unused imports, unused variables, unused private fields, and unreachable code.
73+
- ALWAYS prefer explicit typing to avoid inference warnings.
74+
- ALWAYS mark classes, methods, or variables as `@visibleForTesting` or private when they are not part of the public API.
75+
- NEVER ignore analyzer warnings with `// ignore:` unless explicitly asked.
76+
- ALWAYS keep lint and style problems in VSCode Problems panel at ZERO, unless unavoidable and explicitly justified in comments.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
applyTo: '**/*.dart'
3+
---
4+
5+
# File Ordering (top → bottom)
6+
- library documentation (ONLY when allowed)
7+
- imports (dart: → package: → relative), alphabetical
8+
- exports, alphabetical
9+
- top-level constants
10+
- top-level typedefs, aliases
11+
- top-level public enums
12+
- top-level public classes / mixins / extensions
13+
- top-level private enums
14+
- top-level private classes / mixins / extensions (ALWAYS LAST)
15+
16+
# Class Member Ordering
17+
1. static fields (public → private)
18+
2. instance fields (public → private)
19+
3. constructors (public → named → private)
20+
4. factory constructors
21+
5. public getters
22+
6. public setters
23+
7. public methods
24+
8. operator overloads
25+
9. protected methods
26+
10. private getters / setters
27+
11. private methods
28+
12. static methods (public → private)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
applyTo: '**/*.dart'
3+
---
4+
5+
# Tests
6+
- ALWAYS write tests for EVERY publicly accessible class, function, and method.
7+
- ALWAYS write tests with the primary goal of exposing possible bugs — NOT simply making tests pass.
8+
- ALWAYS test failure cases, invalid input, unexpected state, and edge conditions.
9+
- ALWAYS create exactly one unit test file per class being tested.
10+
- ALWAYS name the test file `<class_name>_test.dart` or `<feature_name>_test.dart`.
11+
12+
# Unit Tests
13+
- ALWAYS use Arrange–Act–Assert pattern with clear separation.
14+
- ALWAYS write descriptive test names that explain expected behavior.
15+
- ALWAYS add inline comments inside tests explaining WHY assertions matter.
16+
- ALWAYS include tests for:
17+
- Happy path behavior
18+
- Error cases and thrown exceptions
19+
- Boundary conditions
20+
- Null / empty values where applicable
21+
- Timing and concurrency behavior if async
22+
- NEVER skip tests for private methods if they contain complex logic.
23+
(If a private method is trivial, call it indirectly through public API instead.)
24+
- WHEN a class depends on collaborators, ALWAYS use fakes or stubs — NEVER use real infrastructure in unit tests.
25+
26+
# Integration Tests (only when applicable)
27+
- ALWAYS write integration tests to verify whole workflows that span multiple public classes.
28+
- ALWAYS cover multi-step flows, IO boundaries, and dependency wiring.
29+
- NEVER write integration tests when a unit test is sufficient.
30+
- ALWAYS isolate integration tests into `test/integration/` and name according to workflow.
31+
32+
# Test Hygiene
33+
- NEVER use random sleeps or timing hacks — use proper async waiting or dependency injection.
34+
- NEVER rely on global order of test execution.
35+
- ALWAYS ensure tests remain readable after years — avoid clever tricks or meta test logic.
36+
37+
# Mocks
38+
- ALWAYS use Mockito for mocking dependencies.
39+
- ALWAYS mock collaborators instead of creating real implementations in unit tests.
40+
- ALWAYS generate mock classes via `build_runner` when needed.
41+
- NEVER use real data sources, HTTP calls, or platform channels in unit tests.
42+
- ALWAYS verify interactions on mocks when behavior depends on method-call side effects.
43+
- ALWAYS keep mock usage minimal and focused — tests should assert behavior, not implementation details.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
applyTo: '**/*.dart'
3+
---
4+
5+
# Clean Architecture for Flutter apps
6+
- ALWAYS separate responsibilities:
7+
- domain/: entities, value objects, business rules
8+
- application/: services, use-cases, orchestrators
9+
- infrastructure/: concrete implementations, IO, APIs
10+
- presentation/: Flutter widgets, controllers, adapters
11+
- NEVER mix domain logic inside UI or infrastructure.
12+
- NEVER inject `WidgetRef` or `Ref` into domain/application classes — ONLY resolve dependencies at provider boundaries.
13+
14+
# Flutter Widgets
15+
- ALWAYS explain the purpose of a widget in Dart-doc.
16+
- ALWAYS extract callbacks into named functions when possible.
17+
- NEVER override themes or text styles unless explicitly requested.

packages/raiser/CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
### Changed
11+
12+
- **BREAKING**: `RaiserEvent` is now a pure `abstract interface class` instead of a base class with implementation
13+
- **Note: Using `RaiserEvent` is completely optional - `EventBus` works with ANY type**
14+
- If you choose to use `RaiserEvent`, you must explicitly implement all three required properties: `id`, `occurredOn`, and `metadata`
15+
- No more automatic property initialization - implement the interface explicitly
16+
- This change allows events to use composition and multiple interfaces without inheritance constraints
17+
- `RaiserEvent` now implements `ZooperDomainEvent` from `zooper_flutter_core` package
18+
- Added `zooper_flutter_core: ^1.0.2` as an optional dependency (only needed if you use `RaiserEvent`)
19+
- Ensures consistency across domain events in the Zooper ecosystem
20+
- Event IDs in `RaiserEvent` now use `EventId` type (ULID-based) instead of `String`
21+
- IDs are generated via `EventId.fromUlid()` for better uniqueness guarantees
22+
- Access raw string value via `event.id.value` when needed
23+
- Renamed `RaiserEvent.timestamp` property to `occurredOn` for clarity and DDD alignment
24+
- Removed `aggregateId` as a direct property from `RaiserEvent`
25+
- Store aggregate identifiers in the `metadata` map instead: `metadata: {'aggregateId': 'user-123'}`
26+
- Access via `event.metadata['aggregateId'] as String?`
27+
- Removed all helper mixins and convenience extensions
28+
- No more `StandardRaiserEvent` mixin
29+
- No more extension methods for `aggregateId` or `timestamp` accessors
30+
- Events implementing `RaiserEvent` must be implemented explicitly following the interface pattern
31+
32+
### Important Note
33+
34+
**`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.
35+
36+
837
## [2.0.1] - 2026-01-08
938

1039
### Changed

packages/raiser/README.md

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,25 @@ A type-safe, async-first domain event library for Dart. Raiser provides a clean
2121

2222
```yaml
2323
dependencies:
24-
raiser: ^1.0.0
24+
raiser: ^2.0.1
2525
```
2626
2727
## Quick Start
2828
29+
**EventBus works with ANY type** - you don't need to use `RaiserEvent` at all:
30+
2931
```dart
3032
import 'package:raiser/raiser.dart';
3133
32-
// Define an event
33-
class UserCreated extends RaiserEvent {
34+
// Define an event - just a simple class!
35+
final class UserCreated {
36+
UserCreated({
37+
required this.userId,
38+
required this.email,
39+
});
40+
3441
final String userId;
3542
final String email;
36-
37-
UserCreated({required this.userId, required this.email});
38-
39-
@override
40-
Map<String, dynamic> toMetadataMap() => {
41-
...super.toMetadataMap(),
42-
'userId': userId,
43-
'email': email,
44-
};
4543
}
4644
4745
void main() async {
@@ -57,32 +55,63 @@ void main() async {
5755
}
5856
```
5957

60-
## Domain Events
58+
## RaiserEvent (Optional)
59+
60+
**You don't have to use `RaiserEvent`!** The `EventBus` is fully generic and works with any type.
61+
62+
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:
6163

62-
All events extend `RaiserEvent`, which provides automatic metadata:
64+
| Property | Type | Description |
65+
|----------|------|-------------|
66+
| `id` | `EventId` | Unique identifier (ULID-based) |
67+
| `occurredOn` | `DateTime` | When the event occurred |
68+
| `metadata` | `Map<String, Object?>` | Additional event context |
6369

64-
| Property | Description |
65-
|----------|-------------|
66-
| `id` | Unique identifier (auto-generated) |
67-
| `timestamp` | Creation time (auto-captured) |
68-
| `aggregateId` | Optional link to a domain aggregate |
70+
`RaiserEvent` is intentionally an **interface** (not a base class) to avoid forcing single inheritance. If you choose to use it, implement it explicitly:
6971

7072
```dart
71-
class OrderPlaced extends RaiserEvent {
73+
import 'package:zooper_flutter_core/zooper_flutter_core.dart';
74+
75+
final class OrderPlaced implements RaiserEvent {
76+
OrderPlaced({
77+
required this.orderId,
78+
required this.amount,
79+
EventId? eventId,
80+
DateTime? occurredOn,
81+
Map<String, Object?> metadata = const {},
82+
}) : id = eventId ?? EventId.fromUlid(),
83+
occurredOn = occurredOn ?? DateTime.now(),
84+
metadata = Map<String, Object?>.unmodifiable(metadata);
85+
7286
final String orderId;
7387
final double amount;
7488
75-
OrderPlaced({required this.orderId, required this.amount, super.aggregateId});
89+
@override
90+
final EventId id;
7691
7792
@override
78-
Map<String, dynamic> toMetadataMap() => {
79-
...super.toMetadataMap(),
80-
'orderId': orderId,
81-
'amount': amount,
82-
};
93+
final DateTime occurredOn;
94+
95+
@override
96+
final Map<String, Object?> metadata;
8397
}
8498
```
8599

100+
### Aggregate IDs
101+
102+
Store aggregate identifiers in the `metadata` map:
103+
104+
```dart
105+
final event = OrderPlaced(
106+
orderId: 'order-123',
107+
amount: 99.99,
108+
metadata: {'aggregateId': 'user-456'},
109+
);
110+
111+
// Access via metadata
112+
final aggregateId = event.metadata['aggregateId'] as String?;
113+
```
114+
86115
## Event Handlers
87116

88117
### Function Handlers
@@ -126,14 +155,14 @@ Wrap handler execution with cross-cutting concerns like logging, timing, or vali
126155

127156
```dart
128157
// Add middleware that wraps all handler execution
129-
bus.addMiddleware((event, next) async {
158+
bus.addMiddleware((Object event, Future<void> Function() next) async {
130159
print('Before: ${event.runtimeType}');
131160
await next();
132161
print('After: ${event.runtimeType}');
133162
}, priority: 100);
134163
135164
// Middleware with higher priority wraps those with lower priority
136-
bus.addMiddleware((event, next) async {
165+
bus.addMiddleware((Object event, Future<void> Function() next) async {
137166
final stopwatch = Stopwatch()..start();
138167
await next();
139168
print('Took ${stopwatch.elapsedMilliseconds}ms');
@@ -198,12 +227,12 @@ For automatic handler discovery and registration, use the companion packages:
198227

199228
```yaml
200229
dependencies:
201-
raiser: ^1.0.0
202-
raiser_annotation: ^1.0.0
230+
raiser: ^2.0.1
231+
raiser_annotation: ^2.0.1
203232
204233
dev_dependencies:
205234
build_runner: ^2.4.0
206-
raiser_generator: ^1.0.0
235+
raiser_generator: ^2.0.1
207236
```
208237

209238
See [raiser_generator](https://pub.dev/packages/raiser_generator) for details.

0 commit comments

Comments
 (0)