1use proc_macro::TokenStream;
16use quote::quote;
17use syn::{Data, DeriveInput, Fields, ImplItem, ItemImpl, parse_macro_input};
18
19#[proc_macro_attribute]
30pub fn lifecycle_trace(_attr: TokenStream, item: TokenStream) -> TokenStream {
31 let mut input = parse_macro_input!(item as ItemImpl);
32
33 let is_lifecycle_impl = input
34 .trait_
35 .as_ref()
36 .and_then(|(_, path, _)| path.segments.last())
37 .map(|seg| seg.ident == "Lifecycle")
38 .unwrap_or(false);
39
40 if !is_lifecycle_impl {
41 return syn::Error::new_spanned(
42 &input,
43 "#[lifecycle_trace] can only be applied to `impl Lifecycle for T` blocks",
44 )
45 .to_compile_error()
46 .into();
47 }
48
49 let entering: ImplItem = syn::parse_quote! {
50 fn wrap_enter(self, ctx: &mut Self::Context, output: &mut Self::Output) -> Result<Self::State, Self::Error> {
51 let _lc = self.lifecycle_context();
52 let _key = self.key();
53 let _meta = serde_json::to_string(&_lc.metadata).unwrap_or_default();
54 let result = self.enter(ctx, output);
55 match &result {
56 Ok(_) => tracing::info!(key = ?_key, display_name = %_lc.display_name, metadata = %_meta, "entering"),
57 Err(e) => tracing::error!(key = ?_key, display_name = %_lc.display_name, metadata = %_meta, error = %e, "entering failed"),
58 }
59 result
60 }
61 };
62
63 let reconciling: ImplItem = syn::parse_quote! {
64 fn wrap_reconcile(self, state: &mut Self::State, ctx: &mut Self::Context, output: &mut Self::Output) -> Result<(), Self::Error> {
65 let _lc = self.lifecycle_context();
66 let _key = self.key();
67 let _meta = serde_json::to_string(&_lc.metadata).unwrap_or_default();
68 let result = self.reconcile_self(state, ctx, output);
69 if let Err(e) = &result {
70 tracing::error!(key = ?_key, display_name = %_lc.display_name, metadata = %_meta, error = %e, "reconciling failed");
71 }
72 result
73 }
74 };
75
76 let exiting: ImplItem = syn::parse_quote! {
77 fn wrap_exit(state: Self::State, ctx: &mut Self::Context, output: &mut Self::Output) -> Result<(), Self::Error> {
78 let _lc = Self::lifecycle_state_context(&state);
79 let _meta = serde_json::to_string(&_lc.metadata).unwrap_or_default();
80 let result = Self::exit(state, ctx, output);
81 match &result {
82 Ok(_) => tracing::info!(display_name = %_lc.display_name, metadata = %_meta, "exiting"),
83 Err(e) => tracing::error!(display_name = %_lc.display_name, metadata = %_meta, error = %e, "exiting failed"),
84 }
85 result
86 }
87 };
88
89 input.items.push(entering);
90 input.items.push(reconciling);
91 input.items.push(exiting);
92
93 quote! { #input }.into()
94}
95
96#[proc_macro_derive(Ephemeral, attributes(reconciler))]
104pub fn derive_ephemeral(input: TokenStream) -> TokenStream {
105 let input = parse_macro_input!(input as DeriveInput);
106 let name = &input.ident;
107 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
108
109 let fields = match &input.data {
110 Data::Struct(s) => match &s.fields {
111 Fields::Named(f) => &f.named,
112 _ => panic!("Ephemeral only supports named fields"),
113 },
114 _ => panic!("Ephemeral only supports structs"),
115 };
116
117 let mut calls = vec![];
118 for field in fields {
119 let field_name = field.ident.as_ref().unwrap();
120 for attr in &field.attrs {
121 if !attr.path().is_ident("reconciler") {
122 continue;
123 }
124 let mut output_ident: Option<syn::Ident> = None;
125 attr.parse_nested_meta(|meta| {
126 if meta.path.is_ident("output")
127 && let syn::Expr::Path(p) = meta.value()?.parse::<syn::Expr>()?
128 {
129 output_ident = p.path.get_ident().cloned();
130 }
131 Ok(())
132 })
133 .unwrap();
134 let output = output_ident.expect("reconciler attribute requires output = <field_name>");
135 calls.push(quote! {
136 { let mut __ctx = ::core::default::Default::default(); self.#field_name.reconcile(::std::vec::Vec::new(), &mut __ctx, &mut self.#output); }
137 });
138 }
139 }
140
141 quote! {
142 impl #impl_generics Drop for #name #ty_generics #where_clause {
143 fn drop(&mut self) { #(#calls)* }
144 }
145 }
146 .into()
147}