Skip to main content

omgbase_mutate/
error.rs

1//! The mutation error (`spec/mutate/README.md` §8): a code and a data object
2//! whose listed fields (`op_index`, `block`, `current`, `retriable`, …) the
3//! fixtures compare; the message is not pinned.
4
5use std::fmt;
6
7use serde_json::{Map, Value};
8
9/// The §8 error codes.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub enum ErrorCode {
12    StaleExpectation,
13    BlockMissing,
14    TargetMissing,
15    ParentMissing,
16    DocMissing,
17    CycleMove,
18    NotContiguous,
19    TypeMismatch,
20    PathTaken,
21    NodeNotEditable,
22    StalePlan,
23    PlanNotConvergent,
24    SyncConflict,
25    /// Two effects of one request contradict each other on one focus (§11).
26    ConflictingEffects,
27    /// A plan's `version` this engine does not read (§11).
28    UnsupportedPlanVersion,
29    /// A `plan_id` that does not digest the plan it was given with (§11).
30    PlanMismatch,
31}
32
33impl ErrorCode {
34    /// Every code, in the §8 order.
35    pub const ALL: [ErrorCode; 16] = [
36        ErrorCode::StaleExpectation,
37        ErrorCode::BlockMissing,
38        ErrorCode::TargetMissing,
39        ErrorCode::ParentMissing,
40        ErrorCode::DocMissing,
41        ErrorCode::CycleMove,
42        ErrorCode::NotContiguous,
43        ErrorCode::TypeMismatch,
44        ErrorCode::PathTaken,
45        ErrorCode::NodeNotEditable,
46        ErrorCode::StalePlan,
47        ErrorCode::PlanNotConvergent,
48        ErrorCode::SyncConflict,
49        ErrorCode::ConflictingEffects,
50        ErrorCode::UnsupportedPlanVersion,
51        ErrorCode::PlanMismatch,
52    ];
53
54    /// The spec's spelling.
55    #[must_use]
56    pub const fn as_str(&self) -> &'static str {
57        match self {
58            ErrorCode::StaleExpectation => "stale_expectation",
59            ErrorCode::BlockMissing => "block_missing",
60            ErrorCode::TargetMissing => "target_missing",
61            ErrorCode::ParentMissing => "parent_missing",
62            ErrorCode::DocMissing => "doc_missing",
63            ErrorCode::CycleMove => "cycle_move",
64            ErrorCode::NotContiguous => "not_contiguous",
65            ErrorCode::TypeMismatch => "type_mismatch",
66            ErrorCode::PathTaken => "path_taken",
67            ErrorCode::NodeNotEditable => "node_not_editable",
68            ErrorCode::StalePlan => "stale_plan",
69            ErrorCode::PlanNotConvergent => "plan_not_convergent",
70            ErrorCode::SyncConflict => "sync_conflict",
71            ErrorCode::ConflictingEffects => "conflicting_effects",
72            ErrorCode::UnsupportedPlanVersion => "unsupported_plan_version",
73            ErrorCode::PlanMismatch => "plan_mismatch",
74        }
75    }
76
77    /// The code for a spec spelling.
78    #[must_use]
79    pub fn parse(s: &str) -> Option<Self> {
80        Self::ALL.iter().copied().find(|c| c.as_str() == s)
81    }
82}
83
84impl fmt::Display for ErrorCode {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        f.write_str(self.as_str())
87    }
88}
89
90/// A refused mutation: the reference's `MutationError` (`code`, `message`,
91/// `data`).
92#[derive(Clone, Debug, PartialEq)]
93pub struct MutationError {
94    pub code: ErrorCode,
95    pub message: String,
96    /// The §8 data fields (`op_index`, `block`, `current`, `retriable`, …).
97    pub data: Map<String, Value>,
98}
99
100impl MutationError {
101    /// An error with no data.
102    #[must_use]
103    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
104        Self {
105            code,
106            message: message.into(),
107            data: Map::new(),
108        }
109    }
110
111    /// An error with a data object.
112    #[must_use]
113    pub fn with_data(code: ErrorCode, message: impl Into<String>, data: Value) -> Self {
114        let data = match data {
115            Value::Object(m) => m,
116            _ => Map::new(),
117        };
118        Self {
119            code,
120            message: message.into(),
121            data,
122        }
123    }
124
125    /// `{ code, ...data }` — the wire shape (§9 `expect.steps[i].error`).
126    #[must_use]
127    pub fn to_json(&self) -> Value {
128        let mut m = Map::new();
129        m.insert(
130            "code".to_owned(),
131            Value::String(self.code.as_str().to_owned()),
132        );
133        for (k, v) in &self.data {
134            m.insert(k.clone(), v.clone());
135        }
136        Value::Object(m)
137    }
138}
139
140impl fmt::Display for MutationError {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(f, "{}: {}", self.code, self.message)
143    }
144}
145
146impl std::error::Error for MutationError {}
147
148/// `Result` with a [`MutationError`].
149pub type Result<T> = std::result::Result<T, MutationError>;
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use serde_json::json;
155
156    #[test]
157    fn codes_round_trip() {
158        for c in ErrorCode::ALL {
159            assert_eq!(ErrorCode::parse(c.as_str()), Some(c));
160        }
161        assert_eq!(ErrorCode::parse("nope"), None);
162        assert_eq!(ErrorCode::ALL.len(), 16);
163    }
164
165    #[test]
166    fn wire_shape_flattens_data_under_code() {
167        let e = MutationError::with_data(
168            ErrorCode::BlockMissing,
169            "block b_x not found",
170            json!({ "op_index": 2, "block": "b_x" }),
171        );
172        assert_eq!(
173            e.to_json(),
174            json!({ "code": "block_missing", "op_index": 2, "block": "b_x" })
175        );
176        assert_eq!(e.to_string(), "block_missing: block b_x not found");
177        let plain = MutationError::new(ErrorCode::CycleMove, "x");
178        assert_eq!(plain.to_json(), json!({ "code": "cycle_move" }));
179    }
180}