Skip to content

Commit

Permalink
feat(partials): New caching policies
Browse files Browse the repository at this point in the history
  • Loading branch information
epage committed Dec 27, 2018
1 parent e18c68e commit d2ba7a6
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use tags;
/// Storage for partial-templates.
///
/// This is the recommended policy. See `liquid::partials` for more options.
pub type Partials = partials::OnDemandCompiler<partials::InMemorySource>;
pub type Partials = partials::EagerCompiler<partials::InMemorySource>;

pub struct ParserBuilder<P = Partials>
where
Expand Down
140 changes: 140 additions & 0 deletions src/partials/eager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
use std::collections::HashMap;
use std::fmt;
use std::sync;

use liquid_compiler;
use liquid_compiler::Language;
use liquid_error::Error;
use liquid_error::Result;
use liquid_interpreter;
use liquid_interpreter::PartialStore;
use liquid_interpreter::Renderable;

use super::PartialCompiler;
use super::PartialSource;

/// An eagerly-caching compiler for `PartialSource`.
///
/// This would be useful in cases where:
/// - Most partial-templates are used
/// - Of the used partial-templates, they are generally used many times.
///
/// Note: partial-compilation error reporting is deferred to render-time so content can still be
/// generated even when the content is in an intermediate-state.
#[derive(Debug)]
pub struct EagerCompiler<S: PartialSource> {
source: S,
}

impl<S> EagerCompiler<S>
where
S: PartialSource,
{
/// Create an on-demand compiler for `PartialSource`.
pub fn new(source: S) -> Self {
EagerCompiler { source }
}
}

impl<S> EagerCompiler<S>
where
S: PartialSource + Default,
{
/// Create an empty compiler for `PartialSource`.
pub fn empty() -> Self {
Default::default()
}
}

impl<S> Default for EagerCompiler<S>
where
S: PartialSource + Default,
{
fn default() -> Self {
Self {
source: Default::default(),
}
}
}

impl<S> ::std::ops::Deref for EagerCompiler<S>
where
S: PartialSource,
{
type Target = S;

fn deref(&self) -> &S {
&self.source
}
}

impl<S> ::std::ops::DerefMut for EagerCompiler<S>
where
S: PartialSource,
{
fn deref_mut(&mut self) -> &mut S {
&mut self.source
}
}

impl<S> PartialCompiler for EagerCompiler<S>
where
S: PartialSource + Send + Sync + 'static,
{
fn compile(self, language: sync::Arc<Language>) -> Result<Box<PartialStore + Send + Sync>> {
let store: HashMap<_, _> = self
.source
.names()
.into_iter()
.map(|name| {
let source = self.source.get(name).and_then(|s| {
liquid_compiler::parse(s.as_ref(), &language)
.map(liquid_interpreter::Template::new)
.map(|t| {
let t: sync::Arc<liquid_interpreter::Renderable> = sync::Arc::new(t);
t
})
});
(name.to_owned(), source)
})
.collect();
let store = EagerStore { store };
Ok(Box::new(store))
}
}

struct EagerStore {
store: HashMap<String, Result<sync::Arc<liquid_interpreter::Renderable>>>,
}

impl PartialStore for EagerStore {
fn contains(&self, name: &str) -> bool {
self.store.contains_key(name)
}

fn names(&self) -> Vec<&str> {
self.store.keys().map(|s| s.as_str()).collect()
}

fn try_get(&self, name: &str) -> Option<sync::Arc<Renderable>> {
self.store.get(name).and_then(|r| r.clone().ok())
}

fn get(&self, name: &str) -> Result<sync::Arc<Renderable>> {
let result = self.store.get(name).ok_or_else(|| {
let mut available: Vec<_> = self.names();
available.sort_unstable();
let available = itertools::join(available, ", ");
Error::with_msg("Unknown partial-template")
.context("requested partial", name.to_owned())
.context("available partials", available)
})?;
result.clone()
}
}

impl fmt::Debug for EagerStore {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.names().fmt(f)
}
}
161 changes: 161 additions & 0 deletions src/partials/lazy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
use std::collections::HashMap;
use std::fmt;
use std::sync;

