Skip to main content

research_master/models/
search.rs

1//! Search request and response models.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Sort order for search results
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum SortOrder {
10    Ascending,
11    Descending,
12}
13
14/// Sort field for search results
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub enum SortBy {
18    Relevance,
19    Date,
20    CitationCount,
21    Title,
22    Author,
23}
24
25/// Search query parameters
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SearchQuery {
28    /// Main search query string
29    pub query: String,
30
31    /// Maximum number of results to return
32    pub max_results: usize,
33
34    /// Year filter (single year, range like "2018-2022", or "2010-" for from, "-2015" for until)
35    pub year: Option<String>,
36
37    /// Sort by field
38    pub sort_by: Option<SortBy>,
39
40    /// Sort order
41    pub sort_order: Option<SortOrder>,
42
43    /// Field-specific filters
44    pub filters: HashMap<String, String>,
45
46    /// Author name for author-specific search
47    pub author: Option<String>,
48
49    /// Category/subject filter
50    pub category: Option<String>,
51
52    /// Whether to fetch detailed information (slower but more complete)
53    pub fetch_details: bool,
54}
55
56impl Default for SearchQuery {
57    fn default() -> Self {
58        Self {
59            query: String::new(),
60            max_results: 10,
61            year: None,
62            sort_by: None,
63            sort_order: None,
64            filters: HashMap::new(),
65            author: None,
66            category: None,
67            fetch_details: true,
68        }
69    }
70}
71
72impl SearchQuery {
73    /// Create a new search query
74    pub fn new(query: impl Into<String>) -> Self {
75        Self {
76            query: query.into(),
77            ..Default::default()
78        }
79    }
80
81    /// Set maximum results
82    pub fn max_results(mut self, max: usize) -> Self {
83        self.max_results = max;
84        self
85    }
86
87    /// Set year filter
88    pub fn year(mut self, year: impl Into<String>) -> Self {
89        self.year = Some(year.into());
90        self
91    }
92
93    /// Set sort by
94    pub fn sort_by(mut self, sort: SortBy) -> Self {
95        self.sort_by = Some(sort);
96        self
97    }
98
99    /// Set sort order
100    pub fn sort_order(mut self, order: SortOrder) -> Self {
101        self.sort_order = Some(order);
102        self
103    }
104
105    /// Add a filter
106    pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
107        self.filters.insert(key.into(), value.into());
108        self
109    }
110
111    /// Set author filter
112    pub fn author(mut self, author: impl Into<String>) -> Self {
113        self.author = Some(author.into());
114        self
115    }
116
117    /// Set category filter
118    pub fn category(mut self, category: impl Into<String>) -> Self {
119        self.category = Some(category.into());
120        self
121    }
122
123    /// Enable/disable detailed fetching
124    pub fn fetch_details(mut self, fetch: bool) -> Self {
125        self.fetch_details = fetch;
126        self
127    }
128}
129
130/// Request for downloading a paper
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct DownloadRequest {
133    /// Paper ID (source-specific)
134    pub paper_id: String,
135
136    /// Where to save the PDF
137    pub save_path: String,
138
139    /// Optional DOI
140    pub doi: Option<String>,
141}
142
143impl DownloadRequest {
144    /// Create a new download request
145    pub fn new(paper_id: impl Into<String>, save_path: impl Into<String>) -> Self {
146        Self {
147            paper_id: paper_id.into(),
148            save_path: save_path.into(),
149            doi: None,
150        }
151    }
152
153    /// Set the DOI
154    pub fn doi(mut self, doi: impl Into<String>) -> Self {
155        self.doi = Some(doi.into());
156        self
157    }
158}
159
160/// Request for reading/parsing a paper
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ReadRequest {
163    /// Paper ID (source-specific)
164    pub paper_id: String,
165
166    /// Path where the PDF is saved (or will be saved)
167    pub save_path: String,
168
169    /// Whether to download if not found
170    pub download_if_missing: bool,
171}
172
173impl ReadRequest {
174    /// Create a new read request
175    pub fn new(paper_id: impl Into<String>, save_path: impl Into<String>) -> Self {
176        Self {
177            paper_id: paper_id.into(),
178            save_path: save_path.into(),
179            download_if_missing: true,
180        }
181    }
182
183    /// Set whether to download if missing
184    pub fn download_if_missing(mut self, download: bool) -> Self {
185        self.download_if_missing = download;
186        self
187    }
188}
189
190/// Request for getting citations/references
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct CitationRequest {
193    /// Paper ID (source-specific)
194    pub paper_id: String,
195
196    /// Maximum results
197    pub max_results: usize,
198}
199
200impl CitationRequest {
201    /// Create a new citation request
202    pub fn new(paper_id: impl Into<String>) -> Self {
203        Self {
204            paper_id: paper_id.into(),
205            max_results: 20,
206        }
207    }
208
209    /// Set max results
210    pub fn max_results(mut self, max: usize) -> Self {
211        self.max_results = max;
212        self
213    }
214}
215
216/// Search response containing papers and metadata
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct SearchResponse {
219    /// Papers found
220    pub papers: Vec<crate::models::Paper>,
221
222    /// Total number of results (may be more than returned)
223    pub total_results: Option<usize>,
224
225    /// Source of the results
226    pub source: String,
227
228    /// Query that was executed
229    pub query: String,
230
231    /// Whether more results are available
232    pub has_more: bool,
233}
234
235impl SearchResponse {
236    /// Create a new search response
237    pub fn new(
238        papers: Vec<crate::models::Paper>,
239        source: impl Into<String>,
240        query: impl Into<String>,
241    ) -> Self {
242        Self {
243            papers,
244            total_results: None,
245            source: source.into(),
246            query: query.into(),
247            has_more: false,
248        }
249    }
250
251    /// Set total results
252    pub fn total_results(mut self, total: usize) -> Self {
253        self.total_results = Some(total);
254        self
255    }
256
257    /// Set has_more flag
258    pub fn has_more(mut self, has_more: bool) -> Self {
259        self.has_more = has_more;
260        self
261    }
262}
263
264/// Result of a download operation
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct DownloadResult {
267    /// Path where the file was saved
268    pub path: String,
269
270    /// Number of bytes downloaded
271    pub bytes: u64,
272
273    /// Whether the download was successful
274    pub success: bool,
275
276    /// Error message if failed
277    pub error: Option<String>,
278}
279
280impl DownloadResult {
281    /// Create a successful download result
282    pub fn success(path: impl Into<String>, bytes: u64) -> Self {
283        Self {
284            path: path.into(),
285            bytes,
286            success: true,
287            error: None,
288        }
289    }
290
291    /// Create a failed download result
292    pub fn error(error: impl Into<String>) -> Self {
293        Self {
294            path: String::new(),
295            bytes: 0,
296            success: false,
297            error: Some(error.into()),
298        }
299    }
300}
301
302/// Batch download request containing multiple individual download requests
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct BatchDownloadRequest {
305    /// List of individual download requests
306    pub requests: Vec<DownloadRequest>,
307}
308
309impl BatchDownloadRequest {
310    /// Create a new batch download request from a list of requests
311    pub fn new(requests: Vec<DownloadRequest>) -> Self {
312        Self { requests }
313    }
314
315    /// Add a download request to the batch
316    pub fn add_request(&mut self, request: DownloadRequest) {
317        self.requests.push(request);
318    }
319
320    /// Get the number of requests in the batch
321    pub fn len(&self) -> usize {
322        self.requests.len()
323    }
324
325    /// Check if the batch is empty
326    pub fn is_empty(&self) -> bool {
327        self.requests.is_empty()
328    }
329}
330
331/// Result of a batch download operation
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct BatchDownloadResult {
334    /// Individual download results
335    pub results: Vec<DownloadResult>,
336
337    /// Total number of successful downloads
338    pub successful: usize,
339
340    /// Total number of failed downloads
341    pub failed: usize,
342
343    /// Total bytes downloaded
344    pub total_bytes: u64,
345}
346
347impl BatchDownloadResult {
348    /// Create a new batch download result from individual results
349    pub fn new(results: Vec<DownloadResult>) -> Self {
350        let successful = results.iter().filter(|r| r.success).count();
351        let failed = results.len() - successful;
352        let total_bytes = results.iter().map(|r| r.bytes).sum();
353
354        Self {
355            results,
356            successful,
357            failed,
358            total_bytes,
359        }
360    }
361
362    /// Get the success rate as a percentage (0.0 to 1.0)
363    pub fn success_rate(&self) -> f64 {
364        if self.results.is_empty() {
365            0.0
366        } else {
367            self.successful as f64 / self.results.len() as f64
368        }
369    }
370
371    /// Check if all downloads succeeded (and there was at least one)
372    pub fn is_all_success(&self) -> bool {
373        !self.results.is_empty() && self.failed == 0
374    }
375}
376
377/// Result of a paper read operation
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct ReadResult {
380    /// Extracted text content
381    pub text: String,
382
383    /// Number of pages
384    pub pages: Option<usize>,
385
386    /// Whether the read was successful
387    pub success: bool,
388
389    /// Error message if failed
390    pub error: Option<String>,
391}
392
393impl ReadResult {
394    /// Create a successful read result
395    pub fn success(text: impl Into<String>) -> Self {
396        Self {
397            text: text.into(),
398            pages: None,
399            success: true,
400            error: None,
401        }
402    }
403
404    /// Set page count
405    pub fn pages(mut self, pages: usize) -> Self {
406        self.pages = Some(pages);
407        self
408    }
409
410    /// Create a failed read result
411    pub fn error(error: impl Into<String>) -> Self {
412        Self {
413            text: String::new(),
414            pages: None,
415            success: false,
416            error: Some(error.into()),
417        }
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use crate::models::{Paper, SourceType};
425
426    #[test]
427    fn test_batch_download_request_new() {
428        let requests = vec![
429            DownloadRequest::new("paper1", "/downloads"),
430            DownloadRequest::new("paper2", "/downloads"),
431        ];
432        let batch = BatchDownloadRequest::new(requests);
433        assert_eq!(batch.len(), 2);
434        assert!(!batch.is_empty());
435    }
436
437    #[test]
438    fn test_batch_download_request_add() {
439        let mut batch = BatchDownloadRequest::new(vec![]);
440        assert!(batch.is_empty());
441
442        batch.add_request(DownloadRequest::new("paper1", "/downloads"));
443        assert_eq!(batch.len(), 1);
444    }
445
446    #[test]
447    fn test_batch_download_result_new() {
448        let results = vec![
449            DownloadResult::success("/path/to/paper1.pdf", 1024),
450            DownloadResult::success("/path/to/paper2.pdf", 2048),
451            DownloadResult::error("Failed to download"),
452        ];
453
454        let batch = BatchDownloadResult::new(results);
455
456        assert_eq!(batch.successful, 2);
457        assert_eq!(batch.failed, 1);
458        assert_eq!(batch.total_bytes, 3072);
459        assert!((batch.success_rate() - 0.666).abs() < 0.001);
460        assert!(!batch.is_all_success());
461    }
462
463    #[test]
464    fn test_batch_download_result_all_success() {
465        let results = vec![
466            DownloadResult::success("/path/to/paper1.pdf", 1024),
467            DownloadResult::success("/path/to/paper2.pdf", 2048),
468        ];
469
470        let batch = BatchDownloadResult::new(results);
471
472        assert_eq!(batch.successful, 2);
473        assert_eq!(batch.failed, 0);
474        assert_eq!(batch.success_rate(), 1.0);
475        assert!(batch.is_all_success());
476    }
477
478    #[test]
479    fn test_batch_download_result_empty() {
480        let batch = BatchDownloadResult::new(vec![]);
481
482        assert_eq!(batch.successful, 0);
483        assert_eq!(batch.failed, 0);
484        assert_eq!(batch.total_bytes, 0);
485        assert_eq!(batch.success_rate(), 0.0);
486        assert!(!batch.is_all_success());
487    }
488
489    #[test]
490    fn test_search_query_new() {
491        let query = SearchQuery::new("machine learning");
492        assert_eq!(query.query, "machine learning");
493        assert_eq!(query.max_results, 10); // default
494        assert!(query.year.is_none());
495        assert!(query.sort_by.is_none());
496        assert!(query.sort_order.is_none());
497    }
498
499    #[test]
500    fn test_search_query_with_options() {
501        let query = SearchQuery::new("neural networks")
502            .max_results(50)
503            .year("2020-2023")
504            .sort_by(SortBy::Relevance)
505            .sort_order(SortOrder::Descending);
506
507        assert_eq!(query.query, "neural networks");
508        assert_eq!(query.max_results, 50);
509        assert_eq!(query.year, Some("2020-2023".to_string()));
510        assert_eq!(query.sort_by, Some(SortBy::Relevance));
511        assert_eq!(query.sort_order, Some(SortOrder::Descending));
512    }
513
514    #[test]
515    fn test_search_query_builder_pattern() {
516        let query = SearchQuery::new("deep learning")
517            .max_results(100)
518            .author("John Doe")
519            .category("cs.AI")
520            .year("2022");
521
522        assert_eq!(query.query, "deep learning");
523        assert_eq!(query.max_results, 100);
524        assert_eq!(query.author, Some("John Doe".to_string()));
525        assert_eq!(query.category, Some("cs.AI".to_string()));
526        assert_eq!(query.year, Some("2022".to_string()));
527    }
528
529    #[test]
530    fn test_search_query_year_formats() {
531        let single_year = SearchQuery::new("test").year("2020");
532        assert_eq!(single_year.year, Some("2020".to_string()));
533
534        let year_range = SearchQuery::new("test").year("2019-2023");
535        assert_eq!(year_range.year, Some("2019-2023".to_string()));
536
537        let from_year = SearchQuery::new("test").year("2020-");
538        assert_eq!(from_year.year, Some("2020-".to_string()));
539    }
540
541    #[test]
542    fn test_search_response_new() {
543        let papers = vec![
544            Paper::new(
545                "1".to_string(),
546                "Paper 1".to_string(),
547                "url1".to_string(),
548                SourceType::Arxiv,
549            ),
550            Paper::new(
551                "2".to_string(),
552                "Paper 2".to_string(),
553                "url2".to_string(),
554                SourceType::Arxiv,
555            ),
556        ];
557        let response = SearchResponse::new(papers, "test source", "search term");
558
559        assert_eq!(response.papers.len(), 2);
560        assert_eq!(response.source, "test source");
561        assert_eq!(response.query, "search term");
562        // total_results is None by default, set via builder method
563        assert!(response.total_results.is_none());
564    }
565
566    #[test]
567    fn test_search_response_with_total() {
568        let papers = vec![Paper::new(
569            "1".to_string(),
570            "Paper 1".to_string(),
571            "url1".to_string(),
572            SourceType::Arxiv,
573        )];
574        let response = SearchResponse::new(papers, "test source", "search term").total_results(100);
575
576        assert_eq!(response.total_results, Some(100));
577    }
578
579    #[test]
580    fn test_search_response_empty() {
581        let response = SearchResponse::new(vec![], "test source", "search term");
582        assert!(response.papers.is_empty());
583        // total_results is None by default
584        assert!(response.total_results.is_none());
585    }
586
587    #[test]
588    fn test_citation_request_new() {
589        let request = CitationRequest::new("paper123");
590        assert_eq!(request.paper_id, "paper123");
591        assert_eq!(request.max_results, 20); // default
592    }
593
594    #[test]
595    fn test_citation_request_with_options() {
596        let request = CitationRequest::new("paper456").max_results(50);
597
598        assert_eq!(request.paper_id, "paper456");
599        assert_eq!(request.max_results, 50);
600    }
601
602    #[test]
603    fn test_download_request_new() {
604        let request = DownloadRequest::new("paper123", "/downloads");
605        assert_eq!(request.paper_id, "paper123");
606        assert_eq!(request.save_path, "/downloads");
607    }
608
609    #[test]
610    fn test_download_result_success() {
611        let result = DownloadResult::success("/path/to/file.pdf", 1024);
612        assert!(result.success);
613        assert_eq!(result.path, "/path/to/file.pdf");
614        assert_eq!(result.bytes, 1024);
615        assert!(result.error.is_none());
616    }
617
618    #[test]
619    fn test_download_result_error() {
620        let result = DownloadResult::error("Network timeout");
621        assert!(!result.success);
622        assert!(result.path.is_empty());
623        assert_eq!(result.bytes, 0);
624        assert_eq!(result.error, Some("Network timeout".to_string()));
625    }
626
627    #[test]
628    fn test_read_request_new() {
629        let request = ReadRequest::new("123", "/papers");
630
631        assert_eq!(request.paper_id, "123");
632        assert_eq!(request.save_path, "/papers");
633        assert!(request.download_if_missing);
634    }
635
636    #[test]
637    fn test_read_request_with_download_option() {
638        let request = ReadRequest::new("123", "/papers").download_if_missing(false);
639
640        assert!(!request.download_if_missing);
641    }
642
643    #[test]
644    fn test_read_result_new() {
645        let result = ReadResult::success("Extracted text content");
646        assert_eq!(result.text, "Extracted text content");
647        assert!(result.success);
648        assert!(result.error.is_none());
649    }
650
651    #[test]
652    fn test_read_result_with_pages() {
653        let result = ReadResult::success("Text".to_string()).pages(5);
654        assert_eq!(result.pages, Some(5));
655    }
656
657    #[test]
658    fn test_sort_by_variants() {
659        // SortBy uses Debug formatting via derive
660        assert_eq!(format!("{:?}", SortBy::Relevance), "Relevance");
661        assert_eq!(format!("{:?}", SortBy::Date), "Date");
662        assert_eq!(format!("{:?}", SortBy::CitationCount), "CitationCount");
663        assert_eq!(format!("{:?}", SortBy::Title), "Title");
664        assert_eq!(format!("{:?}", SortBy::Author), "Author");
665    }
666
667    #[test]
668    fn test_sort_order_variants() {
669        // SortOrder uses Debug formatting via derive
670        assert_eq!(format!("{:?}", SortOrder::Descending), "Descending");
671        assert_eq!(format!("{:?}", SortOrder::Ascending), "Ascending");
672    }
673}