invoance 0.2.0

Official Rust SDK for the Invoance compliance API
Documentation
//! SDK configuration.
//!
//! All fields are optional — when omitted the SDK reads from environment
//! variables:
//!
//! - `INVOANCE_API_KEY`  — API key (required)
//! - `INVOANCE_BASE_URL` — API host (defaults to `https://api.invoance.com`)

use std::collections::HashMap;
use std::time::Duration;

use crate::error::Error;

const DEFAULT_BASE_URL: &str = "https://api.invoance.com";
const DEFAULT_TIMEOUT_MS: u64 = 30_000;

const ENV_API_KEY: &str = "INVOANCE_API_KEY";
const ENV_BASE_URL: &str = "INVOANCE_BASE_URL";

/// Client configuration passed to [`InvoanceClient::from_config`](crate::InvoanceClient::from_config)
/// or built via [`InvoanceClient::builder`](crate::InvoanceClient::builder).
///
/// Every field is optional; unset values fall back to environment variables
/// and then to library defaults.
#[derive(Debug, Clone, Default)]
pub struct ClientConfig {
    /// Your Invoance API key (`inv_live_...` or `inv_test_...`).
    /// Falls back to the `INVOANCE_API_KEY` environment variable.
    pub api_key: Option<String>,

    /// Override the API host. Falls back to `INVOANCE_BASE_URL`, then
    /// `https://api.invoance.com`.
    pub base_url: Option<String>,

    /// API version prefix (default `"v1"`). Prepended to every request path.
    pub api_version: Option<String>,

    /// HTTP request timeout (default 30s).
    pub timeout: Option<Duration>,

    /// Default idempotency key sent with every mutating request.
    pub idempotency_key: Option<String>,

    /// Additional headers merged into every request.
    pub extra_headers: HashMap<String, String>,
}

/// Fully-resolved configuration with all defaults applied.
#[derive(Debug, Clone)]
pub(crate) struct ResolvedConfig {
    pub api_key: String,
    pub base_url: String,
    pub api_version: String,
    pub timeout: Duration,
    pub idempotency_key: Option<String>,
    pub extra_headers: HashMap<String, String>,
}

pub(crate) fn resolve_config(cfg: ClientConfig) -> Result<ResolvedConfig, Error> {
    let api_key = cfg
        .api_key
        .filter(|k| !k.is_empty())
        .or_else(|| std::env::var(ENV_API_KEY).ok().filter(|k| !k.is_empty()))
        .ok_or_else(|| {
            Error::validation(format!(
                "api_key is required. Pass it explicitly or set the {ENV_API_KEY} environment variable."
            ))
        })?;

    let base_url = cfg
        .base_url
        .filter(|u| !u.is_empty())
        .or_else(|| std::env::var(ENV_BASE_URL).ok().filter(|u| !u.is_empty()))
        .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
    // Strip trailing slashes.
    let base_url = base_url.trim_end_matches('/').to_string();

    // Strip surrounding slashes from the api version.
    let api_version = cfg
        .api_version
        .unwrap_or_else(|| "v1".to_string())
        .trim_matches('/')
        .to_string();

    Ok(ResolvedConfig {
        api_key,
        base_url,
        api_version,
        timeout: cfg
            .timeout
            .unwrap_or_else(|| Duration::from_millis(DEFAULT_TIMEOUT_MS)),
        idempotency_key: cfg.idempotency_key,
        extra_headers: cfg.extra_headers,
    })
}