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