trakt_rs/api/
search.rs

1//! Search endpoints
2//!
3//! <https://trakt.docs.apiary.io/#reference/search>
4
5use serde::Serializer;
6
7use crate::smo::Item;
8
9bitflags::bitflags! {
10    #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
11    pub struct SearchType: u8 {
12        const MOVIE = 0b0000_0001;
13        const SHOW = 0b0000_0010;
14        const EPISODE = 0b0000_0100;
15        const PERSON = 0b0000_1000;
16        const LIST = 0b0001_0000;
17    }
18}
19
20impl serde::Serialize for SearchType {
21    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
22        const FLAGS: [&str; 5] = ["movie", "show", "episode", "person", "list"];
23
24        if self.is_empty() {
25            serializer.serialize_none()
26        } else if self.bits().count_ones() == 1 {
27            // Serialize as a single value
28
29            // Get name of the flag
30            let idx = self.bits().trailing_zeros() as usize;
31            serializer.serialize_str(FLAGS[idx])
32        } else {
33            // Serialize as a comma-separated list
34            // We can't serialize as a sequence b/c serde_urlencoded doesn't support it
35
36            // Get names of the flags
37            let iter = self.iter().map(|flag| {
38                let idx = flag.bits().trailing_zeros() as usize;
39                FLAGS[idx]
40            });
41
42            // Join the names
43            let joined = iter.collect::<Vec<_>>().join(",");
44
45            serializer.serialize_str(&joined)
46        }
47    }
48}
49
50#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
51pub struct SearchResult {
52    #[serde(flatten)]
53    pub item: Item,
54    pub score: Option<f64>,
55}
56
57pub mod text_query {
58    //! Text query search
59    //!
60    //! <https://trakt.docs.apiary.io/#reference/search/text-query/get-text-query-results>
61
62    use trakt_core::{Pagination, PaginationResponse};
63
64    use super::{SearchResult, SearchType};
65
66    #[derive(Debug, Clone, PartialEq, Eq, Hash, trakt_macros::Request)]
67    #[trakt(
68    response = Response,
69    endpoint = "/search/{tp}"
70    )]
71    pub struct Request {
72        pub tp: SearchType,
73        pub query: String,
74        #[serde(flatten)]
75        pub pagination: Pagination,
76    }
77
78    #[derive(Debug, Clone, PartialEq, trakt_macros::Response)]
79    pub struct Response {
80        #[trakt(pagination)]
81        pub items: PaginationResponse<SearchResult>,
82    }
83}
84
85pub mod id_lookup {
86    //! Lookup items by their IDs
87    //!
88    //! <https://trakt.docs.apiary.io/#reference/search/text-query/get-id-lookup-results>
89
90    use bytes::BufMut;
91    use serde::Serialize;
92    use trakt_core::{error::IntoHttpError, Context, Metadata, Pagination, PaginationResponse};
93
94    use super::{SearchResult, SearchType};
95    use crate::smo::Id;
96
97    #[derive(Debug, Clone, Eq, PartialEq, Hash)]
98    pub struct Request {
99        pub id: Id,
100        pub tp: SearchType,
101        pub pagination: Pagination,
102    }
103
104    #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
105    struct RequestPathParams {
106        id_type: &'static str,
107        id: Id,
108    }
109
110    #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)]
111    struct RequestQueryParams {
112        #[serde(rename = "type")]
113        tp: SearchType,
114        #[serde(flatten)]
115        pagination: Pagination,
116    }
117
118    impl TryFrom<Request> for (RequestPathParams, RequestQueryParams) {
119        type Error = IntoHttpError;
120
121        fn try_from(value: Request) -> Result<Self, Self::Error> {
122            Ok((
123                RequestPathParams {
124                    id_type: match &value.id {
125                        Id::Trakt(_) => "trakt",
126                        Id::Slug(_) => {
127                            return Err(IntoHttpError::Validation(String::from(
128                                "Slug IDs are not supported",
129                            )));
130                        }
131                        Id::Tvdb(_) => "tvdb",
132                        Id::Imdb(_) => "imdb",
133                        Id::Tmdb(_) => "tmdb",
134                    },
135                    id: value.id,
136                },
137                RequestQueryParams {
138                    tp: value.tp,
139                    pagination: value.pagination,
140                },
141            ))
142        }
143    }
144
145    impl trakt_core::Request for Request {
146        type Response = Response;
147        const METADATA: Metadata = Metadata {
148            endpoint: "/search/{id_type}/{id}",
149            method: http::Method::GET,
150            auth: trakt_core::AuthRequirement::None,
151        };
152
153        fn try_into_http_request<T: Default + BufMut>(
154            self,
155            ctx: Context,
156        ) -> Result<http::Request<T>, IntoHttpError> {
157            let (path, query) = self.try_into()?;
158            trakt_core::construct_req(&ctx, &Self::METADATA, &path, &query, T::default())
159        }
160    }
161
162    #[derive(Debug, Clone, PartialEq, trakt_macros::Response)]
163    pub struct Response {
164        #[trakt(pagination)]
165        pub items: PaginationResponse<SearchResult>,
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use trakt_core::{construct_url, error::IntoHttpError, Context, Pagination, Request};
172
173    use super::*;
174    use crate::{smo::Id, test::assert_request};
175
176    const CTX: Context = Context {
177        base_url: "https://api.trakt.tv",
178        client_id: "client_id",
179        oauth_token: None,
180    };
181
182    #[test]
183    fn test_type_ser() {
184        let tp = SearchType::MOVIE;
185        assert_eq!(serde_json::to_string(&tp).unwrap(), r#""movie""#);
186
187        let tp = SearchType::MOVIE | SearchType::SHOW;
188        assert_eq!(serde_json::to_string(&tp).unwrap(), r#""movie,show""#);
189
190        let tp = SearchType::empty();
191        assert_eq!(serde_json::to_string(&tp).unwrap(), "null");
192    }
193
194    #[test]
195    fn test_type_ser_url() {
196        #[derive(Debug, serde::Serialize)]
197        struct Test {
198            tp: SearchType,
199        }
200
201        let test = Test {
202            tp: SearchType::MOVIE,
203        };
204        let url = construct_url("", "/search/{tp}", &test, &()).unwrap();
205        assert_eq!(url, "/search/movie");
206
207        let test = Test {
208            tp: SearchType::MOVIE | SearchType::SHOW,
209        };
210        let url = construct_url("", "/search/{tp}", &test, &()).unwrap();
211        assert_eq!(url, "/search/movie,show");
212
213        let test = Test {
214            tp: SearchType::empty(),
215        };
216        let url = construct_url("", "/search/{tp}", &test, &()).unwrap();
217        assert_eq!(url, "/search/");
218    }
219
220    #[test]
221    fn test_id_lookup_request() {
222        let req = id_lookup::Request {
223            id: Id::Trakt(1),
224            tp: SearchType::MOVIE,
225            pagination: Pagination::default(),
226        };
227        assert_request(
228            CTX,
229            req,
230            "https://api.trakt.tv/search/trakt/1?type=movie&page=1&limit=10",
231            "",
232        );
233
234        let req = id_lookup::Request {
235            id: Id::Tvdb(1),
236            tp: SearchType::EPISODE | SearchType::SHOW,
237            pagination: Pagination::default(),
238        };
239        assert_request(
240            CTX,
241            req,
242            "https://api.trakt.tv/search/tvdb/1?type=show%2Cepisode&page=1&limit=10",
243            "",
244        );
245
246        let req = id_lookup::Request {
247            id: Id::Imdb("tt12345".into()),
248            tp: SearchType::empty(),
249            pagination: Pagination::default(),
250        };
251        assert_request(
252            CTX,
253            req,
254            "https://api.trakt.tv/search/imdb/tt12345?page=1&limit=10",
255            "",
256        );
257
258        let req = id_lookup::Request {
259            id: Id::Slug("slug".into()),
260            tp: SearchType::PERSON,
261            pagination: Pagination::default(),
262        };
263        assert!(matches!(
264            req.try_into_http_request::<Vec<u8>>(CTX),
265            Err(IntoHttpError::Validation(_))
266        ));
267    }
268}