//! Client for interacting with the Immich API use crate::apis::{AlbumsApi, AssetsApi, SearchApi, ServerApi, TimelineApi}; use crate::error::{ImmichError, Result}; use std::sync::Arc; use std::time::Duration; /// Configuration for the Immich client #[derive(Debug, Clone)] pub struct Config { /// Base URL of the Immich server pub base_url: String, /// API key for authentication pub api_key: Option, /// Request timeout pub timeout: Duration, /// User agent string pub user_agent: String, } impl Default for Config { fn default() -> Self { Self { base_url: String::new(), api_key: None, timeout: Duration::from_secs(30), user_agent: format!("immich-sdk/{} (Rust)", env!("CARGO_PKG_VERSION")), } } } impl Config { /// Create a new config with the given base URL pub fn new(base_url: impl Into) -> Self { Self { base_url: base_url.into(), ..Default::default() } } /// Set the API key pub fn with_api_key(mut self, api_key: impl Into) -> Self { self.api_key = Some(api_key.into()); self } /// Set the timeout pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self } /// Set a custom user agent pub fn with_user_agent(mut self, user_agent: impl Into) -> Self { self.user_agent = user_agent.into(); self } } /// Internal client data wrapped in Arc for cheap cloning #[derive(Debug)] struct ClientInner { config: Config, http: reqwest::Client, } /// Client for making requests to the Immich API /// /// This struct is cheap to clone - it uses an internal Arc to share the underlying /// HTTP client and configuration. #[derive(Debug, Clone)] pub struct Client { inner: Arc, } impl Client { /// Create a new client with the given configuration pub fn new(config: Config) -> Result { // Validate base URL let base_url = if config.base_url.ends_with('/') { config.base_url.trim_end_matches('/').to_string() } else { config.base_url.clone() }; if base_url.is_empty() { return Err(ImmichError::Config("Base URL cannot be empty".to_string())); } // Parse to validate URL let _ = url::Url::parse(&base_url)?; let http = reqwest::Client::builder() .timeout(config.timeout) .user_agent(&config.user_agent) .build()?; Ok(Self { inner: Arc::new(ClientInner { config: Config { base_url, ..config }, http, }), }) } /// Create a client from a base URL string pub fn from_url(base_url: impl Into) -> Result { Self::new(Config::new(base_url)) } /// Set the API key pub fn with_api_key(self, api_key: impl Into) -> Self { // We need to create a new ClientInner since we can't modify Arc contents let mut config = self.inner.config.clone(); config.api_key = Some(api_key.into()); Self { inner: Arc::new(ClientInner { config, http: self.inner.http.clone(), }), } } /// Get the configuration pub fn config(&self) -> &Config { &self.inner.config } /// Get the base URL pub fn base_url(&self) -> &str { &self.inner.config.base_url } /// Check if the client has an API key configured pub fn has_api_key(&self) -> bool { self.inner.config.api_key.is_some() } /// Get the underlying HTTP client pub fn http(&self) -> &reqwest::Client { &self.inner.http } /// Create an authenticated request builder pub fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder { let url = format!("{}/api{}", self.inner.config.base_url, path); let mut builder = self.inner.http.request(method, &url); // Add API key authentication if let Some(ref api_key) = self.inner.config.api_key { builder = builder.header("x-api-key", api_key); } builder } /// Execute a request and handle common error cases pub async fn execute(&self, request: reqwest::Request) -> Result { let response = self.inner.http.execute(request).await?; if response.status().is_success() { Ok(response) } else { Err(ImmichError::from_response(response).await) } } /// Make a GET request pub fn get(&self, path: &str) -> reqwest::RequestBuilder { self.request(reqwest::Method::GET, path) } /// Make a POST request pub fn post(&self, path: &str) -> reqwest::RequestBuilder { self.request(reqwest::Method::POST, path) } /// Make a PUT request pub fn put(&self, path: &str) -> reqwest::RequestBuilder { self.request(reqwest::Method::PUT, path) } /// Make a DELETE request pub fn delete(&self, path: &str) -> reqwest::RequestBuilder { self.request(reqwest::Method::DELETE, path) } /// Make a PATCH request pub fn patch(&self, path: &str) -> reqwest::RequestBuilder { self.request(reqwest::Method::PATCH, path) } /// Access the albums API pub fn albums(&self) -> AlbumsApi { AlbumsApi::new(self.clone()) } /// Access the assets API pub fn assets(&self) -> AssetsApi { AssetsApi::new(self.clone()) } /// Access the search API pub fn search(&self) -> SearchApi { SearchApi::new(self.clone()) } /// Access the server API pub fn server(&self) -> ServerApi { ServerApi::new(self.clone()) } /// Access the timeline API pub fn timeline(&self) -> TimelineApi { TimelineApi::new(self.clone()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_builder() { let config = Config::new("https://example.com") .with_api_key("test-key") .with_timeout(Duration::from_secs(60)); assert_eq!(config.base_url, "https://example.com"); assert_eq!(config.api_key, Some("test-key".to_string())); assert_eq!(config.timeout, Duration::from_secs(60)); } #[test] fn test_client_creation() { let client = Client::from_url("https://example.com").unwrap(); assert_eq!(client.base_url(), "https://example.com"); assert!(!client.has_api_key()); let config = Config::new("https://example.com").with_api_key("test-key"); let client = Client::new(config).unwrap(); assert!(client.has_api_key()); } #[test] fn test_url_normalization() { let client = Client::from_url("https://example.com/").unwrap(); assert_eq!(client.base_url(), "https://example.com"); } #[test] fn test_empty_url_error() { let result = Client::from_url(""); assert!(result.is_err()); } }