Skip to content

Commit

Permalink
Protobuf-json for frontend structs. (exonum#1093)
Browse files Browse the repository at this point in the history
  • Loading branch information
dvush authored and aleksuss committed Dec 13, 2018
1 parent 9bc8ccb commit 5b3ff40
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 24 deletions.
73 changes: 59 additions & 14 deletions components/derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,32 +27,61 @@ mod tx_set;
use proc_macro::TokenStream;
use syn::{Attribute, Lit, Meta, MetaList, MetaNameValue, NestedMeta, Path};

/// Exonum derive attribute names, used as `#[exonum( ATTRIBUTE_NAME = .. )]`
// Exonum derive attribute names, used as
// `#[exonum( [ ATTRIBUTE_NAME = ATTRIBUTE_VALUE or ATTRIBUTE_NAME ],* )]`
const CRATE_PATH_ATTRIBUTE: &str = "crate";
const PB_CONVERT_ATTRIBUTE: &str = "pb";
const SERDE_PB_CONVERT_ATTRIBUTE: &str = "serde_pb_convert";

/// Derives `ProtobufConvert` trait.
///
/// Attributes:
/// `#[exonum( pb = "path" )]`
/// Required. `path` is name of the corresponding protobuf struct(generated from .proto file)
/// `#[exonum( crate = "path" )]`
/// Optional. `path` is prefix of the exonum crate(usually "crate" or "exonum")
///
/// * `#[exonum( pb = "path" )]`
/// Required. `path` is the name of the corresponding protobuf generated struct.
///
/// * `#[exonum( crate = "path" )]`
/// Optional. `path` is prefix of the `exonum` crate(usually "crate" or "exonum").
///
/// * `#[exonum( serde_pb_convert )]`
/// Optional. Implements `serde::{Serialize, Deserialize}` using structs that were generated with
/// protobuf.
/// For example, it should be used if you want json representation of your struct
/// to be compatible with protobuf representation (including proper nesting of fields).
/// ```text
/// // For example, struct with `exonum::crypto::Hash` with this
/// // (de)serializer will be represented as
/// StructName {
/// "hash": {
/// data: [1, 2, ...]
/// },
/// // ...
/// }
///
/// // With default (de)serializer.
/// StructName {
/// "hash": "12af..." // HEX
/// // ...
/// }
/// ```
#[proc_macro_derive(ProtobufConvert, attributes(exonum))]
pub fn generate_protobuf_convert(input: TokenStream) -> TokenStream {
pb_convert::implement_protobuf_convert(input)
}

/// Derives `TransactionSet` trait for selected enum,
/// enum should be set of variants with transactions inside.
/// enum should have transactions as variants.
///
/// Also implements:
/// Conversion from Transaction types into this enum.
/// Conversion from Transaction types, enum into `ServiceTransaction`.
/// Conversion from enum into `Box<dyn Transaction>`.
///
/// * Conversion from Transaction types into this enum.
/// * Conversion from Transaction types and this enum into `ServiceTransaction`.
/// * Conversion from this enum into `Box<dyn Transaction>`.
///
/// Attributes:
/// `#[exonum( crate = "path" )]`
/// Optional. `path` is prefix of the exonum crate(usually "crate" or "exonum")
///
/// * `#[exonum( crate = "path" )]`
/// Optional. `path` is a prefix of types from the `exonum` crate (usually "crate" or "exonum").
#[proc_macro_derive(TransactionSet, attributes(exonum))]
pub fn transaction_set_derive(input: TokenStream) -> TokenStream {
tx_set::implement_transaction_set(input)
Expand All @@ -61,7 +90,7 @@ pub fn transaction_set_derive(input: TokenStream) -> TokenStream {
/// Exonum types should be imported with `crate::` prefix if inside crate
/// or with `exonum::` when outside.
fn get_exonum_types_prefix(attrs: &[Attribute]) -> impl quote::ToTokens {
let map_attrs = get_exonum_attributes(attrs);
let map_attrs = get_exonum_name_value_attributes(attrs);
let crate_path = map_attrs.into_iter().find_map(|nv| match &nv {
MetaNameValue {
lit: Lit::Str(path),
Expand All @@ -86,7 +115,7 @@ fn get_exonum_types_prefix(attrs: &[Attribute]) -> impl quote::ToTokens {
}

/// Extract attributes in the form of `#[exonum(name = "value")]`
fn get_exonum_attributes(attrs: &[Attribute]) -> Vec<MetaNameValue> {
fn get_exonum_attributes(attrs: &[Attribute]) -> Vec<Meta> {
let exonum_meta = attrs
.iter()
.find_map(|attr| attr.parse_meta().ok().filter(|m| m.name() == "exonum"));
Expand All @@ -95,10 +124,26 @@ fn get_exonum_attributes(attrs: &[Attribute]) -> Vec<MetaNameValue> {
Some(Meta::List(MetaList { nested: list, .. })) => list
.into_iter()
.filter_map(|n| match n {
NestedMeta::Meta(Meta::NameValue(named)) => Some(named),
NestedMeta::Meta(meta) => Some(meta),
_ => None,
}).collect(),
Some(_) => panic!("`exonum` attribute should contain list of name value pairs"),
None => vec![],
}
}

fn get_exonum_name_value_attributes(attrs: &[Attribute]) -> Vec<MetaNameValue> {
get_exonum_attributes(attrs)
.into_iter()
.filter_map(|meta| match meta {
Meta::NameValue(name_value) => Some(name_value),
_ => None,
}).collect()
}

fn find_exonum_word_attribute(attrs: &[Attribute], ident_name: &str) -> bool {
get_exonum_attributes(attrs).iter().any(|meta| match meta {
Meta::Word(ident) if ident == ident_name => true,
_ => false,
})
}
46 changes: 42 additions & 4 deletions components/derive/src/pb_convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
// limitations under the License.

use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use proc_macro2::{self, Ident, Span};
use syn::{Attribute, Data, DeriveInput, Lit, Path};

use super::PB_CONVERT_ATTRIBUTE;
use super::{
find_exonum_word_attribute, get_exonum_name_value_attributes, get_exonum_types_prefix,
PB_CONVERT_ATTRIBUTE, SERDE_PB_CONVERT_ATTRIBUTE,
};

fn get_protobuf_struct_path(attrs: &[Attribute]) -> Path {
let map_attrs = super::get_exonum_attributes(attrs);
let map_attrs = get_exonum_name_value_attributes(attrs);
let struct_path = map_attrs.into_iter().find_map(|nv| {
if nv.ident == PB_CONVERT_ATTRIBUTE {
match nv.lit {
Expand Down Expand Up @@ -140,12 +143,37 @@ fn implement_storage_traits(name: &Ident, cr: &quote::ToTokens) -> impl quote::T
}
}

fn implement_serde_protobuf_convert(name: &Ident) -> proc_macro2::TokenStream {
quote! {
extern crate serde as _serde;

impl _serde::Serialize for #name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: _serde::Serializer,
{
self.to_pb().serialize(serializer)
}
}

impl<'de> _serde::Deserialize<'de> for #name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: _serde::Deserializer<'de>,
{
let pb = <#name as ProtobufConvert>::ProtoStruct::deserialize(deserializer)?;
ProtobufConvert::from_pb(pb).map_err(_serde::de::Error::custom)
}
}
}
}

pub fn implement_protobuf_convert(input: TokenStream) -> TokenStream {
let input: DeriveInput = syn::parse(input).unwrap();

let name = input.ident.clone();
let proto_struct_name = get_protobuf_struct_path(&input.attrs);
let cr = super::get_exonum_types_prefix(&input.attrs);
let cr = get_exonum_types_prefix(&input.attrs);

let mod_name = Ident::new(&format!("pb_convert_impl_{}", name), Span::call_site());

Expand All @@ -155,6 +183,15 @@ pub fn implement_protobuf_convert(input: TokenStream) -> TokenStream {
let binary_form = implement_binary_form(&name, &cr);
let storage_traits = implement_storage_traits(&name, &cr);

let serde_traits = {
let serde_needed = find_exonum_word_attribute(&input.attrs, SERDE_PB_CONVERT_ATTRIBUTE);
if serde_needed {
implement_serde_protobuf_convert(&name)
} else {
quote!()
}
};

let expanded = quote! {
mod #mod_name {
extern crate protobuf as _protobuf_crate;
Expand All @@ -169,6 +206,7 @@ pub fn implement_protobuf_convert(input: TokenStream) -> TokenStream {
#protobuf_convert
#binary_form
#storage_traits
#serde_traits
}
};

Expand Down
4 changes: 4 additions & 0 deletions examples/cryptocurrency-advanced/backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ hex = "=0.3.2"

[build-dependencies]
exonum-build = { version = "0.9.0", path = "../../../components/build" }

[features]
default = ["with-serde"]
with-serde = []
4 changes: 2 additions & 2 deletions examples/cryptocurrency-advanced/backend/src/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ impl From<Error> for ExecutionError {
}

/// Transfer `amount` of the currency from one wallet to another.
#[derive(Serialize, Deserialize, Clone, Debug, ProtobufConvert)]
#[exonum(pb = "proto::Transfer")]
#[derive(Clone, Debug, ProtobufConvert)]
#[exonum(pb = "proto::Transfer", serde_pb_convert)]
pub struct Transfer {
/// `PublicKey` of receiver's wallet.
pub to: PublicKey,
Expand Down
4 changes: 2 additions & 2 deletions examples/cryptocurrency-advanced/backend/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ use exonum::crypto::{Hash, PublicKey};
use super::proto;

/// Wallet information stored in the database.
#[derive(Serialize, Deserialize, Clone, Debug, ProtobufConvert)]
#[exonum(pb = "proto::Wallet")]
#[derive(Clone, Debug, ProtobufConvert)]
#[exonum(pb = "proto::Wallet", serde_pb_convert)]
pub struct Wallet {
/// `PublicKey` of the wallet.
pub pub_key: PublicKey,
Expand Down
4 changes: 4 additions & 0 deletions examples/timestamping/backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ pretty_assertions = "=0.5.1"

[build-dependencies]
exonum-build = { version = "0.9.0", path = "../../../components/build" }

[features]
default = ["with-serde"]
with-serde = []
4 changes: 2 additions & 2 deletions examples/timestamping/backend/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ impl Timestamp {
}

/// Timestamp entry.
#[derive(Serialize, Deserialize, Clone, Debug, ProtobufConvert)]
#[exonum(pb = "proto::TimestampEntry")]
#[derive(Clone, Debug, ProtobufConvert)]
#[exonum(pb = "proto::TimestampEntry", serde_pb_convert)]
pub struct TimestampEntry {
/// Timestamp data.
pub timestamp: Timestamp,
Expand Down

0 comments on commit 5b3ff40

Please sign in to comment.