Skip to main content

plexus_macros/
lib.rs

1//! Plexus RPC Method Macro
2//!
3//! Proc macro for defining Plexus RPC 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 handle_enum;
28mod parse;
29mod stream_event;
30
31use codegen::generate_all;
32use parse::HubMethodsAttrs;
33use proc_macro::TokenStream;
34use proc_macro2::TokenStream as TokenStream2;
35use quote::{format_ident, quote};
36use syn::{
37    parse::{Parse, ParseStream},
38    parse_macro_input, punctuated::Punctuated, Expr, ExprLit, FnArg, ItemFn, ItemImpl, Lit, Meta,
39    MetaNameValue, Pat, ReturnType, Token, Type,
40};
41
42/// Parsed attributes for hub_method (standalone version)
43struct HubMethodAttrs {
44    name: Option<String>,
45    /// Base crate path for imports (default: "crate")
46    crate_path: String,
47}
48
49impl Parse for HubMethodAttrs {
50    fn parse(input: ParseStream) -> syn::Result<Self> {
51        let mut name = None;
52        let mut crate_path = "crate".to_string();
53
54        if !input.is_empty() {
55            let metas = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
56
57            for meta in metas {
58                if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta {
59                    if path.is_ident("name") {
60                        if let Expr::Lit(ExprLit {
61                            lit: Lit::Str(s), ..
62                        }) = value
63                        {
64                            name = Some(s.value());
65                        }
66                    } else if path.is_ident("crate_path") {
67                        if let Expr::Lit(ExprLit {
68                            lit: Lit::Str(s), ..
69                        }) = value
70                        {
71                            crate_path = s.value();
72                        }
73                    }
74                }
75            }
76        }
77
78        Ok(HubMethodAttrs { name, crate_path })
79    }
80}
81
82/// Attribute macro for hub methods within an impl block.
83///
84/// This is used inside a `#[hub_methods]` impl block to mark individual methods.
85/// When used standalone, it generates a schema function.
86///
87/// # Example
88///
89/// ```ignore
90/// #[hub_methods(namespace = "bash")]
91/// impl Bash {
92///     /// Execute a bash command
93///     #[hub_method]
94///     async fn execute(&self, command: String) -> impl Stream<Item = BashEvent> {
95///         // ...
96///     }
97/// }
98/// ```
99#[proc_macro_attribute]
100pub fn hub_method(attr: TokenStream, item: TokenStream) -> TokenStream {
101    let args = parse_macro_input!(attr as HubMethodAttrs);
102    let input_fn = parse_macro_input!(item as ItemFn);
103
104    match hub_method_impl(args, input_fn) {
105        Ok(tokens) => tokens.into(),
106        Err(e) => e.to_compile_error().into(),
107    }
108}
109
110fn hub_method_impl(args: HubMethodAttrs, input_fn: ItemFn) -> syn::Result<TokenStream2> {
111    // Extract method name (from attr or function name)
112    let method_name = args
113        .name
114        .unwrap_or_else(|| input_fn.sig.ident.to_string());
115
116    // Extract description from doc comments
117    let description = extract_doc_comment(&input_fn);
118
119    // Extract input type from first parameter (if any)
120    let input_type = extract_input_type(&input_fn)?;
121
122    // Extract return type
123    let return_type = extract_return_type(&input_fn)?;
124
125    // Function name for the schema function
126    let fn_name = &input_fn.sig.ident;
127    let schema_fn_name = format_ident!("{}_schema", fn_name);
128
129    // Parse crate path
130    let crate_path: syn::Path = syn::parse_str(&args.crate_path)
131        .map_err(|e| syn::Error::new_spanned(&input_fn.sig, format!("Invalid crate_path: {}", e)))?;
132
133    // Generate the schema function
134    let schema_fn = generate_schema_fn(
135        &schema_fn_name,
136        &method_name,
137        &description,
138        input_type.as_ref(),
139        &return_type,
140        &crate_path,
141    );
142
143    // Return the original function plus the schema function
144    Ok(quote! {
145        #input_fn
146
147        #schema_fn
148    })
149}
150
151fn extract_doc_comment(input_fn: &ItemFn) -> String {
152    let mut doc_lines = Vec::new();
153
154    for attr in &input_fn.attrs {
155        if attr.path().is_ident("doc") {
156            if let Meta::NameValue(MetaNameValue { value, .. }) = &attr.meta {
157                if let Expr::Lit(ExprLit {
158                    lit: Lit::Str(s), ..
159                }) = value
160                {
161                    doc_lines.push(s.value().trim().to_string());
162                }
163            }
164        }
165    }
166
167    doc_lines.join(" ")
168}
169
170fn extract_input_type(input_fn: &ItemFn) -> syn::Result<Option<Type>> {
171    // Skip self parameter, get first real parameter
172    for arg in &input_fn.sig.inputs {
173        match arg {
174            FnArg::Receiver(_) => continue, // Skip &self
175            FnArg::Typed(pat_type) => {
176                // Skip context-like parameters
177                if let Pat::Ident(ident) = &*pat_type.pat {
178                    let name = ident.ident.to_string();
179                    if name == "ctx" || name == "context" || name == "self_" {
180                        continue;
181                    }
182                }
183                return Ok(Some((*pat_type.ty).clone()));
184            }
185        }
186    }
187
188    Ok(None)
189}
190
191fn extract_return_type(input_fn: &ItemFn) -> syn::Result<Type> {
192    match &input_fn.sig.output {
193        ReturnType::Default => Err(syn::Error::new_spanned(
194            &input_fn.sig,
195            "hub_method requires a return type",
196        )),
197        ReturnType::Type(_, ty) => Ok((*ty.clone()).clone()),
198    }
199}
200
201fn generate_schema_fn(
202    fn_name: &syn::Ident,
203    method_name: &str,
204    description: &str,
205    input_type: Option<&Type>,
206    return_type: &Type,
207    crate_path: &syn::Path,
208) -> TokenStream2 {
209    let input_schema = if let Some(input_ty) = input_type {
210        quote! {
211            Some(serde_json::to_value(schemars::schema_for!(#input_ty)).unwrap())
212        }
213    } else {
214        quote! { None }
215    };
216
217    let _ = return_type; // Will be used for protocol schema in future
218    let _ = crate_path;
219
220    quote! {
221        /// Generated schema function for this hub method
222        #[allow(dead_code)]
223        pub fn #fn_name() -> serde_json::Value {
224            serde_json::json!({
225                "name": #method_name,
226                "description": #description,
227                "input": #input_schema,
228            })
229        }
230    }
231}
232
233/// Attribute macro for impl blocks containing hub methods.
234///
235/// Generates:
236/// - Method enum for schema extraction
237/// - Activation trait implementation
238/// - RPC server trait and implementation
239///
240/// # Attributes
241///
242/// - `namespace = "..."` (required) - The activation namespace
243/// - `version = "..."` (optional, default: "1.0.0") - Version string
244/// - `description = "..."` (optional) - Activation description
245/// - `crate_path = "..."` (optional, default: "crate") - Path to substrate crate
246///
247/// # Example
248///
249/// ```ignore
250/// #[hub_methods(namespace = "bash", version = "1.0.0", description = "Execute bash commands")]
251/// impl Bash {
252///     /// Execute a bash command and stream output
253///     #[hub_method]
254///     async fn execute(&self, command: String) -> impl Stream<Item = BashEvent> + Send + 'static {
255///         self.executor.execute(&command).await
256///     }
257/// }
258/// ```
259#[proc_macro_attribute]
260pub fn hub_methods(attr: TokenStream, item: TokenStream) -> TokenStream {
261    let args = parse_macro_input!(attr as HubMethodsAttrs);
262    let input_impl = parse_macro_input!(item as ItemImpl);
263
264    match generate_all(args, input_impl) {
265        Ok(tokens) => tokens.into(),
266        Err(e) => e.to_compile_error().into(),
267    }
268}
269
270/// **DEPRECATED**: This derive macro is no longer needed.
271///
272/// With the caller-wraps streaming architecture, event types no longer need to
273/// implement `ActivationStreamItem`. Just use plain domain types with standard
274/// derives:
275///
276/// ```ignore
277/// #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
278/// #[serde(tag = "event", rename_all = "snake_case")]
279/// pub enum MyEvent {
280///     Data { value: String },
281///     Complete { result: i32 },
282/// }
283/// ```
284///
285/// The wrapping happens at the call site via `wrap_stream()`.
286#[deprecated(
287    since = "0.2.0",
288    note = "No longer needed - use plain domain types with Serialize/Deserialize"
289)]
290#[proc_macro_derive(StreamEvent, attributes(stream_event, terminal))]
291pub fn stream_event_derive(input: TokenStream) -> TokenStream {
292    stream_event::derive(input)
293}
294
295/// Derive macro for type-safe handle creation and parsing.
296///
297/// Generates:
298/// - `to_handle(&self) -> Handle` - converts enum variant to Handle
299/// - `impl TryFrom<&Handle>` - parses Handle back to enum variant
300/// - `impl From<EnumName> for Handle` - convenience conversion
301///
302/// # Attributes
303///
304/// Enum-level (required):
305/// - `plugin_id = "CONSTANT_NAME"` - Name of the constant holding the plugin UUID
306/// - `version = "1.0.0"` - Semantic version for handles
307/// - `crate_path = "..."` (optional, default: "plexus_core") - Path to plexus_core crate
308///
309/// Variant-level:
310/// - `method = "..."` (required) - The handle.method value
311/// - `table = "..."` (optional) - SQLite table name (for future resolution)
312/// - `key = "..."` (optional) - Primary key column (for future resolution)
313///
314/// # Example
315///
316/// ```ignore
317/// use hub_macro::HandleEnum;
318/// use uuid::Uuid;
319///
320/// pub const MY_PLUGIN_ID: Uuid = uuid::uuid!("550e8400-e29b-41d4-a716-446655440000");
321///
322/// #[derive(HandleEnum)]
323/// #[handle(plugin_id = "MY_PLUGIN_ID", version = "1.0.0")]
324/// pub enum MyPluginHandle {
325///     #[handle(method = "event", table = "events", key = "id")]
326///     Event { event_id: String },
327///
328///     #[handle(method = "message")]
329///     Message { message_id: String, role: String },
330/// }
331///
332/// // Usage:
333/// let handle_enum = MyPluginHandle::Event { event_id: "evt-123".into() };
334/// let handle: Handle = handle_enum.to_handle();
335/// // handle.method == "event"
336/// // handle.meta == ["evt-123"]
337///
338/// // Parsing back:
339/// let parsed = MyPluginHandle::try_from(&handle)?;
340/// ```
341#[proc_macro_derive(HandleEnum, attributes(handle))]
342pub fn handle_enum_derive(input: TokenStream) -> TokenStream {
343    handle_enum::derive(input)
344}