use serde_json::Value;
const REDACT_MAX_DEPTH: usize = 16;
const SENSITIVE_KEYS: &[&str] = &[
"authorization",
"cookie",
"set-cookie",
"x-areev-system-key",
"x-axtion-service-token",
"access_token",
"refresh_token",
"bearer_token",
"id_token",
"client_secret",
"axtion_credential_id",
"api_key",
"apikey",
"secret",
"password",
"private_key",
];
fn redact_body(value: &Value, depth: usize) -> Value {
if depth > REDACT_MAX_DEPTH {
return value.clone();
}
match value {
Value::Object(map) => {
let mut out = serde_json::Map::with_capacity(map.len());
for (k, v) in map {
if SENSITIVE_KEYS.contains(&k.to_ascii_lowercase().as_str()) {
out.insert(k.clone(), Value::String("[redacted]".to_string()));
} else {
out.insert(k.clone(), redact_body(v, depth + 1));
}
}
Value::Object(out)
}
Value::Array(items) => {
Value::Array(items.iter().map(|v| redact_body(v, depth + 1)).collect())
}
other => other.clone(),
}
}
#[derive(Debug, thiserror::Error)]
pub enum AreevError {
#[error("[{code}] authentication failed: {message} (http {http_status}{rid})",
code = code.as_deref().unwrap_or("AUTH"),
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
Authentication {
code: Option<String>,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[error("[{code}] authorization failed: {message} (http {http_status}{rid})",
code = code.as_deref().unwrap_or("AUTHZ"),
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
Authorization {
code: Option<String>,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[error("[{code}] HIPAA policy violation: {message} (http {http_status}{rid})",
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
HipaaPolicyViolation {
code: String,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[error("[{code}] feature not available on this plan: {message} (http {http_status}{rid})",
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
FeatureNotAvailable {
code: String,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[error("[{code}] plan limit reached: {message} (http {http_status}{rid})",
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
PlanLimit {
code: String,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[error("[{code}] insufficient credits: {message} (http {http_status}{rid})",
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
InsufficientCredits {
code: String,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[error("[{code}] not found: {message} (http {http_status}{rid})",
code = code.as_deref().unwrap_or("NOTFOUND"),
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
NotFound {
code: Option<String>,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[error("[{code}] validation error: {message} (http {http_status}{rid})",
code = code.as_deref().unwrap_or("VALIDATION"),
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
Validation {
code: Option<String>,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[error("[{code}] rate limited: {message} (http {http_status}{rid})",
code = code.as_deref().unwrap_or("RATELIMIT"),
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
RateLimit {
code: Option<String>,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[error("[{code}] conflict: {message} (http {http_status}{rid})",
code = code.as_deref().unwrap_or("CONFLICT"),
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
Conflict {
code: Option<String>,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[error("[{code}] server error: {message} (http {http_status}{rid})",
code = code.as_deref().unwrap_or("SERVER"),
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
Server {
code: Option<String>,
http_status: u16,
message: String,
body: Value,
request_id: Option<String>,
},
#[cfg(feature = "http")]
#[error("transport error: {source}")]
Transport {
#[from]
source: reqwest::Error,
},
#[cfg(feature = "grpc")]
#[error("gRPC error: {0}")]
Grpc(#[from] tonic::Status),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("[{code}] {message} (http {http_status}{rid})",
code = code.as_deref().unwrap_or("OTHER"),
rid = request_id.as_deref().map(|r| format!(" request_id={r}")).unwrap_or_default()
)]
Other {
http_status: u16,
code: Option<String>,
message: String,
body: Value,
request_id: Option<String>,
},
}
impl AreevError {
pub fn from_response_status(
http_status: u16,
code: Option<&str>,
message: impl Into<String>,
request_id: Option<String>,
) -> Self {
Self::from_response_with_body(http_status, code, message, Value::Null, request_id)
}
pub fn from_response_with_body(
http_status: u16,
code: Option<&str>,
message: impl Into<String>,
body: Value,
request_id: Option<String>,
) -> Self {
let code_owned = code.map(|c| c.to_string());
let message = message.into();
let body = redact_body(&body, 0);
if let Some(c) = code {
match c {
"FTR-E001" => {
return AreevError::FeatureNotAvailable {
code: c.to_string(),
http_status,
message,
body,
request_id,
}
}
"FTR-E002" => {
return AreevError::PlanLimit {
code: c.to_string(),
http_status,
message,
body,
request_id,
}
}
"CRD-E003" => {
return AreevError::InsufficientCredits {
code: c.to_string(),
http_status,
message,
body,
request_id,
}
}
_ => {}
}
}
match http_status {
401 => AreevError::Authentication {
code: code_owned,
http_status,
message,
body,
request_id,
},
402 => AreevError::InsufficientCredits {
code: code_owned.unwrap_or_else(|| "CRD-E003".to_string()),
http_status,
message,
body,
request_id,
},
403 => {
if let Some(c) = code {
if c.starts_with("HRN-E04") {
return AreevError::HipaaPolicyViolation {
code: c.to_string(),
http_status,
message,
body,
request_id,
};
}
}
AreevError::Authorization {
code: code_owned,
http_status,
message,
body,
request_id,
}
}
404 => AreevError::NotFound {
code: code_owned,
http_status,
message,
body,
request_id,
},
409 => AreevError::Conflict {
code: code_owned,
http_status,
message,
body,
request_id,
},
429 => AreevError::RateLimit {
code: code_owned,
http_status,
message,
body,
request_id,
},
400 | 422 => AreevError::Validation {
code: code_owned,
http_status,
message,
body,
request_id,
},
s if (500..600).contains(&s) => AreevError::Server {
code: code_owned,
http_status,
message,
body,
request_id,
},
_ => AreevError::Other {
http_status,
code: code_owned,
message,
body,
request_id,
},
}
}
pub fn http_status(&self) -> u16 {
match self {
AreevError::Authentication { http_status, .. }
| AreevError::Authorization { http_status, .. }
| AreevError::HipaaPolicyViolation { http_status, .. }
| AreevError::FeatureNotAvailable { http_status, .. }
| AreevError::PlanLimit { http_status, .. }
| AreevError::InsufficientCredits { http_status, .. }
| AreevError::NotFound { http_status, .. }
| AreevError::Validation { http_status, .. }
| AreevError::RateLimit { http_status, .. }
| AreevError::Conflict { http_status, .. }
| AreevError::Server { http_status, .. }
| AreevError::Other { http_status, .. } => *http_status,
#[cfg(feature = "http")]
AreevError::Transport { source } => source.status().map(|s| s.as_u16()).unwrap_or(0),
#[cfg(feature = "grpc")]
AreevError::Grpc(_) => 0,
AreevError::Serialization(_) => 0,
}
}
pub fn code(&self) -> Option<&str> {
match self {
AreevError::Authentication { code, .. }
| AreevError::Authorization { code, .. }
| AreevError::NotFound { code, .. }
| AreevError::Validation { code, .. }
| AreevError::RateLimit { code, .. }
| AreevError::Conflict { code, .. }
| AreevError::Server { code, .. }
| AreevError::Other { code, .. } => code.as_deref(),
AreevError::HipaaPolicyViolation { code, .. }
| AreevError::FeatureNotAvailable { code, .. }
| AreevError::PlanLimit { code, .. }
| AreevError::InsufficientCredits { code, .. } => Some(code.as_str()),
#[cfg(feature = "http")]
AreevError::Transport { .. } => None,
#[cfg(feature = "grpc")]
AreevError::Grpc(_) => None,
AreevError::Serialization(_) => None,
}
}
pub fn request_id(&self) -> Option<&str> {
match self {
AreevError::Authentication { request_id, .. }
| AreevError::Authorization { request_id, .. }
| AreevError::NotFound { request_id, .. }
| AreevError::Validation { request_id, .. }
| AreevError::RateLimit { request_id, .. }
| AreevError::Conflict { request_id, .. }
| AreevError::Server { request_id, .. }
| AreevError::Other { request_id, .. } => request_id.as_deref(),
AreevError::HipaaPolicyViolation { request_id, .. }
| AreevError::FeatureNotAvailable { request_id, .. }
| AreevError::PlanLimit { request_id, .. }
| AreevError::InsufficientCredits { request_id, .. } => request_id.as_deref(),
#[cfg(feature = "http")]
AreevError::Transport { .. } => None,
#[cfg(feature = "grpc")]
AreevError::Grpc(_) => None,
AreevError::Serialization(_) => None,
}
}
pub fn message(&self) -> &str {
match self {
AreevError::Authentication { message, .. }
| AreevError::Authorization { message, .. }
| AreevError::NotFound { message, .. }
| AreevError::Validation { message, .. }
| AreevError::RateLimit { message, .. }
| AreevError::Conflict { message, .. }
| AreevError::Server { message, .. }
| AreevError::Other { message, .. } => message,
AreevError::HipaaPolicyViolation { message, .. }
| AreevError::FeatureNotAvailable { message, .. }
| AreevError::PlanLimit { message, .. }
| AreevError::InsufficientCredits { message, .. } => message,
#[cfg(feature = "http")]
AreevError::Transport { .. } => "transport error",
#[cfg(feature = "grpc")]
AreevError::Grpc(_) => "gRPC error",
AreevError::Serialization(_) => "serialization error",
}
}
pub fn body(&self) -> &Value {
match self {
AreevError::Authentication { body, .. }
| AreevError::Authorization { body, .. }
| AreevError::NotFound { body, .. }
| AreevError::Validation { body, .. }
| AreevError::RateLimit { body, .. }
| AreevError::Conflict { body, .. }
| AreevError::Server { body, .. }
| AreevError::Other { body, .. }
| AreevError::HipaaPolicyViolation { body, .. }
| AreevError::FeatureNotAvailable { body, .. }
| AreevError::PlanLimit { body, .. }
| AreevError::InsufficientCredits { body, .. } => body,
#[cfg(feature = "http")]
AreevError::Transport { .. } => &Value::Null,
#[cfg(feature = "grpc")]
AreevError::Grpc(_) => &Value::Null,
AreevError::Serialization(_) => &Value::Null,
}
}
pub(crate) fn is_retryable(&self) -> bool {
match self {
AreevError::Server { .. } | AreevError::RateLimit { .. } => true,
AreevError::Other { http_status, .. } => *http_status == 408,
#[cfg(feature = "http")]
AreevError::Transport { .. } => true,
_ => false,
}
}
}
pub type Result<T> = std::result::Result<T, AreevError>;