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
13#[cfg(feature = "duckdb")]
14const DUCKDB_COMMANDS: [&'static str; 7] = ["select", "filter", "group", "join", "sort", "load", "sql"];
15const SIMILARITY_THRESHOLD: f64 = 0.85;
16
17fn suggest_command(input: &str) -> Option<&'static str> {
18    let mut commands = KNOWN_COMMANDS.to_vec();
19    #[cfg(feature = "duckdb")]
20    commands.extend(DUCKDB_COMMANDS.iter());
21    
22    commands
23        .iter()
24        .map(|&cmd| (cmd, jaro_winkler(input, cmd)))
25        .filter(|(_, similarity)| *similarity >= SIMILARITY_THRESHOLD)
26        .max_by(|(_, sim1), (_, sim2)| sim1.partial_cmp(sim2).unwrap_or(std::cmp::Ordering::Equal))
27        .map(|(cmd, _)| cmd)
28}
29
30pub struct Parser;
31
32impl Parser {
33    pub fn new() -> Self {
34        Parser
35    }
36
37    // Updated to return Result<ParsedCommand, FCError> for better error reporting
38    pub fn parse(&self, input: &str) -> Result<ParsedCommand, FCError> {
39        let input = input.trim();
40        if input.is_empty() {
41            return Err(FCError::ParseError("empty input".to_string()));
42        }
43
44        let parts: Vec<&str> = input.split_whitespace().collect();
45        let command_name_str = parts[0];
46
47        // Check if the command is known
48        let mut valid_commands = KNOWN_COMMANDS.to_vec();
49        #[cfg(feature = "duckdb")]
50        valid_commands.extend(DUCKDB_COMMANDS.iter());
51        
52        if !valid_commands.contains(&command_name_str) {
53            if let Some(s) = suggest_command(command_name_str) {
54                return Err(FCError::Suggestion(command_name_str.to_string(), s.to_string()));
55            } else {
56                return Err(FCError::UnknownCommand(command_name_str.to_string()));
57            }
58        }
59
60        let command_name = command_name_str.to_string();
61        let command_args = parts[1..]
62            .iter()
63            .map(|s| classify_arg(s))
64            .collect();
65
66        Ok(ParsedCommand {
67            name: command_name,
68            args: command_args,
69        })
70    }
71}
72
73/// Returns a list of all valid command names understood by the parser.
74/// This is primarily intended for internal consistency checks (e.g., tests
75/// that ensure `spec::SPECS` and the parser stay in sync).
76pub fn all_command_names() -> Vec<&'static str> {
77    let mut commands = KNOWN_COMMANDS.to_vec();
78    #[cfg(feature = "duckdb")]
79    commands.extend(DUCKDB_COMMANDS.iter());
80    commands
81}
82
83// ------------------------- helpers -----------------------------
84
85fn classify_arg(raw: &str) -> ArgValue {
86    // Attempt boolean
87    if raw.eq_ignore_ascii_case("true") {
88        return ArgValue::Bool(true);
89    }
90    if raw.eq_ignore_ascii_case("false") {
91        return ArgValue::Bool(false);
92    }
93    // Attempt null
94    if raw.eq_ignore_ascii_case("null") {
95        return ArgValue::Null;
96    }
97    // Attempt number
98    if let Ok(n) = raw.parse::<f64>() {
99        return ArgValue::Number(n);
100    }
101    #[cfg(feature = "typed-args")]
102    if let Ok(d) = chrono::NaiveDate::parse_from_str(raw, "%Y-%m-%d") {
103        return ArgValue::Date(d);
104    }
105    // Default to String
106    ArgValue::String(raw.to_string())
107}