montycat_serialization_derive/lib.rs
1// Importing the necessary crates for procedural macros
2extern crate proc_macro;
3use proc_macro::TokenStream; // TokenStream is used for passing input/output to/from the macro
4use quote::quote; // Quote is used to generate Rust code as tokens
5use syn::{parse_macro_input, DeriveInput, Ident, Fields, Data}; // Syn crate for parsing Rust syntax
6
7/// This procedural macro derives a trait called `BinaryConvert` for the struct
8/// it's applied to. The `BinaryConvert` trait provides methods to convert a
9/// struct to and from its byte representation, useful for serializing and
10/// deserializing to and from binary formats.
11///
12/// ## Overview:
13/// The trait defines two methods:
14/// - `convert_to_bytes(&self) -> Vec<u8>`: Converts the struct into a byte vector.
15/// - `convert_from_bytes(bytes: &[u8]) -> Self`: Converts a byte slice back into the struct.
16///
17/// The implementation uses `rmp_serde` (a MessagePack serializer/deserializer) for binary
18/// serialization and deserialization of the struct. The struct must also implement `Serialize`
19/// and `Deserialize` to work with `rmp_serde` correctly.
20#[proc_macro_derive(BinaryConvert)]
21pub fn binary_convert_derive(input: TokenStream) -> TokenStream {
22 // Parse the input tokens into a DeriveInput, which represents the structure
23 // of the item (e.g., struct, enum) to which the macro is being applied.
24 let ast: DeriveInput = parse_macro_input!(input as DeriveInput);
25 // Extract the identifier (name) of the struct or enum from the parsed input.
26 let name: Ident = ast.ident;
27
28 // Generate the implementation of the `BinaryConvert` trait for the struct
29 let gen = quote! {
30 // Define the BinaryConvert trait
31 pub trait BinaryConvert: Serialize + for<'de> serde::Deserialize<'de> + Default + Sized {
32 // Method to convert the struct into a byte vector
33 fn convert_to_bytes(&self) -> Vec<u8>;
34 // Method to convert a byte slice back into the struct
35 fn convert_from_bytes(bytes: &[u8]) -> Self;
36 // Method to convert a byte slice back into the struct
37 fn convert_from_bytes_populate_option(bytes: &[u8]) -> Option<Self>;
38 // Method to convert a byte slice back into the struct
39 fn convert_to_bytes_populate_option(&self) -> Option<Vec<u8>>;
40 }
41
42 // Implement the BinaryConvert trait for the specific struct (identified by `#name`)
43 impl BinaryConvert for #name
44 where
45 #name: Serialize + for<'de> serde::Deserialize<'de> + Default, // The struct must implement `Serialize`, `Deserialize`, and `Default`
46 {
47 // Implementation of the `convert_to_bytes` method
48 fn convert_to_bytes(&self) -> Vec<u8> {
49 // Attempt to serialize the struct into a MessagePack byte vector.
50 if let Ok(vector) = rmp_serde::to_vec(self) {
51 vector // Return the byte vector if serialization was successful
52 } else {
53 Vec::new() // Return an empty vector if serialization failed
54 }
55 }
56
57 // Implementation of the `convert_from_bytes` method
58 fn convert_from_bytes(bytes: &[u8]) -> Self {
59 // Attempt to deserialize the byte slice into the struct.
60 if let Ok(structure) = rmp_serde::from_slice(bytes) {
61 structure // Return the deserialized struct if successful
62 } else {
63 Self::default() // Return the default value of the struct if deserialization failed
64 }
65 }
66
67 fn convert_from_bytes_populate_option(bytes: &[u8]) -> Option<Self> {
68 // Attempt to deserialize the byte slice into the struct.
69 rmp_serde::from_slice(bytes).ok()
70 }
71
72 fn convert_to_bytes_populate_option(&self) -> Option<Vec<u8>> {
73 // Attempt to serialize the struct into a MessagePack byte vector.
74 rmp_serde::to_vec(self).ok()
75 }
76
77 }
78 };
79
80 // Return the generated code as a TokenStream, which will be inserted into the user's code.
81 gen.into()
82}
83
84/// This procedural macro derives a trait called `RuntimeSchema` for the struct
85/// it's applied to. The `RuntimeSchema` trait provides methods to introspect
86/// the struct's fields at runtime, specifically to identify fields of certain
87/// types (like `Pointer` and `Timestamp`), retrieve all field names and types,
88/// and generate schema parameters.
89///
90//// ## Overview:
91/// The trait defines three methods:
92/// - `pointer_and_timestamp_fields(&self) -> Vec<(&'static str, &'static str)>`:
93/// Returns a vector of field names and their types for fields that are either
94/// `Pointer` or `Timestamp`.
95/// - `field_names_and_types(&self) -> Vec<(&'static str, &'static str)>`:
96/// Returns a vector of all field names and their types in the struct.
97/// - `schema_params() -> (HashMap<&'static str, &'static str>, &'static str)`:
98/// Returns a tuple containing a HashMap of field names to their types
99/// and the name of the struct.
100///
101//// The implementation uses Rust's type identification to check field types
102/// and generates the necessary code to fulfill the trait's requirements.
103///
104/// # Note:
105/// The struct must have named fields for this macro to work correctly.
106///
107/// # Dependencies:
108/// This macro assumes the existence of `Pointer` and `Timestamp` types in scope.
109///
110/// # Example Usage:
111///
112/// ```rust
113/// #[derive(RuntimeSchema)]
114/// struct MyStruct {
115/// id: Pointer,
116/// timestamp: Timestamp,
117/// name: String,
118/// }
119/// ```
120///
121/// The above will generate an implementation of `RuntimeSchema` for `MyStruct`.
122///
123/// # Generated Methods:
124/// - `pointer_and_timestamp_fields`: Will return `[("id", "Pointer"), ("timestamp", "Timestamp")]`
125/// - `field_names_and_types`: Will return `[("id", "Pointer"), ("timestamp", "Timestamp"), ("name", "String")]`
126/// - `schema_params`: Will return a HashMap with all field names and types, along with the struct name "MyStruct".
127///
128/// ```rust
129/// let (schema_map, struct_name) = MyStruct::schema_params();
130/// ```
131///
132/// The above will give you a HashMap of field names to types and the struct name.
133///
134#[proc_macro_derive(RuntimeSchema)]
135pub fn montycat_schema_derive(input: TokenStream) -> TokenStream {
136 let ast: DeriveInput = parse_macro_input!(input as DeriveInput);
137 let name = &ast.ident;
138
139 let mut matches = Vec::new();
140 let mut field_info = Vec::new();
141 let mut schema_inserts = Vec::new();
142
143 if let Data::Struct(data_struct) = &ast.data {
144 if let Fields::Named(fields_named) = &data_struct.fields {
145 for field in fields_named.named.iter() {
146 let ident = &field.ident;
147 let ty = &field.ty;
148
149 matches.push(quote! {
150 if std::any::TypeId::of::<#ty>() == std::any::TypeId::of::<Pointer>() {
151 result.push((stringify!(#ident), "Pointer"));
152 }
153 if std::any::TypeId::of::<#ty>() == std::any::TypeId::of::<Timestamp>() {
154 result.push((stringify!(#ident), "Timestamp"));
155 }
156 });
157
158 field_info.push(quote! {
159 (stringify!(#ident), stringify!(#ty))
160 });
161
162 schema_inserts.push(quote! {
163 map.insert(stringify!(#ident), stringify!(#ty));
164 });
165
166 }
167 }
168 }
169
170 let gen = quote! {
171 impl RuntimeSchema for #name {
172 fn pointer_and_timestamp_fields(&self) -> Vec<(&'static str, &'static str)> {
173 let mut result = Vec::new();
174 #(#matches)*
175 result
176 }
177
178 fn field_names_and_types(&self) -> Vec<(&'static str, &'static str)> {
179 vec![#(#field_info),*]
180 }
181
182 fn schema_params() -> (HashMap<&'static str, &'static str>, &'static str) {
183 let mut map = std::collections::HashMap::new();
184 #(#schema_inserts)*
185 (map, stringify!(#name))
186 }
187
188 }
189 };
190
191 gen.into()
192}