Skip to content

Commit

Permalink
Introduce IntoAttributes trait and from_attrs_sink method
Browse files Browse the repository at this point in the history
This also adds support for collecting all additional properties into a hash map.

**BREAKING CHANGE**:
Now the users have to implement `from_attrs_sink(&mut Attributes)` instead of `from_attrs() -> Attributes`
The existing `from_attrs()` method has a default implementation.
  • Loading branch information
Veetaha committed Sep 8, 2020
1 parent 42df924 commit c7468ed
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 125 deletions.
151 changes: 64 additions & 87 deletions dynomite-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ fn make_dynomite_attributes(
) -> syn::Result<impl ToTokens> {
let item_fields = fields.iter().map(ItemField::new).collect::<Vec<_>>();
// impl ::dynomite::FromAttributes for Name
let from_attribute_map = get_from_attributes_trait(name, &item_fields)?;
let from_attribute_map = get_from_attributes_trait(name, &item_fields);
// impl From<Name> for ::dynomite::Attributes
let to_attribute_map = get_to_attribute_map_trait(name, &item_fields)?;
// impl Attribute for Name (these are essentially just a map)
Expand Down Expand Up @@ -335,7 +335,7 @@ fn make_dynomite_item(
// impl Item for Name + NameKey struct
let dynamodb_traits = get_dynomite_item_traits(vis, name, &item_fields)?;
// impl ::dynomite::FromAttributes for Name
let from_attribute_map = get_from_attributes_trait(name, &item_fields)?;
let from_attribute_map = get_from_attributes_trait(name, &item_fields);
// impl From<Name> for ::dynomite::Attributes
let to_attribute_map = get_to_attribute_map_trait(name, &item_fields)?;

Expand All @@ -346,52 +346,33 @@ fn make_dynomite_item(
})
}

// impl From<Name> for ::dynomite::Attributes {
// fn from(n: Name) -> Self {
// ...
// }
// }
//
fn get_to_attribute_map_trait(
name: &Ident,
fields: &[ItemField],
) -> syn::Result<impl ToTokens> {
let to_attrs = get_to_attrs_function(fields)?;
let into_attrs_sink = get_into_attrs_sink_fn(fields);

Ok(quote! {
impl #name {
#to_attrs
impl ::dynomite::IntoAttributes for #name {
#into_attrs_sink
}

impl ::std::convert::From<#name> for ::dynomite::Attributes {
fn from(item: #name) -> Self {
let mut values = Self::new();
item.__to_attrs(&mut values);
values
::dynomite::IntoAttributes::into_attrs(item)
}
}
})
}

