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}