1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum SortOrder {
10 Ascending,
11 Descending,
12}
13
14#[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#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SearchQuery {
28 pub query: String,
30
31 pub max_results: usize,
33
34 pub year: Option<String>,
36
37 pub sort_by: Option<SortBy>,
39
40 pub sort_order: Option<SortOrder>,
42
43 pub filters: HashMap<String, String>,
45
46 pub author: Option<String>,
48
49 pub category: Option<String>,
51
52 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 pub fn new(query: impl Into<String>) -> Self {
75 Self {
76 query: query.into(),
77 ..Default::default()
78 }
79 }
80
81 pub fn max_results(mut self, max: usize) -> Self {
83 self.max_results = max;
84 self
85 }
86
87 pub fn year(mut self, year: impl Into<String>) -> Self {
89 self.year = Some(year.into());
90 self
91 }
92
93 pub fn sort_by(mut self, sort: SortBy) -> Self {
95 self.sort_by = Some(sort);
96 self
97 }
98
99 pub fn sort_order(mut self, order: SortOrder) -> Self {
101 self.sort_order = Some(order);
102 self
103 }
104
105 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 pub fn author(mut self, author: impl Into<String>) -> Self {
113 self.author = Some(author.into());
114 self
115 }
116
117 pub fn category(mut self, category: impl Into<String>) -> Self {
119 self.category = Some(category.into());
120 self
121 }
122
123 pub fn fetch_details(mut self, fetch: bool) -> Self {
125 self.fetch_details = fetch;
126 self
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct DownloadRequest {
133 pub paper_id: String,
135
136 pub save_path: String,
138
139 pub doi: Option<String>,
141}
142
143impl DownloadRequest {
144 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 pub fn doi(mut self, doi: impl Into<String>) -> Self {
155 self.doi = Some(doi.into());
156 self
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ReadRequest {
163 pub paper_id: String,
165
166 pub save_path: String,
168
169 pub download_if_missing: bool,
171}
172
173impl ReadRequest {
174 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 pub fn download_if_missing(mut self, download: bool) -> Self {
185 self.download_if_missing = download;
186 self
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct CitationRequest {
193 pub paper_id: String,
195
196 pub max_results: usize,
198}
199
200impl CitationRequest {
201 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 pub fn max_results(mut self, max: usize) -> Self {
211 self.max_results = max;
212 self
213 }
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct SearchResponse {
219 pub papers: Vec<crate::models::Paper>,
221
222 pub total_results: Option<usize>,
224
225 pub source: String,
227
228 pub query: String,
230
231 pub has_more: bool,
233}
234
235impl SearchResponse {
236 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 pub fn total_results(mut self, total: usize) -> Self {
253 self.total_results = Some(total);
254 self
255 }
256
257 pub fn has_more(mut self, has_more: bool) -> Self {
259 self.has_more = has_more;
260 self
261 }
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct DownloadResult {
267 pub path: String,
269
270 pub bytes: u64,
272
273 pub success: bool,
275
276 pub error: Option<String>,
278}
279
280impl DownloadResult {
281 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct BatchDownloadRequest {
305 pub requests: Vec<DownloadRequest>,
307}
308
309impl BatchDownloadRequest {
310 pub fn new(requests: Vec<DownloadRequest>) -> Self {
312 Self { requests }
313 }
314
315 pub fn add_request(&mut self, request: DownloadRequest) {
317 self.requests.push(request);
318 }
319
320 pub fn len(&self) -> usize {
322 self.requests.len()
323 }
324
325 pub fn is_empty(&self) -> bool {
327 self.requests.is_empty()
328 }
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct BatchDownloadResult {
334 pub results: Vec<DownloadResult>,
336
337 pub successful: usize,
339
340 pub failed: usize,
342
343 pub total_bytes: u64,
345}
346
347impl BatchDownloadResult {
348 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 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 pub fn is_all_success(&self) -> bool {
373 !self.results.is_empty() && self.failed == 0
374 }
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct ReadResult {
380 pub text: String,
382
383 pub pages: Option<usize>,
385
386 pub success: bool,
388
389 pub error: Option<String>,
391}
392
393impl ReadResult {
394 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 pub fn pages(mut self, pages: usize) -> Self {
406 self.pages = Some(pages);
407 self
408 }
409
410 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); 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 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 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); }
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 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 assert_eq!(format!("{:?}", SortOrder::Descending), "Descending");
671 assert_eq!(format!("{:?}", SortOrder::Ascending), "Ascending");
672 }
673}