// generates the `from(...)` method for attribute map From conversion
//
// fn from(item: Foo) -> Self {
// let mut values = Self::new();
// values.insert(
// "foo".to_string(),
// ::dynomite::Attribute::into_attr(item.field)
// );
// ...
// values
// }
fn get_to_attrs_function(fields: &[ItemField]) -> syn::Result<impl ToTokens> {
fn get_into_attrs_sink_fn(fields: &[ItemField]) -> impl ToTokens {
let field_conversions = fields.iter().map(|field| {
let field_deser_name = field.deser_name();
let field_ident = &field.field.ident;

if field.is_flatten() {
quote! {
self.#field_ident.__to_attrs(attrs);
::dynomite::IntoAttributes::into_attrs_sink(self.#field_ident, attrs);
}
} else {
quote! {
Expand All @@ -403,91 +384,87 @@ fn get_to_attrs_function(fields: &[ItemField]) -> syn::Result<impl ToTokens> {
}
});

Ok(quote! {
// Used to serialize into an attributes map without creating a temporary map
// (useful for `#[dynomite(flatten)]`)
#[doc(hidden)]
pub fn __to_attrs(self, attrs: &mut ::dynomite::Attributes) {
quote! {
fn into_attrs_sink(self, attrs: &mut ::dynomite::Attributes) {
#(#field_conversions)*
}
})
}
}

/// ```rust,ignore
/// impl ::dynomite::FromAttributes for Name {
/// fn from_attrs(mut item: ::dynomite::Attributes) -> Result<Self, ::dynomite::Error> {
/// Ok(Self {
/// field_name: ::dynomite::Attribute::from_attr(
/// item.remove("field_deser_name").ok_or(Error::MissingField { name: "field_deser_name".into() })?
/// )
/// })
/// }
/// fn from_attrs_sink(attrs: &mut ::dynomite::Attributes) -> Result<Self, ::dynomite::Error> {
/// let field_name = ::dynomite::Attribute::from_attr(
/// attrs.remove("field_deser_name").ok_or_else(|| Error::MissingField { name: "field_deser_name".to_string() })?
/// );
/// Ok(Self {
/// field_name,
/// })
/// }
/// }
/// ```
fn get_from_attributes_trait(
name: &Ident,
fields: &[ItemField],
) -> syn::Result<impl ToTokens> {
) -> impl ToTokens {
let from_attrs = quote!(::dynomite::FromAttributes);
let from_attrs_mut_fn = get_from_attributes_mut_function(fields)?;

Ok(quote! {
impl #name {
#from_attrs_mut_fn
}
let from_attrs_sink_fn = get_from_attrs_sink_function(fields);

quote! {
impl #from_attrs for #name {
fn from_attrs(mut attrs: ::dynomite::Attributes) -> ::std::result::Result<Self, ::dynomite::AttributeError> {
Self::__from_attrs_mut(&mut attrs)
}
#from_attrs_sink_fn
}
})
}
}

fn get_from_attributes_mut_function(fields: &[ItemField]) -> syn::Result<impl ToTokens> {
let attributes = quote!(::dynomite::Attributes);
let from_attribute_value = quote!(::dynomite::Attribute::from_attr);
let err = quote!(::dynomite::AttributeError);

let field_conversions = fields.iter().map(|field| {
// field has #[dynomite(renameField = "...")] attribute
let field_deser_name = field.deser_name();

let field_ident = &field.field.ident;
if field.is_default_when_absent() {
return quote! {
#field_ident: match attrs.remove(#field_deser_name) {
Some(field) => #from_attribute_value(field)?,
_ => ::std::default::Default::default()
fn get_from_attrs_sink_function(fields: &[ItemField]) -> impl ToTokens {
let var_init_statements = fields
.iter()
.map(|field| {
// field might have #[dynomite(rename = "...")] attribute
let field_deser_name = field.deser_name();
let field_ident = &field.field.ident;
let expr = if field.is_default_when_absent() {
quote! {
match attrs.remove(#field_deser_name) {
Some(field) => ::dynomite::Attribute::from_attr(field)?,
_ => ::std::default::Default::default()
}
}
}
}

if field.is_flatten() {
let ty = &field.field.ty;
quote! {
#field_ident: #ty::__from_attrs_mut(attrs)?,
}
} else {
} else if field.is_flatten() {
quote! { ::dynomite::FromAttributes::from_attrs_sink(attrs)? }
} else {
quote! {
::dynomite::Attribute::from_attr(
attrs.remove(#field_deser_name).ok_or_else(|| ::dynomite::AttributeError::MissingField {
name: #field_deser_name.to_string()
})?
)?
}
};
quote! {
#field_ident: #from_attribute_value(
attrs.remove(#field_deser_name)
.ok_or(::dynomite::AttributeError::MissingField { name: #field_deser_name.to_string() })?
)?
let #field_ident = #expr;
}
}
});

});
let field_names = fields.iter().map(|it| &it.field.ident);

Ok(quote! {
// Used to deserialize from an attributes map without consuming it (useful for `#[dynomite(flatten)]`)
#[doc(hidden)]
pub fn __from_attrs_mut(attrs: &mut #attributes) -> ::std::result::Result<Self, #err> {
// The order of evaluation of struct literal fields seems
// **informally** left-to-right (as per Niko Matsakis and Steve Klabnik),
// https://stackoverflow.com/a/57612600/9259330
// This means we should not rely on this behavior yet.
// We explicitly make conversion expressions a separate statements.
// This is important, because the order of declaration and evaluation
// of `flatten` fields matters.

quote! {
fn from_attrs_sink(attrs: &mut ::dynomite::Attributes) -> ::std::result::Result<Self, ::dynomite::AttributeError> {
#(#var_init_statements)*
::std::result::Result::Ok(Self {
#(#field_conversions),*
#(#field_names),*
})
}
})
}
}

fn get_dynomite_item_traits(
Expand Down
119 changes: 95 additions & 24 deletions dynomite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
//! // A separate struct to store data without any id
//! #[dynomite(flatten)]
//! data: ShoppingCartData,
//! // Collect all other additional attributes into a map
//! // Beware that the order of declaration will affect the order of
//! // evaluation, so this "wildcard" flatten clause should be the last member
//! #[dynomite(flatten)]
//! remaining_props: Attributes,
//! }
//!
//! // `Attributes` doesn't require neither of #[dynomite(partition_key/sort_key)]
Expand Down Expand Up @@ -153,7 +158,8 @@ pub type Attributes = HashMap<String, AttributeValue>;
///
/// ```
/// use dynomite::{
/// dynamodb::AttributeValue, Attribute, AttributeError, Attributes, FromAttributes, Item,
/// dynamodb::AttributeValue, Attribute, AttributeError, Attributes, FromAttributes,
/// IntoAttributes, Item,
/// };
/// use std::collections::HashMap;
///
Expand All @@ -171,23 +177,33 @@ pub type Attributes = HashMap<String, AttributeValue>;
/// }
///
/// impl FromAttributes for Person {
/// fn from_attrs(attrs: Attributes) -> Result<Self, AttributeError> {
/// fn from_attrs_sink(attrs: &mut Attributes) -> Result<Self, AttributeError> {
/// Ok(Self {
/// id: attrs
/// .get("id")
/// .and_then(|val| val.s.clone())
/// .ok_or(AttributeError::MissingField { name: "id".into() })?,
/// .remove("id")
/// .and_then(|val| val.s)
/// .ok_or_else(|| AttributeError::MissingField { name: "id".into() })?,
/// })
/// }
/// }
///
/// impl Into<Attributes> for Person {
/// fn into(self: Self) -> Attributes {
/// let mut attrs = HashMap::new();
/// impl IntoAttributes for Person {
/// fn into_attrs_sink(
/// self,
/// attrs: &mut Attributes,
/// ) {
/// attrs.insert("id".into(), "123".to_string().into_attr());
/// attrs
/// }
/// }
///
/// // Unfortunately `dynomite` is not able to provide a blanket impl for this trait
/// // due to orphan rules, but it generated via the `dynomite_derive` attributes
/// impl From<Person> for Attributes {
/// fn from(person: Person) -> Attributes {
/// person.into_attrs()
/// }
/// }
///
/// let person = Person { id: "123".into() };
/// let attrs: Attributes = person.clone().into();
/// assert_eq!(Ok(person), FromAttributes::from_attrs(attrs))
Expand Down Expand Up @@ -292,39 +308,94 @@ pub trait Attribute: Sized {
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError>;
}

impl Attribute for AttributeValue {
fn into_attr(self: Self) -> AttributeValue {
self
}
fn from_attr(value: AttributeValue) -> Result<Self, AttributeError> {
Ok(value)
}
}

/// A type capable of being produced from
/// a set of string keys and `AttributeValues`
pub trait FromAttributes: Sized {
/// Shortcut for `FromAttributes::from_attrs_sink(&mut attrs)`.
/// You should generally implement only that method.
fn from_attrs(mut attrs: Attributes) -> Result<Self, AttributeError> {
Self::from_attrs_sink(&mut attrs)
}

/// Returns an instance of of a type resolved at runtime from a collection
/// of a `String` keys and `AttributeValues`. If
/// a instance can not be resolved and `AttributeError` will be returned.
fn from_attrs(attrs: Attributes) -> Result<Self, AttributeError>;
/// of a `String` keys and `AttributeValues`.
/// If an instance can not be resolved and `AttributeError` will be returned.
/// The implementations of this method should remove the relevant key-value
/// pairs from the map to consume them. This is needed to support
/// `#[dynomite(flatten)]` without creating temporary hash maps.
fn from_attrs_sink(attrs: &mut Attributes) -> Result<Self, AttributeError>;
}

/// Coerces a homogenious HashMap of attribute values into a homogeneous Map of types
/// that implement Attribute
#[allow(clippy::implicit_hasher)]
impl<A: Attribute> FromAttributes for HashMap<String, A> {
fn from_attrs(attrs: Attributes) -> Result<Self, AttributeError> {
fn from_attrs_sink(attrs: &mut Attributes) -> Result<Self, AttributeError> {
attrs
.into_iter()
.try_fold(HashMap::new(), |mut result, (k, v)| {
result.insert(k, A::from_attr(v)?);
Ok(result)
})
.drain()
.map(|(k, v)| Ok((k, A::from_attr(v)?)))
.collect()
}
}

/// Coerces a homogenious Map of attribute values into a homogeneous BTreeMap of types
/// that implement Attribute
impl<A: Attribute> FromAttributes for BTreeMap<String, A> {
fn from_attrs(attrs: Attributes) -> Result<Self, AttributeError> {
fn from_attrs_sink(attrs: &mut Attributes) -> Result<Self, AttributeError> {
attrs
.into_iter()
.try_fold(BTreeMap::new(), |mut result, (k, v)| {
result.insert(k, A::from_attr(v)?);
Ok(result)
})
.drain()
.map(|(k, v)| Ok((k, A::from_attr(v)?)))
.collect()
}
}

/// You should implement this trait instead of `From<T> for Attributes`
/// for your type to support flattening, #[dynomite(Attributes/Item)] will
/// generate both the implementation of this trait and `From<>`
/// (there is no blanket impl for `From<>` here due to orphan rules)
pub trait IntoAttributes: Sized {
/// A shortcut for `IntoAttributes::into_attrs_sink()` that creates a new hash map.
/// You should generally implement only that method instead.
fn into_attrs(self) -> Attributes {
let mut attrs = Attributes::new();
self.into_attrs_sink(&mut attrs);
attrs
}

/// Converts `self` into `Attributes` by accepting a `sink` argument and
/// insterting attribute key-value pairs into it.
/// This is needed to support `#[dynomite(flatten)]` without creating
/// temporary hash maps.
fn into_attrs_sink(
self,
sink: &mut Attributes,
);
}

impl<A: Attribute> IntoAttributes for HashMap<String, A> {
fn into_attrs_sink(
self,
sink: &mut Attributes,
) {
sink.extend(self.into_iter().map(|(k, v)| (k, v.into_attr())));
}
}

impl<A: Attribute> IntoAttributes for BTreeMap<String, A> {
fn into_attrs_sink(
self,
sink: &mut Attributes,
) {
sink.extend(self.into_iter().map(|(k, v)| (k, v.into_attr())));
}
}

Expand Down
Loading

0 comments on commit c7468ed

Please sign in to comment.