Skip to content

Commit

Permalink
feat(plugins): Support arbitrary state
Browse files Browse the repository at this point in the history
With the new `registers`, plugins (tags, blocks) can be statful like
`cycle` and `ifchanged`.

This state is as private as your `struct State`.

BREAKING CHANGE: stuff removed from `Context`.
  • Loading branch information
epage committed Dec 10, 2018
1 parent 341e931 commit 033c9b7
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 89 deletions.
1 change: 1 addition & 0 deletions liquid-interpreter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ appveyor = { repository = "johannhof/liquid-rust" }

[dependencies]
itertools = "0.7.0"
anymap = "0.12"
# Exposed in API
liquid-error = { version = "0.16", path = "../liquid-error" }
liquid-value = { version = "0.17", path = "../liquid-value" }
Expand Down
105 changes: 21 additions & 84 deletions liquid-interpreter/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
use std::collections::HashMap;
use std::sync;

use anymap;
use error::{Error, Result};
use itertools;
use value::Value;

use super::Expression;
use super::Stack;
use super::ValueStore;
use super::PluginRegistry;
Expand Down Expand Up @@ -60,71 +58,6 @@ impl InterruptState {
}
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct CycleStateInner {
// The indices of all the cycles encountered during rendering.
cycles: HashMap<String, usize>,
}

impl CycleStateInner {
fn cycle_index(&mut self, name: &str, max: usize) -> usize {
let i = self.cycles.entry(name.to_owned()).or_insert(0);
let j = *i;
*i = (*i + 1) % max;
j
}
}

/// See `cycle` tag.
pub struct CycleState<'a, 'g>
where
'g: 'a,
{
context: &'a mut Context<'g>,
}

impl<'a, 'g> CycleState<'a, 'g> {
/// See `cycle` tag.
pub fn cycle_element<'c>(
&'c mut self,
name: &str,
values: &'c [Expression],
) -> Result<&'c Value> {
let index = self.context.cycles.cycle_index(name, values.len());
if index >= values.len() {
return Err(Error::with_msg(
"cycle index out of bounds, most likely from mismatched cycles",
)
.context("index", format!("{}", index))
.context("count", format!("{}", values.len())));
}

let val = values[index].evaluate(self.context)?;
Ok(val)
}
}

/// Remembers the content of the last rendered `ifstate` block.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct IfChangedState {
last_rendered: Option<String>,
}

impl IfChangedState {
/// Checks whether or not a new rendered `&str` is different from
/// `last_rendered` and updates `last_rendered` value to the new value.
pub fn has_changed(&mut self, rendered: &str) -> bool {
let has_changed = if let Some(last_rendered) = &self.last_rendered {
last_rendered != rendered
} else {
true
};
self.last_rendered = Some(rendered.to_owned());

has_changed
}
}

/// Create processing context for a template.
pub struct ContextBuilder<'g> {
globals: Option<&'g ValueStore>,
Expand Down Expand Up @@ -160,9 +93,8 @@ impl<'g> ContextBuilder<'g> {
};
Context {
stack,
registers: anymap::AnyMap::new(),
interrupt: InterruptState::default(),
cycles: CycleStateInner::default(),
ifchanged: IfChangedState::default(),
filters: self.filters,
}
}
Expand All @@ -175,13 +107,11 @@ impl<'g> Default for ContextBuilder<'g> {
}

/// Processing context for a template.
#[derive(Default)]
pub struct Context<'g> {
stack: Stack<'g>,

registers: anymap::AnyMap,
interrupt: InterruptState,
cycles: CycleStateInner,
ifchanged: IfChangedState,

filters: sync::Arc<PluginRegistry<BoxedValueFilter>>,
}
Expand Down Expand Up @@ -220,17 +150,12 @@ impl<'g> Context<'g> {
&mut self.interrupt
}

/// See `cycle` tag.
pub fn cycles<'a>(&'a mut self) -> CycleState<'a, 'g>
where
'g: 'a,
{
CycleState { context: self }
}

