flag_rs/
context.rs

1//! Context for passing data between commands
2//!
3//! The context module provides a way to pass data between parent and child
4//! commands, including parsed arguments, flags, and arbitrary typed values.
5
6use std::any::{Any, TypeId};
7use std::collections::HashMap;
8
9/// Context passed to command handlers
10///
11/// `Context` provides access to:
12/// - Command arguments
13/// - Parsed flag values
14/// - Arbitrary typed values for sharing state between commands
15///
16/// # Examples
17///
18/// ```
19/// use flag_rs::context::Context;
20/// use std::collections::HashMap;
21///
22/// // Create a context with arguments
23/// let mut ctx = Context::new(vec!["file1.txt".to_string(), "file2.txt".to_string()]);
24///
25/// // Access arguments
26/// assert_eq!(ctx.args(), &["file1.txt", "file2.txt"]);
27///
28/// // Set and retrieve flags
29/// ctx.set_flag("verbose".to_string(), "true".to_string());
30/// assert_eq!(ctx.flag("verbose"), Some(&"true".to_string()));
31///
32/// // Store typed values
33/// #[derive(Debug, PartialEq)]
34/// struct Config {
35///     api_key: String,
36/// }
37///
38/// ctx.set(Config { api_key: "secret".to_string() });
39/// let config = ctx.get::<Config>().unwrap();
40/// assert_eq!(config.api_key, "secret");
41/// ```
42pub struct Context {
43    args: Vec<String>,
44    flags: HashMap<String, String>,
45    values: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
46}
47
48impl Context {
49    /// Creates a new context with the given arguments
50    ///
51    /// # Arguments
52    ///
53    /// * `args` - The command-line arguments (without the command path)
54    pub fn new(args: Vec<String>) -> Self {
55        Self {
56            args,
57            flags: HashMap::new(),
58            values: HashMap::new(),
59        }
60    }
61
62    /// Returns a slice of the command arguments
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use flag_rs::context::Context;
68    ///
69    /// let ctx = Context::new(vec!["file.txt".to_string()]);
70    /// assert_eq!(ctx.args(), &["file.txt"]);
71    /// ```
72    pub fn args(&self) -> &[String] {
73        &self.args
74    }
75
76    /// Returns a mutable reference to the command arguments
77    ///
78    /// # Examples
79    ///
80    /// ```
81    /// use flag_rs::context::Context;
82    ///
83    /// let mut ctx = Context::new(vec!["file.txt".to_string()]);
84    /// ctx.args_mut().push("another.txt".to_string());
85    /// assert_eq!(ctx.args().len(), 2);
86    /// ```
87    pub fn args_mut(&mut self) -> &mut Vec<String> {
88        &mut self.args
89    }
90
91    /// Gets the value of a flag by name
92    ///
93    /// # Arguments
94    ///
95    /// * `name` - The name of the flag
96    ///
97    /// # Returns
98    ///
99    /// Returns `Some(&String)` if the flag exists, `None` otherwise
100    pub fn flag(&self, name: &str) -> Option<&String> {
101        self.flags.get(name)
102    }
103
104    /// Sets a flag value
105    ///
106    /// # Arguments
107    ///
108    /// * `name` - The name of the flag
109    /// * `value` - The value to set
110    pub fn set_flag(&mut self, name: String, value: String) {
111        self.flags.insert(name, value);
112    }
113
114    /// Returns a reference to all flags
115    pub fn flags(&self) -> &HashMap<String, String> {
116        &self.flags
117    }
118
119    /// Gets a flag value as a boolean
120    ///
121    /// # Arguments
122    ///
123    /// * `name` - The name of the flag
124    ///
125    /// # Returns
126    ///
127    /// Returns `Some(bool)` if the flag exists and can be parsed as a boolean, `None` otherwise
128    ///
129    /// # Examples
130    ///
131    /// ```
132    /// use flag_rs::context::Context;
133    ///
134    /// let mut ctx = Context::new(vec![]);
135    /// ctx.set_flag("verbose".to_string(), "true".to_string());
136    /// ctx.set_flag("debug".to_string(), "false".to_string());
137    ///
138    /// assert_eq!(ctx.flag_bool("verbose"), Some(true));
139    /// assert_eq!(ctx.flag_bool("debug"), Some(false));
140    /// assert_eq!(ctx.flag_bool("missing"), None);
141    /// ```
142    pub fn flag_bool(&self, name: &str) -> Option<bool> {
143        self.flag(name)
144            .and_then(|v| match v.to_lowercase().as_str() {
145                "true" | "t" | "1" | "yes" | "y" => Some(true),
146                "false" | "f" | "0" | "no" | "n" => Some(false),
147                _ => None,
148            })
149    }
150
151    /// Gets a flag value as an integer
152    ///
153    /// # Arguments
154    ///
155    /// * `name` - The name of the flag
156    ///
157    /// # Returns
158    ///
159    /// Returns `Some(i64)` if the flag exists and can be parsed as an integer, `None` otherwise
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// use flag_rs::context::Context;
165    ///
166    /// let mut ctx = Context::new(vec![]);
167    /// ctx.set_flag("port".to_string(), "8080".to_string());
168    ///
169    /// assert_eq!(ctx.flag_int("port"), Some(8080));
170    /// assert_eq!(ctx.flag_int("missing"), None);
171    /// ```
172    pub fn flag_int(&self, name: &str) -> Option<i64> {
173        self.flag(name).and_then(|v| v.parse().ok())
174    }
175
176    /// Gets a flag value as a float
177    ///
178    /// # Arguments
179    ///
180    /// * `name` - The name of the flag
181    ///
182    /// # Returns
183    ///
184    /// Returns `Some(f64)` if the flag exists and can be parsed as a float, `None` otherwise
185    ///
186    /// # Examples
187    ///
188    /// ```
189    /// use flag_rs::context::Context;
190    ///
191    /// let mut ctx = Context::new(vec![]);
192    /// ctx.set_flag("ratio".to_string(), "0.75".to_string());
193    ///
194    /// assert_eq!(ctx.flag_float("ratio"), Some(0.75));
195    /// assert_eq!(ctx.flag_float("missing"), None);
196    /// ```
197    pub fn flag_float(&self, name: &str) -> Option<f64> {
198        self.flag(name).and_then(|v| v.parse().ok())
199    }
200
201    /// Gets a flag value as a string, returning a default if not present
202    ///
203    /// # Arguments
204    ///
205    /// * `name` - The name of the flag
206    /// * `default` - The default value to return if the flag is not set
207    ///
208    /// # Returns
209    ///
210    /// Returns the flag value if present, or the default value
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use flag_rs::context::Context;
216    ///
217    /// let mut ctx = Context::new(vec![]);
218    /// ctx.set_flag("env".to_string(), "production".to_string());
219    ///
220    /// assert_eq!(ctx.flag_str_or("env", "development"), "production");
221    /// assert_eq!(ctx.flag_str_or("missing", "development"), "development");
222    /// ```
223    pub fn flag_str_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
224        self.flag(name).map_or(default, String::as_str)
225    }
226
227    /// Gets a flag value as a boolean, returning a default if not present
228    ///
229    /// # Arguments
230    ///
231    /// * `name` - The name of the flag
232    /// * `default` - The default value to return if the flag is not set or cannot be parsed
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use flag_rs::context::Context;
238    ///
239    /// let mut ctx = Context::new(vec![]);
240    /// ctx.set_flag("verbose".to_string(), "true".to_string());
241    ///
242    /// assert_eq!(ctx.flag_bool_or("verbose", false), true);
243    /// assert_eq!(ctx.flag_bool_or("missing", false), false);
244    /// ```
245    pub fn flag_bool_or(&self, name: &str, default: bool) -> bool {
246        self.flag_bool(name).unwrap_or(default)
247    }
248
249    /// Gets a flag value as an integer, returning a default if not present
250    ///
251    /// # Arguments
252    ///
253    /// * `name` - The name of the flag
254    /// * `default` - The default value to return if the flag is not set or cannot be parsed
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use flag_rs::context::Context;
260    ///
261    /// let mut ctx = Context::new(vec![]);
262    /// ctx.set_flag("port".to_string(), "8080".to_string());
263    ///
264    /// assert_eq!(ctx.flag_int_or("port", 3000), 8080);
265    /// assert_eq!(ctx.flag_int_or("missing", 3000), 3000);
266    /// ```
267    pub fn flag_int_or(&self, name: &str, default: i64) -> i64 {
268        self.flag_int(name).unwrap_or(default)
269    }
270
271    /// Gets a flag value as a float, returning a default if not present
272    ///
273    /// # Arguments
274    ///
275    /// * `name` - The name of the flag
276    /// * `default` - The default value to return if the flag is not set or cannot be parsed
277    ///
278    /// # Examples
279    ///
280    /// ```
281    /// use flag_rs::context::Context;
282    ///
283    /// let mut ctx = Context::new(vec![]);
284    /// ctx.set_flag("ratio".to_string(), "0.75".to_string());
285    ///
286    /// assert_eq!(ctx.flag_float_or("ratio", 0.5), 0.75);
287    /// assert_eq!(ctx.flag_float_or("missing", 0.5), 0.5);
288    /// ```
289    pub fn flag_float_or(&self, name: &str, default: f64) -> f64 {
290        self.flag_float(name).unwrap_or(default)
291    }
292
293    /// Stores a typed value in the context
294    ///
295    /// Values are stored by their type, so only one value of each type
296    /// can be stored at a time. Storing a new value of the same type
297    /// will overwrite the previous value.
298    ///
299    /// # Type Parameters
300    ///
301    /// * `T` - The type of value to store (must be `Send + Sync`)
302    ///
303    /// # Examples
304    ///
305    /// ```
306    /// use flag_rs::context::Context;
307    ///
308    /// struct ApiClient {
309    ///     endpoint: String,
310    /// }
311    ///
312    /// let mut ctx = Context::new(vec![]);
313    /// ctx.set(ApiClient { endpoint: "https://api.example.com".to_string() });
314    /// ```
315    pub fn set<T: Any + Send + Sync>(&mut self, value: T) {
316        self.values.insert(TypeId::of::<T>(), Box::new(value));
317    }
318
319    /// Retrieves a typed value from the context
320    ///
321    /// # Type Parameters
322    ///
323    /// * `T` - The type of value to retrieve
324    ///
325    /// # Returns
326    ///
327    /// Returns `Some(&T)` if a value of that type exists, `None` otherwise
328    pub fn get<T: Any + Send + Sync>(&self) -> Option<&T> {
329        self.values
330            .get(&TypeId::of::<T>())
331            .and_then(|v| (**v).downcast_ref())
332    }
333
334    /// Retrieves a mutable reference to a typed value from the context
335    ///
336    /// # Type Parameters
337    ///
338    /// * `T` - The type of value to retrieve
339    ///
340    /// # Returns
341    ///
342    /// Returns `Some(&mut T)` if a value of that type exists, `None` otherwise
343    pub fn get_mut<T: Any + Send + Sync>(&mut self) -> Option<&mut T> {
344        self.values
345            .get_mut(&TypeId::of::<T>())
346            .and_then(|v| (**v).downcast_mut())
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    #[test]
355    fn test_context_args() {
356        let args = vec!["arg1".to_string(), "arg2".to_string()];
357        let mut ctx = Context::new(args.clone());
358        assert_eq!(ctx.args(), &args);
359
360        ctx.args_mut().push("arg3".to_string());
361        assert_eq!(ctx.args().len(), 3);
362    }
363
364    #[test]
365    fn test_context_flags() {
366        let mut ctx = Context::new(vec![]);
367
368        ctx.set_flag("verbose".to_string(), "true".to_string());
369        ctx.set_flag("output".to_string(), "json".to_string());
370
371        assert_eq!(ctx.flag("verbose"), Some(&"true".to_string()));
372        assert_eq!(ctx.flag("output"), Some(&"json".to_string()));
373        assert_eq!(ctx.flag("nonexistent"), None);
374    }
375
376    #[test]
377    fn test_context_values() {
378        #[derive(Debug, PartialEq)]
379        struct Config {
380            timeout: u32,
381        }
382
383        let mut ctx = Context::new(vec![]);
384        let config = Config { timeout: 30 };
385
386        ctx.set(config);
387
388        assert_eq!(ctx.get::<Config>(), Some(&Config { timeout: 30 }));
389        assert_eq!(ctx.get::<String>(), None);
390
391        if let Some(cfg) = ctx.get_mut::<Config>() {
392            cfg.timeout = 60;
393        }
394
395        assert_eq!(ctx.get::<Config>(), Some(&Config { timeout: 60 }));
396    }
397}