Skip to main content

vectorless/client/
types.rs

1// Copyright (c) 2026 vectorless developers
2// SPDX-License-Identifier: Apache-2.0
3
4//! Public API types for the client module.
5//!
6//! This module contains all types exposed in the public API.
7
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10
11use crate::document::DocumentTree;
12use crate::parser::DocumentFormat;
13
14// ============================================================
15// Document Types
16// ============================================================
17
18/// An indexed document with its tree structure and metadata.
19#[derive(Debug, Clone)]
20pub struct IndexedDocument {
21    /// Unique document identifier.
22    pub id: String,
23
24    /// Document format.
25    pub format: DocumentFormat,
26
27    /// Document name/title.
28    pub name: String,
29
30    /// Document description (generated by LLM).
31    pub description: Option<String>,
32
33    /// Source file path.
34    pub source_path: Option<PathBuf>,
35
36    /// Page count (for PDFs).
37    pub page_count: Option<usize>,
38
39    /// Line count (for text files).
40    pub line_count: Option<usize>,
41
42    /// The document tree structure.
43    pub tree: Option<DocumentTree>,
44
45    /// Per-page content (for PDFs).
46    pub pages: Vec<PageContent>,
47}
48
49impl IndexedDocument {
50    /// Create a new indexed document.
51    pub fn new(id: impl Into<String>, format: DocumentFormat) -> Self {
52        Self {
53            id: id.into(),
54            format,
55            name: String::new(),
56            description: None,
57            source_path: None,
58            page_count: None,
59            line_count: None,
60            tree: None,
61            pages: Vec::new(),
62        }
63    }
64
65    /// Set the document name.
66    pub fn with_name(mut self, name: impl Into<String>) -> Self {
67        self.name = name.into();
68        self
69    }
70
71    /// Set the document description.
72    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
73        self.description = Some(desc.into());
74        self
75    }
76
77    /// Set the source path.
78    pub fn with_source_path(mut self, path: impl Into<PathBuf>) -> Self {
79        self.source_path = Some(path.into());
80        self
81    }
82
83    /// Set the page count.
84    pub fn with_page_count(mut self, count: usize) -> Self {
85        self.page_count = Some(count);
86        self
87    }
88
89    /// Set the line count.
90    pub fn with_line_count(mut self, count: usize) -> Self {
91        self.line_count = Some(count);
92        self
93    }
94
95    /// Set the document tree.
96    pub fn with_tree(mut self, tree: DocumentTree) -> Self {
97        self.tree = Some(tree);
98        self
99    }
100
101    /// Add a page content.
102    pub fn add_page(&mut self, page: usize, content: impl Into<String>) {
103        self.pages.push(PageContent {
104            page,
105            content: content.into(),
106        });
107    }
108
109    /// Check if the tree is loaded.
110    pub fn is_loaded(&self) -> bool {
111        self.tree.is_some()
112    }
113}
114
115/// Content for a single page.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct PageContent {
118    /// Page number (1-based).
119    pub page: usize,
120
121    /// Page text content.
122    pub content: String,
123}
124
125// ============================================================
126// Index Types
127// ============================================================
128
129/// Document indexing behavior mode.
130///
131/// Controls how the indexer handles existing documents and re-indexing.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum IndexMode {
134    /// Default mode - skip if already indexed.
135    ///
136    /// If a document with the same source has already been indexed,
137    /// the operation is skipped and the existing document ID is returned.
138    #[default]
139    Default,
140
141    /// Force re-indexing.
142    ///
143    /// Always re-index the document, even if it has been indexed before.
144    /// A new document ID is generated.
145    Force,
146
147    /// Incremental mode - only re-index changed files.
148    ///
149    /// Re-index only if the file has been modified since the last index.
150    /// For content/bytes sources, this behaves like [`IndexMode::Default`].
151    Incremental,
152}
153
154/// Options for indexing a document.
155#[derive(Debug, Clone)]
156pub struct IndexOptions {
157    /// Indexing mode.
158    pub mode: IndexMode,
159
160    /// Whether to generate summaries using LLM.
161    pub generate_summaries: bool,
162
163    /// Whether to include node text in the tree.
164    pub include_text: bool,
165
166    /// Whether to generate node IDs.
167    pub generate_ids: bool,
168
169    /// Whether to generate document description.
170    pub generate_description: bool,
171}
172
173impl Default for IndexOptions {
174    fn default() -> Self {
175        Self {
176            mode: IndexMode::Default,
177            generate_summaries: false,
178            include_text: true,
179            generate_ids: true,
180            generate_description: false,
181        }
182    }
183}
184
185impl IndexOptions {
186    /// Create new index options with defaults.
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    /// Enable summary generation.
192    pub fn with_summaries(mut self) -> Self {
193        self.generate_summaries = true;
194        self
195    }
196
197    /// Enable document description generation.
198    pub fn with_description(mut self) -> Self {
199        self.generate_description = true;
200        self
201    }
202
203    /// Set the indexing mode.
204    ///
205    /// # Modes
206    ///
207    /// - [`IndexMode::Default`] - Skip if already indexed
208    /// - [`IndexMode::Force`] - Always re-index
209    /// - [`IndexMode::Incremental`] - Only re-index changed files
210    pub fn with_mode(mut self, mode: IndexMode) -> Self {
211        self.mode = mode;
212        self
213    }
214}
215
216// ============================================================
217// Query Types
218// ============================================================
219
220/// Result of a document query.
221#[derive(Debug, Clone)]
222pub struct QueryResult {
223    /// The document ID.
224    pub doc_id: String,
225
226    /// Matching node IDs.
227    pub node_ids: Vec<String>,
228
229    /// Retrieved content.
230    pub content: String,
231
232    /// Relevance score.
233    pub score: f32,
234}
235
236impl QueryResult {
237    /// Create a new query result.
238    pub fn new(doc_id: impl Into<String>) -> Self {
239        Self {
240            doc_id: doc_id.into(),
241            node_ids: Vec::new(),
242            content: String::new(),
243            score: 0.0,
244        }
245    }
246
247    /// Check if the result is empty.
248    pub fn is_empty(&self) -> bool {
249        self.node_ids.is_empty()
250    }
251
252    /// Get the number of results.
253    pub fn len(&self) -> usize {
254        self.node_ids.len()
255    }
256}
257
258// ============================================================
259// Document Info Types
260// ============================================================
261
262/// Document info for listing.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct DocumentInfo {
265    /// Document ID.
266    pub id: String,
267
268    /// Document name.
269    pub name: String,
270
271    /// Document format.
272    pub format: String,
273
274    /// Document description.
275    pub description: Option<String>,
276
277    /// Page count (for PDFs).
278    pub page_count: Option<usize>,
279
280    /// Line count (for text files).
281    pub line_count: Option<usize>,
282}
283
284impl DocumentInfo {
285    /// Create a new document info.
286    pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
287        Self {
288            id: id.into(),
289            name: name.into(),
290            format: String::new(),
291            description: None,
292            page_count: None,
293            line_count: None,
294        }
295    }
296
297    /// Set the format.
298    pub fn with_format(mut self, format: impl Into<String>) -> Self {
299        self.format = format.into();
300        self
301    }
302}
303
304// ============================================================
305// Error Types
306// ============================================================
307
308/// Client error types.
309#[derive(Debug, Clone, thiserror::Error)]
310pub enum ClientError {
311    /// Document not found.
312    #[error("Document not found: {0}")]
313    NotFound(String),
314
315    /// Invalid operation.
316    #[error("Invalid operation: {0}")]
317    InvalidOperation(String),
318
319    /// Configuration error.
320    #[error("Configuration error: {0}")]
321    Config(String),
322
323    /// Timeout error.
324    #[error("Operation timed out")]
325    Timeout,
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_indexed_document() {
334        let doc = IndexedDocument::new("doc-1", DocumentFormat::Markdown)
335            .with_name("Test Document")
336            .with_description("A test document");
337
338        assert_eq!(doc.id, "doc-1");
339        assert_eq!(doc.name, "Test Document");
340        assert!(doc.tree.is_none());
341    }
342
343    #[test]
344    fn test_index_options() {
345        let options = IndexOptions::new()
346            .with_summaries()
347            .with_mode(IndexMode::Force);
348
349        assert!(options.generate_summaries);
350        assert_eq!(options.mode, IndexMode::Force);
351    }
352
353    #[test]
354    fn test_query_result() {
355        let result = QueryResult::new("doc-1");
356        assert!(result.is_empty());
357        assert_eq!(result.len(), 0);
358    }
359
360    #[test]
361    fn test_document_info() {
362        let info = DocumentInfo::new("doc-1", "Test").with_format("markdown");
363
364        assert_eq!(info.id, "doc-1");
365        assert_eq!(info.format, "markdown");
366    }
367}