Skip to main content

argot_cmd/model/
argument.rs

1use serde::{Deserialize, Serialize};
2
3use super::BuildError;
4
5/// A positional argument accepted by a command.
6///
7/// Arguments are bound in declaration order when the parser consumes tokens
8/// after the command (and any subcommand) has been identified. Optional
9/// arguments may carry a default value that is substituted when the argument
10/// is absent.
11///
12/// Use [`Argument::builder`] to construct instances.
13///
14/// # Examples
15///
16/// ```
17/// # use argot_cmd::Argument;
18/// let arg = Argument::builder("target")
19///     .description("Deployment target environment")
20///     .required()
21///     .build()
22///     .unwrap();
23///
24/// assert_eq!(arg.name, "target");
25/// assert!(arg.required);
26/// assert!(arg.default.is_none());
27/// ```
28#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
29pub struct Argument {
30    /// The canonical name of this argument, used as the key in [`crate::ParsedCommand::args`].
31    pub name: String,
32    /// Human-readable description shown in help output.
33    pub description: String,
34    /// Whether the parser returns an error when this argument is absent.
35    pub required: bool,
36    /// Value substituted when the argument is not provided (optional arguments only).
37    pub default: Option<String>,
38    /// Whether this argument consumes all remaining tokens (must be the last argument).
39    pub variadic: bool,
40}
41
42/// Consuming builder for [`Argument`].
43///
44/// Obtain via [`Argument::builder`]. Call [`ArgumentBuilder::build`] when done.
45///
46/// # Examples
47///
48/// ```
49/// # use argot_cmd::Argument;
50/// let arg = Argument::builder("format")
51///     .description("Output format")
52///     .default_value("text")
53///     .build()
54///     .unwrap();
55///
56/// assert_eq!(arg.default.as_deref(), Some("text"));
57/// assert!(!arg.required);
58/// ```
59pub struct ArgumentBuilder {
60    name: String,
61    description: String,
62    required: bool,
63    default: Option<String>,
64    variadic: bool,
65}
66
67impl Argument {
68    /// Create a new [`ArgumentBuilder`] with the given name.
69    ///
70    /// # Arguments
71    ///
72    /// - `name` — The argument name. Must be non-empty after trimming
73    ///   (enforced by [`ArgumentBuilder::build`]).
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// # use argot_cmd::Argument;
79    /// let arg = Argument::builder("file").build().unwrap();
80    /// assert_eq!(arg.name, "file");
81    /// ```
82    pub fn builder(name: impl Into<String>) -> ArgumentBuilder {
83        ArgumentBuilder {
84            name: name.into(),
85            description: String::new(),
86            required: false,
87            default: None,
88            variadic: false,
89        }
90    }
91}
92
93impl ArgumentBuilder {
94    /// Set the human-readable description for this argument.
95    pub fn description(mut self, d: impl Into<String>) -> Self {
96        self.description = d.into();
97        self
98    }
99
100    /// Mark this argument as required.
101    ///
102    /// The parser will return [`crate::ParseError::MissingArgument`] if the
103    /// argument is absent from the invocation.
104    pub fn required(mut self) -> Self {
105        self.required = true;
106        self
107    }
108
109    /// Set the default value used when this argument is not provided.
110    ///
111    /// A default value is only meaningful for optional (non-required) arguments.
112    /// If the argument is required *and* has a default, the default is still
113    /// stored but the parser will still require the argument to be supplied.
114    pub fn default_value(mut self, d: impl Into<String>) -> Self {
115        self.default = Some(d.into());
116        self
117    }
118
119    /// Mark this argument as variadic (consumes all remaining tokens).
120    ///
121    /// A variadic argument must be the last argument defined on the command.
122    /// [`crate::CommandBuilder::build`] enforces this constraint and returns
123    /// [`crate::BuildError::VariadicNotLast`] if a variadic argument is
124    /// followed by another argument.
125    pub fn variadic(mut self) -> Self {
126        self.variadic = true;
127        self
128    }
129
130    /// Consume the builder and return an [`Argument`].
131    ///
132    /// # Errors
133    ///
134    /// Returns [`BuildError::EmptyCanonical`] if the argument name is empty or
135    /// consists entirely of whitespace.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// # use argot_cmd::{Argument, BuildError};
141    /// assert!(Argument::builder("env").build().is_ok());
142    /// assert_eq!(Argument::builder("").build().unwrap_err(), BuildError::EmptyCanonical);
143    /// ```
144    pub fn build(self) -> Result<Argument, BuildError> {
145        if self.name.trim().is_empty() {
146            return Err(BuildError::EmptyCanonical);
147        }
148        Ok(Argument {
149            name: self.name,
150            description: self.description,
151            required: self.required,
152            default: self.default,
153            variadic: self.variadic,
154        })
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    struct TestCase {
163        name: &'static str,
164        arg_name: &'static str,
165        required: bool,
166        expect_err: bool,
167    }
168
169    #[test]
170    fn test_builder() {
171        let cases = vec![
172            TestCase {
173                name: "happy path",
174                arg_name: "file",
175                required: false,
176                expect_err: false,
177            },
178            TestCase {
179                name: "required",
180                arg_name: "file",
181                required: true,
182                expect_err: false,
183            },
184            TestCase {
185                name: "empty name",
186                arg_name: "",
187                required: false,
188                expect_err: true,
189            },
190            TestCase {
191                name: "whitespace name",
192                arg_name: "   ",
193                required: false,
194                expect_err: true,
195            },
196        ];
197
198        for tc in cases {
199            let mut b = Argument::builder(tc.arg_name).description("a file");
200            if tc.required {
201                b = b.required();
202            }
203            let result = b.build();
204            assert_eq!(result.is_err(), tc.expect_err, "case: {}", tc.name);
205        }
206    }
207
208    #[test]
209    fn test_serde_round_trip() {
210        let arg = Argument::builder("path")
211            .description("target path")
212            .required()
213            .build()
214            .unwrap();
215        let json = serde_json::to_string(&arg).unwrap();
216        let de: Argument = serde_json::from_str(&json).unwrap();
217        assert_eq!(arg, de);
218    }
219}