Skip to content

Commit

Permalink
feat: add support for ifeval
Browse files Browse the repository at this point in the history
  • Loading branch information
nlopes committed Feb 8, 2025
1 parent b633af2 commit 587525a
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 12 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions acdc-parser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ authors.workspace = true

[dependencies]
acdc-converters-common.workspace = true
evalexpr = "12"
peg = "0.8.4"
pest = "2.7"
pest_derive = "2.7"
Expand Down
3 changes: 3 additions & 0 deletions acdc-parser/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub enum Error {

#[error("Unexpected block: {0}")]
UnexpectedBlock(String),

#[error("Invalid ifeval directive")]
InvalidIfEvalDirectiveMismatchedTypes,
}

#[derive(Debug, PartialEq, Deserialize)]
Expand Down
187 changes: 176 additions & 11 deletions acdc-parser/src/preprocessor/conditional.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::{error::Error, DocumentAttributes};
use crate::{
error::Error,
model::{Substitute, HEADER},
DocumentAttributes,
};

#[derive(Debug)]
pub(crate) enum Conditional {
Expand Down Expand Up @@ -29,7 +33,26 @@ pub(crate) struct Ifndef {

#[derive(Debug)]
pub(crate) struct Ifeval {
expression: String,
left: EvalValue,
operator: Operator,
right: EvalValue,
}

#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub(crate) enum EvalValue {
String(String),
Number(i64),
Boolean(bool),
}

#[derive(Debug, PartialEq)]
pub(crate) enum Operator {
Equal,
NotEqual,
LessThan,
GreaterThan,
LessThanOrEqual,
GreaterThanOrEqual,
}

#[derive(Debug)]
Expand Down Expand Up @@ -68,9 +91,14 @@ peg::parser! {
}

rule ifeval() -> Conditional
= "ifeval::[" expression:content() "]" {
= "ifeval::[" left:eval_value() operator:operator() right:eval_value() "]" {

// We parse everything we get here as a string, then whoever gets this,
// should convert into the proper EvalValue
Conditional::Ifeval(Ifeval {
expression,
left: EvalValue::String(left),
operator,
right: EvalValue::String(right)
})
}

Expand All @@ -86,6 +114,30 @@ peg::parser! {
= "+" { Operation::And }
/ "," { Operation::Or }

rule eval_value() -> String
= n:$((!operator() ![']'] [_])+) {
n.trim().to_string()
//let n = n.trim();
// if n.starts_with('\'') && n.ends_with('\'') || n.starts_with('"') && n.ends_with('"') {
// n[1..n.len() - 1].to_string()
// } else {
// n.to_string()
// }
}

rule operator() -> Operator
= op:$("==" / "!=" / "<=" / ">=" / "<" / ">") {
match op {
"==" => Operator::Equal,
"!=" => Operator::NotEqual,
"<" => Operator::LessThan,
">" => Operator::GreaterThan,
"<=" => Operator::LessThanOrEqual,
">=" => Operator::GreaterThanOrEqual,
_ => unreachable!(),
}
}

rule name_match() = (!['[' | ',' | '+'] [_])+

rule name() -> String
Expand All @@ -101,8 +153,12 @@ peg::parser! {
}

impl Conditional {
pub(crate) fn is_true(&self, attributes: &DocumentAttributes, content: &mut String) -> bool {
match self {
pub(crate) fn is_true(
&self,
attributes: &DocumentAttributes,
content: &mut String,
) -> Result<bool, Error> {
Ok(match self {
Conditional::Ifdef(ifdef) => {
let mut is_true = false;
if ifdef.attributes.is_empty() {
Expand Down Expand Up @@ -149,12 +205,13 @@ impl Conditional {
}
is_true
}
Conditional::Ifeval(_ifeval) => todo!("ifeval conditional check"),
}
Conditional::Ifeval(ifeval) => ifeval.evaluate(attributes)?,
})
}
}

impl Endif {
#[tracing::instrument(level = "trace")]
pub(crate) fn closes(&self, conditional: &Conditional) -> bool {
if let Some(attribute) = &self.attribute {
match conditional {
Expand All @@ -168,6 +225,74 @@ impl Endif {
}
}

impl Ifeval {
#[tracing::instrument(level = "trace")]
fn evaluate(&self, attributes: &DocumentAttributes) -> Result<bool, Error> {
let left = self.left.convert(attributes);
let right = self.right.convert(attributes);

// TOOD(nlopes): There are a few better ways to do this, but for now, this is
// fine. I'm just going for functionality.
match (&left, &right) {
(EvalValue::Number(_), EvalValue::Number(_))
| (EvalValue::Boolean(_), EvalValue::Boolean(_))
| (EvalValue::String(_), EvalValue::String(_)) => {}
_ => {
tracing::error!("cannot compare different types of values in ifeval directive");
return Err(Error::InvalidIfEvalDirectiveMismatchedTypes);
}
}

Ok(match self.operator {
Operator::Equal => left == right,
Operator::NotEqual => left != right,
Operator::LessThan => left < right,
Operator::GreaterThan => left > right,
Operator::LessThanOrEqual => left <= right,
Operator::GreaterThanOrEqual => left >= right,
})
}
}

impl EvalValue {
#[tracing::instrument(level = "trace")]
fn convert(&self, attributes: &DocumentAttributes) -> Self {
match self {
EvalValue::String(s) => {
// First we substitute any attributes in the string with their values
let s = s.substitute(HEADER, attributes);

// Now, we try to parse the string into a number or a boolean if
// possible. If not, we assume it's a string and return it as is.
if let Ok(value) = s.parse::<bool>() {
EvalValue::Boolean(value)
} else if let Ok(value) = s.parse::<i64>() {
EvalValue::Number(value)
} else {
// If we're here, let's check if we can evaluate this as a math expression
// and return the result as a number.
//
// If not, we return the string as is.
if let Ok(value) = evalexpr::eval_int(&s) {
EvalValue::Number(value)
} else {
let s = if s.starts_with('\'') && s.ends_with('\'')
|| s.starts_with('"') && s.ends_with('"')
{
s[1..s.len() - 1].to_string()
} else {
s.to_string()
};

EvalValue::String(s)
}
}
}
value => value.clone(),
}
}
}

#[tracing::instrument(level = "trace")]
pub(crate) fn parse_line(line: &str) -> Result<Conditional, Error> {
match conditional_parser::conditional(line) {
Expand Down Expand Up @@ -251,12 +376,52 @@ mod tests {
}

#[test]
fn test_ifeval() {
fn test_ifeval_simple_math() {
let line = "ifeval::[1 + 1 == 2]";
let conditional = parse_line(line).unwrap();
match conditional {
match &conditional {
Conditional::Ifeval(ifeval) => {
assert_eq!(ifeval.left, EvalValue::String("1 + 1".to_string()));
assert_eq!(ifeval.operator, Operator::Equal);
assert_eq!(ifeval.right, EvalValue::String("2".to_string()));
}
_ => panic!("Expected Ifeval"),
}
assert!(conditional
.is_true(&DocumentAttributes::default(), &mut String::new())
.unwrap());
}

#[test]
fn test_ifeval_str_equality() {
let line = "ifeval::['ASDF' == ASDF]";
let conditional = parse_line(line).unwrap();
match &conditional {
Conditional::Ifeval(ifeval) => {
assert_eq!(ifeval.left, EvalValue::String("'ASDF'".to_string()));
assert_eq!(ifeval.operator, Operator::Equal);
assert_eq!(ifeval.right, EvalValue::String("ASDF".to_string()));
}
_ => panic!("Expected Ifeval"),
}
assert!(conditional
.is_true(&DocumentAttributes::default(), &mut String::new())
.unwrap());
}

#[test]
fn test_ifeval_greater_than_string_vs_number() {
let line = "ifeval::['1+1' >= 2]";
let conditional = parse_line(line).unwrap();
match &conditional {
Conditional::Ifeval(ifeval) => {
assert_eq!(ifeval.expression, "1 + 1 == 2");
assert_eq!(ifeval.left, EvalValue::String("'1+1'".to_string()));
assert_eq!(ifeval.operator, Operator::GreaterThanOrEqual);
assert_eq!(ifeval.right, EvalValue::String("2".to_string()));
assert!(matches!(
ifeval.evaluate(&DocumentAttributes::default()),
Err(Error::InvalidIfEvalDirectiveMismatchedTypes)
));
}
_ => panic!("Expected Ifeval"),
}
Expand Down
2 changes: 1 addition & 1 deletion acdc-parser/src/preprocessor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ impl Preprocessor {
content.push_str(&format!("{next_line}\n"));
lines.next();
}
if condition.is_true(&attributes, &mut content) {
if condition.is_true(&attributes, &mut content)? {
output.push(content);
}
} else if line.starts_with("include") {
Expand Down

0 comments on commit 587525a

Please sign in to comment.