kimun_notes/server_client/
dto.rs1use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Serialize)]
8pub struct IndexDocsRequest {
9 pub vault_id: String,
10 pub docs: Vec<WireDoc>,
11}
12
13#[derive(Debug, Clone, Serialize)]
15pub struct WireDoc {
16 pub path: String,
17 pub hash: String,
18 pub sections: Vec<WireSection>,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct WireSection {
23 pub title: String,
24 pub text: String,
25}
26
27#[derive(Debug, Serialize)]
29pub struct DeleteRequest {
30 pub vault_id: String,
31 pub paths: Vec<String>,
32}
33
34#[derive(Debug, Clone, Serialize)]
36pub struct HistoryTurn {
37 pub question: String,
38 pub answer: String,
39}
40
41#[derive(Debug, Serialize)]
43pub struct QueryRequest {
44 pub vault_id: String,
45 pub query: String,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub context_size: Option<String>,
48 #[serde(skip_serializing_if = "Vec::is_empty")]
49 pub history: Vec<HistoryTurn>,
50}
51
52#[derive(Debug, Deserialize)]
53pub struct EmbeddingsResponse {
54 pub chunks: Vec<ChunkResult>,
55}
56
57#[derive(Debug, Clone, Deserialize)]
58pub struct ChunkResult {
59 pub path: String,
60 pub title: String,
61 pub date: Option<String>,
62 pub content: String,
63 pub hash: String,
64 pub similarity_score: f64,
65 #[serde(default)]
71 pub ordinal: usize,
72}
73
74#[derive(Debug, Clone, Deserialize)]
76pub struct Health {
77 pub status: String,
78 #[serde(default)]
79 pub reranker: bool,
80 #[serde(default)]
84 pub embedder: Option<String>,
85 #[serde(default)]
91 pub llm_provider: Option<String>,
92 #[serde(default)]
93 pub auth_required: bool,
94}
95
96#[derive(Debug, Deserialize)]
98pub struct JobAccepted {
99 pub job_id: String,
100}
101
102#[derive(Debug, Deserialize)]
104pub struct JobStatus {
105 pub status: String,
106 pub result: Option<serde_json::Value>,
107 pub error: Option<String>,
108}
109
110#[derive(Debug, Deserialize)]
112pub struct AnswerResult {
113 pub answer: String,
114 pub sources: Vec<ChunkResult>,
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn query_request_omits_empty_history() {
123 let req = QueryRequest {
124 vault_id: "v".into(),
125 query: "q".into(),
126 context_size: None,
127 history: vec![],
128 };
129 let json = serde_json::to_string(&req).unwrap();
130 assert!(
131 !json.contains("history"),
132 "empty history must not hit the wire: {json}"
133 );
134 }
135
136 #[test]
137 fn query_request_serializes_history_pairs() {
138 let req = QueryRequest {
139 vault_id: "v".into(),
140 query: "q".into(),
141 context_size: None,
142 history: vec![HistoryTurn {
143 question: "q1".into(),
144 answer: "a1".into(),
145 }],
146 };
147 let json = serde_json::to_string(&req).unwrap();
148 assert!(json.contains(r#""history":[{"question":"q1","answer":"a1"}]"#));
149 }
150
151 #[test]
152 fn health_parses_semantic_only_null_llm_provider() {
153 let json = r#"{"status":"ok","reranker":true,"llm_provider":null,"auth_required":false}"#;
157 let health: Health = serde_json::from_str(json).expect("must parse null llm_provider");
158 assert_eq!(health.status, "ok");
159 assert!(health.llm_provider.is_none());
160 }
161
162 #[test]
163 fn health_parses_configured_llm_provider() {
164 let json =
165 r#"{"status":"ok","reranker":false,"llm_provider":"gemini","auth_required":true}"#;
166 let health: Health = serde_json::from_str(json).unwrap();
167 assert_eq!(health.llm_provider.as_deref(), Some("gemini"));
168 assert!(health.auth_required);
169 }
170
171 #[test]
172 fn health_parses_unconfigured_null_embedder() {
173 let json = r#"{"status":"ok","reranker":true,"embedder":null,"llm_provider":null,"auth_required":false}"#;
175 let health: Health = serde_json::from_str(json).expect("must parse null embedder");
176 assert!(health.embedder.is_none());
177 }
178
179 #[test]
180 fn health_parses_configured_embedder() {
181 let json = r#"{"status":"ok","reranker":true,"embedder":"fastembed","llm_provider":null,"auth_required":false}"#;
182 let health: Health = serde_json::from_str(json).unwrap();
183 assert_eq!(health.embedder.as_deref(), Some("fastembed"));
184 }
185
186 #[test]
187 fn health_tolerates_missing_embedder_field() {
188 let json =
190 r#"{"status":"ok","reranker":true,"llm_provider":"gemini","auth_required":false}"#;
191 let health: Health = serde_json::from_str(json).unwrap();
192 assert!(health.embedder.is_none());
193 }
194
195 #[test]
196 fn chunk_result_parses_the_ordinal_when_present() {
197 let json = r#"{"path":"a.md","title":"t","date":null,"content":"c","hash":"h","similarity_score":0.9,"ordinal":3}"#;
198 let c: ChunkResult = serde_json::from_str(json).unwrap();
199 assert_eq!(c.ordinal, 3);
200 }
201
202 #[test]
203 fn chunk_result_defaults_ordinal_to_zero_when_absent() {
204 let json = r#"{"path":"a.md","title":"t","date":null,"content":"c","hash":"h","similarity_score":0.9}"#;
207 let c: ChunkResult = serde_json::from_str(json).unwrap();
208 assert_eq!(c.ordinal, 0);
209 }
210}