Skip to content

Commit

Permalink
Allow several 'field' attributes in all derives.
Browse files Browse the repository at this point in the history
In particular, 'FromFormField' and 'UriDisplayQuery' now allow any
number of form 'field' attributes. For the former, multiple 'value's are
allowed, all of which are used to match against incoming fields - any
match wins. For the latter, multiple 'name' and 'value's are allowed;
the first of each is used to render the query value.

Additionally, 'UriDisplayQuery' can now be derived for C-like enums.
This brings the derive to parity with 'FromFormValue' and allows their
unified application on C-like enums.

Resolves rwf2#843.
  • Loading branch information
SergioBenitez committed Apr 8, 2021
1 parent fd03417 commit c45a838
Show file tree
Hide file tree
Showing 16 changed files with 663 additions and 285 deletions.
2 changes: 1 addition & 1 deletion contrib/codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ proc-macro = true

[dependencies]
quote = "1.0"
devise = { git = "https://github.com/SergioBenitez/Devise.git", rev = "3ebe83" }
devise = { git = "https://github.com/SergioBenitez/Devise.git", rev = "df00b5" }

[dev-dependencies]
rocket = { version = "0.5.0-dev", path = "../../core/lib" }
Expand Down
2 changes: 1 addition & 1 deletion core/codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ proc-macro = true
indexmap = "1.0"
quote = "1.0"
rocket_http = { version = "0.5.0-dev", path = "../http/" }
devise = { git = "https://github.com/SergioBenitez/Devise.git", rev = "3ebe83" }
devise = { git = "https://github.com/SergioBenitez/Devise.git", rev = "df00b5" }
unicode-xid = "0.2"
glob = "0.3"

Expand Down
83 changes: 71 additions & 12 deletions core/codegen/src/derive/form_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use devise::{*, ext::{TypeExt, SpanDiagnosticExt}};
use syn::visit_mut::VisitMut;
use syn::visit::Visit;

use crate::proc_macro2::{TokenStream, TokenTree};
use crate::proc_macro2::{TokenStream, TokenTree, Span};
use crate::syn_ext::IdentExt;
use crate::name::Name;

Expand All @@ -26,11 +26,50 @@ impl FieldAttr {
pub(crate) trait FieldExt {
fn ident(&self) -> &syn::Ident;
fn field_names(&self) -> Result<Vec<FieldName>>;
fn one_field_name(&self) -> Result<FieldName>;
fn first_field_name(&self) -> Result<FieldName>;
fn stripped_ty(&self) -> syn::Type;
fn name_view(&self) -> Result<syn::Expr>;
}

#[derive(FromMeta)]
pub struct VariantAttr {
pub value: Name,
}

impl VariantAttr {
const NAME: &'static str = "field";
}

pub(crate) trait VariantExt {
fn first_form_field_value(&self) -> Result<FieldName>;
fn form_field_values(&self) -> Result<Vec<FieldName>>;
}

impl VariantExt for Variant<'_> {
fn first_form_field_value(&self) -> Result<FieldName> {
let first = VariantAttr::from_attrs(VariantAttr::NAME, &self.attrs)?
.into_iter()
.next();

Ok(first.map_or_else(
|| FieldName::Uncased(Name::from(&self.ident)),
|attr| FieldName::Uncased(attr.value)))
}

fn form_field_values(&self) -> Result<Vec<FieldName>> {
let attr_values = VariantAttr::from_attrs(VariantAttr::NAME, &self.attrs)?
.into_iter()
.map(|attr| FieldName::Uncased(attr.value))
.collect::<Vec<_>>();

if attr_values.is_empty() {
return Ok(vec![FieldName::Uncased(Name::from(&self.ident))]);
}

Ok(attr_values)
}
}

