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}