mod codegen;
mod handle_enum;
mod parse;
mod request;
mod stream_event;
use codegen::generate_all;
use parse::HubMethodsAttrs;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, punctuated::Punctuated, Expr, ExprLit, FnArg, ItemFn, ItemImpl, Lit, Meta,
MetaNameValue, Pat, ReturnType, Token, Type,
};
struct HubMethodAttrs {
name: Option<String>,
description: Option<String>,
crate_path: String,
}
impl Parse for HubMethodAttrs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut name = None;
let mut description: Option<String> = None;
let mut crate_path = "crate".to_string();
if !input.is_empty() {
let metas = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
for meta in metas {
if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta {
if path.is_ident("name") {
if let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = value
{
name = Some(s.value());
}
} else if path.is_ident("description") {
if let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = value
{
description = Some(s.value());
}
} else if path.is_ident("crate_path") {
if let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = value
{
crate_path = s.value();
}
}
}
}
}
Ok(HubMethodAttrs { name, description, crate_path })
}
}
#[proc_macro_attribute]
pub fn method(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as HubMethodAttrs);
let input_fn = parse_macro_input!(item as ItemFn);
match hub_method_impl(args, input_fn) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
#[deprecated(since = "0.5.0", note = "Use `plexus::method` instead")]
#[proc_macro_attribute]
pub fn hub_method(attr: TokenStream, item: TokenStream) -> TokenStream {
method(attr, item)
}
#[proc_macro_attribute]
pub fn child(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
#[proc_macro_attribute]
pub fn removed_in(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr_str: String = match syn::parse::<syn::LitStr>(attr.clone()) {
Ok(s) => s.value(),
Err(_) => {
return item;
}
};
let marker = format!("__plexus_removed_in:{}", attr_str);
let item_ts: proc_macro2::TokenStream = item.into();
let marker_lit = proc_macro2::Literal::string(&marker);
let out = quote::quote! {
#[doc(hidden)]
#[doc = #marker_lit]
#item_ts
};
out.into()
}
fn hub_method_impl(args: HubMethodAttrs, input_fn: ItemFn) -> syn::Result<TokenStream2> {
let method_name = args
.name
.unwrap_or_else(|| input_fn.sig.ident.to_string());
let description = match args.description {
Some(explicit) => explicit,
None => extract_doc_comment(&input_fn),
};
let input_type = extract_input_type(&input_fn)?;
let return_type = extract_return_type(&input_fn)?;
let fn_name = &input_fn.sig.ident;
let schema_fn_name = format_ident!("{}_schema", fn_name);
let crate_path: syn::Path = syn::parse_str(&args.crate_path)
.map_err(|e| syn::Error::new_spanned(&input_fn.sig, format!("Invalid crate_path: {}", e)))?;
let schema_fn = generate_schema_fn(
&schema_fn_name,
&method_name,
&description,
input_type.as_ref(),
&return_type,
&crate_path,
);
Ok(quote! {
#input_fn
#schema_fn
})
}
fn extract_doc_comment(input_fn: &ItemFn) -> String {
parse::extract_doc_description(&input_fn.attrs).unwrap_or_default()
}
fn extract_input_type(input_fn: &ItemFn) -> syn::Result<Option<Type>> {
for arg in &input_fn.sig.inputs {
match arg {
FnArg::Receiver(_) => continue, FnArg::Typed(pat_type) => {
if let Pat::Ident(ident) = &*pat_type.pat {
let name = ident.ident.to_string();
if name == "ctx" || name == "context" || name == "self_" {
continue;
}
}
return Ok(Some((*pat_type.ty).clone()));
}
}
}
Ok(None)
}
fn extract_return_type(input_fn: &ItemFn) -> syn::Result<Type> {
match &input_fn.sig.output {
ReturnType::Default => Err(syn::Error::new_spanned(
&input_fn.sig,
"hub_method requires a return type",
)),
ReturnType::Type(_, ty) => Ok((*ty.clone()).clone()),
}
}
fn generate_schema_fn(
fn_name: &syn::Ident,
method_name: &str,
description: &str,
input_type: Option<&Type>,
return_type: &Type,
crate_path: &syn::Path,
) -> TokenStream2 {
let input_schema = if let Some(input_ty) = input_type {
quote! {
Some(serde_json::to_value(schemars::schema_for!(#input_ty)).unwrap())
}
} else {
quote! { None }
};
let _ = return_type; let _ = crate_path;
quote! {
#[allow(dead_code)]
pub fn #fn_name() -> serde_json::Value {
serde_json::json!({
"name": #method_name,
"description": #description,
"input": #input_schema,
})
}
}
}
#[proc_macro_attribute]
pub fn activation(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as HubMethodsAttrs);
let input_impl = parse_macro_input!(item as ItemImpl);
match generate_all(args, input_impl) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
#[deprecated(since = "0.5.0", note = "Use `plexus::activation` instead")]
#[proc_macro_attribute]
pub fn hub_methods(attr: TokenStream, item: TokenStream) -> TokenStream {
activation(attr, item)
}
#[deprecated(
since = "0.2.0",
note = "No longer needed - use plain domain types with Serialize/Deserialize"
)]
#[proc_macro_derive(StreamEvent, attributes(stream_event, terminal))]
pub fn stream_event_derive(input: TokenStream) -> TokenStream {
stream_event::derive(input)
}
#[proc_macro_derive(HandleEnum, attributes(handle))]
pub fn handle_enum_derive(input: TokenStream) -> TokenStream {
handle_enum::derive(input)
}
#[proc_macro_derive(
PlexusRequest,
attributes(from_cookie, from_header, from_query, from_peer, from_auth_context)
)]
pub fn plexus_request_derive(input: TokenStream) -> TokenStream {
request::derive(input)
}
#[cfg(feature = "schemars-compat")]
#[proc_macro_derive(JsonSchema, attributes(schemars, serde))]
pub fn json_schema_noop_derive(_input: TokenStream) -> TokenStream {
TokenStream::new()
}