impl FromMeta for FieldName {
fn from_meta(meta: &MetaItem) -> Result<Self> {
// These are used during parsing.
Expand Down Expand Up @@ -124,17 +163,9 @@ impl FieldExt for Field<'_> {
Ok(attr_names)
}

fn one_field_name(&self) -> Result<FieldName> {
fn first_field_name(&self) -> Result<FieldName> {
let mut names = self.field_names()?.into_iter();
let first = names.next().expect("always have >= 1 name");

if let Some(name) = names.next() {
return Err(name.span()
.error("unexpected second field name")
.note("only one field rename is allowed in this context"));
}

Ok(first)
Ok(names.next().expect("always have >= 1 name"))
}

fn stripped_ty(&self) -> syn::Type {
Expand Down Expand Up @@ -293,3 +324,31 @@ pub fn validators<'v>(

Ok(exprs)
}

pub fn first_duplicate<K: Spanned, V: PartialEq + Spanned>(
keys: impl Iterator<Item = K> + Clone,
values: impl Fn(&K) -> Result<Vec<V>>,
) -> Result<Option<((usize, Span, Span), (usize, Span, Span))>> {
let (mut all_values, mut key_map) = (vec![], vec![]);
for key in keys {
all_values.append(&mut values(&key)?);
key_map.push((all_values.len(), key));
}

// get the key corresponding to all_value index `k`.
let key = |k| key_map.iter().find(|(i, _)| k < *i).expect("k < *i");

for (i, a) in all_values.iter().enumerate() {
let rest = all_values.iter().enumerate().skip(i + 1);
if let Some((j, b)) = rest.filter(|(_, b)| *b == a).next() {
let (a_i, key_a) = key(i);
let (b_i, key_b) = key(j);

let a = (*a_i, key_a.span(), a.span());
let b = (*b_i, key_b.span(), b.span());
return Ok(Some((a, b)));
}
}

Ok(None)
}
36 changes: 11 additions & 25 deletions core/codegen/src/derive/from_form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,33 +70,19 @@ pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream {
return Err(fields.span().error("at least one field is required"));
}

let mut names = vec![];
let mut field_map = vec![];
for field in fields.iter() {
names.append(&mut field.field_names()?);
field_map.push((names.len(), field));
}

// get the field corresponding to name index `k`.
let field = |k| field_map.iter().find(|(i, _)| k < *i).expect("k < *i");

for (i, a) in names.iter().enumerate() {
let rest = names.iter().enumerate().skip(i + 1);
if let Some((j, b)) = rest.filter(|(_, b)| b == &a).next() {
let ((fa_i, field_a), (fb_i, field_b)) = (field(i), field(j));
if let Some(d) = first_duplicate(fields.iter(), |f| f.field_names())? {
let (field_a_i, field_a, name_a) = d.0;
let (field_b_i, field_b, name_b) = d.1;

if fa_i == fb_i {
return Err(field_a.span()
.error("field has conflicting names")
.span_note(a.span(), "this field name...")
.span_note(b.span(), "...conflicts with this name"));
} else {
return Err(b.span()
.error("field name conflicts with previous name")
.span_note(field_a.span(), "previous field with conflicting name")
.span_help(field_b.span(), "field name is part of this field"));
}
if field_a_i == field_b_i {
return Err(field_a.error("field has conflicting names")
.span_note(name_a, "this field name...")
.span_note(name_b, "...conflicts with this field name"));
}

return Err(name_b.error("field name conflicts with previous name")
.span_help(field_b, "declared in this field")
.span_note(field_a, "previous field with conflicting name"));
}

Ok(())
Expand Down
84 changes: 50 additions & 34 deletions core/codegen/src/derive/from_form_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ use devise::{*, ext::SpanDiagnosticExt};

use crate::exports::*;
use crate::proc_macro2::TokenStream;
use crate::name::Name;

#[derive(FromMeta)]
pub struct FieldAttr {
value: Name,
}
use crate::derive::form_field::{VariantExt, first_duplicate};

pub fn derive_from_form_field(input: proc_macro::TokenStream) -> TokenStream {
DeriveGenerator::build_for(input, quote!(impl<'__v> #_form::FromFormField<'__v>))
Expand All @@ -26,47 +21,68 @@ pub fn derive_from_form_field(input: proc_macro::TokenStream) -> TokenStream {
return Err(data.span().error("enum must have at least one variant"));
}

if let Some(d) = first_duplicate(data.variants(), |v| v.form_field_values())? {
let (variant_a_i, variant_a, value_a) = d.0;
let (variant_b_i, variant_b, value_b) = d.1;

if variant_a_i == variant_b_i {
return Err(variant_a.error("variant has conflicting values")
.span_note(value_a, "this value...")
.span_note(value_b, "...conflicts with this value"));
}

return Err(value_b.error("field value conflicts with previous value")
.span_help(variant_b, "...declared in this variant")
.span_note(variant_a, "previous field with conflicting name"));
}

Ok(())
})
)
// TODO: Devise should have a try_variant_map.
.outer_mapper(quote! {
#[allow(unused_imports)]
use #_http::uncased::AsUncased;
})
.inner_mapper(MapperBuild::new()
.try_enum_map(|_, data| {
let variant_name_sources = data.variants()
.map(|v| FieldAttr::one_from_attrs("field", &v.attrs).map(|o| {
o.map(|f| f.value).unwrap_or_else(|| Name::from(&v.ident))
}))
.collect::<Result<Vec<Name>>>()?;
.with_output(|_, output| quote! {
fn from_value(
__f: #_form::ValueField<'__v>
) -> Result<Self, #_form::Errors<'__v>> {

let variant_name = variant_name_sources.iter()
.map(|n| n.as_str())
.collect::<Vec<_>>();
#output
}
})
.try_enum_map(|mapper, data| {
let mut variant_value = vec![];
for v in data.variants().map(|v| v.form_field_values()) {
variant_value.append(&mut v?);
}

let builder = data.variants()
.map(|v| v.builder(|_| unreachable!("fieldless")));
let variant_condition = data.variants()
.map(|v| mapper.map_variant(v))
.collect::<Result<Vec<_>>>()?;

let (_ok, _cow) = (std::iter::repeat(_Ok), std::iter::repeat(_Cow));
Ok(quote! {
fn from_value(
__f: #_form::ValueField<'__v>
) -> Result<Self, #_form::Errors<'__v>> {
#[allow(unused_imports)]
use #_http::uncased::AsUncased;
#(#variant_condition)*

#(
if __f.value.as_uncased() == #variant_name {
return #_ok(#builder);
}
)*
const OPTS: &'static [#_Cow<'static, str>] =
&[#(#_cow::Borrowed(#variant_value)),*];

const OPTS: &'static [#_Cow<'static, str>] =
&[#(#_cow::Borrowed(#variant_name)),*];
let _error = #_form::Error::from(OPTS)
.with_name(__f.name)
.with_value(__f.value);

let _error = #_form::Error::from(OPTS)
.with_name(__f.name)
.with_value(__f.value);
#_Err(_error)?
})
})
.try_variant_map(|_, variant| {
let builder = variant.builder(|_| unreachable!("fieldless"));
let value = variant.form_field_values()?;

#_Err(_error)?
Ok(quote_spanned! { variant.span() =>
if #(__f.value.as_uncased() == #value)||* {
return #_Ok(#builder);
}
})
})
Expand Down
62 changes: 37 additions & 25 deletions core/codegen/src/derive/uri_display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,15 @@ use devise::{*, ext::SpanDiagnosticExt};
use rocket_http::uri;

use crate::exports::*;
use crate::derive::form_field::FieldExt;
use crate::derive::form_field::{FieldExt, VariantExt};
use crate::proc_macro2::TokenStream;

const NO_EMPTY_FIELDS: &str = "fieldless structs or variants are not supported";
const NO_EMPTY_FIELDS: &str = "fieldless structs are not supported";
const NO_NULLARY: &str = "nullary items are not supported";
const NO_EMPTY_ENUMS: &str = "empty enums are not supported";
const ONLY_ONE_UNNAMED: &str = "tuple structs or variants must have exactly one field";
const EXACTLY_ONE_FIELD: &str = "struct must have exactly one field";

fn validate_fields(fields: Fields<'_>) -> Result<()> {
if fields.count() == 0 {
return Err(fields.parent.span().error(NO_EMPTY_FIELDS))
} else if fields.are_unnamed() && fields.count() > 1 {
return Err(fields.span().error(ONLY_ONE_UNNAMED));
} else if fields.are_unit() {
return Err(fields.span().error(NO_NULLARY));
}

Ok(())
}

fn validate_enum(data: Enum<'_>) -> Result<()> {
if data.variants().count() == 0 {
return Err(data.brace_token.span.error(NO_EMPTY_ENUMS));
}

Ok(())
}

pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream {
use crate::http::uri::Query;

Expand All @@ -41,8 +21,30 @@ pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream {
let uri_display = DeriveGenerator::build_for(input.clone(), quote!(impl #URI_DISPLAY))
.support(Support::Struct | Support::Enum | Support::Type | Support::Lifetime)
.validator(ValidatorBuild::new()
.enum_validate(|_, v| validate_enum(v))
.fields_validate(|_, v| validate_fields(v))
.enum_validate(|_, data| {
if data.variants().count() == 0 {
return Err(data.brace_token.span.error(NO_EMPTY_ENUMS));
} else {
Ok(())
}
})
.struct_validate(|_, data| {
let fields = data.fields();
if fields.is_empty() {
Err(data.span().error(NO_EMPTY_FIELDS))
} else if fields.are_unit() {
Err(data.span().error(NO_NULLARY))
} else {
Ok(())
}
})
.fields_validate(|_, fields| {
if fields.are_unnamed() && fields.count() > 1 {
Err(fields.span().error(ONLY_ONE_UNNAMED))
} else {
Ok(())
}
})
)
.type_bound(URI_DISPLAY)
.inner_mapper(MapperBuild::new()
Expand All @@ -52,11 +54,21 @@ pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream {
Ok(())
}
})
.try_variant_map(|mapper, variant| {
if !variant.fields().is_empty() {
return mapper::variant_default(mapper, variant);
}

let value = variant.first_form_field_value()?;
Ok(quote_spanned! { variant.span() =>
f.write_value(#value)?;
})
})
.try_field_map(|_, field| {
let span = field.span().into();
let accessor = field.accessor();
let tokens = if field.ident.is_some() {
let name = field.one_field_name()?;
let name = field.first_field_name()?;
quote_spanned!(span => f.write_named_value(#name, &#accessor)?;)
} else {
quote_spanned!(span => f.write_value(&#accessor)?;)
Expand Down
Loading

0 comments on commit c45a838

Please sign in to comment.