Skip to main content

ferro_projection/
error.rs

1//! `ProjectionError` — the single error type for the ferro-projection crate.
2//!
3//! Every variant's `Display` impl prefixes `"projection: …"` so production
4//! log greps stay surgical (matches `"guarded: …"`, `"audit: …"`,
5//! `"reservation: …"`, `"config: …"`).
6
7#[derive(Debug, thiserror::Error)]
8pub enum ProjectionError {
9    /// Underlying SeaORM database error (snapshot upsert, find, delete).
10    #[error("projection: db error: {0}")]
11    Db(#[from] sea_orm::DbErr),
12
13    /// JSON serialization / deserialization error on `P::State`
14    /// round-trips against the persisted JSON column.
15    #[error("projection: json error: {0}")]
16    Json(#[from] serde_json::Error),
17
18    /// Broadcast publish error. Stringly-typed because
19    /// `ferro_broadcast::Error` doesn't compose cleanly through
20    /// `thiserror::From` — same pragmatic choice Phase 149 made for
21    /// `ferro_notifications::Error::Broadcast(String)`.
22    ///
23    /// Per D-21: broadcast failure does NOT roll back state — the
24    /// snapshot row is already persisted. Surfaces this error so
25    /// consumers can alarm on it; subscribers reconcile by re-reading
26    /// the snapshot.
27    #[error("projection: broadcast error: {0}")]
28    Broadcast(String),
29
30    /// Event-bus error surfaced from `ferro_events::Error`. Hand-rolled
31    /// `From` impl mirrors `Broadcast` above (D-29).
32    #[error("projection: events error: {0}")]
33    Events(String),
34
35    /// Reserved for an explicit `read_required` helper if a consumer
36    /// wants `Result<State, _>` rather than `Result<Option<State>, _>`.
37    /// Not used by `read` itself (which returns `Result<Option<_>, _>`).
38    #[error("projection: state not found for {name}/{key}")]
39    StateNotFound { name: &'static str, key: String },
40}
41
42impl From<ferro_broadcast::Error> for ProjectionError {
43    fn from(e: ferro_broadcast::Error) -> Self {
44        Self::Broadcast(e.to_string())
45    }
46}
47
48impl From<ferro_events::Error> for ProjectionError {
49    fn from(e: ferro_events::Error) -> Self {
50        Self::Events(e.to_string())
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn db_from_sea_orm_dberr() {
60        let db_err = sea_orm::DbErr::Custom("test".into());
61        let e: ProjectionError = ProjectionError::from(db_err);
62        assert!(matches!(e, ProjectionError::Db(_)));
63        assert!(e.to_string().starts_with("projection: db error: "));
64    }
65
66    #[test]
67    fn json_from_serde_json_error() {
68        let j: serde_json::Error =
69            serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
70        let e: ProjectionError = ProjectionError::from(j);
71        assert!(matches!(e, ProjectionError::Json(_)));
72        assert!(e.to_string().starts_with("projection: json error: "));
73    }
74
75    #[test]
76    fn broadcast_display() {
77        let e = ProjectionError::Broadcast("oops".into());
78        assert_eq!(e.to_string(), "projection: broadcast error: oops");
79    }
80
81    #[test]
82    fn events_display() {
83        let e = ProjectionError::Events("oops".into());
84        assert_eq!(e.to_string(), "projection: events error: oops");
85    }
86
87    #[test]
88    fn state_not_found_display() {
89        let e = ProjectionError::StateNotFound {
90            name: "inventory.dashboard",
91            key: "warehouse-a".into(),
92        };
93        assert_eq!(
94            e.to_string(),
95            "projection: state not found for inventory.dashboard/warehouse-a"
96        );
97    }
98}