A better Rust ATProto crate
at oauth 4.8 kB view raw
1use proc_macro::TokenStream; 2use quote::quote; 3use syn::{Data, DeriveInput, Fields, 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}