use crate::sdf;
use anyhow::{anyhow, bail, ensure, Result};
use logos::{Logos, SpannedIter};
use std::collections::HashMap;
use std::iter::Peekable;
use std::str::FromStr;
use strum::EnumString;
pub fn is_expression(s: &str) -> bool {
s.starts_with('`') && s.ends_with('`') && s.len() >= 2
}
#[derive(Logos, Debug, Clone, PartialEq)]
#[logos(skip r"[ \t\n\f]+")]
pub enum Token<'source> {
#[regex(r#""([^"\\]|\\.)*""#, |lex| trim_quotes(lex.slice()))]
#[regex(r#"'([^'\\]|\\.)*'"#, |lex| trim_quotes(lex.slice()))]
String(&'source str),
#[regex(r"-?[0-9]+", |lex| lex.slice())]
Integer(&'source str),
#[token("True")]
#[token("true")]
True,
#[token("False")]
#[token("false")]
False,
#[token("None")]
None,
#[regex(r"\$\{[a-zA-Z_][a-zA-Z0-9_]*\}", |lex| {
let s = lex.slice();
// Strip ${ and }
&s[2..s.len()-1]
})]
Variable(&'source str),
#[regex(r"[a-zA-Z_][a-zA-Z0-9_]*")]
Identifier(&'source str),
#[token("(")]
LParen,
#[token(")")]
RParen,
#[token("[")]
LBracket,
#[token("]")]
RBracket,
#[token(",")]
Comma,
}
fn trim_quotes(s: &str) -> &str {
&s[1..s.len() - 1]
}
pub fn tokenize(input: &str) -> impl Iterator<Item = Result<Token<'_>, ()>> {
Token::lexer(input).map(|result| result.map_err(|_| ()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum Func {
Defined,
If,
And,
Or,
Not,
Eq,
Neq,
Lt,
Leq,
Gt,
Geq,
Contains,
At,
Len,
}
impl Func {
const fn arg_count(self) -> (usize, usize) {
match self {
Func::Defined => (1, usize::MAX),
Func::If => (2, 3),
Func::And | Func::Or => (2, usize::MAX),
Func::Not | Func::Len => (1, 1),
Func::Eq | Func::Neq | Func::Lt | Func::Leq | Func::Gt | Func::Geq | Func::Contains | Func::At => (2, 2),
}
}
fn validate_arg_count(self, count: usize) -> Result<()> {
let (min, max) = self.arg_count();
if count < min || count > max {
if min == max {
bail!("{:?} requires exactly {} argument(s), got {}", self, min, count);
} else if max == usize::MAX {
bail!("{:?} requires at least {} argument(s), got {}", self, min, count);
} else {
bail!("{:?} requires {} to {} arguments, got {}", self, min, max, count);
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
String(String),
Integer(i64),
Bool(bool),
None,
Variable(String),
Array(Vec<Expr>),
Call { func: Func, args: Vec<Expr> },
}
impl FromStr for Expr {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let mut parser = Parser::new(s);
parser.parse_expr()
}
}
impl Expr {
pub fn parse(s: &str) -> Result<Self> {
s.parse()
}
pub fn eval(&self, vars: &HashMap<String, sdf::Value>) -> Result<sdf::Value> {
match self {
Expr::String(s) => Ok(sdf::Value::String(interpolate_string(s, vars)?)),
Expr::Integer(n) => Ok(sdf::Value::Int64(*n)),
Expr::Bool(b) => Ok(sdf::Value::Bool(*b)),
Expr::None => Ok(sdf::Value::None),
Expr::Variable(name) => vars
.get(name)
.cloned()
.ok_or_else(|| anyhow!("Undefined variable: {}", name)),
Expr::Array(elements) => {
let values: Result<Vec<_>> = elements.iter().map(|e| e.eval(vars)).collect();
eval_array(values?)
}
Expr::Call { func, args } => eval_func(*func, args, vars),
}
}
}
fn interpolate_string(s: &str, vars: &HashMap<String, sdf::Value>) -> Result<String> {
let mut result = String::with_capacity(s.len());
let mut rest = s;
while let Some(pos) = rest.find("${") {
result.push_str(&rest[..pos]);
rest = &rest[pos + 2..];
let end = rest
.find('}')
.ok_or_else(|| anyhow!("Unclosed variable reference in string"))?;
let var_name = &rest[..end];
let value = vars
.get(var_name)
.ok_or_else(|| anyhow!("Undefined variable: {}", var_name))?;
result.push_str(&value_to_string(value)?);
rest = &rest[end + 1..];
}
result.push_str(rest);
Ok(result)
}
fn value_to_string(value: &sdf::Value) -> Result<String> {
match value {
sdf::Value::String(s) => Ok(s.clone()),
sdf::Value::Int64(n) => Ok(n.to_string()),
sdf::Value::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
sdf::Value::None => Ok("None".to_string()),
_ => bail!("Cannot interpolate {} into string", value_type_name(value)),
}
}
fn value_type_name(value: &sdf::Value) -> &'static str {
match value {
sdf::Value::None => "None",
sdf::Value::Bool(_) => "bool",
sdf::Value::BoolVec(_) => "bool[]",
sdf::Value::Int64(_) => "int64",
sdf::Value::Int64Vec(_) => "int64[]",
sdf::Value::String(_) => "string",
sdf::Value::StringVec(_) => "string[]",
_ => "unsupported type",
}
}
fn eval_array(values: Vec<sdf::Value>) -> Result<sdf::Value> {
if values.is_empty() {
return Ok(sdf::Value::StringVec(vec![]));
}
match &values[0] {
sdf::Value::String(_) => {
let mut strings = Vec::with_capacity(values.len());
for v in values {
match v {
sdf::Value::String(s) => strings.push(s),
_ => bail!("Array elements must be the same type"),
}
}
Ok(sdf::Value::StringVec(strings))
}
sdf::Value::Int64(_) => {
let mut ints = Vec::with_capacity(values.len());
for v in values {
match v {
sdf::Value::Int64(n) => ints.push(n),
_ => bail!("Array elements must be the same type"),
}
}
Ok(sdf::Value::Int64Vec(ints))
}
sdf::Value::Bool(_) => {
let mut bools = Vec::with_capacity(values.len());
for v in values {
match v {
sdf::Value::Bool(b) => bools.push(b),
_ => bail!("Array elements must be the same type"),
}
}
Ok(sdf::Value::BoolVec(bools))
}
other => bail!("Unsupported array element type: {}", value_type_name(other)),
}
}
fn value_as_bool(value: &sdf::Value) -> Result<bool> {
match value {
sdf::Value::Bool(b) => Ok(*b),
other => bail!("Expected bool, got {}", value_type_name(other)),
}
}
fn value_as_int(value: &sdf::Value) -> Result<i64> {
match value {
sdf::Value::Int64(n) => Ok(*n),
other => bail!("Expected int64, got {}", value_type_name(other)),
}
}
fn eval_func(func: Func, args: &[Expr], vars: &HashMap<String, sdf::Value>) -> Result<sdf::Value> {
match func {
Func::Defined => {
for arg in args {
let name = match arg {
Expr::Variable(name) => name,
other => bail!("defined() requires variable names, got {:?}", other),
};
if !vars.contains_key(name) {
return Ok(sdf::Value::Bool(false));
}
}
Ok(sdf::Value::Bool(true))
}
Func::If => {
let cond = value_as_bool(&args[0].eval(vars)?)?;
if cond {
args[1].eval(vars)
} else if args.len() == 3 {
args[2].eval(vars)
} else {
Ok(sdf::Value::None)
}
}
Func::And => {
for arg in args {
if !value_as_bool(&arg.eval(vars)?)? {
return Ok(sdf::Value::Bool(false));
}
}
Ok(sdf::Value::Bool(true))
}
Func::Or => {
for arg in args {
if value_as_bool(&arg.eval(vars)?)? {
return Ok(sdf::Value::Bool(true));
}
}
Ok(sdf::Value::Bool(false))
}
Func::Not => {
let val = value_as_bool(&args[0].eval(vars)?)?;
Ok(sdf::Value::Bool(!val))
}
Func::Eq => {
let left = args[0].eval(vars)?;
let right = args[1].eval(vars)?;
Ok(sdf::Value::Bool(values_equal(&left, &right)?))
}
Func::Neq => {
let left = args[0].eval(vars)?;
let right = args[1].eval(vars)?;
Ok(sdf::Value::Bool(!values_equal(&left, &right)?))
}
Func::Lt => {
let left = args[0].eval(vars)?;
let right = args[1].eval(vars)?;
Ok(sdf::Value::Bool(compare_values(&left, &right)?.is_lt()))
}
Func::Leq => {
let left = args[0].eval(vars)?;
let right = args[1].eval(vars)?;
Ok(sdf::Value::Bool(!compare_values(&left, &right)?.is_gt()))
}
Func::Gt => {
let left = args[0].eval(vars)?;
let right = args[1].eval(vars)?;
Ok(sdf::Value::Bool(compare_values(&left, &right)?.is_gt()))
}
Func::Geq => {
let left = args[0].eval(vars)?;
let right = args[1].eval(vars)?;
Ok(sdf::Value::Bool(!compare_values(&left, &right)?.is_lt()))
}
Func::Contains => {
let container = args[0].eval(vars)?;
let value = args[1].eval(vars)?;
match &container {
sdf::Value::String(s) => {
let needle = match &value {
sdf::Value::String(v) => v,
other => bail!(
"contains() with string requires string value, got {}",
value_type_name(other)
),
};
Ok(sdf::Value::Bool(s.contains(needle.as_str())))
}
sdf::Value::StringVec(arr) => {
let needle = match &value {
sdf::Value::String(v) => v,
other => bail!(
"contains() with string[] requires string value, got {}",
value_type_name(other)
),
};
Ok(sdf::Value::Bool(arr.contains(needle)))
}
sdf::Value::Int64Vec(arr) => {
let needle = value_as_int(&value)?;
Ok(sdf::Value::Bool(arr.contains(&needle)))
}
sdf::Value::BoolVec(arr) => {
let needle = value_as_bool(&value)?;
Ok(sdf::Value::Bool(arr.contains(&needle)))
}
other => bail!("contains() requires string or array, got {}", value_type_name(other)),
}
}
Func::At => {
let container = args[0].eval(vars)?;
let index = value_as_int(&args[1].eval(vars)?)?;
match &container {
sdf::Value::String(s) => {
let len = s.chars().count() as i64;
let idx = normalize_index(index, len)?;
let ch = s.chars().nth(idx).ok_or_else(|| anyhow!("Index out of bounds"))?;
Ok(sdf::Value::String(ch.to_string()))
}
sdf::Value::StringVec(arr) => {
let idx = normalize_index(index, arr.len() as i64)?;
Ok(sdf::Value::String(arr[idx].clone()))
}
sdf::Value::Int64Vec(arr) => {
let idx = normalize_index(index, arr.len() as i64)?;
Ok(sdf::Value::Int64(arr[idx]))
}
sdf::Value::BoolVec(arr) => {
let idx = normalize_index(index, arr.len() as i64)?;
Ok(sdf::Value::Bool(arr[idx]))
}
other => bail!("at() requires string or array, got {}", value_type_name(other)),
}
}
Func::Len => {
let container = args[0].eval(vars)?;
match &container {
sdf::Value::String(s) => Ok(sdf::Value::Int64(s.chars().count() as i64)),
sdf::Value::StringVec(arr) => Ok(sdf::Value::Int64(arr.len() as i64)),
sdf::Value::Int64Vec(arr) => Ok(sdf::Value::Int64(arr.len() as i64)),
sdf::Value::BoolVec(arr) => Ok(sdf::Value::Int64(arr.len() as i64)),
other => bail!("len() requires string or array, got {}", value_type_name(other)),
}
}
}
}
fn values_equal(left: &sdf::Value, right: &sdf::Value) -> Result<bool> {
match (left, right) {
(sdf::Value::String(a), sdf::Value::String(b)) => Ok(a == b),
(sdf::Value::Int64(a), sdf::Value::Int64(b)) => Ok(a == b),
(sdf::Value::Bool(a), sdf::Value::Bool(b)) => Ok(a == b),
(sdf::Value::None, sdf::Value::None) => Ok(true),
_ => bail!(
"Cannot compare {} with {}",
value_type_name(left),
value_type_name(right)
),
}
}
fn compare_values(left: &sdf::Value, right: &sdf::Value) -> Result<std::cmp::Ordering> {
match (left, right) {
(sdf::Value::String(a), sdf::Value::String(b)) => Ok(a.cmp(b)),
(sdf::Value::Int64(a), sdf::Value::Int64(b)) => Ok(a.cmp(b)),
(sdf::Value::Bool(a), sdf::Value::Bool(b)) => Ok(a.cmp(b)),
_ => bail!(
"Cannot compare {} with {}",
value_type_name(left),
value_type_name(right)
),
}
}
fn normalize_index(index: i64, len: i64) -> Result<usize> {
let idx = if index < 0 { len + index } else { index };
if idx < 0 || idx >= len {
bail!("Index {} out of bounds for length {}", index, len);
}
Ok(idx as usize)
}
struct Parser<'a> {
iter: Peekable<SpannedIter<'a, Token<'a>>>,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
let input = if is_expression(input) {
&input[1..input.len() - 1]
} else {
input
};
Self {
iter: Token::lexer(input).spanned().peekable(),
}
}
fn peek(&mut self) -> Option<&Token<'a>> {
self.iter.peek().and_then(|(result, _)| result.as_ref().ok())
}
fn next(&mut self) -> Option<Token<'a>> {
loop {
match self.iter.next() {
Some((Ok(token), _)) => return Some(token),
Some((Err(_), _)) => continue, None => return None,
}
}
}
fn is_eof(&mut self) -> bool {
self.peek().is_none()
}
fn expect(&mut self, expected: Token<'a>) -> Result<()> {
match self.next() {
Some(ref token) if *token == expected => Ok(()),
Some(token) => bail!("Expected {:?}, got {:?}", expected, token),
None => bail!("Expected {:?}, got end of input", expected),
}
}
fn parse_expr(&mut self) -> Result<Expr> {
let expr = self.parse()?;
ensure!(self.is_eof(), "Unexpected token after expression: {:?}", self.peek());
Ok(expr)
}
fn parse(&mut self) -> Result<Expr> {
let token = self.next().ok_or_else(|| anyhow!("Unexpected end of input"))?;
match token {
Token::String(s) => Ok(Expr::String(unescape_string(s))),
Token::Integer(s) => {
let value = s.parse::<i64>().map_err(|e| anyhow!("Invalid integer: {}", e))?;
Ok(Expr::Integer(value))
}
Token::True => Ok(Expr::Bool(true)),
Token::False => Ok(Expr::Bool(false)),
Token::None => Ok(Expr::None),
Token::Variable(name) => Ok(Expr::Variable(name.to_string())),
Token::LBracket => self.parse_array(),
Token::Identifier(name) => {
if matches!(self.peek(), Some(Token::LParen)) {
self.parse_call(name)
} else {
Ok(Expr::Variable(name.to_string()))
}
}
other => bail!("Unexpected token: {:?}", other),
}
}
fn parse_array(&mut self) -> Result<Expr> {
let mut elements = Vec::new();
if matches!(self.peek(), Some(Token::RBracket)) {
self.next();
return Ok(Expr::Array(elements));
}
loop {
elements.push(self.parse()?);
match self.peek() {
Some(Token::Comma) => {
self.next();
if matches!(self.peek(), Some(Token::RBracket)) {
self.next();
break;
}
}
Some(Token::RBracket) => {
self.next();
break;
}
other => bail!("Expected ',' or ']' in array, got {:?}", other),
}
}
Ok(Expr::Array(elements))
}
fn parse_call(&mut self, name: &str) -> Result<Expr> {
let func: Func = name.parse()?;
self.expect(Token::LParen)?;
let mut args = Vec::new();
if matches!(self.peek(), Some(Token::RParen)) {
self.next();
func.validate_arg_count(args.len())?;
return Ok(Expr::Call { func, args });
}
loop {
args.push(self.parse()?);
match self.peek() {
Some(Token::Comma) => {
self.next();
}
Some(Token::RParen) => {
self.next();
break;
}
other => bail!("Expected ',' or ')' in function call, got {:?}", other),
}
}
func.validate_arg_count(args.len())?;
Ok(Expr::Call { func, args })
}
}
fn unescape_string(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('\\') => result.push('\\'),
Some('"') => result.push('"'),
Some('\'') => result.push('\''),
Some('$') => result.push('$'),
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some(other) => {
result.push('\\');
result.push(other);
}
None => result.push('\\'),
}
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_expression() {
assert!(is_expression(r#"`"hello"`"#));
assert!(is_expression(r#"`if(${VAR}, "a", "b")`"#));
assert!(is_expression("``"));
assert!(!is_expression("hello"));
assert!(!is_expression("`"));
assert!(!is_expression("`hello"));
assert!(!is_expression("hello`"));
assert!(!is_expression(""));
}
#[test]
fn tokenize_string_literals() {
let tokens: Vec<_> = tokenize(r#""hello world""#).collect();
assert_eq!(tokens, vec![Ok(Token::String("hello world"))]);
let tokens: Vec<_> = tokenize(r#"'single quoted'"#).collect();
assert_eq!(tokens, vec![Ok(Token::String("single quoted"))]);
}
#[test]
fn tokenize_escaped_strings() {
let tokens: Vec<_> = tokenize(r#""escaped \"quote\"""#).collect();
assert_eq!(tokens, vec![Ok(Token::String(r#"escaped \"quote\""#))]);
}
#[test]
fn tokenize_integers() {
let tokens: Vec<_> = tokenize("42").collect();
assert_eq!(tokens, vec![Ok(Token::Integer("42"))]);
let tokens: Vec<_> = tokenize("-100").collect();
assert_eq!(tokens, vec![Ok(Token::Integer("-100"))]);
}
#[test]
fn tokenize_booleans() {
let tokens: Vec<_> = tokenize("True false").collect();
assert_eq!(tokens, vec![Ok(Token::True), Ok(Token::False)]);
let tokens: Vec<_> = tokenize("true False").collect();
assert_eq!(tokens, vec![Ok(Token::True), Ok(Token::False)]);
}
#[test]
fn tokenize_none() {
let tokens: Vec<_> = tokenize("None").collect();
assert_eq!(tokens, vec![Ok(Token::None)]);
}
#[test]
fn tokenize_variables() {
let tokens: Vec<_> = tokenize("${ASSET_PATH}").collect();
assert_eq!(tokens, vec![Ok(Token::Variable("ASSET_PATH"))]);
let tokens: Vec<_> = tokenize("${my_var_123}").collect();
assert_eq!(tokens, vec![Ok(Token::Variable("my_var_123"))]);
}
#[test]
fn tokenize_function_call() {
let tokens: Vec<_> = tokenize("if(${USE_HIGH_RES}, \"high\", \"low\")").collect();
assert_eq!(
tokens,
vec![
Ok(Token::Identifier("if")),
Ok(Token::LParen),
Ok(Token::Variable("USE_HIGH_RES")),
Ok(Token::Comma),
Ok(Token::String("high")),
Ok(Token::Comma),
Ok(Token::String("low")),
Ok(Token::RParen),
]
);
}
#[test]
fn tokenize_defined_function() {
let tokens: Vec<_> = tokenize("defined(RENDER_PASS)").collect();
assert_eq!(
tokens,
vec![
Ok(Token::Identifier("defined")),
Ok(Token::LParen),
Ok(Token::Identifier("RENDER_PASS")),
Ok(Token::RParen),
]
);
}
#[test]
fn tokenize_array_literal() {
let tokens: Vec<_> = tokenize("[\"a\", \"b\", \"c\"]").collect();
assert_eq!(
tokens,
vec![
Ok(Token::LBracket),
Ok(Token::String("a")),
Ok(Token::Comma),
Ok(Token::String("b")),
Ok(Token::Comma),
Ok(Token::String("c")),
Ok(Token::RBracket),
]
);
}
#[test]
fn tokenize_nested_function() {
let tokens: Vec<_> = tokenize("if(and(${A}, ${B}), \"yes\", \"no\")").collect();
assert_eq!(
tokens,
vec![
Ok(Token::Identifier("if")),
Ok(Token::LParen),
Ok(Token::Identifier("and")),
Ok(Token::LParen),
Ok(Token::Variable("A")),
Ok(Token::Comma),
Ok(Token::Variable("B")),
Ok(Token::RParen),
Ok(Token::Comma),
Ok(Token::String("yes")),
Ok(Token::Comma),
Ok(Token::String("no")),
Ok(Token::RParen),
]
);
}
#[test]
fn tokenize_comparison_functions() {
let tokens: Vec<_> = tokenize("lt(${VALUE}, 10)").collect();
assert_eq!(
tokens,
vec![
Ok(Token::Identifier("lt")),
Ok(Token::LParen),
Ok(Token::Variable("VALUE")),
Ok(Token::Comma),
Ok(Token::Integer("10")),
Ok(Token::RParen),
]
);
}
#[test]
fn parse_string_literal() {
let expr: Expr = r#""hello world""#.parse().unwrap();
assert_eq!(expr, Expr::String("hello world".to_string()));
}
#[test]
fn parse_escaped_string() {
let expr: Expr = r#""say \"hello\"""#.parse().unwrap();
assert_eq!(expr, Expr::String("say \"hello\"".to_string()));
}
#[test]
fn parse_integer_literal() {
let expr: Expr = "42".parse().unwrap();
assert_eq!(expr, Expr::Integer(42));
let expr: Expr = "-100".parse().unwrap();
assert_eq!(expr, Expr::Integer(-100));
}
#[test]
fn parse_boolean_literals() {
let expr: Expr = "True".parse().unwrap();
assert_eq!(expr, Expr::Bool(true));
let expr: Expr = "false".parse().unwrap();
assert_eq!(expr, Expr::Bool(false));
}
#[test]
fn parse_none_literal() {
let expr: Expr = "None".parse().unwrap();
assert_eq!(expr, Expr::None);
}
#[test]
fn parse_variable() {
let expr: Expr = "${ASSET_PATH}".parse().unwrap();
assert_eq!(expr, Expr::Variable("ASSET_PATH".to_string()));
}
#[test]
fn parse_empty_array() {
let expr: Expr = "[]".parse().unwrap();
assert_eq!(expr, Expr::Array(vec![]));
}
#[test]
fn parse_string_array() {
let expr: Expr = r#"["a", "b", "c"]"#.parse().unwrap();
assert_eq!(
expr,
Expr::Array(vec![
Expr::String("a".to_string()),
Expr::String("b".to_string()),
Expr::String("c".to_string()),
])
);
}
#[test]
fn parse_integer_array() {
let expr: Expr = "[1, 2, 3]".parse().unwrap();
assert_eq!(
expr,
Expr::Array(vec![Expr::Integer(1), Expr::Integer(2), Expr::Integer(3),])
);
}
#[test]
fn parse_simple_function_call() {
let expr: Expr = "not(True)".parse().unwrap();
assert_eq!(
expr,
Expr::Call {
func: Func::Not,
args: vec![Expr::Bool(true)],
}
);
}
#[test]
fn parse_if_function() {
let expr: Expr = r#"if(${USE_HIGH_RES}, "high", "low")"#.parse().unwrap();
assert_eq!(
expr,
Expr::Call {
func: Func::If,
args: vec![
Expr::Variable("USE_HIGH_RES".to_string()),
Expr::String("high".to_string()),
Expr::String("low".to_string()),
],
}
);
}
#[test]
fn parse_defined_function() {
let expr: Expr = "defined(RENDER_PASS)".parse().unwrap();
assert_eq!(
expr,
Expr::Call {
func: Func::Defined,
args: vec![Expr::Variable("RENDER_PASS".to_string())],
}
);
}
#[test]
fn parse_nested_function() {
let expr: Expr = r#"if(and(${A}, ${B}), "yes", "no")"#.parse().unwrap();
assert_eq!(
expr,
Expr::Call {
func: Func::If,
args: vec![
Expr::Call {
func: Func::And,
args: vec![Expr::Variable("A".to_string()), Expr::Variable("B".to_string())],
},
Expr::String("yes".to_string()),
Expr::String("no".to_string()),
],
}
);
}
#[test]
fn parse_comparison_function() {
let expr: Expr = "lt(${VALUE}, 10)".parse().unwrap();
assert_eq!(
expr,
Expr::Call {
func: Func::Lt,
args: vec![Expr::Variable("VALUE".to_string()), Expr::Integer(10)],
}
);
}
#[test]
fn parse_with_backticks() {
let expr: Expr = r#"`"${ASSET_PATH}/model.usd"`"#.parse().unwrap();
assert_eq!(expr, Expr::String("${ASSET_PATH}/model.usd".to_string()));
}
#[test]
fn parse_contains_function() {
let expr: Expr = r#"contains(["a", "b"], ${VAR})"#.parse().unwrap();
assert_eq!(
expr,
Expr::Call {
func: Func::Contains,
args: vec![
Expr::Array(vec![Expr::String("a".to_string()), Expr::String("b".to_string())]),
Expr::Variable("VAR".to_string()),
],
}
);
}
#[test]
fn parse_complex_nested_expression() {
let expr: Expr = r#"if(or(eq(${MODE}, "debug"), defined(DEBUG)), "dbg", "rel")"#.parse().unwrap();
assert_eq!(
expr,
Expr::Call {
func: Func::If,
args: vec![
Expr::Call {
func: Func::Or,
args: vec![
Expr::Call {
func: Func::Eq,
args: vec![Expr::Variable("MODE".to_string()), Expr::String("debug".to_string())],
},
Expr::Call {
func: Func::Defined,
args: vec![Expr::Variable("DEBUG".to_string())],
},
],
},
Expr::String("dbg".to_string()),
Expr::String("rel".to_string()),
],
}
);
}
#[test]
fn parse_unknown_function_fails() {
let result: Result<Expr, _> = "unknown_func(1, 2)".parse();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Matching variant not found"));
}
#[test]
fn parse_backtick_any_expression() {
let expr: Expr = "`42`".parse().unwrap();
assert_eq!(expr, Expr::Integer(42));
let expr: Expr = "`if(True, 1, 2)`".parse().unwrap();
assert_eq!(
expr,
Expr::Call {
func: Func::If,
args: vec![Expr::Bool(true), Expr::Integer(1), Expr::Integer(2)],
}
);
}
#[test]
fn parse_escaped_backslashes() {
let expr: Expr = r#"if(${COND}, "C:\\USD\\test.usd", "D:\\USD\\test.usd")"#.parse().unwrap();
assert_eq!(
expr,
Expr::Call {
func: Func::If,
args: vec![
Expr::Variable("COND".to_string()),
Expr::String(r"C:\USD\test.usd".to_string()),
Expr::String(r"D:\USD\test.usd".to_string()),
],
}
);
}
#[test]
fn parse_escaped_dollar_sign() {
let expr: Expr = r#"`"escaped_var_\${X}"`"#.parse().unwrap();
assert_eq!(expr, Expr::String("escaped_var_${X}".to_string()));
}
#[test]
fn parse_if_with_two_args() {
let expr: Expr = r#"if(True, "yes")"#.parse().unwrap();
assert_eq!(
expr,
Expr::Call {
func: Func::If,
args: vec![Expr::Bool(true), Expr::String("yes".to_string())],
}
);
}
#[test]
fn parse_function_arg_count_validation() {
let result: Result<Expr, _> = "not()".parse();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("exactly 1"));
let result: Result<Expr, _> = "not(True, False)".parse();
assert!(result.is_err());
let result: Result<Expr, _> = "len()".parse();
assert!(result.is_err());
let result: Result<Expr, _> = "eq(1)".parse();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("exactly 2"));
let result: Result<Expr, _> = "eq(1, 2, 3)".parse();
assert!(result.is_err());
let result: Result<Expr, _> = "if(True)".parse();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("2 to 3"));
let result: Result<Expr, _> = "and(True)".parse();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("at least 2"));
let result: Result<Expr, _> = "defined()".parse();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("at least 1"));
}
fn make_vars(pairs: &[(&str, sdf::Value)]) -> HashMap<String, sdf::Value> {
pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
}
#[test]
fn eval_literals() {
let vars = HashMap::new();
let expr: Expr = "42".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Int64(42));
let expr: Expr = "True".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = r#""hello""#.parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::String("hello".to_string()));
let expr: Expr = "None".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::None);
}
#[test]
fn eval_variable() {
let vars = make_vars(&[("X", sdf::Value::Int64(100))]);
let expr: Expr = "${X}".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Int64(100));
}
#[test]
fn eval_undefined_variable_error() {
let vars = HashMap::new();
let expr: Expr = "${UNDEFINED}".parse().unwrap();
assert!(expr.eval(&vars).is_err());
}
#[test]
fn eval_string_interpolation() {
let vars = make_vars(&[
("PATH", sdf::Value::String("/assets".to_string())),
("NAME", sdf::Value::String("model".to_string())),
]);
let expr: Expr = r#"`"${PATH}/${NAME}.usd"`"#.parse().unwrap();
assert_eq!(
expr.eval(&vars).unwrap(),
sdf::Value::String("/assets/model.usd".to_string())
);
}
#[test]
fn eval_if_function() {
let vars = make_vars(&[("COND", sdf::Value::Bool(true))]);
let expr: Expr = r#"if(${COND}, "yes", "no")"#.parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::String("yes".to_string()));
let vars = make_vars(&[("COND", sdf::Value::Bool(false))]);
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::String("no".to_string()));
}
#[test]
fn eval_if_two_args_returns_none() {
let vars = make_vars(&[("COND", sdf::Value::Bool(false))]);
let expr: Expr = r#"if(${COND}, "yes")"#.parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::None);
}
#[test]
fn eval_defined() {
let vars = make_vars(&[("X", sdf::Value::Int64(1))]);
let expr: Expr = "defined(X)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = "defined(Y)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(false));
let expr: Expr = "defined(X, Y)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(false));
}
#[test]
fn eval_and_or_not() {
let vars = HashMap::new();
let expr: Expr = "and(True, True)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = "and(True, False)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(false));
let expr: Expr = "or(False, True)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = "or(False, False)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(false));
let expr: Expr = "not(True)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(false));
}
#[test]
fn eval_comparisons() {
let vars = HashMap::new();
let expr: Expr = "eq(1, 1)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = "neq(1, 2)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = "lt(1, 2)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = "leq(2, 2)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = "gt(3, 2)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = "geq(2, 2)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
}
#[test]
fn eval_string_comparisons() {
let vars = HashMap::new();
let expr: Expr = r#"lt("apple", "banana")"#.parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = r#"eq("test", "test")"#.parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
}
#[test]
fn eval_contains() {
let vars = make_vars(&[("ARR", sdf::Value::StringVec(vec!["a".into(), "b".into(), "c".into()]))]);
let expr: Expr = r#"contains(${ARR}, "b")"#.parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
let expr: Expr = r#"contains(${ARR}, "z")"#.parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(false));
let vars = make_vars(&[("S", sdf::Value::String("hello world".to_string()))]);
let expr: Expr = r#"contains(${S}, "world")"#.parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Bool(true));
}
#[test]
fn eval_at() {
let vars = make_vars(&[("ARR", sdf::Value::StringVec(vec!["a".into(), "b".into(), "c".into()]))]);
let expr: Expr = "at(${ARR}, 0)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::String("a".to_string()));
let expr: Expr = "at(${ARR}, -1)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::String("c".to_string()));
let vars = make_vars(&[("S", sdf::Value::String("hello".to_string()))]);
let expr: Expr = "at(${S}, 1)".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::String("e".to_string()));
}
#[test]
fn eval_len() {
let vars = make_vars(&[("ARR", sdf::Value::StringVec(vec!["a".into(), "b".into()]))]);
let expr: Expr = "len(${ARR})".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Int64(2));
let vars = make_vars(&[("S", sdf::Value::String("hello".to_string()))]);
let expr: Expr = "len(${S})".parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::Int64(5));
}
#[test]
fn eval_complex_expression() {
let vars = make_vars(&[
("MODE", sdf::Value::String("debug".to_string())),
("DEBUG", sdf::Value::Bool(true)),
]);
let expr: Expr = r#"if(or(eq(${MODE}, "debug"), ${DEBUG}), "dbg", "rel")"#.parse().unwrap();
assert_eq!(expr.eval(&vars).unwrap(), sdf::Value::String("dbg".to_string()));
}
}