1use std::fmt;
6
7use serde_json::{Map, Value};
8
9#[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 ConflictingEffects,
27 UnsupportedPlanVersion,
29 PlanMismatch,
31}
32
33impl ErrorCode {
34 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 #[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 #[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#[derive(Clone, Debug, PartialEq)]
93pub struct MutationError {
94 pub code: ErrorCode,
95 pub message: String,
96 pub data: Map<String, Value>,
98}
99
100impl MutationError {
101 #[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 #[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 #[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
148pub 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}