use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ServiceState {
Available,
NotConfigured,
Unreachable {
message: String,
},
}
impl ServiceState {
pub fn is_available(&self) -> bool {
matches!(self, Self::Available)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetupIssue {
pub object_name: String,
pub store: String,
pub guidance: Guidance,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Guidance {
pub problem: String,
pub action: String,
pub command_hint: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, thiserror::Error)]
pub enum CoreError {
#[error("invalid configuration: {0}")]
InvalidConfig(String),
#[error("required service unavailable: {service} — {message}")]
RequiredServiceUnavailable {
service: String,
message: String,
},
#[error("write failed: {0}")]
WriteFailed(String),
#[error("corrupted input: {0}")]
CorruptedInput(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DegradationKind {
ServiceUnavailable {
service: String,
state: ServiceState,
},
PartialSearch {
available: Vec<String>,
unavailable: Vec<String>,
},
StaleIndex {
paths: Vec<String>,
},
SkippedArtifacts {
count: usize,
reason: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn optional_service_degradation_is_not_fatal() {
let unconfigured = ServiceState::NotConfigured;
let unreachable = ServiceState::Unreachable {
message: "connection refused".to_string(),
};
assert!(!unconfigured.is_available());
assert!(!unreachable.is_available());
let degradation = DegradationKind::ServiceUnavailable {
service: "qdrant".to_string(),
state: unconfigured,
};
let fatal = CoreError::RequiredServiceUnavailable {
service: "postgres".to_string(),
message: "hub is required for writes".to_string(),
};
assert!(matches!(
degradation,
DegradationKind::ServiceUnavailable {
service,
state: ServiceState::NotConfigured
} if service == "qdrant"
));
assert_eq!(
fatal.to_string(),
"required service unavailable: postgres — hub is required for writes"
);
}
#[test]
fn guidance_is_structured() {
let guidance = Guidance {
problem: "BM25 index missing".to_string(),
action: "run attached setup validation".to_string(),
command_hint: Some("gobby setup validate".to_string()),
};
assert_eq!(guidance.problem, "BM25 index missing");
assert_eq!(guidance.action, "run attached setup validation");
assert_eq!(
guidance.command_hint.as_deref(),
Some("gobby setup validate")
);
}
#[test]
fn core_error_serialization_roundtrip() {
let invalid_config = CoreError::InvalidConfig("missing project id".to_string());
let encoded = serde_json::to_string(&invalid_config).expect("serialize invalid config");
let decoded: CoreError =
serde_json::from_str(&encoded).expect("deserialize invalid config");
assert!(matches!(
decoded,
CoreError::InvalidConfig(message) if message == "missing project id"
));
let unavailable = CoreError::RequiredServiceUnavailable {
service: "postgres".to_string(),
message: "connection refused".to_string(),
};
let encoded = serde_json::to_string(&unavailable).expect("serialize unavailable");
let decoded: CoreError = serde_json::from_str(&encoded).expect("deserialize unavailable");
assert!(matches!(
decoded,
CoreError::RequiredServiceUnavailable { service, message }
if service == "postgres" && message == "connection refused"
));
}
}