puerto 0.19.1

Puerto — Rust full-stack DDD framework CLI. Scaffolds Clean Architecture projects with auto-wired DI, SQLx support, and zero manual wiring. Formerly puerto-framework.
// AUTO-GENERATED by puerto from puerto.toml — never hand-edit. The snapshot follows the entity's
// fields; add an event with `puerto generate event {{ pascal }} <name>`.
use puerto_core::DomainEvent;
use serde::{Deserialize, Serialize};
use uuid::Uuid;{% for import in imports %}
use {{ import }};{% endfor %}

use super::model::{{ pascal }};
{% for e in events %}
pub const {{ e.constant }}: &str = "{{ e.event_type }}";{% endfor %}

/// What every {{ snake }} event carries: the {{ snake }} as the API shows it — primitives, value
/// objects unwrapped. A public contract: consumers decode it with
/// `envelope.decode::<{{ pascal }}Snapshot>()`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct {{ pascal }}Snapshot {
    pub id: Uuid,
{% for f in fields %}    pub {{ f.name }}: {{ f.field_type }},
{% endfor %}}

impl {{ pascal }}Snapshot {
    pub fn from_entity(entity: &{{ pascal }}) -> Self {
        Self {
            id: entity.id.value(),
{% for f in fields %}            {{ f.name }}: {{ f.accessor }},
{% endfor %}        }
    }
}

/// What happened to a {{ snake }}. Its write use cases record one through their unit of work.
#[derive(Debug, Clone, PartialEq)]
pub enum {{ pascal }}Event {
{% for e in events %}    {{ e.variant }}({{ pascal }}Snapshot),
{% endfor %}}

impl {{ pascal }}Event {
    pub fn snapshot(&self) -> &{{ pascal }}Snapshot {
        match self {
            {% for e in events %}Self::{{ e.variant }}(snapshot){% if not loop.last %} | {% endif %}{% endfor %} => snapshot,
        }
    }
}

impl DomainEvent for {{ pascal }}Event {
    fn event_type(&self) -> &'static str {
        match self {
{% for e in events %}            Self::{{ e.variant }}(_) => {{ e.constant }},
{% endfor %}        }
    }

    fn aggregate_type(&self) -> &'static str {
        "{{ snake }}"
    }

    fn aggregate_id(&self) -> Uuid {
        self.snapshot().id
    }

    fn schema_version(&self) -> u16 {
        1
    }

    fn payload(&self) -> serde_json::Value {
        // A snapshot holds primitives only: serialising it cannot fail.
        serde_json::to_value(self.snapshot()).unwrap_or_default()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tests::mothers::{{ snake }}_mother::{{ pascal }}Mother;

    #[test]
    fn should_describe_every_event_when_one_is_recorded() {
        // Arrange
        let entity = {{ pascal }}Mother::random();
        let snapshot = {{ pascal }}Snapshot::from_entity(&entity);
        let events = [
{% for e in events %}            ({{ pascal }}Event::{{ e.variant }}(snapshot.clone()), "{{ e.event_type }}"),
{% endfor %}        ];

        for (event, event_type) in events {
            // Act + Assert
            assert_eq!(event.event_type(), event_type);
            assert_eq!(event.aggregate_type(), "{{ snake }}");
            assert_eq!(event.aggregate_id(), entity.id.value());
            assert_eq!(event.schema_version(), 1);
            assert_eq!(event.snapshot(), &snapshot);
        }
    }

    #[test]
    fn should_keep_the_json_contract_when_the_snapshot_is_serialised() {
        // Arrange
        let snapshot = {{ pascal }}Snapshot::from_entity(&{{ pascal }}Mother::random());

        // Act
        let payload = {{ pascal }}Event::{{ events.0.variant }}(snapshot.clone()).payload();

        // Assert — a renamed or removed key breaks every consumer of these events.
        let mut keys: Vec<&str> = payload
            .as_object()
            .map(|object| object.keys().map(String::as_str).collect())
            .unwrap_or_default();
        keys.sort_unstable();
        assert_eq!(keys, [{% for key in json_keys %}"{{ key }}"{% if not loop.last %}, {% endif %}{% endfor %}]);
        assert_eq!(
            serde_json::from_value::<{{ pascal }}Snapshot>(payload).ok(),
            Some(snapshot)
        );
    }
}