Skip to main content

pexels_api/
client.rs

1use reqwest::{header, Client, StatusCode};
2use std::time::Duration;
3use url::Url;
4
5use crate::models::{CollectionsPage, MediaPage, Photo, PhotosPage, Video, VideosPage};
6use crate::search::{
7    CollectionMediaParams, PaginationParams, PopularVideoParams, SearchParams, VideoSearchParams,
8};
9use crate::PexelsError;
10
11/// Main client for the Pexels API
12///
13/// This client provides methods to interact with all endpoints of the Pexels API
14/// and handles authentication, request building and response parsing.
15pub struct PexelsClient {
16    /// API key for authentication with Pexels API
17    api_key: String,
18
19    /// HTTP client with connection pooling and configurable timeouts
20    client: Client,
21
22    /// Base URL for the Pexels API
23    base_url: String,
24}
25
26impl PexelsClient {
27    /// Creates a new PexelsClient with the provided API key
28    ///
29    /// # Arguments
30    ///
31    /// * `api_key` - The Pexels API key
32    ///
33    /// # Returns
34    ///
35    /// A new instance of PexelsClient
36    ///
37    /// # Example
38    ///
39    /// ```
40    /// use pexels_api::PexelsClient;
41    ///
42    /// let client = PexelsClient::new("your_api_key");
43    /// ```
44    pub fn new<S: Into<String>>(api_key: S) -> Self {
45        let client = Client::builder()
46            .timeout(Duration::from_secs(30))
47            .pool_max_idle_per_host(10)
48            .build()
49            .unwrap_or_default();
50
51        Self { api_key: api_key.into(), client, base_url: "https://api.pexels.com/v1".to_string() }
52    }
53
54    /// Creates a new PexelsClient with custom configuration
55    ///
56    /// # Arguments
57    ///
58    /// * `api_key` - The Pexels API key
59    /// * `timeout` - Request timeout in seconds
60    /// * `max_idle_connections` - Maximum number of idle connections per host
61    ///
62    /// # Returns
63    ///
64    /// A new instance of PexelsClient
65    pub fn with_config<S: Into<String>>(
66        api_key: S,
67        timeout: u64,
68        max_idle_connections: usize,
69    ) -> Self {
70        let client = Client::builder()
71            .timeout(Duration::from_secs(timeout))
72            .pool_max_idle_per_host(max_idle_connections)
73            .build()
74            .unwrap_or_default();
75
76        Self { api_key: api_key.into(), client, base_url: "https://api.pexels.com/v1".to_string() }
77    }
78
79    /// Sets a custom base URL for the Pexels API
80    ///
81    /// # Arguments
82    ///
83    /// * `base_url` - The custom base URL
84    ///
85    /// # Returns
86    ///
87    /// Self for method chaining
88    pub fn with_base_url<S: Into<String>>(mut self, base_url: S) -> Self {
89        self.base_url = base_url.into();
90        self
91    }
92
93    /// Search for photos matching the specified query and parameters
94    ///
95    /// # Arguments
96    ///
97    /// * `query` - The search query
98    /// * `params` - Additional search parameters (pagination, filters, etc.)
99    ///
100    /// # Returns
101    ///
102    /// A Result containing the photos search response or an error
103    ///
104    /// # Example
105    ///
106    /// ```rust,no_run
107    /// use pexels_api::{PexelsClient, SearchParams,Size};
108    ///
109    /// #[tokio::main]
110    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
111    ///     let client = PexelsClient::new("your_api_key");
112    ///     let params = SearchParams::new()
113    ///         .page(1)
114    ///         .per_page(15)
115    ///         .size(Size::Large);
116    ///
117    ///     let photos = client.search_photos("nature", &params).await?;
118    ///     println!("Found {} photos", photos.total_results);
119    ///     Ok(())
120    /// }
121    /// ```
122    pub async fn search_photos(
123        &self,
124        query: &str,
125        params: &SearchParams,
126    ) -> Result<PhotosPage, PexelsError> {
127        let mut url = Url::parse(&format!("{}/search", self.base_url))?;
128
129        // Add query parameter
130        url.query_pairs_mut().append_pair("query", query);
131
132        // Add all search parameters
133        for (key, value) in params.to_query_params() {
134            url.query_pairs_mut().append_pair(&key, &value);
135        }
136
137        let response = self.send_request(url).await?;
138
139        match response.status() {
140            StatusCode::OK => {
141                let photos_page: PhotosPage = response.json().await?;
142                Ok(photos_page)
143            }
144            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
145            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
146            status => {
147                Err(PexelsError::ApiError(format!("Search photos failed with status: {status}")))
148            }
149        }
150    }
151
152    /// Fetch curated/featured photos
153    ///
154    /// # Arguments
155    ///
156    /// * `params` - Pagination parameters
157    ///
158    /// # Returns
159    ///
160    /// A Result containing the curated photos response or an error
161    pub async fn curated_photos(
162        &self,
163        params: &PaginationParams,
164    ) -> Result<PhotosPage, PexelsError> {
165        let mut url = Url::parse(&format!("{}/curated", self.base_url))?;
166
167        self.append_query_params(&mut url, params.to_query_params());
168
169        let response = self.send_request(url).await?;
170
171        match response.status() {
172            StatusCode::OK => {
173                let photos_page: PhotosPage = response.json().await?;
174                Ok(photos_page)
175            }
176            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
177            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
178            status => {
179                Err(PexelsError::ApiError(format!("Curated photos failed with status: {status}")))
180            }
181        }
182    }
183
184    /// Get a specific photo by its ID
185    ///
186    /// # Arguments
187    ///
188    /// * `id` - The photo ID
189    ///
190    /// # Returns
191    ///
192    /// A Result containing the photo or an error
193    pub async fn get_photo(&self, id: u64) -> Result<Photo, PexelsError> {
194        let url = Url::parse(&format!("{}/photos/{}", self.base_url, id))?;
195
196        let response = self.send_request(url).await?;
197
198        match response.status() {
199            StatusCode::OK => {
200                let photo: Photo = response.json().await?;
201                Ok(photo)
202            }
203            StatusCode::NOT_FOUND => {
204                Err(PexelsError::NotFound(format!("Photo with ID {id} not found")))
205            }
206            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
207            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
208            status => Err(PexelsError::ApiError(format!("Get photo failed with status: {status}"))),
209        }
210    }
211
212    /// Search for videos matching the specified query and parameters
213    ///
214    /// # Arguments
215    ///
216    /// * `query` - The search query
217    /// * `params` - Additional search parameters (pagination, filters, etc.)
218    ///
219    /// # Returns
220    ///
221    /// A Result containing the videos search response or an error
222    pub async fn search_videos(
223        &self,
224        query: &str,
225        params: &VideoSearchParams,
226    ) -> Result<VideosPage, PexelsError> {
227        let mut url = Url::parse(&format!("{}/videos/search", self.base_url))?;
228
229        // Add query parameter
230        url.query_pairs_mut().append_pair("query", query);
231
232        self.append_query_params(&mut url, params.to_query_params());
233
234        let response = self.send_request(url).await?;
235
236        match response.status() {
237            StatusCode::OK => {
238                let videos_page: VideosPage = response.json().await?;
239                Ok(videos_page)
240            }
241            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
242            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
243            status => {
244                Err(PexelsError::ApiError(format!("Search videos failed with status: {status}")))
245            }
246        }
247    }
248
249    /// Fetch popular videos
250    ///
251    /// # Arguments
252    ///
253    /// * `params` - Pagination parameters
254    ///
255    /// # Returns
256    ///
257    /// A Result containing the popular videos response or an error
258    pub async fn popular_videos(
259        &self,
260        params: &PaginationParams,
261    ) -> Result<VideosPage, PexelsError> {
262        let params = PopularVideoParams::from_pagination(params);
263        self.popular_videos_with_params(&params).await
264    }
265
266    /// Fetch popular videos with documented filters.
267    ///
268    /// # Arguments
269    ///
270    /// * `params` - Pagination, size and duration filters
271    ///
272    /// # Returns
273    ///
274    /// A Result containing the popular videos response or an error
275    pub async fn popular_videos_with_params(
276        &self,
277        params: &PopularVideoParams,
278    ) -> Result<VideosPage, PexelsError> {
279        let mut url = Url::parse(&format!("{}/videos/popular", self.base_url))?;
280
281        self.append_query_params(&mut url, params.to_query_params());
282
283        let response = self.send_request(url).await?;
284
285        match response.status() {
286            StatusCode::OK => {
287                let videos_page: VideosPage = response.json().await?;
288                Ok(videos_page)
289            }
290            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
291            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
292            status => {
293                Err(PexelsError::ApiError(format!("Popular videos failed with status: {status}")))
294            }
295        }
296    }
297
298    /// Get a specific video by its ID
299    ///
300    /// # Arguments
301    ///
302    /// * `id` - The video ID
303    ///
304    /// # Returns
305    ///
306    /// A Result containing the video or an error
307    pub async fn get_video(&self, id: u64) -> Result<Video, PexelsError> {
308        let url = Url::parse(&format!("{}/videos/videos/{}", self.base_url, id))?;
309
310        let response = self.send_request(url).await?;
311
312        match response.status() {
313            StatusCode::OK => {
314                let video: Video = response.json().await?;
315                Ok(video)
316            }
317            StatusCode::NOT_FOUND => {
318                Err(PexelsError::NotFound(format!("Video with ID {id} not found")))
319            }
320            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
321            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
322            status => Err(PexelsError::ApiError(format!("Get video failed with status: {status}"))),
323        }
324    }
325
326    /// Get collections list
327    ///
328    /// # Arguments
329    ///
330    /// * `params` - Pagination parameters
331    ///
332    /// # Returns
333    ///
334    /// A Result containing the collections response or an error
335    pub async fn get_collections(
336        &self,
337        params: &PaginationParams,
338    ) -> Result<CollectionsPage, PexelsError> {
339        let mut url = Url::parse(&format!("{}/collections", self.base_url))?;
340
341        self.append_query_params(&mut url, params.to_query_params());
342
343        let response = self.send_request(url).await?;
344
345        match response.status() {
346            StatusCode::OK => {
347                let collections_page: CollectionsPage = response.json().await?;
348                Ok(collections_page)
349            }
350            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
351            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
352            status => {
353                Err(PexelsError::ApiError(format!("Get collections failed with status: {status}")))
354            }
355        }
356    }
357
358    /// Get featured collections list
359    ///
360    /// # Arguments
361    ///
362    /// * `params` - Pagination parameters
363    ///
364    /// # Returns
365    ///
366    /// A Result containing the featured collections response or an error
367    pub async fn get_featured_collections(
368        &self,
369        params: &PaginationParams,
370    ) -> Result<CollectionsPage, PexelsError> {
371        let mut url = Url::parse(&format!("{}/collections/featured", self.base_url))?;
372
373        self.append_query_params(&mut url, params.to_query_params());
374
375        let response = self.send_request(url).await?;
376
377        match response.status() {
378            StatusCode::OK => {
379                let collections_page: CollectionsPage = response.json().await?;
380                Ok(collections_page)
381            }
382            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
383            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
384            status => Err(PexelsError::ApiError(format!(
385                "Get featured collections failed with status: {status}"
386            ))),
387        }
388    }
389
390    /// Get collection media items (photos and videos)
391    ///
392    /// # Arguments
393    ///
394    /// * `id` - The collection ID
395    /// * `params` - Pagination parameters
396    ///
397    /// # Returns
398    ///
399    /// A Result containing the media response or an error
400    pub async fn get_collection_media(
401        &self,
402        id: &str,
403        params: &PaginationParams,
404    ) -> Result<MediaPage, PexelsError> {
405        let params = CollectionMediaParams::from_pagination(params);
406        self.get_collection_media_with_params(id, &params).await
407    }
408
409    /// Get collection media items (photos and videos) with documented filters
410    ///
411    /// # Arguments
412    ///
413    /// * `id` - The collection ID
414    /// * `params` - Pagination, media type and sort filters
415    ///
416    /// # Returns
417    ///
418    /// A Result containing the media response or an error
419    pub async fn get_collection_media_with_params(
420        &self,
421        id: &str,
422        params: &CollectionMediaParams,
423    ) -> Result<MediaPage, PexelsError> {
424        let mut url = Url::parse(&format!("{}/collections/{}", self.base_url, id))?;
425
426        self.append_query_params(&mut url, params.to_query_params());
427
428        let response = self.send_request(url).await?;
429
430        match response.status() {
431            StatusCode::OK => {
432                let media_page: MediaPage = response.json().await?;
433                Ok(media_page)
434            }
435            StatusCode::NOT_FOUND => {
436                Err(PexelsError::NotFound(format!("Collection with ID {id} not found")))
437            }
438            StatusCode::UNAUTHORIZED => Err(PexelsError::AuthError("Invalid API key".to_string())),
439            StatusCode::TOO_MANY_REQUESTS => Err(PexelsError::RateLimitError),
440            status => Err(PexelsError::ApiError(format!(
441                "Get collection media failed with status: {status}"
442            ))),
443        }
444    }
445
446    fn append_query_params(&self, url: &mut Url, params: Vec<(String, String)>) {
447        for (key, value) in params {
448            url.query_pairs_mut().append_pair(&key, &value);
449        }
450    }
451
452    /// Helper method to send authenticated requests to the Pexels API
453    ///
454    /// # Arguments
455    ///
456    /// * `url` - The fully constructed URL to send the request to
457    ///
458    /// # Returns
459    ///
460    /// A Result containing the HTTP response or an error
461    async fn send_request(&self, url: Url) -> Result<reqwest::Response, PexelsError> {
462        let response =
463            self.client.get(url).header(header::AUTHORIZATION, &self.api_key).send().await?;
464
465        Ok(response)
466    }
467}