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}
26
27impl ErrorCode {
28 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 #[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 #[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#[derive(Clone, Debug, PartialEq)]
81pub struct MutationError {
82 pub code: ErrorCode,
83 pub message: String,
84 pub data: Map<String, Value>,
86}
87
88impl MutationError {
89 #[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 #[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 #[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
136pub 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}