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 #[derive(Default)]
207 struct AnotherContext {
208 value: i32,
209 }
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::default();
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 mut ctx = TestAppContext::default();
312 ctx.command_count = 42;
313
314 let count = read_command_count(&ctx).unwrap();
315 assert_eq!(count, 42);
316 }
317
318 #[test]
319 fn test_integration_stateful_workflow() {
320 // Simulate a series of commands that build up state
321 fn init_command(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
322 let app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
323
324 app_ctx
325 .data
326 .insert("initialized".to_string(), "true".to_string());
327 app_ctx.last_command = Some("init".to_string());
328 Ok(())
329 }
330
331 fn process_command(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
332 let app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
333
334 // Check initialization
335 if app_ctx.data.get("initialized") != Some(&"true".to_string()) {
336 return Err("Not initialized".to_string());
337 }
338
339 app_ctx
340 .data
341 .insert("processed".to_string(), "true".to_string());
342 app_ctx.last_command = Some("process".to_string());
343 Ok(())
344 }
345
346 fn finalize_command(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
347 let app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
348
349 // Check processing
350 if app_ctx.data.get("processed") != Some(&"true".to_string()) {
351 return Err("Not processed".to_string());
352 }
353
354 app_ctx
355 .data
356 .insert("finalized".to_string(), "true".to_string());
357 app_ctx.last_command = Some("finalize".to_string());
358 Ok(())
359 }
360
361 let mut ctx = TestAppContext::default();
362
363 // Execute workflow
364 init_command(&mut ctx).unwrap();
365 process_command(&mut ctx).unwrap();
366 finalize_command(&mut ctx).unwrap();
367
368 // Verify final state
369 assert_eq!(ctx.data.get("initialized"), Some(&"true".to_string()));
370 assert_eq!(ctx.data.get("processed"), Some(&"true".to_string()));
371 assert_eq!(ctx.data.get("finalized"), Some(&"true".to_string()));
372 assert_eq!(ctx.last_command, Some("finalize".to_string()));
373 }
374
375 #[test]
376 fn test_integration_error_propagation() {
377 fn failing_command(ctx: &mut dyn ExecutionContext) -> Result<(), String> {
378 let _app_ctx = downcast_mut::<TestAppContext>(ctx).ok_or("Invalid context")?;
379
380 // Simulate command failure
381 Err("Command failed".to_string())
382 }
383
384 let mut ctx = TestAppContext::default();
385
386 let result = failing_command(&mut ctx);
387 assert!(result.is_err());
388 assert_eq!(result.unwrap_err(), "Command failed");
389 }
390
391 #[test]
392 fn test_boxed_context() {
393 // Test using Box<dyn ExecutionContext>
394 let ctx: Box<dyn ExecutionContext> = Box::new(TestAppContext::default());
395
396 // Should be able to downcast
397 let downcasted = downcast_ref::<TestAppContext>(&*ctx);
398 assert!(downcasted.is_some());
399 }
400
401 #[test]
402 fn test_boxed_context_mutation() {
403 let mut ctx: Box<dyn ExecutionContext> = Box::new(TestAppContext::default());
404
405 // Downcast and modify
406 if let Some(app_ctx) = downcast_mut::<TestAppContext>(&mut *ctx) {
407 app_ctx.command_count = 100;
408 }
409
410 // Verify modification
411 let app_ctx = downcast_ref::<TestAppContext>(&*ctx).unwrap();
412 assert_eq!(app_ctx.command_count, 100);
413 }
414
415 /// Test complex nested context structure
416 #[derive(Default)]
417 struct NestedContext {
418 outer: HashMap<String, InnerContext>,
419 }
420
421 #[derive(Default, Clone)]
422 struct InnerContext {
423 values: Vec<i32>,
424 }
425
426 impl ExecutionContext for NestedContext {
427 fn as_any(&self) -> &dyn Any {
428 self
429 }
430
431 fn as_any_mut(&mut self) -> &mut dyn Any {
432 self
433 }
434 }
435
436 #[test]
437 fn test_nested_context_manipulation() {
438 fn add_value(ctx: &mut dyn ExecutionContext, key: &str, value: i32) -> Result<(), String> {
439 let nested = downcast_mut::<NestedContext>(ctx).ok_or("Invalid context")?;
440
441 nested
442 .outer
443 .entry(key.to_string())
444 .or_insert_with(InnerContext::default)
445 .values
446 .push(value);
447
448 Ok(())
449 }
450
451 let mut ctx = NestedContext::default();
452
453 add_value(&mut ctx, "group1", 10).unwrap();
454 add_value(&mut ctx, "group1", 20).unwrap();
455 add_value(&mut ctx, "group2", 30).unwrap();
456
457 assert_eq!(ctx.outer.get("group1").unwrap().values, vec![10, 20]);
458 assert_eq!(ctx.outer.get("group2").unwrap().values, vec![30]);
459 }
460
461 #[test]
462 fn test_context_with_lifetime_data() {
463 // Test that contexts can hold references (with proper lifetimes)
464 #[derive(Default)]
465 struct RefContext {
466 owned_data: String,
467 }
468
469 impl ExecutionContext for RefContext {
470 fn as_any(&self) -> &dyn Any {
471 self
472 }
473
474 fn as_any_mut(&mut self) -> &mut dyn Any {
475 self
476 }
477 }
478
479 let mut ctx = RefContext {
480 owned_data: "test".to_string(),
481 };
482
483 let ctx_ref: &mut dyn ExecutionContext = &mut ctx;
484
485 if let Some(ref_ctx) = downcast_mut::<RefContext>(ctx_ref) {
486 ref_ctx.owned_data.push_str(" modified");
487 }
488
489 assert_eq!(ctx.owned_data, "test modified");
490 }
491}