postar 0.1.0

A local email filtering service
Documentation
use std::{collections::HashMap, ops::Range};

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

use crate::{
    dsl::{ast::*, error::DslError},
    inbox::Folder,
    process::{Action, Matcher, Rule, StringMatcher},
};

type Span = Range<usize>;

#[derive(Debug, Clone)]
pub enum ResolutionError {
    DuplicateFolder(String, Span, Span),
    FolderNotFound(String, Span),
    InvalidRegex(regex::Error, Span),
}

type ResolutionResult<T> = Result<T, ResolutionError>;

impl DslError for ResolutionError {
    fn print_error(&self, file: &super::File) {
        match self {
            ResolutionError::DuplicateFolder(identifier, first_span, second_span) => {
                Report::build(ReportKind::Error, (&file.file_name, first_span.clone()))
                    .with_message(format!("Duplicate folder definition: '{}'", identifier))
                    .with_label(
                        Label::new((&file.file_name, first_span.clone()))
                            .with_message("First definition found here")
                            .with_color(Color::Red),
                    )
                    .with_label(
                        Label::new((&file.file_name, second_span.clone()))
                            .with_message("Second definition found here")
                            .with_color(Color::Blue),
                    )
                    .with_note("Folder identifiers must be unique within the configuration")
                    .finish()
                    .print((&file.file_name, Source::from(&file.contents)))
                    .unwrap();
            }
            ResolutionError::FolderNotFound(identifier, span) => {
                Report::build(ReportKind::Error, (&file.file_name, span.clone()))
                    .with_message(format!("Folder '{}' not found", identifier))
                    .with_label(
                        Label::new((&file.file_name, span.clone()))
                            .with_message("Referenced here")
                            .with_color(Color::Red),
                    )
                    .with_note("Make sure the folder is defined before being referenced")
                    .finish()
                    .print((&file.file_name, Source::from(&file.contents)))
                    .unwrap();
            }
            ResolutionError::InvalidRegex(regex_error, span) => {
                Report::build(ReportKind::Error, (&file.file_name, span.clone()))
                    .with_message("Invalid regular expression")
                    .with_label(
                        Label::new((&file.file_name, span.clone()))
                            .with_message(format!("Regex error: {}", regex_error))
                            .with_color(Color::Red),
                    )
                    .finish()
                    .print((&file.file_name, Source::from(&file.contents)))
                    .unwrap();
            }
        }
    }
}

pub struct Resolver {
    folders: HashMap<String, (Folder, Span)>,
}

impl Resolver {
    pub fn build(config: &ParserConfig) -> ResolutionResult<Self> {
        let mut folders = HashMap::new();

        for node in &config.folder_definitions {
            let folder_def = &node.value;
            if folders.contains_key(&folder_def.identifier) {
                let existing_folder: &(Folder, Span) = folders.get(&folder_def.identifier).unwrap();
                return Err(ResolutionError::DuplicateFolder(
                    folder_def.identifier.clone(),
                    node.span.clone(),
                    existing_folder.1.clone(),
                ));
            }

            // Create the runtime object
            let runtime_folder = Folder {
                name: folder_def.name.clone(),
            };

            folders.insert(
                folder_def.identifier.clone(),
                (runtime_folder, node.span.clone()),
            );
        }

        Ok(Self { folders })
    }

    fn resolve_folder(&self, identifier: &Node<ParserIdentifier>) -> ResolutionResult<&Folder> {
        self.folders
            .get(&identifier.value.identifier)
            .map(|val| &val.0)
            .ok_or_else(|| {
                ResolutionError::FolderNotFound(
                    identifier.value.identifier.clone(),
                    identifier.span.clone(),
                )
            })
    }
}

pub(super) trait Resolve {
    type Output;
    fn resolve(self, ctx: &Resolver) -> ResolutionResult<Self::Output>;
}

impl Resolve for Node<ParserRule> {
    type Output = Rule;

    fn resolve(self, ctx: &Resolver) -> ResolutionResult<Self::Output> {
        let matcher = self.value.matcher.resolve(ctx)?;
        let action = self.value.action.resolve(ctx)?;
        let name = self.value.name;
        Ok(Rule::new(name, matcher, action))
    }
}

impl Resolve for Node<ParserMatcher> {
    type Output = Matcher;

    fn resolve(self, ctx: &Resolver) -> ResolutionResult<Self::Output> {
        Ok(match self.value {
            ParserMatcher::And(node) => Matcher::And(node.resolve(ctx)?),
            ParserMatcher::Or(node) => Matcher::Or(node.resolve(ctx)?),
            ParserMatcher::Not(node) => Matcher::Not(Box::new(node.resolve(ctx)?)),
            ParserMatcher::Subject(node) => Matcher::Subject(node.resolve(ctx)?),
            ParserMatcher::From(node) => Matcher::From(node.resolve(ctx)?),
            ParserMatcher::To(node) => Matcher::To(node.resolve(ctx)?),
            ParserMatcher::Body(node) => Matcher::Body(node.resolve(ctx)?),
        })
    }
}

impl Resolve for Node<ParserMatchList> {
    type Output = Vec<Matcher>;

    fn resolve(self, ctx: &Resolver) -> ResolutionResult<Self::Output> {
        self.value
            .list
            .into_iter()
            .map(|x| x.resolve(ctx))
            .collect()
    }
}

impl Resolve for Node<ParserStringMatcher> {
    type Output = StringMatcher;

    fn resolve(self, _ctx: &Resolver) -> ResolutionResult<Self::Output> {
        Ok(match self.value {
            ParserStringMatcher::Contains(str) => StringMatcher::Contains(str),
            ParserStringMatcher::StartsWith(str) => StringMatcher::StartsWith(str),
            ParserStringMatcher::Equals(str) => StringMatcher::Equals(str),
            ParserStringMatcher::Regex(str) => {
                let regex = regex::Regex::new(&str)
                    .map_err(|err| ResolutionError::InvalidRegex(err, self.span))?;
                StringMatcher::Regex(regex)
            }
        })
    }
}

impl Resolve for Node<ParserAction> {
    type Output = Action;

    fn resolve(self, ctx: &Resolver) -> ResolutionResult<Self::Output> {
        Ok(match self.value {
            ParserAction::Delete => Action::Delete,
            ParserAction::MoveTo(ident) => {
                let folder = ctx.resolve_folder(&ident)?;
                Action::Move(folder.clone())
            }
        })
    }
}