empress 3.0.3

A D-Bus MPRIS daemon for controlling media players.
use nom::error::Error as NomError;

mod ast;
mod ffi;
mod functions;
mod interp;
mod lexer;

mod __parser {
    #![expect(clippy::all, warnings, reason = "Generated code")]

    lalrpop_util::lalrpop_mod!(pub parser, "/format/parser.rs");
}

use __parser::parser;

type ParseError<T> = lalrpop_util::ParseError<lexer::Pos, T, &'static str>;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Error parsing format string")]
    Lex(#[from] NomError<String>),
    #[error("Error parsing format string")]
    Parse(#[from] ParseError<String>),
    #[error("Error loading context into the interpreter")]
    Values(#[from] anyhow::Error),
    #[error("Error evaluating format string")]
    Interpret(#[from] interp::Error),
}

impl From<NomError<&str>> for Error {
    fn from(NomError { input, code }: NomError<&str>) -> Self {
        Self::Lex(NomError {
            input: input.into(),
            code,
        })
    }
}

impl From<ParseError<lexer::Token<'_>>> for Error {
    fn from(e: ParseError<lexer::Token>) -> Self { Self::Parse(e.map_token(|t| format!("{t:?}"))) }
}

pub struct Formatter<'a>(FormatterState<'a>);

enum FormatterState<'a> {
    Basic(ast::Format<'a>),
    Extended(ast::FormatExtended<'a>),
}

impl<'a> Formatter<'a> {
    pub fn compile<S: AsRef<str> + ?Sized + 'a>(fmt: &'a S, extended: bool) -> Result<Self, Error> {
        if extended {
            let toks = lexer::scan_extended(fmt.as_ref());
            log::trace!("Extended format string: {toks:?}");

            let ast = parser::FormatExtendedParser::new().parse(toks?);
            log::trace!("{ast:?}");

            Ok(Self(FormatterState::Extended(ast?)))
        } else {
            let toks = lexer::scan(fmt.as_ref());
            log::trace!("Format string: {toks:?}");

            let ast = parser::FormatParser::new().parse(toks?);
            log::trace!("{ast:?}");
            Ok(Self(FormatterState::Basic(ast?)))
        }
    }

    pub fn run<V: serde::Serialize>(&self, values: V) -> Result<String, Error> {
        let values = match serde_json::to_value(values) {
            Ok(serde_json::Value::Object(m)) => Ok(m),
            Ok(v) => Err(Error::Values(anyhow::anyhow!(
                "Value provided was not a JSON map ({v:?})",
            ))),
            Err(e) => Err(Error::Values(e.into())),
        }?;

        let mut out = String::new();
        let ctx = interp::Context {
            values,
            functions: functions::all(),
        };
        match &self.0 {
            FormatterState::Basic(s) => s.eval(&ctx, &mut out),
            FormatterState::Extended(c) => c.eval(&ctx, &mut out),
        }?;

        Ok(out)
    }
}