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 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}