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

Allow filter chains to start with a missing variable #496

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 49 additions & 4 deletions crates/core/src/error/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,50 @@ pub struct Error {
inner: Box<InnerError>,
}

impl Error {
/// Identifies the underlying kind for this error
pub fn kind(&self) -> &ErrorKind {
&self.inner.kind
}
}

// Guts of `Error` here to keep `Error`'s memory size small to avoid bloating the size of
// `Result<T>` in the success case and spilling over from register-based returns to stack-based
// returns. There are already enough memory allocations below, one more
// shouldn't hurt.
#[derive(Debug, Clone)]
struct InnerError {
msg: crate::model::KString,
kind: ErrorKind,
user_backtrace: Vec<Trace>,
cause: Option<BoxedError>,
}

impl Error {
/// Create a new compiler error with the given message
/// Create an error that identifies the provided variable as unknown
pub fn unknown_variable<S: Into<crate::model::KString>>(name: S) -> Self {
Self::with_kind(ErrorKind::UnknownVariable).context("requested variable", name)
}

/// Create a new error of the given kind
pub fn with_kind(kind: ErrorKind) -> Self {
let error = InnerError {
kind,
user_backtrace: vec![Trace::empty()],
cause: None,
};
Self {
inner: Box::new(error),
}
}

/// Create a new custom error with the given message
pub fn with_msg<S: Into<crate::model::KString>>(msg: S) -> Self {
Self::with_msg_cow(msg.into())
}

fn with_msg_cow(msg: crate::model::KString) -> Self {
let error = InnerError {
msg,
kind: ErrorKind::Custom(msg),
user_backtrace: vec![Trace::empty()],
cause: None,
};
Expand Down Expand Up @@ -108,7 +132,7 @@ const ERROR_DESCRIPTION: &str = "liquid";

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}: {}", ERROR_DESCRIPTION, self.inner.msg)?;
writeln!(f, "{}: {}", ERROR_DESCRIPTION, self.inner.kind)?;
for trace in &self.inner.user_backtrace {
if let Some(trace) = trace.get_trace() {
writeln!(f, "from: {}", trace)?;
Expand All @@ -129,3 +153,24 @@ impl error::Error for Error {
self.inner.cause.as_ref().and_then(|e| e.source())
}
}

/// The type of an error.
#[derive(Debug, Clone)]
pub enum ErrorKind {
/// A variable was being indexed but the desired index did not exist
UnknownIndex,
/// A referenced variable did not exist
UnknownVariable,
/// A custom error with no discernible kind
Custom(crate::model::KString),
}

impl std::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorKind::UnknownIndex => f.write_str("Unknown index"),
ErrorKind::UnknownVariable => f.write_str("Unknown variable"),
ErrorKind::Custom(s) => s.fmt(f),
}
}
}
2 changes: 1 addition & 1 deletion crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub mod parser;
pub mod partials;
pub mod runtime;

