crag/
soap.rs

1/// This `soap` module defines a single [`Soap`] trait.
2///
3/// The trait can then be implemented for different search engines.
4///
5/// Supporting types are also defined here, such as [`SearchResult`].
6// Track source urls
7use url::Url;
8
9// re-export the `Query` type
10pub use crate::Query;
11
12/// Provides engine customization options
13#[derive(Default, Debug)]
14#[cfg_attr(feature = "cli", derive(clap::Args))]
15pub struct EngineOptions {
16    /// Do not attempt to filter ads from returned responses
17    ///
18    /// Note that filtering ads may impact the number of results returned
19    #[cfg_attr(feature = "cli", arg(long))]
20    pub keep_ads: bool,
21
22    /// Do not attempt to filter video results from returned responses
23    ///
24    /// Note that keeping videos may result in additional results returned
25    #[cfg_attr(feature = "cli", arg(long))]
26    pub keep_videos: bool,
27
28    /// Do not attempt to filter image results from returned responses
29    ///
30    /// Note filtering images may impact the number of results returned
31    #[cfg_attr(feature = "cli", arg(long))]
32    pub keep_images: bool,
33}
34
35/// Expected return format from a search query
36#[derive(Debug)]
37pub struct SearchResult {
38    /// The headline of the search result
39    pub title: String,
40
41    /// More verbose search result description
42    pub description: String,
43
44    /// Source URL of the search result
45    pub source: Url,
46}
47
48/// Type alias for a vector of search results
49pub type SearchResults = Vec<SearchResult>;
50
51impl SearchResult {
52    /// Build a new SearchResult from a title, description, and source URL
53    pub fn try_new<T: Into<String>>(
54        title: T,
55        description: T,
56        source: &str,
57    ) -> Result<Self, Box<dyn std::error::Error>> {
58        Ok(Self {
59            title: title.into(),
60            description: description.into(),
61            source: Url::parse(source)?,
62        })
63    }
64}
65
66/// Provide fmt::Display for SearchResult
67///
68/// Prints the format `title (source): "description"`
69impl std::fmt::Display for SearchResult {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        // Limit description to 40 characters
72        let desc_slice = &self.description[..std::cmp::min(self.description.len(), 40)];
73
74        write!(f, "{} ({}): \"{}...\"", self.title, self.source, desc_slice)
75    }
76}
77
78/// Core functionality that must be implemented in order to use a search engine.
79pub trait Soap {
80    /// Set the engine options
81    fn configure(&mut self, options: EngineOptions) -> Result<(), Box<dyn std::error::Error>>;
82
83    /// Search for a query and return a vector of search results
84    fn search(&self, query: Query) -> Result<SearchResults, Box<dyn std::error::Error>>;
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_search_result_create() {
93        let result = SearchResult::try_new("title", "description", "https://example.com");
94        assert!(result.is_ok());
95    }
96
97    #[test]
98    fn test_search_result_display() {
99        let result = SearchResult::try_new("title", "description", "https://example.com").unwrap();
100        assert_eq!(
101            result.to_string(),
102            "title (https://example.com/): \"description...\""
103        );
104    }
105
106    #[test]
107    fn test_search_result_display_desc_is_truncated() {
108        let desc = "description description description description";
109        let result = SearchResult::try_new("title", desc, "https://example.com").unwrap();
110        assert_eq!(
111            result.to_string(),
112            format!("title (https://example.com/): \"{}...\"", &desc[..40])
113        );
114    }
115}