1mod codegen;
27mod parse;
28mod stream_event;
29
30use codegen::generate_all;
31use parse::HubMethodsAttrs;
32use proc_macro::TokenStream;
33use proc_macro2::TokenStream as TokenStream2;
34use quote::{format_ident, quote};
35use syn::{
36 parse::{Parse, ParseStream},
37 parse_macro_input, punctuated::Punctuated, Expr, ExprLit, FnArg, ItemFn, ItemImpl, Lit, Meta,
38 MetaNameValue, Pat, ReturnType, Token, Type,
39};
40
41struct HubMethodAttrs {
43 name: Option<String>,
44 crate_path: String,
46}
47
48impl Parse for HubMethodAttrs {
49 fn parse(input: ParseStream) -> syn::Result<Self> {
50 let mut name = None;
51 let mut crate_path = "crate".to_string();
52
53 if !input.is_empty() {
54 let metas = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
55
56 for meta in metas {
57 if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta {
58 if path.is_ident("name") {
59 if let Expr::Lit(ExprLit {
60 lit: Lit::Str(s), ..
61 }) = value
62 {
63 name = Some(s.value());
64 }
65 } else if path.is_ident("crate_path") {
66 if let Expr::Lit(ExprLit {
67 lit: Lit::Str(s), ..
68 }) = value
69 {
70 crate_path = s.value();
71 }
72 }
73 }
74 }
75 }
76
77 Ok(HubMethodAttrs { name, crate_path })
78 }
79}
80
81#[proc_macro_attribute]
99pub fn hub_method(attr: TokenStream, item: TokenStream) -> TokenStream {
100 let args = parse_macro_input!(attr as HubMethodAttrs);
101 let input_fn = parse_macro_input!(item as ItemFn);
102
103 match hub_method_impl(args, input_fn) {
104 Ok(tokens) => tokens.into(),
105 Err(e) => e.to_compile_error().into(),
106 }
107}
108
109fn hub_method_impl(args: HubMethodAttrs, input_fn: ItemFn) -> syn::Result<TokenStream2> {
110 let method_name = args
112 .name
113 .unwrap_or_else(|| input_fn.sig.ident.to_string());
114
115 let description = extract_doc_comment(&input_fn);
117
118 let input_type = extract_input_type(&input_fn)?;
120
121 let return_type = extract_return_type(&input_fn)?;
123
124 let fn_name = &input_fn.sig.ident;
126 let schema_fn_name = format_ident!("{}_schema", fn_name);
127
128 let crate_path: syn::Path = syn::parse_str(&args.crate_path)
130 .map_err(|e| syn::Error::new_spanned(&input_fn.sig, format!("Invalid crate_path: {}", e)))?;
131
132 let schema_fn = generate_schema_fn(
134 &schema_fn_name,
135 &method_name,
136 &description,
137 input_type.as_ref(),
138 &return_type,
139 &crate_path,
140 );
141
142 Ok(quote! {
144 #input_fn
145
146 #schema_fn
147 })
148}
149
150fn extract_doc_comment(input_fn: &ItemFn) -> String {
151 let mut doc_lines = Vec::new();
152
153 for attr in &input_fn.attrs {
154 if attr.path().is_ident("doc") {
155 if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta {
156 if let Expr::Lit(ExprLit {
157 lit: Lit::Str(s), ..
158 }) = value
159 {
160 doc_lines.push(s.value().trim().to_string());
161 }
162 }
163 }
164 }
165
166 doc_lines.join(" ")
167}
168
169fn extract_input_type(input_fn: &ItemFn) -> syn::Result<Option<Type>> {
170 for arg in &input_fn.sig.inputs {
172 match arg {
173 FnArg::Receiver(_) => continue, FnArg::Typed(pat_type) => {
175 if let Pat::Ident(ident) = &*pat_type.pat {
177 let name = ident.ident.to_string();
178 if name == "ctx" || name == "context" || name == "self_" {
179 continue;
180 }
181 }
182 return Ok(Some((*pat_type.ty).clone()));
183 }
184 }
185 }
186
187 Ok(None)
188}
189
190fn extract_return_type(input_fn: &ItemFn) -> syn::Result<Type> {
191 match &input_fn.sig.output {
192 ReturnType::Default => Err(syn::Error::new_spanned(
193 &input_fn.sig,
194 "hub_method requires a return type",
195 )),
196 ReturnType::Type(_, ty) => Ok((*ty.clone()).clone()),
197 }
198}
199
200fn generate_schema_fn(
201 fn_name: &syn::Ident,
202 method_name: &str,
203 description: &str,
204 input_type: Option<&Type>,
205 return_type: &Type,
206 crate_path: &syn::Path,
207) -> TokenStream2 {
208 let input_schema = if let Some(input_ty) = input_type {
209 quote! {
210 Some(serde_json::to_value(schemars::schema_for!(#input_ty)).unwrap())
211 }
212 } else {
213 quote! { None }
214 };
215
216 let _ = return_type; let _ = crate_path;
218
219 quote! {
220 #[allow(dead_code)]
222 pub fn #fn_name() -> serde_json::Value {
223 serde_json::json!({
224 "name": #method_name,
225 "description": #description,
226 "input": #input_schema,
227 })
228 }
229 }
230}
231
232#[proc_macro_attribute]
259pub fn hub_methods(attr: TokenStream, item: TokenStream) -> TokenStream {
260 let args = parse_macro_input!(attr as HubMethodsAttrs);
261 let input_impl = parse_macro_input!(item as ItemImpl);
262
263 match generate_all(args, input_impl) {
264 Ok(tokens) => tokens.into(),
265 Err(e) => e.to_compile_error().into(),
266 }
267}
268
269#[deprecated(
286 since = "0.2.0",
287 note = "No longer needed - use plain domain types with Serialize/Deserialize"
288)]
289#[proc_macro_derive(StreamEvent, attributes(stream_event, terminal))]
290pub fn stream_event_derive(input: TokenStream) -> TokenStream {
291 stream_event::derive(input)
292}