···
//! # Derive macros for jacquard lexicon types
+
//! This crate provides attribute and derive macros for working with Jacquard types.
+
//! The code generator uses `#[lexicon]` and `#[open_union]` to add lexicon-specific behavior.
+
//! You'll use `#[derive(IntoStatic)]` frequently, and `#[derive(XrpcRequest)]` when defining
+
//! custom XRPC endpoints.
···
//! // fn into_static(self) -> Self::Output { ... }
+
//! ### `#[derive(XrpcRequest)]`
+
//! Derives XRPC request traits for custom endpoints. Generates the response marker struct
+
//! and implements `XrpcRequest` (and optionally `XrpcEndpoint` for server-side).
+
//! #[derive(Serialize, Deserialize, XrpcRequest)]
+
//! nsid = "com.example.getThing",
+
//! output = GetThingOutput,
+
//! struct GetThing<'a> {
+
//! pub id: CowStr<'a>,
+
//! // - GetThingResponse struct
+
//! // - impl XrpcResp for GetThingResponse
+
//! // - impl XrpcRequest for GetThing
use proc_macro::TokenStream;
+
use quote::{quote, format_ident};
+
Data, DeriveInput, Fields, GenericParam, parse_macro_input,
+
Attribute, Ident, LitStr,
/// Attribute macro that adds an `extra_data` field to structs to capture unknown fields
/// during deserialization.
···
+
/// Derive macro for `XrpcRequest` trait.
+
/// Automatically generates the response marker struct, `XrpcResp` impl, and `XrpcRequest` impl
+
/// for an XRPC endpoint. Optionally generates `XrpcEndpoint` impl for server-side usage.
+
/// - `nsid`: Required. The NSID string (e.g., "com.example.myMethod")
+
/// - `method`: Required. Either `Query` or `Procedure`
+
/// - `output`: Required. The output type (must support lifetime param if request does)
+
/// - `error`: Optional. Error type (defaults to `GenericError`)
+
/// - `server`: Optional flag. If present, generates `XrpcEndpoint` impl too
+
/// #[derive(Serialize, Deserialize, XrpcRequest)]
+
/// nsid = "com.example.getThing",
+
/// output = GetThingOutput,
+
/// struct GetThing<'a> {
+
/// pub id: CowStr<'a>,
+
/// - `GetThingResponse` struct implementing `XrpcResp`
+
/// - `XrpcRequest` impl for `GetThing`
+
/// - Optionally: `GetThingEndpoint` struct implementing `XrpcEndpoint` (if `server` flag present)
+
#[proc_macro_derive(XrpcRequest, attributes(xrpc))]
+
pub fn derive_xrpc_request(input: TokenStream) -> TokenStream {
+
let input = parse_macro_input!(input as DeriveInput);
+
match xrpc_request_impl(&input) {
+
Ok(tokens) => tokens.into(),
+
Err(e) => e.to_compile_error().into(),
+
fn xrpc_request_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
+
let attrs = parse_xrpc_attrs(&input.attrs)?;
+
let name = &input.ident;
+
let generics = &input.generics;
+
// Detect if type has lifetime parameter
+
let has_lifetime = generics.lifetimes().next().is_some();
+
let lifetime = if has_lifetime {
+
let nsid = &attrs.nsid;
+
let method = method_expr(&attrs.method);
+
let output_ty = &attrs.output;
+
let error_ty = attrs.error.as_ref()
+
.map(|e| quote! { #e })
+
.unwrap_or_else(|| quote! { ::jacquard_common::xrpc::GenericError });
+
// Generate response marker struct name
+
let response_name = format_ident!("{}Response", name);
+
let mut output = quote! {
+
/// Response marker for #name
+
pub struct #response_name;
+
impl ::jacquard_common::xrpc::XrpcResp for #response_name {
+
const NSID: &'static str = #nsid;
+
const ENCODING: &'static str = "application/json";
+
type Output<'de> = #output_ty<'de>;
+
type Err<'de> = #error_ty<'de>;
+
impl #generics ::jacquard_common::xrpc::XrpcRequest for #name #lifetime {
+
const NSID: &'static str = #nsid;
+
const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method;
+
type Response = #response_name;
+
// Optional server-side endpoint impl
+
let endpoint_name = format_ident!("{}Endpoint", name);
+
let path = format!("/xrpc/{}", nsid);
+
// Request type with or without lifetime
+
let request_type = if has_lifetime {
+
/// Endpoint marker for #name (server-side)
+
pub struct #endpoint_name;
+
impl ::jacquard_common::xrpc::XrpcEndpoint for #endpoint_name {
+
const PATH: &'static str = #path;
+
const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method;
+
type Request<'de> = #request_type;
+
type Response = #response_name;
+
error: Option<syn::Type>,
+
fn parse_xrpc_attrs(attrs: &[Attribute]) -> syn::Result<XrpcAttrs> {
+
let mut server = false;
+
if !attr.path().is_ident("xrpc") {
+
attr.parse_nested_meta(|meta| {
+
if meta.path.is_ident("nsid") {
+
let value = meta.value()?;
+
let s: LitStr = value.parse()?;
+
nsid = Some(s.value());
+
} else if meta.path.is_ident("method") {
+
// Parse "method = Query" or "method = Procedure"
+
let _eq = meta.input.parse::<syn::Token![=]>()?;
+
let ident: Ident = meta.input.parse()?;
+
match ident.to_string().as_str() {
+
method = Some(XrpcMethod::Query);
+
// Always JSON, no custom encoding support
+
method = Some(XrpcMethod::Procedure);
+
other => Err(meta.error(format!("unknown method: {}, use Query or Procedure", other)))
+
} else if meta.path.is_ident("output") {
+
let value = meta.value()?;
+
output = Some(value.parse()?);
+
} else if meta.path.is_ident("error") {
+
let value = meta.value()?;
+
error = Some(value.parse()?);
+
} else if meta.path.is_ident("server") {
+
Err(meta.error("unknown xrpc attribute"))
+
let nsid = nsid.ok_or_else(|| syn::Error::new(
+
proc_macro2::Span::call_site(),
+
"missing required `nsid` attribute"
+
let method = method.ok_or_else(|| syn::Error::new(
+
proc_macro2::Span::call_site(),
+
"missing required `method` attribute"
+
let output = output.ok_or_else(|| syn::Error::new(
+
proc_macro2::Span::call_site(),
+
"missing required `output` attribute"
+
fn method_expr(method: &XrpcMethod) -> proc_macro2::TokenStream {
+
XrpcMethod::Query => quote! { ::jacquard_common::xrpc::XrpcMethod::Query },
+
XrpcMethod::Procedure => quote! { ::jacquard_common::xrpc::XrpcMethod::Procedure("application/json") },