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::prelude::*;
10//! use mecha10::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::context::ContextBuilder;
53///
54/// # async fn example() -> mecha10::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::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::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    /// Set the Redis connection URL
114    ///
115    /// Override the default Redis URL from environment or config.
116    ///
117    /// # Example
118    ///
119    /// ```rust
120    /// # use mecha10::context::ContextBuilder;
121    /// let builder = ContextBuilder::new("my-node")
122    ///     .with_redis("redis://localhost:6379");
123    /// ```
124    pub fn with_redis(mut self, url: impl Into<String>) -> Self {
125        self.redis_url = Some(url.into());
126        self
127    }
128
129    /// Set the state directory for persistent storage
130    ///
131    /// # Example
132    ///
133    /// ```rust
134    /// # use mecha10::context::ContextBuilder;
135    /// let builder = ContextBuilder::new("my-node")
136    ///     .with_state_dir("./data/state");
137    /// ```
138    pub fn with_state_dir(mut self, dir: impl Into<PathBuf>) -> Self {
139        self.state_dir = Some(dir.into());
140        self
141    }
142
143    /// Configure Redis with custom settings
144    ///
145    /// Provides a callback to customize Redis configuration.
146    ///
147    /// # Example
148    ///
149    /// ```rust
150    /// # use mecha10::context::ContextBuilder;
151    /// let builder = ContextBuilder::new("my-node")
152    ///     .with_custom_redis(|cfg| {
153    ///         cfg.url = "redis://custom:6379".to_string();
154    ///         cfg.max_connections = 20;
155    ///         cfg.timeout_ms = 5000;
156    ///     });
157    /// ```
158    pub fn with_custom_redis<F>(mut self, configure: F) -> Self
159    where
160        F: FnOnce(&mut RedisConfig),
161    {
162        let mut config = RedisConfig::default();
163        configure(&mut config);
164        self.redis_config = Some(config);
165        self
166    }
167
168    /// Build the Context instance
169    ///
170    /// Connects to Redis and initializes the context.
171    ///
172    /// # Returns
173    ///
174    /// A configured `Context` instance
175    ///
176    /// # Errors
177    ///
178    /// Returns error if:
179    /// - Redis connection fails
180    /// - Configuration is invalid
181    ///
182    /// # Example
183    ///
184    /// ```rust
185    /// # use mecha10::context::ContextBuilder;
186    /// # async fn example() -> mecha10::Result<()> {
187    /// let ctx = ContextBuilder::new("my-node")
188    ///     .build()
189    ///     .await?;
190    /// # Ok(())
191    /// # }
192    /// ```
193    pub async fn build(self) -> Result<Context> {
194        // Set environment variables if provided
195        if let Some(redis_url) = &self.redis_url {
196            std::env::set_var("MECHA10_REDIS_URL", redis_url);
197        }
198
199        // Create base context
200        let mut ctx = Context::new(&self.node_id).await?;
201
202        // Apply instance if provided
203        if let Some(instance) = self.instance {
204            ctx = ctx.with_instance(&instance);
205        }
206
207        // Initialize state manager if state_dir provided
208        if let Some(state_dir) = self.state_dir {
209            let state_manager = FilesystemStateManager::new(&state_dir).await?;
210            let concrete = Arc::new(crate::state::ConcreteStateManager::Filesystem(state_manager));
211            ctx.set_state_manager(concrete).await;
212        }
213
214        Ok(ctx)
215    }
216}
217
218// ============================================================================
219// Convenience Constructors
220// ============================================================================
221
222impl Context {
223    /// Create a context builder
224    ///
225    /// Shorthand for `ContextBuilder::new(node_id)`.
226    ///
227    /// # Example
228    ///
229    /// ```rust
230    /// # use mecha10::prelude::*;
231    /// # async fn example() -> Result<()> {
232    /// let ctx = Context::builder("my-node")
233    ///     .with_instance("instance-1")
234    ///     .build()
235    ///     .await?;
236    /// # Ok(())
237    /// # }
238    /// ```
239    pub fn builder(node_id: impl Into<String>) -> ContextBuilder {
240        ContextBuilder::new(node_id)
241    }
242}
243
244// ============================================================================
245// Preset Builders
246// ============================================================================
247
248impl ContextBuilder {
249    /// Create a local development context
250    ///
251    /// Pre-configured for local development:
252    /// - Redis at localhost:6379
253    /// - State directory at ./data/state
254    /// - Relaxed timeouts
255    ///
256    /// # Example
257    ///
258    /// ```rust
259    /// # use mecha10::context::ContextBuilder;
260    /// # async fn example() -> mecha10::Result<()> {
261    /// let ctx = ContextBuilder::local_dev("my-node")
262    ///     .build()
263    ///     .await?;
264    /// # Ok(())
265    /// # }
266    /// ```
267    pub fn local_dev(node_id: impl Into<String>) -> Self {
268        Self::new(node_id)
269            .with_redis("redis://localhost:6379")
270            .with_state_dir("./data/state")
271    }
272
273    /// Create a production context
274    ///
275    /// Pre-configured for production:
276    /// - Redis from environment variable
277    /// - State directory from environment or /var/lib/mecha10
278    /// - Strict timeouts
279    /// - Connection pooling
280    ///
281    /// # Example
282    ///
283    /// ```rust
284    /// # use mecha10::context::ContextBuilder;
285    /// # async fn example() -> mecha10::Result<()> {
286    /// let ctx = ContextBuilder::production("my-node")
287    ///     .build()
288    ///     .await?;
289    /// # Ok(())
290    /// # }
291    /// ```
292    pub fn production(node_id: impl Into<String>) -> Self {
293        let state_dir = std::env::var("MECHA10_STATE_DIR").unwrap_or_else(|_| "/var/lib/mecha10".to_string());
294
295        Self::new(node_id).with_state_dir(state_dir).with_custom_redis(|cfg| {
296            cfg.max_connections = 50;
297            cfg.connection_timeout_ms = 3000;
298        })
299    }
300
301    /// Create a test context
302    ///
303    /// Pre-configured for testing:
304    /// - Redis from TEST_REDIS_URL or localhost:6379
305    /// - State directory in temp folder
306    /// - Fast timeouts
307    ///
308    /// # Example
309    ///
310    /// ```rust
311    /// # use mecha10::context::ContextBuilder;
312    /// # async fn example() -> mecha10::Result<()> {
313    /// let ctx = ContextBuilder::test("test-node")
314    ///     .build()
315    ///     .await?;
316    /// # Ok(())
317    /// # }
318    /// ```
319    pub fn test(node_id: impl Into<String>) -> Self {
320        let redis_url = std::env::var("TEST_REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
321
322        let state_dir = std::env::temp_dir().join("mecha10-test");
323
324        Self::new(node_id).with_redis(redis_url).with_state_dir(state_dir)
325    }
326}