Skip to main content

patchable_macro/
lib.rs

1//! # Patchable Macro
2//!
3//! Procedural macros backing the `patchable` crate.
4//!
5//! Provided macros:
6//!
7//! - `#[patchable_model]`: injects `Patchable`/`Patch` derives; with the `serde`
8//!   Cargo feature enabled for this macro crate it also adds `serde::Serialize`
9//!   and applies `#[serde(skip)]` to fields marked `#[patchable(skip)]`.
10//!
11//! - `#[derive(Patchable)]`: generates the companion `<Struct>Patch` type and the
12//!   `Patchable` impl; with the `impl_from` Cargo feature it also generates
13//!   `From<Struct>` for the patch type.
14//!
15//! - `#[derive(Patch)]`: generates the `Patch` implementation and recursively
16//!   patches fields annotated with `#[patchable]`.
17//!
18//! Feature flags are evaluated in the `patchable-macro` crate itself. See `context`
19//! for details about the generated patch struct and trait implementations.
20
21use proc_macro::TokenStream;
22
23use proc_macro2::TokenStream as TokenStream2;
24use quote::quote;
25use syn::{Fields, ItemStruct, parse_macro_input, parse_quote};
26
27mod context;
28
29use syn::DeriveInput;
30
31use crate::context::{IS_SERDE_ENABLED, has_patchable_skip_attr, use_site_crate_path};
32
33const IS_IMPL_FROM_ENABLED: bool = cfg!(feature = "impl_from");
34
35#[proc_macro_attribute]
36/// Attribute macro that augments a struct with Patchable/Patch derives.
37///
38/// - Always adds `#[derive(Patchable, Patch)]`.
39/// - When the `serde` feature is enabled for the macro crate, it also adds
40///   `#[derive(serde::Serialize)]`.
41/// - For fields annotated with `#[patchable(skip)]`, it injects `#[serde(skip)]`
42///   to keep serde output aligned with patching behavior.
43///
44/// This macro preserves the original struct shape and only mutates attributes.
45pub fn patchable_model(_attr: TokenStream, item: TokenStream) -> TokenStream {
46    let mut input = parse_macro_input!(item as ItemStruct);
47    let crate_root = use_site_crate_path();
48
49    let derives = if IS_SERDE_ENABLED {
50        parse_quote! {
51            #[derive(#crate_root::Patchable, #crate_root::Patch, ::serde::Serialize)]
52        }
53    } else {
54        parse_quote! {
55            #[derive(#crate_root::Patchable, #crate_root::Patch)]
56        }
57    };
58    input.attrs.push(derives);
59
60    if IS_SERDE_ENABLED {
61        add_serde_skip_attrs(&mut input.fields);
62    }
63
64    (quote! { #input }).into()
65}
66
67#[proc_macro_derive(Patchable, attributes(patchable))]
68/// Derive macro that generates the companion `Patch` type and `Patchable` impl.
69///
70/// The generated patch type:
71/// - mirrors the original struct shape (named/tuple/unit),
72/// - includes fields unless marked with `#[patchable(skip)]`,
73/// - also derives `serde::Deserialize` when the `serde` feature is enabled for the
74///   macro crate.
75///
76/// The `Patchable` impl sets `type Patch = <StructName>Patch<...>` and adds
77/// any required generic bounds.
78///
79/// When the `impl_from` feature is enabled for the macro crate, a
80/// `From<Struct>` implementation is also generated for the patch type.
81pub fn derive_patchable(input: TokenStream) -> TokenStream {
82    expand(input, |ctx| {
83        let patch_struct_def = ctx.build_patch_struct();
84        let patchable_trait_impl = ctx.build_patchable_trait_impl();
85        let from_struct_impl = IS_IMPL_FROM_ENABLED.then(|| {
86            let from_struct_impl = ctx.build_from_trait_impl();
87            quote! {
88                #[automatically_derived]
89                #from_struct_impl
90            }
91        });
92
93        quote! {
94            const _: () = {
95                #[automatically_derived]
96                #patch_struct_def
97
98                #[automatically_derived]
99                #patchable_trait_impl
100
101                #from_struct_impl
102            };
103        }
104    })
105}
106
107#[proc_macro_derive(Patch, attributes(patchable))]
108/// Derive macro that generates the `Patch` trait implementation.
109///
110/// The generated `patch` method:
111/// - assigns fields directly by default,
112/// - recursively calls `patch` on fields marked with `#[patchable]`,
113/// - respects `#[patchable(skip)]` by omitting those fields from patching.
114pub fn derive_patch(input: TokenStream) -> TokenStream {
115    expand(input, |ctx| {
116        let patch_trait_impl = ctx.build_patch_trait_impl();
117
118        quote! {
119            const _: () = {
120                #[automatically_derived]
121                #patch_trait_impl
122            };
123        }
124    })
125}
126
127fn expand<F>(input: TokenStream, f: F) -> TokenStream
128where
129    F: FnOnce(&context::MacroContext) -> TokenStream2,
130{
131    let input: DeriveInput = parse_macro_input!(input as DeriveInput);
132    match context::MacroContext::new(&input) {
133        Ok(ctx) => f(&ctx).into(),
134        Err(e) => e.to_compile_error().into(),
135    }
136}
137
138fn add_serde_skip_attrs(fields: &mut Fields) {
139    for field in fields.iter_mut() {
140        if has_patchable_skip_attr(field) {
141            field.attrs.push(parse_quote! { #[serde(skip)] });
142        }
143    }
144}