Skip to main content

irpc_schema_derive/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{parse_macro_input, Data, DeriveInput, Fields, ItemEnum, Lit, Meta};
6
7// The attribute macro for schema generation
8#[proc_macro_attribute]
9pub fn schema(attr: TokenStream, item: TokenStream) -> TokenStream {
10    let input = parse_macro_input!(item as DeriveInput);
11    let name = &input.ident;
12
13    // Parse the attribute to extract schema type and optional name
14    let attr_meta = parse_macro_input!(attr as Meta);
15    let (schema_type, explicit_name) = match attr_meta {
16        Meta::Path(path) => {
17            let schema_type = path.get_ident().unwrap().to_string();
18            (schema_type, None)
19        }
20        Meta::NameValue(name_value) => {
21            let schema_type = name_value.path.get_ident().unwrap().to_string();
22            let explicit_name = match name_value.lit {
23                Lit::Str(lit_str) => lit_str.value(),
24                _ => panic!("Expected string literal for schema name"),
25            };
26            (schema_type, Some(explicit_name))
27        }
28        _ => panic!("Unsupported attribute format"),
29    };
30
31    let schema_impl = match schema_type.as_str() {
32        "Atom" => generate_atom_schema(&name, explicit_name.as_ref().map(|s| s.as_str())),
33        "Structural" => generate_structural_schema(&input.data),
34        "Nominal" => generate_nominal_schema(&name, &input.data, explicit_name.as_ref().map(|s| s.as_str())),
35        _ => panic!("Unsupported schema type"),
36    };
37
38    let expanded = quote! {
39        #input
40
41        impl ::irpc_schema::HasSchema for #name {
42            fn schema() -> ::irpc_schema::Schema {
43                #schema_impl
44            }
45        }
46    };
47
48    TokenStream::from(expanded)
49}
50
51// Generates an Atom schema (just the type name)
52fn generate_atom_schema(name: &syn::Ident, explicit_name: Option<&str>) -> proc_macro2::TokenStream {
53    let type_name = match explicit_name {
54        Some(name) => name.to_string(),
55        None => name.to_string(),
56    };
57    quote! {
58        ::irpc_schema::Schema::Atom(#type_name.to_string())
59    }
60}
61
62// Generates a Structural schema (tuples or unnamed structs)
63fn generate_structural_schema(data: &syn::Data) -> proc_macro2::TokenStream {
64    match data {
65        Data::Struct(data_struct) => match &data_struct.fields {
66            Fields::Named(fields) => {
67                let types: Vec<proc_macro2::TokenStream> = fields
68                    .named
69                    .iter()
70                    .map(|f| {
71                        let ty = &f.ty;
72                        quote! {
73                            <#ty as ::irpc_schema::HasSchema>::schema()
74                        }
75                    })
76                    .collect();
77                if types.is_empty() {
78                    quote! {
79                        ::irpc_schema::Schema::Unit
80                    }
81                } else {
82                    quote! {
83                        ::irpc_schema::Schema::Product(vec![#(#types),*])
84                    }
85                }
86            }
87            Fields::Unnamed(fields) => {
88                let types: Vec<proc_macro2::TokenStream> = fields
89                    .unnamed
90                    .iter()
91                    .map(|f| {
92                        let ty = &f.ty;
93                        quote! {
94                            <#ty as ::irpc_schema::HasSchema>::schema()
95                        }
96                    })
97                    .collect();
98                if types.is_empty() {
99                    quote! {
100                        ::irpc_schema::Schema::Unit
101                    }
102                } else {
103                    quote! {
104                        ::irpc_schema::Schema::Product(vec![#(#types),*])
105                    }
106                }
107            }
108            Fields::Unit => quote! {
109                ::irpc_schema::Schema::Unit
110            },
111        },
112        Data::Enum(data_enum) => {
113            let variant_schemas: Vec<proc_macro2::TokenStream> = data_enum
114                .variants
115                .iter()
116                .map(|v| {
117                    let variant_fields = match &v.fields {
118                        Fields::Named(fields) => fields
119                            .named
120                            .iter()
121                            .map(|f| {
122                                let ty = &f.ty;
123                                quote! {
124                                    <#ty as ::irpc_schema::HasSchema>::schema()
125                                }
126                            })
127                            .collect(),
128                        Fields::Unnamed(fields) => fields
129                            .unnamed
130                            .iter()
131                            .map(|f| {
132                                let ty = &f.ty;
133                                quote! {
134                                    <#ty as ::irpc_schema::HasSchema>::schema()
135                                }
136                            })
137                            .collect(),
138                        Fields::Unit => vec![],
139                    };
140                    if variant_fields.is_empty() {
141                        quote! {
142                            ::irpc_schema::Schema::Unit
143                        }
144                    } else {
145                        quote! {
146                            ::irpc_schema::Schema::Product(vec![#(#variant_fields),*])
147                        }
148                    }
149                })
150                .collect();
151            if variant_schemas.is_empty() {
152                return quote! {
153                    ::irpc_schema::Schema::Bottom
154                };
155            }
156            quote! {
157                ::irpc_schema::Schema::Sum(vec![#(#variant_schemas),*])
158            }
159        }
160        _ => panic!("Unsupported type for Structural schema"),
161    }
162}
163
164// Generates a Nominal schema (Struct or Enum with names)
165fn generate_nominal_schema(name: &syn::Ident, data: &syn::Data, explicit_name: Option<&str>) -> proc_macro2::TokenStream {
166    let name_text = explicit_name.unwrap_or(&name.to_string()).to_string();
167    match data {
168        Data::Struct(data_struct) => match &data_struct.fields {
169            Fields::Named(fields) => {
170                let field_schemas: Vec<proc_macro2::TokenStream> = fields
171                    .named
172                    .iter()
173                    .map(|f| {
174                        let field_name = f.ident.as_ref().unwrap().to_string();
175                        let field_type = &f.ty;
176                        quote! {
177                            ::irpc_schema::Named(#field_name.to_string(), <#field_type as ::irpc_schema::HasSchema>::schema())
178                        }
179                    })
180                    .collect();
181                let schema = if field_schemas.is_empty() {
182                    quote! { ::irpc_schema::Schema::Unit }
183                } else {
184                    quote! { ::irpc_schema::Schema::Struct(vec![#(#field_schemas),*]) }
185                };
186                quote! {
187                    ::irpc_schema::Schema::Named(
188                        Box::new(::irpc_schema::Named(#name_text.to_string(), #schema))
189                    )
190                }
191            }
192            Fields::Unnamed(fields) => {
193                let field_schemas: Vec<proc_macro2::TokenStream> = fields
194                    .unnamed
195                    .iter()
196                    .enumerate()
197                    .map(|(_i, f)| {
198                        let field_type = &f.ty;
199                        quote! {
200                            <#field_type as ::irpc_schema::HasSchema>::schema()
201                        }
202                    })
203                    .collect();
204                let schema = if field_schemas.is_empty() {
205                    quote! { ::irpc_schema::Schema::Unit }
206                } else {
207                    quote! { ::irpc_schema::Schema::Product(vec![#(#field_schemas),*]) }
208                };
209                quote! {
210                    ::irpc_schema::Schema::Named(
211                        Box::new(::irpc_schema::Named(#name_text.to_string(), #schema))
212                    )
213                }
214            }
215            Fields::Unit => quote! {
216                ::irpc_schema::Schema::Named(
217                    Box::new(::irpc_schema::Named(#name_text.to_string(), ::irpc_schema::Schema::Unit))
218                )
219            },
220        },
221        Data::Enum(data_enum) => {
222            let variants: Vec<proc_macro2::TokenStream> = data_enum
223                .variants
224                .iter()
225                .map(|v| {
226                    let variant_name = &v.ident;
227                    let variant_name_text = variant_name.to_string();
228                    match &v.fields {
229                        Fields::Named(fields) => {
230                            let named = fields
231                                .named
232                                .iter()
233                                .map(|f| {
234                                    let field_type = &f.ty;
235                                    let field_name = f.ident.as_ref().unwrap().to_string();
236                                    quote! {
237                                        ::irpc_schema::Named(#field_name.to_string(),<#field_type as ::irpc_schema::HasSchema>::schema())
238                                    }
239                                })
240                                .collect::<Vec<_>>();
241                            let schema_type = if named.is_empty() {
242                                quote! { ::irpc_schema::Schema::Unit }
243                            } else if named.len() == 1 {
244                                quote! { ::irpc_schema::Schema::Struct(vec![#(#named),*]) }
245                            } else {
246                                quote! { ::irpc_schema::Schema::Enum(vec![#(#named),*]) }
247                            };
248                            quote! {
249                                ::irpc_schema::Named(
250                                    #variant_name_text.to_string(),
251                                    #schema_type
252                                )
253                            }
254                        }
255                        Fields::Unnamed(fields) => {
256                            let unnamed = fields
257                                .unnamed
258                                .iter()
259                                .map(|f| {
260                                    let field_type = &f.ty;
261                                    quote! {
262                                        <#field_type as ::irpc_schema::HasSchema>::schema()
263                                    }
264                                })
265                                .collect::<Vec<_>>();
266                            let schema_type = if unnamed.is_empty() {
267                                quote! { ::irpc_schema::Schema::Unit }
268                            } else if unnamed.len() == 1 {
269                                quote! { ::irpc_schema::Schema::Product(vec![#(#unnamed),*]) }
270                            } else {
271                                quote! { ::irpc_schema::Schema::Sum(vec![#(#unnamed),*]) }
272                            };
273                            quote! {
274                                ::irpc_schema::Named(
275                                    #variant_name_text.to_string(),
276                                    #schema_type
277                                )
278                            }
279                        }
280                        Fields::Unit => {
281                            quote! {
282                                ::irpc_schema::Named(
283                                    #variant_name_text.to_string(),
284                                    ::irpc_schema::Schema::Unit
285                                )
286                            }
287                        }
288                    }
289                })
290                .collect::<Vec<_>>();
291
292            let schema = if variants.is_empty() {
293                quote! { ::irpc_schema::Schema::Bottom }
294            } else if variants.len() == 1 {
295                quote! { ::irpc_schema::Schema::Struct(vec![#(#variants),*]) }
296            } else {
297                quote! { ::irpc_schema::Schema::Enum(vec![#(#variants),*]) }
298            };
299            quote! {
300                ::irpc_schema::Schema::Named(
301                    Box::new(::irpc_schema::Named(#name_text.to_string(), #schema))
302                )
303            }
304        }
305        _ => panic!("Unsupported type for Nominal schema"),
306    }
307}
308
309/// Implements stable serialization and deserialization for an enum with
310/// a number of distinct variants.
311///
312/// Each variant must have a single unnamed field of distinct type. Each type
313/// must implement `HasSchema`.
314#[proc_macro_attribute]
315pub fn serialize_stable(_attr: TokenStream, item: TokenStream) -> TokenStream {
316    // Parse the input tokens into a syntax tree
317    let input = parse_macro_input!(item as ItemEnum);
318
319    // Get the original enum
320    let original_enum = input.clone();
321
322    // Get the name of the enum
323    let enum_name = &input.ident;
324
325    // Generate names for our hash struct
326    let hashes_struct_name =
327        syn::Ident::new(&format!("{}SchemaHashes", enum_name), enum_name.span());
328    let static_name = syn::Ident::new(&format!("__{}_SCHEMA_HASHES", enum_name), enum_name.span());
329
330    // Collect all variants
331    let variants = &input.variants;
332
333    // Make sure all variants have a single unnamed field
334    for variant in variants {
335        match &variant.fields {
336            Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
337                // This is good - a single unnamed field
338            }
339            _ => panic!("HashDiscriminator only supports variants with a single unnamed field"),
340        }
341    }
342
343    // Collect all variant names and their field types
344    let mut variant_names = Vec::new();
345    let mut field_types = Vec::new();
346
347    for variant in variants {
348        let variant_name = &variant.ident;
349        variant_names.push(variant_name);
350
351        let field_type = match &variant.fields {
352            Fields::Unnamed(fields) => &fields.unnamed.first().unwrap().ty,
353            _ => unreachable!(), // We've already checked this above
354        };
355
356        field_types.push(field_type);
357    }
358
359    // Define fields for our SchemaHashes struct
360    let hash_fields = variant_names.iter().map(|variant_name| {
361        quote! { pub #variant_name: [u8; 32] }
362    });
363
364    // Generate initialization for our SchemaHashes struct
365    let hash_inits =
366        variant_names
367            .iter()
368            .zip(field_types.iter())
369            .map(|(variant_name, field_type)| {
370                quote! {
371                    #variant_name: *<#field_type as ::irpc_schema::HasSchema>::schema().stable_hash().as_bytes()
372                }
373            });
374
375    // Generate serialization arms using the static hashes
376    let serialize_arms = variant_names.iter().map(|variant_name| {
377        quote! {
378            #enum_name::#variant_name(payload) => {
379                let hash = hashes.#variant_name;
380
381                let mut tup = serializer.serialize_tuple(2)?;
382                tup.serialize_element(&hash)?;
383                tup.serialize_element(payload)?;
384                tup.end()
385            }
386        }
387    });
388
389    // Generate deserialization branches using the static hashes
390    let deserialize_branches =
391        variant_names
392            .iter()
393            .zip(field_types.iter())
394            .map(|(variant_name, field_type)| {
395                quote! {
396                    if hash_bytes == hashes.#variant_name {
397                        let payload = seq.next_element::<#field_type>()?.ok_or_else(||
398                            serde::de::Error::custom("missing payload"))?;
399                        return Ok(#enum_name::#variant_name(payload));
400                    }
401                }
402            });
403
404    // Generate the implementation
405    let generated_impls = quote! {
406        // The original enum definition
407        #original_enum
408
409        // Define a struct to hold the schema hashes
410        struct #hashes_struct_name {
411            #(#hash_fields),*
412        }
413
414        // Create a static instance of our hashes using std::sync::OnceLock
415        use std::sync::OnceLock;
416        static #static_name: OnceLock<#hashes_struct_name> = OnceLock::new();
417
418        impl #hashes_struct_name {
419            // Create a new instance with all the hashes computed
420            fn new() -> Self {
421                Self {
422                    #(#hash_inits),*
423                }
424            }
425
426            // Static accessor function to get or initialize the global instance
427            fn get() -> &'static Self {
428                #static_name.get_or_init(|| Self::new())
429            }
430        }
431
432        // Implementation of serde::Serialize for the enum
433        impl serde::Serialize for #enum_name {
434            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
435            where
436                S: serde::Serializer,
437            {
438                use serde::ser::SerializeTuple;
439                let hashes = #hashes_struct_name::get();
440
441                match self {
442                    #(#serialize_arms),*
443                }
444            }
445        }
446
447        // Implementation of serde::Deserialize for the enum with visitor inside
448        impl<'de> serde::Deserialize<'de> for #enum_name {
449            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
450            where
451                D: serde::Deserializer<'de>,
452            {
453                // Define the visitor struct inside the deserialize implementation
454                struct Visitor;
455
456                impl<'de> serde::de::Visitor<'de> for Visitor {
457                    type Value = #enum_name;
458
459                    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
460                        formatter.write_str("a tuple with a hash discriminator and payload")
461                    }
462
463                    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
464                    where
465                        A: serde::de::SeqAccess<'de>,
466                    {
467                        // Deserialize the hash discriminator (first element)
468                        let hash_bytes = seq.next_element::<[u8; 32]>()?.ok_or_else(||
469                            serde::de::Error::custom("missing hash"))?;
470
471                        // Get the schema hashes
472                        let hashes = #hashes_struct_name::get();
473
474                        // Check against our static hashes
475                        #(#deserialize_branches)*
476
477                        // If none matched, return an error
478                        Err(serde::de::Error::custom("unknown discriminator"))
479                    }
480                }
481
482                // Use the locally-defined visitor
483                deserializer.deserialize_tuple(2, Visitor)
484            }
485        }
486    };
487
488    // Return the generated code
489    TokenStream::from(generated_impls)
490}