openlibrary_rs/
search.rs

1use std::fmt::Display;
2
3use derive_builder::Builder;
4
5use crate::OpenlibraryRequest;
6
7#[derive(Default, Clone, Debug)]
8pub enum SearchType {
9    #[default]
10    Books,
11    Authors,
12    Subjects,
13    Lists,
14}
15
16impl Display for SearchType {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        write!(
19            f,
20            "{}",
21            match self {
22                Self::Books => "",
23                Self::Authors => "/authors",
24                Self::Subjects => "/subjects",
25                Self::Lists => "/lists",
26            }
27        )
28    }
29}
30
31/// The struct representation of a request to the [Search API](https://openlibrary.org/dev/docs/api/search)
32///
33/// The fields of this struct are private. If you want to view available fields that can be set please look at the [`SearchBuilder`] struct.
34/// For more information on query strings and examples please view [Openlibrary's documentation](https://openlibrary.org/search/howto).
35#[derive(Builder, Default, Debug)]
36#[builder(setter(into), default)]
37pub struct Search {
38    #[builder(setter(strip_option))]
39    query: Option<String>,
40    search_type: SearchType,
41    #[builder(default = "1")]
42    page: u32,
43    #[builder(default = "10")]
44    limit: u32,
45    #[builder(default = "vec![]")]
46    fields: Vec<String>,
47}
48
49impl OpenlibraryRequest for Search {
50    fn path(&self) -> String {
51        format!("/search{}.json", self.search_type)
52    }
53
54    fn query(&self) -> Vec<(&'static str, String)> {
55        vec![
56            ("page", self.page.to_string()),
57            ("limit", self.limit.to_string()),
58            ("q", self.query.as_deref().unwrap_or_default().to_string()),
59            ("fields", self.fields.join(",")),
60        ]
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use mockito::mock;
67    use serde_json::json;
68
69    use crate::OpenlibraryRequest;
70
71    use super::SearchBuilder;
72
73    #[test]
74    fn test_search_execute() {
75        let search = SearchBuilder::default()
76            .query("test")
77            .fields(
78                ["key", "title"]
79                    .into_iter()
80                    .map(String::from)
81                    .collect::<Vec<String>>(),
82            )
83            .build()
84            .unwrap();
85
86        let json = json!({
87                "numFound": 1,
88                "start": 0,
89                "numFoundExact": true,
90                "docs": [
91                    {
92                        "key": "/works/43242",
93                        "title": "test",
94                    }
95                ]
96        });
97
98        let _m = mock(
99            "GET",
100            format!("{}?{}", search.url().path(), search.url().query().unwrap()).as_str(),
101        )
102        .with_header("content-type", "application/json")
103        .with_body(json.to_string())
104        .create();
105
106        let search_result = search.execute();
107
108        assert_eq!(search_result["numFound"], 1);
109        assert_eq!(search_result["start"], 0);
110        assert_eq!(search_result["numFoundExact"], true);
111        assert_eq!(search_result["docs"].as_array().unwrap().len(), 1);
112
113        let doc = &search_result["docs"][0];
114
115        assert_eq!(doc.get("key").unwrap().as_str().unwrap(), "/works/43242");
116        assert_eq!(doc.get("title").unwrap().as_str().unwrap(), "test");
117    }
118}