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::{self, DeriveInput};
26
27mod context;
28
29use syn::{Fields, ItemStruct, parse_macro_input, parse_quote};
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    // Note: We use parse_quote! to easily generate Attribute types
50    if !IS_SERDE_ENABLED {
51        input.attrs.push(parse_quote! {
52            #[derive(#crate_root::Patchable, #crate_root::Patch)]
53        });
54    } else {
55        input.attrs.push(parse_quote! {
56            #[derive(#crate_root::Patchable, #crate_root::Patch, ::serde::Serialize)]
57        });
58
59        match input.fields {
60            Fields::Named(ref mut fields) => {
61                for field in &mut fields.named {
62                    // Check if this field has the #[patchable(skip)] attribute
63                    if has_patchable_skip_attr(field) {
64                        field.attrs.push(parse_quote! { #[serde(skip)] });
65                    }
66                }
67            }
68            Fields::Unnamed(ref mut fields) => {
69                for field in &mut fields.unnamed {
70                    if has_patchable_skip_attr(field) {
71                        field.attrs.push(parse_quote! { #[serde(skip)] });
72                    }
73                }
74            }
75            Fields::Unit => {}
76        }
77    }
78
79    (quote! { #input }).into()
80}
81
82#[proc_macro_derive(Patchable, attributes(patchable))]
83/// Derive macro that generates the companion `Patch` type and `Patchable` impl.
84///
85/// The generated patch type:
86/// - mirrors the original struct shape (named/tuple/unit),
87/// - includes fields unless marked with `#[patchable(skip)]`,
88/// - implements `Clone` and `PartialEq`,
89/// - also derives `serde::Deserialize` when the `serde` feature is enabled for the
90///   macro crate.
91///
92/// The `Patchable` impl sets `type Patch = <StructName>Patch<...>` and adds
93/// any required generic bounds.
94///
95/// When the `impl_from` feature is enabled for the macro crate, a
96/// `From<Struct>` implementation is also generated for the patch type.
97pub fn derive_patchable(input: TokenStream) -> TokenStream {
98    derive_with(input, |ctx| {
99        let patch_struct_def = ctx.build_patch_struct();
100        let patchable_trait_impl = ctx.build_patchable_trait_impl();
101        let from_struct_impl = IS_IMPL_FROM_ENABLED.then(|| {
102            let from_struct_impl = ctx.build_from_trait_impl();
103            quote! {
104                #[automatically_derived]
105                #from_struct_impl
106            }
107        });
108
109        quote! {
110            const _: () = {
111                #[automatically_derived]
112                #patch_struct_def
113
114                #[automatically_derived]
115                #patchable_trait_impl
116
117                #from_struct_impl
118            };
119        }
120    })
121}
122
123#[proc_macro_derive(Patch, attributes(patchable))]
124/// Derive macro that generates the `Patch` trait implementation.
125///
126/// The generated `patch` method:
127/// - assigns fields directly by default,
128/// - recursively calls `patch` on fields marked with `#[patchable]`,
129/// - respects `#[patchable(skip)]` by omitting those fields from patching.
130pub fn derive_patch(input: TokenStream) -> TokenStream {
131    derive_with(input, |ctx| {
132        let patch_trait_impl = ctx.build_patch_trait_impl();
133
134        quote! {
135            const _: () = {
136                #[automatically_derived]
137                #patch_trait_impl
138            };
139        }
140    })
141}
142
143fn derive_with<F>(input: TokenStream, f: F) -> TokenStream
144where
145    F: FnOnce(&context::MacroContext) -> TokenStream2,
146{
147    let input: DeriveInput = syn::parse_macro_input!(input as DeriveInput);
148    match context::MacroContext::new(&input) {
149        Ok(ctx) => f(&ctx).into(),
150        Err(e) => e.to_compile_error().into(),
151    }
152}