Skip to content

Commit e46ca9a

Browse files
committed
Merge branch 'release/v3.1.0' into main
2 parents 459cd21 + 45fdb82 commit e46ca9a

21 files changed

Lines changed: 981 additions & 13 deletions

packages/continuum/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ 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+
## [3.1.0] - 2026-01-13
9+
10+
### Added
11+
12+
- Added an example demonstrating code generation for `abstract class` and `abstract interface class` aggregates.
13+
14+
### Fixed
15+
16+
- Combining builder now skips non-library Dart files (e.g. `*.freezed.dart` part files) when scanning `lib/`, preventing build failures in apps using Freezed.
17+
818
## [3.0.1] - 2026-01-12
919

1020
### Changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/// Example: Abstract and Interface Aggregates
2+
///
3+
/// This example demonstrates that Continuum can generate event handlers and
4+
/// dispatch logic for aggregates declared as an `abstract class` or an
5+
/// `interface class`.
6+
library;
7+
8+
import 'package:continuum/continuum.dart';
9+
import 'package:zooper_flutter_core/zooper_flutter_core.dart';
10+
11+
part 'abstract_interface_aggregates.g.dart';
12+
13+
void main() {
14+
print('═══════════════════════════════════════════════════════════════════');
15+
print('Example: Abstract and Interface Aggregates');
16+
print('═══════════════════════════════════════════════════════════════════');
17+
print('');
18+
19+
_runAbstractAggregateExample();
20+
print('');
21+
_runInterfaceAggregateExample();
22+
}
23+
24+
void _runAbstractAggregateExample() {
25+
print('ABSTRACT AGGREGATE');
26+
27+
final user = AbstractUser(
28+
id: 'abstract-user-1',
29+
email: 'alice@example.com',
30+
name: 'Alice',
31+
);
32+
33+
print(' Initial state: email=${user.email}');
34+
35+
user.applyEvent(
36+
AbstractUserEmailChanged(
37+
newEmail: 'alice@company.com',
38+
),
39+
);
40+
41+
print(' After event: email=${user.email}');
42+
print(' ✓ Event dispatch works via AbstractUserBase');
43+
}
44+
45+
void _runInterfaceAggregateExample() {
46+
print('INTERFACE AGGREGATE');
47+
48+
final user = ContractUser(
49+
id: 'contract-user-1',
50+
displayName: 'Bob',
51+
);
52+
53+
print(' Initial state: displayName=${user.displayName}');
54+
55+
user.applyEvent(
56+
ContractUserRenamed(
57+
newDisplayName: 'Bobby',
58+
),
59+
);
60+
61+
print(' After event: displayName=${user.displayName}');
62+
print(' ✓ Event dispatch works via UserContract');
63+
}
64+
65+
/// An abstract aggregate base type.
66+
///
67+
/// The generator produces:
68+
/// - `mixin _$AbstractUserBaseEventHandlers`
69+
/// - `extension $AbstractUserBaseEventDispatch on AbstractUserBase`
70+
@Aggregate()
71+
abstract class AbstractUserBase with _$AbstractUserBaseEventHandlers {
72+
AbstractUserBase({
73+
required this.id,
74+
required this.email,
75+
required this.name,
76+
});
77+
78+
final String id;
79+
String email;
80+
final String name;
81+
}
82+
83+
/// A concrete implementation of the abstract aggregate.
84+
class AbstractUser extends AbstractUserBase {
85+
AbstractUser({
86+
required super.id,
87+
required super.email,
88+
required super.name,
89+
});
90+
91+
@override
92+
void applyAbstractUserEmailChanged(AbstractUserEmailChanged event) {
93+
email = event.newEmail;
94+
}
95+
}
96+
97+
/// An interface aggregate type.
98+
///
99+
/// The generator produces:
100+
/// - `mixin _$UserContractEventHandlers`
101+
/// - `extension $UserContractEventDispatch on UserContract`
102+
@Aggregate()
103+
abstract interface class UserContract with _$UserContractEventHandlers {
104+
String get id;
105+
String get displayName;
106+
}
107+
108+
/// A concrete implementation of the interface aggregate.
109+
class ContractUser with _$UserContractEventHandlers implements UserContract {
110+
ContractUser({
111+
required this.id,
112+
required this.displayName,
113+
});
114+
115+
@override
116+
final String id;
117+
118+
@override
119+
String displayName;
120+
121+
@override
122+
void applyContractUserRenamed(ContractUserRenamed event) {
123+
displayName = event.newDisplayName;
124+
}
125+
}
126+
127+
/// Event that changes an abstract user's email.
128+
@AggregateEvent(of: AbstractUserBase, type: 'example.abstract_user.email_changed')
129+
class AbstractUserEmailChanged implements ContinuumEvent {
130+
AbstractUserEmailChanged({
131+
required this.newEmail,
132+
EventId? eventId,
133+
DateTime? occurredOn,
134+
Map<String, Object?> metadata = const {},
135+
}) : id = eventId ?? EventId.fromUlid(),
136+
occurredOn = occurredOn ?? DateTime.now(),
137+
metadata = Map<String, Object?>.unmodifiable(metadata);
138+
139+
final String newEmail;
140+
141+
@override
142+
final EventId id;
143+
144+
@override
145+
final DateTime occurredOn;
146+
147+
@override
148+
final Map<String, Object?> metadata;
149+
150+
factory AbstractUserEmailChanged.fromJson(Map<String, dynamic> json) {
151+
return AbstractUserEmailChanged(
152+
newEmail: json['newEmail'] as String,
153+
eventId: EventId.fromJson(json['eventId'] as String),
154+
occurredOn: DateTime.parse(json['occurredOn'] as String),
155+
metadata: Map<String, Object?>.from(json['metadata'] as Map),
156+
);
157+
}
158+
159+
Map<String, dynamic> toJson() => {
160+
'newEmail': newEmail,
161+
'eventId': id.toString(),
162+
'occurredOn': occurredOn.toIso8601String(),
163+
'metadata': metadata,
164+
};
165+
}
166+
167+
/// Event that renames a user implementing an interface aggregate.
168+
@AggregateEvent(of: UserContract, type: 'example.contract_user.renamed')
169+
class ContractUserRenamed implements ContinuumEvent {
170+
ContractUserRenamed({
171+
required this.newDisplayName,
172+
EventId? eventId,
173+
DateTime? occurredOn,
174+
Map<String, Object?> metadata = const {},
175+
}) : id = eventId ?? EventId.fromUlid(),
176+
occurredOn = occurredOn ?? DateTime.now(),
177+
metadata = Map<String, Object?>.unmodifiable(metadata);
178+
179+
final String newDisplayName;
180+
181+
@override
182+
final EventId id;
183+
184+
@override
185+
final DateTime occurredOn;
186+
187+
@override
188+
final Map<String, Object?> metadata;
189+
190+
factory ContractUserRenamed.fromJson(Map<String, dynamic> json) {
191+
return ContractUserRenamed(
192+
newDisplayName: json['newDisplayName'] as String,
193+
eventId: EventId.fromJson(json['eventId'] as String),
194+
occurredOn: DateTime.parse(json['occurredOn'] as String),
195+
metadata: Map<String, Object?>.from(json['metadata'] as Map),
196+
);
197+
}
198+
199+
Map<String, dynamic> toJson() => {
200+
'newDisplayName': newDisplayName,
201+
'eventId': id.toString(),
202+
'occurredOn': occurredOn.toIso8601String(),
203+
'metadata': metadata,
204+
};
205+
}

packages/continuum/example/lib/abstract_interface_aggregates.g.dart

Lines changed: 157 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/continuum/example/lib/continuum.g.dart

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)