A better Rust ATProto crate
at main 196 lines 5.9 kB view raw
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}