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}
26
27impl ErrorCode {
28    /// Every code, in the §8 order.
29    pub const ALL: [ErrorCode; 13] = [
30        ErrorCode::StaleExpectation,
31        ErrorCode::BlockMissing,
32        ErrorCode::TargetMissing,
33        ErrorCode::ParentMissing,
34        ErrorCode::DocMissing,
35        ErrorCode::CycleMove,
36        ErrorCode::NotContiguous,
37        ErrorCode::TypeMismatch,
38        ErrorCode::PathTaken,
39        ErrorCode::NodeNotEditable,
40        ErrorCode::StalePlan,
41        ErrorCode::PlanNotConvergent,
42        ErrorCode::SyncConflict,
43    ];
44
45    /// The spec's spelling.
46    #[must_use]
47    pub const fn as_str(&self) -> &'static str {
48        match self {
49            ErrorCode::StaleExpectation => "stale_expectation",
50            ErrorCode::BlockMissing => "block_missing",
51            ErrorCode::TargetMissing => "target_missing",
52            ErrorCode::ParentMissing => "parent_missing",
53            ErrorCode::DocMissing => "doc_missing",
54            ErrorCode::CycleMove => "cycle_move",
55            ErrorCode::NotContiguous => "not_contiguous",
56            ErrorCode::TypeMismatch => "type_mismatch",
57            ErrorCode::PathTaken => "path_taken",
58            ErrorCode::NodeNotEditable => "node_not_editable",
59            ErrorCode::StalePlan => "stale_plan",
60            ErrorCode::PlanNotConvergent => "plan_not_convergent",
61            ErrorCode::SyncConflict => "sync_conflict",
62        }
63    }
64
65    /// The code for a spec spelling.
66    #[must_use]
67    pub fn parse(s: &str) -> Option<Self> {
68        Self::ALL.iter().copied().find(|c| c.as_str() == s)
69    }
70}
71
72impl fmt::Display for ErrorCode {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str(self.as_str())
75    }
76}
77
78/// A refused mutation: the reference's `MutationError` (`code`, `message`,
79/// `data`).
80#[derive(Clone, Debug, PartialEq)]
81pub struct MutationError {
82    pub code: ErrorCode,
83    pub message: String,
84    /// The §8 data fields (`op_index`, `block`, `current`, `retriable`, …).
85    pub data: Map<String, Value>,
86}
87
88impl MutationError {
89    /// An error with no data.
90    #[must_use]
91    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
92        Self {
93            code,
94            message: message.into(),
95            data: Map::new(),
96        }
97    }
98
99    /// An error with a data object.
100    #[must_use]
101    pub fn with_data(code: ErrorCode, message: impl Into<String>, data: Value) -> Self {
102        let data = match data {
103            Value::Object(m) => m,
104            _ => Map::new(),
105        };
106        Self {
107            code,
108            message: message.into(),
109            data,
110        }
111    }
112
113    /// `{ code, ...data }` — the wire shape (§9 `expect.steps[i].error`).
114    #[must_use]
115    pub fn to_json(&self) -> Value {
116        let mut m = Map::new();
117        m.insert(
118            "code".to_owned(),
119            Value::String(self.code.as_str().to_owned()),
120        );
121        for (k, v) in &self.data {
122            m.insert(k.clone(), v.clone());
123        }
124        Value::Object(m)
125    }
126}
127
128impl fmt::Display for MutationError {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{}: {}", self.code, self.message)
131    }
132}
133
134impl std::error::Error for MutationError {}
135
136/// `Result` with a [`MutationError`].
137pub type Result<T> = std::result::Result<T, MutationError>;
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use serde_json::json;
143
144    #[test]
145    fn codes_round_trip() {
146        for c in ErrorCode::ALL {
147            assert_eq!(ErrorCode::parse(c.as_str()), Some(c));
148        }
149        assert_eq!(ErrorCode::parse("nope"), None);
150        assert_eq!(ErrorCode::ALL.len(), 13);
151    }
152
153    #[test]
154    fn wire_shape_flattens_data_under_code() {
155        let e = MutationError::with_data(
156            ErrorCode::BlockMissing,
157            "block b_x not found",
158            json!({ "op_index": 2, "block": "b_x" }),
159        );
160        assert_eq!(
161            e.to_json(),
162            json!({ "code": "block_missing", "op_index": 2, "block": "b_x" })
163        );
164        assert_eq!(e.to_string(), "block_missing: block b_x not found");
165        let plain = MutationError::new(ErrorCode::CycleMove, "x");
166        assert_eq!(plain.to_json(), json!({ "code": "cycle_move" }));
167    }
168}