flowcode-core 0.4.1-alpha

Core execution engine for FlowCode data scripting language
Documentation
// Command parsing logic
use crate::error::FCError; // Import FCError
use crate::ast::ArgValue; // Import ArgValue
use strsim::jaro_winkler;

#[derive(Debug, PartialEq, Clone)] // Added Clone for easier testing if needed
pub struct ParsedCommand {
    pub name: String,
    pub args: Vec<ArgValue>,
}

const KNOWN_COMMANDS: [&'static str; 4] = ["combine", "predict", "show", "help"]; // Later from SPECS
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
    }

    // Updated to return Result<ParsedCommand, FCError> for better error reporting
    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];

        // Check if the command is known
        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,
        })
    }
}

/// Returns a list of all valid command names understood by the parser.
/// This is primarily intended for internal consistency checks (e.g., tests
/// that ensure `spec::SPECS` and the parser stay in sync).
pub fn all_command_names() -> Vec<&'static str> {
    KNOWN_COMMANDS.to_vec()
}

// ------------------------- helpers -----------------------------

fn classify_arg(raw: &str) -> ArgValue {
    // Attempt boolean
    if raw.eq_ignore_ascii_case("true") {
        return ArgValue::Bool(true);
    }
    if raw.eq_ignore_ascii_case("false") {
        return ArgValue::Bool(false);
    }
    // Attempt null
    if raw.eq_ignore_ascii_case("null") {
        return ArgValue::Null;
    }
    // Attempt number
    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);
    }
    // Default to String
    ArgValue::String(raw.to_string())
}