1use super::error::{suggest, ParseError};
7use super::token::Token;
8use super::types::{FlagSchema, FlagValue, ValueType};
9use std::collections::HashMap;
10
11pub struct SchemaResult {
13 pub flags: HashMap<String, FlagValue>,
14 pub positionals: Vec<String>,
15 pub errors: Vec<ParseError>,
16}
17
18pub struct SchemaParser {
20 flags: Vec<FlagSchema>,
21}
22
23impl SchemaParser {
24 pub fn new(flags: Vec<FlagSchema>) -> Self {
25 Self { flags }
26 }
27
28 pub fn parse(&self, tokens: &[Token]) -> SchemaResult {
34 let mut long_map: HashMap<&str, &FlagSchema> = HashMap::new();
36 let mut short_map: HashMap<char, &FlagSchema> = HashMap::new();
37
38 for schema in &self.flags {
39 long_map.insert(&schema.long, schema);
40 if let Some(ch) = schema.short {
41 short_map.insert(ch, schema);
42 }
43 }
44
45 let all_longs: Vec<&str> = self.flags.iter().map(|f| f.long.as_str()).collect();
47
48 let mut flags: HashMap<String, FlagValue> = HashMap::new();
50 for schema in &self.flags {
51 if let Some(ref default) = schema.default {
52 if let Ok(val) = coerce_value(default, schema.value_type) {
53 flags.insert(schema.long.clone(), val);
54 }
55 }
56 }
57
58 let mut positionals: Vec<String> = Vec::new();
59 let mut errors: Vec<ParseError> = Vec::new();
60 let mut i = 0;
61
62 while i < tokens.len() {
63 match &tokens[i] {
64 Token::LongFlag { name, value } => {
65 if let Some(base) = name.strip_prefix("no-") {
67 if let Some(schema) = long_map.get(base) {
68 if schema.value_type == ValueType::Bool {
69 flags.insert(base.to_string(), FlagValue::Bool(false));
70 i += 1;
71 continue;
72 }
73 }
74 }
75
76 match long_map.get(name.as_str()) {
77 None => {
78 let suggestions = suggest(name, &all_longs, 3)
79 .into_iter()
80 .map(|s| format!("--{}", s))
81 .collect();
82 errors.push(ParseError::UnknownFlag {
83 flag: format!("--{}", name),
84 suggestions,
85 });
86 }
87 Some(schema) => {
88 self.process_flag(
89 schema,
90 value,
91 tokens,
92 &mut i,
93 &mut flags,
94 &mut errors,
95 );
96 }
97 }
98 i += 1;
99 }
100
101 Token::ShortFlag { name, value } => {
102 match short_map.get(name) {
103 None => {
104 let all_shorts: Vec<&str> = self
106 .flags
107 .iter()
108 .filter_map(|f| f.short.as_ref())
109 .map(|_| "")
110 .collect();
111 let _ = all_shorts; errors.push(ParseError::UnknownFlag {
113 flag: format!("-{}", name),
114 suggestions: vec![],
115 });
116 }
117 Some(schema) => {
118 let str_value = value.as_ref().map(|v| v.to_string());
119 self.process_flag(
120 schema,
121 &str_value,
122 tokens,
123 &mut i,
124 &mut flags,
125 &mut errors,
126 );
127 }
128 }
129 i += 1;
130 }
131
132 Token::ShortCluster(chars) => {
133 let last_idx = chars.len() - 1;
134 for (ci, &ch) in chars.iter().enumerate() {
135 match short_map.get(&ch) {
136 None => {
137 errors.push(ParseError::UnknownFlag {
138 flag: format!("-{}", ch),
139 suggestions: vec![],
140 });
141 }
142 Some(schema) => {
143 if ci < last_idx {
144 if schema.expects_value {
146 errors.push(ParseError::InvalidValue {
147 flag: format!("-{}", ch),
148 value: String::new(),
149 expected_type: format_type(schema.value_type),
150 reason: "flag requires a value and cannot appear in a cluster".to_string(),
151 });
152 } else {
153 store_flag(&mut flags, schema, FlagValue::Bool(true));
154 }
155 } else {
156 if schema.expects_value {
158 if i + 1 < tokens.len() {
159 if let Token::Positional(ref val) = tokens[i + 1] {
160 match coerce_value(val, schema.value_type) {
161 Ok(fv) => {
162 store_flag(&mut flags, schema, fv);
163 }
164 Err(reason) => {
165 errors.push(ParseError::InvalidValue {
166 flag: format!("-{}", ch),
167 value: val.clone(),
168 expected_type: format_type(
169 schema.value_type,
170 ),
171 reason,
172 });
173 }
174 }
175 i += 1; } else {
177 errors.push(ParseError::MissingFlagValue {
178 flag: format!("-{}", ch),
179 expected_type: format_type(schema.value_type),
180 });
181 }
182 } else {
183 errors.push(ParseError::MissingFlagValue {
184 flag: format!("-{}", ch),
185 expected_type: format_type(schema.value_type),
186 });
187 }
188 } else {
189 store_flag(&mut flags, schema, FlagValue::Bool(true));
190 }
191 }
192 }
193 }
194 }
195 i += 1;
196 }
197
198 Token::Positional(s) => {
199 positionals.push(s.clone());
200 i += 1;
201 }
202
203 Token::EndOfOptions => {
204 i += 1;
206 while i < tokens.len() {
207 match &tokens[i] {
208 Token::Positional(s) => positionals.push(s.clone()),
209 Token::LongFlag { name, value } => {
212 let mut repr = format!("--{}", name);
213 if let Some(v) = value {
214 repr.push('=');
215 repr.push_str(v);
216 }
217 positionals.push(repr);
218 }
219 Token::ShortFlag { name, value } => {
220 let mut repr = format!("-{}", name);
221 if let Some(v) = value {
222 repr.push('=');
223 repr.push_str(v);
224 }
225 positionals.push(repr);
226 }
227 Token::ShortCluster(chars) => {
228 let s: String = chars.iter().collect();
229 positionals.push(format!("-{}", s));
230 }
231 Token::EndOfOptions => {
232 positionals.push("--".to_string());
233 }
234 }
235 i += 1;
236 }
237 }
238 }
239 }
240
241 for schema in &self.flags {
243 if schema.required && !flags.contains_key(&schema.long) {
244 errors.push(ParseError::MissingRequired {
245 flag: format!("--{}", schema.long),
246 });
247 }
248 }
249
250 for schema in &self.flags {
252 if let Some(ref choices) = schema.choices {
253 if let Some(val) = flags.get(&schema.long) {
254 let s = val.as_str_value();
255 if !choices.contains(&s) {
256 errors.push(ParseError::InvalidChoice {
257 flag: format!("--{}", schema.long),
258 value: s,
259 allowed: choices.clone(),
260 });
261 }
262 }
263 }
264 }
265
266 SchemaResult {
267 flags,
268 positionals,
269 errors,
270 }
271 }
272
273 fn process_flag(
276 &self,
277 schema: &FlagSchema,
278 value: &Option<String>,
279 tokens: &[Token],
280 i: &mut usize,
281 flags: &mut HashMap<String, FlagValue>,
282 errors: &mut Vec<ParseError>,
283 ) {
284 let flag_display = if let Some(ch) = schema.short {
285 if schema.long.is_empty() {
286 format!("-{}", ch)
287 } else {
288 format!("--{}", schema.long)
289 }
290 } else {
291 format!("--{}", schema.long)
292 };
293
294 if schema.expects_value {
295 match value {
296 Some(raw) => match coerce_value(raw, schema.value_type) {
297 Ok(fv) => {
298 store_flag(flags, schema, fv);
299 }
300 Err(reason) => {
301 errors.push(ParseError::InvalidValue {
302 flag: flag_display,
303 value: raw.clone(),
304 expected_type: format_type(schema.value_type),
305 reason,
306 });
307 }
308 },
309 None => {
310 if *i + 1 < tokens.len() {
312 if let Token::Positional(ref val) = tokens[*i + 1] {
313 match coerce_value(val, schema.value_type) {
314 Ok(fv) => {
315 store_flag(flags, schema, fv);
316 }
317 Err(reason) => {
318 errors.push(ParseError::InvalidValue {
319 flag: flag_display,
320 value: val.clone(),
321 expected_type: format_type(schema.value_type),
322 reason,
323 });
324 }
325 }
326 *i += 1; return;
328 }
329 }
330 errors.push(ParseError::MissingFlagValue {
331 flag: flag_display,
332 expected_type: format_type(schema.value_type),
333 });
334 }
335 }
336 } else {
337 if let Some(inline_value) = value {
339 errors.push(ParseError::InvalidValue {
340 flag: flag_display,
341 value: inline_value.clone(),
342 expected_type: "bool".to_string(),
343 reason: "boolean flags do not accept a value".to_string(),
344 });
345 } else {
346 store_flag(flags, schema, FlagValue::Bool(true));
347 }
348 }
349 }
350}
351
352fn store_flag(flags: &mut HashMap<String, FlagValue>, schema: &FlagSchema, value: FlagValue) {
354 if schema.value_type == ValueType::Count {
355 let current = flags.get(&schema.long).and_then(|v| {
356 if let FlagValue::Count(n) = v {
357 Some(*n)
358 } else {
359 None
360 }
361 });
362 flags.insert(
363 schema.long.clone(),
364 FlagValue::Count(current.unwrap_or(0) + 1),
365 );
366 } else {
367 flags.insert(schema.long.clone(), value);
369 }
370}
371
372fn coerce_value(raw: &str, value_type: ValueType) -> Result<FlagValue, String> {
374 match value_type {
375 ValueType::String => Ok(FlagValue::Str(raw.to_string())),
376 ValueType::Bool => match raw.to_lowercase().as_str() {
377 "true" | "yes" | "1" | "on" => Ok(FlagValue::Bool(true)),
378 "false" | "no" | "0" | "off" => Ok(FlagValue::Bool(false)),
379 _ => Err(format!("expected boolean, got '{}'", raw)),
380 },
381 ValueType::Integer => raw
382 .parse::<i64>()
383 .map(FlagValue::Int)
384 .map_err(|_| format!("expected integer, got '{}'", raw)),
385 ValueType::Float => raw
386 .parse::<f64>()
387 .map(FlagValue::Float)
388 .map_err(|_| format!("expected number, got '{}'", raw)),
389 ValueType::Count => Ok(FlagValue::Count(1)),
390 }
391}
392
393fn format_type(vt: ValueType) -> String {
395 match vt {
396 ValueType::String => "string".to_string(),
397 ValueType::Bool => "bool".to_string(),
398 ValueType::Integer => "integer".to_string(),
399 ValueType::Float => "float".to_string(),
400 ValueType::Count => "count".to_string(),
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::cli::token::tokenize;
408
409 fn args(v: &[&str]) -> Vec<String> {
410 v.iter().map(|a| a.to_string()).collect()
411 }
412
413 fn make_parser(schemas: Vec<FlagSchema>) -> SchemaParser {
414 SchemaParser::new(schemas)
415 }
416
417 #[test]
418 fn test_parse_long_flag_bool() {
419 let parser = make_parser(vec![FlagSchema::boolean("verbose").with_short('v')]);
420 let tokens = tokenize(&args(&["--verbose"]));
421 let result = parser.parse(&tokens);
422
423 assert!(result.errors.is_empty());
424 assert_eq!(result.flags.get("verbose"), Some(&FlagValue::Bool(true)));
425 }
426
427 #[test]
428 fn test_parse_long_flag_with_value() {
429 let parser = make_parser(vec![FlagSchema::new("output").with_short('o')]);
430 let tokens = tokenize(&args(&["--output", "json"]));
431 let result = parser.parse(&tokens);
432
433 assert!(result.errors.is_empty());
434 assert_eq!(
435 result.flags.get("output"),
436 Some(&FlagValue::Str("json".to_string()))
437 );
438 assert!(result.positionals.is_empty());
439 }
440
441 #[test]
442 fn test_parse_long_flag_equals() {
443 let parser = make_parser(vec![FlagSchema::new("output")]);
444 let tokens = tokenize(&args(&["--output=json"]));
445 let result = parser.parse(&tokens);
446
447 assert!(result.errors.is_empty());
448 assert_eq!(
449 result.flags.get("output"),
450 Some(&FlagValue::Str("json".to_string()))
451 );
452 }
453
454 #[test]
455 fn test_parse_short_flag() {
456 let parser = make_parser(vec![FlagSchema::boolean("verbose").with_short('v')]);
457 let tokens = tokenize(&args(&["-v"]));
458 let result = parser.parse(&tokens);
459
460 assert!(result.errors.is_empty());
461 assert_eq!(result.flags.get("verbose"), Some(&FlagValue::Bool(true)));
462 }
463
464 #[test]
465 fn test_parse_short_cluster() {
466 let parser = make_parser(vec![
467 FlagSchema::boolean("verbose").with_short('v'),
468 FlagSchema::boolean("all").with_short('a'),
469 FlagSchema::boolean("force").with_short('f'),
470 ]);
471 let tokens = tokenize(&args(&["-vaf"]));
472 let result = parser.parse(&tokens);
473
474 assert!(result.errors.is_empty());
475 assert_eq!(result.flags.get("verbose"), Some(&FlagValue::Bool(true)));
476 assert_eq!(result.flags.get("all"), Some(&FlagValue::Bool(true)));
477 assert_eq!(result.flags.get("force"), Some(&FlagValue::Bool(true)));
478 }
479
480 #[test]
481 fn test_parse_short_cluster_last_takes_value() {
482 let parser = make_parser(vec![
483 FlagSchema::boolean("verbose").with_short('v'),
484 FlagSchema::new("output").with_short('o'),
485 ]);
486 let tokens = tokenize(&args(&["-vo", "json"]));
487 let result = parser.parse(&tokens);
488
489 assert!(result.errors.is_empty());
490 assert_eq!(result.flags.get("verbose"), Some(&FlagValue::Bool(true)));
491 assert_eq!(
492 result.flags.get("output"),
493 Some(&FlagValue::Str("json".to_string()))
494 );
495 assert!(result.positionals.is_empty());
496 }
497
498 #[test]
499 fn test_parse_negation() {
500 let parser = make_parser(vec![FlagSchema::boolean("verbose").with_short('v')]);
501 let tokens = tokenize(&args(&["--no-verbose"]));
502 let result = parser.parse(&tokens);
503
504 assert!(result.errors.is_empty());
505 assert_eq!(result.flags.get("verbose"), Some(&FlagValue::Bool(false)));
506 }
507
508 #[test]
509 fn test_parse_unknown_flag_suggests() {
510 let parser = make_parser(vec![
511 FlagSchema::boolean("verbose"),
512 FlagSchema::new("output"),
513 ]);
514 let tokens = tokenize(&args(&["--verbos"]));
515 let result = parser.parse(&tokens);
516
517 assert_eq!(result.errors.len(), 1);
518 if let ParseError::UnknownFlag {
519 ref flag,
520 ref suggestions,
521 } = result.errors[0]
522 {
523 assert_eq!(flag, "--verbos");
524 assert!(suggestions.contains(&"--verbose".to_string()));
525 } else {
526 panic!("expected UnknownFlag error");
527 }
528 }
529
530 #[test]
531 fn test_parse_missing_value() {
532 let parser = make_parser(vec![FlagSchema::new("output")]);
533 let tokens = tokenize(&args(&["--output"]));
534 let result = parser.parse(&tokens);
535
536 assert_eq!(result.errors.len(), 1);
537 assert!(matches!(
538 &result.errors[0],
539 ParseError::MissingFlagValue { flag, .. } if flag == "--output"
540 ));
541 }
542
543 #[test]
544 fn test_parse_invalid_type() {
545 let parser = make_parser(vec![{
546 let mut s = FlagSchema::new("threads");
547 s.value_type = ValueType::Integer;
548 s
549 }]);
550 let tokens = tokenize(&args(&["--threads=abc"]));
551 let result = parser.parse(&tokens);
552
553 assert_eq!(result.errors.len(), 1);
554 assert!(matches!(
555 &result.errors[0],
556 ParseError::InvalidValue { flag, value, .. } if flag == "--threads" && value == "abc"
557 ));
558 }
559
560 #[test]
561 fn test_parse_invalid_choice() {
562 let parser = make_parser(vec![
563 FlagSchema::new("output").with_choices(&["text", "json", "yaml"])
564 ]);
565 let tokens = tokenize(&args(&["--output=xml"]));
566 let result = parser.parse(&tokens);
567
568 assert_eq!(result.errors.len(), 1);
569 assert!(matches!(
570 &result.errors[0],
571 ParseError::InvalidChoice { flag, value, .. } if flag == "--output" && value == "xml"
572 ));
573 }
574
575 #[test]
576 fn test_parse_required_missing() {
577 let parser = make_parser(vec![FlagSchema::new("target").required()]);
578 let tokens = tokenize(&args(&[]));
579 let result = parser.parse(&tokens);
580
581 assert_eq!(result.errors.len(), 1);
582 assert!(matches!(
583 &result.errors[0],
584 ParseError::MissingRequired { flag } if flag == "--target"
585 ));
586 }
587
588 #[test]
589 fn test_parse_defaults_applied() {
590 let parser = make_parser(vec![
591 FlagSchema::new("output").with_default("text"),
592 FlagSchema::boolean("verbose"),
593 ]);
594 let tokens = tokenize(&args(&[]));
595 let result = parser.parse(&tokens);
596
597 assert!(result.errors.is_empty());
598 assert_eq!(
599 result.flags.get("output"),
600 Some(&FlagValue::Str("text".to_string()))
601 );
602 assert!(!result.flags.contains_key("verbose"));
604 }
605
606 #[test]
607 fn test_parse_end_of_options() {
608 let parser = make_parser(vec![FlagSchema::boolean("verbose").with_short('v')]);
609 let tokens = tokenize(&args(&["-v", "--", "--not-a-flag", "target"]));
610 let result = parser.parse(&tokens);
611
612 assert!(result.errors.is_empty());
613 assert_eq!(result.flags.get("verbose"), Some(&FlagValue::Bool(true)));
614 assert_eq!(result.positionals, vec!["--not-a-flag", "target"]);
615 }
616
617 #[test]
618 fn test_parse_positionals_preserved() {
619 let parser = make_parser(vec![FlagSchema::boolean("verbose").with_short('v')]);
620 let tokens = tokenize(&args(&["server", "192.168.1.1", "-v", "extra"]));
621 let result = parser.parse(&tokens);
622
623 assert!(result.errors.is_empty());
624 assert_eq!(result.flags.get("verbose"), Some(&FlagValue::Bool(true)));
625 assert_eq!(result.positionals, vec!["server", "192.168.1.1", "extra"]);
626 }
627
628 #[test]
629 fn test_parse_mixed_realistic() {
630 let parser = make_parser(vec![
632 FlagSchema::new("path").with_short('p'),
633 FlagSchema::new("bind").with_short('b'),
634 FlagSchema::new("role").with_short('r'),
635 FlagSchema::boolean("verbose").with_short('v'),
636 FlagSchema::new("output")
637 .with_short('o')
638 .with_choices(&["text", "json", "yaml"]),
639 FlagSchema::boolean("no-color"),
640 ]);
641 let tokens = tokenize(&args(&[
642 "server",
643 "--path",
644 "/data",
645 "--bind",
646 "0.0.0.0:6380",
647 "--role",
648 "primary",
649 "-vo",
650 "json",
651 "--no-color",
652 ]));
653 let result = parser.parse(&tokens);
654
655 assert!(result.errors.is_empty(), "errors: {:?}", result.errors);
656 assert_eq!(result.positionals, vec!["server"]);
657 assert_eq!(
658 result.flags.get("path"),
659 Some(&FlagValue::Str("/data".to_string()))
660 );
661 assert_eq!(
662 result.flags.get("bind"),
663 Some(&FlagValue::Str("0.0.0.0:6380".to_string()))
664 );
665 assert_eq!(
666 result.flags.get("role"),
667 Some(&FlagValue::Str("primary".to_string()))
668 );
669 assert_eq!(result.flags.get("verbose"), Some(&FlagValue::Bool(true)));
670 assert_eq!(
671 result.flags.get("output"),
672 Some(&FlagValue::Str("json".to_string()))
673 );
674 assert_eq!(result.flags.get("no-color"), Some(&FlagValue::Bool(true)));
675 }
676}