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