flowglad 0.1.1

(Unofficial) Rust SDK for FlowGlad - Open source billing infrastructure
Documentation
//! Client configuration
//!
//! This module provides the configuration structure for the FlowGlad client.

use crate::error::{Error, Result};
use std::time::Duration;
use url::Url;

/// Configuration for the FlowGlad client
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Config {
    /// API key for authentication
    pub(crate) api_key: String,
    /// Base URL for the API
    pub(crate) base_url: Url,
    /// Request timeout duration
    pub(crate) timeout: Duration,
    /// Maximum number of retry attempts
    pub(crate) max_retries: u32,
}

impl Config {
    /// Create a new configuration with the given API key
    ///
    /// Uses default values for base URL (https://api.flowglad.com),
    /// timeout (30 seconds), and max retries (3).
    ///
    /// # Example
    ///
    /// ```
    /// use flowglad::Config;
    ///
    /// let config = Config::new("sk_test_...");
    /// ```
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            api_key: api_key.into(),
            base_url: Url::parse("https://app.flowglad.com/api/v1/").unwrap(),
            timeout: Duration::from_secs(30),
            max_retries: 3,
        }
    }

    /// Create a configuration from environment variables
    ///
    /// Reads the API key from the `FLOWGLAD_API_KEY` environment variable.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use flowglad::Config;
    ///
    /// let config = Config::from_env().expect("FLOWGLAD_API_KEY not set");
    /// ```
    pub fn from_env() -> Result<Self> {
        let api_key = std::env::var("FLOWGLAD_API_KEY")
            .map_err(|_| Error::Config("FLOWGLAD_API_KEY environment variable not set".into()))?;

        Ok(Self::new(api_key))
    }

    /// Create a builder for more advanced configuration
    ///
    /// # Example
    ///
    /// ```
    /// use flowglad::Config;
    /// use std::time::Duration;
    ///
    /// let config = Config::builder()
    ///     .api_key("sk_test_...")
    ///     .timeout(Duration::from_secs(60))
    ///     .max_retries(5)
    ///     .build()
    ///     .unwrap();
    /// ```
    pub fn builder() -> ConfigBuilder {
        ConfigBuilder::default()
    }

    /// Get the API key
    pub fn api_key(&self) -> &str {
        &self.api_key
    }

    /// Get the base URL
    pub fn base_url(&self) -> &Url {
        &self.base_url
    }

    /// Get the timeout duration
    pub fn timeout(&self) -> Duration {
        self.timeout
    }

    /// Get the maximum number of retries
    pub fn max_retries(&self) -> u32 {
        self.max_retries
    }
}

/// Builder for constructing a Config
#[derive(Debug, Default)]
pub struct ConfigBuilder {
    api_key: Option<String>,
    base_url: Option<String>,
    timeout: Option<Duration>,
    max_retries: Option<u32>,
}

impl ConfigBuilder {
    /// Set the API key
    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
        self.api_key = Some(api_key.into());
        self
    }

    /// Set the base URL
    ///
    /// This is useful for testing or using a different API endpoint.
    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
        self.base_url = Some(base_url.into());
        self
    }

    /// Set the request timeout
    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = Some(timeout);
        self
    }

    /// Set the maximum number of retry attempts
    pub fn max_retries(mut self, max_retries: u32) -> Self {
        self.max_retries = Some(max_retries);
        self
    }

    /// Build the configuration
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The API key is not set
    /// - The base URL is invalid
    pub fn build(self) -> Result<Config> {
        let api_key = self
            .api_key
            .ok_or_else(|| Error::Config("API key is required".into()))?;

        let base_url = if let Some(url) = self.base_url {
            Url::parse(&url)?
        } else {
            Url::parse("https://app.flowglad.com/api/v1/").unwrap()
        };

        Ok(Config {
            api_key,
            base_url,
            timeout: self.timeout.unwrap_or_else(|| Duration::from_secs(30)),
            max_retries: self.max_retries.unwrap_or(3),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_config_new() {
        let config = Config::new("sk_test_123");
        assert_eq!(config.api_key(), "sk_test_123");
        // URL may or may not have trailing slash depending on the url crate
        assert!(config
            .base_url()
            .as_str()
            .starts_with("https://app.flowglad.com/api/v1"));
        assert_eq!(config.timeout(), Duration::from_secs(30));
        assert_eq!(config.max_retries(), 3);
    }

    #[test]
    fn test_config_builder() {
        let config = Config::builder()
            .api_key("sk_test_456")
            .timeout(Duration::from_secs(60))
            .max_retries(5)
            .build()
            .unwrap();

        assert_eq!(config.api_key(), "sk_test_456");
        assert_eq!(config.timeout(), Duration::from_secs(60));
        assert_eq!(config.max_retries(), 5);
    }

    #[test]
    fn test_config_builder_custom_url() {
        let config = Config::builder()
            .api_key("sk_test_789")
            .base_url("https://test.example.com")
            .build()
            .unwrap();

        assert_eq!(config.base_url().as_str(), "https://test.example.com/");
    }

    #[test]
    fn test_config_builder_missing_api_key() {
        let result = Config::builder().timeout(Duration::from_secs(60)).build();

        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), Error::Config(_)));
    }
}