1use std::borrow::Cow;
2
3#[derive(Clone, Debug, Default, serde::Serialize)]
4pub struct Params<'a> {
5 #[serde(skip_serializing_if = "Option::is_none")]
8 pub language: Option<Cow<'a, str>>,
9 #[serde(skip_serializing_if = "Option::is_none")]
11 pub page: Option<u32>,
12 #[serde(skip_serializing_if = "crate::util::is_false")]
14 pub include_adult: bool,
15 #[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 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 }
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}