ddd-toolkit-macro
English | 日本語
Derive macros for ddd-toolkit-core:
value objects, identity-based entities, domain events, and aggregate event queues.
The macros generate trait implementations and accessors; they do not provide
persistence, event delivery, or synchronization.
Maintained in the ddd-toolkit monorepo. For development and releases, see the root repository's contribution guide.
Installation
Tested with the latest stable Rust (edition 2024). No minimum supported Rust version (MSRV) is specified.
Generated paths prefer a direct ddd-toolkit-core dependency, then fall back
to ddd-toolkit and its re-exports. Both dependencies may be renamed. Using
only the facade with its default derive feature is also supported.
Derives
| Derive | Supported input | Generated API |
|---|---|---|
ValueObject |
Nonempty named or tuple structs, nonempty enums | Core ValueObject, Debug, Clone, PartialEq, Eq, Hash, constructors, and accessors |
Entity |
Named struct with exactly one #[entity(id)] field |
Core Entity, inherent id(), and identity-based PartialEq, Eq, Hash |
DomainEvent |
Struct or enum | Core DomainEvent marker |
AggregateRoot |
Named struct implementing core Entity, with exactly one #[aggregate(events)] field |
Core AggregateRoot with record_event and pull_events |
Do not separately implement or derive traits already generated by a macro. Field types must satisfy the corresponding generated trait bounds.
Value objects
For a single-field struct, Inner is the field type; as_inner() borrows that
field and into_inner() returns it by value. For multi-field structs and enums,
Inner is Self, so those methods borrow or return the whole value.
Structs receive new(...) with arguments in field declaration order.
Multi-field named structs also receive borrowed accessors named after their
fields; multi-field tuple structs receive field_0(), field_1(), and so on.
use ValueObject;
;
Enums receive new_<variant>, is_<variant>, and as_<variant> methods.
The suffix is the fully lowercased variant name, not snake case: InProgress
becomes inprogress. A variant accessor returns None for another variant,
Some(()) for a unit variant, or a tuple of field references for a data variant,
even when it has only one field. Lowercased names must be distinct, and generated
method names must not collide with other methods or field accessors.
Validation
Specify validate and error together. The validator is a Rust path, not a
quoted string. It receives &Inner for a single-field struct or &Self for a
multi-field struct or enum, and returns Result<(), ErrorType>.
use ValueObject;
;
Validation replaces new with try_new (or new_<variant> with
try_new_<variant>). Constructor errors are propagated unchanged; validator
panics are not caught.
Validated struct fields cannot be unrestricted pub. This does not prevent all
bypasses: direct construction within the defining module, restricted field
visibility, and direct enum variant construction can bypass validation.
Serialization
Enable serde support on the core dependency:
Then opt in per value object:
use ValueObject;
;
#[value_object(serde)] generates Serialize and Deserialize through the core
crate's serde re-export; it is not a feature on ddd-toolkit-macro. Add serde or
a format crate directly to your dependencies if your own code uses their APIs.
The attribute can be combined with validate and error in the same
#[value_object(...)] declaration.
Single-field structs serialize as their inner value, multi-field named structs
as structs, multi-field tuple structs as tuples, and enums as externally tagged
variants. Single-field tuple variants use Serde's newtype format (Code(7) →
{"Code":7}); other tuple variants retain tuple serialization. Container fields
keep their own shape (Codes(vec![7, 8]) → {"Codes":[7,8]}).
Breaking in macro 0.5.0 / facade 0.4.0: older serializers added an extra tuple
wrapper, e.g. {"Code":[7]} or {"Codes":[[7,8]]}. Remove exactly that outer
array for affected variants in saved records before upgrading readers. Identify
old records using schema/version metadata rather than unwrapping every array.
No automatic legacy fallback is provided: container-valued fields can make the
formats ambiguous. Coordinate reader/writer upgrades or pin macro 0.4 / facade
0.3 until migration is ready. Other Serde formats also change from tuple to
newtype variants and need format-specific migration.
Deserialization runs the configured validator; validation errors become
value object validation failed rather than preserving the original error text.
Entities and aggregate events
Entity compares and hashes only the field marked #[entity(id)]; other fields
do not participate. It provides an inherent borrowed id() accessor, but no
constructor or identifier validation.
DomainEvent is a marker implementation for a struct or enum. It does not add
serialization, methods, or Send/Sync bounds.
use AggregateRoot as _;
use ;
The event queue must be spelled Vec<Event> or std::vec::Vec<Event>; aliases
and other vector paths are not accepted. Its event type must implement core
DomainEvent.
Import the core AggregateRoot trait to call record_event and pull_events:
they are trait methods, not inherent methods. Recording appends to the queue in
insertion order and may allocate. Pulling moves out the queued events and leaves
an empty vector. Both methods require mutable access; neither publishes events,
persists them, or updates domain state.
Attributes
| Attribute | Placement | Effect |
|---|---|---|
#[value_object(validate = path, error = ErrorType)] |
ValueObject struct or enum |
Generates fallible, validated constructors |
#[value_object(serde)] |
ValueObject struct or enum |
Generates serde serialization and deserialization; requires core's serde feature |
#[entity(id)] |
One named struct field | Selects the entity identifier |
#[aggregate(events)] |
One named struct field | Selects the aggregate's pending event vector |
See the API documentation for further details and executable examples.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.