A better Rust ATProto crate
1//! Implementation of #[derive(XrpcRequest)] macro
2
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote};
5use syn::{parse2, Attribute, DeriveInput, Ident, LitStr};
6
7/// Implementation for the XrpcRequest derive macro
8pub fn impl_derive_xrpc_request(input: TokenStream) -> TokenStream {
9 let input = match parse2::<DeriveInput>(input) {
10 Ok(input) => input,
11 Err(e) => return e.to_compile_error(),
12 };
13
14 match xrpc_request_impl(&input) {
15 Ok(tokens) => tokens,
16 Err(e) => e.to_compile_error(),
17 }
18}
19
20fn xrpc_request_impl(input: &DeriveInput) -> syn::Result<TokenStream> {
21 // Parse attributes
22 let attrs = parse_xrpc_attrs(&input.attrs)?;
23
24 let name = &input.ident;
25 let generics = &input.generics;
26
27 // Detect if type has lifetime parameter
28 let has_lifetime = generics.lifetimes().next().is_some();
29 let lifetime = if has_lifetime {
30 quote! { <'_> }
31 } else {
32 quote! {}
33 };
34
35 let nsid = &attrs.nsid;
36 let method = method_expr(&attrs.method);
37 let output_ty = &attrs.output;
38 let error_ty = attrs
39 .error
40 .as_ref()
41 .map(|e| quote! { #e })
42 .unwrap_or_else(|| quote! { ::jacquard_common::xrpc::GenericError });
43
44 // Generate response marker struct name
45 let response_name = format_ident!("{}Response", name);
46
47 // Build the impls
48 let mut output = quote! {
49 /// Response marker for #name
50 pub struct #response_name;
51
52 impl ::jacquard_common::xrpc::XrpcResp for #response_name {
53 const NSID: &'static str = #nsid;
54 const ENCODING: &'static str = "application/json";
55 type Output<'de> = #output_ty<'de>;
56 type Err<'de> = #error_ty<'de>;
57 }
58
59 impl #generics ::jacquard_common::xrpc::XrpcRequest for #name #lifetime {
60 const NSID: &'static str = #nsid;
61 const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method;
62 type Response = #response_name;
63 }
64 };
65
66 // Optional server-side endpoint impl
67 if attrs.server {
68 let endpoint_name = format_ident!("{}Endpoint", name);
69 let path = format!("/xrpc/{}", nsid);
70
71 // Request type with or without lifetime
72 let request_type = if has_lifetime {
73 quote! { #name<'de> }
74 } else {
75 quote! { #name }
76 };
77
78 output.extend(quote! {
79 /// Endpoint marker for #name (server-side)
80 pub struct #endpoint_name;
81
82 impl ::jacquard_common::xrpc::XrpcEndpoint for #endpoint_name {
83 const PATH: &'static str = #path;
84 const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method;
85 type Request<'de> = #request_type;
86 type Response = #response_name;
87 }
88 });
89 }
90
91 Ok(output)
92}
93
94struct XrpcAttrs {
95 nsid: String,
96 method: XrpcMethod,
97 output: syn::Type,
98 error: Option<syn::Type>,
99 server: bool,
100}
101
102enum XrpcMethod {
103 Query,
104 Procedure,
105}
106
107fn parse_xrpc_attrs(attrs: &[Attribute]) -> syn::Result<XrpcAttrs> {
108 let mut nsid = None;
109 let mut method = None;
110 let mut output = None;
111 let mut error = None;
112 let mut server = false;
113
114 for attr in attrs {
115 if !attr.path().is_ident("xrpc") {
116 continue;
117 }
118
119 attr.parse_nested_meta(|meta| {
120 if meta.path.is_ident("nsid") {
121 let value = meta.value()?;
122 let s: LitStr = value.parse()?;
123 nsid = Some(s.value());
124 Ok(())
125 } else if meta.path.is_ident("method") {
126 // Parse "method = Query" or "method = Procedure"
127 let _eq = meta.input.parse::<syn::Token![=]>()?;
128 let ident: Ident = meta.input.parse()?;
129 match ident.to_string().as_str() {
130 "Query" => {
131 method = Some(XrpcMethod::Query);
132 Ok(())
133 }
134 "Procedure" => {
135 // Always JSON, no custom encoding support
136 method = Some(XrpcMethod::Procedure);
137 Ok(())
138 }
139 other => {
140 Err(meta
141 .error(format!("unknown method: {}, use Query or Procedure", other)))
142 }
143 }
144 } else if meta.path.is_ident("output") {
145 let value = meta.value()?;
146 output = Some(value.parse()?);
147 Ok(())
148 } else if meta.path.is_ident("error") {
149 let value = meta.value()?;
150 error = Some(value.parse()?);
151 Ok(())
152 } else if meta.path.is_ident("server") {
153 server = true;
154 Ok(())
155 } else {
156 Err(meta.error("unknown xrpc attribute"))
157 }
158 })?;
159 }
160
161 let nsid = nsid.ok_or_else(|| {
162 syn::Error::new(
163 proc_macro2::Span::call_site(),
164 "missing required `nsid` attribute",
165 )
166 })?;
167 let method = method.ok_or_else(|| {
168 syn::Error::new(
169 proc_macro2::Span::call_site(),
170 "missing required `method` attribute",
171 )
172 })?;
173 let output = output.ok_or_else(|| {
174 syn::Error::new(
175 proc_macro2::Span::call_site(),
176 "missing required `output` attribute",
177 )
178 })?;
179
180 Ok(XrpcAttrs {
181 nsid,
182 method,
183 output,
184 error,
185 server,
186 })
187}
188
189fn method_expr(method: &XrpcMethod) -> TokenStream {
190 match method {
191 XrpcMethod::Query => quote! { ::jacquard_common::xrpc::XrpcMethod::Query },
192 XrpcMethod::Procedure => {
193 quote! { ::jacquard_common::xrpc::XrpcMethod::Procedure("application/json") }
194 }
195 }
196}