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}