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 AmbiguousTarget,
34 SelectionTooLarge,
36}
37
38impl ErrorCode {
39 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 #[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 #[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#[derive(Clone, Debug, PartialEq)]
102pub struct MutationError {
103 pub code: ErrorCode,
104 pub message: String,
105 pub data: Map<String, Value>,
107}
108
109impl MutationError {
110 #[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 #[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 #[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
157pub 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}