matugen-parser 0.1.0

The templating engine for Matugen
Documentation
use std::{cell::RefCell, collections::HashSet};

use ariadne::{Color, Label, Report, ReportKind, Source};
use chumsky::span::SimpleSpan;

use thiserror::Error as ThisError;

use crate::Engine;

#[derive(Debug, Default)]
pub struct ErrorCollector {
    errors: RefCell<Vec<Error>>,
    seen_spans: RefCell<HashSet<SimpleSpan>>,
}

impl ErrorCollector {
    pub fn new() -> Self {
        Self {
            errors: RefCell::new(Vec::new()),
            seen_spans: RefCell::new(HashSet::new()),
        }
    }

    pub fn add(&self, error: Error) {
        let seen = match &error {
            Error::TemplateNotFound { template: _, .. } => false,
            Error::ParseError { kind: _, span, .. } => self.seen_spans.borrow().contains(span),
            Error::ResolveError { span, .. } => self.seen_spans.borrow().contains(span),
            Error::IncludeError { span, .. } => self.seen_spans.borrow().contains(span),
        };
        if !seen {
            let span = error.get_span();
            if span.is_some() {
                self.seen_spans.borrow_mut().insert(span.unwrap());
            }

            self.errors.borrow_mut().push(error);
        }
    }

    pub fn is_empty(&self) -> bool {
        self.errors.borrow().is_empty()
    }

    pub fn into_inner(self) -> Vec<Error> {
        self.errors.into_inner()
    }

    pub fn take(&self) -> Vec<Error> {
        let mut errors = self.errors.borrow_mut();
        let mut taken = Vec::new();
        std::mem::swap(&mut *errors, &mut taken);
        self.seen_spans.borrow_mut().clear();
        taken
    }
}

#[derive(ThisError, Debug)]
pub enum Error {
    #[error("Could not find template: {template}")]
    TemplateNotFound { template: String, name: String },
    #[error("Parse Error: {kind}")]
    ParseError {
        kind: ParseErrorKind,
        span: SimpleSpan,
        name: String,
    },
    #[error("Value does not exist in the context")]
    ResolveError { span: SimpleSpan, name: String },
    #[error("Failed to include file")]
    IncludeError { span: SimpleSpan, name: String },
}

#[derive(Debug, ThisError)]
pub enum ParseErrorKind {
    #[error(transparent)]
    Filter(#[from] FilterError),

    #[error(transparent)]
    Keyword(#[from] KeywordError),

    #[error(transparent)]
    Loop(#[from] LoopError),

    #[error(transparent)]
    BinOp(#[from] BinaryOperatorError),

    #[error(transparent)]
    If(#[from] IfError),
}

#[derive(Debug, ThisError)]
pub enum IfError {
    #[error("You can only use if conditions with Booleans")]
    InvalidIfCondition,
}

#[derive(Debug, ThisError)]
pub enum BinaryOperatorError {
    #[error("Cannot apply '{op}' operator between {lhs} and {rhs}")]
    InvalidBinaryOperatorType {
        lhs: String,
        op: String,
        rhs: String,
    },
}

#[derive(Debug, ThisError)]
pub enum LoopError {
    #[error("You can only loop over Arrays, Maps and Colors")]
    LoopOverNonIterableValue,
    #[error(
        "For loop over an Array supports only one variable. Key and value iteration (`<* for key, value in map *>`) is only valid for Maps."
    )]
    TooManyLoopVariablesArray,
    #[error("For loop supports only one or two variables")]
    TooManyLoopVariables,
}

#[derive(Debug, ThisError)]
pub enum KeywordError {
    #[error("The format provided is not valid. Available formats are: {formats:?}")]
    InvalidFormat { formats: &'static [&'static str] },
    #[error("Invalid color mode. The color mode can only be one of: [dark, light, default]")]
    ColorDoesNotExist,
    #[error("The format for colors is 'colors.<color>.<scheme>.<format>'")]
    InvalidColorDefinition,
}

#[derive(Debug, ThisError)]
pub enum FilterError {
    #[error("Not enough arguments provided for filter")]
    NotEnoughArguments,
    #[error("Found '{actual}' expected '{expected}'")]
    InvalidArgumentType {
        span: SimpleSpan,
        expected: String,
        actual: String,
    },
    #[error("Cannot use color filters on a string filter, consider using the 'to_color' filter")]
    ColorFilterOnString,
    #[error("Cannot use color filters on a boolean value")]
    ColorFilterOnBool,
    #[error("Could not find the filter: {filter}")]
    FilterNotFound { filter: String },
    #[error("Invalid String, expected one of: [{expected}]")]
    UnexpectedStringValue { expected: String, span: SimpleSpan },
    #[error("Invalid format for the 'format' filter, expected one of: {expected:?}")]
    InvalidFormatString {
        expected: &'static [&'static str],
        span: SimpleSpan,
    },
    #[error(
        "You should not use the set_alpha filter with a format that doesn't have an alpha channel. Consider using one of these formats instead: [{replacement}]"
    )]
    SetAlphaOnNonAlphaFormat { replacement: &'static str },
}

