Skip to main content

pexels_api/photos/
search.rs

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/// Represents a hexadecimal color code.
8/// Used as an input value for [`Color::Hex`] when specifying a hexadecimal color code.
9///
10/// #Example
11///
12/// ```
13/// use pexels_api::{Color, Hex, SearchBuilder};
14///
15/// fn main() -> Result<(), Box<dyn std::error::Error>> {
16///        let hex_color = Hex::from_borrowed_str("#FFFFFF")?;
17///        let uri = SearchBuilder::new().color(Color::Hex(hex_color)).build();
18///        assert_eq!(
19///            "https://api.pexels.com/v1/search?query=&color=%23FFFFFF",
20///            uri.create_uri()?
21///        );
22///        Ok(())
23///  }
24/// ```
25///
26/// # Errors
27/// Returns [`PexelsError::HexColorCodeError`] if the string is not a valid hexadecimal color code.
28#[derive(Debug, PartialEq)]
29pub struct Hex<'a>(&'a str);
30
31impl<'a> Hex<'a> {
32    /// Create a new [`Hex`] from a string literal.
33    #[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        // 检查是否为有效的 ASCII 字符
44        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
54/// Represents the desired photo color.
55pub 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    /// Returns the string representation of the color.
73    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
94/// Represents a search query to the Pexels API.
95pub 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    /// Creates a new [`SearchBuilder`] for building URI's.
107    pub fn builder() -> SearchBuilder<'a> {
108        SearchBuilder::default()
109    }
110
111    /// Creates a URI from the search parameters. [`SearchBuilder`].
112    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    /// Fetches the list of photos from the Pexels API based on the search parameters.
146    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/// Builder for [`Search`].
155#[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    /// Creates a new [`SearchBuilder`].
168    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    /// Sets the search query.
181    pub fn query(mut self, query: &'a str) -> Self {
182        self.query = query;
183        self
184    }
185
186    /// Sets the page number for the request.
187    pub fn page(mut self, page: usize) -> Self {
188        self.page = Some(page);
189        self
190    }
191
192    /// Sets the number of results per page for the request.
193    pub fn per_page(mut self, per_page: usize) -> Self {
194        self.per_page = Some(per_page);
195        self
196    }
197
198    /// Sets the desired photo orientation.
199    pub fn orientation(mut self, orientation: Orientation) -> Self {
200        self.orientation = Some(orientation);
201        self
202    }
203
204    /// Sets the minimum photo size.
205    pub fn size(mut self, size: Size) -> Self {
206        self.size = Some(size);
207        self
208    }
209
210    /// Sets the desired photo color.
211    pub fn color(mut self, color: Color<'a>) -> Self {
212        self.color = Some(color);
213        self
214    }
215
216    /// Sets the locale of the search.
217    pub fn locale(mut self, locale: Locale) -> Self {
218        self.locale = Some(locale);
219        self
220    }
221
222    /// Builds a `Search` instance from the `SearchBuilder`
223    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}