1use crate::error::FCError; use crate::ast::ArgValue; use strsim::jaro_winkler;
5
6#[derive(Debug, PartialEq, Clone)] pub struct ParsedCommand {
8 pub name: String,
9 pub args: Vec<ArgValue>,
10}
11
12const KNOWN_COMMANDS: [&'static str; 4] = ["combine", "predict", "show", "help"]; #[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 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 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
73pub 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
83fn classify_arg(raw: &str) -> ArgValue {
86 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 if raw.eq_ignore_ascii_case("null") {
95 return ArgValue::Null;
96 }
97 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 ArgValue::String(raw.to_string())
107}