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