giphy_api/
lib.rs

1//! A fully generated, opinionated API client library for Giphy.
2//!
3//! [![docs.rs](https://docs.rs/giphy-api/badge.svg)](https://docs.rs/giphy-api)
4//!
5//! ## API Details
6//!
7//! Giphy API
8//!
9//! [API Terms of Service](https://developers.giphy.com/)
10//!
11//! ### Contact
12//!
13//!
14//! | email |
15//! |----|
16//! | support@giphy.com |
17//!
18//!
19//!
20//! ## Client Details
21//!
22//! This client is generated from the [Giphy OpenAPI
23//! specs](https://github.com/APIs-guru/openapi-directory/tree/main/APIs/giphy.com) based on API spec version `1.0`. This way it will remain
24//! up to date as features are added. The documentation for the crate is generated
25//! along with the code to make this library easy to use.
26//!
27//!
28//! To install the library, add the following to your `Cargo.toml` file.
29//!
30//! ```toml
31//! [dependencies]
32//! giphy-api = "0.7.0"
33//! ```
34//!
35//! ## Basic example
36//!
37//! Typical use will require intializing a `Client`. This requires
38//! a user agent string and set of credentials.
39//!
40//! ```rust
41//! use giphy_api::Client;
42//!
43//! let giphy = Client::new(
44//!     String::from("api-key"),
45//! );
46//! ```
47//!
48//! Alternatively, the library can search for most of the variables required for
49//! the client in the environment:
50//!
51//! - `GIPHY_API_KEY`
52//!
53//! And then you can create a client from the environment.
54//!
55//! ```rust
56//! use giphy_api::Client;
57//!
58//! let giphy = Client::new_from_env();
59//! ```
60//!
61#![allow(clippy::derive_partial_eq_without_eq)]
62#![allow(clippy::too_many_arguments)]
63#![allow(clippy::nonstandard_macro_braces)]
64#![allow(clippy::large_enum_variant)]
65#![allow(clippy::tabs_in_doc_comments)]
66#![allow(missing_docs)]
67#![cfg_attr(docsrs, feature(doc_cfg))]
68
69pub mod gifs;
70pub mod stickers;
71pub mod types;
72#[doc(hidden)]
73pub mod utils;
74
75pub use reqwest::{header::HeaderMap, StatusCode};
76
77#[derive(Debug)]
78pub struct Response<T> {
79    pub status: reqwest::StatusCode,
80    pub headers: reqwest::header::HeaderMap,
81    pub body: T,
82}
83
84impl<T> Response<T> {
85    pub fn new(status: reqwest::StatusCode, headers: reqwest::header::HeaderMap, body: T) -> Self {
86        Self {
87            status,
88            headers,
89            body,
90        }
91    }
92}
93
94type ClientResult<T> = Result<T, ClientError>;
95
96use thiserror::Error;
97
98/// Errors returned by the client
99#[derive(Debug, Error)]
100pub enum ClientError {
101    /// utf8 convertion error
102    #[error(transparent)]
103    FromUtf8Error(#[from] std::string::FromUtf8Error),
104    /// URL Parsing Error
105    #[error(transparent)]
106    UrlParserError(#[from] url::ParseError),
107    /// Serde JSON parsing error
108    #[error(transparent)]
109    SerdeJsonError(#[from] serde_json::Error),
110    /// Errors returned by reqwest
111    #[error(transparent)]
112    ReqwestError(#[from] reqwest::Error),
113    /// Errors returned by reqwest::header
114    #[error(transparent)]
115    InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
116    /// Errors returned by reqwest middleware
117    #[error(transparent)]
118    ReqwestMiddleWareError(#[from] reqwest_middleware::Error),
119    /// Generic HTTP Error
120    #[error("HTTP Error. Code: {status}, message: {error}")]
121    HttpError {
122        status: http::StatusCode,
123        headers: reqwest::header::HeaderMap,
124        error: String,
125    },
126}
127
128pub const FALLBACK_HOST: &str = "https://api.giphy.com/v1";
129
130mod progenitor_support {
131    use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
132
133    const PATH_SET: &AsciiSet = &CONTROLS
134        .add(b' ')
135        .add(b'"')
136        .add(b'#')
137        .add(b'<')
138        .add(b'>')
139        .add(b'?')
140        .add(b'`')
141        .add(b'{')
142        .add(b'}');
143
144    #[allow(dead_code)]
145    pub(crate) fn encode_path(pc: &str) -> String {
146        utf8_percent_encode(pc, PATH_SET).to_string()
147    }
148}
149
150#[derive(Debug, Default)]
151pub(crate) struct Message {
152    pub body: Option<reqwest::Body>,
153    pub content_type: Option<String>,
154}
155
156use std::env;
157
158#[derive(Debug, Default, Clone)]
159pub struct RootDefaultServer {}
160
161impl RootDefaultServer {
162    pub fn default_url(&self) -> &str {
163        "https://api.giphy.com/v1"
164    }
165}
166
167/// Entrypoint for interacting with the API client.
168#[derive(Clone)]
169pub struct Client {
170    host: String,
171    host_override: Option<String>,
172    token: String,
173
174    client: reqwest_middleware::ClientWithMiddleware,
175}
176
177impl Client {
178    /// Create a new Client struct.
179    ///
180    /// # Panics
181    ///
182    /// This function will panic if the internal http client fails to create
183    pub fn new<T>(token: T) -> Self
184    where
185        T: ToString,
186    {
187        let client = reqwest::Client::builder()
188            .redirect(reqwest::redirect::Policy::none())
189            .build();
190        let retry_policy =
191            reqwest_retry::policies::ExponentialBackoff::builder().build_with_max_retries(3);
192        match client {
193            Ok(c) => {
194                let client = reqwest_middleware::ClientBuilder::new(c)
195                    // Trace HTTP requests. See the tracing crate to make use of these traces.
196                    .with(reqwest_tracing::TracingMiddleware::default())
197                    // Retry failed requests.
198                    .with(reqwest_conditional_middleware::ConditionalMiddleware::new(
199                        reqwest_retry::RetryTransientMiddleware::new_with_policy(retry_policy),
200                        |req: &reqwest::Request| req.try_clone().is_some(),
201                    ))
202                    .build();
203
204                let host = RootDefaultServer::default().default_url().to_string();
205
206                Client {
207                    host,
208                    host_override: None,
209                    token: token.to_string(),
210
211                    client,
212                }
213            }
214            Err(e) => panic!("creating reqwest client failed: {:?}", e),
215        }
216    }
217
218    /// Override the host for all endpoins in the client.
219    pub fn with_host_override<H>(&mut self, host: H) -> &mut Self
220    where
221        H: ToString,
222    {
223        self.host_override = Some(host.to_string());
224        self
225    }
226
227    /// Disables the global host override for the client.
228    pub fn remove_host_override(&mut self) -> &mut Self {
229        self.host_override = None;
230        self
231    }
232
233    pub fn get_host_override(&self) -> Option<&str> {
234        self.host_override.as_deref()
235    }
236
237    pub(crate) fn url(&self, path: &str, host: Option<&str>) -> String {
238        format!(
239            "{}{}",
240            self.get_host_override()
241                .or(host)
242                .unwrap_or(self.host.as_str()),
243            path
244        )
245    }
246
247    /// Create a new Client struct from environment variables.
248    ///
249    /// The following environment variables are expected to be set:
250    ///   * `GIPHY_API_KEY`
251    ///
252    /// # Panics
253    ///
254    /// This function will panic if the expected environment variables can not be found
255    pub fn new_from_env() -> Self {
256        let token = env::var("GIPHY_API_KEY").expect("must set GIPHY_API_KEY");
257
258        Client::new(token)
259    }
260
261    async fn url_and_auth(&self, uri: &str) -> ClientResult<(reqwest::Url, Option<String>)> {
262        let parsed_url = uri.parse::<reqwest::Url>()?;
263        let auth = format!("Bearer {}", self.token);
264        Ok((parsed_url, Some(auth)))
265    }
266
267    async fn request_raw(
268        &self,
269        method: reqwest::Method,
270        uri: &str,
271        message: Message,
272    ) -> ClientResult<reqwest::Response> {
273        let (url, auth) = self.url_and_auth(uri).await?;
274        let instance = <&Client>::clone(&self);
275        let mut req = instance.client.request(method.clone(), url);
276        // Set the default headers.
277        req = req.header(
278            reqwest::header::ACCEPT,
279            reqwest::header::HeaderValue::from_static("application/json"),
280        );
281
282        if let Some(content_type) = &message.content_type {
283            req = req.header(
284                reqwest::header::CONTENT_TYPE,
285                reqwest::header::HeaderValue::from_str(content_type).unwrap(),
286            );
287        } else {
288            req = req.header(
289                reqwest::header::CONTENT_TYPE,
290                reqwest::header::HeaderValue::from_static("application/json"),
291            );
292        }
293
294        if let Some(auth_str) = auth {
295            req = req.header(http::header::AUTHORIZATION, &*auth_str);
296        }
297        if let Some(body) = message.body {
298            req = req.body(body);
299        }
300        Ok(req.send().await?)
301    }
302
303    async fn request<Out>(
304        &self,
305        method: reqwest::Method,
306        uri: &str,
307        message: Message,
308    ) -> ClientResult<crate::Response<Out>>
309    where
310        Out: serde::de::DeserializeOwned + 'static + Send,
311    {
312        let response = self.request_raw(method, uri, message).await?;
313
314        let status = response.status();
315        let headers = response.headers().clone();
316
317        let response_body = response.bytes().await?;
318
319        if status.is_success() {
320            log::debug!("Received successful response. Read payload.");
321            let parsed_response = if status == http::StatusCode::NO_CONTENT
322                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
323            {
324                serde_json::from_str("null")?
325            } else {
326                serde_json::from_slice::<Out>(&response_body)?
327            };
328            Ok(crate::Response::new(status, headers, parsed_response))
329        } else {
330            let error = if response_body.is_empty() {
331                ClientError::HttpError {
332                    status,
333                    headers,
334                    error: "empty response".into(),
335                }
336            } else {
337                ClientError::HttpError {
338                    status,
339                    headers,
340                    error: String::from_utf8_lossy(&response_body).into(),
341                }
342            };
343
344            Err(error)
345        }
346    }
347
348    async fn request_with_links<Out>(
349        &self,
350        method: http::Method,
351        uri: &str,
352        message: Message,
353    ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Out>)>
354    where
355        Out: serde::de::DeserializeOwned + 'static + Send,
356    {
357        let response = self.request_raw(method, uri, message).await?;
358
359        let status = response.status();
360        let headers = response.headers().clone();
361        let link = response
362            .headers()
363            .get(http::header::LINK)
364            .and_then(|l| l.to_str().ok())
365            .and_then(|l| parse_link_header::parse(l).ok())
366            .as_ref()
367            .and_then(crate::utils::next_link);
368
369        let response_body = response.bytes().await?;
370
371        if status.is_success() {
372            log::debug!("Received successful response. Read payload.");
373
374            let parsed_response = if status == http::StatusCode::NO_CONTENT
375                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
376            {
377                serde_json::from_str("null")?
378            } else {
379                serde_json::from_slice::<Out>(&response_body)?
380            };
381            Ok((link, crate::Response::new(status, headers, parsed_response)))
382        } else {
383            let error = if response_body.is_empty() {
384                ClientError::HttpError {
385                    status,
386                    headers,
387                    error: "empty response".into(),
388                }
389            } else {
390                ClientError::HttpError {
391                    status,
392                    headers,
393                    error: String::from_utf8_lossy(&response_body).into(),
394                }
395            };
396            Err(error)
397        }
398    }
399
400    /* TODO: make this more DRY */
401    #[allow(dead_code)]
402    async fn post_form<Out>(
403        &self,
404        uri: &str,
405        form: reqwest::multipart::Form,
406    ) -> ClientResult<crate::Response<Out>>
407    where
408        Out: serde::de::DeserializeOwned + 'static + Send,
409    {
410        let (url, auth) = self.url_and_auth(uri).await?;
411
412        let instance = <&Client>::clone(&self);
413
414        let mut req = instance.client.request(http::Method::POST, url);
415
416        // Set the default headers.
417        req = req.header(
418            reqwest::header::ACCEPT,
419            reqwest::header::HeaderValue::from_static("application/json"),
420        );
421
422        if let Some(auth_str) = auth {
423            req = req.header(http::header::AUTHORIZATION, &*auth_str);
424        }
425
426        req = req.multipart(form);
427
428        let response = req.send().await?;
429
430        let status = response.status();
431        let headers = response.headers().clone();
432
433        let response_body = response.bytes().await?;
434
435        if status.is_success() {
436            log::debug!("Received successful response. Read payload.");
437            let parsed_response = if status == http::StatusCode::NO_CONTENT
438                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
439            {
440                serde_json::from_str("null")?
441            } else if std::any::TypeId::of::<Out>() == std::any::TypeId::of::<String>() {
442                // Parse the output as a string.
443                let s = String::from_utf8(response_body.to_vec())?;
444                serde_json::from_value(serde_json::json!(&s))?
445            } else {
446                serde_json::from_slice::<Out>(&response_body)?
447            };
448            Ok(crate::Response::new(status, headers, parsed_response))
449        } else {
450            let error = if response_body.is_empty() {
451                ClientError::HttpError {
452                    status,
453                    headers,
454                    error: "empty response".into(),
455                }
456            } else {
457                ClientError::HttpError {
458                    status,
459                    headers,
460                    error: String::from_utf8_lossy(&response_body).into(),
461                }
462            };
463
464            Err(error)
465        }
466    }
467
468    /* TODO: make this more DRY */
469    #[allow(dead_code)]
470    async fn request_with_accept_mime<Out>(
471        &self,
472        method: reqwest::Method,
473        uri: &str,
474        accept_mime_type: &str,
475    ) -> ClientResult<crate::Response<Out>>
476    where
477        Out: serde::de::DeserializeOwned + 'static + Send,
478    {
479        let (url, auth) = self.url_and_auth(uri).await?;
480
481        let instance = <&Client>::clone(&self);
482
483        let mut req = instance.client.request(method, url);
484
485        // Set the default headers.
486        req = req.header(
487            reqwest::header::ACCEPT,
488            reqwest::header::HeaderValue::from_str(accept_mime_type)?,
489        );
490
491        if let Some(auth_str) = auth {
492            req = req.header(http::header::AUTHORIZATION, &*auth_str);
493        }
494
495        let response = req.send().await?;
496
497        let status = response.status();
498        let headers = response.headers().clone();
499
500        let response_body = response.bytes().await?;
501
502        if status.is_success() {
503            log::debug!("Received successful response. Read payload.");
504            let parsed_response = if status == http::StatusCode::NO_CONTENT
505                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
506            {
507                serde_json::from_str("null")?
508            } else if std::any::TypeId::of::<Out>() == std::any::TypeId::of::<String>() {
509                // Parse the output as a string.
510                let s = String::from_utf8(response_body.to_vec())?;
511                serde_json::from_value(serde_json::json!(&s))?
512            } else {
513                serde_json::from_slice::<Out>(&response_body)?
514            };
515            Ok(crate::Response::new(status, headers, parsed_response))
516        } else {
517            let error = if response_body.is_empty() {
518                ClientError::HttpError {
519                    status,
520                    headers,
521                    error: "empty response".into(),
522                }
523            } else {
524                ClientError::HttpError {
525                    status,
526                    headers,
527                    error: String::from_utf8_lossy(&response_body).into(),
528                }
529            };
530
531            Err(error)
532        }
533    }
534
535    /* TODO: make this more DRY */
536    #[allow(dead_code)]
537    async fn request_with_mime<Out>(
538        &self,
539        method: reqwest::Method,
540        uri: &str,
541        content: &[u8],
542        mime_type: &str,
543    ) -> ClientResult<crate::Response<Out>>
544    where
545        Out: serde::de::DeserializeOwned + 'static + Send,
546    {
547        let (url, auth) = self.url_and_auth(uri).await?;
548
549        let instance = <&Client>::clone(&self);
550
551        let mut req = instance.client.request(method, url);
552
553        // Set the default headers.
554        req = req.header(
555            reqwest::header::ACCEPT,
556            reqwest::header::HeaderValue::from_static("application/json"),
557        );
558        req = req.header(
559            reqwest::header::CONTENT_TYPE,
560            reqwest::header::HeaderValue::from_bytes(mime_type.as_bytes()).unwrap(),
561        );
562        // We are likely uploading a file so add the right headers.
563        req = req.header(
564            reqwest::header::HeaderName::from_static("x-upload-content-type"),
565            reqwest::header::HeaderValue::from_static("application/octet-stream"),
566        );
567        req = req.header(
568            reqwest::header::HeaderName::from_static("x-upload-content-length"),
569            reqwest::header::HeaderValue::from_bytes(format!("{}", content.len()).as_bytes())
570                .unwrap(),
571        );
572
573        if let Some(auth_str) = auth {
574            req = req.header(http::header::AUTHORIZATION, &*auth_str);
575        }
576
577        if content.len() > 1 {
578            let b = bytes::Bytes::copy_from_slice(content);
579            // We are uploading a file so add that as the body.
580            req = req.body(b);
581        }
582
583        let response = req.send().await?;
584
585        let status = response.status();
586        let headers = response.headers().clone();
587
588        let response_body = response.bytes().await?;
589
590        if status.is_success() {
591            log::debug!("Received successful response. Read payload.");
592            let parsed_response = if status == http::StatusCode::NO_CONTENT
593                || std::any::TypeId::of::<Out>() == std::any::TypeId::of::<()>()
594            {
595                serde_json::from_str("null")?
596            } else {
597                serde_json::from_slice::<Out>(&response_body)?
598            };
599            Ok(crate::Response::new(status, headers, parsed_response))
600        } else {
601            let error = if response_body.is_empty() {
602                ClientError::HttpError {
603                    status,
604                    headers,
605                    error: "empty response".into(),
606                }
607            } else {
608                ClientError::HttpError {
609                    status,
610                    headers,
611                    error: String::from_utf8_lossy(&response_body).into(),
612                }
613            };
614
615            Err(error)
616        }
617    }
618
619    async fn request_entity<D>(
620        &self,
621        method: http::Method,
622        uri: &str,
623        message: Message,
624    ) -> ClientResult<crate::Response<D>>
625    where
626        D: serde::de::DeserializeOwned + 'static + Send,
627    {
628        let r = self.request(method, uri, message).await?;
629        Ok(r)
630    }
631
632    #[allow(dead_code)]
633    async fn get<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
634    where
635        D: serde::de::DeserializeOwned + 'static + Send,
636    {
637        self.request_entity(http::Method::GET, uri, message).await
638    }
639
640    #[allow(dead_code)]
641    async fn get_all_pages<D>(&self, uri: &str, _message: Message) -> ClientResult<Response<Vec<D>>>
642    where
643        D: serde::de::DeserializeOwned + 'static + Send,
644    {
645        // TODO: implement this.
646        self.unfold(uri).await
647    }
648
649    /// "unfold" paginated results of a vector of items
650    #[allow(dead_code)]
651    async fn unfold<D>(&self, uri: &str) -> ClientResult<crate::Response<Vec<D>>>
652    where
653        D: serde::de::DeserializeOwned + 'static + Send,
654    {
655        let mut global_items = Vec::new();
656        let (new_link, mut response) = self.get_pages(uri).await?;
657        let mut link = new_link;
658        while !response.body.is_empty() {
659            global_items.append(&mut response.body);
660            // We need to get the next link.
661            if let Some(url) = &link {
662                let url = reqwest::Url::parse(&url.0)?;
663                let (new_link, new_response) = self.get_pages_url(&url).await?;
664                link = new_link;
665                response = new_response;
666            }
667        }
668
669        Ok(Response::new(
670            response.status,
671            response.headers,
672            global_items,
673        ))
674    }
675
676    #[allow(dead_code)]
677    async fn get_pages<D>(
678        &self,
679        uri: &str,
680    ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Vec<D>>)>
681    where
682        D: serde::de::DeserializeOwned + 'static + Send,
683    {
684        self.request_with_links(http::Method::GET, uri, Message::default())
685            .await
686    }
687
688    #[allow(dead_code)]
689    async fn get_pages_url<D>(
690        &self,
691        url: &reqwest::Url,
692    ) -> ClientResult<(Option<crate::utils::NextLink>, crate::Response<Vec<D>>)>
693    where
694        D: serde::de::DeserializeOwned + 'static + Send,
695    {
696        self.request_with_links(http::Method::GET, url.as_str(), Message::default())
697            .await
698    }
699
700    #[allow(dead_code)]
701    async fn post<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
702    where
703        D: serde::de::DeserializeOwned + 'static + Send,
704    {
705        self.request_entity(http::Method::POST, uri, message).await
706    }
707
708    #[allow(dead_code)]
709    async fn patch<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
710    where
711        D: serde::de::DeserializeOwned + 'static + Send,
712    {
713        self.request_entity(http::Method::PATCH, uri, message).await
714    }
715
716    #[allow(dead_code)]
717    async fn put<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
718    where
719        D: serde::de::DeserializeOwned + 'static + Send,
720    {
721        self.request_entity(http::Method::PUT, uri, message).await
722    }
723
724    #[allow(dead_code)]
725    async fn delete<D>(&self, uri: &str, message: Message) -> ClientResult<crate::Response<D>>
726    where
727        D: serde::de::DeserializeOwned + 'static + Send,
728    {
729        self.request_entity(http::Method::DELETE, uri, message)
730            .await
731    }
732
733    /// Return a reference to an interface that provides access to gifs operations.
734    pub fn gifs(&self) -> gifs::Gifs {
735        gifs::Gifs::new(self.clone())
736    }
737
738    /// Return a reference to an interface that provides access to stickers operations.
739    pub fn stickers(&self) -> stickers::Stickers {
740        stickers::Stickers::new(self.clone())
741    }
742}