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}