use syn::{ Attribute, Field, Fields, MetaNameValue };
use syn::punctuated::{ Punctuated, Pair };
use syn::token::Comma;
use proc_macro2::TokenStream;
use case::RenameRule;
use error::{ Error, Result };
use meta;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TagExtra<'a> {
pub tag: &'a str,
pub variant: &'a str,
}
pub fn impl_bson_schema_fields(attrs: &[Attribute], fields: Fields) -> Result<TokenStream> {
impl_bson_schema_fields_extra(attrs, fields, None)
}
pub fn impl_bson_schema_fields_extra(
attrs: &[Attribute],
fields: Fields,
extra: Option<TagExtra>
) -> Result<TokenStream> {
match fields {
Fields::Named(fields) => {
impl_bson_schema_named_fields(attrs, fields.named, extra)
},
Fields::Unnamed(fields) => {
impl_bson_schema_indexed_fields(fields.unnamed, extra)
},
Fields::Unit => {
assert!(extra.is_none(), "internally-tagged unit should've been handled");
impl_bson_schema_unit_field()
},
}
}
fn impl_bson_schema_named_fields(
attrs: &[Attribute],
fields: Punctuated<Field, Comma>,
extra: Option<TagExtra>,
) -> Result<TokenStream> {
let properties = &field_names(attrs, &fields)?;
let defs: Vec<_> = fields.iter().map(field_def).collect::<Result<_>>()?;
let tokens = if let Some(TagExtra { tag, variant }) = extra {
quote! {
doc! {
"type": "object",
"additionalProperties": false,
"required": [ #tag, #(#properties,)* ],
"properties": {
#tag: { "enum": [ #variant ] },
#(#properties: #defs,)*
},
}
}
} else {
quote! {
doc! {
"type": "object",
"additionalProperties": false,
"required": [ #(#properties,)* ],
"properties": {
#(#properties: #defs,)*
},
}
}
};
Ok(tokens)
}
fn field_def(field: &Field) -> Result<TokenStream> {
let ty = &field.ty;
let min_incl = meta::magnet_name_value(&field.attrs, "min_incl")?;
let min_excl = meta::magnet_name_value(&field.attrs, "min_excl")?;
let max_incl = meta::magnet_name_value(&field.attrs, "max_incl")?;
let max_excl = meta::magnet_name_value(&field.attrs, "max_excl")?;
let lower = bounds_from_meta(min_incl, min_excl)?;
let upper = bounds_from_meta(max_incl, max_excl)?;
Ok(quote! {
::magnet_schema::support::extend_schema_with_bounds(
<#ty as ::magnet_schema::BsonSchema>::bson_schema(),
::magnet_schema::support::Bounds {
lower: #lower,
upper: #upper,
},
)
})
}
fn bounds_from_meta(incl: Option<MetaNameValue>, excl: Option<MetaNameValue>) -> Result<TokenStream> {
if let Some(nv) = incl {
let value = meta::value_as_num(&nv)?;
Ok(quote! {
::magnet_schema::support::Bound::Inclusive(#value)
})
} else if let Some(nv) = excl {
let value = meta::value_as_num(&nv)?;
Ok(quote! {
::magnet_schema::support::Bound::Exclusive(#value)
})
} else {
Ok(quote! {
::magnet_schema::support::Bound::Unbounded
})
}
}
fn field_names(attrs: &[Attribute], fields: &Punctuated<Field, Comma>) -> Result<Vec<String>> {
let rename_all_str = meta::serde_name_value(attrs, "rename_all")?;
let rename_all: Option<RenameRule> = match rename_all_str {
Some(s) => Some(meta::value_as_str(&s)?.parse()?),
None => None,
};
let iter = fields.iter().map(|field| {
let name = field.ident.as_ref().ok_or_else(
|| Error::new("no name for named field?!")
)?;
if meta::magnet_name_value(&field.attrs, "rename")?.is_some() {
return Err(Error::new("`#[magnet(rename = \"...\")]` no longer exists"))
}
let rename = meta::serde_name_value(&field.attrs, "rename")?;
let name = match rename {
Some(nv) => meta::value_as_str(&nv)?,
None => rename_all.map_or_else(
|| name.to_string(),
|rule| rule.apply_to_field(name.to_string()),
),
};
Ok(name)
});
iter.collect()
}
fn impl_bson_schema_indexed_fields(
mut fields: Punctuated<Field, Comma>,
extra: Option<TagExtra>,
) -> Result<TokenStream> {
if extra.is_some() && fields.len() != 1 {
return Err(Error::new("internal tagging not usable with tuple variant"))
}
match fields.pop().map(Pair::into_value) {
None => impl_bson_schema_unit_field(), Some(field) => match fields.len() {
0 => {
let def = field_def(&field)?;
let tokens = if let Some(TagExtra { tag, variant }) = extra {
quote! {
::magnet_schema::support::extend_schema_with_tag(
#def,
#tag,
#variant,
)
}
} else {
def
};
Ok(tokens)
},
_ => {
fields.push(field);
let defs: Vec<_> = fields
.iter()
.map(field_def)
.collect::<Result<_>>()?;
Ok(quote! {
doc! {
"type": "array",
"additionalItems": false,
"items": [ #(#defs,)* ],
}
})
},
}
}
}
fn impl_bson_schema_unit_field() -> Result<TokenStream> {
Ok(quote!{ <() as ::magnet_schema::BsonSchema>::bson_schema() })
}