1use toolkit_canonical_errors::CanonicalError;
11
12use crate::Error;
13use crate::errors::OdataError;
14
15impl From<Error> for CanonicalError {
16 fn from(err: Error) -> Self {
17 use Error::{
18 CursorInvalidBase64, CursorInvalidDirection, CursorInvalidFields, CursorInvalidJson,
19 CursorInvalidKeys, CursorInvalidVersion, Db, FilterMismatch, InvalidCursor,
20 InvalidFilter, InvalidLimit, InvalidOrderByField, OrderMismatch, OrderWithCursor,
21 ParsingUnavailable,
22 };
23
24 match err {
25 InvalidFilter(msg) => OdataError::invalid_argument()
26 .with_field_violation(
27 "$filter",
28 format!("Invalid $filter: {msg}"),
29 "INVALID_FILTER",
30 )
31 .create(),
32
33 InvalidOrderByField(field) => OdataError::invalid_argument()
34 .with_field_violation(
35 "$orderby",
36 format!("Unsupported $orderby field: {field}"),
37 "INVALID_ORDERBY_FIELD",
38 )
39 .create(),
40
41 InvalidCursor => OdataError::invalid_argument()
42 .with_field_violation("cursor", "invalid cursor", "INVALID_CURSOR")
43 .create(),
44
45 CursorInvalidBase64 => OdataError::invalid_argument()
46 .with_field_violation(
47 "cursor",
48 "invalid cursor: invalid base64url encoding",
49 "INVALID_CURSOR",
50 )
51 .create(),
52
53 CursorInvalidJson => OdataError::invalid_argument()
54 .with_field_violation("cursor", "invalid cursor: malformed JSON", "INVALID_CURSOR")
55 .create(),
56
57 CursorInvalidVersion => OdataError::invalid_argument()
58 .with_field_violation(
59 "cursor",
60 "invalid cursor: unsupported version",
61 "INVALID_CURSOR",
62 )
63 .create(),
64
65 CursorInvalidKeys => OdataError::invalid_argument()
66 .with_field_violation(
67 "cursor",
68 "invalid cursor: empty or invalid keys",
69 "INVALID_CURSOR",
70 )
71 .create(),
72
73 CursorInvalidFields => OdataError::invalid_argument()
74 .with_field_violation(
75 "cursor",
76 "invalid cursor: empty or invalid fields",
77 "INVALID_CURSOR",
78 )
79 .create(),
80
81 CursorInvalidDirection => OdataError::invalid_argument()
82 .with_field_violation(
83 "cursor",
84 "invalid cursor: invalid sort direction",
85 "INVALID_CURSOR",
86 )
87 .create(),
88
89 OrderMismatch => OdataError::invalid_argument()
90 .with_field_violation(
91 "cursor",
92 "Order mismatch between cursor and query",
93 "ORDER_MISMATCH",
94 )
95 .create(),
96
97 FilterMismatch => OdataError::invalid_argument()
98 .with_field_violation(
99 "cursor",
100 "Filter mismatch between cursor and query",
101 "FILTER_MISMATCH",
102 )
103 .create(),
104
105 InvalidLimit => OdataError::invalid_argument()
106 .with_field_violation("$top", "Invalid limit parameter", "INVALID_LIMIT")
107 .create(),
108
109 OrderWithCursor => OdataError::invalid_argument()
114 .with_field_violation(
115 "$orderby",
116 "Cannot specify $orderby when cursor is present",
117 "ORDER_WITH_CURSOR",
118 )
119 .with_field_violation(
120 "cursor",
121 "Cannot specify cursor when $orderby is present",
122 "ORDER_WITH_CURSOR",
123 )
124 .create(),
125
126 Db(msg) => CanonicalError::internal(format!("OData Db error: {msg}")).create(),
131
132 ParsingUnavailable(msg) => {
133 CanonicalError::internal(format!("OData parsing unavailable: {msg}")).create()
134 }
135 }
136 }
137}
138
139#[cfg(test)]
140#[cfg_attr(coverage_nightly, coverage(off))]
141mod tests {
142 use super::*;
143 use toolkit_canonical_errors::Problem;
144 use toolkit_gts::gts_id;
145
146 const ODATA_RESOURCE_TYPE: &str = gts_id!("cf.core.odata.query.v1~");
147
148 fn wire(err: Error) -> Problem {
149 Problem::from(CanonicalError::from(err))
150 }
151
152 fn field_violations(p: &Problem) -> &Vec<serde_json::Value> {
153 p.context
154 .get("field_violations")
155 .and_then(|v| v.as_array())
156 .expect("invalid_argument context must carry field_violations[]")
157 }
158
159 fn assert_violation(v: &serde_json::Value, field: &str, reason: &str) {
160 assert_eq!(
161 v.get("field").and_then(|x| x.as_str()),
162 Some(field),
163 "unexpected violation field in {v}"
164 );
165 assert_eq!(
166 v.get("reason").and_then(|x| x.as_str()),
167 Some(reason),
168 "unexpected violation reason in {v}"
169 );
170 }
171
172 fn resource_type(p: &Problem) -> &str {
173 p.context
174 .get("resource_type")
175 .and_then(|v| v.as_str())
176 .expect("InvalidArgument from the OData scope must tag resource_type")
177 }
178
179 #[test]
180 fn invalid_filter_populates_field_violation() {
181 let p = wire(Error::InvalidFilter("malformed".into()));
182 assert_eq!(p.status, 400);
183 assert!(p.problem_type.contains("invalid_argument"));
184 assert_eq!(resource_type(&p), ODATA_RESOURCE_TYPE);
185 let violations = field_violations(&p);
186 assert_eq!(violations.len(), 1);
187 assert_violation(&violations[0], "$filter", "INVALID_FILTER");
188 let desc = violations[0]
189 .get("description")
190 .and_then(|x| x.as_str())
191 .unwrap_or_default();
192 assert!(desc.contains("malformed"), "description was {desc:?}");
193 }
194
195 #[test]
196 fn invalid_orderby_field_populates_field_violation() {
197 let p = wire(Error::InvalidOrderByField("unknown".into()));
198 assert_eq!(p.status, 400);
199 assert_eq!(resource_type(&p), ODATA_RESOURCE_TYPE);
200 let violations = field_violations(&p);
201 assert_eq!(violations.len(), 1);
202 assert_violation(&violations[0], "$orderby", "INVALID_ORDERBY_FIELD");
203 }
204
205 #[test]
206 fn cursor_invalid_base64_populates_field_violation() {
207 let p = wire(Error::CursorInvalidBase64);
208 assert_eq!(p.status, 400);
209 assert_eq!(resource_type(&p), ODATA_RESOURCE_TYPE);
210 let violations = field_violations(&p);
211 assert_eq!(violations.len(), 1);
212 assert_violation(&violations[0], "cursor", "INVALID_CURSOR");
213 }
214
215 #[test]
216 fn order_with_cursor_emits_two_violations() {
217 let p = wire(Error::OrderWithCursor);
218 assert_eq!(p.status, 400);
219 assert_eq!(resource_type(&p), ODATA_RESOURCE_TYPE);
220 let violations = field_violations(&p);
221 assert_eq!(
222 violations.len(),
223 2,
224 "OrderWithCursor must surface both `$orderby` and `cursor`"
225 );
226 let fields: Vec<&str> = violations
227 .iter()
228 .filter_map(|v| v.get("field").and_then(|x| x.as_str()))
229 .collect();
230 assert!(fields.contains(&"$orderby"));
231 assert!(fields.contains(&"cursor"));
232 for v in violations {
233 assert_eq!(
234 v.get("reason").and_then(|x| x.as_str()),
235 Some("ORDER_WITH_CURSOR")
236 );
237 }
238 }
239
240 #[test]
241 fn order_mismatch_keys_to_cursor() {
242 let p = wire(Error::OrderMismatch);
243 let violations = field_violations(&p);
244 assert_eq!(violations.len(), 1);
245 assert_violation(&violations[0], "cursor", "ORDER_MISMATCH");
246 }
247
248 #[test]
249 fn filter_mismatch_keys_to_cursor() {
250 let p = wire(Error::FilterMismatch);
251 let violations = field_violations(&p);
252 assert_eq!(violations.len(), 1);
253 assert_violation(&violations[0], "cursor", "FILTER_MISMATCH");
254 }
255
256 #[test]
257 fn invalid_limit_keys_to_top() {
258 let p = wire(Error::InvalidLimit);
259 let violations = field_violations(&p);
260 assert_eq!(violations.len(), 1);
261 assert_violation(&violations[0], "$top", "INVALID_LIMIT");
262 }
263
264 #[test]
265 fn db_error_maps_to_internal_with_diagnostic() {
266 let canonical =
267 CanonicalError::from(Error::Db("connection refused: 127.0.0.1:5432".into()));
268 let p = Problem::from(canonical.clone());
270 assert_eq!(p.status, 500);
271 assert!(p.problem_type.contains("internal"));
272 assert!(!p.detail.contains("127.0.0.1"));
274 let diag = canonical.diagnostic().expect("Internal carries diagnostic");
276 assert!(
277 diag.contains("connection refused"),
278 "diagnostic was {diag:?}"
279 );
280 }
281
282 #[test]
283 fn parsing_unavailable_maps_to_internal_with_diagnostic() {
284 let canonical = CanonicalError::from(Error::ParsingUnavailable("feature off"));
285 assert_eq!(canonical.status_code(), 500);
286 let diag = canonical.diagnostic().expect("Internal carries diagnostic");
287 assert!(diag.contains("feature off"), "diagnostic was {diag:?}");
288 }
289}