Skip to main content

mecha10_core/context/
builder.rs

1//! Context Builder Pattern
2//!
3//! Provides a fluent API for creating and configuring Context instances,
4//! reducing boilerplate and making configuration more discoverable.
5//!
6//! # Example
7//!
8//! ```rust
9//! use mecha10_core::prelude::*;
10//! use mecha10_core::context::ContextBuilder;
11//!
12//! # async fn example() -> Result<()> {
13//! // Simple usage
14//! let ctx = ContextBuilder::new("my-node")
15//!     .build()
16//!     .await?;
17//!
18//! // With configuration
19//! let ctx = ContextBuilder::new("camera-node")
20//!     .with_instance("left")
21//!     .with_redis("redis://localhost:6379")
22//!     .with_state_dir("./state")
23//!     .build()
24//!     .await?;
25//!
26//! // With custom config
27//! let ctx = ContextBuilder::new("motor-node")
28//!     .with_instance("front-left")
29//!     .with_custom_redis(|cfg| {
30//!         cfg.url = "redis://custom:6379".to_string();
31//!         cfg.max_connections = 20;
32//!     })
33//!     .build()
34//!     .await?;
35//! # Ok(())
36//! # }
37//! ```
38
39use crate::context::{Context, RedisConfig};
40use crate::error::Result;
41use crate::state::FilesystemStateManager;
42use std::path::PathBuf;
43use std::sync::Arc;
44
45/// Builder for creating Context instances with fluent API
46///
47/// Provides a more ergonomic way to create contexts with various configurations.
48///
49/// # Example
50///
51/// ```rust
52/// use mecha10_core::context::ContextBuilder;
53///
54/// # async fn example() -> mecha10_core::Result<()> {
55/// let ctx = ContextBuilder::new("my-node")
56///     .with_instance("instance-1")
57///     .with_redis("redis://localhost:6379")
58///     .build()
59///     .await?;
60/// # Ok(())
61/// # }
62/// ```
63#[derive(Debug, Clone)]
64pub struct ContextBuilder {
65    node_id: String,
66    instance: Option<String>,
67    redis_url: Option<String>,
68    state_dir: Option<PathBuf>,
69    redis_config: Option<RedisConfig>,
70}
71
72impl ContextBuilder {
73    /// Create a new context builder
74    ///
75    /// # Arguments
76    ///
77    /// * `node_id` - Unique identifier for the node
78    ///
79    /// # Example
80    ///
81    /// ```rust
82    /// use mecha10_core::context::ContextBuilder;
83    ///
84    /// let builder = ContextBuilder::new("my-node");
85    /// ```
86    pub fn new(node_id: impl Into<String>) -> Self {
87        Self {
88            node_id: node_id.into(),
89            instance: None,
90            redis_url: None,
91            state_dir: None,
92            redis_config: None,
93        }
94    }
95
96    /// Set an instance name for this context
97    ///
98    /// Allows running multiple instances of the same node with different names.
99    /// The final node_id becomes "{node_name}/{instance}".
100    ///
101    /// # Example
102    ///
103    /// ```rust
104    /// # use mecha10_core::context::ContextBuilder;
105    /// let builder = ContextBuilder::new("camera")
106    ///     .with_instance("left");
107    /// ```
108    pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
109        self.instance = Some(instance.into());
110        self
111    }
112
113    crate::builder_setter!(
114        /// Set the Redis connection URL
115        ///
116        /// Override the default Redis URL from environment or config.
117        ///
118        /// # Example
119        ///
120        /// ```rust
121        /// # use mecha10_core::context::ContextBuilder;
122        /// let builder = ContextBuilder::new("my-node")
123        ///     .with_redis("redis://localhost:6379");
124        /// ```
125        into_some, with_redis, redis_url, impl Into<String>
126    );
127
128    crate::builder_setter!(
129        /// Set the state directory for persistent storage
130        ///
131        /// # Example
132        ///
133        /// ```rust
134        /// # use mecha10_core::context::ContextBuilder;
135        /// let builder = ContextBuilder::new("my-node")
136        ///     .with_state_dir("./data/state");
137        /// ```
138        into_some, with_state_dir, state_dir, impl Into<PathBuf>
139    );
140
141    /// Configure Redis with custom settings
142    ///
143    /// Provides a callback to customize Redis configuration.
144    ///
145    /// # Example
146    ///
147    /// ```rust
148    /// # use mecha10_core::context::ContextBuilder;
149    /// let builder = ContextBuilder::new("my-node")
150    ///     .with_custom_redis(|cfg| {
151    ///         cfg.url = "redis://custom:6379".to_string();
152    ///         cfg.max_connections = 20;
153    ///         cfg.timeout_ms = 5000;
154    ///     });
155    /// ```
156    pub fn with_custom_redis<F>(mut self, configure: F) -> Self
157    where
158        F: FnOnce(&mut RedisConfig),
159    {
160        let mut config = RedisConfig::default();
161        configure(&mut config);
162        self.redis_config = Some(config);
163        self
164    }
165
166    /// Build the Context instance
167    ///
168    /// Connects to Redis and initializes the context.
169    ///
170    /// # Returns
171    ///
172    /// A configured `Context` instance
173    ///
174    /// # Errors
175    ///
176    /// Returns error if:
177    /// - Redis connection fails
178    /// - Configuration is invalid
179    ///
180    /// # Example
181    ///
182    /// ```rust
183    /// # use mecha10_core::context::ContextBuilder;
184    /// # async fn example() -> mecha10_core::Result<()> {
185    /// let ctx = ContextBuilder::new("my-node")
186    ///     .build()
187    ///     .await?;
188    /// # Ok(())
189    /// # }
190    /// ```
191    pub async fn build(self) -> Result<Context> {
192        if let Some(redis_url) = &self.redis_url {
193            std::env::set_var("MECHA10_REDIS_URL", redis_url);
194        }
195
196        let mut ctx = Context::new(&self.node_id).await?;
197
198        if let Some(instance) = self.instance {
199            ctx = ctx.with_instance(&instance);
200        }
201
202        if let Some(state_dir) = self.state_dir {
203            let state_manager = FilesystemStateManager::new(&state_dir).await?;
204            let concrete = Arc::new(crate::state::ConcreteStateManager::Filesystem(state_manager));
205            ctx.set_state_manager(concrete).await;
206        }
207
208        Ok(ctx)
209    }
210}
211
212impl Context {
213    /// Create a context builder
214    ///
215    /// Shorthand for `ContextBuilder::new(node_id)`.
216    ///
217    /// # Example
218    ///
219    /// ```rust
220    /// # use mecha10_core::prelude::*;
221    /// # async fn example() -> Result<()> {
222    /// let ctx = Context::builder("my-node")
223    ///     .with_instance("instance-1")
224    ///     .build()
225    ///     .await?;
226    /// # Ok(())
227    /// # }
228    /// ```
229    pub fn builder(node_id: impl Into<String>) -> ContextBuilder {
230        ContextBuilder::new(node_id)
231    }
232}
233
234impl ContextBuilder {
235    /// Create a local development context
236    ///
237    /// Pre-configured for local development:
238    /// - Redis at localhost:6379
239    /// - State directory at ./data/state
240    /// - Relaxed timeouts
241    ///
242    /// # Example
243    ///
244    /// ```rust
245    /// # use mecha10_core::context::ContextBuilder;
246    /// # async fn example() -> mecha10_core::Result<()> {
247    /// let ctx = ContextBuilder::local_dev("my-node")
248    ///     .build()
249    ///     .await?;
250    /// # Ok(())
251    /// # }
252    /// ```
253    pub fn local_dev(node_id: impl Into<String>) -> Self {
254        Self::new(node_id)
255            .with_redis("redis://localhost:6379")
256            .with_state_dir("./data/state")
257    }
258
259    /// Create a production context
260    ///
261    /// Pre-configured for production:
262    /// - Redis from environment variable
263    /// - State directory from environment or /var/lib/mecha10
264    /// - Strict timeouts
265    /// - Connection pooling
266    ///
267    /// # Example
268    ///
269    /// ```rust
270    /// # use mecha10_core::context::ContextBuilder;
271    /// # async fn example() -> mecha10_core::Result<()> {
272    /// let ctx = ContextBuilder::production("my-node")
273    ///     .build()
274    ///     .await?;
275    /// # Ok(())
276    /// # }
277    /// ```
278    pub fn production(node_id: impl Into<String>) -> Self {
279        let state_dir = std::env::var("MECHA10_STATE_DIR").unwrap_or_else(|_| "/var/lib/mecha10".to_string());
280
281        Self::new(node_id).with_state_dir(state_dir).with_custom_redis(|cfg| {
282            cfg.max_connections = 50;
283            cfg.connection_timeout_ms = 3000;
284        })
285    }
286
287    /// Create a test context
288    ///
289    /// Pre-configured for testing:
290    /// - Redis from TEST_REDIS_URL or localhost:6379
291    /// - State directory in temp folder
292    /// - Fast timeouts
293    ///
294    /// # Example
295    ///
296    /// ```rust
297    /// # use mecha10_core::context::ContextBuilder;
298    /// # async fn example() -> mecha10_core::Result<()> {
299    /// let ctx = ContextBuilder::test("test-node")
300    ///     .build()
301    ///     .await?;
302    /// # Ok(())
303    /// # }
304    /// ```
305    pub fn test(node_id: impl Into<String>) -> Self {
306        let redis_url = std::env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
307
308        let state_dir = std::env::temp_dir().join("mecha10-test");
309
310        Self::new(node_id).with_redis(redis_url).with_state_dir(state_dir)
311    }
312}