1use super::commands::{Flag, Route};
7
8const LABEL_WIDTH: usize = 20;
10
11pub fn format_global_help(domains: &[(String, String, Vec<String>)]) -> String {
15 let mut out = String::with_capacity(1024);
16
17 out.push_str("reddb -- unified multi-model database engine\n");
18 out.push('\n');
19 out.push_str("Usage: red <command> [args] [flags]\n");
20 out.push('\n');
21
22 out.push_str("Commands:\n");
23 for (name, description, aliases) in domains {
24 let alias_text = if aliases.is_empty() {
25 String::new()
26 } else {
27 format!(" [{}]", aliases.join(", "))
28 };
29 out.push_str(&format!(" {:<14} {}{}\n", name, description, alias_text));
30 }
31 out.push('\n');
32
33 out.push_str("Global flags:\n");
34 out.push_str(&format!(" {:<20} {}\n", "-h, --help", "Show help"));
35 out.push_str(&format!(" {:<20} {}\n", "-j, --json", "Force JSON output"));
36 out.push_str(&format!(
37 " {:<20} {}\n",
38 "-o, --output FORMAT", "Output format [text|json|yaml]"
39 ));
40 out.push_str(&format!(" {:<20} {}\n", "-v, --verbose", "Verbose output"));
41 out.push_str(&format!(
42 " {:<20} {}\n",
43 " --no-color", "Disable colors"
44 ));
45 out.push_str(&format!(" {:<20} {}\n", " --version", "Show version"));
46 out.push('\n');
47
48 out.push_str("Run 'red <command> help' for more information\n");
49 out
50}
51
52pub fn format_domain_help(domain: &str, resources: &[(String, String, Vec<Route>)]) -> String {
56 let mut out = String::with_capacity(512);
57
58 out.push_str(&format!("red {} -- {}\n", domain, domain_label(domain)));
59 out.push('\n');
60
61 out.push_str("Resources:\n");
62 for (name, description, routes) in resources {
63 out.push_str(&format!(" {:<14} {}\n", name, description));
64 for route in routes {
65 out.push_str(&format!(" {:<12} {}\n", route.verb, route.summary));
66 }
67 }
68 out.push('\n');
69
70 out.push_str(&format!(
71 "Run 'red {} <resource> help' for more information\n",
72 domain
73 ));
74 out
75}
76
77pub fn format_command_help(
79 domain: &str,
80 resource: &str,
81 routes: &[Route],
82 flags: &[Flag],
83) -> String {
84 let mut out = String::with_capacity(512);
85
86 out.push_str(&format!("red {} {} -- {}\n", domain, resource, resource));
87 out.push('\n');
88
89 if !routes.is_empty() {
90 out.push_str("Verbs:\n");
91 for route in routes {
92 out.push_str(&format!(" {:<14} {}\n", route.verb, route.summary));
93 }
94 out.push('\n');
95 }
96
97 let all_flags = merge_with_global_flags(flags);
98 if !all_flags.is_empty() {
99 out.push_str("Flags:\n");
100 for flag in &all_flags {
101 out.push_str(&format_flag(flag));
102 out.push('\n');
103 }
104 out.push('\n');
105 }
106
107 if !routes.is_empty() {
108 out.push_str("Examples:\n");
109 for route in routes {
110 if !route.usage.is_empty() {
111 out.push_str(&format!(" {}\n", route.usage));
112 }
113 }
114 if routes.iter().any(|r| !r.usage.is_empty()) {
115 out.push('\n');
116 }
117 }
118
119 out
120}
121
122pub fn format_route_help(domain: &str, resource: &str, route: &Route, flags: &[Flag]) -> String {
124 let mut out = String::with_capacity(512);
125
126 out.push_str(&format!(
127 "red {} {} {} -- {}\n",
128 domain, resource, route.verb, route.summary
129 ));
130 out.push('\n');
131
132 out.push_str(&format!(
134 "Usage: red {} {} {} <target>",
135 domain, resource, route.verb
136 ));
137 for flag in flags {
138 if let Some(ref arg_name) = flag.arg {
139 let token = if let Some(ch) = flag.short {
140 format!("-{}", ch)
141 } else {
142 format!("--{}", flag.long)
143 };
144 out.push_str(&format!(" [{} {}]", token, arg_name.to_uppercase()));
145 }
146 }
147 out.push('\n');
148 out.push('\n');
149
150 if !flags.is_empty() {
152 out.push_str("Flags:\n");
153 for flag in flags {
154 out.push_str(&format_flag(flag));
155 out.push('\n');
156 }
157 out.push('\n');
158 }
159
160 out.push_str("Global flags:\n");
162 for flag in &global_flag_list() {
163 out.push_str(&format_flag(flag));
164 out.push('\n');
165 }
166 out.push('\n');
167
168 out
169}
170
171fn format_flag(flag: &Flag) -> String {
173 let short_part = match flag.short {
174 Some(ch) => format!("-{}, ", ch),
175 None => " ".to_string(),
176 };
177
178 let arg_part = match flag.arg {
179 Some(ref name) => format!(" {}", name.to_uppercase()),
180 None => String::new(),
181 };
182
183 let label = format!("{}--{}{}", short_part, flag.long, arg_part);
184
185 let padding = if label.len() < LABEL_WIDTH {
187 LABEL_WIDTH - label.len()
188 } else {
189 2
190 };
191
192 let default_text = match flag.default {
193 Some(ref d) => format!(" (default: {})", d),
194 None => String::new(),
195 };
196
197 format!(
198 " {}{}{}{}",
199 label,
200 " ".repeat(padding),
201 flag.description,
202 default_text,
203 )
204}
205
206fn domain_label(domain: &str) -> &str {
208 match domain {
209 "server" => "Start the database server/router",
210 "query" => "Execute queries",
211 "insert" => "Insert entities",
212 "get" => "Retrieve entities",
213 "delete" => "Delete entities",
214 "health" => "Health check",
215 "status" => "Replication status",
216 "version" => "Version information",
217 "data" => "Data operations",
218 "index" => "Index management",
219 "graph" => "Graph operations",
220 "replica" => "Start as read replica",
221 _ => domain,
222 }
223}
224
225fn merge_with_global_flags(command_flags: &[Flag]) -> Vec<Flag> {
227 let mut result: Vec<Flag> = command_flags.to_vec();
228 let existing_longs: Vec<&str> = command_flags.iter().map(|f| f.long.as_str()).collect();
229
230 for gf in global_flag_list() {
231 if !existing_longs.contains(&gf.long.as_str()) {
232 result.push(gf);
233 }
234 }
235
236 result
237}
238
239fn global_flag_list() -> Vec<Flag> {
241 vec![
242 Flag::new("help", "Show help").with_short('h'),
243 Flag::new("json", "Force JSON output").with_short('j'),
244 Flag::new("output", "Output format")
245 .with_short('o')
246 .with_arg("FORMAT"),
247 Flag::new("verbose", "Verbose output").with_short('v'),
248 Flag::new("no-color", "Disable colors"),
249 Flag::new("version", "Show version"),
250 ]
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_format_global_help() {
259 let domains = vec![
260 (
261 "server".to_string(),
262 "Start database server".to_string(),
263 vec![],
264 ),
265 (
266 "query".to_string(),
267 "Execute queries".to_string(),
268 vec!["q".to_string()],
269 ),
270 ];
271 let help = format_global_help(&domains);
272
273 assert!(help.contains("reddb"));
274 assert!(help.contains("Usage: red <command>"));
275 assert!(help.contains("Commands:"));
276 assert!(help.contains("server"));
277 assert!(help.contains("Start database server"));
278 assert!(help.contains("[q]"));
279 assert!(help.contains("query"));
280 assert!(help.contains("Global flags:"));
281 assert!(help.contains("-h, --help"));
282 assert!(help.contains("-j, --json"));
283 assert!(help.contains("Run 'red <command> help'"));
284 }
285
286 #[test]
287 fn test_format_domain_help() {
288 let resources = vec![(
289 "collection".to_string(),
290 "Collection operations".to_string(),
291 vec![Route {
292 verb: "list",
293 summary: "List all collections",
294 usage: "red data collection list",
295 }],
296 )];
297 let help = format_domain_help("data", &resources);
298
299 assert!(help.contains("red data"));
300 assert!(help.contains("Resources:"));
301 assert!(help.contains("collection"));
302 assert!(help.contains("Collection operations"));
303 assert!(help.contains("list"));
304 assert!(help.contains("List all collections"));
305 assert!(help.contains("Run 'red data <resource> help'"));
306 }
307
308 #[test]
309 fn test_format_command_help() {
310 let routes = vec![Route {
311 verb: "list",
312 summary: "List all collections",
313 usage: "red data collection list --output json",
314 }];
315 let flags = vec![Flag::new("format", "Output format")
316 .with_short('f')
317 .with_arg("FORMAT")];
318 let help = format_command_help("data", "collection", &routes, &flags);
319
320 assert!(help.contains("red data collection"));
321 assert!(help.contains("Verbs:"));
322 assert!(help.contains("list"));
323 assert!(help.contains("Flags:"));
324 assert!(help.contains("-f, --format FORMAT"));
325 assert!(help.contains("Output format"));
326 assert!(help.contains("Examples:"));
327 assert!(help.contains("red data collection list --output json"));
328 }
329
330 #[test]
331 fn test_format_route_help() {
332 let route = Route {
333 verb: "list",
334 summary: "List all collections",
335 usage: "red data collection list",
336 };
337 let flags = vec![Flag::new("format", "Output format")
338 .with_short('f')
339 .with_arg("FORMAT")];
340 let help = format_route_help("data", "collection", &route, &flags);
341
342 assert!(help.contains("red data collection list -- List all collections"));
343 assert!(help.contains("Usage: red data collection list <target>"));
344 assert!(help.contains("Flags:"));
345 assert!(help.contains("-f, --format FORMAT"));
346 assert!(help.contains("Global flags:"));
347 assert!(help.contains("-h, --help"));
348 }
349
350 #[test]
351 fn test_format_flag_with_short_and_arg() {
352 let flag = Flag::new("type", "Record type")
353 .with_short('t')
354 .with_arg("TYPE");
355 let formatted = format_flag(&flag);
356
357 assert!(formatted.contains("-t, --type TYPE"));
358 assert!(formatted.contains("Record type"));
359 }
360
361 #[test]
362 fn test_format_flag_boolean() {
363 let flag = Flag::new("verbose", "Enable verbose output");
364 let formatted = format_flag(&flag);
365
366 assert!(formatted.starts_with(" --verbose"));
367 assert!(formatted.contains("Enable verbose output"));
368 assert!(!formatted.contains("VERBOSE"));
370 }
371
372 #[test]
373 fn test_format_flag_with_default() {
374 let flag = Flag::new("format", "Output format")
375 .with_short('f')
376 .with_arg("FORMAT")
377 .with_default("text");
378 let formatted = format_flag(&flag);
379
380 assert!(formatted.contains("-f, --format FORMAT"));
381 assert!(formatted.contains("Output format"));
382 assert!(formatted.contains("(default: text)"));
383 }
384
385 #[test]
386 fn test_format_flag_long_only_no_arg() {
387 let flag = Flag::new("no-color", "Disable colors");
388 let formatted = format_flag(&flag);
389
390 assert!(formatted.contains(" --no-color"));
392 assert!(formatted.contains("Disable colors"));
393 }
394
395 #[test]
396 fn test_format_global_help_empty_domains() {
397 let domains: Vec<(String, String, Vec<String>)> = vec![];
398 let help = format_global_help(&domains);
399 assert!(help.contains("Commands:"));
400 assert!(help.contains("Global flags:"));
401 }
402
403 #[test]
404 fn test_format_command_help_no_flags() {
405 let routes = vec![Route {
406 verb: "start",
407 summary: "Start the server",
408 usage: "red serve start",
409 }];
410 let help = format_command_help("server", "grpc", &routes, &[]);
411
412 assert!(help.contains("Verbs:"));
413 assert!(help.contains("start"));
414 assert!(help.contains("Flags:"));
416 assert!(help.contains("--help"));
417 }
418}