pexels_sdk/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_sdk::{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!(
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        // 检查是否为有效的 ASCII 字符
48        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
58/// Represents the desired photo color.
59pub 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    /// Returns the string representation of the color.
77    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
98/// Represents a search query to the Pexels API.
99pub 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    /// Creates a new [`SearchBuilder`] for building URI's.
111    pub fn builder() -> SearchBuilder<'a> {
112        SearchBuilder::default()
113    }
114
115    /// Creates a URI from the search parameters. [`SearchBuilder`].
116    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    /// Fetches the list of photos from the Pexels API based on the search parameters.
153    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/// Builder for [`Search`].
162#[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    /// Creates a new [`SearchBuilder`].
175    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    /// Sets the search query.
188    pub fn query(mut self, query: &'a str) -> Self {
189        self.query = query;
190        self
191    }
192
193    /// Sets the page number for the request.
194    pub fn page(mut self, page: usize) -> Self {
195        self.page = Some(page);
196        self
197    }
198
199    /// Sets the number of results per page for the request.
200    pub fn per_page(mut self, per_page: usize) -> Self {
201        self.per_page = Some(per_page);
202        self
203    }
204
205    /// Sets the desired photo orientation.
206    pub fn orientation(mut self, orientation: Orientation) -> Self {
207        self.orientation = Some(orientation);
208        self
209    }
210
211    /// Sets the minimum photo size.
212    pub fn size(mut self, size: Size) -> Self {
213        self.size = Some(size);
214        self
215    }
216
217    /// Sets the desired photo color.
218    pub fn color(mut self, color: Color<'a>) -> Self {
219        self.color = Some(color);
220        self
221    }
222
223    /// Sets the locale of the search.
224    pub fn locale(mut self, locale: Locale) -> Self {
225        self.locale = Some(locale);
226        self
227    }
228
229    /// Builds a `Search` instance from the `SearchBuilder`
230    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}