Skip to content

Commit

Permalink
feat(Revamp Filters API): Create liquid-derive crate
Browse files Browse the repository at this point in the history
Crate that provides macros to reduce boilerplate in the creation of filters, as well as reduce the burden of error handling.
  • Loading branch information
Goncalerta committed Feb 17, 2019
1 parent 7a7de4b commit c05525d
Show file tree
Hide file tree
Showing 13 changed files with 1,979 additions and 1 deletion.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["liquid-error", "liquid-value", "liquid-compiler", "liquid-interpreter"]
members = ["liquid-error", "liquid-value", "liquid-compiler", "liquid-interpreter", "liquid-derive"]

[package]
name = "liquid"
Expand Down Expand Up @@ -44,6 +44,7 @@ liquid-error = { version = "0.18", path = "liquid-error" }
liquid-value = { version = "0.18", path = "liquid-value" }
liquid-compiler = { version = "0.18", path = "liquid-compiler" }
liquid-interpreter = { version = "0.18", path = "liquid-interpreter" }
liquid-derive = { version = "0.18", path = "liquid-derive" }

serde = { version = "1.0", optional = true, features = ["derive"] }
clap = { version = "2.26", optional = true }
Expand Down
27 changes: 27 additions & 0 deletions liquid-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "liquid-derive"
version = "0.18.0"
authors = ["Pedro Gonçalo Correia <[email protected]>"]
description = "The liquid templating language for Rust"
readme = "README.md"
categories = ["template-engine"]
keywords = ["liquid", "template", "templating", "language", "html"]
license = "MIT"

[lib]
proc-macro = true

[badges]
travis-ci = { repository = "cobalt-org/liquid-rust" }
appveyor = { repository = "johannhof/liquid-rust" }

[dependencies]
syn = "0.15"
proc-quote = "0.1"
proc-macro2 = "0.4.27"

# Exposed in API
liquid-error = { version = "0.18", path = "../liquid-error" }
liquid-value = { version = "0.18", path = "../liquid-value" }
liquid-interpreter = { version = "0.18", path = "../liquid-interpreter" }
liquid-compiler = { version = "0.18", path = "../liquid-compiler" }
26 changes: 26 additions & 0 deletions liquid-derive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
liquid-derive
===========

