dynamic_cli/context/mod.rs
1//! Execution context module
2//!
3//! This module provides traits and utilities for managing execution context
4//! in CLI/REPL applications built with dynamic-cli.
5//!
6//! # Overview
7//!
8//! The execution context is shared state that persists across command executions.
9//! Each application defines its own context type that implements the
10//! [`ExecutionContext`] trait.
11//!
12//! # Key Concepts
13//!
14//! ## Execution Context
15//!
16//! The context holds application-specific state that commands can read and modify:
17//!
18//! ```
19//! use dynamic_cli::context::ExecutionContext;
20//! use std::any::Any;
21//!
22//! #[derive(Default)]
23//! struct AppContext {
24//! session_id: String,
25//! user_data: Vec<String>,
26//! settings: std::collections::HashMap<String, String>,
27//! }
28//!
29//! impl ExecutionContext for AppContext {
30//! fn as_any(&self) -> &dyn Any {
31//! self
32//! }
33//!
34//! fn as_any_mut(&mut self) -> &mut dyn Any {
35//! self
36//! }
37//! }
38//! ```
39//!
40//! ## Type-Safe Downcasting
41//!
42//! Since the framework works with trait objects, commands must downcast
43//! the context to access their specific type:
44//!
45//! ```
46//! use dynamic_cli::context::{ExecutionContext, downcast_mut};
47//! # use std::any::Any;
48//! # #[derive(Default)]
49//! # struct AppContext { counter: u32 }
50//! # impl ExecutionContext for AppContext {
51//! # fn as_any(&self) -> &dyn Any { self }
52//! # fn as_any_mut(&mut self) -> &mut dyn Any { self }
53//! # }
54//!
55//! fn my_command(context: &mut dyn ExecutionContext) -> Result<(), String> {
56//! // Downcast to concrete type
57//! let app_ctx = downcast_mut::<AppContext>(context)
58//! .ok_or("Invalid context type")?;
59//!
60//! // Use the context
61//! app_ctx.counter += 1;
62//!
63//! Ok(())
64//! }
65//! ```
66//!
67//! ## Thread Safety
68//!
69//! All contexts must be `Send + Sync` to support:
70//! - Multi-threaded command execution
71//! - Async/await patterns
72//! - Future framework extensibility
73//!
74//! # Common Patterns
75//!
76//! ## Stateless Context
77//!
78//! For simple applications that don't need state:
79//!
80//! ```
81//! use dynamic_cli::context::ExecutionContext;
82//! use std::any::Any;
83//!
84//! #[derive(Default)]
85//! struct EmptyContext;
86//!
87//! impl ExecutionContext for EmptyContext {
88//! fn as_any(&self) -> &dyn Any { self }
89//! fn as_any_mut(&mut self) -> &mut dyn Any { self }
90//! }
91//! ```
92//!
93//! ## Stateful Context
94//!
95//! For applications that maintain state:
96//!
97//! ```
98//! use dynamic_cli::context::ExecutionContext;
99//! use std::any::Any;
100//! use std::collections::HashMap;
101//!
102//! struct DatabaseContext {
103//! connection_pool: Vec<String>, // Simplified example
104//! cache: HashMap<String, String>,
105//! transaction_count: u64,
106//! }
107//!
108//! impl Default for DatabaseContext {
109//! fn default() -> Self {
110//! Self {
111//! connection_pool: vec!["conn1".to_string()],
112//! cache: HashMap::new(),
113//! transaction_count: 0,
114//! }
115//! }
116//! }
117//!
118//! impl ExecutionContext for DatabaseContext {
119//! fn as_any(&self) -> &dyn Any { self }
120//! fn as_any_mut(&mut self) -> &mut dyn Any { self }
121//! }
122//! ```
123//!
124//! ## Error Handling in Commands
125//!
126//! Best practice for handling downcast failures:
127//!
128//! ```
129//! use dynamic_cli::context::{ExecutionContext, downcast_mut};
130//! # use std::any::Any;
131//! # struct MyContext { value: i32 }
132//! # impl ExecutionContext for MyContext {
133//! # fn as_any(&self) -> &dyn Any { self }
134//! # fn as_any_mut(&mut self) -> &mut dyn Any { self }
135//! # }
136//!
137//! fn robust_handler(
138//! context: &mut dyn ExecutionContext
139//! ) -> Result<(), Box<dyn std::error::Error>> {
140//! let ctx = downcast_mut::<MyContext>(context)
141//! .ok_or("Context type mismatch: expected MyContext")?;
142//!
143//! ctx.value += 1;
144//! Ok(())
145//! }
146//! ```
147//!
148//! # Architecture Notes
149//!
150//! ## Why Use Trait Objects?
151//!
152//! The framework uses `dyn ExecutionContext` because:
153//! 1. Each application defines its own context type
154//! 2. The framework can't know concrete types at compile time
155//! 3. This provides maximum flexibility for users
156//!
157//! ## Why Require Send + Sync?
158//!
159//! Thread safety bounds enable:
160//! - Sharing contexts across threads
161//! - Compatibility with async runtimes (tokio, async-std)
162//! - Future features like parallel command execution
163//!
164//! ## Performance Considerations
165//!
166//! - Downcasting has minimal overhead (type ID comparison)
167//! - Context access is not on the hot path for most commands
168//! - The trait object indirection is negligible compared to I/O operations
169//!
170//! # See Also
171//!
172//! - [`ExecutionContext`]: Core trait for contexts
173//! - [`downcast_ref()`]: Helper function for immutable downcasting
174//! - [`downcast_mut()`]: Helper function for mutable downcasting
175
176pub mod traits;
177
178// Re-export commonly used types for convenience
179pub use traits::{downcast_mut, downcast_ref, ExecutionContext};
180
181#[cfg(test)]
182mod tests {
183 use super::{downcast_mut, downcast_ref, ExecutionContext};
184 use std::any::Any;
185 use std::collections::HashMap;
186
187 /// Example context for testing
188 #[derive(Default)]
189 struct TestAppContext {
190 command_count: u32,
191 last_command: Option<String>,
192 data: HashMap<String, String>,
193 }
194
195 impl ExecutionContext for TestAppContext {
196 fn as_any(&self) -> &dyn Any {
197 self
198 }
199
200 fn as_any_mut(&mut self) -> &mut dyn Any {
201 self
202 }
203 }
204
205 /// Another context type for testing type safety.
206 ///
207 /// Unit struct: only its distinct *type* matters for this
208 /// wrong-context-type test, no payload is ever read.
209 struct AnotherContext;
210
211 impl ExecutionContext for AnotherContext {
212 fn as_any(&self) -> &dyn Any {
213 self
214 }
215
216 fn as_any_mut(&mut self) -> &mut dyn Any {
217 self
218 }
219 }
220
221 #[test]
222 fn test_module_exports() {
223 // Verify that types are exported correctly from the module
224 let ctx = TestAppContext::default();
225
226 // Should be able to use the context as a trait object
227 let _ctx_ref: &dyn ExecutionContext = &ctx;
228
229 // Should be able to use free functions
230 let ctx_ref: &dyn ExecutionContext = &ctx;
231 let _downcasted = downcast_ref::<TestAppContext>(ctx_ref);
232 }
233
234 #[test]
235 fn test_integration_command_pattern() {
236 /// Simulate a command handler
237 fn increment_command(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
238 let app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
239
240 app_ctx.command_count += 1;
241 app_ctx.last_command = Some("increment".to_string());
242
243 Ok(())
244 }
245
246 let mut ctx = TestAppContext::default();
247
248 // Execute command
249 increment_command(&mut ctx).unwrap();
250
251 assert_eq!(ctx.command_count, 1);
252 assert_eq!(ctx.last_command, Some("increment".to_string()));
253 }
254
255 #[test]
256 fn test_integration_multiple_commands() {
257 fn command_a(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
258 let app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
259
260 app_ctx
261 .data
262 .insert("key_a".to_string(), "value_a".to_string());
263 app_ctx.command_count += 1;
264 Ok(())
265 }
266
267 fn command_b(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
268 let app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
269
270 app_ctx
271 .data
272 .insert("key_b".to_string(), "value_b".to_string());
273 app_ctx.command_count += 1;
274 Ok(())
275 }
276
277 let mut ctx = TestAppContext::default();
278
279 // Execute multiple commands
280 command_a(&mut ctx).unwrap();
281 command_b(&mut ctx).unwrap();
282
283 assert_eq!(ctx.command_count, 2);
284 assert_eq!(ctx.data.len(), 2);
285 assert_eq!(ctx.data.get("key_a"), Some(&"value_a".to_string()));
286 assert_eq!(ctx.data.get("key_b"), Some(&"value_b".to_string()));
287 }
288
289 #[test]
290 fn test_integration_wrong_context_type() {
291 fn command_expecting_test_context(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
292 downcast_mut::<TestAppContext>(ctx).ok_or("Expected TestAppContext")?;
293 Ok(())
294 }
295
296 let mut wrong_ctx = AnotherContext;
297
298 // Should fail because we're passing the wrong context type
299 let result = command_expecting_test_context(&mut wrong_ctx);
300 assert!(result.is_err());
301 }
302
303 #[test]
304 fn test_integration_read_only_access() {
305 fn read_command_count(ctx: &dyn ExecutionContext) -> Result<u32, String> {
306 let app_ctx = downcast_ref::<TestAppContext>(ctx).ok_or("Invalid context")?;
307
308 Ok(app_ctx.command_count)
309 }
310
311 let ctx = TestAppContext {
312 command_count: 42,
313 ..Default::default()
314 };
315
316 let count = read_command_count(&ctx).unwrap();
317 assert_eq!(count, 42);
318 }
319
320 #[test]
321 fn test_integration_stateful_workflow() {
322 // Simulate a series of commands that build up state
323 fn init_command(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
324 let app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
325
326 app_ctx
327 .data
328 .insert("initialized".to_string(), "true".to_string());
329 app_ctx.last_command = Some("init".to_string());
330 Ok(())
331 }
332
333 fn process_command(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
334 let app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
335
336 // Check initialization
337 if app_ctx.data.get("initialized") != Some(&"true".to_string()) {
338 return Err("Not initialized".to_string());
339 }
340
341 app_ctx
342 .data
343 .insert("processed".to_string(), "true".to_string());
344 app_ctx.last_command = Some("process".to_string());
345 Ok(())
346 }
347
348 fn finalize_command(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
349 let app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
350
351 // Check processing
352 if app_ctx.data.get("processed") != Some(&"true".to_string()) {
353 return Err("Not processed".to_string());
354 }
355
356 app_ctx
357 .data
358 .insert("finalized".to_string(), "true".to_string());
359 app_ctx.last_command = Some("finalize".to_string());
360 Ok(())
361 }
362
363 let mut ctx = TestAppContext::default();
364
365 // Execute workflow
366 init_command(&mut ctx).unwrap();
367 process_command(&mut ctx).unwrap();
368 finalize_command(&mut ctx).unwrap();
369
370 // Verify final state
371 assert_eq!(ctx.data.get("initialized"), Some(&"true".to_string()));
372 assert_eq!(ctx.data.get("processed"), Some(&"true".to_string()));
373 assert_eq!(ctx.data.get("finalized"), Some(&"true".to_string()));
374 assert_eq!(ctx.last_command, Some("finalize".to_string()));
375 }
376
377 #[test]
378 fn test_integration_error_propagation() {
379 fn failing_command(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
380 let _app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
381
382 // Simulate command failure
383 Err("Command failed".to_string())
384 }
385
386 let mut ctx = TestAppContext::default();
387
388 let result = failing_command(&mut ctx);
389 assert!(result.is_err());
390 assert_eq!(result.unwrap_err(), "Command failed");
391 }
392
393 #[test]
394 fn test_boxed_context() {
395 // Test using Box<dyn ExecutionContext>
396 let ctx: Box<dyn ExecutionContext> = Box::new(TestAppContext::default());
397
398 // Should be able to downcast
399 let downcasted = downcast_ref::<TestAppContext>(&*ctx);
400 assert!(downcasted.is_some());
401 }
402
403 #[test]
404 fn test_boxed_context_mutation() {
405 let mut ctx: Box<dyn ExecutionContext> = Box::new(TestAppContext::default());
406
407 // Downcast and modify
408 if let Some(app_ctx) = downcast_mut::<TestAppContext>(&mut *ctx) {
409 app_ctx.command_count = 100;
410 }
411
412 // Verify modification
413 let app_ctx = downcast_ref::<TestAppContext>(&*ctx).unwrap();
414 assert_eq!(app_ctx.command_count, 100);
415 }
416
417 /// Test complex nested context structure
418 #[derive(Default)]
419 struct NestedContext {
420 outer: HashMap<String, InnerContext>,
421 }
422
423 #[derive(Default, Clone)]
424 struct InnerContext {
425 values: Vec<i32>,
426 }
427
428 impl ExecutionContext for NestedContext {
429 fn as_any(&self) -> &dyn Any {
430 self
431 }
432
433 fn as_any_mut(&mut self) -> &mut dyn Any {
434 self
435 }
436 }
437
438 #[test]
439 fn test_nested_context_manipulation() {
440 fn add_value(ctx: &mut dyn ExecutionContext, key: &str, value: i32) -> Result<(), String> {
441 let nested = downcast_mut::<NestedContext>(ctx).ok_or("Invalid context")?;
442
443 nested
444 .outer
445 .entry(key.to_string())
446 .or_insert_with(InnerContext::default)
447 .values
448 .push(value);
449
450 Ok(())
451 }
452
453 let mut ctx = NestedContext::default();
454
455 add_value(&mut ctx, "group1", 10).unwrap();
456 add_value(&mut ctx, "group1", 20).unwrap();
457 add_value(&mut ctx, "group2", 30).unwrap();
458
459 assert_eq!(ctx.outer.get("group1").unwrap().values, vec![10, 20]);
460 assert_eq!(ctx.outer.get("group2").unwrap().values, vec![30]);
461 }
462
463 #[test]
464 fn test_context_with_lifetime_data() {
465 // Test that contexts can hold references (with proper lifetimes)
466 #[derive(Default)]
467 struct RefContext {
468 owned_data: String,
469 }
470
471 impl ExecutionContext for RefContext {
472 fn as_any(&self) -> &dyn Any {
473 self
474 }
475
476 fn as_any_mut(&mut self) -> &mut dyn Any {
477 self
478 }
479 }
480
481 let mut ctx = RefContext {
482 owned_data: "test".to_string(),
483 };
484
485 let ctx_ref: &mut dyn ExecutionContext = &mut ctx;
486
487 if let Some(ref_ctx) = downcast_mut::<RefContext>(ctx_ref) {
488 ref_ctx.owned_data.push_str(" modified");
489 }
490
491 assert_eq!(ctx.owned_data, "test modified");
492 }
493}