1pub mod bootstrap;
7pub mod commands;
8pub mod complete;
9pub mod error;
10pub mod help;
11pub mod router;
12pub mod schema;
13pub mod token;
14pub mod types;
15
16use std::collections::HashMap;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum OutputFormat {
21 #[default]
23 Human,
24 Json,
26 Yaml,
28}
29
30impl OutputFormat {
31 pub fn from_str(s: &str) -> Option<Self> {
32 match s.to_lowercase().as_str() {
33 "human" | "h" | "text" => Some(OutputFormat::Human),
34 "json" | "j" => Some(OutputFormat::Json),
35 "yaml" | "yml" | "y" => Some(OutputFormat::Yaml),
36 _ => None,
37 }
38 }
39
40 pub fn as_str(&self) -> &str {
41 match self {
42 OutputFormat::Human => "human",
43 OutputFormat::Json => "json",
44 OutputFormat::Yaml => "yaml",
45 }
46 }
47}
48
49#[derive(Debug, Clone, Default)]
54pub struct CliContext {
55 pub raw: Vec<String>,
57 pub domain: Option<String>,
59 pub resource: Option<String>,
61 pub verb: Option<String>,
63 pub target: Option<String>,
65 pub args: Vec<String>,
67 pub flags: HashMap<String, String>,
69}
70
71impl CliContext {
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 pub fn get_flag(&self, key: &str) -> Option<String> {
78 self.flags.get(key).cloned()
79 }
80
81 pub fn has_flag(&self, key: &str) -> bool {
83 self.flags.contains_key(key)
84 }
85
86 pub fn get_flag_or(&self, key: &str, default: &str) -> String {
88 self.get_flag(key).unwrap_or_else(|| default.to_string())
89 }
90
91 pub fn domain_only(&self) -> Option<&str> {
93 self.domain.as_deref()
94 }
95
96 pub fn wants_json(&self) -> bool {
98 self.has_flag("json") || self.has_flag("j")
99 }
100
101 pub fn wants_machine_output(&self) -> bool {
103 self.get_output_format() != OutputFormat::Human
104 }
105
106 pub fn get_output_format(&self) -> OutputFormat {
108 if self.wants_json() {
109 return OutputFormat::Json;
110 }
111
112 let format_str = self
114 .get_flag("output")
115 .or_else(|| self.get_flag("o"))
116 .or_else(|| self.get_flag("format"));
117
118 if let Some(format_str) = format_str {
119 OutputFormat::from_str(&format_str).unwrap_or_default()
120 } else {
121 OutputFormat::default()
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_cli_context_default() {
132 let ctx = CliContext::default();
133 assert!(ctx.raw.is_empty());
134 assert!(ctx.domain.is_none());
135 assert!(ctx.resource.is_none());
136 assert!(ctx.verb.is_none());
137 assert!(ctx.target.is_none());
138 assert!(ctx.args.is_empty());
139 assert!(ctx.flags.is_empty());
140 }
141
142 #[test]
143 fn test_cli_context_new() {
144 let ctx = CliContext::new();
145 assert!(ctx.raw.is_empty());
146 assert!(ctx.domain.is_none());
147 }
148
149 #[test]
150 fn test_get_flag_from_cli() {
151 let mut ctx = CliContext::new();
152 ctx.flags.insert("path".to_string(), "/data".to_string());
153 ctx.flags
154 .insert("bind".to_string(), "0.0.0.0:6380".to_string());
155
156 assert_eq!(ctx.get_flag("path"), Some("/data".to_string()));
157 assert_eq!(ctx.get_flag("bind"), Some("0.0.0.0:6380".to_string()));
158 assert_eq!(ctx.get_flag("nonexistent"), None);
159 }
160
161 #[test]
162 fn test_has_flag() {
163 let mut ctx = CliContext::new();
164 ctx.flags.insert("verbose".to_string(), "true".to_string());
165 ctx.flags.insert("quiet".to_string(), "".to_string());
166
167 assert!(ctx.has_flag("verbose"));
168 assert!(ctx.has_flag("quiet"));
169 assert!(!ctx.has_flag("nonexistent"));
170 }
171
172 #[test]
173 fn test_get_flag_or() {
174 let mut ctx = CliContext::new();
175 ctx.flags.insert("path".to_string(), "/data".to_string());
176
177 assert_eq!(ctx.get_flag_or("path", "/default"), "/data");
178 assert_eq!(ctx.get_flag_or("bind", "0.0.0.0:6380"), "0.0.0.0:6380");
179 }
180
181 #[test]
182 fn test_domain_only() {
183 let mut ctx = CliContext::new();
184 assert_eq!(ctx.domain_only(), None);
185
186 ctx.domain = Some("server".to_string());
187 assert_eq!(ctx.domain_only(), Some("server"));
188 }
189
190 #[test]
191 fn test_get_output_format_default() {
192 let ctx = CliContext::new();
193 let format = ctx.get_output_format();
194 assert_eq!(format, OutputFormat::default());
195 }
196
197 #[test]
198 fn test_get_output_format_from_output_flag() {
199 let mut ctx = CliContext::new();
200 ctx.flags.insert("output".to_string(), "json".to_string());
201 let format = ctx.get_output_format();
202 assert_eq!(format, OutputFormat::Json);
203 }
204
205 #[test]
206 fn test_get_output_format_from_o_flag() {
207 let mut ctx = CliContext::new();
208 ctx.flags.insert("o".to_string(), "json".to_string());
209 let format = ctx.get_output_format();
210 assert_eq!(format, OutputFormat::Json);
211 }
212
213 #[test]
214 fn test_get_output_format_from_format_flag() {
215 let mut ctx = CliContext::new();
216 ctx.flags.insert("format".to_string(), "json".to_string());
217 let format = ctx.get_output_format();
218 assert_eq!(format, OutputFormat::Json);
219 }
220
221 #[test]
222 fn test_get_output_format_from_json_flag() {
223 let mut ctx = CliContext::new();
224 ctx.flags.insert("json".to_string(), "true".to_string());
225 let format = ctx.get_output_format();
226 assert_eq!(format, OutputFormat::Json);
227 }
228
229 #[test]
230 fn test_wants_json_from_short_flag() {
231 let mut ctx = CliContext::new();
232 ctx.flags.insert("j".to_string(), "true".to_string());
233 assert!(ctx.wants_json());
234 assert_eq!(ctx.get_output_format(), OutputFormat::Json);
235 }
236
237 #[test]
238 fn test_wants_machine_output_from_yaml_flag() {
239 let mut ctx = CliContext::new();
240 ctx.flags.insert("output".to_string(), "yaml".to_string());
241 assert!(ctx.wants_machine_output());
242 }
243
244 #[test]
245 fn test_get_output_format_priority() {
246 let mut ctx = CliContext::new();
247 ctx.flags.insert("output".to_string(), "json".to_string());
249 ctx.flags.insert("o".to_string(), "text".to_string());
250 ctx.flags.insert("format".to_string(), "csv".to_string());
251 let format = ctx.get_output_format();
252 assert_eq!(format, OutputFormat::Json);
253 }
254
255 #[test]
256 fn test_cli_context_with_full_command() {
257 let mut ctx = CliContext::new();
258 ctx.raw = vec![
259 "server".to_string(),
260 "--path".to_string(),
261 "/data".to_string(),
262 "--bind".to_string(),
263 "0.0.0.0:6380".to_string(),
264 ];
265 ctx.domain = Some("server".to_string());
266 ctx.flags.insert("path".to_string(), "/data".to_string());
267 ctx.flags
268 .insert("bind".to_string(), "0.0.0.0:6380".to_string());
269
270 assert_eq!(ctx.domain_only(), Some("server"));
271 assert_eq!(ctx.get_flag("path"), Some("/data".to_string()));
272 assert_eq!(ctx.get_flag_or("bind", "localhost:6380"), "0.0.0.0:6380");
273 assert!(ctx.has_flag("path"));
274 assert!(!ctx.has_flag("verbose"));
275 }
276
277 #[test]
278 fn test_cli_context_with_args() {
279 let mut ctx = CliContext::new();
280 ctx.args = vec!["arg1".to_string(), "arg2".to_string(), "arg3".to_string()];
281
282 assert_eq!(ctx.args.len(), 3);
283 assert_eq!(ctx.args[0], "arg1");
284 assert_eq!(ctx.args[1], "arg2");
285 assert_eq!(ctx.args[2], "arg3");
286 }
287
288 #[test]
289 fn test_cli_context_clone() {
290 let mut ctx = CliContext::new();
291 ctx.domain = Some("server".to_string());
292 ctx.flags
293 .insert("bind".to_string(), "0.0.0.0:6380".to_string());
294
295 let ctx2 = ctx.clone();
296 assert_eq!(ctx2.domain, ctx.domain);
297 assert_eq!(ctx2.get_flag("bind"), ctx.get_flag("bind"));
298 }
299
300 #[test]
301 fn test_cli_context_debug() {
302 let ctx = CliContext::new();
303 let debug_str = format!("{:?}", ctx);
304 assert!(debug_str.contains("CliContext"));
305 assert!(debug_str.contains("raw"));
306 assert!(debug_str.contains("domain"));
307 }
308
309 #[test]
310 fn test_output_format_from_str() {
311 assert_eq!(OutputFormat::from_str("json"), Some(OutputFormat::Json));
312 assert_eq!(OutputFormat::from_str("JSON"), Some(OutputFormat::Json));
313 assert_eq!(OutputFormat::from_str("yaml"), Some(OutputFormat::Yaml));
314 assert_eq!(OutputFormat::from_str("yml"), Some(OutputFormat::Yaml));
315 assert_eq!(OutputFormat::from_str("human"), Some(OutputFormat::Human));
316 assert_eq!(OutputFormat::from_str("text"), Some(OutputFormat::Human));
317 assert_eq!(OutputFormat::from_str("xml"), None);
318 }
319
320 #[test]
321 fn test_output_format_as_str() {
322 assert_eq!(OutputFormat::Human.as_str(), "human");
323 assert_eq!(OutputFormat::Json.as_str(), "json");
324 assert_eq!(OutputFormat::Yaml.as_str(), "yaml");
325 }
326}