batata-consul-client 0.0.2

Rust client for HashiCorp Consul or batata
Documentation
use std::time::Duration;

use crate::error::{ConsulError, Result};

/// Default Consul HTTP port
pub const DEFAULT_HTTP_PORT: u16 = 8500;
/// Default Consul address
pub const DEFAULT_ADDRESS: &str = "127.0.0.1:8500";
/// Default HTTP scheme
pub const DEFAULT_SCHEME: &str = "http";

/// TLS configuration for HTTPS connections
#[derive(Clone, Debug, Default)]
pub struct TlsConfig {
    /// CA certificate path
    pub ca_cert: Option<String>,
    /// CA certificate PEM data
    pub ca_cert_pem: Option<Vec<u8>>,
    /// Client certificate path
    pub client_cert: Option<String>,
    /// Client certificate PEM data
    pub client_cert_pem: Option<Vec<u8>>,
    /// Client key path
    pub client_key: Option<String>,
    /// Client key PEM data
    pub client_key_pem: Option<Vec<u8>>,
    /// Skip server certificate verification (insecure)
    pub insecure_skip_verify: bool,
}

impl TlsConfig {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_ca_cert(mut self, path: &str) -> Self {
        self.ca_cert = Some(path.to_string());
        self
    }

    pub fn with_client_cert(mut self, cert_path: &str, key_path: &str) -> Self {
        self.client_cert = Some(cert_path.to_string());
        self.client_key = Some(key_path.to_string());
        self
    }

    pub fn with_insecure_skip_verify(mut self) -> Self {
        self.insecure_skip_verify = true;
        self
    }
}

/// HTTP basic authentication
#[derive(Clone, Debug)]
pub struct HttpBasicAuth {
    /// Username
    pub username: String,
    /// Password
    pub password: String,
}

impl HttpBasicAuth {
    pub fn new(username: &str, password: &str) -> Self {
        Self {
            username: username.to_string(),
            password: password.to_string(),
        }
    }
}

/// Client configuration
#[derive(Clone, Debug)]
pub struct Config {
    /// Consul server address (host:port)
    pub address: String,
    /// HTTP scheme (http or https)
    pub scheme: String,
    /// Datacenter to use
    pub datacenter: Option<String>,
    /// HTTP basic authentication
    pub http_auth: Option<HttpBasicAuth>,
    /// ACL token
    pub token: Option<String>,
    /// Token file path
    pub token_file: Option<String>,
    /// TLS configuration
    pub tls_config: Option<TlsConfig>,
    /// Request timeout
    pub timeout: Duration,
    /// Namespace (Enterprise only)
    pub namespace: Option<String>,
    /// Partition (Enterprise only)
    pub partition: Option<String>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            address: DEFAULT_ADDRESS.to_string(),
            scheme: DEFAULT_SCHEME.to_string(),
            datacenter: None,
            http_auth: None,
            token: None,
            token_file: None,
            tls_config: None,
            timeout: Duration::from_secs(30),
            namespace: None,
            partition: None,
        }
    }
}

impl Config {
    /// Create a new default configuration
    pub fn new() -> Self {
        Self::default()
    }

    /// Create configuration from environment variables
    pub fn from_env() -> Result<Self> {
        let mut config = Self::default();

        if let Ok(addr) = std::env::var("CONSUL_HTTP_ADDR") {
            // Parse address which may include scheme
            if addr.starts_with("https://") {
                config.scheme = "https".to_string();
                config.address = addr.trim_start_matches("https://").to_string();
            } else if addr.starts_with("http://") {
                config.scheme = "http".to_string();
                config.address = addr.trim_start_matches("http://").to_string();
            } else {
                config.address = addr;
            }
        }

        if let Ok(token) = std::env::var("CONSUL_HTTP_TOKEN") {
            config.token = Some(token);
        }

        if let Ok(token_file) = std::env::var("CONSUL_HTTP_TOKEN_FILE") {
            config.token_file = Some(token_file);
        }

        if let Ok(auth) = std::env::var("CONSUL_HTTP_AUTH") {
            if let Some((username, password)) = auth.split_once(':') {
                config.http_auth = Some(HttpBasicAuth::new(username, password));
            }
        }

        if let Ok(ssl) = std::env::var("CONSUL_HTTP_SSL") {
            if ssl == "true" || ssl == "1" {
                config.scheme = "https".to_string();
            }
        }

        if let Ok(verify) = std::env::var("CONSUL_HTTP_SSL_VERIFY") {
            if verify == "false" || verify == "0" {
                let mut tls = config.tls_config.unwrap_or_default();
                tls.insecure_skip_verify = true;
                config.tls_config = Some(tls);
            }
        }

        if let Ok(ca_cert) = std::env::var("CONSUL_CACERT") {
            let mut tls = config.tls_config.unwrap_or_default();
            tls.ca_cert = Some(ca_cert);
            config.tls_config = Some(tls);
        }

        if let Ok(client_cert) = std::env::var("CONSUL_CLIENT_CERT") {
            let mut tls = config.tls_config.unwrap_or_default();
            tls.client_cert = Some(client_cert);
            config.tls_config = Some(tls);
        }

        if let Ok(client_key) = std::env::var("CONSUL_CLIENT_KEY") {
            let mut tls = config.tls_config.unwrap_or_default();
            tls.client_key = Some(client_key);
            config.tls_config = Some(tls);
        }

        if let Ok(ns) = std::env::var("CONSUL_NAMESPACE") {
            config.namespace = Some(ns);
        }

        if let Ok(partition) = std::env::var("CONSUL_PARTITION") {
            config.partition = Some(partition);
        }

        Ok(config)
    }

    /// Get the base URL for API requests
    pub fn base_url(&self) -> String {
        format!("{}://{}", self.scheme, self.address)
    }

    /// Validate the configuration
    pub fn validate(&self) -> Result<()> {
        if self.address.is_empty() {
            return Err(ConsulError::InvalidConfig("address is required".to_string()));
        }

        if self.scheme != "http" && self.scheme != "https" {
            return Err(ConsulError::InvalidConfig(format!(
                "invalid scheme: {}",
                self.scheme
            )));
        }

        Ok(())
    }
}

/// Builder for Config
pub struct ConfigBuilder {
    config: Config,
}

impl ConfigBuilder {
    pub fn new() -> Self {
        Self {
            config: Config::default(),
        }
    }

    pub fn address(mut self, address: &str) -> Self {
        self.config.address = address.to_string();
        self
    }

    pub fn scheme(mut self, scheme: &str) -> Self {
        self.config.scheme = scheme.to_string();
        self
    }

    pub fn datacenter(mut self, dc: &str) -> Self {
        self.config.datacenter = Some(dc.to_string());
        self
    }

    pub fn token(mut self, token: &str) -> Self {
        self.config.token = Some(token.to_string());
        self
    }

    pub fn http_auth(mut self, username: &str, password: &str) -> Self {
        self.config.http_auth = Some(HttpBasicAuth::new(username, password));
        self
    }

    pub fn tls_config(mut self, tls: TlsConfig) -> Self {
        self.config.tls_config = Some(tls);
        self
    }

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

    pub fn namespace(mut self, ns: &str) -> Self {
        self.config.namespace = Some(ns.to_string());
        self
    }

    pub fn partition(mut self, partition: &str) -> Self {
        self.config.partition = Some(partition.to_string());
        self
    }

    pub fn build(self) -> Result<Config> {
        self.config.validate()?;
        Ok(self.config)
    }
}

impl Default for ConfigBuilder {
    fn default() -> Self {
        Self::new()
    }
}