flowfull 0.1.0

Async Rust client for Flowfull and Flowless-compatible backends
Documentation
use std::{sync::Arc, time::Duration};

use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use url::Url;

use crate::{FlowfullError, error::Result, storage::Storage};

pub type SessionProvider = Arc<
    dyn Fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Option<String>>> + Send>>
        + Send
        + Sync,
>;

#[derive(Clone)]
pub struct ClientConfig {
    pub base_url: Url,
    pub session_id: Option<String>,
    pub session_provider: Option<SessionProvider>,
    pub include_session: bool,
    pub session_header: HeaderName,
    pub session_cookie: String,
    pub timeout: Duration,
    pub headers: HeaderMap,
    pub retry: RetryConfig,
    pub storage: Option<Arc<dyn Storage>>,
}

#[derive(Debug, Clone)]
pub struct RetryConfig {
    pub attempts: usize,
    pub delay: Duration,
    pub exponential: bool,
    pub max_delay: Option<Duration>,
    pub retry_statuses: Vec<u16>,
    pub retry_non_idempotent: bool,
}

impl Default for RetryConfig {
    fn default() -> Self {
        Self {
            attempts: 1,
            delay: Duration::from_secs(1),
            exponential: false,
            max_delay: Some(Duration::from_secs(30)),
            retry_statuses: vec![408, 429, 500, 502, 503, 504],
            retry_non_idempotent: false,
        }
    }
}

impl RetryConfig {
    pub fn exponential(attempts: usize, delay: Duration) -> Self {
        Self {
            attempts: attempts.max(1),
            delay,
            exponential: true,
            ..Self::default()
        }
    }
}

pub struct ClientConfigBuilder {
    base_url: Url,
    session_id: Option<String>,
    session_provider: Option<SessionProvider>,
    include_session: bool,
    session_header: HeaderName,
    session_cookie: String,
    timeout: Duration,
    headers: HeaderMap,
    retry: RetryConfig,
    storage: Option<Arc<dyn Storage>>,
}

impl ClientConfigBuilder {
    pub fn new(base_url: impl AsRef<str>) -> Result<Self> {
        let mut base_url = Url::parse(base_url.as_ref())?;
        if !base_url.path().ends_with('/') {
            let path = format!("{}/", base_url.path());
            base_url.set_path(&path);
        }

        Ok(Self {
            base_url,
            session_id: None,
            session_provider: None,
            include_session: false,
            session_header: HeaderName::from_static("x-session-id"),
            session_cookie: "session_id".to_string(),
            timeout: Duration::from_secs(30),
            headers: HeaderMap::new(),
            retry: RetryConfig::default(),
            storage: None,
        })
    }

    pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
        self.session_id = Some(session_id.into());
        self
    }

    pub fn session_provider<F, Fut>(mut self, provider: F) -> Self
    where
        F: Fn() -> Fut + Send + Sync + 'static,
        Fut: std::future::Future<Output = Result<Option<String>>> + Send + 'static,
    {
        self.session_provider = Some(Arc::new(move || Box::pin(provider())));
        self
    }

    pub fn include_session(mut self, include_session: bool) -> Self {
        self.include_session = include_session;
        self
    }

    pub fn session_header(mut self, header: impl AsRef<str>) -> Result<Self> {
        self.session_header = HeaderName::from_bytes(header.as_ref().as_bytes())
            .map_err(|err| FlowfullError::Config(format!("invalid session header: {err}")))?;
        Ok(self)
    }

    pub fn session_cookie(mut self, cookie: impl Into<String>) -> Self {
        self.session_cookie = cookie.into();
        self
    }

    pub fn timeout(mut self, timeout: Duration) -> Self {
        self.timeout = timeout;
        self
    }

    pub fn header(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Result<Self> {
        let key = HeaderName::from_bytes(key.as_ref().as_bytes())
            .map_err(|err| FlowfullError::Config(format!("invalid header name: {err}")))?;
        let value = HeaderValue::from_str(value.as_ref())
            .map_err(|err| FlowfullError::Config(format!("invalid header value: {err}")))?;
        self.headers.insert(key, value);
        Ok(self)
    }

    pub fn headers(mut self, headers: HeaderMap) -> Self {
        self.headers = headers;
        self
    }

    pub fn retry(mut self, retry: RetryConfig) -> Self {
        self.retry = retry;
        self
    }

    pub fn storage<S>(mut self, storage: S) -> Self
    where
        S: Storage + 'static,
    {
        self.storage = Some(Arc::new(storage));
        self
    }

    pub fn shared_storage(mut self, storage: Arc<dyn Storage>) -> Self {
        self.storage = Some(storage);
        self
    }

    pub fn build(self) -> Result<ClientConfig> {
        Ok(ClientConfig {
            base_url: self.base_url,
            session_id: self.session_id,
            session_provider: self.session_provider,
            include_session: self.include_session,
            session_header: self.session_header,
            session_cookie: self.session_cookie,
            timeout: self.timeout,
            headers: self.headers,
            retry: self.retry,
            storage: self.storage,
        })
    }
}