commit c9eecd89fe484ea0d83f0d1ac144dbb3c745e041
parent 1d440b4ecffb159f5f281cbf49f2e843e57e106e
Author: Sylvia Ivory <git@sivory.net>
Date: Tue, 17 Jun 2025 02:12:30 -0700
Add collection photos api
Diffstat:
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>,
+}