tmdb_api/movie/
search.rs

1use std::borrow::Cow;
2
3#[derive(Clone, Debug, Default, serde::Serialize)]
4pub struct Params<'a> {
5    /// ISO 639-1 value to display translated data for the fields that support it.
6    #[serde(skip_serializing_if = "Option::is_none")]
7    pub language: Option<Cow<'a, str>>,
8    /// Which page to query.
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub page: Option<u32>,
11    /// Whether to include adult (pornography) content in the results.
12    #[serde(skip_serializing_if = "crate::util::is_false")]
13    pub include_adult: bool,
14    /// ISO 3166-1 code to filter release region. Must be uppercase.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub region: Option<Cow<'a, str>>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub year: Option<u16>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub primary_release_year: Option<u16>,
21}
22
23impl<'a> Params<'a> {
24    pub fn set_language(&mut self, value: impl Into<Cow<'a, str>>) {
25        self.language = Some(value.into());
26    }
27
28    pub fn with_language(mut self, value: impl Into<Cow<'a, str>>) -> Self {
29        self.set_language(value);
30        self
31    }
32
33    pub fn set_page(&mut self, value: u32) {
34        self.page = Some(value);
35    }
36
37    pub fn with_page(mut self, value: u32) -> Self {
38        self.set_page(value);
39        self
40    }
41
42    pub fn set_include_adult(&mut self, value: bool) {
43        self.include_adult = value;
44    }
45
46    pub fn with_include_adult(mut self, value: bool) -> Self {
47        self.set_include_adult(value);
48        self
49    }
50
51    pub fn set_region(&mut self, value: impl Into<Cow<'a, str>>) {
52        self.region = Some(value.into());
53    }
54
55    pub fn with_region(mut self, value: impl Into<Cow<'a, str>>) -> Self {
56        self.set_region(value);
57        self
58    }
59
60    pub fn set_year(&mut self, value: u16) {
61        self.year = Some(value);
62    }
63
64    pub fn with_year(mut self, value: u16) -> Self {
65        self.set_year(value);
66        self
67    }
68
69    pub fn set_primary_release_year(&mut self, value: u16) {
70        self.primary_release_year = Some(value);
71    }
72
73    pub fn with_primary_release_year(mut self, value: u16) -> Self {
74        self.set_primary_release_year(value);
75        self
76    }
77}
78
79#[derive(serde::Serialize)]
80struct WithQuery<'a, V> {
81    query: Cow<'a, str>,
82    #[serde(flatten)]
83    inner: V,
84}
85
86impl<E: crate::client::Executor> crate::Client<E> {
87    /// Search for movies by their original, translated and alternative titles.
88    ///
89    /// ```rust
90    /// use tmdb_api::client::Client;
91    /// use tmdb_api::client::reqwest::Client as ReqwestClient;
92    ///
93    /// #[tokio::main]
94    /// async fn main() {
95    ///     let client = Client::<ReqwestClient>::new("this-is-my-secret-token".into());
96    ///     match client.search_movies("die hard", &Default::default()).await {
97    ///         Ok(res) => println!("found: {:#?}", res),
98    ///         Err(err) => eprintln!("error: {:?}", err),
99    ///     };
100    /// }
101    /// ```
102    pub async fn search_movies<'a>(
103        &self,
104        query: impl Into<Cow<'a, str>>,
105        params: &Params<'a>,
106    ) -> crate::Result<crate::common::PaginatedResult<super::MovieShort>> {
107        self.execute(
108            "/search/movie",
109            &WithQuery {
110                query: query.into(),
111                inner: params,
112            },
113        )
114        .await
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use crate::client::Client;
121    use crate::client::reqwest::Client as ReqwestClient;
122    use mockito::Matcher;
123
124    #[tokio::test]
125    async fn it_works() {
126        let mut server = mockito::Server::new_async().await;
127        let client = Client::<ReqwestClient>::builder()
128            .with_api_key("secret".into())
129            .with_base_url(server.url())
130            .build()
131            .unwrap();
132
133        let _m = server
134            .mock("GET", "/search/movie")
135            .match_query(Matcher::AllOf(vec![
136                Matcher::UrlEncoded("api_key".into(), "secret".into()),
137                Matcher::UrlEncoded("query".into(), "Whatever".into()),
138            ]))
139            .with_status(200)
140            .with_header("content-type", "application/json")
141            .with_body(include_str!("../../assets/search-movie.json"))
142            .create_async()
143            .await;
144        let result = client
145            .search_movies("Whatever", &Default::default())
146            .await
147            .unwrap();
148        assert_eq!(result.page, 1);
149        assert!(!result.results.is_empty());
150        assert!(result.total_pages > 0);
151        assert!(result.total_results > 0);
152        let item = result.results.first().unwrap();
153        assert_eq!(item.inner.title, "RRRrrrr!!!");
154    }
155
156    #[tokio::test]
157    async fn invalid_api_key() {
158        let mut server = mockito::Server::new_async().await;
159        let client = Client::<ReqwestClient>::builder()
160            .with_api_key("secret".into())
161            .with_base_url(server.url())
162            .build()
163            .unwrap();
164
165        let _m = server
166            .mock("GET", "/search/movie")
167            .match_query(Matcher::AllOf(vec![
168                Matcher::UrlEncoded("api_key".into(), "secret".into()),
169                Matcher::UrlEncoded("query".into(), "Whatever".into()),
170            ]))
171            .with_status(401)
172            .with_header("content-type", "application/json")
173            .with_body(include_str!("../../assets/invalid-api-key.json"))
174            .create_async()
175            .await;
176        let err = client
177            .search_movies("Whatever", &Default::default())
178            .await
179            .unwrap_err();
180        println!("err {err:?}");
181        let server_err = err.as_server_error().unwrap();
182        assert_eq!(server_err.status_code, 7);
183    }
184
185    #[tokio::test]
186    async fn resource_not_found() {
187        let mut server = mockito::Server::new_async().await;
188        let client = Client::<ReqwestClient>::builder()
189            .with_api_key("secret".into())
190            .with_base_url(server.url())
191            .build()
192            .unwrap();
193
194        let _m = server
195            .mock("GET", "/search/movie")
196            .match_query(Matcher::AllOf(vec![
197                Matcher::UrlEncoded("api_key".into(), "secret".into()),
198                Matcher::UrlEncoded("query".into(), "Whatever".into()),
199            ]))
200            .with_status(404)
201            .with_header("content-type", "application/json")
202            .with_body(include_str!("../../assets/resource-not-found.json"))
203            .create_async()
204            .await;
205        let err = client
206            .search_movies("Whatever", &Default::default())
207            .await
208            .unwrap_err();
209        let server_err = err.as_server_error().unwrap();
210        assert_eq!(server_err.status_code, 34);
211    }
212
213    #[tokio::test]
214    async fn validation_error() {
215        let mut server = mockito::Server::new_async().await;
216        let client = Client::<ReqwestClient>::builder()
217            .with_api_key("secret".into())
218            .with_base_url(server.url())
219            .build()
220            .unwrap();
221
222        let _m = server
223            .mock("GET", "/search/movie")
224            .match_query(Matcher::AllOf(vec![
225                Matcher::UrlEncoded("api_key".into(), "secret".into()),
226                Matcher::UrlEncoded("query".into(), "".into()),
227            ]))
228            .with_status(422)
229            .with_header("content-type", "application/json")
230            .with_body(include_str!("../../assets/validation-error.json"))
231            .create_async()
232            .await;
233        let err = client
234            .search_movies("", &Default::default())
235            .await
236            .unwrap_err();
237        let validation_err = err.as_validation_error().unwrap();
238        assert_eq!(validation_err.errors.len(), 1);
239    }
240
241    // #[tokio::test]
242    // async fn premature_end_of_line() {
243    // let mut server = mockito::Server::new_async().await;
244    // let client = Client::<ReqwestClient>::builder().with_api_key("secret".into()).with_base_url(server.url()).build().unwrap();
245
246    //     let client = Client::<ReqwestClient>::new("secret".into()).with_base_url(mockito::server_url());
247    //     let cmd = MovieSearch::new("game of thrones".into());
248
249    //     let _m = mock("GET", super::PATH)
250    //         .match_query(Matcher::AllOf(vec![
251    //             Matcher::UrlEncoded("api_key".into(), "secret".into()),
252    //             Matcher::UrlEncoded("query".into(), "game of thrones".into()),
253    //         ]))
254    //         .with_status(200)
255    //         .with_header("content-type", "application/json;charset=utf-8")
256    //         .with_body(include_str!("../../assets/search-tv-decoding-error.json"))
257    //         .create_async().await;
258    //     let result = cmd.execute(&client).await.unwrap();
259    //     assert_eq!(result.page, 1);
260    // }
261}
262
263#[cfg(all(test, feature = "integration"))]
264mod integration_tests {
265    use crate::client::Client;
266    use crate::client::reqwest::Client as ReqwestClient;
267
268    #[tokio::test]
269    async fn search_rrrrrrr() {
270        let secret = std::env::var("TMDB_TOKEN_V3").unwrap();
271        let client = Client::<ReqwestClient>::new(secret);
272        let result = client
273            .search_movies("Rrrrrrr", &Default::default())
274            .await
275            .unwrap();
276        assert_eq!(result.page, 1);
277        assert_eq!(result.results.len(), 1);
278        assert_eq!(result.total_pages, 1);
279        assert_eq!(result.total_results, 1);
280        let item = result.results.first().unwrap();
281        assert_eq!(item.inner.title, "RRRrrrr!!!");
282    }
283
284    #[tokio::test]
285    async fn search_simpsons() {
286        let secret = std::env::var("TMDB_TOKEN_V3").unwrap();
287        let client = Client::<ReqwestClient>::new(secret);
288        let _result = client
289            .search_movies("simpsons", &Default::default())
290            .await
291            .unwrap();
292    }
293}