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!("{} 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        // 检查是否为有效的 ASCII 字符
44        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
55/// Represents the desired photo color.
56pub 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    /// Returns the string representation of the color.
74    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
95/// Represents a search query to the Pexels API.
96pub 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    /// Creates a new [`SearchBuilder`] for building URI's.
108    pub fn builder() -> SearchBuilder<'a> {
109        SearchBuilder::default()
110    }
111
112    /// Creates a URI from the search parameters. [`SearchBuilder`].
113    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    /// Fetches the list of photos from the Pexels API based on the search parameters.
147    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/// Builder for [`Search`].
156#[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    /// Creates a new [`SearchBuilder`].
169    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    /// Sets the search query.
182    pub fn query(mut self, query: &'a str) -> Self {
183        self.query = query;
184        self
185    }
186
187    /// Sets the page number for the request.
188    pub fn page(mut self, page: usize) -> Self {
189        self.page = Some(page);
190        self
191    }
192
193    /// Sets the number of results per page for the request.
194    pub fn per_page(mut self, per_page: usize) -> Self {
195        self.per_page = Some(per_page);
196        self
197    }
198
199    /// Sets the desired photo orientation.
200    pub fn orientation(mut self, orientation: Orientation) -> Self {
201        self.orientation = Some(orientation);
202        self
203    }
204
205    /// Sets the minimum photo size.
206    pub fn size(mut self, size: Size) -> Self {
207        self.size = Some(size);
208        self
209    }
210
211    /// Sets the desired photo color.
212    pub fn color(mut self, color: Color<'a>) -> Self {
213        self.color = Some(color);
214        self
215    }
216
217    /// Sets the locale of the search.
218    pub fn locale(mut self, locale: Locale) -> Self {
219        self.locale = Some(locale);
220        self
221    }
222
223    /// Builds a `Search` instance from the `SearchBuilder`
224    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}