pub use error::{Error, Result};
pub use error::{Error, ErrorKind, Result};
#[cfg(feature = "derive")]
#[doc(hidden)]
pub use liquid_derive::{
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/model/find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ pub fn find<'o>(value: &'o dyn ValueView, path: &[ScalarCow<'_>]) -> Result<Valu
Vec::new()
};
let available = itertools::join(available.iter(), ", ");
return Error::with_msg("Unknown index")
return Error::with_kind(crate::ErrorKind::UnknownIndex)
.context("variable", subpath)
.context("requested index", format!("{}", requested.render()))
.context("available indexes", available)
Expand Down
30 changes: 20 additions & 10 deletions crates/core/src/parser/filter_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::model::{ValueCow, ValueView};
use crate::runtime::Expression;
use crate::runtime::Renderable;
use crate::runtime::Runtime;
use crate::{ErrorKind, Value};

/// A `Value` expression.
#[derive(Debug)]
Expand All @@ -24,19 +25,28 @@ impl FilterChain {
/// Process `Value` expression within `runtime`'s stack.
pub fn evaluate<'s>(&'s self, runtime: &'s dyn Runtime) -> Result<ValueCow<'s>> {
// take either the provided value or the value from the provided variable
let mut entry = self.entry.evaluate(runtime)?;
let mut entry = match self.entry.evaluate(runtime) {
Ok(v) => v,
Err(err) if self.filters.is_empty() => return Err(err),
Err(err) => match err.kind() {
// a missing value is not an error if there are filters
// that come next to process it. They will decide if this
// is appropriate or not (eg: the default filter)
ErrorKind::UnknownIndex | ErrorKind::UnknownVariable => ValueCow::Owned(Value::Nil),
_ => return Err(err),
},
};

// apply all specified filters
for filter in &self.filters {
entry = ValueCow::Owned(
filter
.evaluate(entry.as_view(), runtime)
.trace("Filter error")
.context_key("filter")
.value_with(|| format!("{}", filter).into())
.context_key("input")
.value_with(|| format!("{}", entry.source()).into())?,
);
entry = filter
.evaluate(entry.as_view(), runtime)
.trace("Filter error")
.context_key("filter")
.value_with(|| format!("{}", filter).into())
.context_key("input")
.value_with(|| format!("{}", entry.source()).into())
.map(ValueCow::Owned)?;
}

Ok(entry)
Expand Down
2 changes: 1 addition & 1 deletion crates/core/src/parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ impl<'a> TagTokenIter<'a> {
::pest::error::ErrorVariant::CustomError {
message: error_msg.to_string(),
},
self.position,
self.position.to_owned(),
);
convert_pest_error(pest_error)
}
Expand Down
4 changes: 1 addition & 3 deletions crates/core/src/runtime/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,7 @@ impl<'g> Runtime for RuntimeCore<'g> {

fn get(&self, path: &[ScalarCow<'_>]) -> Result<ValueCow<'_>> {
let key = path.first().cloned().unwrap_or_else(|| Scalar::new("nil"));
Error::with_msg("Unknown variable")
.context("requested variable", key.to_kstr())
.into_err()
Error::unknown_variable(key.to_kstr()).into_err()
}

fn set_global(
Expand Down
12 changes: 3 additions & 9 deletions crates/core/src/runtime/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,7 @@ impl<P: super::Runtime, O: ObjectView> super::Runtime for StackFrame<P, O> {
}

fn get(&self, path: &[ScalarCow<'_>]) -> Result<ValueCow<'_>> {
let key = path.first().ok_or_else(|| {
Error::with_msg("Unknown variable").context("requested variable", "nil")
})?;
let key = path.first().ok_or_else(|| Error::unknown_variable("nil"))?;
let key = key.to_kstr();
let data = &self.data;
if data.contains_key(key.as_str()) {
Expand Down Expand Up @@ -130,9 +128,7 @@ impl<P: super::Runtime> super::Runtime for GlobalFrame<P> {
}

fn get(&self, path: &[ScalarCow<'_>]) -> Result<ValueCow<'_>> {
let key = path.first().ok_or_else(|| {
Error::with_msg("Unknown variable").context("requested variable", "nil")
})?;
let key = path.first().ok_or_else(|| Error::unknown_variable("nil"))?;
let key = key.to_kstr();
let data = self.data.borrow();
if data.contains_key(key.as_str()) {
Expand Down Expand Up @@ -205,9 +201,7 @@ impl<P: super::Runtime> super::Runtime for IndexFrame<P> {
}

fn get(&self, path: &[ScalarCow<'_>]) -> Result<ValueCow<'_>> {
let key = path.first().ok_or_else(|| {
Error::with_msg("Unknown variable").context("requested variable", "nil")
})?;
let key = path.first().ok_or_else(|| Error::unknown_variable("nil"))?;
let key = key.to_kstr();
let data = self.data.borrow();
if data.contains_key(key.as_str()) {
Expand Down
29 changes: 29 additions & 0 deletions tests/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,3 +539,32 @@ fn test_compact() {
let output = template.render(&globals).unwrap();
assert_eq!(output, "A C".to_string());
}

#[test]
fn indexed_variable_default() {
let text = "{{ does.not.exist | default: 'default' }}";
let globals = liquid::object!({});

let template = liquid::ParserBuilder::with_stdlib()
.build()
.unwrap()
.parse(text)
.unwrap();
let output = template.render(&globals).unwrap();
assert_eq!(output, "default".to_string());
}

#[test]
fn indexed_variable_no_default() {
let text = "{{ does.not.exist }}";
let globals = liquid::object!({});

let template = liquid::ParserBuilder::with_stdlib()
.build()
.unwrap()
.parse(text)
.unwrap();
template
.render(&globals)
.expect_err("should fail when the filter ends with a missing value");
}