-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Revamp Filters API): Create liquid-derive crate
Crate that provides macros to reduce boilerplate in the creation of filters, as well as reduce the burden of error handling.
- Loading branch information
1 parent
7a7de4b
commit c05525d
Showing
13 changed files
with
1,979 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.