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, crate_path, has_patchable_skip_attr};
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 crate_path = crate_path();
47 let derives = if IS_SERDE_ENABLED {
48 parse_quote! {
49 #[derive(#crate_path::Patchable, #crate_path::Patch, ::serde::Serialize)]
50 }
51 } else {
52 parse_quote! {
53 #[derive(#crate_path::Patchable, #crate_path::Patch)]
54 }
55 };
56
57 let mut input = parse_macro_input!(item as ItemStruct);
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}