1use std::borrow::Cow;
2
3#[derive(Clone, Debug, Default, serde::Serialize)]
4pub struct Params<'a> {
5 #[serde(skip_serializing_if = "Option::is_none")]
7 pub language: Option<Cow<'a, str>>,
8 #[serde(skip_serializing_if = "Option::is_none")]
10 pub page: Option<u32>,
11 #[serde(skip_serializing_if = "crate::util::is_false")]
13 pub include_adult: bool,
14 #[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 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::ReqwestExecutor;
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::<ReqwestExecutor>::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::<ReqwestExecutor>::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::<ReqwestExecutor>::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::<ReqwestExecutor>::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 }
262
263#[cfg(all(test, feature = "integration"))]
264mod integration_tests {
265 use crate::client::Client;
266 use crate::client::reqwest::ReqwestExecutor;
267
268 #[tokio::test]
269 async fn search_rrrrrrr() {
270 let secret = std::env::var("TMDB_TOKEN_V3").unwrap();
271 let client = Client::<ReqwestExecutor>::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::<ReqwestExecutor>::new(secret);
288 let _result = client
289 .search_movies("simpsons", &Default::default())
290 .await
291 .unwrap();
292 }
293}