1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
//! Derive a constant value holding the struct's name as [`&str`].
//!
//! All case modifications from the [`Case`] enum can be used to modify the value of the constant.
//!
//! The constant's key can be renamed.

extern crate convert_case;
extern crate proc_macro;
extern crate proc_macro2;
#[macro_use]
extern crate syn;
extern crate quote;

use {
    crate::{
        convert_case::{Case, Casing},
        proc_macro::TokenStream,
        proc_macro2::{Literal, TokenStream as TokenStream2},
        quote::{quote, ToTokens},
        syn::{DeriveInput, Ident, LitStr, TypePath},
    },
    std::convert::From,
};

/// Derivable macro that provides you with a constant variable that holds the name of the item derived on.
///
/// Key and value of the constant can be manipulated to some degree.
///
/// This macro uses the [`convert_case`] crate internally.
/// Its [`Case`] enum does not implement much conversion, so this macro relies on its [`Debug`] implementation.
///
/// # Examples
///
/// The default implementation names the key after the struct in SCREAMING_SNAKE_CASE and the value after the struct's name as-is.
///
/// ```rust
/// use mongodb_ext_derive::ConstName;
///
/// #[derive(ConstName)]
/// struct MyStruct {
///     some_field: u16,
/// }
///
/// assert_eq!("MyStruct", MY_STRUCT);
/// ```
///
/// ## Customizing the [`ConstName`] derive
///
/// Change the constant's value's case by using any type from [`Case`] inside of `#[const_name_value()]`.
///
/// ```rust
/// use mongodb_ext_derive::ConstName;
/// use convert_case::Case;
///
/// #[derive(ConstName)]
/// #[const_name_value(Case::Snake)]
/// struct MyStruct {
///     some_field: u16,
/// }
///
/// assert_eq!("my_struct", MY_STRUCT);
/// ```
///
/// ```rust
/// use mongodb_ext_derive::ConstName;
///
/// #[derive(ConstName)]
/// #[const_name_value(convert_case::Case::Camel)]
/// struct MyStruct {
///     some_field: u16,
/// }
///
/// assert_eq!("myStruct", MY_STRUCT);
/// ```
///
/// You do not actually need to specify a valid path to [`Case`], but all [`Case`]'s types are dynamically supported:
///
/// ```rust
/// use mongodb_ext_derive::ConstName;
///
/// #[derive(ConstName)]
/// // Specifying "Camel" without having Case in scope, still works.
/// #[const_name_value(Camel)]
/// struct MyStruct {
///     some_field: u16,
/// }
///
/// assert_eq!("myStruct", MY_STRUCT);
/// ```
///
/// Change the constant's key by specifying a new key with `#[const_name_key()]`.
///
/// ```rust
/// use mongodb_ext_derive::ConstName;
///
/// #[derive(ConstName)]
/// #[const_name_key("STRUCT_NAME")]
/// struct MyStruct {
///     some_field: u16,
/// }
///
/// assert_eq!("MyStruct", STRUCT_NAME);
/// ```
///
/// Invalid attributes end up in a panic whilst compiling.
///
/// ```compile_fail
/// use mongodb_ext_derive::ConstName;
///
/// #[derive(ConstName)]
/// #[const_name_value(NotACaseValue)]
/// struct MyStruct {
///     some_field: u16,
/// }
/// ```
///
/// ```compile_fail
/// use mongodb_ext_derive::ConstName;
///
/// #[derive(ConstName)]
/// #[const_name_key(THIS_IS_NOT_A_STRING_LITERAL)]
/// struct MyStruct {
///     some_field: u16,
/// }
/// ```
#[proc_macro_derive(ConstName, attributes(const_name_value, const_name_key))]
pub fn const_name_derive(items: TokenStream) -> TokenStream {
    let ast: DeriveInput = parse_macro_input!(items as DeriveInput);

    // save span for later
    let key_value_span = ast.ident.span();
    // create default constant variable value
    let mut value = ast.ident.to_string();
    // create default constant variable name
    let mut key = value.to_case(Case::UpperSnake);

    // loop through all attributes on the struct to find relevant ones
    'outer: for attr in ast.attrs {
        if attr.path.is_ident("const_name_key") {
            key = attr
                // parse key as literal
                .parse_args::<Literal>()
                .expect("Could not parse `const_name` derive attribute's arguments as literal")
                // convert to string
                .to_string()
                // strip quotes
                .replace("\"", "");
        } else if attr.path.is_ident("const_name_value") {
            let given_case = attr
                // parse value case as path
                .parse_args::<TypePath>()
                .expect("Could not parse `name_case` derive attribute's arguments as path");
            // get last part of parsed path and convert it to string
            let given_case: String = given_case
                .path
                .segments
                .last()
                .expect("Could not get identifier of given path in `name_case` derive attribute")
                .ident
                .to_string();
            // search for a matching case in Case enum
            for case in Case::all_cases() {
                if format!("{:?}", case).eq(&given_case) {
                    // case found, set it and continue the outer loop
                    value = value.to_case(Case::from(case));
                    continue 'outer;
                }
            }
            // no matching cases found
            panic!(
                "Supplied attribute to `name_case` is invalid ({})",
                given_case
            );
        }
    }

    let mut ts: TokenStream2 = quote!(pub const);
    Ident::new(&key, key_value_span).to_tokens(&mut ts);
    ts.extend(quote!(: &str =));
    LitStr::new(&value, key_value_span).to_tokens(&mut ts);
    ts.extend(quote!(;));
    ts.into()
}