Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for ifeval #52

Merged
merged 1 commit into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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