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#[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}