diff --git a/dynomite-derive/src/lib.rs b/dynomite-derive/src/lib.rs index e401add..3043ad1 100644 --- a/dynomite-derive/src/lib.rs +++ b/dynomite-derive/src/lib.rs @@ -38,8 +38,8 @@ use proc_macro2::Span; use proc_macro_error::{abort, ResultExt}; use quote::{quote, ToTokens}; use syn::{ - parse::Parse, punctuated::Punctuated, Attribute, DataStruct, DeriveInput, Field, Fields, Ident, - Token, Visibility, + parse::Parse, punctuated::Punctuated, Attribute, DataStruct, DeriveInput, Field, Fields, + FieldsUnnamed, Ident, Token, Visibility, }; struct Variant { @@ -324,7 +324,8 @@ pub fn derive_attributes(input: TokenStream) -> TokenStream { expand_attributes(ast).unwrap_or_else(|e| e.to_compile_error().into()) } -/// Derives `dynomite::Attribute` for enum types +/// Derives `dynomite::Attribute` for enum and single-field tuple struct (newtype) types that wrap +/// other `dynomite::Attribute` types. /// /// # Panics /// @@ -333,17 +334,33 @@ pub fn derive_attributes(input: TokenStream) -> TokenStream { #[proc_macro_derive(Attribute)] pub fn derive_attribute(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input); - let gen = expand_attribute(ast); - gen.into_token_stream().into() + expand_attribute(ast).unwrap_or_else(|e| e.to_compile_error().into()) } -fn expand_attribute(ast: DeriveInput) -> impl ToTokens { +fn expand_attribute(ast: DeriveInput) -> syn::Result { let name = &ast.ident; + match ast.data { - syn::Data::Enum(variants) => { - make_dynomite_attr(name, &variants.variants.into_iter().collect::>()) - } - _ => panic!("Dynomite Attributes can only be generated for enum types"), + syn::Data::Enum(variants) => Ok(make_dynomite_attr_for_enum( + name, + &variants.variants.into_iter().collect::>(), + ) + .to_token_stream() + .into()), + syn::Data::Struct(DataStruct { + fields: Fields::Unnamed(FieldsUnnamed { unnamed, .. }), + .. + }) if unnamed.len() == 1 => Ok(make_dynomite_attr_for_newtype_struct( + name, + unnamed.first().expect("first field should exist"), + ) + .to_token_stream() + .into()), + _ => Err(syn::Error::new( + ast.ident.span(), + "Dynomite Attributes can only be generated for enum and single-field tuple struct \ + (newtype) types", + )), } } @@ -367,7 +384,7 @@ fn expand_attribute(ast: DeriveInput) -> impl ToTokens { /// } /// } /// ``` -fn make_dynomite_attr( +fn make_dynomite_attr_for_enum( name: &Ident, variants: &[syn::Variant], ) -> impl ToTokens { @@ -408,6 +425,37 @@ fn make_dynomite_attr( } } +/// ```rust,ignore +/// impl ::dynomite::Attribute for Name { +/// fn into_attr(self) -> ::dynomite::dynamodb::AttributeValue { +/// self.0.into_attr() +/// } +/// fn from_attr( +/// value: ::dynomite::dynamodb::AttributeValue, +/// ) -> ::std::result::Result { +/// FieldType::from_attr(value).map(Self) +/// } +/// } +/// ``` +fn make_dynomite_attr_for_newtype_struct( + name: &Ident, + field: &Field, +) -> impl ToTokens { + let ty = field.ty.to_token_stream(); + quote! { + impl ::dynomite::Attribute for #name { + fn into_attr(self) -> ::dynomite::dynamodb::AttributeValue { + self.0.into_attr() + } + fn from_attr( + value: ::dynomite::dynamodb::AttributeValue, + ) -> ::std::result::Result { + #ty::from_attr(value).map(Self) + } + } + } +} + fn expand_attributes(ast: DeriveInput) -> syn::Result { use syn::spanned::Spanned as _; let name = ast.ident; diff --git a/dynomite/src/lib.rs b/dynomite/src/lib.rs index 2ee2387..de1f222 100644 --- a/dynomite/src/lib.rs +++ b/dynomite/src/lib.rs @@ -277,6 +277,24 @@ //! //! `role` field here may be any of `Admin`, `Moderator`, or `Regular` strings. //! +//! #### Newtype Structs +//! +//! Types that implement the [dynomite::Attribute](trait.Attribute.html) trait can be wrapped in +//! single-field tuple (newtype) structs. For example, a `String` newtype can be used as follows: +//! ```rust +//! use dynomite::{Attribute, Item}; +//! +//! #[derive(Attribute)] +//! struct Author(String); +//! +//! #[derive(Item)] +//! struct Book { +//! #[dynomite(partition_key)] +//! id: String, +//! author: Author, +//! } +//! ``` +//! //! ## Rusoto extensions //! //! By importing the [dynomite::DynamoDbExt](trait.DynamoDbExt.html) trait, dynomite diff --git a/dynomite/tests/derived.rs b/dynomite/tests/derived.rs index b1a1300..cd6024f 100644 --- a/dynomite/tests/derived.rs +++ b/dynomite/tests/derived.rs @@ -1,15 +1,21 @@ use dynomite::{Attribute, Attributes, Item}; use serde::{Deserialize, Serialize}; +#[derive(Attribute, Default, PartialEq, Debug, Clone, Serialize, Deserialize)] +pub struct AuthorName(String); + #[derive(Item, Default, PartialEq, Debug, Clone, Serialize, Deserialize)] pub struct Author { #[dynomite(partition_key)] // Test that the serde attr is not propagated to the generated key // Issue: https://github.com/softprops/dynomite/issues/121 #[serde(rename = "Name")] - name: String, + name: AuthorName, } +#[derive(Attribute, Default, PartialEq, Debug, Clone, Serialize, Deserialize)] +pub struct NewAuthor(Author); + #[derive(Attribute, PartialEq, Debug, Clone)] pub enum Category { Foo,