daxle_gen 0.3.0
daxle_gen: ^0.3.0 copied to clipboard
AST code generator and compile-time validation hook for Daxle.
Daxle Code Generator (daxle_gen) #
Compile-time functional code generator and AST inspection pipeline for Dart 3+.
daxle_gen provides pure, deterministic, and type-safe code generation targeting modern Dart 3 features: primary constructors, switch pattern matching, record types, extension types, sealed class hierarchies, sensitive field redaction, and preview state machines. It generates top-level functional serialization (toMap, fromMap, toList, fromList), deep proxied immutable update lenses (copyWith, copyWithNull), tiered structural value equality (operator ==, hashCode), clean string formatting (toString), sensitive field masking (toDebugMap), and state machine workflow mixins (_$ClassNameMachine).
Table of Contents #
- Features
- Installation & Setup
- Execution Workflows
- Core Capabilities
- 1. Primary Constructors & Data Classes
- 2. Dart 3 Record Types
- 3. Deep & Proxied
copyWith - 4. Structural Equality & HashCode
- 5. Stringify
- 6. Composite Annotations (
AnnotationBundle) - 7. Member & Field Annotations
- 8. Sealed Class Polymorphism
- 9. Extension Types
- 10. Part-File Support
- 11. State Machine Workflows (Preview & Experimental)
- 12. Sensitive Field Redaction (
@redact)
- Error Diagnostics & Fail-Fast Guarantees
- Architecture & Design
- License
Features #
- Primary Constructor First: Full syntax support for Dart 3 concise parameter declarations (
class User(final String id, final String name);). - Functional Serialization: Pure, standalone functions (
<name>FromMap,<name>ToMap) and zero-cost extension methods. - Dart 3 Records: Comprehensive serialization and deserialization for positional
(int, String)and named({String name, int age})records with standalone helper functions and extensions. - Deep Proxied
copyWith: Fluent nested updates (user.copyWith.address.city(name: 'NYC')) returning the root type, identity checks avoiding redundant allocations, and explicit nullification (copyWithNull). - Collection-Aware Deep Equality: Tiered structural equality sorting cheap primitives first, nested objects second, and deep collections (
$listEquals,$setEquals,$mapEquals,$deepEquals) last. - Clean
toStringMixins: Readable string representations for classes and enums. - Sensitive Field Redaction: Automatic value masking in
toString()and synthesizedtoDebugMap()to prevent secrets and PII from leaking into logs. - Annotation Bundling: Group multiple annotations into reusable constants via
AnnotationBundle(e.g.@DataClass). - Granular Member Control: Field renaming, backwards-compatible wire aliases, default value fallback injection, nested map flattening, and field exclusion.
- Sealed Class Polymorphism: Automatic switch dispatch with default (
'type') or custom discriminators and subclass tags. - State Machine Workflows (Preview & Experimental): Synthesizes
_$ClassNameMachinemixin with strict transition graphs, async flow root stubs, default concrete sync transitions, epoch-based stale cancellation, and typed event dispatching. - High-Performance AST Tooling: Analyzes syntax trees with
DaxleFileVisitorwithout running slow builder cascades, featuring SHA-256 incremental caching and debounced watching.
Installation & Setup #
Add daxle and daxle_gen to your project:
dart pub add daxle
dart pub add dev:daxle_gen
Or declare them directly in your pubspec.yaml:
dependencies:
daxle: ^5.0.0
dev_dependencies:
daxle_gen: ^0.3.0
In any source file where generation is required, declare the generated part file:
import 'package:daxle/daxle.dart';
part 'user.daxle.dart';
@serialize
@deserialize
@copyWith
@equalsAndHashCode
@stringify
class User(
final String id,
final String name,
final Address? address,
);
Execution Workflows #
1. CLI Runner #
Run the generator directly using the Dart tool:
# Generate all .daxle.dart part files in the project
dart run daxle_gen generate
# Or use the concise package executable shortcut
dart run daxle:generate
CLI Flags & Options #
| Flag | Short | Description |
|---|---|---|
--watch |
-w |
Watches the filesystem for .dart file modifications and regenerates code incrementally with automated cache invalidation. |
--filter |
-f |
Evaluates glob patterns to include or exclude files (e.g., -f "lib/models/**" or -f "!lib/generated/**"). |
--clean |
-c |
Removes all generated .daxle.dart files registered in .dart_tool/daxle_gen/manifest.json. |
--force |
Bypasses the SHA-256 fingerprint cache and forces regeneration of every file. | |
--check |
Validates whether generated files on disk are up-to-date without modifying them (returns exit code 1 on drift; recommended for CI). |
|
--verbose |
-v |
Emits granular processing times and cache hit/miss statistics. |
--help |
-h |
Prints available command options and usage guidance. |
# Watch with glob filters
dart run daxle_gen generate lib/ -w -f "lib/domain/**"
# Verify in CI
dart run daxle_gen generate --check
# Force clean build
dart run daxle_gen generate --force
2. Native Assets Build Hook #
daxle_gen can execute directly inside Dart's compilation lifecycle via hook/build.dart. When configured, code is generated automatically before dart run, dart test, or flutter build:
// hook/build.dart
import 'package:daxle_gen/daxle_gen.dart';
import 'package:hooks/hooks.dart';
void main(List<String> args) async {
await buildHook(args, (input, output) async {
final generator = DaxleGenerator();
await generator.run(targetPath: 'lib');
});
}
Core Capabilities #
1. Primary Constructors & Data Classes #
Annotate classes with @Serialize() and @Deserialize() (or shorthand @serialize / @deserialize):
part 'product.daxle.dart';
@serialize
@deserialize
class Product(
final String id,
final String title,
final double price,
final bool inStock,
);
Generated Top-Level Functions & Extensions:
// product.daxle.dart
part of 'product.dart';
Product productFromJson(Map<String, dynamic> json) => productFromMap(json);
Product productFromMap(Map<String, dynamic> json) {
return switch (json) {
{
'id': final String id,
'title': final String title,
'price': final num price,
'inStock': final bool inStock,
} => Product(id, title, price.toDouble(), inStock),
_ => () {
// Precise diagnostic checks for each field and runtime type...
}(),
};
}
Map<String, dynamic> productToMap(Product instance, {bool excludeNull = false}) {
final map = <String, dynamic>{
'id': instance.id,
'title': instance.title,
'price': instance.price,
'inStock': instance.inStock,
};
if (excludeNull) {
map.removeWhere((_, value) => value == null);
}
return map;
}
extension ProductToMapExtension on Product {
Map<String, dynamic> toMap({bool excludeNull = false}) =>
productToMap(this, excludeNull: excludeNull);
}
2. Dart 3 Record Types #
daxle_gen fully supports modern Dart record types, both as fields inside models and as standalone top-level typedefs.
Positional Records:
Positional records serialize as JSON arrays [a, b] and deserialize into type-safe tuples:
@serialize
@deserialize
typedef Coordinates = (double latitude, double longitude);
Generated API:
List<dynamic> coordinatesToList(Coordinates instance)Coordinates coordinatesFromList(List<dynamic> list)- Extension
instance.toList()onCoordinates - Extension
list.toCoordinates()onList<dynamic>
Named Records:
Named records serialize as JSON key-value objects {'x': a, 'y': b}:
@serialize
@deserialize
typedef GridPosition = ({int x, int y, String? label});
Generated API:
Map<String, dynamic> gridPositionToMap(GridPosition instance)GridPosition gridPositionFromMap(Map<String, dynamic> map)- Extension
instance.toMap()onGridPosition - Extension
map.toGridPosition()onMap<String, dynamic>
3. Deep & Proxied copyWith #
Annotate any class with @CopyWith() or @copyWith to generate immutable mutation lenses.
When nested models are annotated with @copyWith, daxle_gen synthesizes typed proxy getters that allow chaining mutation calls directly down the object tree while returning a newly constructed root instance:
@copyWith
class Address(final String street, final String city);
@copyWith
class Company(final String name, final Address address);
@copyWith
class User(final String id, final Company company);
Fluent Deep Updates:
final user = User(
'u_1',
Company('Acme Corp', Address('100 Main St', 'Old Town')),
);
// Deep nested modification returning the root User:
final updatedUser = user.copyWith.company.address(city: 'New Town');
print(updatedUser.company.address.city); // New Town
print(updatedUser.company.name); // Acme Corp (preserved)
Additional CopyWith Features:
- Instance Reuse (Identity Checks): If arguments passed to
copyWithevaluate to identical or equal values, the existing instance is reused without allocating new objects. - Explicit Nullification (
copyWithNull):// Sets nullable fields to null: final cleared = user.copyWithNull(company: true); - Field Exclusion: Exclude specific fields from copy methods via
@CopyWith(ignoreFields: ['id']).
4. Structural Equality & HashCode #
Annotate classes with @EqualsAndHashCode() or @equalsAndHashCode to generate deep, collection-aware operator == and hashCode implementations.
@equalsAndHashCode
class Order(
final String id,
final List<String> itemIds,
final Map<String, double> tags,
) with _$OrderEqualsAndHashCode;
Performance Tier Sorting:
Fields are evaluated using a 3-tier precedence strategy to short-circuit comparisons as early as possible:
- Tier 0 (Cheap Primitives & Enums):
bool,int,double,String, and enum comparisons run first. - Tier 1 (Non-Collection Objects):
DateTime,Uri,Duration,BigInt, and custom model checks run second. - Tier 2 (Deep Collections): Nested
List,Set,Map, andQueryMapstructures run last via optimized collection equivalence helpers ($listEquals,$setEquals,$mapEquals,$deepEquals).
When a class defines more than 20 fields, daxle_gen automatically splits hashing across nested Object.hash invocations to adhere to SDK limits.
5. Stringify #
Annotate classes or enums with @Stringify() or @stringify to generate clean, readable toString() implementations:
@stringify
class Person(final String name, final int age) with _$PersonStringify;
@stringify
enum Status { pending, active, completed }
print(Person('Alice', 30).toString()); // Person(name: Alice, age: 30)
print(Status.active.toString()); // Status.active
Consolidated Mixin: When applying both
@equalsAndHashCodeand@stringify, mix in_$ClassNameto inherit both implementations together:class User(final String name) with _$User;
6. Composite Annotations (AnnotationBundle) #
Define reusable annotation bundles using AnnotationBundle to keep domain definitions concise and standard across your codebase:
import 'package:daxle/daxle.dart';
const DataClass = AnnotationBundle([
Serialize(),
Deserialize(),
CopyWith(),
EqualsAndHashCode(),
Stringify(),
]);
@DataClass
class Customer(
final String id,
final String email,
) with _$Customer;
daxle_gen expands AnnotationBundle constants at compile time while preserving any individual parameter configurations.
7. Member & Field Annotations #
Fine-tune serialization and model behavior using member-level annotations:
| Annotation | Placement | Description |
|---|---|---|
@SerializedValue('wire_key', {aliases, converter}) |
Field / Enum | Overrides JSON key name, defines fallback wire aliases, or binds a custom DaxleJsonConverter. |
@Fallback(value) |
Field / Enum | Injects a default value when incoming JSON is null/missing, or designates the fallback case for unrecognized enum values. |
@Flatten({prefix}) |
Field | Inlines nested object fields directly into the parent JSON map, with an optional key prefix. |
@Redact({mask, preserveLength}) / @redact |
Field | Masks sensitive values (passwords, tokens, PII) in toString() and synthesizes toDebugMap(). |
@Ignore() / @ignore |
Field / Enum | Completely strips the member from serialization, deserialization, equality, hashCode, copyWith, and stringify. |
Comprehensive Member Example:
@Serialize(caseStyle: CaseStyle.snakeCase)
@Deserialize(caseStyle: CaseStyle.snakeCase)
class Account(
// Custom wire key and backwards-compatible aliases
@SerializedValue('acc_id', aliases: ['id', 'account_number'])
final String accountId,
// Fallback default value if null or missing
@Fallback('standard')
final String plan,
// Inlined fields: address.street -> street, address.city -> city
@flatten
final Address address,
// Excluded completely from all generated code
@ignore
final String internalCache,
);
Enums with CaseStyle and Fallbacks:
@Serialize(caseStyle: CaseStyle.kebabCase)
@Deserialize(caseStyle: CaseStyle.kebabCase)
@Fallback(AccountStatus.unknown)
enum AccountStatus {
active,
suspended,
@SerializedValue('pending-verification')
pending,
unknown,
}
8. Sealed Class Polymorphism #
daxle_gen generates exhaustive switch expressions for polymorphic sealed class hierarchies.
Default Discriminator:
By default, @serialize and @deserialize use 'type' as the discriminator key and class names as tags:
@serialize
@deserialize
sealed class Event {}
class LoginEvent(final String userId) extends Event;
class LogoutEvent() extends Event;
Custom Discriminators & Subclass Tags:
Specify custom discriminator keys and explicit subclass tag identifiers:
@Serialize(discriminator: 'event_type')
@Deserialize(discriminator: 'event_type')
sealed class NetworkEvent {}
@SerializedValue('auth.login')
class AuthLoginEvent(final String token) implements NetworkEvent;
@SerializedValue('auth.logout')
class AuthLogoutEvent() implements NetworkEvent;
Generated Sealed Switch (event.daxle.dart):
NetworkEvent networkEventFromMap(Map<String, dynamic> json) {
return switch (json) {
{'event_type': 'auth.login'} => authLoginEventFromMap(json),
{'event_type': 'auth.logout'} => authLogoutEventFromMap(json),
_ => () {
if (!json.containsKey('event_type')) {
throw FormatException("Missing required discriminator 'event_type' for NetworkEvent", json);
}
throw FormatException("Unknown NetworkEvent discriminator: '${json['event_type']}'", json);
}(),
};
}
9. Extension Types #
daxle_gen seamlessly generates serialization helpers for modern Dart 3 extension type declarations:
@serialize
@deserialize
extension type UserId(String value);
Generated helpers allow transparent conversion between the underlying representation and the typed abstraction:
UserId userIdFromMap(dynamic value)String userIdToMap(UserId instance)
10. Part-File Support #
Daxle automatically handles complex project structures where annotated models are defined inside part files:
bloc/
├── auth_bloc.dart → part 'auth_state.dart'; part 'auth_bloc.daxle.dart';
├── auth_state.dart → part of 'auth_bloc.dart';
When auth_state.dart is annotated, daxle_gen traverses up to the owning library root (auth_bloc.dart) and correctly generates auth_bloc.daxle.dart attached to the primary library.
11. State Machine Workflows (Preview & Experimental) #
Warning
Preview & Experimental: State Machine generation (@StateMachine) is currently in preview and under active experimental development. Generated mixin signatures and API conventions may change based on developer feedback.
Annotate classes or services with @StateMachine([Flow(...)]) to synthesize a typed state machine mixin named _$ClassNameMachine.
import 'dart:async';
import 'package:daxle/daxle.dart';
part 'startup_service.daxle.dart';
sealed class StartupState {
const StartupState();
}
final class StartupIdle extends StartupState {
const StartupIdle();
}
final class StartupBooting extends StartupState {
const StartupBooting();
}
final class StartupProgress extends StartupState {
final String step;
const StartupProgress(this.step);
}
final class StartupReady extends StartupState {
const StartupReady();
}
final class StartupFailure extends StartupState {
final String error;
const StartupFailure(this.error);
}
sealed class StartupEvent {
const StartupEvent();
}
final class StartEngine extends StartupEvent {
final String environment;
const StartEngine(this.environment);
}
final class ResetRequested extends StartupEvent {
const ResetRequested();
}
@StateMachine([
Flow(from: StartupIdle, to: StartupBooting, using: StartEngine),
Flow(from: StartupBooting, to: StartupProgress),
Flow(from: StartupBooting, to: StartupReady),
Flow(from: StartupBooting, to: StartupFailure),
Flow(from: StartupProgress, to: StartupProgress),
Flow(from: StartupProgress, to: StartupReady),
Flow(from: StartupProgress, to: StartupFailure),
Flow(from: StartupFailure, to: StartupIdle, using: ResetRequested),
])
final class StartupService with _$StartupServiceMachine {
@override
StartupState activeState = const StartupIdle();
// Async flow handlers generated as FutureOr<void> stubs for async flow roots:
@override
FutureOr<void> onBooting(TransitionScope<StartupState> scope, StartEngine event) async {
scope.transit(const StartupProgress('loading configuration'));
try {
await initializeEngine(event.environment);
scope.transit(const StartupReady());
} catch (e) {
scope.transit(StartupFailure(e.toString()));
}
}
// Trivial/sync transitions (e.g. StartupFailure -> StartupIdle via ResetRequested)
// receive concrete default implementations in the mixin and can be optionally overridden.
}
What daxle_gen Synthesizes:
-
Static Transition Table (
_$transitions): Enforces compile-time transition verification via an immutable lookup table:static const Map<Type, Set<Type>> _$transitions = { StartupIdle: {StartupBooting}, StartupBooting: {StartupProgress, StartupReady, StartupFailure}, StartupProgress: {StartupProgress, StartupReady, StartupFailure}, StartupFailure: {StartupIdle}, }; -
Flow Handlers (
on<State>): GeneratesFutureOr<void> on<State>(TransitionScope<State> scope, Event event)handler stubs. States that are async flow roots (have outgoing edges without triggers) generate handler stubs for you to implement. Simple terminal or single-edge sync transitions generate default concrete methods. -
Epoch-Based Stale Flow Cancellation (
_daxleEpoch): Maintains a monotonically increasing epoch counter. Wheneverdispatchprocesses an event,_daxleEpochincrements. Asynchronous operations insideon<State>checkscope.isCurrentto guarantee that stale operations cannot overwrite subsequent state. -
Type-Safe Dispatch (
dispatch): Exhaustive pattern matching dispatches events based on the active state:void dispatch(StartupEvent event) { final current = activeState; switch (current) { case StartupIdle(): if (event is StartEngine) { _transitInternal(const StartupBooting(), event); return; } case StartupFailure(): if (event is ResetRequested) { _transitInternal(const StartupIdle(), event); return; } default: break; } throw InvalidFlowException( from: current.runtimeType, attempted: event.runtimeType, allowed: _$transitions[current.runtimeType] ?? const {}, ); } -
Flexible Generic Typing: Supports explicit generics (
@StateMachine<MyState, MyEvent>([Flow(...)])) as well as auto-inference from declaredFlowarguments or class supertypes (e.g.,Bloc<Event, State>).
12. Sensitive Field Redaction (@redact) #
Annotate fields containing secrets, authentication tokens, or personally identifiable information (PII) with @redact or @Redact():
@serialize
@stringify
class UserSession(
final String userId,
@redact
final String authToken,
@Redact(mask: '***', preserveLength: true)
final String secretPin,
) with _$UserSession;
Generated Behavior:
- Clean
toString(): Redacted values are masked ([REDACTED]or custom mask) in string output, protecting production logs and debug consoles:print(session); // UserSession(userId: u_1, authToken: [REDACTED], secretPin: ****) - Diagnostic Debug Map (
toDebugMap()): Generates a dedicated debugging serialization method where sensitive fields are masked while preserving non-sensitive data:final debugJson = session.toDebugMap(); // {'userId': 'u_1', 'authToken': '[REDACTED]', 'secretPin': '****'} - Uncompromised Wire Serialization (
toMap()): The standardtoMap()method remains completely unredacted for correct network and storage serialization:final wireJson = session.toMap(); // {'userId': 'u_1', 'authToken': 'jwt_real_token', 'secretPin': '1234'}
Error Diagnostics & Fail-Fast Guarantees #
daxle_gen prioritizes actionable, clear error diagnostics. When deserialization encounters missing required fields, incompatible types, or missing discriminators, it throws a detailed FormatException and passes the offending payload directly to FormatException.source:
FormatException: Missing required field 'price' for Product
Source: {"id": "p_1", "title": "Desk"}
FormatException: Invalid type for 'price' on Product. Expected double, got String ('free')
Source: {"id": "p_1", "title": "Desk", "price": "free"}
Architecture & Design #
daxle_gen is built using a clean, layered AST visitor pipeline:
Source .dart File
│
▼
[DaxleAstParser] ──── Uses analyzer's parseString (lightweight, no build_runner)
│
▼
[DaxleFileVisitor] ── Specialized RecursiveAstVisitor extracting declarations
│
▼
[AnnotationRegistry] ─ Modular handlers (Serialize, Deserialize, CopyWith, etc.)
│
▼
[FileGenerator] ──── Synthesizes specs via code_builder & formats via dart_style
│
▼
Generated .daxle.dart Part File
- Zero Heavy Build Step: Bypasses slow code generator frameworks (
build_runner) by reading ASTs directly withpackage:analyzer. - Content-Addressed Caching: Computes SHA-256 digests over source contents and generator version to skip unmutated files in sub-millisecond times.
- Reactive Watcher: In-memory debounced file watcher that invalidates modified compilation units instantaneously.
License #
daxle_gen is distributed under the terms of the MIT License.