use serde::{Deserialize, Serialize};
use super::types::LlmRoute;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PrivateUnavailableReason {
NoEsp32Endpoint,
FirmwareUpgradeRequired,
PeerOffline,
PlatformNotEnabled,
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
#[error("private routing unavailable: {reason:?} (fallback_available={fallback_available})")]
pub struct PrivateUnavailableError {
pub reason: PrivateUnavailableReason,
pub fallback_available: bool,
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
#[error("invalid route_mode + execution_preference combination: route_mode={route_mode:?}, execution_preference={execution_preference:?} — {hint}")]
pub struct InvalidRouteModeCombinationError {
pub route_mode: LlmRoute,
pub execution_preference: Option<String>,
pub hint: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PrivateUnavailableBody {
pub code: String,
pub requested_route: String,
pub reason: PrivateUnavailableReason,
pub fallback_available: bool,
}
impl From<PrivateUnavailableBody> for PrivateUnavailableError {
fn from(body: PrivateUnavailableBody) -> Self {
Self {
reason: body.reason,
fallback_available: body.fallback_available,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct InvalidRouteModeCombinationBody {
pub code: String,
pub route_mode: LlmRoute,
#[serde(default)]
pub execution_preference: Option<String>,
pub hint: String,
}
impl From<InvalidRouteModeCombinationBody> for InvalidRouteModeCombinationError {
fn from(body: InvalidRouteModeCombinationBody) -> Self {
Self {
route_mode: body.route_mode,
execution_preference: body.execution_preference,
hint: body.hint,
}
}
}
pub fn map_api_error_body(bytes: &[u8]) -> Option<Box<dyn std::error::Error + Send + Sync>> {
let v: serde_json::Value = serde_json::from_slice(bytes).ok()?;
let code = v.get("code")?.as_str()?;
match code {
"api_store.private_unavailable" => {
let body: PrivateUnavailableBody = serde_json::from_value(v).ok()?;
Some(Box::new(PrivateUnavailableError::from(body)))
}
"api_store.invalid_route_mode_combination" => {
let body: InvalidRouteModeCombinationBody = serde_json::from_value(v).ok()?;
Some(Box::new(InvalidRouteModeCombinationError::from(body)))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decodes_private_unavailable() {
let body = br#"{
"code": "api_store.private_unavailable",
"requested_route": "private",
"reason": "no_esp32_endpoint",
"fallback_available": true
}"#;
let err = map_api_error_body(body).expect("typed err");
let downcast = err
.downcast_ref::<PrivateUnavailableError>()
.expect("PrivateUnavailableError");
assert_eq!(downcast.reason, PrivateUnavailableReason::NoEsp32Endpoint);
assert!(downcast.fallback_available);
}
#[test]
fn unknown_code_returns_none() {
assert!(map_api_error_body(br#"{ "code": "api_store.other" }"#).is_none());
}
#[test]
fn invalid_route_mode_combination_decodes() {
let body = br#"{
"code": "api_store.invalid_route_mode_combination",
"route_mode": "private",
"execution_preference": "network_only",
"hint": "fix it"
}"#;
let err = map_api_error_body(body).expect("typed err");
let downcast = err
.downcast_ref::<InvalidRouteModeCombinationError>()
.expect("InvalidRouteModeCombinationError");
assert_eq!(downcast.route_mode, LlmRoute::Private);
assert_eq!(downcast.execution_preference.as_deref(), Some("network_only"));
}
}