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    // ——— Convenience root/leaf getters (use the leaf scope) ———
100
101    /// Get a positional by name in the *leaf* scope as a slice (one becomes a 1‑elem slice).
102    /// Returns None if not present.
103    #[must_use]
104    pub fn get_position(&self, name: &str) -> Option<&[OsString]> {
105        let k = pos_key_for_strings(&self.leaf_path, name);
106        match self.values.get(&k) {
107            Some(Value::Many(vs)) => Some(vs.as_slice()),
108            Some(Value::One(v)) => Some(std::slice::from_ref(v)),
109            _ => None,
110        }
111    }
112
113    /// Get a single positional by name in the leaf scope.
114    #[must_use]
115    pub fn get_position_one(&self, name: &str) -> Option<&OsStr> {
116        let k = pos_key_for_strings(&self.leaf_path, name);
117        match self.values.get(&k) {
118            Some(Value::One(v)) => Some(v.as_os_str()),
119            Some(Value::Many(vs)) => vs.first().map(std::ffi::OsString::as_os_str),
120            _ => None,
121        }
122    }
123
124    /// Get an option value in the leaf scope (first of many if repeated).
125    #[must_use]
126    pub fn get_value(&self, name: &str) -> Option<&OsStr> {
127        let k = key_for_strings(&self.leaf_path, name);
128        match self.values.get(&k) {
129            Some(Value::One(v)) => Some(v.as_os_str()),
130            Some(Value::Many(vs)) => vs.first().map(std::ffi::OsString::as_os_str),
131            _ => None,
132        }
133    }
134
135    /// Get all values for an option in the leaf scope.
136    #[must_use]
137    pub fn get_values(&self, name: &str) -> Option<&[OsString]> {
138        let k = key_for_strings(&self.leaf_path, name);
139        match self.values.get(&k) {
140            Some(Value::Many(vs)) => Some(vs.as_slice()),
141            Some(Value::One(v)) => Some(std::slice::from_ref(v)),
142            _ => None,
143        }
144    }
145
146    /// Test whether a flag/option was set in the leaf scope (from any Source).
147    #[must_use]
148    pub fn is_set(&self, name: &str) -> bool {
149        let k = key_for_strings(&self.leaf_path, name);
150        self.status.contains_key(&k)
151    }
152
153    /// Like `is_set`, but only if set from a specific Source.
154    #[must_use]
155    pub fn is_set_from(&self, name: &str, src: Source) -> bool {
156        let k = key_for_strings(&self.leaf_path, name);
157        matches!(self.status.get(&k), Some(Status::Set(s)) if *s == src)
158    }
159}
160
161/// Read‑only scoped accessor into `Matches`.
162pub struct MatchView<'a> {
163    m: &'a Matches,
164    path: Vec<&'a str>,
165}
166
167impl MatchView<'_> {
168    /// Current command path for this view.
169    #[must_use]
170    #[allow(clippy::missing_const_for_fn)]
171    pub fn path(&self) -> &[&str] {
172        &self.path
173    }
174
175    /// True if a flag/option `name` is present in this scope.
176    #[must_use]
177    pub fn is_set(&self, name: &str) -> bool {
178        let k = key_for(&self.path, name);
179        self.m.status.contains_key(&k)
180    }
181
182    /// True if present and from the given `Source`.
183    #[must_use]
184    pub fn is_set_from(&self, name: &str, src: Source) -> bool {
185        let k = key_for(&self.path, name);
186        matches!(self.m.status.get(&k), Some(Status::Set(s)) if *s == src)
187    }
188
189    /// Get a single **option** value (first of many if repeated).
190    #[must_use]
191    pub fn value(&self, name: &str) -> Option<&OsStr> {
192        let k = key_for(&self.path, name);
193        match self.m.values.get(&k) {
194            Some(Value::One(v)) => Some(v.as_os_str()),
195            Some(Value::Many(vs)) => vs.first().map(std::ffi::OsString::as_os_str),
196            Some(Value::Flag) | None => None,
197        }
198    }
199
200    /// Get all **option** values as a slice.
201    #[must_use]
202    pub fn values(&self, name: &str) -> Option<&[OsString]> {
203        let k = key_for(&self.path, name);
204        match self.m.values.get(&k) {
205            Some(Value::Many(vs)) => Some(vs.as_slice()),
206            Some(Value::One(v)) => Some(std::slice::from_ref(v)),
207            _ => None,
208        }
209    }
210
211    /// Get the first **positional** with `name`.
212    #[must_use]
213    pub fn pos_one(&self, name: &str) -> Option<&OsStr> {
214        let k = pos_key_for(&self.path, name);
215        match self.m.values.get(&k) {
216            Some(Value::One(v)) => Some(v.as_os_str()),
217            Some(Value::Many(vs)) => vs.first().map(std::ffi::OsString::as_os_str),
218            _ => None,
219        }
220    }
221
222    /// Get all **positionals** with `name` as a slice (one becomes a 1‑elem slice).
223    #[must_use]
224    pub fn pos_many(&self, name: &str) -> Option<&[OsString]> {
225        let k = pos_key_for(&self.path, name);
226        match self.m.values.get(&k) {
227            Some(Value::Many(vs)) => Some(vs.as_slice()),
228            Some(Value::One(v)) => Some(std::slice::from_ref(v)),
229            _ => None,
230        }
231    }
232
233    /// Typed parse helper (option). Parses the *first* value via `FromStr`.
234    #[must_use]
235    pub fn parse<T: std::str::FromStr>(&self, name: &str) -> Option<Result<T, T::Err>> {
236        self.value(name).map(|s| s.to_string_lossy().parse::<T>())
237    }
238}