> [Liquid templating](http://liquidmarkup.org/) for Rust
[![Travis Status](https://travis-ci.org/cobalt-org/liquid-rust.svg?branch=master)](https://travis-ci.org/cobalt-org/liquid-rust)
[![Appveyor Status](https://ci.appveyor.com/api/projects/status/n1nqaitd5uja8tsi/branch/master?svg=true)](https://ci.appveyor.com/project/johannhof/liquid-rust/branch/master)
[![Crates Status](https://img.shields.io/crates/v/liquid.svg)](https://crates.io/crates/liquid)
[![Coverage Status](https://coveralls.io/repos/github/cobalt-org/liquid-rust/badge.svg?branch=master)](https://coveralls.io/github/cobalt-org/liquid-rust?branch=master)
[![Dependency Status](https://dependencyci.com/github/cobalt-org/liquid-rust/badge)](https://dependencyci.com/github/cobalt-org/liquid-rust)

Usage
----------

To include liquid in your project add the following to your Cargo.toml:

```toml
[dependencies]
liquid-derive = "0.18"
```

Now you can use the crate in your code:

```rust
extern crate liquid_derive;
```
188 changes: 188 additions & 0 deletions liquid-derive/src/filter/display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
use helpers::*;
use proc_macro2::*;
use proc_quote::*;
use syn::*;

/// Struct that contains information about the `Filter` struct to generate the
/// necessary code for `Display`.
struct FilterStruct<'a> {
name: &'a Ident,
filter_name: String,
parameters: Option<Parameters<'a>>,
generics: &'a Generics,
}

/// The field that holds `FilterParameters`.
enum Parameters<'a> {
Ident(&'a Ident),
Pos(usize),
}

impl<'a> Parameters<'a> {
/// Creates a new `Parameters` from the given `ident` (if it is
/// a struct with named fields) or the given position of the field
/// (in case of unnamed parameters).
fn new(ident: Option<&'a Ident>, pos: usize) -> Self {
match ident {
Some(ident) => Parameters::Ident(ident),
None => Parameters::Pos(pos),
}
}
}

impl<'a> ToTokens for Parameters<'a> {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Parameters::Ident(ident) => ident.to_tokens(tokens),
Parameters::Pos(pos) => pos.to_tokens(tokens),
}
}
}

impl<'a> FilterStruct<'a> {
/// Generates `impl` declaration of the given trait for the structure
/// represented by `self`.
fn generate_impl(&self, trait_name: TokenStream) -> TokenStream {
let name = &self.name;
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
quote! {
impl #impl_generics #trait_name for #name #ty_generics #where_clause
}
}

/// Searches for `#[name(...)]` in order to parse `filter_name`.
fn parse_attrs(attrs: &Vec<Attribute>) -> Result<String> {
let mut evaluated_attrs = attrs.iter().filter(|attr| attr.path.is_ident("name"));

match (evaluated_attrs.next(), evaluated_attrs.next()) {
(Some(attr), None) => Self::parse_name_attr(attr),

(_, Some(attr)) => Err(Error::new_spanned(
attr,
"Found multiple definitions for `name` attribute.",
)),

_ => Err(Error::new(
Span::call_site(),
"Cannot find `name` attribute in target struct. Have you tried adding `#[name = \"...\"]`?",
)),
}
}

/// Parses `#[name(...)]` attribute.
fn parse_name_attr(attr: &Attribute) -> Result<String> {
let meta = attr.parse_meta().map_err(|err| {
Error::new(
err.span(),
format!("Could not parse `evaluated` attribute: {}", err),
)
})?;

if let Meta::NameValue(meta) = meta {
if let Lit::Str(name) = &meta.lit {
Ok(name.value())
} else {
Err(Error::new_spanned(&meta.lit, "Expected string literal."))
}
} else {
Err(Error::new_spanned(
meta,
"Couldn't parse evaluated attribute. Have you tried `#[evaluated(\"...\")]`?",
))
}
}

/// Tries to create a new `FilterStruct` from the given `DeriveInput`
fn from_input(input: &'a DeriveInput) -> Result<Self> {
let DeriveInput {
ident,
generics,
data,
attrs,
..
} = &input;
let mut parameters = AssignOnce::Unset;

let fields = match data {
Data::Struct(data) => &data.fields,
Data::Enum(data) => {
return Err(Error::new_spanned(
data.enum_token,
"Filters cannot be `enum`s.",
));
}
Data::Union(data) => {
return Err(Error::new_spanned(
data.union_token,
"Filters cannot be `union`s.",
));
}
};

let marked = fields.iter().enumerate().filter(|(_, field)| {
field
.attrs
.iter()
.any(|attr| attr.path.is_ident("parameters"))
});

for (i, field) in marked {
let params = Parameters::new(field.ident.as_ref(), i);
parameters.set(params, || Error::new_spanned(
field,
"A previous field was already marked as `parameters`. Only one field can be marked as so.",
))?;
}

let name = ident;
let filter_name = Self::parse_attrs(attrs)?;
let parameters = parameters.to_option();

Ok(Self {
name,
filter_name,
parameters,
generics,
})
}
}

/// Generates implementation of `Display`.
fn generate_impl_display(filter: &FilterStruct) -> TokenStream {
let FilterStruct {
filter_name,
parameters,
..
} = &filter;

let impl_display = filter.generate_impl(quote! { ::std::fmt::Display });

if let Some(parameters) = parameters {
quote! {
#impl_display {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::std::write!(f, "{} : {}", #filter_name, &self.#parameters)
}
}
}
} else {
quote! {
#impl_display {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::std::write!(f, "{}", #filter_name)
}
}
}
}
}

pub fn derive(input: &DeriveInput) -> TokenStream {
let filter = match FilterStruct::from_input(input) {
Ok(filter) => filter,
Err(err) => return err.to_compile_error(),
};

let output = generate_impl_display(&filter);

output
}
Loading

0 comments on commit c05525d

Please sign in to comment.