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