mecha10-core 0.8.0

Mecha10 core runtime and foundational types
Documentation
//! Context Builder Pattern
//!
//! Provides a fluent API for creating and configuring Context instances,
//! reducing boilerplate and making configuration more discoverable.
//!
//! # Example
//!
//! ```rust
//! use mecha10_core::prelude::*;
//! use mecha10_core::context::ContextBuilder;
//!
//! # async fn example() -> Result<()> {
//! // Simple usage
//! let ctx = ContextBuilder::new("my-node")
//!     .build()
//!     .await?;
//!
//! // With configuration
//! let ctx = ContextBuilder::new("camera-node")
//!     .with_instance("left")
//!     .with_redis("redis://localhost:6379")
//!     .with_state_dir("./state")
//!     .build()
//!     .await?;
//!
//! // With custom config
//! let ctx = ContextBuilder::new("motor-node")
//!     .with_instance("front-left")
//!     .with_custom_redis(|cfg| {
//!         cfg.url = "redis://custom:6379".to_string();
//!         cfg.max_connections = 20;
//!     })
//!     .build()
//!     .await?;
//! # Ok(())
//! # }
//! ```

use crate::context::{Context, RedisConfig};
use crate::error::Result;
use crate::state::FilesystemStateManager;
use std::path::PathBuf;
use std::sync::Arc;

#[derive(Debug, Clone)]
pub struct ContextBuilder {
    node_id: String,
    instance: Option<String>,
    redis_url: Option<String>,
    state_dir: Option<PathBuf>,
    redis_config: Option<RedisConfig>,
}

impl ContextBuilder {
    /// Create a new context builder
    ///
    /// # Arguments
    ///
    /// * `node_id` - Unique identifier for the node
    ///
    /// # Example
    ///
    /// ```rust
    /// use mecha10_core::context::ContextBuilder;
    ///
    /// let builder = ContextBuilder::new("my-node");
    /// ```
    pub fn new(node_id: impl Into<String>) -> Self {
        Self {
            node_id: node_id.into(),
            instance: None,
            redis_url: None,
            state_dir: None,
            redis_config: None,
        }
    }

    /// Set an instance name for this context
    ///
    /// Allows running multiple instances of the same node with different names.
    /// The final node_id becomes "{node_name}/{instance}".
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::context::ContextBuilder;
    /// let builder = ContextBuilder::new("camera")
    ///     .with_instance("left");
    /// ```
    pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
        self.instance = Some(instance.into());
        self
    }

    crate::builder_setter!(
        /// Set the Redis connection URL
        ///
        /// Override the default Redis URL from environment or config.
        ///
        /// # Example
        ///
        /// ```rust
        /// # use mecha10_core::context::ContextBuilder;
        /// let builder = ContextBuilder::new("my-node")
        ///     .with_redis("redis://localhost:6379");
        /// ```
        into_some, with_redis, redis_url, impl Into<String>
    );

    crate::builder_setter!(
        /// Set the state directory for persistent storage
        ///
        /// # Example
        ///
        /// ```rust
        /// # use mecha10_core::context::ContextBuilder;
        /// let builder = ContextBuilder::new("my-node")
        ///     .with_state_dir("./data/state");
        /// ```
        into_some, with_state_dir, state_dir, impl Into<PathBuf>
    );

    /// Configure Redis with custom settings
    ///
    /// Provides a callback to customize Redis configuration.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::context::ContextBuilder;
    /// let builder = ContextBuilder::new("my-node")
    ///     .with_custom_redis(|cfg| {
    ///         cfg.url = "redis://custom:6379".to_string();
    ///         cfg.max_connections = 20;
    ///         cfg.connection_timeout_ms = 5000;
    ///     });
    /// ```
    pub fn with_custom_redis<F>(mut self, configure: F) -> Self
    where
        F: FnOnce(&mut RedisConfig),
    {
        let mut config = RedisConfig::default();
        configure(&mut config);
        self.redis_config = Some(config);
        self
    }

    /// Build the Context instance
    ///
    /// Connects to Redis and initializes the context.
    ///
    /// # Returns
    ///
    /// A configured `Context` instance
    ///
    /// # Errors
    ///
    /// Returns error if:
    /// - Redis connection fails
    /// - Configuration is invalid
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::context::ContextBuilder;
    /// # async fn example() -> mecha10_core::Result<()> {
    /// let ctx = ContextBuilder::new("my-node")
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn build(self) -> Result<Context> {
        if let Some(redis_url) = &self.redis_url {
            std::env::set_var("MECHA10_REDIS_URL", redis_url);
        }

        let mut ctx = Context::new(&self.node_id).await?;

        if let Some(instance) = self.instance {
            ctx = ctx.with_instance(&instance);
        }

        if let Some(state_dir) = self.state_dir {
            let state_manager = FilesystemStateManager::new(&state_dir).await?;
            let concrete = Arc::new(crate::state::ConcreteStateManager::Filesystem(state_manager));
            ctx.set_state_manager(concrete).await;
        }

        Ok(ctx)
    }
}

impl Context {
    /// Create a context builder
    ///
    /// Shorthand for `ContextBuilder::new(node_id)`.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::prelude::*;
    /// # async fn example() -> Result<()> {
    /// let ctx = Context::builder("my-node")
    ///     .with_instance("instance-1")
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn builder(node_id: impl Into<String>) -> ContextBuilder {
        ContextBuilder::new(node_id)
    }
}

impl ContextBuilder {
    /// Create a local development context
    ///
    /// Pre-configured for local development:
    /// - Redis at localhost:6379
    /// - State directory at ./data/state
    /// - Relaxed timeouts
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::context::ContextBuilder;
    /// # async fn example() -> mecha10_core::Result<()> {
    /// let ctx = ContextBuilder::local_dev("my-node")
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn local_dev(node_id: impl Into<String>) -> Self {
        Self::new(node_id)
            .with_redis("redis://localhost:6379")
            .with_state_dir("./data/state")
    }

    /// Create a production context
    ///
    /// Pre-configured for production:
    /// - Redis from environment variable
    /// - State directory from environment or /var/lib/mecha10
    /// - Strict timeouts
    /// - Connection pooling
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::context::ContextBuilder;
    /// # async fn example() -> mecha10_core::Result<()> {
    /// let ctx = ContextBuilder::production("my-node")
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn production(node_id: impl Into<String>) -> Self {
        let state_dir = std::env::var("MECHA10_STATE_DIR").unwrap_or_else(|_| "/var/lib/mecha10".to_string());

        Self::new(node_id).with_state_dir(state_dir).with_custom_redis(|cfg| {
            cfg.max_connections = 50;
            cfg.connection_timeout_ms = 3000;
        })
    }

    /// Create a test context
    ///
    /// Pre-configured for testing:
    /// - Redis from TEST_REDIS_URL or localhost:6379
    /// - State directory in temp folder
    /// - Fast timeouts
    ///
    /// # Example
    ///
    /// ```rust
    /// # use mecha10_core::context::ContextBuilder;
    /// # async fn example() -> mecha10_core::Result<()> {
    /// let ctx = ContextBuilder::test("test-node")
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn test(node_id: impl Into<String>) -> Self {
        let redis_url = std::env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());

        let state_dir = std::env::temp_dir().join("mecha10-test");

        Self::new(node_id).with_redis(redis_url).with_state_dir(state_dir)
    }
}