fjordgard

A desktop clock application
Log | Files | Refs | README | LICENSE

lib.rs (4376B)


      1 use std::fmt::Debug;
      2 
      3 use bytes::Bytes;
      4 use reqwest::{
      5     Client, StatusCode,
      6     header::{self, HeaderMap, HeaderValue},
      7 };
      8 
      9 pub use error::Error;
     10 use error::Result;
     11 use model::*;
     12 use serde::{Serialize, de::DeserializeOwned};
     13 mod error;
     14 pub mod model;
     15 
     16 const USER_AGENT: &str = concat!("fjordgard/", env!("CARGO_PKG_VERSION"));
     17 const UNSPLASH_API_HOST: &str = "https://api.unsplash.com/";
     18 
     19 #[derive(Clone)]
     20 pub struct UnsplashClient {
     21     client: Client,
     22 }
     23 
     24 impl UnsplashClient {
     25     pub fn new(api_key: &str) -> Result<Self> {
     26         let mut headers = HeaderMap::new();
     27         headers.insert("Accept-Version", HeaderValue::from_static("v1"));
     28 
     29         let mut api_key = HeaderValue::from_str(&format!("Client-ID {api_key}"))
     30             .map_err(|_| Error::InvalidAPIKey)?;
     31         api_key.set_sensitive(true);
     32         headers.insert(header::AUTHORIZATION, api_key);
     33 
     34         let client = Client::builder()
     35             .default_headers(headers)
     36             .user_agent(USER_AGENT)
     37             .build()?;
     38 
     39         Ok(Self { client })
     40     }
     41 
     42     async fn request<Q: Serialize, T: DeserializeOwned + Debug>(
     43         &self,
     44         route: &str,
     45         query: Option<Q>,
     46     ) -> Result<(T, HeaderMap)> {
     47         let mut req = self.client.get(format!("{UNSPLASH_API_HOST}/{route}"));
     48 
     49         if let Some(ref query) = query {
     50             req = req.query(query)
     51         };
     52 
     53         let res = req.send().await?;
     54 
     55         if res.status() == StatusCode::UNAUTHORIZED {
     56             return Err(Error::InvalidAPIKey);
     57         }
     58 
     59         let headers = res.headers().clone();
     60         let body: UnsplashResponse = res.json().await?;
     61 
     62         match body {
     63             UnsplashResponse::Error { errors } => Err(Error::Unsplash(errors.join(", "))),
     64             UnsplashResponse::Success(v) => match serde_json::from_value(v) {
     65                 Ok(o) => Ok((o, headers)),
     66                 Err(e) => Err(Error::SerdeJson(e)),
     67             },
     68         }
     69     }
     70 
     71     // Endpoint: `/collections/:id/photos`
     72     pub async fn collection_photos(
     73         &self,
     74         id: &str,
     75         opt: Option<CollectionPhotosOptions>,
     76     ) -> Result<CollectionPhotos> {
     77         let (photos, headers) = self
     78             .request(&format!("collections/{id}/photos"), opt)
     79             .await?;
     80 
     81         Ok(CollectionPhotos {
     82             collection_total: headers
     83                 .get("X-Total")
     84                 .ok_or(Error::MissingHeader("X-Total"))?
     85                 .to_str()
     86                 .map_err(|_| Error::MalformedResponse)?
     87                 .parse::<usize>()
     88                 .map_err(|_| Error::MalformedResponse)?,
     89             per_page: headers
     90                 .get("X-Per-Page")
     91                 .ok_or(Error::MissingHeader("X-Per-Page"))?
     92                 .to_str()
     93                 .map_err(|_| Error::MalformedResponse)?
     94                 .parse::<usize>()
     95                 .map_err(|_| Error::MalformedResponse)?,
     96             photos,
     97         })
     98     }
     99 
    100     pub async fn collection(&self, id: &str) -> Result<Collection> {
    101         let (collection, _) = self
    102             .request(&format!("collections/{id}"), None::<()>)
    103             .await?;
    104 
    105         Ok(collection)
    106     }
    107 
    108     pub async fn download_photo(
    109         &self,
    110         photo: &Photo,
    111         opts: Option<PhotoFetchOptions>,
    112     ) -> Result<Bytes> {
    113         let mut req = self.client.get(&photo.urls.raw);
    114 
    115         if let Some(ref query) = opts {
    116             req = req.query(query);
    117         }
    118 
    119         Ok(req.send().await?.error_for_status()?.bytes().await?)
    120     }
    121 }
    122 
    123 #[cfg(test)]
    124 mod tests {
    125     use std::env;
    126 
    127     use super::*;
    128 
    129     fn api_key() -> String {
    130         env::var("UNSPLASH_KEY").expect("expected env:UNSPLASH_KEY")
    131     }
    132 
    133     #[tokio::test]
    134     async fn collection_photos() {
    135         let client = UnsplashClient::new(&api_key()).unwrap();
    136         let collection = client
    137             .collection_photos(
    138                 "1053828",
    139                 Some(CollectionPhotosOptions {
    140                     per_page: Some(5),
    141                     ..Default::default()
    142                 }),
    143             )
    144             .await
    145             .unwrap();
    146 
    147         assert_eq!(collection.per_page, 5);
    148     }
    149 
    150     #[tokio::test]
    151     async fn collection() {
    152         let client = UnsplashClient::new(&api_key()).unwrap();
    153         let collection = client.collection("1053828").await.unwrap();
    154 
    155         assert_eq!(collection.title, "Tabliss Official");
    156     }
    157 }