Skip to main content

crous_derive/
lib.rs

1//! # crous-derive
2//!
3//! Proc-macro crate providing `#[derive(Crous)]` and `#[derive(CrousSchema)]`
4//! for automatic serialization with stable field IDs.
5//!
6//! ## Usage
7//!
8//! ```rust,ignore
9//! use crous_derive::{Crous, CrousSchema};
10//! use crous_core::Value;
11//!
12//! #[derive(Debug, PartialEq, Crous, CrousSchema)]
13//! struct Person {
14//!     #[crous(id = 1)] name: String,
15//!     #[crous(id = 2)] age: u8,
16//!     #[crous(id = 3)] tags: Vec<String>,
17//! }
18//! ```
19
20use proc_macro::TokenStream;
21use quote::quote;
22use syn::{Data, DeriveInput, Fields, Lit, parse_macro_input};
23
24/// Derive the `Crous` trait for a struct, generating encode/decode
25/// implementations with stable field IDs.
26///
27/// Each field must be annotated with `#[crous(id = N)]` where N is a
28/// unique, stable integer identifier for schema evolution.
29#[proc_macro_derive(Crous, attributes(crous))]
30pub fn derive_crous(input: TokenStream) -> TokenStream {
31    let input = parse_macro_input!(input as DeriveInput);
32    let name = &input.ident;
33    let name_str = name.to_string();
34
35    let fields = match &input.data {
36        Data::Struct(data) => match &data.fields {
37            Fields::Named(fields) => &fields.named,
38            _ => panic!("Crous derive only supports structs with named fields"),
39        },
40        _ => panic!("Crous derive only supports structs"),
41    };
42
43    // Extract field names and their crous(id = N) attributes.
44    let mut field_infos = Vec::new();
45    for field in fields {
46        let field_name = field.ident.as_ref().unwrap();
47        let field_name_str = field_name.to_string();
48        let mut field_id: Option<u64> = None;
49
50        for attr in &field.attrs {
51            if attr.path().is_ident("crous") {
52                attr.parse_nested_meta(|meta| {
53                    if meta.path.is_ident("id") {
54                        let value = meta.value()?;
55                        let lit: Lit = value.parse()?;
56                        if let Lit::Int(lit_int) = lit {
57                            field_id = Some(lit_int.base10_parse().unwrap());
58                        }
59                    }
60                    Ok(())
61                })
62                .ok();
63            }
64        }
65
66        let id = field_id.unwrap_or_else(|| {
67            // If no explicit id, use hash of field name as fallback.
68            // This is not recommended but provides a default.
69            let hash = xxhash_rust::xxh64::xxh64(field_name_str.as_bytes(), 0);
70            hash & 0xFFFF // Use lower 16 bits.
71        });
72
73        field_infos.push((field_name.clone(), field_name_str, id));
74    }
75
76    // Generate schema fingerprint from type name + field IDs.
77    let schema_str = format!(
78        "{}:{}",
79        name_str,
80        field_infos
81            .iter()
82            .map(|(_, n, id)| format!("{n}={id}"))
83            .collect::<Vec<_>>()
84            .join(",")
85    );
86    let fingerprint = xxhash_rust::xxh64::xxh64(schema_str.as_bytes(), 0);
87
88    // Generate to_crous_value: creates an Object with field name keys.
89    let encode_fields: Vec<_> = field_infos
90        .iter()
91        .map(|(fname, fname_str, _id)| {
92            quote! {
93                (
94                    #fname_str.to_string(),
95                    crous_core::Crous::to_crous_value(&self.#fname)
96                )
97            }
98        })
99        .collect();
100
101    // Generate from_crous_value: matches field names from Object entries.
102    let decode_fields_init: Vec<_> = field_infos
103        .iter()
104        .map(|(fname, _fname_str, _id)| {
105            quote! {
106                let mut #fname = None;
107            }
108        })
109        .collect();
110
111    let decode_fields_match: Vec<_> = field_infos
112        .iter()
113        .map(|(fname, fname_str, _id)| {
114            quote! {
115                #fname_str => {
116                    #fname = Some(crous_core::Crous::from_crous_value(v)?);
117                }
118            }
119        })
120        .collect();
121
122    let decode_fields_unwrap: Vec<_> = field_infos
123        .iter()
124        .map(|(fname, fname_str, _id)| {
125            quote! {
126                #fname: #fname.ok_or_else(|| crous_core::CrousError::SchemaMismatch(
127                    format!("missing field '{}' in {}", #fname_str, #name_str)
128                ))?
129            }
130        })
131        .collect();
132
133    let expanded = quote! {
134        impl crous_core::Crous for #name {
135            fn to_crous_value(&self) -> crous_core::Value {
136                crous_core::Value::Object(vec![
137                    #(#encode_fields),*
138                ])
139            }
140
141            fn from_crous_value(value: &crous_core::Value) -> crous_core::Result<Self> {
142                match value {
143                    crous_core::Value::Object(entries) => {
144                        #(#decode_fields_init)*
145
146                        for (k, v) in entries {
147                            match k.as_str() {
148                                #(#decode_fields_match)*
149                                _ => {} // Skip unknown fields for forward compatibility.
150                            }
151                        }
152
153                        Ok(Self {
154                            #(#decode_fields_unwrap),*
155                        })
156                    }
157                    _ => Err(crous_core::CrousError::SchemaMismatch(
158                        format!("expected object for {}", #name_str)
159                    )),
160                }
161            }
162
163            fn schema_fingerprint() -> u64 {
164                #fingerprint
165            }
166
167            fn type_name() -> &'static str {
168                #name_str
169            }
170        }
171    };
172
173    TokenStream::from(expanded)
174}
175
176/// Derive `CrousSchema` — generates a `schema_info()` method that returns
177/// metadata about the struct's field IDs and types.
178///
179/// This is a companion to `#[derive(Crous)]` and provides introspection.
180#[proc_macro_derive(CrousSchema, attributes(crous))]
181pub fn derive_crous_schema(input: TokenStream) -> TokenStream {
182    let input = parse_macro_input!(input as DeriveInput);
183    let name = &input.ident;
184    let name_str = name.to_string();
185
186    let fields = match &input.data {
187        Data::Struct(data) => match &data.fields {
188            Fields::Named(fields) => &fields.named,
189            _ => panic!("CrousSchema only supports structs with named fields"),
190        },
191        _ => panic!("CrousSchema only supports structs"),
192    };
193
194    let mut field_entries = Vec::new();
195    for field in fields {
196        let fname = field.ident.as_ref().unwrap().to_string();
197        let mut fid: u64 = 0;
198
199        for attr in &field.attrs {
200            if attr.path().is_ident("crous") {
201                attr.parse_nested_meta(|meta| {
202                    if meta.path.is_ident("id") {
203                        let value = meta.value()?;
204                        let lit: Lit = value.parse()?;
205                        if let Lit::Int(lit_int) = lit {
206                            fid = lit_int.base10_parse().unwrap();
207                        }
208                    }
209                    Ok(())
210                })
211                .ok();
212            }
213        }
214
215        field_entries.push(quote! {
216            (#fname, #fid)
217        });
218    }
219
220    let expanded = quote! {
221        impl #name {
222            /// Returns schema metadata: pairs of (field_name, field_id).
223            pub fn schema_info() -> &'static [(&'static str, u64)] {
224                &[ #(#field_entries),* ]
225            }
226
227            /// Returns the type name.
228            pub fn schema_type_name() -> &'static str {
229                #name_str
230            }
231        }
232    };
233
234    TokenStream::from(expanded)
235}