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