batata-consul-client 0.0.2

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

use serde::{Deserialize, Serialize};

use crate::client::HttpClient;
use crate::error::Result;
use crate::types::{QueryMeta, QueryOptions, WriteMeta, WriteOptions};

/// Session behavior when a session is invalidated
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SessionBehavior {
    /// Release locks (default)
    #[serde(rename = "release")]
    Release,
    /// Delete the key
    #[serde(rename = "delete")]
    Delete,
}

impl Default for SessionBehavior {
    fn default() -> Self {
        Self::Release
    }
}

/// Session entry
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SessionEntry {
    /// Session ID
    #[serde(rename = "ID")]
    pub id: String,
    /// Session name
    #[serde(default)]
    pub name: String,
    /// Node the session is on
    pub node: String,
    /// Lock delay duration
    #[serde(default)]
    pub lock_delay: u64,
    /// Session behavior on invalidation
    #[serde(default)]
    pub behavior: Option<String>,
    /// TTL for the session
    #[serde(rename = "TTL")]
    #[serde(default)]
    pub ttl: String,
    /// Checks associated with the session
    #[serde(default)]
    pub checks: Vec<String>,
    /// Node checks associated with the session
    #[serde(default)]
    pub node_checks: Vec<String>,
    /// Service checks associated with the session
    #[serde(default)]
    pub service_checks: Vec<ServiceCheck>,
    /// Create index
    pub create_index: u64,
    /// Modify index
    pub modify_index: u64,
}

/// Service check for session
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ServiceCheck {
    /// Service ID
    #[serde(rename = "ID")]
    pub id: String,
    /// Service namespace
    #[serde(default)]
    pub namespace: String,
}

/// Session creation request
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SessionRequest {
    /// Session name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Node to create session on
    #[serde(skip_serializing_if = "Option::is_none")]
    pub node: Option<String>,
    /// Lock delay duration (e.g., "15s")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lock_delay: Option<String>,
    /// Session behavior on invalidation
    #[serde(skip_serializing_if = "Option::is_none")]
    pub behavior: Option<String>,
    /// TTL for the session (e.g., "30s")
    #[serde(rename = "TTL")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ttl: Option<String>,
    /// Checks to associate with session
    #[serde(skip_serializing_if = "Option::is_none")]
    pub checks: Option<Vec<String>>,
    /// Node checks to associate with session
    #[serde(skip_serializing_if = "Option::is_none")]
    pub node_checks: Option<Vec<String>>,
    /// Service checks to associate with session
    #[serde(skip_serializing_if = "Option::is_none")]
    pub service_checks: Option<Vec<ServiceCheck>>,
}

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

impl SessionRequest {
    pub fn new() -> Self {
        Self {
            name: None,
            node: None,
            lock_delay: None,
            behavior: None,
            ttl: None,
            checks: None,
            node_checks: None,
            service_checks: None,
        }
    }

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

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

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

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

    pub fn with_behavior(mut self, behavior: SessionBehavior) -> Self {
        self.behavior = Some(match behavior {
            SessionBehavior::Release => "release".to_string(),
            SessionBehavior::Delete => "delete".to_string(),
        });
        self
    }

    pub fn with_checks(mut self, checks: Vec<String>) -> Self {
        self.checks = Some(checks);
        self
    }
}

/// Session creation response
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SessionCreateResponse {
    /// Created session ID
    #[serde(rename = "ID")]
    pub id: String,
}

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

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

    /// Create a new session
    pub async fn create(&self, session: &SessionRequest, opts: Option<&WriteOptions>) -> Result<(String, WriteMeta)> {
        let mut builder = self.client.put("/v1/session/create").json(session);

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

        let (response, meta): (SessionCreateResponse, WriteMeta) = self.client.write(builder).await?;
        Ok((response.id, meta))
    }

    /// Create a session with just a name (convenience method)
    pub async fn create_named(&self, name: &str, opts: Option<&WriteOptions>) -> Result<(String, WriteMeta)> {
        let session = SessionRequest::new().with_name(name);
        self.create(&session, opts).await
    }

    /// Create a session with TTL
    pub async fn create_with_ttl(&self, name: &str, ttl: &str, opts: Option<&WriteOptions>) -> Result<(String, WriteMeta)> {
        let session = SessionRequest::new()
            .with_name(name)
            .with_ttl(ttl);
        self.create(&session, opts).await
    }

    /// Destroy a session
    pub async fn destroy(&self, id: &str, opts: Option<&WriteOptions>) -> Result<(bool, WriteMeta)> {
        let path = format!("/v1/session/destroy/{}", id);
        let mut builder = self.client.put(&path);

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

        self.client.write_bool(builder).await
    }

    /// Get information about a session
    pub async fn info(&self, id: &str, opts: Option<&QueryOptions>) -> Result<(Option<SessionEntry>, QueryMeta)> {
        let path = format!("/v1/session/info/{}", id);
        let mut builder = self.client.get(&path);

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

        let (entries, meta): (Vec<SessionEntry>, QueryMeta) = self.client.query(builder).await?;
        Ok((entries.into_iter().next(), meta))
    }

    /// List sessions for a node
    pub async fn node(&self, node: &str, opts: Option<&QueryOptions>) -> Result<(Vec<SessionEntry>, QueryMeta)> {
        let path = format!("/v1/session/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 sessions
    pub async fn list(&self, opts: Option<&QueryOptions>) -> Result<(Vec<SessionEntry>, QueryMeta)> {
        let mut builder = self.client.get("/v1/session/list");

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

        self.client.query(builder).await
    }

    /// Renew a session
    pub async fn renew(&self, id: &str, opts: Option<&WriteOptions>) -> Result<(Option<SessionEntry>, WriteMeta)> {
        let path = format!("/v1/session/renew/{}", id);
        let mut builder = self.client.put(&path);

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

        let (entries, meta): (Vec<SessionEntry>, WriteMeta) = self.client.write(builder).await?;
        Ok((entries.into_iter().next(), meta))
    }

    /// Renew a session, returning an error if it doesn't exist
    pub async fn renew_periodic(&self, id: &str, opts: Option<&WriteOptions>) -> Result<(SessionEntry, WriteMeta)> {
        let (entry, meta) = self.renew(id, opts).await?;
        match entry {
            Some(e) => Ok((e, meta)),
            None => Err(crate::error::ConsulError::SessionNotFound(id.to_string())),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_session_request_builder() {
        let session = SessionRequest::new()
            .with_name("my-session")
            .with_ttl("30s")
            .with_lock_delay("15s")
            .with_behavior(SessionBehavior::Delete);

        assert_eq!(session.name, Some("my-session".to_string()));
        assert_eq!(session.ttl, Some("30s".to_string()));
        assert_eq!(session.lock_delay, Some("15s".to_string()));
        assert_eq!(session.behavior, Some("delete".to_string()));
    }
}