Skip to main content

hush_macros/
lib.rs

1//! Proc macros for the Hush workflow engine.
2//!
3//! Provides:
4//! - `#[hush_op]` — auto-register Rust ops via `inventory`
5//! - `#[hush_model]` — shorthand for `#[derive(Serialize, Deserialize, Debug, Clone)]`
6//!
7//! # Usage
8//!
9//! ```rust,ignore
10//! use hush_serve::{hush_op, hush_model};
11//!
12//! #[hush_model]
13//! struct Conversation { vads: Vec<Vad> }
14//!
15//! // Legacy style (untyped):
16//! #[hush_op]
17//! fn double(inputs: &Value) -> Value {
18//!     let x = inputs["x"].as_i64().unwrap();
19//!     serde_json::json!({"result": x * 2})
20//! }
21//!
22//! // Typed style (auto-generates serde wrapper):
23//! #[hush_op]
24//! fn classify(conversation: Conversation, threshold: f64) -> ClassifyResult {
25//!     // Pure business logic — no JSON parsing
26//! }
27//!
28//! #[hush_op(generator)]
29//! fn each_item(inputs: &Value) -> Value {
30//!     let items = inputs["items"].as_array().unwrap();
31//!     Value::Array(items.iter().map(|i| serde_json::json!({"value": i})).collect())
32//! }
33//! ```
34
35use proc_macro::TokenStream;
36use quote::quote;
37use syn::{parse_macro_input, ItemFn, ItemStruct, Meta, parse::Parse, parse::ParseStream, LitStr, Token};
38
39/// Auto-register a function as a Hush op.
40///
41/// Supports both legacy `fn(inputs: &Value) -> Value` and typed signatures.
42/// For typed signatures, a serde deserialize/serialize wrapper is generated.
43///
44/// # Attributes
45///
46/// - `#[hush_op]` — register as a regular op
47/// - `#[hush_op(generator)]` — register as a generator op
48/// - `#[hush_op(name = "custom_name")]` — override the op name
49/// - `#[hush_op(generator, name = "custom_name")]` — both
50#[proc_macro_attribute]
51pub fn hush_op(attr: TokenStream, item: TokenStream) -> TokenStream {
52    let input_fn = parse_macro_input!(item as ItemFn);
53    let fn_name = &input_fn.sig.ident;
54    let fn_name_str = fn_name.to_string();
55
56    // Parse attributes
57    let mut is_generator = false;
58    let mut custom_name: Option<String> = None;
59
60    if !attr.is_empty() {
61        let meta_list: syn::punctuated::Punctuated<Meta, syn::Token![,]> =
62            parse_macro_input!(attr with syn::punctuated::Punctuated::parse_terminated);
63
64        for meta in &meta_list {
65            match meta {
66                Meta::Path(path) if path.is_ident("generator") => {
67                    is_generator = true;
68                }
69                Meta::NameValue(nv) if nv.path.is_ident("name") => {
70                    if let syn::Expr::Lit(syn::ExprLit {
71                        lit: syn::Lit::Str(s),
72                        ..
73                    }) = &nv.value
74                    {
75                        custom_name = Some(s.value());
76                    }
77                }
78                _ => {}
79            }
80        }
81    }
82
83    let op_name = custom_name.unwrap_or(fn_name_str);
84
85    // Detect typed vs legacy signature
86    let is_typed = is_typed_signature(&input_fn);
87
88    let (call_expr, wrapper_fn) = if is_typed {
89        generate_typed_wrapper(&input_fn, &op_name)
90    } else {
91        // Legacy: fn(inputs: &Value) -> Value — no wrapper
92        let call = quote! { |v| #fn_name(v) };
93        (call, quote! {})
94    };
95
96    let submit = if is_generator {
97        quote! {
98            ::inventory::submit! {
99                ::hush_serve::OpEntry::new_gen(#op_name, module_path!(), #call_expr)
100            }
101        }
102    } else {
103        quote! {
104            ::inventory::submit! {
105                ::hush_serve::OpEntry::new_op(#op_name, module_path!(), #call_expr)
106            }
107        }
108    };
109
110    let output = quote! {
111        #input_fn
112        #wrapper_fn
113        #submit
114    };
115
116    output.into()
117}
118
119/// Check if the function has a typed signature (not `fn(inputs: &Value) -> Value`).
120///
121/// Legacy detection: single param of type `&Value` (any param name).
122/// This covers `inputs: &Value`, `_inputs: &Value`, `input: &Value`, etc.
123fn is_typed_signature(func: &ItemFn) -> bool {
124    let params: Vec<_> = func.sig.inputs.iter().collect();
125    if params.len() == 1 {
126        if let syn::FnArg::Typed(pat_type) = &params[0] {
127            if is_ref_to_value(&pat_type.ty) {
128                return false; // Legacy: single &Value param
129            }
130        }
131    }
132    // Multiple params or non-Value param → typed
133    !params.is_empty()
134}
135
136/// Check if a type is `&Value` or `&serde_json::Value`.
137fn is_ref_to_value(ty: &syn::Type) -> bool {
138    if let syn::Type::Reference(r) = ty {
139        return is_value_type(&r.elem);
140    }
141    false
142}
143
144/// Check if a type is `Value` or `serde_json::Value`.
145fn is_value_type(ty: &syn::Type) -> bool {
146    match ty {
147        syn::Type::Path(tp) => {
148            let segments: Vec<_> = tp.path.segments.iter().collect();
149            match segments.len() {
150                1 => segments[0].ident == "Value",
151                2 => segments[0].ident == "serde_json" && segments[1].ident == "Value",
152                _ => false,
153            }
154        }
155        _ => false,
156    }
157}
158
159/// Generate a typed wrapper function that deserializes params and serializes the return.
160fn generate_typed_wrapper(func: &ItemFn, _op_name: &str) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
161    let fn_name = &func.sig.ident;
162    let wrapper_name = syn::Ident::new(
163        &format!("__hush_{}_wrapper", fn_name),
164        fn_name.span(),
165    );
166
167    // Extract param names and types
168    let mut deserialize_stmts = Vec::new();
169    let mut call_args = Vec::new();
170
171    for arg in &func.sig.inputs {
172        if let syn::FnArg::Typed(pat_type) = arg {
173            if let syn::Pat::Ident(ident) = &*pat_type.pat {
174                let param_name = &ident.ident;
175                let param_name_str = param_name.to_string();
176                let param_type = &pat_type.ty;
177
178                deserialize_stmts.push(quote! {
179                    let #param_name: #param_type = match ::serde_json::from_value(
180                        __inputs.get(#param_name_str).cloned().unwrap_or(::serde_json::Value::Null)
181                    ) {
182                        Ok(v) => v,
183                        Err(e) => return ::serde_json::json!({
184                            "error": format!("hush_op '{}': param '{}': {}", stringify!(#fn_name), #param_name_str, e)
185                        }),
186                    };
187                });
188                call_args.push(quote! { #param_name });
189            }
190        }
191    }
192
193    // Check return type
194    let is_value_return = match &func.sig.output {
195        syn::ReturnType::Default => true,
196        syn::ReturnType::Type(_, ty) => is_value_type(ty),
197    };
198
199    let call_and_return = if is_value_return {
200        quote! { #fn_name(#(#call_args),*) }
201    } else {
202        quote! {
203            let __result = #fn_name(#(#call_args),*);
204            ::serde_json::to_value(__result).unwrap_or_else(|e| ::serde_json::json!({
205                "error": format!("hush_op '{}': failed to serialize return: {}", stringify!(#fn_name), e)
206            }))
207        }
208    };
209
210    let wrapper = quote! {
211        fn #wrapper_name(__inputs: &::serde_json::Value) -> ::serde_json::Value {
212            #(#deserialize_stmts)*
213            #call_and_return
214        }
215    };
216
217    let call_expr = quote! { |v| #wrapper_name(v) };
218
219    (call_expr, wrapper)
220}
221
222// --- #[hush_resource] ---
223
224struct ResourceArgs {
225    name: String,
226}
227
228impl Parse for ResourceArgs {
229    fn parse(input: ParseStream) -> syn::Result<Self> {
230        let _ident: syn::Ident = input.parse()?;
231        let _eq: Token![=] = input.parse()?;
232        let name: LitStr = input.parse()?;
233        Ok(ResourceArgs { name: name.value() })
234    }
235}
236
237/// Auto-register a function as a resource factory.
238#[proc_macro_attribute]
239pub fn hush_resource(attr: TokenStream, item: TokenStream) -> TokenStream {
240    let args = parse_macro_input!(attr as ResourceArgs);
241    let input_fn = parse_macro_input!(item as ItemFn);
242    let fn_name = &input_fn.sig.ident;
243    let resource_name = &args.name;
244
245    let submit = quote! {
246        ::inventory::submit! {
247            ::hush_serve::ResourceEntry::new(#resource_name, |config| {
248                Box::new(#fn_name(config)) as Box<dyn ::std::any::Any + Send + Sync>
249            })
250        }
251    };
252
253    let output = quote! {
254        #input_fn
255        #submit
256    };
257
258    output.into()
259}
260
261// --- #[hush_model] ---
262
263/// Shorthand for `#[derive(Serialize, Deserialize, Debug, Clone)]`.
264///
265/// ```rust,ignore
266/// #[hush_model]
267/// struct Conversation {
268///     vads: Vec<Vad>,
269/// }
270/// ```
271#[proc_macro_attribute]
272pub fn hush_model(_attr: TokenStream, item: TokenStream) -> TokenStream {
273    let input_struct = parse_macro_input!(item as ItemStruct);
274
275    let output = quote! {
276        #[derive(::serde::Serialize, ::serde::Deserialize, Debug, Clone)]
277        #input_struct
278    };
279
280    output.into()
281}