dynamic_cli/parser/type_parser.rs
1//! Type parsing functions
2//!
3//! This module provides functions to parse string values into typed values
4//! according to the [`ArgumentType`] specification. Each type has dedicated
5//! parsing logic with detailed error messages.
6//!
7//! # Supported Types
8//!
9//! - **String**: Pass-through (no conversion)
10//! - **Integer**: Signed 64-bit integers (i64)
11//! - **Float**: 64-bit floating point (f64)
12//! - **Bool**: true/false, yes/no, 1/0, on/off (case-insensitive)
13//! - **Path**: File system paths (validated as PathBuf)
14//!
15//! # Example
16//!
17//! ```
18//! use dynamic_cli::parser::type_parser::{parse_value, parse_integer};
19//! use dynamic_cli::config::schema::ArgumentType;
20//!
21//! // Parse a value according to its type
22//! let result = parse_value("42", ArgumentType::Integer).unwrap();
23//! assert_eq!(result, "42");
24//!
25//! // Parse directly to specific type
26//! let number = parse_integer("42").unwrap();
27//! assert_eq!(number, 42);
28//! ```
29
30use crate::config::schema::ArgumentType;
31use crate::error::{ParseError, Result};
32use std::path::PathBuf;
33
34/// Parse a string value according to its expected type
35///
36/// This is the main entry point for type parsing. It dispatches to
37/// type-specific parsers and validates the conversion.
38///
39/// # Arguments
40///
41/// * `value` - The string value to parse
42/// * `arg_type` - The expected type for this value
43///
44/// # Returns
45///
46/// Returns the original string value if parsing succeeds. The actual
47/// conversion to the target type is done to validate the input, but
48/// we return the string to maintain flexibility in the HashMap storage.
49///
50/// # Errors
51///
52/// Returns [`ParseError::TypeParseError`] if the value cannot be
53/// converted to the expected type.
54///
55/// # Example
56///
57/// ```
58/// use dynamic_cli::parser::type_parser::parse_value;
59/// use dynamic_cli::config::schema::ArgumentType;
60///
61/// // Valid integer
62/// assert!(parse_value("123", ArgumentType::Integer).is_ok());
63///
64/// // Invalid integer
65/// assert!(parse_value("abc", ArgumentType::Integer).is_err());
66///
67/// // Valid boolean
68/// assert!(parse_value("yes", ArgumentType::Bool).is_ok());
69///
70/// // Valid path
71/// assert!(parse_value("/tmp/file.txt", ArgumentType::Path).is_ok());
72/// ```
73pub fn parse_value(value: &str, arg_type: ArgumentType) -> Result<String> {
74 // Validate by attempting conversion to target type
75 match arg_type {
76 ArgumentType::String => {
77 // String is always valid
78 Ok(value.to_string())
79 }
80
81 ArgumentType::Integer => {
82 // Try to parse as i64
83 parse_integer(value)?;
84 Ok(value.to_string())
85 }
86
87 ArgumentType::Float => {
88 // Try to parse as f64
89 parse_float(value)?;
90 Ok(value.to_string())
91 }
92
93 ArgumentType::Bool => {
94 // Try to parse as boolean
95 parse_bool(value)?;
96 Ok(value.to_string())
97 }
98
99 ArgumentType::Path => {
100 // Try to parse as PathBuf
101 parse_path(value)?;
102 Ok(value.to_string())
103 }
104 }
105}
106
107/// Parse a string as a signed integer
108///
109/// Accepts standard integer formats including:
110/// - Positive numbers: "42"
111/// - Negative numbers: "-42"
112/// - With underscores: "1_000_000"
113///
114/// # Arguments
115///
116/// * `value` - The string to parse
117///
118/// # Returns
119///
120/// The parsed integer value as i64
121///
122/// # Errors
123///
124/// Returns [`ParseError::TypeParseError`] if the string cannot be
125/// parsed as a valid integer or if it overflows i64.
126///
127/// # Example
128///
129/// ```
130/// use dynamic_cli::parser::type_parser::parse_integer;
131///
132/// assert_eq!(parse_integer("42").unwrap(), 42);
133/// assert_eq!(parse_integer("-123").unwrap(), -123);
134/// assert_eq!(parse_integer("1000").unwrap(), 1000);
135///
136/// // These will fail
137/// assert!(parse_integer("abc").is_err());
138/// assert!(parse_integer("12.5").is_err());
139/// ```
140pub fn parse_integer(value: &str) -> Result<i64> {
141 value.parse::<i64>().map_err(|e| {
142 ParseError::TypeParseError {
143 arg_name: "value".to_string(),
144 expected_type: "integer".to_string(),
145 value: value.to_string(),
146 details: Some(e.to_string()),
147 }
148 .into()
149 })
150}
151
152/// Parse a string as a floating-point number
153///
154/// Accepts standard float formats including:
155/// - Integers: "42" → 42.0
156/// - Decimals: "3.14"
157/// - Scientific notation: "1e-10", "2.5E+3"
158/// - Special values: "inf", "-inf", "NaN"
159///
160/// # Arguments
161///
162/// * `value` - The string to parse
163///
164/// # Returns
165///
166/// The parsed floating-point value as f64
167///
168/// # Errors
169///
170/// Returns [`ParseError::TypeParseError`] if the string cannot be
171/// parsed as a valid float.
172///
173/// # Example
174///
175/// ```
176/// use dynamic_cli::parser::type_parser::parse_float;
177///
178/// assert_eq!(parse_float("3.14").unwrap(), 3.14);
179/// assert_eq!(parse_float("42").unwrap(), 42.0);
180/// assert_eq!(parse_float("-1.5").unwrap(), -1.5);
181/// assert_eq!(parse_float("1e-3").unwrap(), 0.001);
182///
183/// // These will fail
184/// assert!(parse_float("abc").is_err());
185/// assert!(parse_float("12.34.56").is_err());
186/// ```
187pub fn parse_float(value: &str) -> Result<f64> {
188 value.parse::<f64>().map_err(|e| {
189 ParseError::TypeParseError {
190 arg_name: "value".to_string(),
191 expected_type: "float".to_string(),
192 value: value.to_string(),
193 details: Some(e.to_string()),
194 }
195 .into()
196 })
197}
198
199/// Parse a string as a boolean
200///
201/// Accepts multiple representations (case-insensitive):
202/// - **true**: "true", "yes", "y", "1", "on"
203/// - **false**: "false", "no", "n", "0", "off"
204///
205/// This flexibility allows users to use natural language or
206/// common programming conventions.
207///
208/// # Arguments
209///
210/// * `value` - The string to parse
211///
212/// # Returns
213///
214/// The parsed boolean value
215///
216/// # Errors
217///
218/// Returns [`ParseError::TypeParseError`] if the string is not
219/// a recognized boolean value.
220///
221/// # Example
222///
223/// ```
224/// use dynamic_cli::parser::type_parser::parse_bool;
225///
226/// // True values
227/// assert_eq!(parse_bool("true").unwrap(), true);
228/// assert_eq!(parse_bool("YES").unwrap(), true);
229/// assert_eq!(parse_bool("1").unwrap(), true);
230/// assert_eq!(parse_bool("on").unwrap(), true);
231///
232/// // False values
233/// assert_eq!(parse_bool("false").unwrap(), false);
234/// assert_eq!(parse_bool("NO").unwrap(), false);
235/// assert_eq!(parse_bool("0").unwrap(), false);
236/// assert_eq!(parse_bool("off").unwrap(), false);
237///
238/// // Invalid values
239/// assert!(parse_bool("maybe").is_err());
240/// assert!(parse_bool("2").is_err());
241/// ```
242pub fn parse_bool(value: &str) -> Result<bool> {
243 let normalized = value.trim().to_lowercase();
244
245 match normalized.as_str() {
246 // True values
247 "true" | "yes" | "y" | "1" | "on" => Ok(true),
248
249 // False values
250 "false" | "no" | "n" | "0" | "off" => Ok(false),
251
252 // Invalid value
253 _ => Err(ParseError::TypeParseError {
254 arg_name: "value".to_string(),
255 expected_type: "bool".to_string(),
256 value: value.to_string(),
257 details: Some("expected true/false, yes/no, 1/0, on/off".to_string()),
258 }
259 .into()),
260 }
261}
262
263/// Parse a string as a file system path
264///
265/// Validates that the string can be converted to a valid PathBuf.
266/// Note: This does NOT check if the path exists on the file system -
267/// that validation is done separately in the validator module.
268///
269/// # Arguments
270///
271/// * `value` - The string to parse as a path
272///
273/// # Returns
274///
275/// The parsed PathBuf
276///
277/// # Errors
278///
279/// Returns [`ParseError::TypeParseError`] if the string contains
280/// invalid path characters (very rare on Unix, more common on Windows).
281///
282/// # Example
283///
284/// ```
285/// use dynamic_cli::parser::type_parser::parse_path;
286///
287/// // Valid paths
288/// assert!(parse_path("/tmp/file.txt").is_ok());
289/// assert!(parse_path("./relative/path").is_ok());
290/// assert!(parse_path("C:\\Windows\\System32").is_ok());
291///
292/// // Empty path is technically valid (current directory)
293/// assert!(parse_path("").is_ok());
294/// ```
295pub fn parse_path(value: &str) -> Result<PathBuf> {
296 // PathBuf::from() is very permissive - it accepts almost any string
297 // More strict validation (existence, permissions) is done by the validator module
298 let path = PathBuf::from(value);
299
300 // We could add more validation here if needed, but for now
301 // we trust that PathBuf::from() will handle platform-specific rules
302 Ok(path)
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 // ========================================================================
310 // parse_value tests
311 // ========================================================================
312
313 #[test]
314 fn test_parse_value_string() {
315 let result = parse_value("hello world", ArgumentType::String).unwrap();
316 assert_eq!(result, "hello world");
317
318 // Empty string is valid
319 let result = parse_value("", ArgumentType::String).unwrap();
320 assert_eq!(result, "");
321 }
322
323 #[test]
324 fn test_parse_value_integer_valid() {
325 assert!(parse_value("42", ArgumentType::Integer).is_ok());
326 assert!(parse_value("-123", ArgumentType::Integer).is_ok());
327 assert!(parse_value("0", ArgumentType::Integer).is_ok());
328 }
329
330 #[test]
331 fn test_parse_value_integer_invalid() {
332 assert!(parse_value("abc", ArgumentType::Integer).is_err());
333 assert!(parse_value("12.5", ArgumentType::Integer).is_err());
334 assert!(parse_value("", ArgumentType::Integer).is_err());
335 }
336
337 #[test]
338 fn test_parse_value_float_valid() {
339 assert!(parse_value("3.14", ArgumentType::Float).is_ok());
340 assert!(parse_value("42", ArgumentType::Float).is_ok());
341 assert!(parse_value("-1.5", ArgumentType::Float).is_ok());
342 }
343
344 #[test]
345 fn test_parse_value_float_invalid() {
346 assert!(parse_value("abc", ArgumentType::Float).is_err());
347 assert!(parse_value("", ArgumentType::Float).is_err());
348 }
349
350 #[test]
351 fn test_parse_value_bool_valid() {
352 assert!(parse_value("true", ArgumentType::Bool).is_ok());
353 assert!(parse_value("false", ArgumentType::Bool).is_ok());
354 assert!(parse_value("yes", ArgumentType::Bool).is_ok());
355 assert!(parse_value("no", ArgumentType::Bool).is_ok());
356 }
357
358 #[test]
359 fn test_parse_value_bool_invalid() {
360 assert!(parse_value("maybe", ArgumentType::Bool).is_err());
361 assert!(parse_value("2", ArgumentType::Bool).is_err());
362 }
363
364 #[test]
365 fn test_parse_value_path_valid() {
366 assert!(parse_value("/tmp/file", ArgumentType::Path).is_ok());
367 assert!(parse_value("./relative", ArgumentType::Path).is_ok());
368 }
369
370 // ========================================================================
371 // parse_integer tests
372 // ========================================================================
373
374 #[test]
375 fn test_parse_integer_positive() {
376 assert_eq!(parse_integer("42").unwrap(), 42);
377 assert_eq!(parse_integer("0").unwrap(), 0);
378 assert_eq!(parse_integer("999999").unwrap(), 999999);
379 }
380
381 #[test]
382 fn test_parse_integer_negative() {
383 assert_eq!(parse_integer("-42").unwrap(), -42);
384 assert_eq!(parse_integer("-1").unwrap(), -1);
385 }
386
387 // Note: Rust's parse() does not support underscores in string literals
388 // Underscores are only a syntax feature when writing Rust code
389 // If we want to support this, we'd need to strip underscores first
390
391 #[test]
392 fn test_parse_integer_invalid() {
393 assert!(parse_integer("abc").is_err());
394 assert!(parse_integer("12.5").is_err());
395 assert!(parse_integer("").is_err());
396 assert!(parse_integer("12a").is_err());
397 }
398
399 #[test]
400 fn test_parse_integer_overflow() {
401 // i64::MAX + 1 should fail
402 let too_large = "9223372036854775808";
403 assert!(parse_integer(too_large).is_err());
404 }
405
406 // ========================================================================
407 // parse_float tests
408 // ========================================================================
409
410 #[test]
411 fn test_parse_float_integer() {
412 assert_eq!(parse_float("42").unwrap(), 42.0);
413 assert_eq!(parse_float("0").unwrap(), 0.0);
414 }
415
416 #[test]
417 fn test_parse_float_decimal() {
418 assert_eq!(parse_float("3.14").unwrap(), 3.14);
419 assert_eq!(parse_float("-1.5").unwrap(), -1.5);
420 assert_eq!(parse_float("0.5").unwrap(), 0.5);
421 }
422
423 #[test]
424 fn test_parse_float_scientific() {
425 assert_eq!(parse_float("1e3").unwrap(), 1000.0);
426 assert_eq!(parse_float("1.5e2").unwrap(), 150.0);
427 assert_eq!(parse_float("1e-3").unwrap(), 0.001);
428 }
429
430 #[test]
431 fn test_parse_float_special_values() {
432 assert!(parse_float("inf").unwrap().is_infinite());
433 assert!(parse_float("-inf").unwrap().is_infinite());
434 assert!(parse_float("NaN").unwrap().is_nan());
435 }
436
437 #[test]
438 fn test_parse_float_invalid() {
439 assert!(parse_float("abc").is_err());
440 assert!(parse_float("").is_err());
441 assert!(parse_float("12.34.56").is_err());
442 }
443
444 // ========================================================================
445 // parse_bool tests
446 // ========================================================================
447
448 #[test]
449 fn test_parse_bool_true_variants() {
450 // All true variants
451 assert_eq!(parse_bool("true").unwrap(), true);
452 assert_eq!(parse_bool("True").unwrap(), true);
453 assert_eq!(parse_bool("TRUE").unwrap(), true);
454 assert_eq!(parse_bool("yes").unwrap(), true);
455 assert_eq!(parse_bool("YES").unwrap(), true);
456 assert_eq!(parse_bool("y").unwrap(), true);
457 assert_eq!(parse_bool("Y").unwrap(), true);
458 assert_eq!(parse_bool("1").unwrap(), true);
459 assert_eq!(parse_bool("on").unwrap(), true);
460 assert_eq!(parse_bool("ON").unwrap(), true);
461 }
462
463 #[test]
464 fn test_parse_bool_false_variants() {
465 // All false variants
466 assert_eq!(parse_bool("false").unwrap(), false);
467 assert_eq!(parse_bool("False").unwrap(), false);
468 assert_eq!(parse_bool("FALSE").unwrap(), false);
469 assert_eq!(parse_bool("no").unwrap(), false);
470 assert_eq!(parse_bool("NO").unwrap(), false);
471 assert_eq!(parse_bool("n").unwrap(), false);
472 assert_eq!(parse_bool("N").unwrap(), false);
473 assert_eq!(parse_bool("0").unwrap(), false);
474 assert_eq!(parse_bool("off").unwrap(), false);
475 assert_eq!(parse_bool("OFF").unwrap(), false);
476 }
477
478 #[test]
479 fn test_parse_bool_with_whitespace() {
480 assert_eq!(parse_bool(" true ").unwrap(), true);
481 assert_eq!(parse_bool("\tfalse\n").unwrap(), false);
482 }
483
484 #[test]
485 fn test_parse_bool_invalid() {
486 assert!(parse_bool("maybe").is_err());
487 assert!(parse_bool("2").is_err());
488 assert!(parse_bool("").is_err());
489 assert!(parse_bool("tr").is_err());
490 }
491
492 // ========================================================================
493 // parse_path tests
494 // ========================================================================
495
496 #[test]
497 fn test_parse_path_unix_style() {
498 let path = parse_path("/tmp/file.txt").unwrap();
499 assert_eq!(path.to_str().unwrap(), "/tmp/file.txt");
500 }
501
502 #[test]
503 fn test_parse_path_relative() {
504 let path = parse_path("./relative/path").unwrap();
505 assert!(path.to_str().unwrap().contains("relative"));
506 }
507
508 #[test]
509 fn test_parse_path_windows_style() {
510 // This should work on all platforms (PathBuf is permissive)
511 let path = parse_path("C:\\Windows\\System32").unwrap();
512 assert!(path.to_str().is_some());
513 }
514
515 #[test]
516 fn test_parse_path_empty() {
517 // Empty path is technically valid (current directory)
518 let path = parse_path("").unwrap();
519 assert_eq!(path, PathBuf::from(""));
520 }
521
522 #[test]
523 fn test_parse_path_with_spaces() {
524 let path = parse_path("/path/with spaces/file.txt").unwrap();
525 assert!(path.to_str().unwrap().contains("spaces"));
526 }
527
528 // ========================================================================
529 // Integration tests
530 // ========================================================================
531
532 #[test]
533 fn test_all_types_roundtrip() {
534 // Test that parse_value works for all types
535 let test_cases = vec![
536 ("hello", ArgumentType::String),
537 ("42", ArgumentType::Integer),
538 ("3.14", ArgumentType::Float),
539 ("true", ArgumentType::Bool),
540 ("/tmp/file", ArgumentType::Path),
541 ];
542
543 for (value, arg_type) in test_cases {
544 let result = parse_value(value, arg_type);
545 assert!(
546 result.is_ok(),
547 "Failed to parse '{}' as {:?}",
548 value,
549 arg_type
550 );
551 assert_eq!(result.unwrap(), value);
552 }
553 }
554
555 #[test]
556 fn test_error_messages_contain_details() {
557 // Verify that error messages are helpful
558 let result = parse_integer("not_a_number");
559 assert!(result.is_err());
560
561 let error = result.unwrap_err();
562 let error_msg = format!("{}", error);
563 assert!(error_msg.contains("integer"));
564 assert!(error_msg.contains("not_a_number"));
565 }
566}