1use proc_macro::TokenStream;
36use quote::quote;
37use syn::{parse_macro_input, ItemFn, ItemStruct, Meta, parse::Parse, parse::ParseStream, LitStr, Token};
38
39#[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 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 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 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
119fn 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) = ¶ms[0] {
127 if is_ref_to_value(&pat_type.ty) {
128 return false; }
130 }
131 }
132 !params.is_empty()
134}
135
136fn 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
144fn 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
159fn 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 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 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
222struct 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#[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#[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}