ddd-toolkit-macro 0.4.0

Derive macros for ddd-toolkit-core's ValueObject, Entity, DomainEvent, and AggregateRoot traits.
Documentation

ddd-toolkit-macro

docs.rs CI codecov license

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.

Installation

cargo add ddd-toolkit-core ddd-toolkit-macro

Your crate must depend directly on ddd-toolkit-core: generated code references its traits. Renamed core dependencies are supported, but depending only on the ddd-toolkit facade is not sufficient for crate-path resolution.

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 ddd_toolkit_macro::ValueObject;

#[derive(ValueObject)]
struct Quantity(u32);

#[derive(ValueObject)]
struct Address {
    city: String,
    postal_code: String,
}

#[derive(ValueObject)]
enum Status {
    Pending,
    Ready(u32),
}

fn main() {
    let quantity = Quantity::new(3);
    assert_eq!(quantity.as_inner(), &3);
    assert_eq!(quantity.into_inner(), 3);

    let address = Address::new("Tokyo".into(), "100-0001".into());
    assert_eq!(address.city(), "Tokyo");
    assert_eq!(address.as_inner(), &address);

    let status = Status::new_ready(7);
    assert!(status.is_ready());
    assert_eq!(status.as_ready(), Some((&7,)));
    assert_eq!(status.as_pending(), None);
    assert_eq!(Status::new_pending().as_pending(), Some(()));
}

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 ddd_toolkit_macro::ValueObject;

#[derive(ValueObject)]
#[value_object(validate = validate_email, error = &'static str)]
struct Email(String);

fn validate_email(value: &String) -> Result<(), &'static str> {
    if value.contains('@') {
        Ok(())
    } else {
        Err("missing @")
    }
}

fn main() {
    let email = Email::try_new("a@b.com".into()).unwrap();
    assert_eq!(email.as_inner(), "a@b.com");
    assert_eq!(Email::try_new("invalid".into()), Err("missing @"));
}

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:

cargo add ddd-toolkit-core --features serde

Then opt in per value object:

use ddd_toolkit_macro::ValueObject;

#[derive(ValueObject)]
#[value_object(serde)]
struct Quantity(u32);

fn main() {
    assert_eq!(Quantity::new(3).into_inner(), 3);
}

#[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. Tuple variants retain tuple serialization even with one field. 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 ddd_toolkit_core::AggregateRoot as _;
use ddd_toolkit_macro::{AggregateRoot, DomainEvent, Entity};

#[derive(DomainEvent, Debug, PartialEq)]
enum OrderEvent {
    Placed { order_id: u64 },
}

#[derive(Entity, AggregateRoot)]
struct Order {
    #[entity(id)]
    id: u64,
    total: u32,
    #[aggregate(events)]
    events: Vec<OrderEvent>,
}

fn main() {
    let mut order = Order { id: 1, total: 10, events: Vec::new() };
    let updated = Order { id: 1, total: 20, events: Vec::new() };
    assert_ne!(order.total, updated.total);
    assert!(order == updated);
    assert_eq!(order.id(), &1);

    order.record_event(OrderEvent::Placed { order_id: 1 });
    assert_eq!(order.pull_events(), vec![OrderEvent::Placed { order_id: 1 }]);
    assert!(order.pull_events().is_empty());
}

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.