A better Rust ATProto crate
1use proc_macro::TokenStream; 2use quote::quote; 3use syn::{Data, DeriveInput, Fields, GenericParam, parse_macro_input}; 4 5/// Attribute macro that adds an `extra_data` field to structs to capture unknown fields 6/// during deserialization. 7/// 8/// # Example 9/// ```ignore 10/// #[lexicon] 11/// struct Post<'s> { 12/// text: &'s str, 13/// } 14/// // Expands to: 15/// // struct Post<'s> { 16/// // text: &'s str, 17/// // #[serde(flatten)] 18/// // pub extra_data: BTreeMap<SmolStr, Data<'s>>, 19/// // } 20/// ``` 21#[proc_macro_attribute] 22pub fn lexicon(_attr: TokenStream, item: TokenStream) -> TokenStream { 23 let mut input = parse_macro_input!(item as DeriveInput); 24 25 match &mut input.data { 26 Data::Struct(data_struct) => { 27 if let Fields::Named(fields) = &mut data_struct.fields { 28 // Check if extra_data field already exists 29 let has_extra_data = fields 30 .named 31 .iter() 32 .any(|f| f.ident.as_ref().map(|i| i == "extra_data").unwrap_or(false)); 33 34 if !has_extra_data { 35 // Determine the lifetime parameter to use 36 let lifetime = if let Some(lt) = input.generics.lifetimes().next() { 37 quote! { #lt } 38 } else { 39 quote! { 'static } 40 }; 41 42 // Add the extra_data field with serde(borrow) if there's a lifetime 43 let new_field: syn::Field = if input.generics.lifetimes().next().is_some() { 44 syn::parse_quote! { 45 #[serde(flatten)] 46 #[serde(borrow)] 47 pub extra_data: ::std::collections::BTreeMap< 48 ::jacquard_common::smol_str::SmolStr, 49 ::jacquard_common::types::value::Data<#lifetime> 50 > 51 } 52 } else { 53 // For types without lifetimes, make it optional to avoid lifetime conflicts 54 syn::parse_quote! { 55 #[serde(flatten)] 56 #[serde(skip_serializing_if = "std::option::Option::is_none")] 57 #[serde(default)] 58 pub extra_data: std::option::Option<::std::collections::BTreeMap< 59 ::jacquard_common::smol_str::SmolStr, 60 ::jacquard_common::types::value::Data<'static> 61 >> 62 } 63 }; 64 fields.named.push(new_field); 65 } 66 } else { 67 return syn::Error::new_spanned( 68 input, 69 "lexicon attribute can only be used on structs with named fields", 70 ) 71 .to_compile_error() 72 .into(); 73 } 74 75 quote! { #input }.into() 76 } 77 _ => syn::Error::new_spanned(input, "lexicon attribute can only be used on structs") 78 .to_compile_error() 79 .into(), 80 } 81} 82 83/// Attribute macro that adds an `Other(Data)` variant to enums to make them open unions. 84/// 85/// # Example 86/// ```ignore 87/// #[open_union] 88/// enum RecordEmbed<'s> { 89/// #[serde(rename = "app.bsky.embed.images")] 90/// Images(Images), 91/// } 92/// // Expands to: 93/// // enum RecordEmbed<'s> { 94/// // #[serde(rename = "app.bsky.embed.images")] 95/// // Images(Images), 96/// // #[serde(untagged)] 97/// // Unknown(Data<'s>), 98/// // } 99/// ``` 100#[proc_macro_attribute] 101pub fn open_union(_attr: TokenStream, item: TokenStream) -> TokenStream { 102 let mut input = parse_macro_input!(item as DeriveInput); 103 104 match &mut input.data { 105 Data::Enum(data_enum) => { 106 // Check if Unknown variant already exists 107 let has_other = data_enum.variants.iter().any(|v| v.ident == "Unknown"); 108 109 if !has_other { 110 // Determine the lifetime parameter to use 111 let lifetime = if let Some(lt) = input.generics.lifetimes().next() { 112 quote! { #lt } 113 } else { 114 quote! { 'static } 115 }; 116 117 // Add the Unknown variant 118 let new_variant: syn::Variant = syn::parse_quote! { 119 #[serde(untagged)] 120 Unknown(::jacquard_common::types::value::Data<#lifetime>) 121 }; 122 data_enum.variants.push(new_variant); 123 } 124 125 quote! { #input }.into() 126 } 127 _ => syn::Error::new_spanned(input, "open_union attribute can only be used on enums") 128 .to_compile_error() 129 .into(), 130 } 131} 132 133/// Derive macro for `IntoStatic` trait. 134/// 135/// Automatically implements conversion from borrowed to owned ('static) types. 136/// Works with structs and enums that have lifetime parameters. 137/// 138/// # Example 139/// ```ignore 140/// #[derive(IntoStatic)] 141/// struct Post<'a> { 142/// text: CowStr<'a>, 143/// } 144/// // Generates: 145/// // impl IntoStatic for Post<'_> { 146/// // type Output = Post<'static>; 147/// // fn into_static(self) -> Self::Output { 148/// // Post { text: self.text.into_static() } 149/// // } 150/// // } 151/// ``` 152#[proc_macro_derive(IntoStatic)] 153pub fn derive_into_static(input: TokenStream) -> TokenStream { 154 let input = parse_macro_input!(input as DeriveInput); 155 156 let name = &input.ident; 157 let generics = &input.generics; 158 159 // Build impl generics and where clause 160 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 161 162 // Build the Output type with all lifetimes replaced by 'static 163 let output_generics = generics.params.iter().map(|param| { 164 match param { 165 GenericParam::Lifetime(_) => quote! { 'static }, 166 GenericParam::Type(ty) => { 167 let ident = &ty.ident; 168 quote! { #ident } 169 } 170 GenericParam::Const(c) => { 171 let ident = &c.ident; 172 quote! { #ident } 173 } 174 } 175 }); 176 177 let output_type = if generics.params.is_empty() { 178 quote! { #name } 179 } else { 180 quote! { #name<#(#output_generics),*> } 181 }; 182 183 // Generate the conversion body based on struct/enum 184 let conversion = match &input.data { 185 Data::Struct(data_struct) => { 186 generate_struct_conversion(name, &data_struct.fields) 187 } 188 Data::Enum(data_enum) => { 189 generate_enum_conversion(name, data_enum) 190 } 191 Data::Union(_) => { 192 return syn::Error::new_spanned( 193 input, 194 "IntoStatic cannot be derived for unions" 195 ) 196 .to_compile_error() 197 .into(); 198 } 199 }; 200 201 let expanded = quote! { 202 impl #impl_generics ::jacquard_common::IntoStatic for #name #ty_generics #where_clause { 203 type Output = #output_type; 204 205 fn into_static(self) -> Self::Output { 206 #conversion 207 } 208 } 209 }; 210 211 expanded.into() 212} 213 214fn generate_struct_conversion(name: &syn::Ident, fields: &Fields) -> proc_macro2::TokenStream { 215 match fields { 216 Fields::Named(fields) => { 217 let field_conversions = fields.named.iter().map(|f| { 218 let field_name = &f.ident; 219 quote! { #field_name: self.#field_name.into_static() } 220 }); 221 quote! { 222 #name { 223 #(#field_conversions),* 224 } 225 } 226 } 227 Fields::Unnamed(fields) => { 228 let field_conversions = fields.unnamed.iter().enumerate().map(|(i, _)| { 229 let index = syn::Index::from(i); 230 quote! { self.#index.into_static() } 231 }); 232 quote! { 233 #name(#(#field_conversions),*) 234 } 235 } 236 Fields::Unit => { 237 quote! { #name } 238 } 239 } 240} 241 242fn generate_enum_conversion(name: &syn::Ident, data_enum: &syn::DataEnum) -> proc_macro2::TokenStream { 243 let variants = data_enum.variants.iter().map(|variant| { 244 let variant_name = &variant.ident; 245 match &variant.fields { 246 Fields::Named(fields) => { 247 let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect(); 248 let field_conversions = field_names.iter().map(|field_name| { 249 quote! { #field_name: #field_name.into_static() } 250 }); 251 quote! { 252 #name::#variant_name { #(#field_names),* } => { 253 #name::#variant_name { 254 #(#field_conversions),* 255 } 256 } 257 } 258 } 259 Fields::Unnamed(fields) => { 260 let field_bindings: Vec<_> = (0..fields.unnamed.len()) 261 .map(|i| syn::Ident::new(&format!("field_{}", i), proc_macro2::Span::call_site())) 262 .collect(); 263 let field_conversions = field_bindings.iter().map(|binding| { 264 quote! { #binding.into_static() } 265 }); 266 quote! { 267 #name::#variant_name(#(#field_bindings),*) => { 268 #name::#variant_name(#(#field_conversions),*) 269 } 270 } 271 } 272 Fields::Unit => { 273 quote! { 274 #name::#variant_name => #name::#variant_name 275 } 276 } 277 } 278 }); 279 280 quote! { 281 match self { 282 #(#variants),* 283 } 284 } 285}