1use crate::{
2 Locale, Orientation, Pexels, PexelsError, PhotosResponse, Size, PEXELS_API, PEXELS_VERSION,
3};
4use url::Url;
5const PEXELS_PHOTO_SEARCH_PATH: &str = "search";
6
7#[derive(Debug, PartialEq)]
29pub struct Hex<'a>(&'a str);
30
31impl<'a> Hex<'a> {
32 #[allow(clippy::should_implement_trait)]
34 pub fn from_borrowed_str(v: &'a str) -> Result<Self, PexelsError> {
35 if v.len() != 7 {
36 return Err(PexelsError::HexColorCodeError(format!(
37 "{v} is not 7 characters long."
38 )));
39 }
40
41 if !v.starts_with("#") {
42 return Err(PexelsError::HexColorCodeError(format!(
43 "{v} does not start with #."
44 )));
45 }
46
47 if !v[1..].chars().all(|c| c.is_ascii_hexdigit()) {
49 return Err(PexelsError::HexColorCodeError(format!(
50 "{v} have values that are not valid ASCII punctuation character."
51 )));
52 }
53
54 Ok(Self(v))
55 }
56}
57
58pub enum Color<'a> {
60 Red,
61 Orange,
62 Yellow,
63 Green,
64 Turquoise,
65 Blue,
66 Violet,
67 Pink,
68 Brown,
69 Black,
70 Gray,
71 White,
72 Hex(Hex<'a>),
73}
74
75impl Color<'_> {
76 fn as_str(&self) -> Result<&str, PexelsError> {
78 let value = match self {
79 Color::Red => "red",
80 Color::Orange => "orange",
81 Color::Yellow => "yellow",
82 Color::Green => "green",
83 Color::Turquoise => "turquoise",
84 Color::Blue => "blue",
85 Color::Violet => "violet",
86 Color::Pink => "pink",
87 Color::Brown => "brown",
88 Color::Black => "black",
89 Color::Gray => "gray",
90 Color::White => "white",
91 Color::Hex(v) => v.0,
92 };
93
94 Ok(value)
95 }
96}
97
98pub struct Search<'a> {
100 query: &'a str,
101 page: Option<usize>,
102 per_page: Option<usize>,
103 orientation: Option<Orientation>,
104 size: Option<Size>,
105 color: Option<Color<'a>>,
106 locale: Option<Locale>,
107}
108
109impl<'a> Search<'a> {
110 pub fn builder() -> SearchBuilder<'a> {
112 SearchBuilder::default()
113 }
114
115 pub fn create_uri(&self) -> crate::BuilderResult {
117 let uri = format!("{PEXELS_API}/{PEXELS_VERSION}/{PEXELS_PHOTO_SEARCH_PATH}");
118
119 let mut url = Url::parse(uri.as_str())?;
120 url.query_pairs_mut().append_pair("query", self.query);
121
122 if let Some(page) = &self.page {
123 url.query_pairs_mut()
124 .append_pair("page", page.to_string().as_str());
125 }
126
127 if let Some(per_page) = &self.per_page {
128 url.query_pairs_mut()
129 .append_pair("per_page", per_page.to_string().as_str());
130 }
131
132 if let Some(orientation) = &self.orientation {
133 url.query_pairs_mut()
134 .append_pair("orientation", orientation.as_str());
135 }
136
137 if let Some(size) = &self.size {
138 url.query_pairs_mut().append_pair("size", size.as_str());
139 }
140
141 if let Some(color) = &self.color {
142 url.query_pairs_mut().append_pair("color", color.as_str()?);
143 }
144
145 if let Some(locale) = &self.locale {
146 url.query_pairs_mut().append_pair("locale", locale.as_str());
147 }
148
149 Ok(url.into())
150 }
151
152 pub async fn fetch(&self, client: &Pexels) -> Result<PhotosResponse, PexelsError> {
154 let url = self.create_uri()?;
155 let response = client.make_request(url.as_str()).await?;
156 let photos_response: PhotosResponse = serde_json::from_value(response)?;
157 Ok(photos_response)
158 }
159}
160
161#[derive(Default)]
163pub struct SearchBuilder<'a> {
164 query: &'a str,
165 page: Option<usize>,
166 per_page: Option<usize>,
167 orientation: Option<Orientation>,
168 size: Option<Size>,
169 color: Option<Color<'a>>,
170 locale: Option<Locale>,
171}
172
173impl<'a> SearchBuilder<'a> {
174 pub fn new() -> Self {
176 Self {
177 query: "",
178 page: None,
179 per_page: None,
180 orientation: None,
181 size: None,
182 color: None,
183 locale: None,
184 }
185 }
186
187 pub fn query(mut self, query: &'a str) -> Self {
189 self.query = query;
190 self
191 }
192
193 pub fn page(mut self, page: usize) -> Self {
195 self.page = Some(page);
196 self
197 }
198
199 pub fn per_page(mut self, per_page: usize) -> Self {
201 self.per_page = Some(per_page);
202 self
203 }
204
205 pub fn orientation(mut self, orientation: Orientation) -> Self {
207 self.orientation = Some(orientation);
208 self
209 }
210
211 pub fn size(mut self, size: Size) -> Self {
213 self.size = Some(size);
214 self
215 }
216
217 pub fn color(mut self, color: Color<'a>) -> Self {
219 self.color = Some(color);
220 self
221 }
222
223 pub fn locale(mut self, locale: Locale) -> Self {
225 self.locale = Some(locale);
226 self
227 }
228
229 pub fn build(self) -> Search<'a> {
231 Search {
232 query: self.query,
233 page: self.page,
234 per_page: self.per_page,
235 orientation: self.orientation,
236 size: self.size,
237 color: self.color,
238 locale: self.locale,
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_query() {
249 let uri = SearchBuilder::new().query("bar").build();
250 assert_eq!(
251 "https://api.pexels.com/v1/search?query=bar",
252 uri.create_uri().unwrap()
253 );
254 }
255
256 #[test]
257 fn test_page() {
258 let uri = SearchBuilder::new().page(1).build();
259 assert_eq!(
260 "https://api.pexels.com/v1/search?query=&page=1",
261 uri.create_uri().unwrap()
262 );
263 }
264
265 #[test]
266 fn test_per_page() {
267 let uri = SearchBuilder::new().per_page(1).build();
268 assert_eq!(
269 "https://api.pexels.com/v1/search?query=&per_page=1",
270 uri.create_uri().unwrap()
271 );
272 }
273
274 #[test]
275 fn test_orientation() {
276 let uri = SearchBuilder::new()
277 .orientation(Orientation::Landscape)
278 .build();
279 assert_eq!(
280 "https://api.pexels.com/v1/search?query=&orientation=landscape",
281 uri.create_uri().unwrap()
282 );
283 }
284
285 #[test]
286 fn test_size() {
287 let uri = SearchBuilder::new().size(Size::Small).build();
288 assert_eq!(
289 "https://api.pexels.com/v1/search?query=&size=small",
290 uri.create_uri().unwrap()
291 );
292 }
293
294 #[test]
295 fn test_color() {
296 let uri = SearchBuilder::new().color(Color::Pink).build();
297 assert_eq!(
298 "https://api.pexels.com/v1/search?query=&color=pink",
299 uri.create_uri().unwrap()
300 );
301 }
302
303 #[test]
304 fn test_hex_color_code() {
305 let hex_color = Hex::from_borrowed_str("#FFFFFF").unwrap();
306 let uri = SearchBuilder::new().color(Color::Hex(hex_color)).build();
307 assert_eq!(
308 "https://api.pexels.com/v1/search?query=&color=%23FFFFFF",
309 uri.create_uri().unwrap()
310 );
311 }
312
313 #[test]
314 fn test_locale() {
315 let uri = SearchBuilder::new().locale(Locale::sv_SE).build();
316 assert_eq!(
317 "https://api.pexels.com/v1/search?query=&locale=sv-SE",
318 uri.create_uri().unwrap()
319 );
320 }
321
322 #[test]
323 fn test_hex_struct_length() {
324 let hex_color = Hex::from_borrowed_str("#allanballan");
325 assert_eq!(
326 hex_color,
327 Err(PexelsError::HexColorCodeError(String::from(
328 "#allanballan is not 7 characters long."
329 )))
330 );
331 }
332
333 #[test]
334 fn test_hex_struct_box_validation() {
335 let hex_color = Hex::from_borrowed_str("FFFFFFF");
336 assert_eq!(
337 hex_color,
338 Err(PexelsError::HexColorCodeError(String::from(
339 "FFFFFFF does not start with #."
340 )))
341 );
342 }
343
344 #[test]
345 fn test_hex_struct_ascii_validation() {
346 let hex_color = Hex::from_borrowed_str("#??????");
347 assert_eq!(
348 hex_color,
349 Err(PexelsError::HexColorCodeError(String::from(
350 "#?????? have values that are not valid ASCII punctuation character."
351 )))
352 );
353 }
354}