flowcode_core/
parser.rs

1// Command parsing logic
2use crate::error::FCError; // Import FCError
3use crate::ast::ArgValue; // Import ArgValue
4use strsim::jaro_winkler;
5
6#[derive(Debug, PartialEq, Clone)] // Added Clone for easier testing if needed
7pub struct ParsedCommand {
8    pub name: String,
9    pub args: Vec<ArgValue>,
10}
11
12const KNOWN_COMMANDS: [&'static str; 4] = ["combine", "predict", "show", "help"]; // Later from SPECS
13const SIMILARITY_THRESHOLD: f64 = 0.85;
14
15fn suggest_command(input: &str) -> Option<&'static str> {
16    KNOWN_COMMANDS
17        .iter()
18        .map(|&cmd| (cmd, jaro_winkler(input, cmd)))
19        .filter(|(_, similarity)| *similarity >= SIMILARITY_THRESHOLD)
20        .max_by(|(_, sim1), (_, sim2)| sim1.partial_cmp(sim2).unwrap_or(std::cmp::Ordering::Equal))
21        .map(|(cmd, _)| cmd)
22}
23
24pub struct Parser;
25
26impl Parser {
27    pub fn new() -> Self {
28        Parser
29    }
30
31    // Updated to return Result<ParsedCommand, FCError> for better error reporting
32    pub fn parse(&self, input: &str) -> Result<ParsedCommand, FCError> {
33        let input = input.trim();
34        if input.is_empty() {
35            return Err(FCError::ParseError("empty input".to_string()));
36        }
37
38        let parts: Vec<&str> = input.split_whitespace().collect();
39        let command_name_str = parts[0];
40
41        // Check if the command is known
42        if !KNOWN_COMMANDS.contains(&command_name_str) {
43            if let Some(s) = suggest_command(command_name_str) {
44                return Err(FCError::Suggestion(command_name_str.to_string(), s.to_string()));
45            } else {
46                return Err(FCError::UnknownCommand(command_name_str.to_string()));
47            }
48        }
49
50        let command_name = command_name_str.to_string();
51        let command_args = parts[1..]
52            .iter()
53            .map(|s| classify_arg(s))
54            .collect();
55
56        Ok(ParsedCommand {
57            name: command_name,
58            args: command_args,
59        })
60    }
61}
62
63/// Returns a list of all valid command names understood by the parser.
64/// This is primarily intended for internal consistency checks (e.g., tests
65/// that ensure `spec::SPECS` and the parser stay in sync).
66pub fn all_command_names() -> Vec<&'static str> {
67    KNOWN_COMMANDS.to_vec()
68}
69
70// ------------------------- helpers -----------------------------
71
72fn classify_arg(raw: &str) -> ArgValue {
73    // Attempt boolean
74    if raw.eq_ignore_ascii_case("true") {
75        return ArgValue::Bool(true);
76    }
77    if raw.eq_ignore_ascii_case("false") {
78        return ArgValue::Bool(false);
79    }
80    // Attempt null
81    if raw.eq_ignore_ascii_case("null") {
82        return ArgValue::Null;
83    }
84    // Attempt number
85    if let Ok(n) = raw.parse::<f64>() {
86        return ArgValue::Number(n);
87    }
88    #[cfg(feature = "typed-args")]
89    if let Ok(d) = chrono::NaiveDate::parse_from_str(raw, "%Y-%m-%d") {
90        return ArgValue::Date(d);
91    }
92    // Default to String
93    ArgValue::String(raw.to_string())
94}