Skip to main content

payrix_macros/
lib.rs

1//! Procedural macros for Payrix SDK entity types.
2//!
3//! This crate provides the `PayrixEntity` derive macro which generates
4//! Create and Update request types from a single entity definition.
5
6use darling::{ast::Data, FromDeriveInput, FromField};
7use proc_macro::TokenStream;
8use proc_macro2::TokenStream as TokenStream2;
9use quote::{format_ident, quote};
10use syn::{parse_macro_input, DeriveInput, Ident, Type};
11
12/// Field-level attributes for the PayrixEntity derive macro.
13#[derive(Debug, FromField)]
14#[darling(attributes(payrix))]
15struct PayrixField {
16    ident: Option<Ident>,
17    ty: Type,
18
19    /// Field is read-only (id, created, modified, creator, modifier).
20    /// Excluded from both Create and Update types.
21    #[darling(default)]
22    readonly: bool,
23
24    /// Field is only settable at creation time (merchant, customer, forlogin).
25    /// Included in Create type, excluded from Update type.
26    #[darling(default)]
27    create_only: bool,
28
29    /// Field is mutable after creation (name, description, inactive).
30    /// Included in both Create and Update types.
31    #[darling(default)]
32    mutable: bool,
33
34    /// Field is required in create type (not wrapped in Option).
35    /// Use this for fields that must be provided when creating a resource.
36    #[darling(default)]
37    create_required: bool,
38
39    /// Override the type for the create request.
40    /// Use this when the input type differs from the response type.
41    /// Example: `#[payrix(create_only, create_type = "PaymentInfo")]`
42    #[darling(default)]
43    create_type: Option<String>,
44}
45
46/// Struct-level attributes for the PayrixEntity derive macro.
47#[derive(Debug, FromDeriveInput)]
48#[darling(attributes(payrix), supports(struct_named))]
49struct PayrixEntityArgs {
50    ident: Ident,
51    data: Data<(), PayrixField>,
52
53    /// Name for the generated Create type (e.g., CreateAlert).
54    #[darling(default)]
55    create: Option<Ident>,
56
57    /// Name for the generated Update type (e.g., UpdateAlert).
58    #[darling(default)]
59    update: Option<Ident>,
60}
61
62/// Extract the serde rename attribute value from field attributes.
63fn get_serde_rename(attrs: &[syn::Attribute]) -> Option<String> {
64    for attr in attrs {
65        if attr.path().is_ident("serde") {
66            if let Ok(nested) = attr.parse_args_with(
67                syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
68            ) {
69                for meta in nested {
70                    if let syn::Meta::NameValue(nv) = meta {
71                        if nv.path.is_ident("rename") {
72                            if let syn::Expr::Lit(syn::ExprLit {
73                                lit: syn::Lit::Str(s),
74                                ..
75                            }) = nv.value
76                            {
77                                return Some(s.value());
78                            }
79                        }
80                    }
81                }
82            }
83        }
84    }
85    None
86}
87
88/// Check if a type is Option<T>.
89fn is_option_type(ty: &Type) -> bool {
90    if let Type::Path(type_path) = ty {
91        if let Some(segment) = type_path.path.segments.last() {
92            return segment.ident == "Option";
93        }
94    }
95    false
96}
97
98/// Check if a type is Vec<T>.
99fn is_vec_type(ty: &Type) -> bool {
100    if let Type::Path(type_path) = ty {
101        if let Some(segment) = type_path.path.segments.last() {
102            return segment.ident == "Vec";
103        }
104    }
105    false
106}
107
108/// Check if a type is bool.
109fn is_bool_type(ty: &Type) -> bool {
110    if let Type::Path(type_path) = ty {
111        if let Some(segment) = type_path.path.segments.last() {
112            return segment.ident == "bool";
113        }
114    }
115    false
116}
117
118/// Transform a type to Option<T> for request types.
119/// - Option<T> stays as Option<T>
120/// - bool becomes Option<bool>
121/// - T becomes Option<T>
122fn wrap_in_option(ty: &Type) -> TokenStream2 {
123    if is_option_type(ty) {
124        quote! { #ty }
125    } else if is_bool_type(ty) {
126        quote! { Option<bool> }
127    } else {
128        quote! { Option<#ty> }
129    }
130}
131
132/// Information about a field to include in a request type.
133struct RequestField {
134    name: Ident,
135    ty: Type,
136    rename: Option<String>,
137    /// If true, the field is required (not wrapped in Option).
138    required: bool,
139    /// Override type for create requests (parsed from string).
140    override_type: Option<String>,
141}
142
143/// Generate a request type (Create or Update) with the specified fields.
144fn generate_request_type(
145    type_name: &Ident,
146    fields: &[RequestField],
147    is_create: bool,
148    source_name: &Ident,
149) -> TokenStream2 {
150    let field_defs: Vec<TokenStream2> = fields
151        .iter()
152        .map(|field| {
153            let name = &field.name;
154            let rename_attr = field.rename.as_ref().map(|r| {
155                quote! { #[serde(rename = #r)] }
156            });
157            let field_doc = format!("See [`{}`] for field documentation.", source_name);
158
159            // Determine the field type
160            let (field_ty, skip_attr) = if field.required {
161                // Required field: use override type if specified, otherwise original type
162                if let Some(ref override_type) = field.override_type {
163                    let ty: Type = syn::parse_str(override_type)
164                        .expect("Invalid create_type value");
165                    (quote! { #ty }, quote! {})
166                } else {
167                    // For required fields, extract inner type if it's Option<T>
168                    let ty = &field.ty;
169                    if is_option_type(ty) {
170                        // Extract inner type from Option<T>
171                        if let Type::Path(type_path) = ty {
172                            if let Some(segment) = type_path.path.segments.last() {
173                                if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
174                                    if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
175                                        return quote! {
176                                            #[doc = #field_doc]
177                                            #rename_attr
178                                            pub #name: #inner
179                                        };
180                                    }
181                                }
182                            }
183                        }
184                    }
185                    (quote! { #ty }, quote! {})
186                }
187            } else {
188                // Optional field: use override type wrapped in Option, or wrap original
189                if let Some(ref override_type) = field.override_type {
190                    let ty: Type = syn::parse_str(override_type)
191                        .expect("Invalid create_type value");
192                    (quote! { Option<#ty> }, quote! { #[serde(skip_serializing_if = "Option::is_none")] })
193                } else {
194                    let wrapped_ty = wrap_in_option(&field.ty);
195                    (wrapped_ty, quote! { #[serde(skip_serializing_if = "Option::is_none")] })
196                }
197            };
198
199            quote! {
200                #[doc = #field_doc]
201                #rename_attr
202                #skip_attr
203                pub #name: #field_ty
204            }
205        })
206        .collect();
207
208    let type_doc = if is_create {
209        format!("Request body for creating a new [`{}`].", source_name)
210    } else {
211        format!("Request body for updating an existing [`{}`].", source_name)
212    };
213
214    // Don't derive Default if there are required fields
215    let has_required = fields.iter().any(|f| f.required);
216    let derives = if has_required {
217        quote! { #[derive(Debug, Clone, serde::Serialize)] }
218    } else {
219        quote! { #[derive(Debug, Clone, Default, serde::Serialize)] }
220    };
221
222    quote! {
223        #[doc = #type_doc]
224        #derives
225        #[serde(rename_all = "camelCase")]
226        pub struct #type_name {
227            #(#field_defs),*
228        }
229    }
230}
231
232/// Derive macro for generating Create and Update types from a Payrix entity.
233///
234/// # Attributes
235///
236/// ## Struct-level
237/// - `#[payrix(create = CreateTypeName)]` - Name for the Create type
238/// - `#[payrix(update = UpdateTypeName)]` - Name for the Update type
239///
240/// ## Field-level
241/// - `#[payrix(readonly)]` - Field is read-only, excluded from request types
242/// - `#[payrix(create_only)]` - Field only in Create type (e.g., merchant, customer)
243/// - `#[payrix(mutable)]` - Field in both Create and Update types
244/// - `#[payrix(create_required)]` - Field is required in Create type (not wrapped in Option)
245/// - `#[payrix(create_type = "SomeType")]` - Use a different type for Create requests
246///
247/// Fields without any payrix attribute are excluded from request types.
248///
249/// # Example
250///
251/// ```ignore
252/// #[derive(PayrixEntity)]
253/// #[payrix(create = CreateAlert, update = UpdateAlert)]
254/// pub struct Alert {
255///     #[payrix(readonly)]
256///     pub id: PayrixId,
257///
258///     #[payrix(readonly)]
259///     pub created: Option<String>,
260///
261///     #[payrix(create_only)]
262///     pub forlogin: Option<PayrixId>,
263///
264///     #[payrix(mutable)]
265///     pub name: Option<String>,
266/// }
267///
268/// // For types with required create fields:
269/// #[derive(PayrixEntity)]
270/// #[payrix(create = CreateToken)]
271/// pub struct Token {
272///     #[payrix(readonly)]
273///     pub id: PayrixId,
274///
275///     // Required for creation, uses a different input type
276///     #[payrix(create_only, create_required, create_type = "PaymentInfo")]
277///     pub payment: Option<PaymentMethod>,
278///
279///     // Required for creation
280///     #[payrix(create_only, create_required)]
281///     pub customer: Option<PayrixId>,
282/// }
283/// ```
284#[proc_macro_derive(PayrixEntity, attributes(payrix))]
285pub fn derive_payrix_entity(input: TokenStream) -> TokenStream {
286    let input = parse_macro_input!(input as DeriveInput);
287
288    // Get original attributes to check for serde renames on fields
289    let original_fields: Vec<_> = if let syn::Data::Struct(data) = &input.data {
290        if let syn::Fields::Named(fields) = &data.fields {
291            fields.named.iter().collect()
292        } else {
293            Vec::new()
294        }
295    } else {
296        Vec::new()
297    };
298
299    let args = match PayrixEntityArgs::from_derive_input(&input) {
300        Ok(args) => args,
301        Err(e) => return TokenStream::from(e.write_errors()),
302    };
303
304    let struct_name = &args.ident;
305
306    // Default type names if not specified
307    let create_name = args
308        .create
309        .unwrap_or_else(|| format_ident!("Create{}", struct_name));
310    let update_name = args
311        .update
312        .unwrap_or_else(|| format_ident!("Update{}", struct_name));
313
314    // Collect fields for each request type
315    let mut create_fields: Vec<RequestField> = Vec::new();
316    let mut update_fields: Vec<RequestField> = Vec::new();
317
318    let fields = match args.data {
319        Data::Struct(ref fields) => fields,
320        _ => panic!("PayrixEntity only supports structs"),
321    };
322
323    for (idx, field) in fields.iter().enumerate() {
324        let field_name = match &field.ident {
325            Some(name) => name.clone(),
326            None => continue,
327        };
328
329        // Skip Vec<T> fields (nested relations)
330        if is_vec_type(&field.ty) {
331            continue;
332        }
333
334        // Get serde rename from original field
335        let serde_rename = original_fields
336            .get(idx)
337            .and_then(|f| get_serde_rename(&f.attrs));
338
339        // Determine which types this field should be included in
340        if field.readonly {
341            // Readonly fields are excluded from both types
342            continue;
343        } else if field.create_only {
344            // Create-only fields go in Create type only
345            create_fields.push(RequestField {
346                name: field_name,
347                ty: field.ty.clone(),
348                rename: serde_rename,
349                required: field.create_required,
350                override_type: field.create_type.clone(),
351            });
352        } else if field.mutable {
353            // Mutable fields go in both types
354            create_fields.push(RequestField {
355                name: field_name.clone(),
356                ty: field.ty.clone(),
357                rename: serde_rename.clone(),
358                required: field.create_required,
359                override_type: field.create_type.clone(),
360            });
361            // Update fields don't use create_required or create_type
362            update_fields.push(RequestField {
363                name: field_name,
364                ty: field.ty.clone(),
365                rename: serde_rename,
366                required: false,
367                override_type: None,
368            });
369        }
370        // Fields without any payrix attribute are excluded
371    }
372
373    // Generate the types
374    let create_type = generate_request_type(&create_name, &create_fields, true, struct_name);
375    let update_type = generate_request_type(&update_name, &update_fields, false, struct_name);
376
377    let expanded = quote! {
378        #create_type
379
380        #update_type
381    };
382
383    TokenStream::from(expanded)
384}