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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
/*!
Derive macros for automatically adding an implementation of
pandora_api::Pandora<Json|Rest>ApiRequest to a struct.

The name of the Pandora API method that will be called defaults to the
result of converting the struct name to lower camel case (GetFoo -> getFoo).
This may be overridden using the #[pandora_request(method_name = getFOOBar)
struct attribute.

The default error type is Error.  If a different type name is required,
this may be overridden using the #[pandora_request(error_type = FooError)]
struct attribute.

The default return type of the request is <struct name>Response. This may be
overridden using the #[pandora_request(response_type = FooResponse)] struct
attribute.

The default for a request is to send it unencrypted.  If the request must be
encrypted, this may be overridden using the #[pandora_request(encrypted = true)]
struct attribute.

*/
// SPDX-License-Identifier: MIT

#![deny(missing_docs)]
extern crate proc_macro;

use darling::FromDeriveInput;
use heck::ToLowerCamelCase;
use proc_macro::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::{Generics, Ident};

/// Derive macro for adding implementation of pandora_api::PandoraJsonApiRequest
/// trait to a struct.
#[derive(FromDeriveInput)]
#[darling(attributes(pandora_request))]
struct PandoraJsonRequest {
    ident: Ident,
    generics: Generics,
    // Default is <StructName>Response
    #[darling(default = "std::option::Option::default")]
    response_type: Option<String>,
    // Default is "Error"
    #[darling(default = "std::option::Option::default")]
    error_type: Option<String>,
    // Default is the output of format!("{}.{}", "<ModuleName>", "<StructName>".to_lower_camel_case())
    #[darling(default = "std::option::Option::default")]
    method_name: Option<String>,
    #[darling(default = "std::option::Option::default")]
    encrypted: Option<bool>,
}

impl ToTokens for PandoraJsonRequest {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let PandoraJsonRequest {
            ref ident,
            ref generics,
            ref response_type,
            ref error_type,
            ref method_name,
            ref encrypted,
        } = *self;

        // if no response_type was specified, we default
        // to the Self type + "Response".
        let final_response_type = format_ident!(
            "{}",
            response_type
                .as_ref()
                .map(|s| s.to_string())
                .unwrap_or_else(|| format!("{ident}Response"))
        );
        let final_error_type = format_ident!(
            "{}",
            error_type
                .as_ref()
                .map(|s| s.to_string())
                .unwrap_or_else(|| "Error".to_string())
        );

        let get_method_decl = if let Some(method_name) = method_name {
            quote! {
                fn get_method(&self) -> String {
                    stringify!(#method_name).to_string()
                }
            }
        } else {
            let lower_camel_case_method = ident.to_string().to_lower_camel_case();
            quote! {
                fn get_method(&self) -> String {
                    let module_name = std::module_path!();
                    let class_name = module_name.rsplitn(2, "::").next().expect("Could not infer a valid method name since there is no current module. Must pass #[pandora_request(method_name = \"<value>\")] as part of the derive.");
                    format!("{}.{}", class_name, #lower_camel_case_method)
                }
            }
        };

        let encrypt_expr = encrypted
            .map(|b| {
                quote! {
                    fn encrypt_request(&self) -> bool {
                        #b
                    }
                }
            })
            .unwrap_or_else(|| quote! {});

        // If the type is generic, we need to pass that
        // through to the impl
        let (imp, ty, wher) = generics.split_for_impl();
        tokens.extend(quote! {
            impl #imp PandoraJsonApiRequest for #ident #ty #wher {
                type Response = #final_response_type;
                type Error = #final_error_type;
                #encrypt_expr
                #get_method_decl
            }
        });
    }
}

/// Derive macro for adding implementation of pandora_api::PandoraJsonApiRequest
/// trait to a struct.
#[proc_macro_derive(PandoraJsonRequest, attributes(pandora_request))]
pub fn derive_pandora_json_request(input: TokenStream) -> TokenStream {
    let request = PandoraJsonRequest::from_derive_input(&syn::parse(input).unwrap())
        .expect("Failed parsing macro input");
    let pm2_tokens: proc_macro2::TokenStream = quote! {#request};
    //panic!("Generated tokens: {}", pm2_tokens.to_string());
    pm2_tokens.into()
}

/// Derive macro for adding implementation of pandora_api::PandoraRestApiRequest
/// trait to a struct.
#[derive(FromDeriveInput)]
#[darling(attributes(pandora_request))]
struct PandoraRestRequest {
    ident: Ident,
    generics: Generics,
    // Default is <StructName>Response
    #[darling(default = "std::option::Option::default")]
    response_type: Option<String>,
    // Default is "Error"
    #[darling(default = "std::option::Option::default")]
    error_type: Option<String>,
    // Default is the output of format!("{}.{}", "<ModuleName>", "<StructName>".to_lower_camel_case())
    #[darling(default = "std::option::Option::default")]
    method_name: Option<String>,
    #[darling(default = "std::option::Option::default")]
    encrypted: Option<bool>,
}

impl ToTokens for PandoraRestRequest {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let PandoraRestRequest {
            ref ident,
            ref generics,
            ref response_type,
            ref error_type,
            ref method_name,
            ref encrypted,
        } = *self;

        // if no response_type was specified, we default
        // to the Self type + "Response".
        let final_response_type = format_ident!(
            "{}",
            response_type
                .as_ref()
                .map(|s| s.to_string())
                .unwrap_or_else(|| format!("{ident}Response"))
        );
        let final_error_type = format_ident!(
            "{}",
            error_type
                .as_ref()
                .map(|s| s.to_string())
                .unwrap_or_else(|| "Error".to_string())
        );

        let get_method_decl = if let Some(method_name) = method_name {
            quote! {
                fn get_method(&self) -> String {
                    stringify!(#method_name).to_string()
                }
            }
        } else {
            let lower_camel_case_method = ident.to_string().to_lower_camel_case();
            quote! {
                fn get_method(&self) -> String {
                    let module_name = std::module_path!();
                    let class_name = module_name.rsplitn(2, "::").next().expect("Could not infer a valid method name since there is no current module. Must pass #[pandora_request(method_name = \"<value>\")] as part of the derive.");
                    format!("/api/v1/{}/{}", class_name, #lower_camel_case_method)
                }
            }
        };

        let encrypt_expr = encrypted
            .map(|b| {
                quote! {
                    fn encrypt_request(&self) -> bool {
                        #b
                    }
                }
            })
            .unwrap_or_else(|| quote! {});

        // If the type is generic, we need to pass that
        // through to the impl
        let (imp, ty, wher) = generics.split_for_impl();
        tokens.extend(quote! {
            impl #imp PandoraRestApiRequest for #ident #ty #wher {
                type Response = #final_response_type;
                type Error = #final_error_type;
                #encrypt_expr
                #get_method_decl
            }
        });
    }
}

/// Derive macro for adding implementation of pandora_api::PandoraRestApiRequest
/// trait to a struct.
#[proc_macro_derive(PandoraRestRequest, attributes(pandora_request))]
pub fn derive_pandora_rest_request(input: TokenStream) -> TokenStream {
    let request = PandoraRestRequest::from_derive_input(&syn::parse(input).unwrap())
        .expect("Failed parsing macro input");
    let pm2_tokens: proc_macro2::TokenStream = quote! {#request};
    //panic!("Generated tokens: {}", pm2_tokens.to_string());
    pm2_tokens.into()
}