rust_args_parser/
matches.rs

1use crate::Source;
2use std::collections::HashMap;
3use std::ffi::{OsStr, OsString};
4
5/// Value container stored in `Matches`.
6#[derive(Clone, Debug)]
7pub enum Value {
8    Flag,
9    One(OsString),
10    Many(Vec<OsString>),
11}
12
13/// Whether a key is set and from where.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Status {
16    Unset,
17    Set(Source),
18}
19
20/// Prefix used to differentiate positional keys.
21const POS_PREFIX: &str = "@pos";
22
23/// Join a command path and logical name into the internal key (options/flags).
24#[must_use]
25pub fn key_for(path: &[&str], name: &str) -> String {
26    if path.is_empty() {
27        name.to_string()
28    } else {
29        format!("{}.{}", path.join("."), name)
30    }
31}
32
33/// Join a command path and positional name into the internal key.
34#[must_use]
35pub fn pos_key_for(path: &[&str], name: &str) -> String {
36    if path.is_empty() {
37        format!("{POS_PREFIX}.{name}")
38    } else {
39        format!("{}.{}.{}", path.join("."), POS_PREFIX, name)
40    }
41}
42
43fn key_for_strings(path: &[String], name: &str) -> String {
44    if path.is_empty() {
45        name.to_string()
46    } else {
47        format!("{}.{}", path.join("."), name)
48    }
49}
50
51/// Same as `pos_key_for`, but for an owned `Vec<String>` without temporaries.
52fn pos_key_for_strings(path: &[String], name: &str) -> String {
53    if path.is_empty() {
54        format!("{POS_PREFIX}.{name}")
55    } else {
56        format!("{}.{}.{}", path.join("."), POS_PREFIX, name)
57    }
58}
59
60/// All parsed values and their sources. Internally keyed by flattened strings.
61/// We add `leaf_path` so callers can query *scoped* without spelling keys.
62#[derive(Debug)]
63pub struct Matches {
64    pub(crate) values: HashMap<String, Value>,
65    pub(crate) status: HashMap<String, Status>,
66    pub(crate) flag_counts: HashMap<String, usize>,
67    leaf_path: Vec<String>,
68}
69
70impl Matches {
71    pub(crate) fn new() -> Self {
72        Self { values: HashMap::new(), status: HashMap::new(), flag_counts: HashMap::new(), leaf_path: Vec::new() }
73    }
74
75    /// Set the *leaf* (selected) command path. Parser calls this before returning.
76    pub(crate) fn set_leaf_path(&mut self, path: &[&str]) {
77        self.leaf_path.clear();
78        self.leaf_path.extend(path.iter().map(|s| (*s).to_string()));
79    }
80
81    /// The leaf command path as `Vec<&str>`.
82    #[must_use]
83    pub fn leaf_path(&self) -> Vec<&str> {
84        self.leaf_path.iter().map(std::string::String::as_str).collect()
85    }
86
87    /// Scoped view at the *leaf* command. Most handlers will use this.
88    #[must_use]
89    pub fn view(&self) -> MatchView<'_> {
90        MatchView { m: self, path: self.leaf_path() }
91    }
92
93    /// Scoped view at an explicit command path, e.g., `&["remote", "add"]`.
94    #[must_use]
95    pub fn at<'a>(&'a self, path: &[&'a str]) -> MatchView<'a> {
96        MatchView { m: self, path: path.to_vec() }
97    }
98
99    /// Get a positional by name in the *leaf* scope as a slice (one becomes a 1‑elem slice).
100    /// Returns None if not present.
101    #[must_use]
102    pub fn get_position(&self, name: &str) -> Option<&[OsString]> {
103        let k = pos_key_for_strings(&self.leaf_path, name);
104        match self.values.get(&k) {
105            Some(Value::Many(vs)) => Some(vs.as_slice()),
106            Some(Value::One(v)) => Some(std::slice::from_ref(v)),
107            _ => None,
108        }
109    }
110
111    /// Get a single positional by name in the leaf scope.
112    #[must_use]
113    pub fn get_position_one(&self, name: &str) -> Option<&OsStr> {
114        let k = pos_key_for_strings(&self.leaf_path, name);
115        match self.values.get(&k) {
116            Some(Value::One(v)) => Some(v.as_os_str()),
117            Some(Value::Many(vs)) => vs.first().map(std::ffi::OsString::as_os_str),
118            _ => None,
119        }
120    }
121
122    /// Get an option value in the leaf scope (first of many if repeated).
123    #[must_use]
124    pub fn get_value(&self, name: &str) -> Option<&OsStr> {
125        let k = key_for_strings(&self.leaf_path, name);
126        match self.values.get(&k) {
127            Some(Value::One(v)) => Some(v.as_os_str()),
128            Some(Value::Many(vs)) => vs.first().map(std::ffi::OsString::as_os_str),
129            _ => None,
130        }
131    }
132
133    /// Get all values for an option in the leaf scope.
134    #[must_use]
135    pub fn get_values(&self, name: &str) -> Option<&[OsString]> {
136        let k = key_for_strings(&self.leaf_path, name);
137        match self.values.get(&k) {
138            Some(Value::Many(vs)) => Some(vs.as_slice()),
139            Some(Value::One(v)) => Some(std::slice::from_ref(v)),
140            _ => None,
141        }
142    }
143
144    /// Test whether a flag/option was set in the leaf scope (from any Source).
145    #[must_use]
146    pub fn is_set(&self, name: &str) -> bool {
147        let k = key_for_strings(&self.leaf_path, name);
148        self.status.contains_key(&k)
149    }
150
151    /// Like `is_set`, but only if set from a specific Source.
152    #[must_use]
153    pub fn is_set_from(&self, name: &str, src: Source) -> bool {
154        let k = key_for_strings(&self.leaf_path, name);
155        matches!(self.status.get(&k), Some(Status::Set(s)) if *s == src)
156    }
157
158    /// Number of times a `flag` appeared in the leaf scope.
159    /// NOTE: only flags are counted; options with values return 0.
160    #[must_use]
161    pub fn flag_count(&self, name: &str) -> usize {
162        let k = key_for_strings(&self.leaf_path, name);
163        *self.flag_counts.get(&k).unwrap_or(&0)
164    }
165}
166
167/// Read‑only scoped accessor into `Matches`.
168pub struct MatchView<'a> {
169    m: &'a Matches,
170    path: Vec<&'a str>,
171}
172
173impl MatchView<'_> {
174    /// Current command path for this view.
175    #[must_use]
176    #[allow(clippy::missing_const_for_fn)]
177    pub fn path(&self) -> &[&str] {
178        &self.path
179    }
180
181    /// True if a flag/option `name` is present in this scope.
182    #[must_use]
183    pub fn is_set(&self, name: &str) -> bool {
184        let k = key_for(&self.path, name);
185        self.m.status.contains_key(&k)
186    }
187
188    /// True if present and from the given `Source`.
189    #[must_use]
190    pub fn is_set_from(&self, name: &str, src: Source) -> bool {
191        let k = key_for(&self.path, name);
192        matches!(self.m.status.get(&k), Some(Status::Set(s)) if *s == src)
193    }
194
195    /// Number of times a `flag` appeared in this scope.
196    /// NOTE: only flags are counted; options with values return 0.
197    #[must_use]
198    pub fn flag_count(&self, name: &str) -> usize {
199        let k = key_for(&self.path, name);
200        *self.m.flag_counts.get(&k).unwrap_or(&0)
201    }
202
203    /// Get a single **option** value (first of many if repeated).
204    #[must_use]
205    pub fn value(&self, name: &str) -> Option<&OsStr> {
206        let k = key_for(&self.path, name);
207        match self.m.values.get(&k) {
208            Some(Value::One(v)) => Some(v.as_os_str()),
209            Some(Value::Many(vs)) => vs.first().map(std::ffi::OsString::as_os_str),
210            Some(Value::Flag) | None => None,
211        }
212    }
213
214    /// Get all **option** values as a slice.
215    #[must_use]
216    pub fn values(&self, name: &str) -> Option<&[OsString]> {
217        let k = key_for(&self.path, name);
218        match self.m.values.get(&k) {
219            Some(Value::Many(vs)) => Some(vs.as_slice()),
220            Some(Value::One(v)) => Some(std::slice::from_ref(v)),
221            _ => None,
222        }
223    }
224
225    /// Get the first **positional** with `name`.
226    #[must_use]
227    pub fn pos_one(&self, name: &str) -> Option<&OsStr> {
228        let k = pos_key_for(&self.path, name);
229        match self.m.values.get(&k) {
230            Some(Value::One(v)) => Some(v.as_os_str()),
231            Some(Value::Many(vs)) => vs.first().map(std::ffi::OsString::as_os_str),
232            _ => None,
233        }
234    }
235
236    /// Get all **positionals** with `name` as a slice (one becomes a 1‑elem slice).
237    #[must_use]
238    pub fn pos_many(&self, name: &str) -> Option<&[OsString]> {
239        let k = pos_key_for(&self.path, name);
240        match self.m.values.get(&k) {
241            Some(Value::Many(vs)) => Some(vs.as_slice()),
242            Some(Value::One(v)) => Some(std::slice::from_ref(v)),
243            _ => None,
244        }
245    }
246
247    /// Typed parse helper (option). Parses the *first* value via `FromStr`.
248    #[must_use]
249    pub fn parse<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
250        self.value(name).map(|s| s.to_string_lossy().parse::<T>())
251    }
252}