fv-value 0.1.1

An expression dialect for computing values over rows: arithmetic, strings, dates, lists, logic and geospatial functions, with an extensible function registry.
Documentation
//! The error type. Every failure is one of a handful of kinds, so callers can tell a bad
//! expression (report it at authoring time) from a bad row (skip or null it at run time).

use std::fmt;

/// Why compiling or evaluating an expression failed.
///
/// `Syntax`, `UnknownFunction` and `Arity` are compile-time: [`Dialect::compile`](crate::Dialect::compile)
/// reports them once, before any row is seen. `Type`, `DivisionByZero` and `Call` are run-time:
/// the expression is well-formed but a particular row's values do not fit it.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExprError {
    /// The source text could not be tokenized or parsed. `position` is the character offset of
    /// the offending token when it is known.
    Syntax {
        /// What was wrong.
        message: String,
        /// Character offset into the source, if known.
        position: Option<usize>,
    },
    /// A call names a function the dialect does not have.
    UnknownFunction {
        /// The name as written.
        name: String,
    },
    /// A call has the wrong number of arguments.
    Arity {
        /// The function.
        name: String,
        /// Fewest arguments accepted.
        min: usize,
        /// Most arguments accepted; `None` = variadic.
        max: Option<usize>,
        /// How many were given.
        given: usize,
    },
    /// An operator was applied to operands of the wrong type (e.g. `"a" - 1`, `!5`).
    Type {
        /// What was wrong.
        message: String,
    },
    /// `/` or `%` by zero.
    DivisionByZero,
    /// A function rejected its arguments or could not produce a result.
    Call {
        /// What was wrong, prefixed by the function name.
        message: String,
    },
}

impl ExprError {
    pub(crate) fn syntax_at(message: impl Into<String>, position: usize) -> Self {
        ExprError::Syntax {
            message: message.into(),
            position: Some(position),
        }
    }

    pub(crate) fn type_error(message: impl Into<String>) -> Self {
        ExprError::Type {
            message: message.into(),
        }
    }

    /// A function's own failure. Public so custom functions registered on a
    /// [`Functions`](crate::Functions) registry can report problems the same way builtins do.
    pub fn call(message: impl Into<String>) -> Self {
        ExprError::Call {
            message: message.into(),
        }
    }

    /// Whether this error is detectable at compile time (a bad expression) rather than a
    /// run-time value problem (a bad row).
    pub fn is_compile_error(&self) -> bool {
        matches!(
            self,
            ExprError::Syntax { .. } | ExprError::UnknownFunction { .. } | ExprError::Arity { .. }
        )
    }
}

impl fmt::Display for ExprError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ExprError::Syntax {
                message,
                position: Some(p),
            } => write!(f, "syntax error at {p}: {message}"),
            ExprError::Syntax {
                message,
                position: None,
            } => write!(f, "syntax error: {message}"),
            ExprError::UnknownFunction { name } => write!(f, "unknown function '{name}'"),
            ExprError::Arity { name, min, max, given } => match max {
                Some(max) if max == min => write!(f, "{name}() takes {min} argument(s), got {given}"),
                Some(max) => write!(f, "{name}() takes {min} to {max} arguments, got {given}"),
                None => write!(f, "{name}() takes at least {min} argument(s), got {given}"),
            },
            ExprError::Type { message } => write!(f, "type error: {message}"),
            ExprError::DivisionByZero => write!(f, "division by zero"),
            ExprError::Call { message } => write!(f, "{message}"),
        }
    }
}

impl std::error::Error for ExprError {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn display_and_classification() {
        assert_eq!(
            ExprError::syntax_at("expected ')'", 7).to_string(),
            "syntax error at 7: expected ')'"
        );
        assert_eq!(
            ExprError::UnknownFunction { name: "nope".into() }.to_string(),
            "unknown function 'nope'"
        );
        let a = ExprError::Arity {
            name: "round".into(),
            min: 1,
            max: Some(2),
            given: 3,
        };
        assert_eq!(a.to_string(), "round() takes 1 to 2 arguments, got 3");
        let v = ExprError::Arity {
            name: "concat".into(),
            min: 1,
            max: None,
            given: 0,
        };
        assert_eq!(v.to_string(), "concat() takes at least 1 argument(s), got 0");
        let x = ExprError::Arity {
            name: "abs".into(),
            min: 1,
            max: Some(1),
            given: 2,
        };
        assert_eq!(x.to_string(), "abs() takes 1 argument(s), got 2");
        assert!(a.is_compile_error() && !ExprError::DivisionByZero.is_compile_error());
        assert_eq!(
            ExprError::call("upper: expected a string").to_string(),
            "upper: expected a string"
        );
        assert_eq!(
            ExprError::type_error("'-': operands must be numbers").to_string(),
            "type error: '-': operands must be numbers"
        );
    }
}