├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── src ├── lib.rs └── pb_convert.rs └── tests ├── pb_convert.rs └── proto ├── message.proto └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # idea 13 | .idea 14 | 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "protobuf-convert" 3 | version = "0.4.0" 4 | edition = "2018" 5 | authors = ["The Exonum Team ", "Witnet Foundation "] 6 | repository = "https://github.com/witnet/protobuf-convert" 7 | documentation = "https://docs.rs/protobuf-convert" 8 | license = "Apache-2.0" 9 | keywords = ["protobuf", "macro"] 10 | categories = ["development-tools"] 11 | description = "Macros for convenient serialization of Rust data structures into/from Protocol Buffers" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | darling = "0.10.2" 18 | heck = "0.3.1" 19 | proc-macro2 = "1.0" 20 | quote = "1.0" 21 | semver = "0.9" 22 | syn = "1.0" 23 | 24 | [dev-dependencies] 25 | anyhow = "1.0" 26 | protobuf = "2.12" 27 | serde = { version = "1.0", features = ["derive"] } 28 | 29 | [build-dependencies] 30 | protoc-rust = "2.12" 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # protobuf-convert 2 | 3 | Macros for convenient serialization of Rust data structures into/from Protocol Buffers. 4 | 5 | ## Introduction 6 | 7 | This is a fork of [exonum-derive](https://crates.io/crates/exonum-derive) with 8 | some changes to allow easier integration with other projects, and some new 9 | features. 10 | 11 | ## Usage 12 | 13 | First, add the dependency in `Cargo.toml`: 14 | 15 | ```toml 16 | protobuf-convert = "0.4.0" 17 | ``` 18 | 19 | Then, define a `ProtobufConvert` trait: 20 | 21 | ```rust 22 | trait ProtobufConvert { 23 | /// Type of the protobuf clone of Self 24 | type ProtoStruct; 25 | 26 | /// Struct -> ProtoStruct 27 | fn to_pb(&self) -> Self::ProtoStruct; 28 | 29 | /// ProtoStruct -> Struct 30 | fn from_pb(pb: Self::ProtoStruct) -> Result; 31 | } 32 | ``` 33 | 34 | And to use it, import the trait and the macro: 35 | 36 | For example, given the following protobuf: 37 | 38 | ```protobuf 39 | message Ping { 40 | fixed64 nonce = 1; 41 | } 42 | ``` 43 | 44 | rust-protobuf will generate the following struct: 45 | 46 | ```rust 47 | #[derive(PartialEq,Clone,Default)] 48 | #[cfg_attr(feature = "with-serde", derive(Serialize, Deserialize))] 49 | pub struct Ping { 50 | // message fields 51 | pub nonce: u64, 52 | // special fields 53 | #[cfg_attr(feature = "with-serde", serde(skip))] 54 | pub unknown_fields: ::protobuf::UnknownFields, 55 | #[cfg_attr(feature = "with-serde", serde(skip))] 56 | pub cached_size: ::protobuf::CachedSize, 57 | } 58 | ``` 59 | 60 | We may want to convert that struct into a more idiomatic one, and derive more traits. 61 | This is the necessary code: 62 | 63 | ```rust 64 | // Import trait 65 | use crate::proto::ProtobufConvert; 66 | // Import macro 67 | use protobuf_convert::ProtobufConvert; 68 | // Import module autogenerated by protocol buffers 69 | use crate::proto::schema; 70 | 71 | #[derive(ProtobufConvert)] 72 | #[protobuf_convert(source = "schema::Ping")] 73 | struct Ping { 74 | nonce: u64, 75 | } 76 | ``` 77 | 78 | Note that the `ProtobufConvert` trait must be implemented for all the fields, 79 | see an example implementation for `u64`: 80 | 81 | ```rust 82 | impl ProtobufConvert for u64 { 83 | type ProtoStruct = u64; 84 | 85 | fn to_pb(&self) -> Self::ProtoStruct { 86 | *self 87 | } 88 | 89 | fn from_pb(pb: Self::ProtoStruct) -> Result { 90 | Ok(pb) 91 | } 92 | } 93 | ``` 94 | 95 | Now, converting between `Ping` and `schema::Ping` can be done effortlessly. 96 | 97 | ### `Enum` support 98 | 99 | A more complex example, featuring enums: 100 | 101 | ```protobuf 102 | message Ping { 103 | fixed64 nonce = 1; 104 | } 105 | message Pong { 106 | fixed64 nonce = 1; 107 | } 108 | message Message { 109 | oneof kind { 110 | Ping Ping = 1; 111 | Pong Pong = 2; 112 | } 113 | } 114 | ``` 115 | 116 | ```rust 117 | #[derive(ProtobufConvert)] 118 | #[protobuf_convert(source = "schema::Ping")] 119 | struct Ping { 120 | nonce: u64, 121 | } 122 | #[derive(ProtobufConvert)] 123 | #[protobuf_convert(source = "schema::Pong")] 124 | struct Pong { 125 | nonce: u64, 126 | } 127 | #[derive(ProtobufConvert)] 128 | #[protobuf_convert(source = "schema::Message")] 129 | enum Message { 130 | Ping(Ping), 131 | Pong(Pong), 132 | } 133 | ``` 134 | 135 | And it just works! 136 | 137 | You can also generate `From` and `TryFrom` traits for enum variants. Note that this will not work if enum has variants 138 | with the same field types. To use this feature add `impl_from_trait` attribute. 139 | 140 | ```rust 141 | #[derive(ProtobufConvert)] 142 | #[protobuf_convert(source = "schema::Message"), impl_from_trait] 143 | enum Message { 144 | Ping(Ping), 145 | Pong(Pong), 146 | } 147 | ``` 148 | 149 | `From`, `From` and also `TryFrom<..>` traits will be generated. 150 | 151 | Another attribute that can be used with enum is `rename`. It instructs macro to generate methods with case 152 | specified in attribute param. 153 | 154 | ```rust 155 | #[derive(ProtobufConvert)] 156 | #[protobuf_convert(source = "schema::Message"), rename(case = "snake_case")] 157 | enum Message { 158 | Ping(Ping), 159 | Pong(Pong), 160 | } 161 | ``` 162 | 163 | Currently, only snake case is supported. 164 | 165 | ### Skipping fields 166 | 167 | This macro also supports skipping fields in `struct`s so they are ignored when serializing, i.e they will not be mapped to any field in the schema: 168 | 169 | ```rust 170 | #[derive(ProtobufConvert)] 171 | #[protobuf_convert(source = "schema::Ping")] 172 | struct Ping { 173 | pub nonce: u64, 174 | #[protobuf_convert(skip)] 175 | my_private_field: u64 176 | } 177 | ``` 178 | 179 | Note that you can only skip fields whose type implements the `Default` trait. 180 | 181 | ### Overriding conversion rules 182 | 183 | This macro also supports serde-like attribute `with` for modules with the custom implementation of `from_pb` and `to_pb` conversions. 184 | 185 | `protobuf-convert` will use functions `$module::from_pb` and `$module::to_pb` instead of `ProtobufConvert` trait for the specified field. 186 | 187 | ```rust 188 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 189 | enum CustomId { 190 | First = 5, 191 | Second = 15, 192 | Third = 35, 193 | } 194 | 195 | #[derive(Debug, Clone, ProtobufConvert, Eq, PartialEq)] 196 | #[protobuf_convert(source = "proto::SimpleMessage")] 197 | struct CustomMessage { 198 | #[protobuf_convert(with = "custom_id_pb_convert")] 199 | id: Option, 200 | name: String, 201 | } 202 | 203 | mod custom_id_pb_convert { 204 | use super::*; 205 | 206 | pub(super) fn from_pb(pb: u32) -> Result, anyhow::Error> { 207 | match pb { 208 | 0 => Ok(None), 209 | 5 => Ok(Some(CustomId::First)), 210 | 15 => Ok(Some(CustomId::Second)), 211 | 35 => Ok(Some(CustomId::Third)), 212 | other => Err(anyhow::anyhow!("Unknown enum discriminant: {}", other)), 213 | } 214 | } 215 | 216 | pub(super) fn to_pb(v: &Option) -> u32 { 217 | match v { 218 | Some(id) => *id as u32, 219 | None => 0, 220 | } 221 | } 222 | } 223 | ``` 224 | 225 | ## See also 226 | 227 | * [rust-protobuf](https://github.com/stepancheg/rust-protobuf) 228 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team, 2019 Witnet Foundation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use protoc_rust::Customize; 16 | 17 | use std::{fs, io::prelude::*, path::Path}; 18 | 19 | fn main() { 20 | let out_dir = std::env::var("OUT_DIR").expect("Unable to get OUT_DIR"); 21 | 22 | protoc_rust::run(protoc_rust::Args { 23 | out_dir: &out_dir, 24 | input: &["tests/proto/message.proto"], 25 | includes: &["tests/proto"], 26 | customize: Customize { 27 | ..Default::default() 28 | }, 29 | }) 30 | .expect("Couldn't compile proto sources"); 31 | 32 | let mod_file_content = r#" 33 | pub use self::message::*; 34 | 35 | mod message;"#; 36 | let mod_file_path = Path::new(&out_dir).join("mod.rs"); 37 | 38 | let mut file = fs::File::create(&mod_file_path).expect("Unable to create mod.rs file"); 39 | file.write_all(&mod_file_content.to_string().as_ref()) 40 | .expect("Unable to write mod.rs file"); 41 | } 42 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team, 2019 Witnet Foundation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![recursion_limit = "256"] 16 | 17 | extern crate proc_macro; 18 | 19 | mod pb_convert; 20 | 21 | use proc_macro::TokenStream; 22 | use syn::{Attribute, NestedMeta}; 23 | 24 | const PB_CONVERT_ATTRIBUTE: &str = "protobuf_convert"; 25 | const PB_SNAKE_CASE_ATTRIBUTE: &str = "snake_case"; 26 | const DEFAULT_ONEOF_FIELD_NAME: &str = "kind"; 27 | 28 | /// ProtobufConvert derive macro. 29 | /// 30 | /// Attributes: 31 | /// 32 | /// ## Required 33 | /// 34 | /// * `#[protobuf_convert(source = "path")]` 35 | /// 36 | /// ```ignore 37 | /// #[derive(Clone, Debug, ProtobufConvert)] 38 | /// #[protobuf_convert(source = "proto::Message")] 39 | /// pub struct Message { 40 | /// /// Message author id. 41 | /// pub author: u32, 42 | /// /// Message text. 43 | /// pub text: String, 44 | /// } 45 | /// 46 | /// let msg = Message::new(); 47 | /// let serialized_msg = msg.to_pb(); 48 | /// 49 | /// let deserialized_msg = ProtobufConvert::from_pb(serialized_msg).unwrap(); 50 | /// assert_eq!(msg, deserialized_msg); 51 | /// ``` 52 | /// 53 | /// Corresponding proto file: 54 | /// ```proto 55 | /// message Message { 56 | /// // Message author id.. 57 | /// uint32 author = 1; 58 | /// // Message text. 59 | /// string text = 2; 60 | /// } 61 | /// ``` 62 | /// 63 | /// This macro can also be applied to enums. In proto files enums are represented 64 | /// by `oneof` field. You can specify `oneof` field name, default is "kind". 65 | /// Corresponding proto file must contain only this oneof field. Possible enum 66 | /// variants are zero-field and one-field variants. 67 | /// Another enum attribute is `impl_from_trait`. If you specify it then `From` and `TryFrom` 68 | /// traits for enum variants will be generated. Note that this will not work if enum has 69 | /// variants with the same field types. 70 | /// ```ignore 71 | /// #[derive(Debug, Clone, ProtobufConvert)] 72 | /// #[protobuf_convert(source = "proto::Message", oneof_field = "message")] 73 | /// pub enum Message { 74 | /// /// Plain message. 75 | /// Plain(String), 76 | /// /// Encoded message. 77 | /// Encoded(String), 78 | /// } 79 | /// ``` 80 | /// 81 | /// Corresponding proto file: 82 | /// ```proto 83 | /// message Message { 84 | /// oneof message { 85 | /// // Plain message. 86 | /// string plain = 1; 87 | /// // Encoded message. 88 | /// string encoded = 2; 89 | /// } 90 | /// } 91 | /// ``` 92 | /// 93 | /// Path is the name of the corresponding protobuf generated struct. 94 | /// 95 | /// * `#[protobuf_convert(source = "path", serde_pb_convert)]` 96 | /// 97 | /// Implement `serde::{Serialize, Deserialize}` using structs that were generated with 98 | /// protobuf. 99 | /// For example, it should be used if you want json representation of your struct 100 | /// to be compatible with protobuf representation (including proper nesting of fields). 101 | /// For example, struct with `crypto::Hash` with this 102 | /// (de)serializer will be represented as 103 | /// ```text 104 | /// StructName { 105 | /// "hash": { 106 | /// "data": [1, 2, ...] 107 | /// }, 108 | /// // ... 109 | /// } 110 | /// // With default (de)serializer. 111 | /// StructName { 112 | /// "hash": "12af..." // HEX 113 | /// // ... 114 | /// } 115 | /// ``` 116 | #[proc_macro_derive(ProtobufConvert, attributes(protobuf_convert))] 117 | pub fn generate_protobuf_convert(input: TokenStream) -> TokenStream { 118 | pb_convert::implement_protobuf_convert(input) 119 | } 120 | 121 | pub(crate) fn find_protobuf_convert_meta(args: &[Attribute]) -> Option { 122 | args.as_ref() 123 | .iter() 124 | .filter_map(|a| a.parse_meta().ok()) 125 | .find(|m| m.path().is_ident(PB_CONVERT_ATTRIBUTE)) 126 | .map(NestedMeta::from) 127 | } 128 | -------------------------------------------------------------------------------- /src/pb_convert.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team, 2019 Witnet Foundation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use darling::{FromDeriveInput, FromMeta}; 16 | use heck::SnakeCase; 17 | use proc_macro::TokenStream; 18 | use proc_macro2::{Ident, Span}; 19 | use quote::{quote, ToTokens}; 20 | use syn::{Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Path, Type, Variant}; 21 | 22 | use std::convert::TryFrom; 23 | 24 | use super::{find_protobuf_convert_meta, DEFAULT_ONEOF_FIELD_NAME, PB_SNAKE_CASE_ATTRIBUTE}; 25 | 26 | #[derive(Debug, FromMeta)] 27 | #[darling(default)] 28 | struct ProtobufConvertStructAttrs { 29 | source: Option, 30 | serde_pb_convert: bool, 31 | } 32 | 33 | impl Default for ProtobufConvertStructAttrs { 34 | fn default() -> Self { 35 | Self { 36 | source: None, 37 | serde_pb_convert: false, 38 | } 39 | } 40 | } 41 | 42 | impl TryFrom<&[Attribute]> for ProtobufConvertStructAttrs { 43 | type Error = darling::Error; 44 | 45 | fn try_from(args: &[Attribute]) -> Result { 46 | find_protobuf_convert_meta(args) 47 | .map(|meta| Self::from_nested_meta(&meta)) 48 | .unwrap_or_else(|| Ok(Self::default())) 49 | } 50 | } 51 | 52 | #[derive(Debug, FromMeta)] 53 | #[darling(default)] 54 | struct ProtobufConvertEnumAttrs { 55 | source: Option, 56 | serde_pb_convert: bool, 57 | impl_from_trait: bool, 58 | rename: Rename, 59 | oneof_field: Ident, 60 | } 61 | 62 | impl Default for ProtobufConvertEnumAttrs { 63 | fn default() -> Self { 64 | Self { 65 | source: None, 66 | oneof_field: syn::parse_str(DEFAULT_ONEOF_FIELD_NAME).unwrap(), 67 | serde_pb_convert: false, 68 | impl_from_trait: false, 69 | rename: Default::default(), 70 | } 71 | } 72 | } 73 | 74 | impl TryFrom<&[Attribute]> for ProtobufConvertEnumAttrs { 75 | type Error = darling::Error; 76 | 77 | fn try_from(args: &[Attribute]) -> Result { 78 | find_protobuf_convert_meta(args) 79 | .map(|meta| Self::from_nested_meta(&meta)) 80 | .unwrap_or_else(|| Ok(Self::default())) 81 | } 82 | } 83 | 84 | #[derive(Debug)] 85 | struct ProtobufConvertStruct { 86 | name: Ident, 87 | fields: Vec<(Ident, ProtobufConvertFieldAttrs)>, 88 | attrs: ProtobufConvertStructAttrs, 89 | } 90 | 91 | #[derive(Debug, FromMeta, Default)] 92 | #[darling(default)] 93 | struct ProtobufConvertFieldAttrs { 94 | skip: bool, 95 | with: Option, 96 | } 97 | 98 | impl TryFrom<&[Attribute]> for ProtobufConvertFieldAttrs { 99 | type Error = darling::Error; 100 | 101 | fn try_from(args: &[Attribute]) -> Result { 102 | find_protobuf_convert_meta(args) 103 | .map(|meta| Self::from_nested_meta(&meta)) 104 | .unwrap_or_else(|| Ok(Self::default())) 105 | } 106 | } 107 | 108 | fn get_field_names( 109 | data: &DataStruct, 110 | ) -> Result, darling::Error> { 111 | data.fields 112 | .iter() 113 | .map(|f| { 114 | let attrs = ProtobufConvertFieldAttrs::try_from(f.attrs.as_ref())?; 115 | let ident = f.ident.clone().ok_or_else(|| { 116 | darling::Error::unsupported_shape("Struct fields must have an identifier.") 117 | })?; 118 | 119 | Ok((ident, attrs)) 120 | }) 121 | .collect() 122 | } 123 | 124 | impl ProtobufConvertStruct { 125 | fn from_derive_input( 126 | name: Ident, 127 | data: &DataStruct, 128 | attrs: &[Attribute], 129 | ) -> Result { 130 | let attrs = ProtobufConvertStructAttrs::try_from(attrs)?; 131 | let fields = get_field_names(data)?; 132 | 133 | Ok(Self { 134 | name, 135 | attrs, 136 | fields, 137 | }) 138 | } 139 | } 140 | 141 | impl ProtobufConvertFieldAttrs { 142 | fn impl_field_setter(&self, ident: &Ident) -> impl ToTokens { 143 | let pb_getter = Ident::new(&format!("get_{}", ident), Span::call_site()); 144 | 145 | let setter = match (self.skip, &self.with) { 146 | // Usual setter. 147 | (false, None) => quote! { ProtobufConvert::from_pb(pb.#pb_getter().to_owned())? }, 148 | // Setter with the overridden Protobuf conversion. 149 | (false, Some(with)) => quote! { #with::from_pb(pb.#pb_getter().to_owned())? }, 150 | // Default setter for the skipped fields. 151 | (true, _) => quote! { Default::default() }, 152 | }; 153 | 154 | quote! { #ident: #setter, } 155 | } 156 | 157 | fn impl_field_getter(&self, ident: &Ident) -> impl ToTokens { 158 | let pb_setter = Ident::new(&format!("set_{}", ident), Span::call_site()); 159 | 160 | match (self.skip, &self.with) { 161 | // Usual getter. 162 | (false, None) => quote! { 163 | msg.#pb_setter(ProtobufConvert::to_pb(&self.#ident).into()); 164 | }, 165 | // Getter with the overridden Protobuf conversion. 166 | (false, Some(with)) => quote! { 167 | msg.#pb_setter(#with::to_pb(&self.#ident).into()); 168 | }, 169 | // Skipped getter does nothing. 170 | (true, _) => quote! {}, 171 | } 172 | } 173 | } 174 | 175 | impl ToTokens for ProtobufConvertStruct { 176 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 177 | let name = &self.name; 178 | let pb_name = &self.attrs.source; 179 | 180 | let from_pb_impl = { 181 | let fields = self 182 | .fields 183 | .iter() 184 | .map(|(ident, attrs)| attrs.impl_field_setter(ident)); 185 | 186 | quote! { 187 | let inner = Self { 188 | #(#fields)* 189 | }; 190 | Ok(inner) 191 | } 192 | }; 193 | 194 | let to_pb_impl = { 195 | let fields = self 196 | .fields 197 | .iter() 198 | .map(|(ident, attrs)| attrs.impl_field_getter(ident)); 199 | 200 | quote! { 201 | let mut msg = Self::ProtoStruct::default(); 202 | #(#fields)* 203 | msg 204 | } 205 | }; 206 | 207 | let expanded = quote! { 208 | impl ProtobufConvert for #name { 209 | type ProtoStruct = #pb_name; 210 | 211 | fn from_pb(pb: Self::ProtoStruct) -> std::result::Result { 212 | #from_pb_impl 213 | } 214 | 215 | fn to_pb(&self) -> Self::ProtoStruct { 216 | #to_pb_impl 217 | } 218 | } 219 | }; 220 | tokens.extend(expanded); 221 | } 222 | } 223 | 224 | #[derive(Debug)] 225 | struct ParsedVariant { 226 | name: Ident, 227 | field_name: Path, 228 | } 229 | 230 | impl TryFrom<&Variant> for ParsedVariant { 231 | type Error = darling::Error; 232 | 233 | fn try_from(value: &Variant) -> Result { 234 | let name = value.ident.clone(); 235 | let field_name = match &value.fields { 236 | Fields::Unnamed(fields) => { 237 | if fields.unnamed.len() != 1 { 238 | return Err(darling::Error::unsupported_shape( 239 | "Too many fields in the enum variant", 240 | )); 241 | } 242 | 243 | match &fields.unnamed.first().unwrap().ty { 244 | Type::Path(type_path) => Ok(type_path.path.clone()), 245 | _ => Err(darling::Error::unsupported_shape( 246 | "Only variants in form Foo(Bar) are supported.", 247 | )), 248 | } 249 | } 250 | _ => Err(darling::Error::unsupported_shape( 251 | "Only variants in form Foo(Bar) are supported.", 252 | )), 253 | }?; 254 | 255 | Ok(Self { name, field_name }) 256 | } 257 | } 258 | 259 | #[derive(Debug)] 260 | struct ProtobufConvertEnum { 261 | name: Ident, 262 | variants: Vec, 263 | attrs: ProtobufConvertEnumAttrs, 264 | } 265 | 266 | #[derive(Debug, Default, FromMeta)] 267 | #[darling(default)] 268 | pub struct Rename { 269 | case: Option, 270 | } 271 | 272 | impl ProtobufConvertEnum { 273 | fn from_derive_input( 274 | name: Ident, 275 | data: &DataEnum, 276 | attrs: &[Attribute], 277 | ) -> Result { 278 | let attrs = ProtobufConvertEnumAttrs::try_from(attrs)?; 279 | let variants = data 280 | .variants 281 | .iter() 282 | .map(ParsedVariant::try_from) 283 | .collect::, _>>()?; 284 | 285 | Ok(Self { 286 | name, 287 | attrs, 288 | variants, 289 | }) 290 | } 291 | 292 | fn impl_protobuf_convert(&self) -> impl ToTokens { 293 | let pb_oneof_enum = { 294 | let mut pb = self.attrs.source.clone().unwrap(); 295 | let oneof = pb.segments.pop().unwrap().value().ident.clone(); 296 | let oneof_enum = Ident::new( 297 | &format!("{}_oneof_{}", oneof, &self.attrs.oneof_field), 298 | Span::call_site(), 299 | ); 300 | quote! { #pb #oneof_enum } 301 | }; 302 | let name = &self.name; 303 | let pb_name = &self.attrs.source; 304 | let oneof = &self.attrs.oneof_field; 305 | 306 | let from_pb_impl = { 307 | let match_arms = self.variants.iter().map(|variant| { 308 | let variant_name = self.get_variant_name(variant); 309 | let pb_variant = Ident::new(variant_name.as_ref(), Span::call_site()); 310 | let variant_name = &variant.name; 311 | let field_name = &variant.field_name; 312 | 313 | quote! { 314 | Some(#pb_oneof_enum::#pb_variant(pb)) => { 315 | #field_name::from_pb(pb).map(#name::#variant_name) 316 | } 317 | } 318 | }); 319 | 320 | quote! { 321 | match pb.#oneof { 322 | #( #match_arms )* 323 | None => Err(anyhow::anyhow!("Failed to decode #name from protobuf")) 324 | } 325 | } 326 | }; 327 | let to_pb_impl = { 328 | let match_arms = self.variants.iter().map(|variant| { 329 | let pb_variant = self.get_variant_name(variant); 330 | let variant_name = &variant.name; 331 | 332 | let setter = Ident::new(&format!("set_{}", pb_variant), Span::call_site()); 333 | quote! { 334 | #name::#variant_name(msg) => inner.#setter(msg.to_pb()), 335 | } 336 | }); 337 | 338 | quote! { 339 | let mut inner = Self::ProtoStruct::new(); 340 | match self { 341 | #( #match_arms )* 342 | } 343 | inner 344 | } 345 | }; 346 | 347 | quote! { 348 | impl ProtobufConvert for #name { 349 | type ProtoStruct = #pb_name; 350 | 351 | fn from_pb(mut pb: Self::ProtoStruct) -> std::result::Result { 352 | #from_pb_impl 353 | } 354 | 355 | fn to_pb(&self) -> Self::ProtoStruct { 356 | #to_pb_impl 357 | } 358 | } 359 | } 360 | } 361 | 362 | fn impl_enum_conversions(&self) -> impl ToTokens { 363 | let name = &self.name; 364 | 365 | if self.attrs.impl_from_trait { 366 | let conversions = self.variants.iter().map(|variant| { 367 | let variant_name = &variant.name; 368 | let field_name = &variant.field_name; 369 | let variant_err = format!("Expected variant {}, but got {{:?}}", variant_name); 370 | 371 | quote! { 372 | impl From<#field_name> for #name { 373 | fn from(variant: #field_name) -> Self { 374 | #name::#variant_name(variant) 375 | } 376 | } 377 | 378 | impl std::convert::TryFrom<#name> for #field_name { 379 | type Error = anyhow::Error; 380 | 381 | fn try_from(msg: #name) -> Result { 382 | if let #name::#variant_name(inner) = msg { 383 | Ok(inner) 384 | } else { 385 | Err(anyhow::anyhow!( 386 | #variant_err, msg 387 | )) 388 | } 389 | } 390 | } 391 | } 392 | }); 393 | 394 | quote! { 395 | #( #conversions )* 396 | } 397 | } else { 398 | quote! {} 399 | } 400 | } 401 | 402 | fn get_variant_name(&self, variant: &ParsedVariant) -> String { 403 | if let Some(case) = self.attrs.rename.case.as_ref() { 404 | if case == PB_SNAKE_CASE_ATTRIBUTE { 405 | return variant.name.to_string().to_snake_case(); 406 | } else { 407 | panic!("Undefined case type") 408 | } 409 | } 410 | 411 | variant.name.to_string() 412 | } 413 | } 414 | 415 | impl ToTokens for ProtobufConvertEnum { 416 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 417 | let pb_convert = self.impl_protobuf_convert(); 418 | let conversions = self.impl_enum_conversions(); 419 | 420 | let expanded = quote! { 421 | #pb_convert 422 | #conversions 423 | }; 424 | tokens.extend(expanded) 425 | } 426 | } 427 | 428 | #[derive(Debug)] 429 | enum ProtobufConvert { 430 | Enum(ProtobufConvertEnum), 431 | Struct(ProtobufConvertStruct), 432 | } 433 | 434 | impl FromDeriveInput for ProtobufConvert { 435 | fn from_derive_input(input: &DeriveInput) -> Result { 436 | match &input.data { 437 | Data::Struct(data) => Ok(ProtobufConvert::Struct( 438 | ProtobufConvertStruct::from_derive_input( 439 | input.ident.clone(), 440 | data, 441 | input.attrs.as_ref(), 442 | )?, 443 | )), 444 | Data::Enum(data) => Ok(ProtobufConvert::Enum( 445 | ProtobufConvertEnum::from_derive_input( 446 | input.ident.clone(), 447 | data, 448 | input.attrs.as_ref(), 449 | )?, 450 | )), 451 | _ => Err(darling::Error::unsupported_shape( 452 | "Only for enums and structs.", 453 | )), 454 | } 455 | } 456 | } 457 | 458 | impl ProtobufConvert { 459 | fn name(&self) -> &Ident { 460 | match self { 461 | ProtobufConvert::Enum(inner) => &inner.name, 462 | ProtobufConvert::Struct(inner) => &inner.name, 463 | } 464 | } 465 | 466 | fn serde_needed(&self) -> bool { 467 | match self { 468 | ProtobufConvert::Enum(inner) => inner.attrs.serde_pb_convert, 469 | ProtobufConvert::Struct(inner) => inner.attrs.serde_pb_convert, 470 | } 471 | } 472 | 473 | fn implement_serde_protobuf_convert(&self) -> impl ToTokens { 474 | let name = self.name(); 475 | quote! { 476 | impl serde::Serialize for #name { 477 | fn serialize(&self, serializer: S) -> Result 478 | where 479 | S: serde::Serializer, 480 | { 481 | self.to_pb().serialize(serializer) 482 | } 483 | } 484 | 485 | impl<'de> serde::Deserialize<'de> for #name { 486 | fn deserialize(deserializer: D) -> Result 487 | where 488 | D: serde::Deserializer<'de>, 489 | { 490 | let pb = <#name as ProtobufConvert>::ProtoStruct::deserialize(deserializer)?; 491 | ProtobufConvert::from_pb(pb).map_err(serde::de::Error::custom) 492 | } 493 | } 494 | } 495 | } 496 | 497 | fn implement_protobuf_convert(&self) -> impl ToTokens { 498 | match self { 499 | ProtobufConvert::Enum(data) => quote! { #data }, 500 | ProtobufConvert::Struct(data) => quote! { #data }, 501 | } 502 | } 503 | } 504 | 505 | impl ToTokens for ProtobufConvert { 506 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 507 | let mod_name = Ident::new( 508 | &format!("pb_convert_impl_{}", self.name()), 509 | Span::call_site(), 510 | ); 511 | let protobuf_convert = self.implement_protobuf_convert(); 512 | let serde_traits = if self.serde_needed() { 513 | let serde = self.implement_serde_protobuf_convert(); 514 | quote! { #serde } 515 | } else { 516 | quote! {} 517 | }; 518 | 519 | let expanded = quote! { 520 | mod #mod_name { 521 | use super::*; 522 | 523 | use protobuf::Message as _ProtobufMessage; 524 | 525 | #protobuf_convert 526 | #serde_traits 527 | } 528 | }; 529 | tokens.extend(expanded) 530 | } 531 | } 532 | 533 | pub fn implement_protobuf_convert(input: TokenStream) -> TokenStream { 534 | let input = ProtobufConvert::from_derive_input(&syn::parse(input).unwrap()) 535 | .unwrap_or_else(|e| panic!("ProtobufConvert: {}", e)); 536 | let tokens = quote! {#input}; 537 | tokens.into() 538 | } 539 | -------------------------------------------------------------------------------- /tests/pb_convert.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team, 2019 Witnet Foundation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use serde::{Deserialize, Serialize}; 16 | 17 | use crate::proto::ProtobufConvert; 18 | use protobuf_convert::ProtobufConvert; 19 | use std::convert::TryFrom; 20 | 21 | mod proto; 22 | 23 | #[derive(Debug, Clone, ProtobufConvert, Eq, PartialEq, Serialize, Deserialize)] 24 | #[protobuf_convert(source = "proto::SkipFieldsMessage")] 25 | struct SkipFieldsMessage { 26 | id: u32, 27 | #[protobuf_convert(skip)] 28 | #[serde(skip)] 29 | name: String, 30 | } 31 | 32 | #[derive(Debug, Clone, ProtobufConvert, Eq, PartialEq)] 33 | #[protobuf_convert(source = "proto::SimpleMessage")] 34 | struct Message { 35 | id: u32, 36 | name: String, 37 | } 38 | 39 | #[derive(Debug, ProtobufConvert, Eq, PartialEq)] 40 | #[protobuf_convert( 41 | source = "proto::EnumMessage", 42 | impl_from_trait, 43 | rename(case = "snake_case") 44 | )] 45 | enum EnumMessage { 46 | Simple(Message), 47 | Skip(SkipFieldsMessage), 48 | } 49 | 50 | #[derive(Debug, ProtobufConvert, Eq, PartialEq)] 51 | #[protobuf_convert( 52 | source = "proto::EnumMessageWithSimilarFields", 53 | rename(case = "snake_case") 54 | )] 55 | enum EnumMessageWithSimilarFields { 56 | Simple(Message), 57 | Skip(Message), 58 | } 59 | 60 | #[derive(Debug, ProtobufConvert, Eq, PartialEq)] 61 | #[protobuf_convert(source = "proto::EnumMessageWithUpperCaseField")] 62 | enum EnumMessageWithUpperCaseField { 63 | Simple(Message), 64 | } 65 | 66 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 67 | enum CustomId { 68 | First = 5, 69 | Second = 15, 70 | Third = 35, 71 | } 72 | 73 | #[derive(Debug, Clone, ProtobufConvert, Eq, PartialEq)] 74 | #[protobuf_convert(source = "proto::SimpleMessage")] 75 | struct CustomMessage { 76 | #[protobuf_convert(with = "custom_id_pb_convert")] 77 | id: Option, 78 | name: String, 79 | } 80 | 81 | mod custom_id_pb_convert { 82 | use super::*; 83 | 84 | pub(super) fn from_pb(pb: u32) -> Result, anyhow::Error> { 85 | match pb { 86 | 0 => Ok(None), 87 | 5 => Ok(Some(CustomId::First)), 88 | 15 => Ok(Some(CustomId::Second)), 89 | 35 => Ok(Some(CustomId::Third)), 90 | other => Err(anyhow::anyhow!("Unknown enum discriminant: {}", other)), 91 | } 92 | } 93 | 94 | #[allow(clippy::trivially_copy_pass_by_ref)] 95 | pub(super) fn to_pb(v: &Option) -> u32 { 96 | match v { 97 | Some(id) => *id as u32, 98 | None => 0, 99 | } 100 | } 101 | } 102 | 103 | #[test] 104 | fn simple_message_roundtrip() { 105 | let message = Message { 106 | id: 1, 107 | name: "SimpleMessage".into(), 108 | }; 109 | let pb_message = message.to_pb(); 110 | let de_message = Message::from_pb(pb_message).unwrap(); 111 | 112 | assert_eq!(message, de_message); 113 | } 114 | 115 | #[test] 116 | fn skip_field_message() { 117 | let message = SkipFieldsMessage { 118 | id: 1, 119 | name: "SimpleMessage".into(), 120 | }; 121 | let pb_message = message.to_pb(); 122 | let de_message = SkipFieldsMessage::from_pb(pb_message).unwrap(); 123 | 124 | assert_eq!(message.id, de_message.id); 125 | assert!(de_message.name.is_empty()); 126 | } 127 | 128 | #[test] 129 | fn custom_message_roundtrip() { 130 | let message = CustomMessage { 131 | id: None, 132 | name: "SimpleMessage".into(), 133 | }; 134 | let pb_message = message.to_pb(); 135 | let de_message = CustomMessage::from_pb(pb_message).unwrap(); 136 | 137 | assert_eq!(message, de_message); 138 | 139 | // Check `from_pb` with the unknown enum discriminant. 140 | let message = Message { 141 | id: 12, 142 | name: "Weird message".into(), 143 | }; 144 | let pb_message = message.to_pb(); 145 | 146 | let e = CustomMessage::from_pb(pb_message).unwrap_err(); 147 | assert_eq!(e.to_string(), "Unknown enum discriminant: 12") 148 | } 149 | 150 | #[test] 151 | fn enum_message() { 152 | let message = SkipFieldsMessage { 153 | id: 1, 154 | name: "SimpleMessage".into(), 155 | }; 156 | 157 | let enum_message = EnumMessage::Skip(message.clone()); 158 | 159 | let pb_message = enum_message.to_pb(); 160 | let de_message = EnumMessage::from_pb(pb_message).unwrap(); 161 | 162 | match de_message { 163 | EnumMessage::Skip(msg) => assert_eq!(msg.id, message.id), 164 | _ => panic!("Deserialized message has wrong type"), 165 | } 166 | } 167 | 168 | #[test] 169 | fn from_trait() { 170 | let message = Message { 171 | id: 1, 172 | name: "message".into(), 173 | }; 174 | let converted = EnumMessage::from(message.clone()); 175 | 176 | match converted { 177 | EnumMessage::Simple(msg) => assert_eq!(msg.id, message.id), 178 | _ => panic!("Converted message has wrong type"), 179 | }; 180 | 181 | let skip = SkipFieldsMessage { 182 | id: 1, 183 | name: "skip".into(), 184 | }; 185 | let converted = EnumMessage::from(skip.clone()); 186 | let err = Message::try_from(converted).unwrap_err(); 187 | 188 | assert!(err 189 | .to_string() 190 | .contains("Expected variant Simple, but got Skip")); 191 | } 192 | -------------------------------------------------------------------------------- /tests/proto/message.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team, 2019 Witnet Foundation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | message SimpleMessage { 18 | uint32 id = 1; 19 | string name = 2; 20 | } 21 | 22 | message SkipFieldsMessage { 23 | uint32 id = 1; 24 | } 25 | 26 | message EnumMessage { 27 | oneof kind { 28 | SimpleMessage simple = 1; 29 | SkipFieldsMessage skip = 2; 30 | } 31 | } 32 | 33 | message EnumMessageWithSimilarFields { 34 | oneof kind { 35 | SimpleMessage simple = 1; 36 | SimpleMessage skip = 2; 37 | } 38 | } 39 | 40 | message EnumMessageWithUpperCaseField { 41 | oneof kind { 42 | SimpleMessage Simple = 1; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/proto/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Exonum Team, 2019 Witnet Foundation 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub use message::*; 16 | 17 | use anyhow::Error; 18 | 19 | pub mod message { 20 | include!(concat!(env!("OUT_DIR"), "/mod.rs")); 21 | } 22 | 23 | pub trait ProtobufConvert: Sized { 24 | /// Type of the protobuf clone of Self 25 | type ProtoStruct; 26 | 27 | /// Struct -> ProtoStruct 28 | fn to_pb(&self) -> Self::ProtoStruct; 29 | 30 | /// ProtoStruct -> Struct 31 | fn from_pb(pb: Self::ProtoStruct) -> Result; 32 | } 33 | 34 | impl ProtobufConvert for String { 35 | type ProtoStruct = Self; 36 | 37 | fn to_pb(&self) -> Self::ProtoStruct { 38 | self.clone() 39 | } 40 | 41 | fn from_pb(pb: Self::ProtoStruct) -> Result { 42 | Ok(pb) 43 | } 44 | } 45 | 46 | impl ProtobufConvert for u32 { 47 | type ProtoStruct = Self; 48 | 49 | fn to_pb(&self) -> Self::ProtoStruct { 50 | u32::from(*self) 51 | } 52 | 53 | fn from_pb(pb: Self::ProtoStruct) -> Result { 54 | Ok(pb) 55 | } 56 | } 57 | --------------------------------------------------------------------------------