reasoninglayer 0.2.1

Rust client SDK for the Reasoning Layer API
Documentation
//! Client configuration.
//!
//! The [`ClientConfig`] builder is the single entry point for configuring a
//! [`ReasoningLayerClient`](crate::ReasoningLayerClient). Required fields are `base_url` and
//! `tenant_id`; all others have sensible defaults:
//!
//! | Field | Default |
//! | --- | --- |
//! | `timeout` | 30 seconds |
//! | `max_retries` | 3 |
//! | `retry_on_503` | `false` |
//!
//! ```
//! use reasoninglayer::ClientConfig;
//!
//! let config = ClientConfig::new("http://localhost:8083", "00000000-0000-0000-0000-000000000001");
//! assert_eq!(config.max_retries, 3);
//! ```

use std::time::Duration;

use crate::error::Error;

/// SDK version — sent as `X-SDK-Version` on every request.
pub const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");

/// Language identifier — sent as `X-SDK-Language` on every request.
pub const SDK_LANGUAGE: &str = "rust";

/// Default request timeout.
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);

/// Default maximum retry attempts.
pub const DEFAULT_MAX_RETRIES: u32 = 3;

/// Configuration for a [`ReasoningLayerClient`](crate::ReasoningLayerClient).
///
/// Build with [`ClientConfig::new`] and set optional fields with the builder-style `with_*` methods.
/// All fields are `pub` so the struct can also be constructed directly if preferred.
#[derive(Debug, Clone)]
pub struct ClientConfig {
    /// Base URL of the Reasoning Layer API (e.g., `"http://localhost:8083"`). Trailing slashes stripped.
    pub base_url: String,
    /// Tenant UUID — sent as `X-Tenant-Id`. Set once; never overridable per-call.
    pub tenant_id: String,
    /// Default user UUID for `X-User-Id`. Overridable per-call via [`RequestOptions`](crate::RequestOptions).
    pub user_id: Option<String>,
    /// Default namespace UUID for `X-Namespace-Id`.
    pub namespace_id: Option<String>,
    /// Authenticated user identifier for `X-Authenticated-User`.
    pub authenticated_user: Option<String>,
    /// Bearer token for `Authorization: Bearer ...`.
    pub bearer_token: Option<String>,
    /// Default request timeout.
    pub timeout: Duration,
    /// Maximum retry attempts on transient failures.
    pub max_retries: u32,
    /// Retry on HTTP 503 (Service Unavailable).
    pub retry_on_503: bool,
    /// Additional custom headers sent on every request.
    pub extra_headers: Vec<(String, String)>,
}

impl ClientConfig {
    /// Create a configuration with the two required fields.
    pub fn new(base_url: impl Into<String>, tenant_id: impl Into<String>) -> Self {
        Self {
            base_url: base_url.into(),
            tenant_id: tenant_id.into(),
            user_id: None,
            namespace_id: None,
            authenticated_user: None,
            bearer_token: None,
            timeout: DEFAULT_TIMEOUT,
            max_retries: DEFAULT_MAX_RETRIES,
            retry_on_503: false,
            extra_headers: Vec::new(),
        }
    }

    /// Set the default user ID (`X-User-Id`).
    #[must_use]
    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
        self.user_id = Some(user_id.into());
        self
    }

    /// Set the default namespace ID (`X-Namespace-Id`).
    #[must_use]
    pub fn with_namespace_id(mut self, namespace_id: impl Into<String>) -> Self {
        self.namespace_id = Some(namespace_id.into());
        self
    }

    /// Set the authenticated user identifier (`X-Authenticated-User`).
    #[must_use]
    pub fn with_authenticated_user(mut self, authenticated_user: impl Into<String>) -> Self {
        self.authenticated_user = Some(authenticated_user.into());
        self
    }

    /// Set a bearer token (`Authorization: Bearer ...`).
    #[must_use]
    pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
        self.bearer_token = Some(bearer_token.into());
        self
    }

    /// Override the default request timeout (default: 30 seconds).
    #[must_use]
    pub fn with_timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    /// Override the maximum retry attempts (default: 3).
    #[must_use]
    pub fn with_max_retries(mut self, max_retries: u32) -> Self {
        self.max_retries = max_retries;
        self
    }

    /// Enable retry on HTTP 503 (default: `false`).
    #[must_use]
    pub fn with_retry_on_503(mut self, retry_on_503: bool) -> Self {
        self.retry_on_503 = retry_on_503;
        self
    }

    /// Add a custom header that will be sent on every request.
    #[must_use]
    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
        self.extra_headers.push((name.into(), value.into()));
        self
    }

    pub(crate) fn resolve(self) -> Result<ResolvedConfig, Error> {
        if self.base_url.is_empty() {
            return Err(Error::validation("base_url", "base_url is required"));
        }
        if self.tenant_id.is_empty() {
            return Err(Error::validation("tenant_id", "tenant_id is required"));
        }
        let base_url = self.base_url.trim_end_matches('/').to_string();
        Ok(ResolvedConfig {
            base_url,
            tenant_id: self.tenant_id,
            user_id: self.user_id,
            namespace_id: self.namespace_id,
            authenticated_user: self.authenticated_user,
            bearer_token: self.bearer_token,
            timeout: self.timeout,
            max_retries: self.max_retries,
            retry_on_503: self.retry_on_503,
            extra_headers: self.extra_headers,
        })
    }
}

/// A fully resolved configuration with defaults applied.
#[derive(Debug, Clone)]
pub(crate) struct ResolvedConfig {
    pub base_url: String,
    pub tenant_id: String,
    pub user_id: Option<String>,
    pub namespace_id: Option<String>,
    pub authenticated_user: Option<String>,
    pub bearer_token: Option<String>,
    pub timeout: Duration,
    pub max_retries: u32,
    pub retry_on_503: bool,
    pub extra_headers: Vec<(String, String)>,
}