1use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13pub type CirrusResult<T> = Result<T, CirrusError>;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SalesforceError {
22 pub message: String,
24 #[serde(rename = "errorCode")]
26 pub error_code: String,
27 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub fields: Vec<String>,
30}
31
32#[derive(Debug, Error)]
34pub enum CirrusError {
35 #[error("missing required builder field: {0}")]
37 MissingField(&'static str),
38
39 #[error("failed to construct HTTP client: {0}")]
41 HttpClient(#[source] reqwest::Error),
42
43 #[error("HTTP request failed: {0}")]
45 Http(#[from] reqwest::Error),
46
47 #[error("Salesforce API error (status {status}): {}", display_errors(.errors, .raw))]
51 Api {
52 status: u16,
54 errors: Vec<SalesforceError>,
57 raw: Option<String>,
60 },
61
62 #[error("OAuth error: {error}{}", .error_description.as_deref().map(|d| format!(" — {d}")).unwrap_or_default())]
65 OAuth {
66 error: String,
67 error_description: Option<String>,
68 },
69
70 #[error("authentication failed: {0}")]
73 Auth(String),
74
75 #[error("serialization error: {0}")]
77 Serialization(#[from] serde_json::Error),
78
79 #[error("invalid URL: {0}")]
81 Url(#[from] url::ParseError),
82
83 #[error("invalid header value: {0}")]
85 InvalidHeader(String),
86
87 #[error("invalid response: {0}")]
89 InvalidResponse(String),
90}
91
92fn display_errors(errors: &[SalesforceError], raw: &Option<String>) -> String {
93 if errors.is_empty() {
94 return raw
95 .as_deref()
96 .map(|r| format!("<unparsed body: {r}>"))
97 .unwrap_or_else(|| "<no body>".to_string());
98 }
99 errors
100 .iter()
101 .map(|e| {
102 if e.fields.is_empty() {
103 format!("[{}] {}", e.error_code, e.message)
104 } else {
105 format!(
106 "[{}] {} (fields: {})",
107 e.error_code,
108 e.message,
109 e.fields.join(", ")
110 )
111 }
112 })
113 .collect::<Vec<_>>()
114 .join("; ")
115}
116
117#[cfg(test)]
118#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn display_errors_formats_single_error() {
124 let err = CirrusError::Api {
125 status: 400,
126 errors: vec![SalesforceError {
127 message: "Required field missing".to_string(),
128 error_code: "REQUIRED_FIELD_MISSING".to_string(),
129 fields: vec!["Name".to_string()],
130 }],
131 raw: None,
132 };
133 let msg = err.to_string();
134 assert!(msg.contains("400"));
135 assert!(msg.contains("REQUIRED_FIELD_MISSING"));
136 assert!(msg.contains("Name"));
137 }
138
139 #[test]
140 fn display_errors_falls_back_to_raw_body() {
141 let err = CirrusError::Api {
142 status: 500,
143 errors: vec![],
144 raw: Some("Internal Server Error".to_string()),
145 };
146 let msg = err.to_string();
147 assert!(msg.contains("500"));
148 assert!(msg.contains("Internal Server Error"));
149 }
150
151 #[test]
152 fn salesforce_error_deserializes_canonical_shape() {
153 let json = r#"[{"message":"bad","errorCode":"INVALID_FIELD","fields":["Foo"]}]"#;
154 let parsed: Vec<SalesforceError> = serde_json::from_str(json).unwrap();
155 assert_eq!(parsed.len(), 1);
156 assert_eq!(parsed[0].error_code, "INVALID_FIELD");
157 assert_eq!(parsed[0].fields, vec!["Foo"]);
158 }
159
160 #[test]
161 fn salesforce_error_handles_missing_fields() {
162 let json = r#"[{"message":"bad","errorCode":"INVALID_SESSION_ID"}]"#;
163 let parsed: Vec<SalesforceError> = serde_json::from_str(json).unwrap();
164 assert_eq!(parsed.len(), 1);
165 assert!(parsed[0].fields.is_empty());
166 }
167}