use crate::error::FCError; use crate::ast::ArgValue; use strsim::jaro_winkler;
#[derive(Debug, PartialEq, Clone)] pub struct ParsedCommand {
pub name: String,
pub args: Vec<ArgValue>,
}
const KNOWN_COMMANDS: [&'static str; 4] = ["combine", "predict", "show", "help"]; const SIMILARITY_THRESHOLD: f64 = 0.85;
fn suggest_command(input: &str) -> Option<&'static str> {
KNOWN_COMMANDS
.iter()
.map(|&cmd| (cmd, jaro_winkler(input, cmd)))
.filter(|(_, similarity)| *similarity >= SIMILARITY_THRESHOLD)
.max_by(|(_, sim1), (_, sim2)| sim1.partial_cmp(sim2).unwrap_or(std::cmp::Ordering::Equal))
.map(|(cmd, _)| cmd)
}
pub struct Parser;
impl Parser {
pub fn new() -> Self {
Parser
}
pub fn parse(&self, input: &str) -> Result<ParsedCommand, FCError> {
let input = input.trim();
if input.is_empty() {
return Err(FCError::ParseError("empty input".to_string()));
}
let parts: Vec<&str> = input.split_whitespace().collect();
let command_name_str = parts[0];
if !KNOWN_COMMANDS.contains(&command_name_str) {
if let Some(s) = suggest_command(command_name_str) {
return Err(FCError::Suggestion(command_name_str.to_string(), s.to_string()));
} else {
return Err(FCError::UnknownCommand(command_name_str.to_string()));
}
}
let command_name = command_name_str.to_string();
let command_args = parts[1..]
.iter()
.map(|s| classify_arg(s))
.collect();
Ok(ParsedCommand {
name: command_name,
args: command_args,
})
}
}
pub fn all_command_names() -> Vec<&'static str> {
KNOWN_COMMANDS.to_vec()
}
fn classify_arg(raw: &str) -> ArgValue {
if raw.eq_ignore_ascii_case("true") {
return ArgValue::Bool(true);
}
if raw.eq_ignore_ascii_case("false") {
return ArgValue::Bool(false);
}
if raw.eq_ignore_ascii_case("null") {
return ArgValue::Null;
}
if let Ok(n) = raw.parse::<f64>() {
return ArgValue::Number(n);
}
#[cfg(feature = "typed-args")]
if let Ok(d) = chrono::NaiveDate::parse_from_str(raw, "%Y-%m-%d") {
return ArgValue::Date(d);
}
ArgValue::String(raw.to_string())
}