hub_macro/
lib.rs

1//! Hub Method Macro
2//!
3//! Proc macro for defining hub methods where the function signature IS the schema.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use hub_macro::{hub_methods, hub_method};
9//!
10//! #[hub_methods(namespace = "bash", version = "1.0.0")]
11//! impl Bash {
12//!     /// Execute a bash command
13//!     #[hub_method]
14//!     async fn execute(&self, command: String) -> impl Stream<Item = BashEvent> {
15//!         // implementation
16//!     }
17//! }
18//! ```
19//!
20//! The macro extracts:
21//! - Method name from function name
22//! - Description from doc comments
23//! - Input schema from parameter types
24//! - Return type schema from Stream Item type
25
26mod 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
41/// Parsed attributes for hub_method (standalone version)
42struct HubMethodAttrs {
43    name: Option<String>,
44    /// Base crate path for imports (default: "crate")
45    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/// Attribute macro for hub methods within an impl block.
82///
83/// This is used inside a `#[hub_methods]` impl block to mark individual methods.
84/// When used standalone, it generates a schema function.
85///
86/// # Example
87///
88/// ```ignore
89/// #[hub_methods(namespace = "bash")]
90/// impl Bash {
91///     /// Execute a bash command
92///     #[hub_method]
93///     async fn execute(&self, command: String) -> impl Stream<Item = BashEvent> {
94///         // ...
95///     }
96/// }
97/// ```
98#[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    // Extract method name (from attr or function name)
111    let method_name = args
112        .name
113        .unwrap_or_else(|| input_fn.sig.ident.to_string());
114
115    // Extract description from doc comments
116    let description = extract_doc_comment(&input_fn);
117
118    // Extract input type from first parameter (if any)
119    let input_type = extract_input_type(&input_fn)?;
120
121    // Extract return type
122    let return_type = extract_return_type(&input_fn)?;
123
124    // Function name for the schema function
125    let fn_name = &input_fn.sig.ident;
126    let schema_fn_name = format_ident!("{}_schema", fn_name);
127
128    // Parse crate path
129    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    // Generate the schema function
133    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    // Return the original function plus the schema function
143    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    // Skip self parameter, get first real parameter
171    for arg in &input_fn.sig.inputs {
172        match arg {
173            FnArg::Receiver(_) => continue, // Skip &self
174            FnArg::Typed(pat_type) => {
175                // Skip context-like parameters
176                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; // Will be used for protocol schema in future
217    let _ = crate_path;
218
219    quote! {
220        /// Generated schema function for this hub method
221        #[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/// Attribute macro for impl blocks containing hub methods.
233///
234/// Generates:
235/// - Method enum for schema extraction
236/// - Activation trait implementation
237/// - RPC server trait and implementation
238///
239/// # Attributes
240///
241/// - `namespace = "..."` (required) - The activation namespace
242/// - `version = "..."` (optional, default: "1.0.0") - Version string
243/// - `description = "..."` (optional) - Activation description
244/// - `crate_path = "..."` (optional, default: "crate") - Path to substrate crate
245///
246/// # Example
247///
248/// ```ignore
249/// #[hub_methods(namespace = "bash", version = "1.0.0", description = "Execute bash commands")]
250/// impl Bash {
251///     /// Execute a bash command and stream output
252///     #[hub_method]
253///     async fn execute(&self, command: String) -> impl Stream<Item = BashEvent> + Send + 'static {
254///         self.executor.execute(&command).await
255///     }
256/// }
257/// ```
258#[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**: This derive macro is no longer needed.
270///
271/// With the caller-wraps streaming architecture, event types no longer need to
272/// implement `ActivationStreamItem`. Just use plain domain types with standard
273/// derives:
274///
275/// ```ignore
276/// #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
277/// #[serde(tag = "event", rename_all = "snake_case")]
278/// pub enum MyEvent {
279///     Data { value: String },
280///     Complete { result: i32 },
281/// }
282/// ```
283///
284/// The wrapping happens at the call site via `wrap_stream()`.
285#[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}