use rust_decimal::Decimal;
use rustledger_core::{MetaValue, Metadata};
use crate::ast::FunctionCall;
use crate::error::QueryError;
use super::super::Executor;
use super::super::types::{PostingContext, SourceLocation, Value};
impl Executor<'_> {
pub(crate) fn eval_meta_function(
&self,
name: &str,
func: &FunctionCall,
ctx: &PostingContext,
) -> Result<Value, QueryError> {
Self::require_args(name, func, 1)?;
let key = match self.evaluate_expr(&func.args[0], ctx)? {
Value::String(s) => s,
_ => {
return Err(QueryError::Type(format!(
"{name}: argument must be a string key"
)));
}
};
let posting = &ctx.transaction.postings[ctx.posting_index];
let posting_loc = self.resolved_source_location(ctx);
let entry_loc = ctx
.directive_index
.and_then(|i| self.get_source_location(i).cloned());
let meta_value = match name {
"META" | "POSTING_META" => Self::meta_lookup(&posting.meta, posting_loc.as_ref(), &key),
"ENTRY_META" => Self::meta_lookup(&ctx.transaction.meta, entry_loc.as_ref(), &key),
"ANY_META" => Self::meta_lookup(&posting.meta, posting_loc.as_ref(), &key)
.or_else(|| Self::meta_lookup(&ctx.transaction.meta, entry_loc.as_ref(), &key)),
_ => unreachable!(),
};
Ok(Self::meta_value_to_value(meta_value.as_ref()))
}
fn source_location_meta_key(loc: Option<&SourceLocation>, key: &str) -> Option<MetaValue> {
let loc = loc?;
match key {
"filename" => Some(MetaValue::String(loc.filename.clone())),
"lineno" => Some(i64::try_from(loc.lineno).map_or_else(
|_| MetaValue::Number(Decimal::from(loc.lineno as u64)),
MetaValue::Int,
)),
_ => None,
}
}
pub(crate) fn source_filename_value(loc: Option<&SourceLocation>) -> Value {
loc.map_or(Value::Null, |l| Value::String(l.filename.clone()))
}
fn lineno_i64(loc: &SourceLocation) -> i64 {
i64::try_from(loc.lineno).unwrap_or(i64::MAX)
}
pub(crate) fn source_lineno_value(loc: Option<&SourceLocation>) -> Value {
loc.map_or(Value::Null, |l| Value::Integer(Self::lineno_i64(l)))
}
pub(crate) fn source_location_value(loc: Option<&SourceLocation>) -> Value {
loc.map_or(Value::Null, |l| {
Value::String(format!("{}:{}", l.filename, Self::lineno_i64(l)))
})
}
fn meta_lookup(raw: &Metadata, loc: Option<&SourceLocation>, key: &str) -> Option<MetaValue> {
raw.get(key)
.cloned()
.or_else(|| Self::source_location_meta_key(loc, key))
}
pub(crate) fn augmented_meta(raw: &Metadata, loc: Option<&SourceLocation>) -> Metadata {
if loc.is_none() {
return raw.clone();
}
let mut meta = raw.clone();
for key in ["filename", "lineno"] {
if !meta.contains_key(key)
&& let Some(value) = Self::source_location_meta_key(loc, key)
{
meta.insert(key.to_string(), value);
}
}
meta
}
pub(crate) fn meta_value_to_value(mv: Option<&MetaValue>) -> Value {
match mv {
None => Value::Null,
Some(MetaValue::String(s)) => Value::String(s.clone()),
Some(MetaValue::Number(n)) => Value::Number(*n),
Some(MetaValue::Int(i)) => Value::Integer(*i),
Some(MetaValue::Date(d)) => Value::Date(*d),
Some(MetaValue::Bool(b)) => Value::Boolean(*b),
Some(MetaValue::Amount(a)) => Value::Amount(a.clone()),
Some(MetaValue::Account(a)) => Value::String(a.to_string()),
Some(MetaValue::Currency(c)) => Value::String(c.to_string()),
Some(MetaValue::Tag(t)) => Value::String(t.to_string()),
Some(MetaValue::Link(l)) => Value::String(l.to_string()),
Some(MetaValue::None) => Value::Null,
}
}
pub(crate) fn value_to_str(val: &Value) -> Result<Value, QueryError> {
match val {
Value::String(s) => Ok(Value::String(s.clone())),
Value::Integer(i) => Ok(Value::String(i.to_string())),
Value::Number(n) => Ok(Value::String(n.to_string())),
Value::Boolean(b) => Ok(Value::String(if *b { "TRUE" } else { "FALSE" }.to_string())),
Value::Date(d) => Ok(Value::String(d.to_string())),
Value::Amount(a) => Ok(Value::String(format!("{} {}", a.number, a.currency))),
Value::Null => Ok(Value::Null),
_ => Err(QueryError::Type(
"STR expects a string, integer, number, boolean, date, or amount".to_string(),
)),
}
}
pub(crate) fn value_to_int(val: &Value) -> Result<Value, QueryError> {
use rust_decimal::prelude::ToPrimitive;
match val {
Value::Integer(i) => Ok(Value::Integer(*i)),
Value::Number(n) => {
let truncated = n.trunc();
truncated.to_i64().map(Value::Integer).ok_or_else(|| {
QueryError::Type(format!("INT: cannot convert '{n}' to integer"))
})
}
Value::Boolean(b) => Ok(Value::Integer(i64::from(*b))),
Value::String(s) => s
.parse::<i64>()
.map(Value::Integer)
.map_err(|_| QueryError::Type(format!("INT: cannot parse '{s}' as integer"))),
Value::Null => Ok(Value::Null),
_ => Err(QueryError::Type(
"INT expects a number, integer, boolean, or string".to_string(),
)),
}
}
pub(crate) fn value_to_decimal(val: &Value) -> Result<Value, QueryError> {
match val {
Value::Number(n) => Ok(Value::Number(*n)),
Value::Integer(i) => Ok(Value::Number(Decimal::from(*i))),
Value::Boolean(b) => Ok(Value::Number(if *b { Decimal::ONE } else { Decimal::ZERO })),
Value::String(s) => s
.parse::<Decimal>()
.map(Value::Number)
.map_err(|_| QueryError::Type(format!("DECIMAL: cannot parse '{s}' as decimal"))),
Value::Null => Ok(Value::Null),
_ => Err(QueryError::Type(
"DECIMAL expects a number, integer, boolean, or string".to_string(),
)),
}
}
pub(crate) fn value_to_bool(val: &Value) -> Result<Value, QueryError> {
match val {
Value::Boolean(b) => Ok(Value::Boolean(*b)),
Value::Integer(i) => Ok(Value::Boolean(*i != 0)),
Value::Number(n) => Ok(Value::Boolean(!n.is_zero())),
Value::String(s) => {
let s_upper = s.to_uppercase();
match s_upper.as_str() {
"TRUE" | "YES" | "1" | "T" | "Y" => Ok(Value::Boolean(true)),
"FALSE" | "NO" | "0" | "F" | "N" | "" => Ok(Value::Boolean(false)),
_ => Err(QueryError::Type(format!(
"BOOL: cannot parse '{s}' as boolean"
))),
}
}
Value::Null => Ok(Value::Null),
_ => Err(QueryError::Type(
"BOOL expects a boolean, number, integer, or string".to_string(),
)),
}
}
pub(crate) fn eval_coalesce(
&self,
func: &FunctionCall,
ctx: &PostingContext,
) -> Result<Value, QueryError> {
for arg in &func.args {
let val = self.evaluate_expr(arg, ctx)?;
if !matches!(val, Value::Null) {
return Ok(val);
}
}
Ok(Value::Null)
}
}
#[cfg(test)]
mod tests {
use super::super::super::Executor;
use super::super::super::types::{SourceLocation, Value};
fn loc(lineno: usize) -> SourceLocation {
SourceLocation {
filename: "f.bean".to_string(),
lineno,
}
}
#[test]
fn source_lineno_value_basics() {
assert_eq!(
Executor::source_lineno_value(Some(&loc(42))),
Value::Integer(42)
);
assert_eq!(Executor::source_lineno_value(None), Value::Null);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn source_lineno_value_saturates_on_overflow() {
assert_eq!(
Executor::source_lineno_value(Some(&loc(usize::MAX))),
Value::Integer(i64::MAX)
);
assert_eq!(
Executor::source_location_value(Some(&loc(usize::MAX))),
Value::String(format!("f.bean:{}", i64::MAX))
);
}
#[test]
fn source_filename_and_location_values() {
assert_eq!(
Executor::source_filename_value(Some(&loc(7))),
Value::String("f.bean".to_string())
);
assert_eq!(
Executor::source_location_value(Some(&loc(7))),
Value::String("f.bean:7".to_string())
);
assert_eq!(Executor::source_filename_value(None), Value::Null);
assert_eq!(Executor::source_location_value(None), Value::Null);
}
}