use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize)]
pub struct IndexDocsRequest {
pub vault_id: String,
pub docs: Vec<WireDoc>,
}
#[derive(Debug, Clone, Serialize)]
pub struct WireDoc {
pub path: String,
pub hash: String,
pub sections: Vec<WireSection>,
}
#[derive(Debug, Clone, Serialize)]
pub struct WireSection {
pub title: String,
pub text: String,
}
#[derive(Debug, Serialize)]
pub struct DeleteRequest {
pub vault_id: String,
pub paths: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct HistoryTurn {
pub question: String,
pub answer: String,
}
#[derive(Debug, Serialize)]
pub struct QueryRequest {
pub vault_id: String,
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_size: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub history: Vec<HistoryTurn>,
}
#[derive(Debug, Deserialize)]
pub struct EmbeddingsResponse {
pub chunks: Vec<ChunkResult>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChunkResult {
pub path: String,
pub title: String,
pub date: Option<String>,
pub content: String,
pub hash: String,
pub similarity_score: f64,
#[serde(default)]
pub ordinal: usize,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Health {
pub status: String,
#[serde(default)]
pub reranker: bool,
#[serde(default)]
pub embedder: Option<String>,
#[serde(default)]
pub llm_provider: Option<String>,
#[serde(default)]
pub auth_required: bool,
}
#[derive(Debug, Deserialize)]
pub struct JobAccepted {
pub job_id: String,
}
#[derive(Debug, Deserialize)]
pub struct JobStatus {
pub status: String,
pub result: Option<serde_json::Value>,
pub error: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct AnswerResult {
pub answer: String,
pub sources: Vec<ChunkResult>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_request_omits_empty_history() {
let req = QueryRequest {
vault_id: "v".into(),
query: "q".into(),
context_size: None,
history: vec![],
};
let json = serde_json::to_string(&req).unwrap();
assert!(
!json.contains("history"),
"empty history must not hit the wire: {json}"
);
}
#[test]
fn query_request_serializes_history_pairs() {
let req = QueryRequest {
vault_id: "v".into(),
query: "q".into(),
context_size: None,
history: vec![HistoryTurn {
question: "q1".into(),
answer: "a1".into(),
}],
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains(r#""history":[{"question":"q1","answer":"a1"}]"#));
}
#[test]
fn health_parses_semantic_only_null_llm_provider() {
let json = r#"{"status":"ok","reranker":true,"llm_provider":null,"auth_required":false}"#;
let health: Health = serde_json::from_str(json).expect("must parse null llm_provider");
assert_eq!(health.status, "ok");
assert!(health.llm_provider.is_none());
}
#[test]
fn health_parses_configured_llm_provider() {
let json =
r#"{"status":"ok","reranker":false,"llm_provider":"gemini","auth_required":true}"#;
let health: Health = serde_json::from_str(json).unwrap();
assert_eq!(health.llm_provider.as_deref(), Some("gemini"));
assert!(health.auth_required);
}
#[test]
fn health_parses_unconfigured_null_embedder() {
let json = r#"{"status":"ok","reranker":true,"embedder":null,"llm_provider":null,"auth_required":false}"#;
let health: Health = serde_json::from_str(json).expect("must parse null embedder");
assert!(health.embedder.is_none());
}
#[test]
fn health_parses_configured_embedder() {
let json = r#"{"status":"ok","reranker":true,"embedder":"fastembed","llm_provider":null,"auth_required":false}"#;
let health: Health = serde_json::from_str(json).unwrap();
assert_eq!(health.embedder.as_deref(), Some("fastembed"));
}
#[test]
fn health_tolerates_missing_embedder_field() {
let json =
r#"{"status":"ok","reranker":true,"llm_provider":"gemini","auth_required":false}"#;
let health: Health = serde_json::from_str(json).unwrap();
assert!(health.embedder.is_none());
}
#[test]
fn chunk_result_parses_the_ordinal_when_present() {
let json = r#"{"path":"a.md","title":"t","date":null,"content":"c","hash":"h","similarity_score":0.9,"ordinal":3}"#;
let c: ChunkResult = serde_json::from_str(json).unwrap();
assert_eq!(c.ordinal, 3);
}
#[test]
fn chunk_result_defaults_ordinal_to_zero_when_absent() {
let json = r#"{"path":"a.md","title":"t","date":null,"content":"c","hash":"h","similarity_score":0.9}"#;
let c: ChunkResult = serde_json::from_str(json).unwrap();
assert_eq!(c.ordinal, 0);
}
}