pokeapi_macro/
lib.rs

1#![warn(
2    clippy::all,
3    clippy::cargo,
4    clippy::nursery,
5    clippy::pedantic,
6    rust_2018_idioms
7)]
8#![forbid(unsafe_code)]
9#![doc = include_str!("../README.md")]
10
11use proc_macro::TokenStream;
12use quote::quote;
13use syn::fold::Fold;
14use syn::{
15    parse_macro_input, parse_quote, token, Attribute, Data, DataStruct, DeriveInput, Fields,
16    FieldsNamed, VisPublic, Visibility,
17};
18
19/// Struct for implementing a `Fold` hook.
20///
21/// [Reference](https://docs.rs/syn/*/syn/fold/index.html)
22struct PokeAPIFields;
23
24impl Fold for PokeAPIFields {
25    /// Fold `FieldsNamed` and replace every field's visibility with `Visibility::Public`, unless
26    /// the field has `#[serde(skip)]`.
27    fn fold_fields_named(&mut self, fields: FieldsNamed) -> FieldsNamed {
28        let serde_skip: Attribute = parse_quote!(#[serde(skip)]);
29
30        let brace_token = fields.brace_token;
31        let named = fields
32            .named
33            .into_iter()
34            .map(|mut field| {
35                // Safe to unwrap because we know the field is named.
36                let ident = field.ident.as_ref().unwrap();
37
38                if ident.to_string().starts_with('_') {
39                    if !field.attrs.iter().any(|attr| attr == &serde_skip) {
40                        field.attrs.push(serde_skip.clone());
41                    }
42                }
43
44                if !field.attrs.iter().any(|attr| attr == &serde_skip) {
45                    field.vis = Visibility::Public(VisPublic {
46                        pub_token: token::Pub::default(),
47                    });
48                }
49
50                field
51            })
52            .collect();
53
54        FieldsNamed { brace_token, named }
55    }
56}
57
58/// Attribute macro that generate a `pokeapi-model` struct.
59///
60/// # Panics
61///
62/// Panics if the passed `item` is not a valid struct.
63///
64/// # Examples
65///
66/// ```ignore
67/// // Consider the following example:
68/// use pokeapi_macro::pokeapi_struct;
69///
70/// #[pokeapi_struct]
71/// struct NamedAPIResource<T> {
72///     description: String,
73///     url: String,
74///     _resource_type: std::marker::PhantomData<*const T>,
75/// }
76///
77/// // This attribute will output the struct with required derived traits and visibility:
78/// #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
79/// pub struct NamedAPIResource<T> {
80///     pub description: String,
81///     pub url: String,
82///     #[serde(skip)]
83///     _resource_type: std::marker::PhantomData<*const T>
84/// }
85/// ```
86#[allow(clippy::doc_markdown)]
87#[proc_macro_attribute]
88pub fn pokeapi_struct(_attr: TokenStream, item: TokenStream) -> TokenStream {
89    // Destructure and assign parameter `item` to its corresponding tokens. The field `vis` is not
90    // necessary because it will be made `pub`.
91    let DeriveInput {
92        attrs,
93        ident,
94        generics,
95        data,
96        ..
97    } = parse_macro_input!(item as DeriveInput);
98
99    let ident_lower = ident.to_string().to_ascii_lowercase();
100    let doc_comment = format!(
101        "[{url}{ident_lower}]({url}{ident_lower})",
102        url = "https://pokeapi.co/docs/v2#"
103    );
104    let doc_attr: Attribute = parse_quote!(#[doc = #doc_comment]);
105
106    // Ensure parameter `item` is a `struct` with named fields, and call the
107    // `PokeAPIFields.fold_fields_name` hook on the struct's `Field`s.
108    let fields = match data {
109        Data::Struct(DataStruct {
110            fields: Fields::Named(named_fields),
111            ..
112        }) => PokeAPIFields
113            .fold_fields_named(named_fields)
114            .named
115            .into_iter(),
116        _ => panic!("This attribute requires a struct with named fields."),
117    };
118
119    // Quasi-quote the syntax tree and return as a `TokenStream`.
120    TokenStream::from(quote! {
121        #doc_attr
122        #(#attrs)*
123        #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
124        #[non_exhaustive]
125        pub struct #ident #generics {
126            #(#fields),*
127        }
128    })
129}