/// Access the block's `IfChangedState`.
pub fn ifchanged(&mut self) -> &mut IfChangedState {
&mut self.ifchanged
/// Data store for stateful tags/blocks.
///
/// If a plugin needs state, it creates a `struct State : Default` and accesses it via
/// `get_register_mut`.
pub fn get_register_mut<T: anymap::any::IntoBox<anymap::any::Any> + Default>(&mut self) -> &mut T {
self.registers.entry::<T>().or_insert_with(|| Default::default())
}

/// Access the current `Stack`.
Expand Down Expand Up @@ -260,10 +185,22 @@ impl<'g> Context<'g> {
}
}

impl<'g> Default for Context<'g> {
fn default() -> Self {
Self {
stack: Stack::empty(),
registers: anymap::AnyMap::new(),
interrupt: InterruptState::default(),
filters: Default::default(),
}
}
}

#[cfg(test)]
mod test {
use super::*;

use value::Value;
use value::Scalar;

#[test]
Expand Down
1 change: 1 addition & 0 deletions liquid-interpreter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#![warn(missing_docs)]
#![warn(unused_extern_crates)]

extern crate anymap;
extern crate itertools;
extern crate liquid_error;
extern crate liquid_value;
Expand Down
39 changes: 35 additions & 4 deletions src/tags/cycle_tag.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::collections::HashMap;
use std::io::Write;

use itertools;
use liquid_error::{Result, ResultLiquidChainExt, ResultLiquidExt};
use liquid_error::{Error, Result, ResultLiquidChainExt, ResultLiquidExt};

use compiler::LiquidOptions;
use compiler::TagToken;
Expand All @@ -28,9 +29,11 @@ impl Cycle {

impl Renderable for Cycle {
fn render_to(&self, writer: &mut Write, context: &mut Context) -> Result<()> {
let mut cycles = context.cycles();
let value = cycles
.cycle_element(&self.name, &self.values)
let expr = context
.get_register_mut::<State>()
.cycle(&self.name, &self.values)
.trace_with(|| self.trace().into())?;
let value = expr.evaluate(context)
.trace_with(|| self.trace().into())?;
write!(writer, "{}", value).chain("Failed to render")?;
Ok(())
Expand Down Expand Up @@ -100,6 +103,34 @@ pub fn cycle_tag(
parse_cycle(arguments, options).map(|opt| Box::new(opt) as Box<Renderable>)
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct State {
// The indices of all the cycles encountered during rendering.
cycles: HashMap<String, usize>,
}

impl State{
fn cycle<'e>(& mut self, name: &str, values: &'e [Expression]) -> Result<&'e Expression> {
let index = self.cycle_index(name, values.len());
if index >= values.len() {
return Err(Error::with_msg(
"cycle index out of bounds, most likely from mismatched cycles",
)
.context("index", format!("{}", index))
.context("count", format!("{}", values.len())));
}

Ok(&values[index])
}

fn cycle_index(&mut self, name: &str, max: usize) -> usize {
let i = self.cycles.entry(name.to_owned()).or_insert(0);
let j = *i;
*i = (*i + 1) % max;
j
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
23 changes: 22 additions & 1 deletion src/tags/ifchanged_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl Renderable for IfChanged {
.trace_with(|| self.trace().into())?;

let rendered = String::from_utf8(rendered).expect("render only writes UTF-8");
if context.ifchanged().has_changed(&rendered) {
if context.get_register_mut::<State>().has_changed(&rendered) {
write!(writer, "{}", rendered).chain("Failed to render")?;
}

Expand All @@ -51,6 +51,27 @@ pub fn ifchanged_block(
Ok(Box::new(IfChanged { if_changed }))
}

/// Remembers the content of the last rendered `ifstate` block.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct State {
last_rendered: Option<String>,
}

impl State {
/// Checks whether or not a new rendered `&str` is different from
/// `last_rendered` and updates `last_rendered` value to the new value.
fn has_changed(&mut self, rendered: &str) -> bool {
let has_changed = if let Some(last_rendered) = &self.last_rendered {
last_rendered != rendered
} else {
true
};
self.last_rendered = Some(rendered.to_owned());

has_changed
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down

0 comments on commit 033c9b7

Please sign in to comment.