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