1use std::collections::HashMap;
4use std::fmt;
5
6use serde_json::Value;
7
8#[derive(Debug, Clone)]
10pub struct MappingIssue {
11 pub direction: String,
13 pub reason: String,
15 pub attribute_name: String,
17 pub attribute_value: Option<Value>,
19 pub object_index: Option<usize>,
21 pub qualifier: Option<String>,
23}
24
25impl MappingIssue {
26 #[must_use]
28 pub fn to_payload(&self) -> HashMap<String, Value> {
29 let mut map = HashMap::new();
30 map.insert("direction".into(), Value::String(self.direction.clone()));
31 map.insert("reason".into(), Value::String(self.reason.clone()));
32 map.insert(
33 "attribute_name".into(),
34 Value::String(self.attribute_name.clone()),
35 );
36 map.insert(
37 "attribute_value".into(),
38 self.attribute_value.clone().unwrap_or(Value::Null),
39 );
40 map.insert(
41 "object_index".into(),
42 self.object_index
43 .map_or(Value::Null, |i| Value::Number(i.into())),
44 );
45 map.insert(
46 "qualifier".into(),
47 self.qualifier
48 .as_ref()
49 .map_or(Value::Null, |q| Value::String(q.clone())),
50 );
51 map
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct MappingError {
58 pub issues: Vec<MappingIssue>,
60 message: String,
61}
62
63impl MappingError {
64 #[must_use]
66 pub fn new(issues: Vec<MappingIssue>) -> Self {
67 let message = build_mapping_message(&issues);
68 Self { issues, message }
69 }
70
71 #[must_use]
73 pub fn to_payload(&self) -> Vec<HashMap<String, Value>> {
74 self.issues.iter().map(MappingIssue::to_payload).collect()
75 }
76}
77
78impl fmt::Display for MappingError {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.write_str(&self.message)
81 }
82}
83
84impl std::error::Error for MappingError {}
85
86fn build_mapping_message(issues: &[MappingIssue]) -> String {
87 if issues.is_empty() {
88 return "Mapping failed with no issues reported.".into();
89 }
90 let mut lines = vec![format!("Mapping failed with {} issue(s):", issues.len())];
91 for issue in issues {
92 let index_label = issue
93 .object_index
94 .map_or_else(|| "-".into(), |i| i.to_string());
95 let qualifier_label = issue.qualifier.as_deref().unwrap_or("-");
96 let value_label = issue
97 .attribute_value
98 .as_ref()
99 .map_or_else(|| "-".into(), |v| format!("{v}"));
100 lines.push(format!(
101 "index={} | qualifier={} | direction={} | reason={} | attribute={} | value={}",
102 index_label,
103 qualifier_label,
104 issue.direction,
105 issue.reason,
106 issue.attribute_name,
107 value_label,
108 ));
109 }
110 lines.join("\n")
111}
112
113#[derive(Debug, thiserror::Error)]
115pub enum MqRestError {
116 #[error("Failed to reach MQ REST endpoint: {url}")]
118 Transport {
119 url: String,
121 #[source]
123 source: reqwest::Error,
124 },
125
126 #[error("{message}")]
128 Response {
129 message: String,
131 response_text: Option<String>,
133 },
134
135 #[error("{message}")]
137 Auth {
138 url: String,
140 status_code: Option<u16>,
142 message: String,
144 },
145
146 #[error("{message}")]
148 Command {
149 payload: HashMap<String, Value>,
151 status_code: Option<u16>,
153 message: String,
155 },
156
157 #[error("{message}")]
159 Timeout {
160 name: String,
162 operation: String,
164 elapsed: f64,
166 message: String,
168 },
169
170 #[error("{message}")]
172 InvalidConfig {
173 message: String,
175 },
176
177 #[error(transparent)]
179 Mapping(#[from] MappingError),
180}
181
182pub type Result<T> = std::result::Result<T, MqRestError>;
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use serde_json::json;
189
190 #[test]
193 fn mapping_issue_to_payload_all_some() {
194 let issue = MappingIssue {
195 direction: "request".into(),
196 reason: "unknown_key".into(),
197 attribute_name: "foo".into(),
198 attribute_value: Some(json!("bar")),
199 object_index: Some(2),
200 qualifier: Some("queue".into()),
201 };
202 let payload = issue.to_payload();
203 assert_eq!(payload["direction"], json!("request"));
204 assert_eq!(payload["reason"], json!("unknown_key"));
205 assert_eq!(payload["attribute_name"], json!("foo"));
206 assert_eq!(payload["attribute_value"], json!("bar"));
207 assert_eq!(payload["object_index"], json!(2));
208 assert_eq!(payload["qualifier"], json!("queue"));
209 }
210
211 #[test]
212 fn mapping_issue_to_payload_all_none() {
213 let issue = MappingIssue {
214 direction: "response".into(),
215 reason: "unknown_value".into(),
216 attribute_name: "x".into(),
217 attribute_value: None,
218 object_index: None,
219 qualifier: None,
220 };
221 let payload = issue.to_payload();
222 assert_eq!(payload["attribute_value"], Value::Null);
223 assert_eq!(payload["object_index"], Value::Null);
224 assert_eq!(payload["qualifier"], Value::Null);
225 }
226
227 #[test]
230 fn mapping_error_new_and_display() {
231 let issue = MappingIssue {
232 direction: "request".into(),
233 reason: "unknown_key".into(),
234 attribute_name: "foo".into(),
235 attribute_value: Some(json!("bar")),
236 object_index: None,
237 qualifier: Some("queue".into()),
238 };
239 let error = MappingError::new(vec![issue]);
240 let display = format!("{error}");
241 assert!(display.contains("1 issue(s)"));
242 assert!(display.contains("unknown_key"));
243 }
244
245 #[test]
246 fn mapping_error_to_payload() {
247 let issue = MappingIssue {
248 direction: "request".into(),
249 reason: "r".into(),
250 attribute_name: "a".into(),
251 attribute_value: None,
252 object_index: None,
253 qualifier: None,
254 };
255 let error = MappingError::new(vec![issue]);
256 let payload = error.to_payload();
257 assert_eq!(payload.len(), 1);
258 assert_eq!(payload[0]["direction"], json!("request"));
259 }
260
261 #[test]
262 fn mapping_error_is_error_trait() {
263 let error = MappingError::new(vec![]);
264 let _: &dyn std::error::Error = &error;
265 }
266
267 #[test]
270 fn build_mapping_message_empty() {
271 let msg = build_mapping_message(&[]);
272 assert_eq!(msg, "Mapping failed with no issues reported.");
273 }
274
275 #[test]
276 fn build_mapping_message_single_issue() {
277 let issue = MappingIssue {
278 direction: "request".into(),
279 reason: "unknown_key".into(),
280 attribute_name: "foo".into(),
281 attribute_value: Some(json!("bar")),
282 object_index: Some(0),
283 qualifier: Some("queue".into()),
284 };
285 let msg = build_mapping_message(&[issue]);
286 assert!(msg.contains("1 issue(s)"));
287 assert!(msg.contains("index=0"));
288 assert!(msg.contains("qualifier=queue"));
289 }
290
291 #[test]
292 fn build_mapping_message_multi_issue() {
293 let issues = vec![
294 MappingIssue {
295 direction: "request".into(),
296 reason: "unknown_key".into(),
297 attribute_name: "a".into(),
298 attribute_value: None,
299 object_index: None,
300 qualifier: None,
301 },
302 MappingIssue {
303 direction: "response".into(),
304 reason: "unknown_value".into(),
305 attribute_name: "b".into(),
306 attribute_value: Some(json!(42)),
307 object_index: Some(1),
308 qualifier: Some("channel".into()),
309 },
310 ];
311 let msg = build_mapping_message(&issues);
312 assert!(msg.contains("2 issue(s)"));
313 assert!(msg.contains("index=-"));
314 assert!(msg.contains("qualifier=-"));
315 assert!(msg.contains("index=1"));
316 assert!(msg.contains("qualifier=channel"));
317 assert!(msg.contains("value=-"));
318 assert!(msg.contains("value=42"));
319 }
320
321 #[test]
324 fn mq_rest_error_transport_display() {
325 let client = reqwest::blocking::Client::new();
326 let err = client.get("http://[::1]:0/bad").send().unwrap_err();
327 let mq_err = MqRestError::Transport {
328 url: "http://test".into(),
329 source: err,
330 };
331 let display = format!("{mq_err}");
332 assert!(display.contains("http://test"));
333 }
334
335 #[test]
336 fn mq_rest_error_response_display() {
337 let err = MqRestError::Response {
338 message: "bad json".into(),
339 response_text: Some("raw".into()),
340 };
341 assert_eq!(format!("{err}"), "bad json");
342 }
343
344 #[test]
345 fn mq_rest_error_auth_display() {
346 let err = MqRestError::Auth {
347 url: "https://host/login".into(),
348 status_code: Some(401),
349 message: "auth failed".into(),
350 };
351 assert_eq!(format!("{err}"), "auth failed");
352 }
353
354 #[test]
355 fn mq_rest_error_command_display() {
356 let err = MqRestError::Command {
357 payload: HashMap::new(),
358 status_code: Some(200),
359 message: "command failed".into(),
360 };
361 assert_eq!(format!("{err}"), "command failed");
362 }
363
364 #[test]
365 fn mq_rest_error_timeout_display() {
366 let err = MqRestError::Timeout {
367 name: "MY.CHANNEL".into(),
368 operation: "start".into(),
369 elapsed: 30.0,
370 message: "timed out".into(),
371 };
372 assert_eq!(format!("{err}"), "timed out");
373 }
374
375 #[test]
376 fn mq_rest_error_invalid_config_display() {
377 let err = MqRestError::InvalidConfig {
378 message: "timeout must be positive".into(),
379 };
380 assert_eq!(format!("{err}"), "timeout must be positive");
381 }
382
383 #[test]
384 fn mq_rest_error_mapping_display() {
385 let mapping_err = MappingError::new(vec![]);
386 let err = MqRestError::Mapping(mapping_err);
387 let display = format!("{err}");
388 assert!(display.contains("no issues reported"));
389 }
390
391 #[test]
392 fn mq_rest_error_mapping_from() {
393 let mapping_err = MappingError::new(vec![]);
394 let err: MqRestError = mapping_err.into();
395 assert!(format!("{err:?}").starts_with("Mapping"));
396 }
397}