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