impl Error {
    pub fn get_span(&self) -> Option<SimpleSpan> {
        match self {
            Error::TemplateNotFound { template: _, .. } => None,
            Error::ParseError { kind: _, span, .. } => Some(*span),
            Error::ResolveError { span, .. } => Some(*span),
            Error::IncludeError { span, .. } => Some(*span),
        }
    }

    pub fn get_name(&self) -> String {
        match self {
            Error::TemplateNotFound { .. } => "TemplateNotFound".to_owned(),
            Error::ParseError { kind, .. } => match kind {
                ParseErrorKind::Filter(e) => format!("ParseError::{}", e.name()),
                ParseErrorKind::Keyword(e) => format!("ParseError::{}", e.name()),
                ParseErrorKind::Loop(e) => format!("ParseError::{}", e.name()),
                ParseErrorKind::BinOp(e) => format!("ParseError::{}", e.name()),
                ParseErrorKind::If(e) => format!("ParseError::{}", e.name()),
            },
            Error::ResolveError { .. } => "ResolveError".to_owned(),
            Error::IncludeError { .. } => "IncludeError".to_owned(),
        }
    }

    pub fn get_file_name(&self) -> &String {
        match self {
            Error::TemplateNotFound { name, .. } => name,
            Error::ParseError { name, .. } => name,
            Error::ResolveError { name, .. } => name,
            Error::IncludeError { name, .. } => name,
        }
    }

    pub fn emit(&self, engine: &Engine) -> Result<(), color_eyre::Report> {
        let name = self.get_name();
        let message = self.to_string();
        let span = self.get_span();
        let file_name = self.get_file_name();
        let source_code = engine.get_source(&file_name)?;

        if let Some(span) = span {
            Ok(build_report(&name, source_code, message, span, file_name))
        } else {
            Ok(eprintln!("{}", message))
        }
    }
}

impl FilterError {
    pub fn name(&self) -> &str {
        match self {
            FilterError::NotEnoughArguments => "NotEnoughArguments",
            FilterError::InvalidArgumentType { .. } => "InvalidArgumentType",
            FilterError::ColorFilterOnString => "ColorFilterOnString",
            FilterError::ColorFilterOnBool => "ColorFilterOnBool",
            FilterError::FilterNotFound { .. } => "FilterNotFound",
            FilterError::UnexpectedStringValue { .. } => "UnexpectedStringValue",
            FilterError::InvalidFormatString { .. } => "InvalidFormatString",
            FilterError::SetAlphaOnNonAlphaFormat { .. } => "SetAlphaOnNonAlphaFormat",
        }
    }
}

impl KeywordError {
    pub fn name(&self) -> &str {
        match self {
            KeywordError::InvalidFormat { .. } => "InvalidFormat",
            KeywordError::ColorDoesNotExist => "ColorDoesNotExist",
            KeywordError::InvalidColorDefinition => "InvalidColorDefinition",
        }
    }
}

impl LoopError {
    pub fn name(&self) -> &str {
        match self {
            LoopError::LoopOverNonIterableValue => "LoopOverNonIterableValue",
            LoopError::TooManyLoopVariablesArray => "TooManyLoopVariables",
            LoopError::TooManyLoopVariables => "TooManyLoopVariables",
        }
    }
}

impl BinaryOperatorError {
    pub fn name(&self) -> &str {
        match self {
            BinaryOperatorError::InvalidBinaryOperatorType { .. } => "InvalidBinaryOperatorType",
        }
    }
}

impl IfError {
    pub fn name(&self) -> &str {
        match self {
            IfError::InvalidIfCondition => "InvalidIfCondition",
        }
    }
}

fn build_report(name: &str, source_code: &str, message: String, span: SimpleSpan, file_name: &str) {
    Report::build(ReportKind::Error, (file_name, span.into_range()))
        .with_config(ariadne::Config::default().with_index_type(ariadne::IndexType::Byte))
        .with_message(name)
        .with_label(
            Label::new((file_name, span.into_range()))
                .with_message(message)
                .with_color(Color::Red),
        )
        .finish()
        .print((file_name, Source::from(&source_code)))
        .unwrap();
}