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 #[serde(default)]
97 pub version: Option<String>,
98 #[serde(default)]
101 pub latest_version: Option<String>,
102}
103
104#[derive(Debug, Deserialize)]
106pub struct JobAccepted {
107 pub job_id: String,
108}
109
110#[derive(Debug, Deserialize)]
112pub struct JobStatus {
113 pub status: String,
114 pub result: Option<serde_json::Value>,
115 pub error: Option<String>,
116}
117
118#[derive(Debug, Deserialize)]
120pub struct AnswerResult {
121 pub answer: String,
122 pub sources: Vec<ChunkResult>,
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn query_request_omits_empty_history() {
131 let req = QueryRequest {
132 vault_id: "v".into(),
133 query: "q".into(),
134 context_size: None,
135 history: vec![],
136 };
137 let json = serde_json::to_string(&req).unwrap();
138 assert!(
139 !json.contains("history"),
140 "empty history must not hit the wire: {json}"
141 );
142 }
143
144 #[test]
145 fn query_request_serializes_history_pairs() {
146 let req = QueryRequest {
147 vault_id: "v".into(),
148 query: "q".into(),
149 context_size: None,
150 history: vec![HistoryTurn {
151 question: "q1".into(),
152 answer: "a1".into(),
153 }],
154 };
155 let json = serde_json::to_string(&req).unwrap();
156 assert!(json.contains(r#""history":[{"question":"q1","answer":"a1"}]"#));
157 }
158
159 #[test]
160 fn health_parses_semantic_only_null_llm_provider() {
161 let json = r#"{"status":"ok","reranker":true,"llm_provider":null,"auth_required":false}"#;
165 let health: Health = serde_json::from_str(json).expect("must parse null llm_provider");
166 assert_eq!(health.status, "ok");
167 assert!(health.llm_provider.is_none());
168 }
169
170 #[test]
171 fn health_parses_configured_llm_provider() {
172 let json =
173 r#"{"status":"ok","reranker":false,"llm_provider":"gemini","auth_required":true}"#;
174 let health: Health = serde_json::from_str(json).unwrap();
175 assert_eq!(health.llm_provider.as_deref(), Some("gemini"));
176 assert!(health.auth_required);
177 }
178
179 #[test]
180 fn health_parses_unconfigured_null_embedder() {
181 let json = r#"{"status":"ok","reranker":true,"embedder":null,"llm_provider":null,"auth_required":false}"#;
183 let health: Health = serde_json::from_str(json).expect("must parse null embedder");
184 assert!(health.embedder.is_none());
185 }
186
187 #[test]
188 fn health_parses_configured_embedder() {
189 let json = r#"{"status":"ok","reranker":true,"embedder":"fastembed","llm_provider":null,"auth_required":false}"#;
190 let health: Health = serde_json::from_str(json).unwrap();
191 assert_eq!(health.embedder.as_deref(), Some("fastembed"));
192 }
193
194 #[test]
195 fn health_tolerates_missing_embedder_field() {
196 let json =
198 r#"{"status":"ok","reranker":true,"llm_provider":"gemini","auth_required":false}"#;
199 let health: Health = serde_json::from_str(json).unwrap();
200 assert!(health.embedder.is_none());
201 }
202
203 #[test]
204 fn chunk_result_parses_the_ordinal_when_present() {
205 let json = r#"{"path":"a.md","title":"t","date":null,"content":"c","hash":"h","similarity_score":0.9,"ordinal":3}"#;
206 let c: ChunkResult = serde_json::from_str(json).unwrap();
207 assert_eq!(c.ordinal, 3);
208 }
209
210 #[test]
211 fn chunk_result_defaults_ordinal_to_zero_when_absent() {
212 let json = r#"{"path":"a.md","title":"t","date":null,"content":"c","hash":"h","similarity_score":0.9}"#;
215 let c: ChunkResult = serde_json::from_str(json).unwrap();
216 assert_eq!(c.ordinal, 0);
217 }
218}