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}