cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use serde::Serialize;

// ── RawEventAction ────────────────────────────────────────────────────────────

/// Intermediate event action after YAML deserialisation, before status enrichment.
///
/// Status names are raw strings because [`StatusesConfig`] is not available at
/// deserialisation time. Call `enrich::enrich_event_action` to convert to a typed
/// [`crate::domain::model::event::EventAction`].
#[derive(Debug, Clone)]
pub enum RawEventAction {
    Created {
        status: String,
    },
    StatusChanged {
        from: String,
        to: String,
    },
    /// Any unknown action name — dropped during enrichment (not preserved).
    Other(String),
}

/// Deserialisation of the raw event action map.
pub mod raw_event_action {
    use super::RawEventAction;
    use serde::de::{self, MapAccess, Visitor};
    use serde::Deserializer;
    use std::fmt;

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<RawEventAction, D::Error> {
        d.deserialize_map(RawEventActionVisitor)
    }

    struct RawEventActionVisitor;

    impl<'de> Visitor<'de> for RawEventActionVisitor {
        type Value = RawEventAction;

        fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
            f.write_str("an event action map with a 'name' field")
        }

        fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<RawEventAction, A::Error> {
            let mut name: Option<String> = None;
            let mut status: Option<String> = None;
            let mut from: Option<String> = None;
            let mut to: Option<String> = None;

            while let Some(key) = map.next_key::<String>()? {
                match key.as_str() {
                    "name" => name = Some(map.next_value()?),
                    "status" => status = Some(map.next_value()?),
                    "from" => from = Some(map.next_value()?),
                    "to" => to = Some(map.next_value()?),
                    _ => {
                        let _ = map.next_value::<serde::de::IgnoredAny>()?;
                    }
                }
            }

            let name = name.ok_or_else(|| de::Error::missing_field("name"))?;

            Ok(match name.as_str() {
                "created" => RawEventAction::Created {
                    status: status.ok_or_else(|| de::Error::missing_field("status"))?,
                },
                "status_changed" => RawEventAction::StatusChanged {
                    from: from.ok_or_else(|| de::Error::missing_field("from"))?,
                    to: to.ok_or_else(|| de::Error::missing_field("to"))?,
                },
                other => RawEventAction::Other(other.to_string()),
            })
        }
    }
}

// ── RawEvent ──────────────────────────────────────────────────────────────────

/// A pre-enrichment event — holds a [`RawEventAction`] instead of a typed
/// [`crate::domain::model::event::EventAction`].
///
/// Handles both the v1 flat format and the v2 tagged format for backward
/// compatibility.
#[derive(Debug, Clone)]
pub struct RawEvent {
    pub timestamp: crate::domain::model::temporal::timestamp::Timestamp,
    pub action: RawEventAction,
}

impl<'de> serde::Deserialize<'de> for RawEvent {
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        raw_event_compat::deserialize(d)
    }
}

/// Deserialise a YAML event map into a [`RawEvent`].
///
/// Handles v1 (flat) and v2 (tagged) formats. Unknown fields (including `by`
/// and `notes` from older files) are silently ignored.
///
/// **v1 (legacy)**
/// ```yaml
/// - timestamp: "2026-03-10T10:00:00Z"
///   action: status_changed
///   from: open
///   to: closed
/// ```
///
/// **v2 (current)**
/// ```yaml
/// - timestamp: "2026-03-10T10:00:00Z"
///   action:
///     name: status_changed
///     from: open
///     to: closed
/// ```
pub mod raw_event_compat {
    use super::{RawEvent, RawEventAction};
    use crate::domain::model::temporal::timestamp::Timestamp;
    use serde::de::{self, MapAccess, Visitor};
    use serde::Deserializer;
    use std::fmt;

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<RawEvent, D::Error> {
        d.deserialize_map(RawEventVisitor)
    }

    struct RawEventVisitor;

    impl<'de> Visitor<'de> for RawEventVisitor {
        type Value = RawEvent;

        fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
            f.write_str("an event map")
        }

        fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<RawEvent, A::Error> {
            let mut timestamp: Option<Timestamp> = None;
            let mut action_raw: Option<ActionRaw> = None;
            // v1 siblings
            let mut from_v1: Option<String> = None;
            let mut to_v1: Option<String> = None;

            while let Some(key) = map.next_key::<String>()? {
                match key.as_str() {
                    "timestamp" => timestamp = Some(map.next_value()?),
                    "action" => action_raw = Some(map.next_value()?),
                    "from" => from_v1 = Some(map.next_value()?),
                    "to" => to_v1 = Some(map.next_value()?),
                    // Silently ignore: by, notes, details, and any future fields.
                    _ => {
                        let _ = map.next_value::<serde::de::IgnoredAny>()?;
                    }
                }
            }

            let timestamp = timestamp.ok_or_else(|| de::Error::missing_field("timestamp"))?;
            let action_raw = action_raw.ok_or_else(|| de::Error::missing_field("action"))?;

            let action = match action_raw {
                ActionRaw::V2(a) => a,
                ActionRaw::V1(name) => match name.as_str() {
                    "created" => RawEventAction::Created {
                        // v1 format has no status field — use from_v1 or empty string;
                        // enrich_event_action will produce Status::unresolved which
                        // validate_event_chain will report as an error.
                        status: from_v1.unwrap_or_default(),
                    },
                    "status_changed" => RawEventAction::StatusChanged {
                        from: from_v1.unwrap_or_default(),
                        to: to_v1.unwrap_or_default(),
                    },
                    other => RawEventAction::Other(other.to_string()),
                },
            };

            Ok(RawEvent { timestamp, action })
        }
    }

    enum ActionRaw {
        V1(String),
        V2(RawEventAction),
    }

    impl<'de> serde::Deserialize<'de> for ActionRaw {
        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
            struct ActionRawVisitor;

            impl<'de> Visitor<'de> for ActionRawVisitor {
                type Value = ActionRaw;

                fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                    f.write_str("a string (v1) or a map (v2) for the action field")
                }

                fn visit_str<E: de::Error>(self, v: &str) -> Result<ActionRaw, E> {
                    Ok(ActionRaw::V1(v.to_string()))
                }

                fn visit_string<E: de::Error>(self, v: String) -> Result<ActionRaw, E> {
                    Ok(ActionRaw::V1(v))
                }

                fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<ActionRaw, A::Error> {
                    use serde::de::value::MapAccessDeserializer;
                    let action =
                        super::raw_event_action::deserialize(MapAccessDeserializer::new(map))?;
                    Ok(ActionRaw::V2(action))
                }
            }

            d.deserialize_any(ActionRawVisitor)
        }
    }
}

// ── EventLog serialisation ────────────────────────────────────────────────────

pub struct EventLogSer<'a>(pub &'a crate::domain::model::event::EventLog);

impl Serialize for EventLogSer<'_> {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        use serde::ser::SerializeSeq;
        let mut seq = s.serialize_seq(Some(self.0.len()))?;
        for event in self.0.iter() {
            seq.serialize_element(&EventSer(event))?;
        }
        seq.end()
    }
}

struct EventSer<'a>(&'a crate::domain::model::event::Event);

impl Serialize for EventSer<'_> {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        use serde::ser::SerializeMap;
        let e = self.0;
        let mut map = s.serialize_map(Some(2))?;
        map.serialize_entry("timestamp", &e.timestamp)?;
        map.serialize_entry("action", &e.action)?;
        map.end()
    }
}