soma_cli_core/
common_args.rs1use std::error::Error;
14use std::fmt;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ArgParseError(String);
28
29impl ArgParseError {
30 pub fn message(&self) -> &str {
32 &self.0
33 }
34}
35
36impl fmt::Display for ArgParseError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 f.write_str(&self.0)
39 }
40}
41
42impl Error for ArgParseError {}
43
44fn err(message: impl Into<String>) -> ArgParseError {
45 ArgParseError(message.into())
46}
47
48pub fn reject_args(args: &[String], command: &str) -> Result<(), ArgParseError> {
50 if args.is_empty() {
51 Ok(())
52 } else {
53 Err(err(format!(
54 "{command} does not accept argument `{}`",
55 args[0]
56 )))
57 }
58}
59
60pub fn parse_bool_flag(args: &[String], command: &str, flag: &str) -> Result<bool, ArgParseError> {
63 let mut found = false;
64 for arg in args {
65 if arg == flag {
66 if found {
67 return Err(err(format!("{command} received duplicate {flag}")));
68 }
69 found = true;
70 } else {
71 return Err(err(format!("{command} does not accept argument `{arg}`")));
72 }
73 }
74 Ok(found)
75}
76
77pub fn parse_optional_value_flag(
81 args: &[String],
82 command: &str,
83 flag: &str,
84) -> Result<Option<String>, ArgParseError> {
85 match args {
86 [] => Ok(None),
87 [found_flag, value] if found_flag == flag => {
88 if value.starts_with("--") {
89 Err(err(format!("{command} requires a value after {flag}")))
90 } else {
91 Ok(Some(value.clone()))
92 }
93 }
94 [found_flag] if found_flag == flag => {
95 Err(err(format!("{command} requires a value after {flag}")))
96 }
97 [found_flag, value, rest @ ..] if found_flag == flag => {
98 if value.starts_with("--") {
99 Err(err(format!("{command} requires a value after {flag}")))
100 } else if rest.iter().any(|arg| arg == flag) {
101 Err(err(format!("{command} received duplicate {flag}")))
102 } else {
103 Err(err(format!(
104 "{command} does not accept argument `{}`",
105 rest[0]
106 )))
107 }
108 }
109 [unexpected, ..] => Err(err(format!(
110 "{command} does not accept argument `{unexpected}`"
111 ))),
112 }
113}
114
115pub fn parse_required_value_flag(
122 args: &[String],
123 command: &str,
124 flag: &str,
125) -> Result<Option<String>, ArgParseError> {
126 parse_optional_value_flag(args, command, flag)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn v(items: &[&str]) -> Vec<String> {
134 items.iter().map(|s| s.to_string()).collect()
135 }
136
137 #[test]
138 fn reject_args_accepts_empty() {
139 reject_args(&[], "status").unwrap();
140 }
141
142 #[test]
143 fn reject_args_rejects_extra() {
144 let err = reject_args(&v(&["bogus"]), "status").unwrap_err();
145 assert!(err.to_string().contains("status does not accept argument"));
146 }
147
148 #[test]
149 fn bool_flag_found_and_absent() {
150 assert!(parse_bool_flag(&v(&["--json"]), "doctor", "--json").unwrap());
151 assert!(!parse_bool_flag(&[], "doctor", "--json").unwrap());
152 }
153
154 #[test]
155 fn bool_flag_rejects_duplicate() {
156 let err = parse_bool_flag(&v(&["--json", "--json"]), "doctor", "--json").unwrap_err();
157 assert!(err.to_string().contains("duplicate"));
158 }
159
160 #[test]
161 fn optional_value_flag_round_trips() {
162 assert_eq!(
163 parse_optional_value_flag(&v(&["--name", "Alice"]), "greet", "--name").unwrap(),
164 Some("Alice".to_string())
165 );
166 assert_eq!(
167 parse_optional_value_flag(&[], "greet", "--name").unwrap(),
168 None
169 );
170 }
171
172 #[test]
173 fn optional_value_flag_requires_value() {
174 let err = parse_optional_value_flag(&v(&["--name"]), "greet", "--name").unwrap_err();
175 assert!(err.to_string().contains("requires a value"));
176 }
177
178 #[test]
179 fn optional_value_flag_rejects_duplicate() {
180 let err =
181 parse_optional_value_flag(&v(&["--name", "Alice", "--name", "Bob"]), "greet", "--name")
182 .unwrap_err();
183 assert!(err.to_string().contains("duplicate --name"));
184 }
185}