1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use std::fmt::Display;

use derive_builder::Builder;
use reqwest::Url;

use crate::OpenlibraryRequest;

#[derive(Default, Clone, Debug)]
pub enum SearchType {
    #[default]
    Books,
    Authors,
    Subjects,
    Lists,
}

impl Display for SearchType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::Books => "",
                Self::Authors => "/authors",
                Self::Subjects => "/subjects",
                Self::Lists => "/lists",
            }
        )
    }
}

/// The struct representation of a request to the [Search API](https://openlibrary.org/dev/docs/api/search)
///
/// The fields of this struct are private. If you want to view available fields that can be set please look at the [`SearchBuilder`] struct.
/// For more information on query strings and examples please view [Openlibrary's documentation](https://openlibrary.org/search/howto).
#[derive(Builder, Default, Debug)]
#[builder(setter(into), default)]
pub struct Search {
    #[builder(setter(strip_option))]
    query: Option<String>,
    search_type: SearchType,
    #[builder(default = "1")]
    page: u32,
    #[builder(default = "10")]
    limit: u32,
    #[builder(default = "vec![]")]
    fields: Vec<String>,
}

impl OpenlibraryRequest for Search {
    fn url(&self) -> Url {
        let mut params = Vec::new();
        params.push(("page", self.page.to_string()));
        params.push(("limit", self.limit.to_string()));
        params.push(("q", self.query.as_deref().unwrap_or_default().to_string()));
        params.push(("fields", self.fields.join(",")));

        Url::parse_with_params(
            format!("{}/search{}.json", Self::host(), self.search_type,).as_str(),
            params,
        )
        .unwrap()
    }
}

#[cfg(test)]
mod tests {
    use mockito::mock;
    use serde_json::json;

    use crate::OpenlibraryRequest;

    use super::SearchBuilder;

    #[test]
    fn test_search_execute_valid_response() {
        let search = SearchBuilder::default()
            .query("test")
            .fields(
                ["key", "title"]
                    .into_iter()
                    .map(String::from)
                    .collect::<Vec<String>>(),
            )
            .build()
            .unwrap();

        let json = json!({
                "numFound": 1,
                "start": 0,
                "numFoundExact": true,
                "docs": [
                    {
                        "key": "/works/43242",
                        "title": "test",
                    }
                ]
        });

        let _m = mock(
            "GET",
            format!("{}?{}", search.url().path(), search.url().query().unwrap()).as_str(),
        )
        .with_header("content-type", "application/json")
        .with_body(json.to_string())
        .create();

        let search_result = search.execute();

        assert_eq!(search_result["numFound"], 1);
        assert_eq!(search_result["start"], 0);
        assert_eq!(search_result["numFoundExact"], true);
        assert_eq!(search_result["docs"].as_array().unwrap().len(), 1);

        let doc = &search_result["docs"][0];

        assert_eq!(doc.get("key").unwrap().as_str().unwrap(), "/works/43242");
        assert_eq!(doc.get("title").unwrap().as_str().unwrap(), "test");
    }
}