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}