Skip to main content

mq_rest_admin/
error.rs

1//! Error types for the MQ REST admin library.
2
3use std::collections::HashMap;
4use std::fmt;
5
6use serde_json::Value;
7
8/// A single mapping issue recorded during attribute translation.
9#[derive(Debug, Clone)]
10pub struct MappingIssue {
11    /// Whether the issue occurred during `"request"` or `"response"` mapping.
12    pub direction: String,
13    /// Category of the mapping failure.
14    pub reason: String,
15    /// The attribute name that triggered the issue.
16    pub attribute_name: String,
17    /// The attribute value, if relevant to the issue.
18    pub attribute_value: Option<Value>,
19    /// Zero-based index within a response list.
20    pub object_index: Option<usize>,
21    /// The qualifier that was being mapped.
22    pub qualifier: Option<String>,
23}
24
25impl MappingIssue {
26    /// Return the issue as a JSON-serialisable map.
27    #[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/// Error raised when attribute mapping fails in strict mode.
56#[derive(Debug, Clone)]
57pub struct MappingError {
58    /// The mapping issues captured during the failed operation.
59    pub issues: Vec<MappingIssue>,
60    message: String,
61}
62
63impl MappingError {
64    /// Create a new mapping error from the captured issues.
65    #[must_use]
66    pub fn new(issues: Vec<MappingIssue>) -> Self {
67        let message = build_mapping_message(&issues);
68        Self { issues, message }
69    }
70
71    /// Return mapping issues as JSON-serialisable maps.
72    #[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/// All error types for the MQ REST admin library.
114#[derive(Debug, thiserror::Error)]
115pub enum MqRestError {
116    /// The transport failed to reach the MQ REST endpoint.
117    #[error("Failed to reach MQ REST endpoint: {url}")]
118    Transport {
119        /// The endpoint URL that could not be reached.
120        url: String,
121        /// The underlying transport error.
122        #[source]
123        source: reqwest::Error,
124    },
125
126    /// The MQ REST response was malformed or unexpected.
127    #[error("{message}")]
128    Response {
129        /// Human-readable error description.
130        message: String,
131        /// The raw response body, if available.
132        response_text: Option<String>,
133    },
134
135    /// Authentication with the MQ REST API failed.
136    #[error("{message}")]
137    Auth {
138        /// The endpoint URL where authentication failed.
139        url: String,
140        /// The HTTP status code, if available.
141        status_code: Option<u16>,
142        /// Human-readable error description.
143        message: String,
144    },
145
146    /// The MQ REST response indicates MQSC command failure.
147    #[error("{message}")]
148    Command {
149        /// The full JSON response payload.
150        payload: HashMap<String, Value>,
151        /// The HTTP status code, if available.
152        status_code: Option<u16>,
153        /// Human-readable error description.
154        message: String,
155    },
156
157    /// A synchronous operation exceeded its timeout.
158    #[error("{message}")]
159    Timeout {
160        /// The MQ object name that timed out.
161        name: String,
162        /// A description of the operation that timed out.
163        operation: String,
164        /// Seconds elapsed before the timeout was raised.
165        elapsed: f64,
166        /// Human-readable error description.
167        message: String,
168    },
169
170    /// Configuration is invalid.
171    #[error("{message}")]
172    InvalidConfig {
173        /// Human-readable description of the invalid configuration.
174        message: String,
175    },
176
177    /// Attribute mapping failed.
178    #[error(transparent)]
179    Mapping(#[from] MappingError),
180}
181
182/// Convenience alias for `Result<T, MqRestError>`.
183pub type Result<T> = std::result::Result<T, MqRestError>;
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use serde_json::json;
189
190    // ---- MappingIssue::to_payload ----
191
192    #[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    // ---- MappingError ----
228
229    #[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    // ---- build_mapping_message ----
268
269    #[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    // ---- MqRestError variants ----
322
323    #[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}