cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use super::State;
use serde::ser::SerializeMap;
use serde::Serialize;
use std::fmt;

/// The action that produced a history event.
///
/// Only status transitions are tracked — all other field changes are managed
/// directly as frontmatter. The initial state of a record is carried by the
/// `Created` event itself; subsequent transitions use `StatusChanged` where
/// both `from` and `to` are always present.
///
/// The journal stores `State` (a validated name) and is universe-agnostic —
/// the issue and decision-record universes each decode the state to their
/// own typed status when they need to reason about lifecycle.
///
/// Serialisation (ADR-0016) emits a map with the variant tag under `name`
/// and the state(s) under `status` (created) or `from`/`to` (status
/// change). The deserialisation side lives in `infra::serde_support` since
/// it has to tolerate legacy on-disk shapes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventAction {
    /// The record was created with an initial state.
    Created { state: State },
    /// The state was changed.
    StatusChanged { from: State, to: State },
}

impl EventAction {
    pub fn as_str(&self) -> &str {
        match self {
            EventAction::Created { .. } => "created",
            EventAction::StatusChanged { .. } => "status_changed",
        }
    }

    pub fn is_created(&self) -> bool {
        matches!(self, EventAction::Created { .. })
    }

    pub fn is_status_changed(&self) -> bool {
        matches!(self, EventAction::StatusChanged { .. })
    }
}

impl fmt::Display for EventAction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl Serialize for EventAction {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        match self {
            EventAction::Created { state } => {
                let mut m = s.serialize_map(Some(2))?;
                m.serialize_entry("name", "created")?;
                m.serialize_entry("status", state.as_str())?;
                m.end()
            }
            EventAction::StatusChanged { from, to } => {
                let mut m = s.serialize_map(Some(3))?;
                m.serialize_entry("name", "status_changed")?;
                m.serialize_entry("from", from.as_str())?;
                m.serialize_entry("to", to.as_str())?;
                m.end()
            }
        }
    }
}

#[cfg(test)]
pub mod strategy {
    use super::super::state::strategy::state;
    use super::EventAction;
    use proptest::prelude::*;

    /// Generate an arbitrary `EventAction` covering both variants.
    pub fn event_action() -> impl Strategy<Value = EventAction> {
        prop_oneof![
            state().prop_map(|s| EventAction::Created { state: s }),
            (state(), state()).prop_map(|(from, to)| EventAction::StatusChanged { from, to }),
        ]
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn as_str_is_one_of_known_labels(a in strategy::event_action()) {
            prop_assert!(matches!(a.as_str(), "created" | "status_changed"));
        }

        #[test]
        fn classifier_methods_partition_variants(a in strategy::event_action()) {
            prop_assert_ne!(a.is_created(), a.is_status_changed());
        }
    }
}