diff_enum/
lib.rs

1//! Attribute macro to define enum by differences of variants with useful accessors
2//!
3//! This is a small Rust library provides one attribute macro `#[diff_enum::common_fields]` to help defining
4//! `enum` variants by their differences. It is useful when you need to handle data which are almost the
5//! same, but different partially.
6//!
7//! By the attribute macro, common fields among all variants and different fields for each variant can
8//! be defined separately. Common fields are defined once. Additionally accessor methods for common fields
9//! are automatically defined.
10//!
11//! For example,
12//!
13//! ```rust
14//! extern crate diff_enum;
15//! use diff_enum::common_fields;
16//!
17//! #[common_fields {
18//!     user: String,
19//!     name: String,
20//!     stars: u32,
21//!     issues: u32,
22//! }]
23//! #[derive(Debug)]
24//! enum RemoteRepo {
25//!     GitHub {
26//!         language: String,
27//!         pull_requests: u32,
28//!     },
29//!     GitLab {
30//!         merge_requests: u32,
31//!     },
32//! }
33//! # let repo = RemoteRepo::GitHub {
34//! #     user: "rust-lang".to_string(),
35//! #     name: "rust".to_string(),
36//! #     language: "rust".to_string(),
37//! #     issues: 4536,
38//! #     pull_requests: 129,
39//! #     stars: 33679,
40//! # };
41//!
42//! # println!("User: {}", repo.user());
43//! ```
44//!
45//! is expanded to
46//!
47//! ```rust,ignore
48//! #[derive(Debug)]
49//! enum RemoteRepo {
50//!     GitHub {
51//!         language: String,
52//!         pull_requests: u32,
53//!         user: String,
54//!         name: String,
55//!         stars: u32,
56//!         issues: u32,
57//!     },
58//!     GitLab {
59//!         merge_requests: u32,
60//!         user: String,
61//!         name: String,
62//!         stars: u32,
63//!         issues: u32,
64//!     },
65//! }
66//! ```
67//!
68//! Additionally, accessor functions are defined for common fields. For example,
69//!
70//! ```rust
71//! # extern crate diff_enum;
72//! # use diff_enum::common_fields;
73//!
74//! # #[common_fields {
75//! #     user: String,
76//! #     name: String,
77//! #     stars: u32,
78//! #     issues: u32,
79//! # }]
80//! # #[derive(Debug)]
81//! # enum RemoteRepo {
82//! #     GitHub {
83//! #         language: String,
84//! #         pull_requests: u32,
85//! #     },
86//! #     GitLab {
87//! #         merge_requests: u32,
88//! #     },
89//! # }
90//! let repo = RemoteRepo::GitHub {
91//!     user: "rust-lang".to_string(),
92//!     name: "rust".to_string(),
93//!     language: "rust".to_string(),
94//!     issues: 4536,
95//!     pull_requests: 129,
96//!     stars: 33679,
97//! };
98//!
99//! println!("User: {}", repo.user());
100//! ```
101//!
102//!
103//!
104//! ## Alternative
105//!
106//! Without this crate, it's typical to separate the data into a struct with common fields and a enum
107//! variants for differences.
108//!
109//! For above `RemoteRepo` example,
110//!
111//! ```rust,ignore
112//! enum RemoteRepoKind {
113//!     GitHub {
114//!         language: String,
115//!         pull_requests: u32,
116//!     },
117//!     GitLab {
118//!         merge_requests: u32,
119//!     },
120//! }
121//! struct RemoteRepo {
122//!     user: String,
123//!     name: String,
124//!     stars: u32,
125//!     issues: u32,
126//!     kind: RemoteRepoKind,
127//! }
128//! ```
129//!
130//! This solution has problems as follows:
131//!
132//! - Fields are split into 2 parts for the reason of Rust enum. Essentially number of issues and number
133//!   of pull requests are both properties of a GitHub repository. As natural data structure they should
134//!   be in the same flat struct.
135//! - Naming the inner enum is difficult. Here I used 'Kind' to separate parts. But is it appropriate?
136//!   'Kind' is too generic name with weak meaning. The weak name comes from awkwardness of the data
137//!   structure.
138//!
139//! ## Usage
140//!
141//! At first, please load the crate.
142//!
143//! ```rust,irgnore
144//! extern crate diff_enum;
145//! use diff_enum::common_fields;
146//! ```
147//!
148//! And use `#[common_fields]` attribute macro for your enum definitions.
149//!
150//! ```ignore
151//! #[common_fields {
152//!     common fields here...
153//! }]
154//! enum ...
155//! ```
156//!
157//! or fully qualified name if you like
158//!
159//! ```ignore
160//! #[diff_enum::common_fields {
161//!     common fields here...
162//! }]
163//! enum ...
164//! ```
165//!
166//! Any attributes and comments can be put to the common fields as normal `enum` fields.
167//!
168//! Accessor methods corresponding to common fields are defined. It is a useful helper to access common
169//! fields without using pattern match.
170//!
171//! For example,
172//!
173//! ```rust,ignore
174//! #[common_fields { i: i32 }]
175//! enum E { A, B{ b: bool } }
176//! ```
177//!
178//! Generates an accessor method for `i` as follows:
179//!
180//! ```rust,ignore
181//! impl E {
182//!     fn i(&self) -> &i32 {
183//!         match self {
184//!             E::A{ref i, ..} => i,
185//!             E::B{ref i, ..} => i,
186//!         }
187//!     }
188//! }
189//! ```
190//!
191//! ## Errors
192//!
193//! The attribute macro causes compilation errors in the following cases.
194//!
195//! - When no common field is put
196//! - When fields in attribute argument is not form of `field: type`
197//! - When `#[common_fields {...}]` is set to other than `enum` definitions
198//! - When tuple style enum variant is used in `enum` definition
199
200extern crate proc_macro;
201extern crate proc_macro2;
202extern crate quote;
203extern crate syn;
204
205use proc_macro::TokenStream;
206use proc_macro2::TokenStream as TokenStream2;
207use proc_macro_attribute;
208use quote::quote;
209use syn::{Data, DeriveInput, Fields, FieldsNamed, Ident};
210
211#[proc_macro_attribute]
212pub fn common_fields(attr: TokenStream, item: TokenStream) -> TokenStream {
213    let shared: FieldsNamed = parse_shared_fields(attr);
214    if shared.named.is_empty() {
215        panic!("No shared field is set to #[diff_enum::common_fields]");
216    }
217
218    let input: DeriveInput = match syn::parse(item) {
219        Ok(parsed) => parsed,
220        Err(err) => panic!(
221            "#[diff_enum::common_fields] only can be set at enum definition: {}",
222            err
223        ),
224    };
225
226    let impl_accessors = generate_accessors(&shared, &input, input.ident.clone());
227    let expanded_enum = expand_shared_fields(&shared, input);
228    let tokens = quote! {
229        #expanded_enum
230        #impl_accessors
231    };
232
233    tokens.into()
234}
235
236fn parse_shared_fields(attr: TokenStream) -> FieldsNamed {
237    use proc_macro::{Delimiter, Group, TokenTree};
238    let braced = TokenStream::from(TokenTree::Group(Group::new(Delimiter::Brace, attr)));
239    match syn::parse(braced) {
240        Ok(fields) => fields,
241        Err(err) => panic!(
242            "Cannot parse fields in attributes at #[diff_enum::common_fields]: {}",
243            err
244        ),
245    }
246}
247
248fn expand_shared_fields(shared: &FieldsNamed, mut input: DeriveInput) -> TokenStream2 {
249    let mut enum_ = match input.data {
250        Data::Enum(e) => e,
251        _ => panic!("#[diff_enum::common_fields] can be set at only enum"),
252    };
253
254    for variant in enum_.variants.iter_mut() {
255        match variant.fields {
256            Fields::Named(ref mut f) => {
257                for shared_field in shared.named.iter() {
258                    f.named.push(shared_field.clone());
259                }
260            }
261            Fields::Unnamed(_) => panic!(
262                "#[diff_enum::common_fields] cannot mix named fields with unnamed fields at enum variant {}",
263                variant.ident.to_string()
264            ),
265            Fields::Unit => {
266                variant.fields = Fields::Named(shared.clone());
267            }
268        }
269    }
270
271    input.data = Data::Enum(enum_);
272    quote!(#input)
273}
274
275fn generate_accessors(shared: &FieldsNamed, input: &DeriveInput, enum_name: Ident) -> TokenStream2 {
276    let variants = match input.data {
277        Data::Enum(ref e) => &e.variants,
278        _ => panic!("#[diff_enum::common_fields] can be set at only enum"),
279    };
280
281    let accessors = shared.named.iter().map(|field| {
282        let field_name = &field.ident;
283        let ty = &field.ty;
284        let arms = variants.iter().map(|variant| {
285            let ident = &variant.ident;
286            quote! {
287                #enum_name::#ident{ref #field_name, ..} => #field_name,
288            }
289        });
290        quote! {
291            #[inline]
292            #[allow(dead_code)]
293            #[allow(missing_docs)]
294            pub fn #field_name (&self) -> &#ty {
295                match self {
296                    #( #arms )*
297                }
298            }
299        }
300    });
301
302    quote! {
303        impl #enum_name {
304            #( #accessors )*
305        }
306    }
307}