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