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}