use liquid_compiler;
use liquid_compiler::Language;
use liquid_error::Result;
use liquid_interpreter;
use liquid_interpreter::PartialStore;
use liquid_interpreter::Renderable;

use super::PartialCompiler;
use super::PartialSource;

/// An lazily-caching compiler for `PartialSource`.
///
/// This would be useful in cases where:
/// - Most partial-templates aren't used
/// - Of the used partial-templates, they are generally used many times.
///
/// Note: partial-compilation error reporting is deferred to render-time so content can still be
/// generated even when the content is in an intermediate-state.
#[derive(Debug)]
pub struct LazyCompiler<S: PartialSource> {
source: S,
}

impl<S> LazyCompiler<S>
where
S: PartialSource,
{
/// Create an on-demand compiler for `PartialSource`.
pub fn new(source: S) -> Self {
LazyCompiler { source }
}
}

impl<S> LazyCompiler<S>
where
S: PartialSource + Default,
{
/// Create an empty compiler for `PartialSource`.
pub fn empty() -> Self {
Default::default()
}
}

impl<S> Default for LazyCompiler<S>
where
S: PartialSource + Default,
{
fn default() -> Self {
Self {
source: Default::default(),
}
}
}

impl<S> ::std::ops::Deref for LazyCompiler<S>
where
S: PartialSource,
{
type Target = S;

fn deref(&self) -> &S {
&self.source
}
}

impl<S> ::std::ops::DerefMut for LazyCompiler<S>
where
S: PartialSource,
{
fn deref_mut(&mut self) -> &mut S {
&mut self.source
}
}

impl<S> PartialCompiler for LazyCompiler<S>
where
S: PartialSource + Send + Sync + 'static,
{
fn compile(self, language: sync::Arc<Language>) -> Result<Box<PartialStore + Send + Sync>> {
let store = LazyStore {
language: language,
source: self.source,
cache: sync::Mutex::new(Default::default()),
};
Ok(Box::new(store))
}
}

struct LazyStore<S: PartialSource> {
language: sync::Arc<Language>,
source: S,
cache: sync::Mutex<HashMap<String, Result<sync::Arc<liquid_interpreter::Renderable>>>>,
}

impl<S> LazyStore<S>
where
S: PartialSource,
{
fn try_get_or_create(&self, name: &str) -> Option<sync::Arc<Renderable>> {
let cache = self.cache.lock().expect("not to be poisoned and reused");
if let Some(result) = cache.get(name) {
result.as_ref().ok().cloned()
} else {
let s = self.source.try_get(name)?;
let s = s.as_ref();
let template = liquid_compiler::parse(s, &self.language)
.map(liquid_interpreter::Template::new)
.map(sync::Arc::new)
.ok()?;
Some(template)
}
}

fn get_or_create(&self, name: &str) -> Result<sync::Arc<Renderable>> {
let cache = self.cache.lock().expect("not to be poisoned and reused");
if let Some(result) = cache.get(name) {
result.clone()
} else {
let s = self.source.get(name)?;
let s = s.as_ref();
let template = liquid_compiler::parse(s, &self.language)
.map(liquid_interpreter::Template::new)
.map(sync::Arc::new)?;
Ok(template)
}
}
}

impl<S> PartialStore for LazyStore<S>
where
S: PartialSource,
{
fn contains(&self, name: &str) -> bool {
self.source.contains(name)
}

fn names(&self) -> Vec<&str> {
self.source.names()
}

fn try_get(&self, name: &str) -> Option<sync::Arc<Renderable>> {
self.try_get_or_create(name)
}

fn get(&self, name: &str) -> Result<sync::Arc<Renderable>> {
self.get_or_create(name)
}
}

impl<S> fmt::Debug for LazyStore<S>
where
S: PartialSource,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.source.fmt(f)
}
}
4 changes: 4 additions & 0 deletions src/partials/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ use liquid_error::Error;
use liquid_error::Result;
use liquid_interpreter::PartialStore;

mod eager;
mod inmemory;
mod lazy;
mod ondemand;

pub use self::eager::*;
pub use self::inmemory::*;
pub use self::lazy::*;
pub use self::ondemand::*;

/// Compile a `PartialSource` into a `PartialStore` of `Renderable`s.
Expand Down

0 comments on commit d2ba7a6

Please sign in to comment.