···
//! # Derive macros for jacquard lexicon types
3
-
//! This crate provides attribute macros that the code generator uses to add lexicon-specific
4
-
//! behavior to generated types. You'll rarely need to use these directly unless you're writing
5
-
//! custom lexicon types by hand. However, deriving IntoStatic will likely be very useful.
3
+
//! This crate provides attribute and derive macros for working with Jacquard types.
4
+
//! The code generator uses `#[lexicon]` and `#[open_union]` to add lexicon-specific behavior.
5
+
//! You'll use `#[derive(IntoStatic)]` frequently, and `#[derive(XrpcRequest)]` when defining
6
+
//! custom XRPC endpoints.
···
//! // fn into_static(self) -> Self::Output { ... }
58
+
//! ### `#[derive(XrpcRequest)]`
60
+
//! Derives XRPC request traits for custom endpoints. Generates the response marker struct
61
+
//! and implements `XrpcRequest` (and optionally `XrpcEndpoint` for server-side).
64
+
//! #[derive(Serialize, Deserialize, XrpcRequest)]
66
+
//! nsid = "com.example.getThing",
68
+
//! output = GetThingOutput,
70
+
//! struct GetThing<'a> {
71
+
//! #[serde(borrow)]
72
+
//! pub id: CowStr<'a>,
75
+
//! // - GetThingResponse struct
76
+
//! // - impl XrpcResp for GetThingResponse
77
+
//! // - impl XrpcRequest for GetThing
use proc_macro::TokenStream;
59
-
use syn::{Data, DeriveInput, Fields, GenericParam, parse_macro_input};
81
+
use quote::{quote, format_ident};
83
+
Data, DeriveInput, Fields, GenericParam, parse_macro_input,
84
+
Attribute, Ident, LitStr,
/// Attribute macro that adds an `extra_data` field to structs to capture unknown fields
/// during deserialization.
···
365
+
/// Derive macro for `XrpcRequest` trait.
367
+
/// Automatically generates the response marker struct, `XrpcResp` impl, and `XrpcRequest` impl
368
+
/// for an XRPC endpoint. Optionally generates `XrpcEndpoint` impl for server-side usage.
372
+
/// - `nsid`: Required. The NSID string (e.g., "com.example.myMethod")
373
+
/// - `method`: Required. Either `Query` or `Procedure`
374
+
/// - `output`: Required. The output type (must support lifetime param if request does)
375
+
/// - `error`: Optional. Error type (defaults to `GenericError`)
376
+
/// - `server`: Optional flag. If present, generates `XrpcEndpoint` impl too
380
+
/// #[derive(Serialize, Deserialize, XrpcRequest)]
382
+
/// nsid = "com.example.getThing",
383
+
/// method = Query,
384
+
/// output = GetThingOutput,
386
+
/// struct GetThing<'a> {
387
+
/// #[serde(borrow)]
388
+
/// pub id: CowStr<'a>,
392
+
/// This generates:
393
+
/// - `GetThingResponse` struct implementing `XrpcResp`
394
+
/// - `XrpcRequest` impl for `GetThing`
395
+
/// - Optionally: `GetThingEndpoint` struct implementing `XrpcEndpoint` (if `server` flag present)
396
+
#[proc_macro_derive(XrpcRequest, attributes(xrpc))]
397
+
pub fn derive_xrpc_request(input: TokenStream) -> TokenStream {
398
+
let input = parse_macro_input!(input as DeriveInput);
400
+
match xrpc_request_impl(&input) {
401
+
Ok(tokens) => tokens.into(),
402
+
Err(e) => e.to_compile_error().into(),
406
+
fn xrpc_request_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
407
+
// Parse attributes
408
+
let attrs = parse_xrpc_attrs(&input.attrs)?;
410
+
let name = &input.ident;
411
+
let generics = &input.generics;
413
+
// Detect if type has lifetime parameter
414
+
let has_lifetime = generics.lifetimes().next().is_some();
415
+
let lifetime = if has_lifetime {
421
+
let nsid = &attrs.nsid;
422
+
let method = method_expr(&attrs.method);
423
+
let output_ty = &attrs.output;
424
+
let error_ty = attrs.error.as_ref()
425
+
.map(|e| quote! { #e })
426
+
.unwrap_or_else(|| quote! { ::jacquard_common::xrpc::GenericError });
428
+
// Generate response marker struct name
429
+
let response_name = format_ident!("{}Response", name);
432
+
let mut output = quote! {
433
+
/// Response marker for #name
434
+
pub struct #response_name;
436
+
impl ::jacquard_common::xrpc::XrpcResp for #response_name {
437
+
const NSID: &'static str = #nsid;
438
+
const ENCODING: &'static str = "application/json";
439
+
type Output<'de> = #output_ty<'de>;
440
+
type Err<'de> = #error_ty<'de>;
443
+
impl #generics ::jacquard_common::xrpc::XrpcRequest for #name #lifetime {
444
+
const NSID: &'static str = #nsid;
445
+
const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method;
446
+
type Response = #response_name;
450
+
// Optional server-side endpoint impl
452
+
let endpoint_name = format_ident!("{}Endpoint", name);
453
+
let path = format!("/xrpc/{}", nsid);
455
+
// Request type with or without lifetime
456
+
let request_type = if has_lifetime {
457
+
quote! { #name<'de> }
462
+
output.extend(quote! {
463
+
/// Endpoint marker for #name (server-side)
464
+
pub struct #endpoint_name;
466
+
impl ::jacquard_common::xrpc::XrpcEndpoint for #endpoint_name {
467
+
const PATH: &'static str = #path;
468
+
const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method;
469
+
type Request<'de> = #request_type;
470
+
type Response = #response_name;
480
+
method: XrpcMethod,
482
+
error: Option<syn::Type>,
491
+
fn parse_xrpc_attrs(attrs: &[Attribute]) -> syn::Result<XrpcAttrs> {
492
+
let mut nsid = None;
493
+
let mut method = None;
494
+
let mut output = None;
495
+
let mut error = None;
496
+
let mut server = false;
498
+
for attr in attrs {
499
+
if !attr.path().is_ident("xrpc") {
503
+
attr.parse_nested_meta(|meta| {
504
+
if meta.path.is_ident("nsid") {
505
+
let value = meta.value()?;
506
+
let s: LitStr = value.parse()?;
507
+
nsid = Some(s.value());
509
+
} else if meta.path.is_ident("method") {
510
+
// Parse "method = Query" or "method = Procedure"
511
+
let _eq = meta.input.parse::<syn::Token![=]>()?;
512
+
let ident: Ident = meta.input.parse()?;
513
+
match ident.to_string().as_str() {
515
+
method = Some(XrpcMethod::Query);
519
+
// Always JSON, no custom encoding support
520
+
method = Some(XrpcMethod::Procedure);
523
+
other => Err(meta.error(format!("unknown method: {}, use Query or Procedure", other)))
525
+
} else if meta.path.is_ident("output") {
526
+
let value = meta.value()?;
527
+
output = Some(value.parse()?);
529
+
} else if meta.path.is_ident("error") {
530
+
let value = meta.value()?;
531
+
error = Some(value.parse()?);
533
+
} else if meta.path.is_ident("server") {
537
+
Err(meta.error("unknown xrpc attribute"))
542
+
let nsid = nsid.ok_or_else(|| syn::Error::new(
543
+
proc_macro2::Span::call_site(),
544
+
"missing required `nsid` attribute"
546
+
let method = method.ok_or_else(|| syn::Error::new(
547
+
proc_macro2::Span::call_site(),
548
+
"missing required `method` attribute"
550
+
let output = output.ok_or_else(|| syn::Error::new(
551
+
proc_macro2::Span::call_site(),
552
+
"missing required `output` attribute"
564
+
fn method_expr(method: &XrpcMethod) -> proc_macro2::TokenStream {
566
+
XrpcMethod::Query => quote! { ::jacquard_common::xrpc::XrpcMethod::Query },
567
+
XrpcMethod::Procedure => quote! { ::jacquard_common::xrpc::XrpcMethod::Procedure("application/json") },