tera icon indicating copy to clipboard operation
tera copied to clipboard

undefined test has weird behavior with an Option<T>

Open LeBoucEtMistere opened this issue 3 years ago • 4 comments

Hi, creating an issue since after 2 hours of debugging, I think there is something weird going on here.

I have roughly this snippet of code

let mut ctx = tera::Context::new();
ctx.insert("foo", &serializable_object)

let v = ctx.get("foo.x.y");
eprintln!("{:?}", v);

tera_instance.render_to("whatever.tpl", &ctx, output)?;

Where the serializable_object has a field x that is Option<Y> and Y has a field y that is Option<String>.

The issue happens when x is Some(Y) and y is None. In that case, the value obtained from the context when accessing key "foo.x.y" is None (which is expected), but in the whatever.tpl template, I have something like this:

{%- if foo.x.y is undefined %}{{ throw(message="Bad Bad Bad") }}{%- endif %}

and it never throws, the undefined check returns true.

I fired up LLDB, and set up a breakpoint in renderer.process.eval_test in the Tera source code

fn eval_test(&mut self, test: &'a Test) -> Result<bool> {
    let tester_fn = self.tera.get_tester(&test.name)?;
    let err_wrap = |e| Error::call_test(&test.name, e);

    let mut tester_args = vec![];
    for arg in &test.args {
        tester_args
            .push(self.safe_eval_expression(arg).map_err(err_wrap)?.clone().into_owned());
    }

    let found = self.lookup_ident(&test.ident).map(|found| found.clone().into_owned()).ok();

    let result = tester_fn.test(found.as_ref(), &tester_args).map_err(err_wrap)?;
    if test.negated {
        Ok(!result)
    } else {
        Ok(result)
    }
}

and it appears that the found variable is Some{...} instead of None, which is not what I expect it to be. Is this the normal behavior or a bug ?

Thanks for any help on this :) and great work on Tera by the way !

LeBoucEtMistere avatar Jan 21 '22 14:01 LeBoucEtMistere

If I understood correctly, it's the expected behaviour. y is defined as None, {%- if foo.x.z is undefined %} should throw though, assuming there's no field named z.

Keats avatar Jan 21 '22 15:01 Keats

So a None value is considered as a defined one ? In that case, is there some way to check whether a value is Some or None from the template ? I haven't seen any is_null check or anything similar in the Tera docs

LeBoucEtMistere avatar Jan 21 '22 15:01 LeBoucEtMistere

I will need to double-check, the undefined test code seem like it should fail on that but I haven't looked at that part of the code in a long time.

Keats avatar Jan 21 '22 15:01 Keats

I ran into what I think is a similar/the same issue. From what I have observed the defined tester function will only evaluate to false if the argument being checked doesn't exist in the Context used to render the template, and the inverse being true for undefined. That is, calling defined with Some(_) | None will evaluate to true, and will only be false if the Context doesn't have the key being tested.

To get around this I registered a custom tester function

fn is_null(value: Option<&Value>, _args: &[Value]) -> tera::Result<bool> {
    match value {
        Some(Value::Null) | None => Ok(true),
        _ => Ok(false),
    }
}

Personally, I can see the current behavior of the defined and undefined testers being useful. But then I would expect there to be analogous null and notnull testers implemented similarly to the custom is_null tester mentioned above.

joshleeb avatar Jun 11 '22 03:06 joshleeb