fjordgard

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

commit c9eecd89fe484ea0d83f0d1ac144dbb3c745e041
parent 1d440b4ecffb159f5f281cbf49f2e843e57e106e
Author: Sylvia Ivory <git@sivory.net>
Date:   Tue, 17 Jun 2025 02:12:30 -0700

Add collection photos api

Diffstat:
MCargo.lock | 2++
Mcrates/unsplash/Cargo.toml | 4++++
Mcrates/unsplash/src/lib.rs | 36++++++++++++++++++++++++++++++++----
Mcrates/unsplash/src/model.rs | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 158 insertions(+), 5 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -235,7 +235,9 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_with", "thiserror", + "tokio", ] [[package]] diff --git a/crates/unsplash/Cargo.toml b/crates/unsplash/Cargo.toml @@ -7,4 +7,8 @@ edition = "2024" reqwest = { version = "0.12.20", features = ["json"] } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" +serde_with = "3.13.0" thiserror = "2.0.12" + +[dev-dependencies] +tokio = { version = "1.45.1", features = ["rt", "macros"] } diff --git a/crates/unsplash/src/lib.rs b/crates/unsplash/src/lib.rs @@ -1,7 +1,8 @@ use std::fmt::Debug; use reqwest::{ - header::{self, HeaderMap, HeaderValue}, Client, StatusCode + Client, StatusCode, + header::{self, HeaderMap, HeaderValue}, }; pub use error::Error; @@ -50,7 +51,7 @@ impl UnsplashClient { let res = req.send().await?; if res.status() == StatusCode::UNAUTHORIZED { - return Err(Error::InvalidAPIKey) + return Err(Error::InvalidAPIKey); } let body: UnsplashResponse = res.json().await?; @@ -59,8 +60,35 @@ impl UnsplashClient { UnsplashResponse::Error { errors } => Err(Error::Unsplash(errors.join(", "))), UnsplashResponse::Success(v) => match serde_json::from_value(v) { Ok(o) => Ok(o), - Err(e) => Err(Error::SerdeJson(e)) - } + Err(e) => Err(Error::SerdeJson(e)), + }, } } + + // Endpoint: `/collections/:id/photos` + pub async fn collection_photos( + &self, + id: &str, + opt: Option<CollectionPhotosOptions>, + ) -> Result<Vec<Photo>> { + self.request(&format!("collections/{id}/photos"), opt).await + } +} + +#[cfg(test)] +mod tests { + use std::env; + + use super::*; + + fn api_key() -> String { + env::var("UNSPLASH_KEY").expect("expected env:UNSPLASH_KEY") + } + + #[tokio::test] + async fn collection_photos() { + let client = UnsplashClient::new(&api_key()).unwrap(); + + client.collection_photos("1053828", None).await.unwrap(); + } } diff --git a/crates/unsplash/src/model.rs b/crates/unsplash/src/model.rs @@ -1,4 +1,6 @@ -use serde::Deserialize; +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; #[derive(Deserialize, Debug)] #[serde(untagged)] @@ -6,3 +8,120 @@ pub(crate) enum UnsplashResponse { Success(serde_json::Value), Error { errors: Vec<String> }, } + +#[derive(Serialize)] +#[serde(rename = "lowercase")] +pub enum Orientation { + Landscape, + Portrait, + Squarish, +} + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Default)] +pub struct CollectionPhotosOptions { + pub page: Option<usize>, + pub per_page: Option<usize>, + pub orientation: Option<Orientation>, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Photo { + pub id: String, + pub slug: String, + pub alternative_slugs: HashMap<String, String>, + pub created_at: String, + pub updated_at: String, + pub promoted_at: Option<String>, + pub width: usize, + pub height: usize, + pub color: String, + pub blur_hash: String, + pub description: Option<String>, + pub alt_description: Option<String>, + pub urls: PhotoUrls, + pub links: PhotoLinks, + pub likes: usize, + pub liked_by_user: bool, + pub topic_submissions: HashMap<String, TopicSubmission>, + pub asset_type: String, + pub user: User, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct PhotoUrls { + pub raw: String, + pub full: String, + pub regular: String, + pub small: String, + pub thumb: String, + pub small_s3: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct PhotoLinks { + #[serde(rename = "self")] + pub this: String, + pub html: String, + pub download: String, + pub download_location: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct TopicSubmission { + pub status: String, + pub approved_on: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct User { + pub id: String, + pub updated_at: String, + pub username: String, + pub first_name: String, + pub last_name: Option<String>, + pub twitter_username: Option<String>, + pub portfolio_url: Option<String>, + pub bio: Option<String>, + pub location: Option<String>, + pub links: UserLinks, + pub profile_image: ProfileImageLinks, + pub instagram_username: Option<String>, + pub total_collections: usize, + pub total_likes: usize, + pub total_photos: usize, + pub total_promoted_photos: usize, + pub total_illustrations: usize, + pub total_promoted_illustrations: usize, + pub accepted_tos: bool, + pub for_hire: bool, + // a bit redundant aye unsplash + pub social: UserSocials, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct UserLinks { + #[serde(rename = "self")] + pub this: String, + pub html: String, + pub photos: String, + pub likes: String, + pub portfolio: String, + pub following: Option<String>, + pub followers: Option<String>, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ProfileImageLinks { + pub small: String, + pub medium: String, + pub large: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct UserSocials { + pub instagram_username: Option<String>, + pub portfolio_url: Option<String>, + pub twitter_username: Option<String>, + pub paypal_email: Option<String>, +}