1use std::fmt;
8
9use serde_json::{Map, Value, json};
10
11#[derive(Clone, Debug, PartialEq)]
13pub struct SurfaceError {
14 pub code: String,
16 pub message: String,
17 pub data: Option<Value>,
19 pub retriable: bool,
20}
21
22impl SurfaceError {
23 #[must_use]
24 pub fn new(code: &str, message: impl Into<String>) -> Self {
25 Self {
26 code: code.to_owned(),
27 message: message.into(),
28 data: None,
29 retriable: false,
30 }
31 }
32
33 #[must_use]
34 pub fn with_data(code: &str, message: impl Into<String>, data: Value) -> Self {
35 Self {
36 code: code.to_owned(),
37 message: message.into(),
38 data: Some(data),
39 retriable: false,
40 }
41 }
42
43 #[must_use]
47 pub fn filter_invalid(message: impl Into<String>, hint: &str) -> Self {
48 let message = message.into();
49 Self::with_data(
50 "filter_invalid",
51 message.clone(),
52 json!({ "reason": message, "hint": hint }),
53 )
54 }
55
56 #[must_use]
58 pub fn cursor_invalid(surface: &str) -> Self {
59 Self::with_data(
60 "filter_invalid",
61 "invalid cursor",
62 json!({
63 "reason": format!("cursor was not issued by {surface}"),
64 "hint": "resume only with a `cursor` returned by a truncated page of the same tool",
65 }),
66 )
67 }
68
69 #[must_use]
71 pub fn other(message: impl Into<String>) -> Self {
72 Self::new("repo_not_found", message)
73 }
74
75 #[must_use]
77 pub fn to_json(&self) -> Value {
78 let mut m = Map::new();
79 m.insert("error".to_owned(), Value::String(self.code.clone()));
80 m.insert("message".to_owned(), Value::String(self.message.clone()));
81 if let Some(d) = &self.data {
82 m.insert("data".to_owned(), d.clone());
83 }
84 m.insert("retriable".to_owned(), Value::Bool(self.retriable));
85 Value::Object(m)
86 }
87}
88
89impl fmt::Display for SurfaceError {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 write!(f, "{}: {}", self.code, self.message)
92 }
93}
94
95impl std::error::Error for SurfaceError {}
96
97impl From<omgbase_store::Error> for SurfaceError {
98 fn from(e: omgbase_store::Error) -> Self {
99 match e {
100 omgbase_store::Error::Mutation(m) => Self::from(m),
101 omgbase_store::Error::Search(s) => Self::new(s.code(), s.to_string()),
102 other => Self::other(other.to_string()),
103 }
104 }
105}
106
107impl From<omgbase_store::MutationError> for SurfaceError {
108 fn from(m: omgbase_store::MutationError) -> Self {
109 let retriable = m
110 .data
111 .get("retriable")
112 .and_then(Value::as_bool)
113 .unwrap_or(false);
114 Self {
115 code: m.code.as_str().to_owned(),
116 message: m.message,
117 data: Some(Value::Object(m.data)),
118 retriable,
119 }
120 }
121}
122
123impl From<omgbase_sync::Error> for SurfaceError {
124 fn from(e: omgbase_sync::Error) -> Self {
125 match e {
126 omgbase_sync::Error::Store(s) => Self::from(s),
127 omgbase_sync::Error::RepoNotFound {
128 message,
129 candidates,
130 } => Self::with_data(
131 "repo_not_found",
132 message,
133 json!({ "candidates": candidates }),
134 ),
135 other => Self::other(other.to_string()),
136 }
137 }
138}
139
140impl From<rusqlite::Error> for SurfaceError {
141 fn from(e: rusqlite::Error) -> Self {
142 Self::other(format!("sqlite: {e}"))
143 }
144}
145
146impl From<oqx::OqxError> for SurfaceError {
147 fn from(e: oqx::OqxError) -> Self {
149 Self::filter_invalid(e.message, "OQX")
150 }
151}
152
153pub type Result<T> = std::result::Result<T, SurfaceError>;
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn envelope_shape() {
162 let e = SurfaceError::filter_invalid("bad", "OQX");
163 let j = e.to_json();
164 assert_eq!(j["error"], "filter_invalid");
165 assert_eq!(j["message"], "bad");
166 assert_eq!(j["data"]["reason"], "bad");
167 assert_eq!(j["data"]["hint"], "OQX");
168 assert_eq!(j["retriable"], false);
169 let plain = SurfaceError::other("boom").to_json();
170 assert!(plain.get("data").is_none());
171 assert_eq!(plain["error"], "repo_not_found");
172 }
173
174 #[test]
175 fn mutation_errors_keep_code_and_data() {
176 let m = omgbase_store::MutationError::with_data(
177 omgbase_store::mutate_kernel::ErrorCode::StaleExpectation,
178 "stale",
179 json!({ "block": "b_1", "retriable": true }),
180 );
181 let e = SurfaceError::from(omgbase_store::Error::from(m));
182 assert_eq!(e.code, "stale_expectation");
183 assert!(e.retriable);
184 assert_eq!(e.data.unwrap()["block"], "b_1");
185 }
186}