batata-consul-client 0.0.2

Rust client for HashiCorp Consul or batata
Documentation
use std::sync::Arc;

use crate::client::HttpClient;
use crate::error::Result;
use crate::types::{HealthCheck, QueryMeta, QueryOptions, ServiceEntry};

/// Health check status constants
pub mod status {
    pub const PASSING: &str = "passing";
    pub const WARNING: &str = "warning";
    pub const CRITICAL: &str = "critical";
    pub const MAINTENANCE: &str = "maintenance";
    pub const ANY: &str = "any";
}

/// Health API client
pub struct Health {
    client: Arc<HttpClient>,
}

impl Health {
    /// Create a new Health client
    pub fn new(client: Arc<HttpClient>) -> Self {
        Self { client }
    }

    /// List all checks for a node
    pub async fn node(&self, node: &str, opts: Option<&QueryOptions>) -> Result<(Vec<HealthCheck>, QueryMeta)> {
        let path = format!("/v1/health/node/{}", node);
        let mut builder = self.client.get(&path);

        if let Some(opts) = opts {
            builder = self.client.apply_query_options(builder, opts);
        }

        self.client.query(builder).await
    }

    /// List all checks for a service
    pub async fn checks(&self, service: &str, opts: Option<&QueryOptions>) -> Result<(Vec<HealthCheck>, QueryMeta)> {
        let path = format!("/v1/health/checks/{}", service);
        let mut builder = self.client.get(&path);

        if let Some(opts) = opts {
            builder = self.client.apply_query_options(builder, opts);
        }

        self.client.query(builder).await
    }

    /// List nodes providing a service with health info
    pub async fn service(
        &self,
        service: &str,
        tag: Option<&str>,
        passing_only: bool,
        opts: Option<&QueryOptions>,
    ) -> Result<(Vec<ServiceEntry>, QueryMeta)> {
        let mut path = format!("/v1/health/service/{}", service);
        let mut params = Vec::new();

        if let Some(t) = tag {
            params.push(format!("tag={}", t));
        }

        if passing_only {
            params.push("passing=true".to_string());
        }

        if !params.is_empty() {
            path.push('?');
            path.push_str(&params.join("&"));
        }

        let mut builder = self.client.get(&path);

        if let Some(opts) = opts {
            builder = self.client.apply_query_options(builder, opts);
        }

        self.client.query(builder).await
    }

    /// Get healthy service instances (convenience method)
    pub async fn service_healthy(
        &self,
        service: &str,
        opts: Option<&QueryOptions>,
    ) -> Result<(Vec<ServiceEntry>, QueryMeta)> {
        self.service(service, None, true, opts).await
    }

    /// List checks in a given state
    pub async fn state(&self, state: &str, opts: Option<&QueryOptions>) -> Result<(Vec<HealthCheck>, QueryMeta)> {
        let path = format!("/v1/health/state/{}", state);
        let mut builder = self.client.get(&path);

        if let Some(opts) = opts {
            builder = self.client.apply_query_options(builder, opts);
        }

        self.client.query(builder).await
    }

    /// List all passing checks
    pub async fn state_passing(&self, opts: Option<&QueryOptions>) -> Result<(Vec<HealthCheck>, QueryMeta)> {
        self.state(status::PASSING, opts).await
    }

    /// List all warning checks
    pub async fn state_warning(&self, opts: Option<&QueryOptions>) -> Result<(Vec<HealthCheck>, QueryMeta)> {
        self.state(status::WARNING, opts).await
    }

    /// List all critical checks
    pub async fn state_critical(&self, opts: Option<&QueryOptions>) -> Result<(Vec<HealthCheck>, QueryMeta)> {
        self.state(status::CRITICAL, opts).await
    }

    /// List all checks (any state)
    pub async fn state_any(&self, opts: Option<&QueryOptions>) -> Result<(Vec<HealthCheck>, QueryMeta)> {
        self.state(status::ANY, opts).await
    }

    /// Connect-capable service instances
    pub async fn connect(
        &self,
        service: &str,
        tag: Option<&str>,
        passing_only: bool,
        opts: Option<&QueryOptions>,
    ) -> Result<(Vec<ServiceEntry>, QueryMeta)> {
        let mut path = format!("/v1/health/connect/{}", service);
        let mut params = Vec::new();

        if let Some(t) = tag {
            params.push(format!("tag={}", t));
        }

        if passing_only {
            params.push("passing=true".to_string());
        }

        if !params.is_empty() {
            path.push('?');
            path.push_str(&params.join("&"));
        }

        let mut builder = self.client.get(&path);

        if let Some(opts) = opts {
            builder = self.client.apply_query_options(builder, opts);
        }

        self.client.query(builder).await
    }

    /// Ingress gateway service instances
    pub async fn ingress(
        &self,
        service: &str,
        tag: Option<&str>,
        passing_only: bool,
        opts: Option<&QueryOptions>,
    ) -> Result<(Vec<ServiceEntry>, QueryMeta)> {
        let mut path = format!("/v1/health/ingress/{}", service);
        let mut params = Vec::new();

        if let Some(t) = tag {
            params.push(format!("tag={}", t));
        }

        if passing_only {
            params.push("passing=true".to_string());
        }

        if !params.is_empty() {
            path.push('?');
            path.push_str(&params.join("&"));
        }

        let mut builder = self.client.get(&path);

        if let Some(opts) = opts {
            builder = self.client.apply_query_options(builder, opts);
        }

        self.client.query(builder).await
    }
}

/// Health check aggregation utility
pub fn aggregate_status(checks: &[HealthCheck]) -> &'static str {
    let mut critical = false;
    let mut warning = false;

    for check in checks {
        match check.status.as_str() {
            "critical" => critical = true,
            "warning" => warning = true,
            _ => {}
        }
    }

    if critical {
        status::CRITICAL
    } else if warning {
        status::WARNING
    } else {
        status::PASSING
    }
}