blob: 3e35a236b541e3a88babfd27a229e21ce2e25e3e [file] [log] [blame]
//! Types for "shape" validation. This allows types deriving `FromDeriveInput` etc. to declare
//! that they only work on - for example - structs with named fields, or newtype enum variants.
use proc_macro2::TokenStream;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{parse_quote, Meta};
use crate::ast::NestedMeta;
use crate::{Error, FromMeta, Result};
/// Receiver struct for shape validation. Shape validation allows a deriving type
/// to declare that it only accepts - for example - named structs, or newtype enum
/// variants.
///
/// ```rust,ignore
/// #[ignore(any, struct_named, enum_newtype)]
/// ```
#[derive(Debug, Clone)]
pub struct DeriveInputShapeSet {
enum_values: DataShape,
struct_values: DataShape,
any: bool,
}
impl Default for DeriveInputShapeSet {
fn default() -> Self {
DeriveInputShapeSet {
enum_values: DataShape::new("enum_"),
struct_values: DataShape::new("struct_"),
any: Default::default(),
}
}
}
impl FromMeta for DeriveInputShapeSet {
fn from_list(items: &[NestedMeta]) -> Result<Self> {
let mut new = DeriveInputShapeSet::default();
for item in items {
if let NestedMeta::Meta(Meta::Path(ref path)) = *item {
let ident = &path.segments.first().unwrap().ident;
let word = ident.to_string();
if word == "any" {
new.any = true;
} else if word.starts_with("enum_") {
new.enum_values
.set_word(&word)
.map_err(|e| e.with_span(&ident))?;
} else if word.starts_with("struct_") {
new.struct_values
.set_word(&word)
.map_err(|e| e.with_span(&ident))?;
} else {
return Err(Error::unknown_value(&word).with_span(&ident));
}
} else {
return Err(Error::unsupported_format("non-word").with_span(item));
}
}
Ok(new)
}
}
impl ToTokens for DeriveInputShapeSet {
fn to_tokens(&self, tokens: &mut TokenStream) {
let fn_body = if self.any {
quote!(::darling::export::Ok(()))
} else {
let en = &self.enum_values;
let st = &self.struct_values;
quote! {
{
let struct_check = #st;
let enum_check = #en;
match *__body {
::darling::export::syn::Data::Enum(ref data) => {
if enum_check.is_empty() {
return ::darling::export::Err(
::darling::Error::unsupported_shape_with_expected("enum", &format!("struct with {}", struct_check))
);
}
let mut variant_errors = ::darling::Error::accumulator();
for variant in &data.variants {
variant_errors.handle(enum_check.check(variant));
}
variant_errors.finish()
}
::darling::export::syn::Data::Struct(ref struct_data) => {
if struct_check.is_empty() {
return ::darling::export::Err(
::darling::Error::unsupported_shape_with_expected("struct", &format!("enum with {}", enum_check))
);
}
struct_check.check(struct_data)
}
::darling::export::syn::Data::Union(_) => unreachable!(),
}
}
}
};
tokens.append_all(quote! {
#[allow(unused_variables)]
fn __validate_body(__body: &::darling::export::syn::Data) -> ::darling::Result<()> {
#fn_body
}
});
}
}
/// Receiver for shape information within a struct or enum context. See `Shape` for more information
/// on valid uses of shape validation.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DataShape {
/// The kind of shape being described. This can be `struct_` or `enum_`.
prefix: &'static str,
newtype: bool,
named: bool,
tuple: bool,
unit: bool,
any: bool,
}
impl DataShape {
fn new(prefix: &'static str) -> Self {
DataShape {
prefix,
..Default::default()
}
}
fn set_word(&mut self, word: &str) -> Result<()> {
match word.trim_start_matches(self.prefix) {
"newtype" => {
self.newtype = true;
Ok(())
}
"named" => {
self.named = true;
Ok(())
}
"tuple" => {
self.tuple = true;
Ok(())
}
"unit" => {
self.unit = true;
Ok(())
}
"any" => {
self.any = true;
Ok(())
}
_ => Err(Error::unknown_value(word)),
}
}
}
impl FromMeta for DataShape {
fn from_list(items: &[NestedMeta]) -> Result<Self> {
let mut errors = Error::accumulator();
let mut new = DataShape::default();
for item in items {
if let NestedMeta::Meta(Meta::Path(ref path)) = *item {
errors.handle(new.set_word(&path.segments.first().unwrap().ident.to_string()));
} else {
errors.push(Error::unsupported_format("non-word").with_span(item));
}
}
errors.finish_with(new)
}
}
impl ToTokens for DataShape {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
any,
named,
tuple,
unit,
newtype,
..
} = *self;
let shape_path: syn::Path = parse_quote!(::darling::util::Shape);
let mut shapes = vec![];
if any || named {
shapes.push(quote!(#shape_path::Named));
}
if any || tuple {
shapes.push(quote!(#shape_path::Tuple));
}
if any || newtype {
shapes.push(quote!(#shape_path::Newtype));
}
if any || unit {
shapes.push(quote!(#shape_path::Unit));
}
tokens.append_all(quote! {
::darling::util::ShapeSet::new(vec![#(#shapes),*])
});
}
}
#[cfg(test)]
mod tests {
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse_quote;
use super::DeriveInputShapeSet;
use crate::FromMeta;
/// parse a string as a syn::Meta instance.
fn pm(tokens: TokenStream) -> ::std::result::Result<syn::Meta, String> {
let attribute: syn::Attribute = parse_quote!(#[#tokens]);
Ok(attribute.meta)
}
fn fm<T: FromMeta>(tokens: TokenStream) -> T {
FromMeta::from_meta(&pm(tokens).expect("Tests should pass well-formed input"))
.expect("Tests should pass valid input")
}
#[test]
fn supports_any() {
let decl = fm::<DeriveInputShapeSet>(quote!(ignore(any)));
assert!(decl.any);
}
#[test]
fn supports_struct() {
let decl = fm::<DeriveInputShapeSet>(quote!(ignore(struct_any, struct_newtype)));
assert!(decl.struct_values.any);
assert!(decl.struct_values.newtype);
}
#[test]
fn supports_mixed() {
let decl =
fm::<DeriveInputShapeSet>(quote!(ignore(struct_newtype, enum_newtype, enum_tuple)));
assert!(decl.struct_values.newtype);
assert!(decl.enum_values.newtype);
assert!(decl.enum_values.tuple);
assert!(!decl.struct_values.any);
}
}