rbx-rsml 1.0.1

A lexer and parser for the RSML language.
Documentation
use std::cmp::min;

use levenshtein::levenshtein;
use serde_json::Value;
use crate::types::{Range, Severity};

use crate::collection;

#[derive(Debug, Clone, PartialEq)]
pub enum ParseErrorMessage<'a> {
    Expected(&'a str),
    Correction { closest: Option<&'a str>, range: Range }
}

impl<'a> ParseErrorMessage<'a> {
    pub fn correction<const N: usize>(name: Option<String>, range: Range, allow_list: &[&'static str; N]) -> Self {
        Self::Correction {
            closest:
                if let Some(name) = name { calc_closest(name, allow_list) }
                else { None },
            range
        }
    }
}

impl<'a> ToString for ParseErrorMessage<'a> {
    fn to_string(&self) -> String {
        match self {
            Self::Expected(str) => format!("Expected {str}."),
            Self::Correction { closest, .. } => {
                closest
                    .map(|x| format!("Did you mean {x}?"))
                    .unwrap_or_default()
            },
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum ParseError<'a> {
    UnexpectedTokens { msg: Option<ParseErrorMessage<'a>> },
    MissingToken { msg: Option<ParseErrorMessage<'a>> },
    UnknownDirective { name: String },
    EmptyDirective,
    DirectiveNotAtTop { name: String },
}

impl<'a> ParseError<'a> {
    pub fn severity(&self) -> Severity {
        match self {
            Self::UnexpectedTokens { .. } |
            Self::MissingToken { .. } |
            Self::UnknownDirective { .. } |
            Self::EmptyDirective |
            Self::DirectiveNotAtTop { .. } => Severity::Error,
        }
    }

    pub fn message(&self) -> String {
        match self {
            Self::UnexpectedTokens { msg } => match msg {
                Some(msg) => format!("Unexpected Token(s): {}", msg.to_string()),
                None => String::from("Unexpected Token(s)")
            },

            Self::MissingToken { msg } => match msg {
                Some(msg) => format!("Missing Token: {}", msg.to_string()),
                None => String::from("Missing Token")
            },

            Self::UnknownDirective { name } => format!("Unknown directive: \"{name}\""),
            Self::EmptyDirective => String::from("Directive is empty"),
            Self::DirectiveNotAtTop { .. } => String::from(
                "Directives must appear at the top of the file"
            ),
        }
    }

    pub fn data(&self) -> Option<Value> {
        match self {
            Self::UnexpectedTokens {
                msg: Some(ParseErrorMessage::Correction { closest, range })
            } | Self::MissingToken {
                msg: Some(ParseErrorMessage::Correction { closest, range })
            } => {
                let (range_start, range_end) = (range.start, range.end);

                closest.as_ref().map(|x| {
                    Value::Object(collection!{
                        "range_start".to_string() => Value::Object(collection!{
                            "line".to_string() => Value::Number((range_start.line).into()),
                            "char".to_string() => Value::Number((range_start.character).into()),
                        }),
                        "range_end".to_string() => Value::Object(collection!{
                            "line".to_string() => Value::Number((range_end.line).into()),
                            "char".to_string() => Value::Number((range_end.character).into()),
                        }),
                        "closest".to_string() => Value::String(x.to_string()),
                    })
                })
            },
            _ => None
        }
    }
}

impl<'a> ToString for ParseError<'a> {
    fn to_string(&self) -> String {
         match self {
            Self::UnexpectedTokens { .. } => "UNEXPECTED_TOKENS",
            Self::MissingToken { .. } => "MISSING_TOKEN",
            Self::UnknownDirective { .. } => "UNKNOWN_DIRECTIVE",
            Self::EmptyDirective => "EMPTY_DIRECTIVE",
            Self::DirectiveNotAtTop { .. } => "DIRECTIVE_NOT_AT_TOP",
        }.into()
    }
}

pub fn calc_closest<'a, const N: usize>(name: String, allow_list: &[&'static str; N]) -> Option<&'a str> {
    let name_len = name.len();

    allow_list
        .iter()
        .map(|x| (levenshtein(&name[0..min(name_len, x.len())], x), *x))
        .min_by_key(|x| x.0)
        .map(|x| x.1)
}