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}