Skip to main content

noargs/
arg.rs

1use crate::{
2    args::{Metadata, RawArgs},
3    error::Error,
4};
5
6/// Specification for [`Arg`].
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub struct ArgSpec {
9    /// Value name (usually SCREAMING_SNAKE_CASE).
10    pub name: &'static str,
11
12    /// Documentation.
13    pub doc: &'static str,
14
15    /// Default value.
16    pub default: Option<&'static str>,
17
18    /// Example value (if this is set, the argument is considered to be requried when generating the help text).
19    ///
20    /// This is only used if `RawArgs::metadata().help_mode` is `true`.
21    pub example: Option<&'static str>,
22}
23
24impl ArgSpec {
25    /// The default specification.
26    pub const DEFAULT: Self = Self {
27        name: "<ARGUMENT>",
28        doc: "",
29        default: None,
30        example: None,
31    };
32
33    /// Makes an [`ArgSpec`] instance with a specified name (equivalent to `noargs::arg(name)`).
34    pub const fn new(name: &'static str) -> Self {
35        Self {
36            name,
37            ..Self::DEFAULT
38        }
39    }
40
41    /// Updates the value of [`ArgSpec::doc`].
42    pub const fn doc(mut self, doc: &'static str) -> Self {
43        self.doc = doc;
44        self
45    }
46
47    /// Updates the value of [`ArgSpec::default`].
48    pub const fn default(mut self, default: &'static str) -> Self {
49        self.default = Some(default);
50        self
51    }
52
53    /// Updates the value of [`ArgSpec::example`].
54    pub const fn example(mut self, example: &'static str) -> Self {
55        self.example = Some(example);
56        self
57    }
58
59    /// Takes the first [`Arg`] instance that satisfies this specification from the raw arguments.
60    pub fn take(self, args: &mut RawArgs) -> Arg {
61        let metadata = args.metadata();
62        args.with_record_arg(|args| {
63            if args.metadata().help_mode {
64                return if self.default.is_some() {
65                    Arg::Default {
66                        spec: self,
67                        metadata,
68                    }
69                } else if self.example.is_some() {
70                    Arg::Example {
71                        spec: self,
72                        metadata,
73                    }
74                } else {
75                    Arg::None { spec: self }
76                };
77            }
78
79            for (index, raw_arg) in args.raw_args_mut().iter_mut().enumerate() {
80                if let Some(value) = raw_arg.value.take() {
81                    return Arg::Positional {
82                        spec: self,
83                        metadata,
84                        index,
85                        value,
86                    };
87                };
88            }
89
90            if self.default.is_some() {
91                Arg::Default {
92                    spec: self,
93                    metadata,
94                }
95            } else {
96                Arg::None { spec: self }
97            }
98        })
99    }
100}
101
102impl Default for ArgSpec {
103    fn default() -> Self {
104        Self::DEFAULT
105    }
106}
107
108/// A positional argument.
109#[derive(Debug, Clone, PartialEq, Eq, Hash)]
110#[allow(missing_docs)]
111pub enum Arg {
112    Positional {
113        spec: ArgSpec,
114        metadata: Metadata,
115        index: usize,
116        value: String,
117    },
118    Default {
119        spec: ArgSpec,
120        metadata: Metadata,
121    },
122    Example {
123        spec: ArgSpec,
124        metadata: Metadata,
125    },
126    None {
127        spec: ArgSpec,
128    },
129}
130
131impl Arg {
132    /// Returns the specification of this argument.
133    pub fn spec(&self) -> ArgSpec {
134        match self {
135            Arg::Positional { spec, .. }
136            | Arg::Default { spec, .. }
137            | Arg::Example { spec, .. }
138            | Arg::None { spec } => *spec,
139        }
140    }
141
142    /// Returns `true` if this argument has a value.
143    pub fn is_present(&self) -> bool {
144        !matches!(self, Self::None { .. })
145    }
146
147    /// Returns `Some(self)` if this argument is present.
148    pub fn present(self) -> Option<Self> {
149        self.is_present().then_some(self)
150    }
151
152    /// Applies additional conversion or validation to the argument.
153    ///
154    /// This method allows for chaining transformations and validations when an argument is present.
155    /// It first checks if the argument has a value and then applies the provided function.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// let mut args = noargs::RawArgs::new(["example", "42"].iter().map(|a| a.to_string()));
161    /// let arg = noargs::arg("<NUMBER>").take(&mut args);
162    ///
163    /// // Parse as number and ensure it's positive
164    /// let num = arg.then(|arg| -> Result<_, Box<dyn std::error::Error>> {
165    ///     let n: i32 = arg.value().parse()?;
166    ///     if n <= 0 {
167    ///         return Err("number must be positive".into());
168    ///     }
169    ///     Ok(n)
170    /// })?;
171    /// # Ok::<(), noargs::Error>(())
172    /// ```
173    ///
174    /// # Errors
175    ///
176    /// - Returns [`Error::MissingArg`] if `self.is_present()` is `false` (argument is missing)
177    /// - Returns [`Error::InvalidArg`] if `f(self)` returns `Err(_)` (validation or conversion failed)
178    pub fn then<F, T, E>(self, f: F) -> Result<T, Error>
179    where
180        F: FnOnce(Self) -> Result<T, E>,
181        E: std::fmt::Display,
182    {
183        if !self.is_present() {
184            return Err(Error::MissingArg {
185                arg: Box::new(self),
186            });
187        }
188        f(self.clone()).map_err(|e| Error::InvalidArg {
189            arg: Box::new(self),
190            reason: e.to_string(),
191        })
192    }
193
194    /// Shorthand for `self.present().map(|arg| arg.then(f)).transpose()`.
195    pub fn present_and_then<F, T, E>(self, f: F) -> Result<Option<T>, Error>
196    where
197        F: FnOnce(Self) -> Result<T, E>,
198        E: std::fmt::Display,
199    {
200        self.present().map(|arg| arg.then(f)).transpose()
201    }
202
203    /// Returns the raw value of this argument, or an empty string if not present.
204    pub fn value(&self) -> &str {
205        match self {
206            Arg::Positional { value, .. } => value.as_str(),
207            Arg::Default { spec, .. } => spec.default.unwrap_or(""),
208            Arg::Example { spec, .. } => spec.example.unwrap_or(""),
209            Arg::None { .. } => "",
210        }
211    }
212
213    /// Returns the index at which the raw value of this argument was located in [`RawArgs`].
214    pub fn index(&self) -> Option<usize> {
215        if let Arg::Positional { index, .. } = self {
216            Some(*index)
217        } else {
218            None
219        }
220    }
221
222    pub(crate) fn metadata(&self) -> Option<Metadata> {
223        match self {
224            Arg::Positional { metadata, .. }
225            | Arg::Default { metadata, .. }
226            | Arg::Example { metadata, .. } => Some(*metadata),
227            Arg::None { .. } => None,
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn required_arg() {
238        let mut args = test_args(&["test", "foo", "bar"]);
239        let arg = crate::arg("ARG");
240        assert!(matches!(
241            arg.take(&mut args),
242            Arg::Positional { index: 1, .. }
243        ));
244        assert!(matches!(
245            arg.take(&mut args),
246            Arg::Positional { index: 2, .. }
247        ));
248        assert!(matches!(arg.take(&mut args), Arg::None { .. }));
249    }
250
251    #[test]
252    fn optional_arg() {
253        let mut args = test_args(&["test", "foo"]);
254        let arg = crate::arg("ARG").default("bar");
255        assert!(matches!(
256            arg.take(&mut args),
257            Arg::Positional { index: 1, .. }
258        ));
259        assert!(matches!(arg.take(&mut args), Arg::Default { .. }));
260        assert!(matches!(arg.take(&mut args), Arg::Default { .. }));
261    }
262
263    #[test]
264    fn example_arg() {
265        let mut args = test_args(&["test", "foo"]);
266        args.metadata_mut().help_mode = true;
267
268        let arg = crate::arg("ARG").example("bar");
269        assert!(matches!(arg.take(&mut args), Arg::Example { .. }));
270        assert!(matches!(arg.take(&mut args), Arg::Example { .. }));
271    }
272
273    #[test]
274    fn parse_arg() {
275        let mut args = test_args(&["test", "1", "not a number"]);
276        let arg = crate::arg("ARG");
277        assert_eq!(
278            arg.take(&mut args)
279                .then(|a| a.value().parse::<usize>())
280                .ok(),
281            Some(1)
282        );
283        assert_eq!(
284            arg.take(&mut args)
285                .then(|a| a.value().parse::<usize>())
286                .ok(),
287            None
288        );
289        assert_eq!(
290            arg.take(&mut args)
291                .then(|a| a.value().parse::<usize>())
292                .ok(),
293            None
294        );
295    }
296
297    fn test_args(raw_args: &[&str]) -> RawArgs {
298        RawArgs::new(raw_args.iter().map(|a| a.to_string()))
299    }
300}