-
Notifications
You must be signed in to change notification settings - Fork 76
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #129 from greyblake/customize-fields
Support customization of fields on derive
- Loading branch information
Showing
8 changed files
with
409 additions
and
80 deletions.
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
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
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,117 @@ | ||
use proc_macro2::{Group, Span, TokenStream, TokenTree}; | ||
use quote::quote; | ||
use syn::{spanned::Spanned, *}; | ||
|
||
/// Used to filter out necessary field attribute and within error messages. | ||
static ARBITRARY_ATTRIBUTE_NAME: &str = "arbitrary"; | ||
|
||
/// Determines how a value for a field should be constructed. | ||
#[cfg_attr(test, derive(Debug))] | ||
pub enum FieldConstructor { | ||
/// Assume that Arbitrary is defined for the type of this field and use it (default) | ||
Arbitrary, | ||
|
||
/// Places `Default::default()` as a field value. | ||
Default, | ||
|
||
/// Use custom function or closure to generate a value for a field. | ||
With(TokenStream), | ||
|
||
/// Set a field always to the given value. | ||
Value(TokenStream), | ||
} | ||
|
||
pub fn determine_field_constructor(field: &Field) -> Result<FieldConstructor> { | ||
let opt_attr = fetch_attr_from_field(field)?; | ||
let ctor = match opt_attr { | ||
Some(attr) => parse_attribute(attr)?, | ||
None => FieldConstructor::Arbitrary, | ||
}; | ||
Ok(ctor) | ||
} | ||
|
||
fn fetch_attr_from_field(field: &Field) -> Result<Option<&Attribute>> { | ||
let found_attributes: Vec<_> = field | ||
.attrs | ||
.iter() | ||
.filter(|a| { | ||
let path = &a.path; | ||
let name = quote!(#path).to_string(); | ||
name == ARBITRARY_ATTRIBUTE_NAME | ||
}) | ||
.collect(); | ||
if found_attributes.len() > 1 { | ||
let name = field.ident.as_ref().unwrap(); | ||
let msg = format!( | ||
"Multiple conflicting #[{ARBITRARY_ATTRIBUTE_NAME}] attributes found on field `{name}`" | ||
); | ||
return Err(syn::Error::new(field.span(), msg)); | ||
} | ||
Ok(found_attributes.into_iter().next()) | ||
} | ||
|
||
fn parse_attribute(attr: &Attribute) -> Result<FieldConstructor> { | ||
let group = { | ||
let mut tokens_iter = attr.clone().tokens.into_iter(); | ||
let token = tokens_iter.next().ok_or_else(|| { | ||
let msg = format!("#[{ARBITRARY_ATTRIBUTE_NAME}] cannot be empty."); | ||
syn::Error::new(attr.span(), msg) | ||
})?; | ||
match token { | ||
TokenTree::Group(g) => g, | ||
t => { | ||
let msg = format!("#[{ARBITRARY_ATTRIBUTE_NAME}] must contain a group, got: {t})"); | ||
return Err(syn::Error::new(attr.span(), msg)); | ||
} | ||
} | ||
}; | ||
parse_attribute_internals(group) | ||
} | ||
|
||
fn parse_attribute_internals(group: Group) -> Result<FieldConstructor> { | ||
let stream = group.stream(); | ||
let mut tokens_iter = stream.into_iter(); | ||
let token = tokens_iter.next().ok_or_else(|| { | ||
let msg = format!("#[{ARBITRARY_ATTRIBUTE_NAME}] cannot be empty."); | ||
syn::Error::new(group.span(), msg) | ||
})?; | ||
match token.to_string().as_ref() { | ||
"default" => Ok(FieldConstructor::Default), | ||
"with" => { | ||
let func_path = parse_assigned_value("with", tokens_iter, group.span())?; | ||
Ok(FieldConstructor::With(func_path)) | ||
} | ||
"value" => { | ||
let value = parse_assigned_value("value", tokens_iter, group.span())?; | ||
Ok(FieldConstructor::Value(value)) | ||
} | ||
_ => { | ||
let msg = format!("Unknown option for #[{ARBITRARY_ATTRIBUTE_NAME}]: `{token}`"); | ||
Err(syn::Error::new(token.span(), msg)) | ||
} | ||
} | ||
} | ||
|
||
// Input: | ||
// = 2 + 2 | ||
// Output: | ||
// 2 + 2 | ||
fn parse_assigned_value( | ||
opt_name: &str, | ||
mut tokens_iter: impl Iterator<Item = TokenTree>, | ||
default_span: Span, | ||
) -> Result<TokenStream> { | ||
let eq_sign = tokens_iter.next().ok_or_else(|| { | ||
let msg = format!( | ||
"Invalid syntax for #[{ARBITRARY_ATTRIBUTE_NAME}], `{opt_name}` is missing assignment." | ||
); | ||
syn::Error::new(default_span, msg) | ||
})?; | ||
|
||
if eq_sign.to_string() == "=" { | ||
Ok(tokens_iter.collect()) | ||
} else { | ||
let msg = format!("Invalid syntax for #[{ARBITRARY_ATTRIBUTE_NAME}], expected `=` after `{opt_name}`, got: `{eq_sign}`"); | ||
Err(syn::Error::new(eq_sign.span(), msg)) | ||
} | ||
} |
Oops, something went wrong.