use std::collections::HashMap;
use std::fmt;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct MappingIssue {
pub direction: String,
pub reason: String,
pub attribute_name: String,
pub attribute_value: Option<Value>,
pub object_index: Option<usize>,
pub qualifier: Option<String>,
}
impl MappingIssue {
#[must_use]
pub fn to_payload(&self) -> HashMap<String, Value> {
let mut map = HashMap::new();
map.insert("direction".into(), Value::String(self.direction.clone()));
map.insert("reason".into(), Value::String(self.reason.clone()));
map.insert(
"attribute_name".into(),
Value::String(self.attribute_name.clone()),
);
map.insert(
"attribute_value".into(),
self.attribute_value.clone().unwrap_or(Value::Null),
);
map.insert(
"object_index".into(),
self.object_index
.map_or(Value::Null, |i| Value::Number(i.into())),
);
map.insert(
"qualifier".into(),
self.qualifier
.as_ref()
.map_or(Value::Null, |q| Value::String(q.clone())),
);
map
}
}
#[derive(Debug, Clone)]
pub struct MappingError {
pub issues: Vec<MappingIssue>,
message: String,
}
impl MappingError {
#[must_use]
pub fn new(issues: Vec<MappingIssue>) -> Self {
let message = build_mapping_message(&issues);
Self { issues, message }
}
#[must_use]
pub fn to_payload(&self) -> Vec<HashMap<String, Value>> {
self.issues.iter().map(MappingIssue::to_payload).collect()
}
}
impl fmt::Display for MappingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for MappingError {}
fn build_mapping_message(issues: &[MappingIssue]) -> String {
if issues.is_empty() {
return "Mapping failed with no issues reported.".into();
}
let mut lines = vec![format!("Mapping failed with {} issue(s):", issues.len())];
for issue in issues {
let index_label = issue
.object_index
.map_or_else(|| "-".into(), |i| i.to_string());
let qualifier_label = issue.qualifier.as_deref().unwrap_or("-");
let value_label = issue
.attribute_value
.as_ref()
.map_or_else(|| "-".into(), |v| format!("{v}"));
lines.push(format!(
"index={} | qualifier={} | direction={} | reason={} | attribute={} | value={}",
index_label,
qualifier_label,
issue.direction,
issue.reason,
issue.attribute_name,
value_label,
));
}
lines.join("\n")
}
#[derive(Debug, thiserror::Error)]
pub enum MqRestError {
#[error("Failed to reach MQ REST endpoint: {url}")]
Transport {
url: String,
#[source]
source: reqwest::Error,
},
#[error("{message}")]
Response {
message: String,
response_text: Option<String>,
},
#[error("{message}")]
Auth {
url: String,
status_code: Option<u16>,
message: String,
},
#[error("{message}")]
Command {
payload: HashMap<String, Value>,
status_code: Option<u16>,
message: String,
},
#[error("{message}")]
Timeout {
name: String,
operation: String,
elapsed: f64,
message: String,
},
#[error("{message}")]
InvalidConfig {
message: String,
},
#[error(transparent)]
Mapping(#[from] MappingError),
}
pub type Result<T> = std::result::Result<T, MqRestError>;
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn mapping_issue_to_payload_all_some() {
let issue = MappingIssue {
direction: "request".into(),
reason: "unknown_key".into(),
attribute_name: "foo".into(),
attribute_value: Some(json!("bar")),
object_index: Some(2),
qualifier: Some("queue".into()),
};
let payload = issue.to_payload();
assert_eq!(payload["direction"], json!("request"));
assert_eq!(payload["reason"], json!("unknown_key"));
assert_eq!(payload["attribute_name"], json!("foo"));
assert_eq!(payload["attribute_value"], json!("bar"));
assert_eq!(payload["object_index"], json!(2));
assert_eq!(payload["qualifier"], json!("queue"));
}
#[test]
fn mapping_issue_to_payload_all_none() {
let issue = MappingIssue {
direction: "response".into(),
reason: "unknown_value".into(),
attribute_name: "x".into(),
attribute_value: None,
object_index: None,
qualifier: None,
};
let payload = issue.to_payload();
assert_eq!(payload["attribute_value"], Value::Null);
assert_eq!(payload["object_index"], Value::Null);
assert_eq!(payload["qualifier"], Value::Null);
}
#[test]
fn mapping_error_new_and_display() {
let issue = MappingIssue {
direction: "request".into(),
reason: "unknown_key".into(),
attribute_name: "foo".into(),
attribute_value: Some(json!("bar")),
object_index: None,
qualifier: Some("queue".into()),
};
let error = MappingError::new(vec![issue]);
let display = format!("{error}");
assert!(display.contains("1 issue(s)"));
assert!(display.contains("unknown_key"));
}
#[test]
fn mapping_error_to_payload() {
let issue = MappingIssue {
direction: "request".into(),
reason: "r".into(),
attribute_name: "a".into(),
attribute_value: None,
object_index: None,
qualifier: None,
};
let error = MappingError::new(vec![issue]);
let payload = error.to_payload();
assert_eq!(payload.len(), 1);
assert_eq!(payload[0]["direction"], json!("request"));
}
#[test]
fn mapping_error_is_error_trait() {
let error = MappingError::new(vec![]);
let _: &dyn std::error::Error = &error;
}
#[test]
fn build_mapping_message_empty() {
let msg = build_mapping_message(&[]);
assert_eq!(msg, "Mapping failed with no issues reported.");
}
#[test]
fn build_mapping_message_single_issue() {
let issue = MappingIssue {
direction: "request".into(),
reason: "unknown_key".into(),
attribute_name: "foo".into(),
attribute_value: Some(json!("bar")),
object_index: Some(0),
qualifier: Some("queue".into()),
};
let msg = build_mapping_message(&[issue]);
assert!(msg.contains("1 issue(s)"));
assert!(msg.contains("index=0"));
assert!(msg.contains("qualifier=queue"));
}
#[test]
fn build_mapping_message_multi_issue() {
let issues = vec![
MappingIssue {
direction: "request".into(),
reason: "unknown_key".into(),
attribute_name: "a".into(),
attribute_value: None,
object_index: None,
qualifier: None,
},
MappingIssue {
direction: "response".into(),
reason: "unknown_value".into(),
attribute_name: "b".into(),
attribute_value: Some(json!(42)),
object_index: Some(1),
qualifier: Some("channel".into()),
},
];
let msg = build_mapping_message(&issues);
assert!(msg.contains("2 issue(s)"));
assert!(msg.contains("index=-"));
assert!(msg.contains("qualifier=-"));
assert!(msg.contains("index=1"));
assert!(msg.contains("qualifier=channel"));
assert!(msg.contains("value=-"));
assert!(msg.contains("value=42"));
}
#[test]
fn mq_rest_error_transport_display() {
let client = reqwest::blocking::Client::new();
let err = client.get("http://[::1]:0/bad").send().unwrap_err();
let mq_err = MqRestError::Transport {
url: "http://test".into(),
source: err,
};
let display = format!("{mq_err}");
assert!(display.contains("http://test"));
}
#[test]
fn mq_rest_error_response_display() {
let err = MqRestError::Response {
message: "bad json".into(),
response_text: Some("raw".into()),
};
assert_eq!(format!("{err}"), "bad json");
}
#[test]
fn mq_rest_error_auth_display() {
let err = MqRestError::Auth {
url: "https://host/login".into(),
status_code: Some(401),
message: "auth failed".into(),
};
assert_eq!(format!("{err}"), "auth failed");
}
#[test]
fn mq_rest_error_command_display() {
let err = MqRestError::Command {
payload: HashMap::new(),
status_code: Some(200),
message: "command failed".into(),
};
assert_eq!(format!("{err}"), "command failed");
}
#[test]
fn mq_rest_error_timeout_display() {
let err = MqRestError::Timeout {
name: "MY.CHANNEL".into(),
operation: "start".into(),
elapsed: 30.0,
message: "timed out".into(),
};
assert_eq!(format!("{err}"), "timed out");
}
#[test]
fn mq_rest_error_invalid_config_display() {
let err = MqRestError::InvalidConfig {
message: "timeout must be positive".into(),
};
assert_eq!(format!("{err}"), "timeout must be positive");
}
#[test]
fn mq_rest_error_mapping_display() {
let mapping_err = MappingError::new(vec![]);
let err = MqRestError::Mapping(mapping_err);
let display = format!("{err}");
assert!(display.contains("no issues reported"));
}
#[test]
fn mq_rest_error_mapping_from() {
let mapping_err = MappingError::new(vec![]);
let err: MqRestError = mapping_err.into();
assert!(format!("{err:?}").starts_with("Mapping"));
}
}