endurox_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, Data, DeriveInput, Fields};
4
5/// Derive macro for automatic UbfStruct implementation
6///
7/// # Example
8///
9/// ```ignore
10/// #[derive(UbfStruct)]
11/// struct Transaction {
12///     #[ubf(field = 1002)]
13///     name: String,
14///     
15///     #[ubf(field = 1012)]
16///     id: i64,
17///     
18///     #[ubf(field = 1021)]
19///     amount: f64,
20///     
21///     #[ubf(field = 1004, default = "pending")]
22///     status: String,
23/// }
24/// ```
25#[proc_macro_derive(UbfStruct, attributes(ubf))]
26pub fn derive_ubf_struct(input: TokenStream) -> TokenStream {
27    let input = parse_macro_input!(input as DeriveInput);
28
29    let name = &input.ident;
30
31    // Parse struct fields
32    let fields = match &input.data {
33        Data::Struct(data) => match &data.fields {
34            Fields::Named(fields) => &fields.named,
35            _ => panic!("UbfStruct only supports named fields"),
36        },
37        _ => panic!("UbfStruct only supports structs"),
38    };
39
40    // Generate from_ubf implementation
41    let mut from_ubf_fields = Vec::new();
42    let mut to_ubf_fields = Vec::new();
43
44    for field in fields {
45        let field_name = field.ident.as_ref().unwrap();
46        let field_type = &field.ty;
47
48        // Parse #[ubf(field = ...)] attribute
49        let mut field_expr: Option<proc_macro2::TokenStream> = None;
50        let mut default_value: Option<String> = None;
51
52        for attr in &field.attrs {
53            if attr.path().is_ident("ubf") {
54                // Parse the meta list manually from tokens
55                let tokens_str = attr
56                    .meta
57                    .require_list()
58                    .expect("Expected meta list")
59                    .tokens
60                    .to_string();
61
62                // Split by comma and process each part
63                for part in tokens_str.split(',') {
64                    let part = part.trim();
65
66                    if part.starts_with("field") {
67                        // Parse "field = <expr>" where expr can be a constant or literal
68                        if let Some(eq_pos) = part.find('=') {
69                            let value_str = part[eq_pos + 1..].trim();
70                            // Store as token stream to support both literals and constants
71                            field_expr =
72                                Some(value_str.parse().expect("Failed to parse field expression"));
73                        }
74                    } else if part.starts_with("default") {
75                        // Parse "default = "value""
76                        if let Some(eq_pos) = part.find('=') {
77                            let value_str = part[eq_pos + 1..].trim();
78                            default_value = Some(value_str.trim_matches('"').to_string());
79                        }
80                    }
81                }
82            }
83        }
84
85        let fid = field_expr.unwrap_or_else(|| {
86            panic!(
87                "Field {} must have #[ubf(field = ...)] attribute",
88                field_name
89            )
90        });
91
92        // Generate field reading code based on type
93        let field_getter = generate_field_getter(
94            field_name,
95            field_type,
96            fid.clone(),
97            default_value.as_deref(),
98        );
99        from_ubf_fields.push(field_getter);
100
101        // Generate field writing code
102        let field_setter = generate_field_setter(field_name, field_type, fid);
103        to_ubf_fields.push(field_setter);
104    }
105
106    let field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect();
107
108    // Generate the implementation
109    let expanded = quote! {
110        impl ::endurox_sys::ubf_struct::UbfStruct for #name {
111            fn from_ubf(buf: &::endurox_sys::ubf::UbfBuffer) -> Result<Self, ::endurox_sys::ubf_struct::UbfError> {
112                #(#from_ubf_fields)*
113
114                Ok(Self {
115                    #(#field_names),*
116                })
117            }
118
119            fn to_ubf(&self) -> Result<::endurox_sys::ubf::UbfBuffer, ::endurox_sys::ubf_struct::UbfError> {
120                let mut buf = ::endurox_sys::ubf::UbfBuffer::new(2048)
121                    .map_err(|e| ::endurox_sys::ubf_struct::UbfError::AllocationError(e))?;
122                self.update_ubf(&mut buf)?;
123                Ok(buf)
124            }
125
126            fn update_ubf(&self, buf: &mut ::endurox_sys::ubf::UbfBuffer) -> Result<(), ::endurox_sys::ubf_struct::UbfError> {
127                #(#to_ubf_fields)*
128                Ok(())
129            }
130        }
131    };
132
133    TokenStream::from(expanded)
134}
135
136fn generate_field_getter(
137    field_name: &syn::Ident,
138    field_type: &syn::Type,
139    field_id: proc_macro2::TokenStream,
140    default_value: Option<&str>,
141) -> proc_macro2::TokenStream {
142    let type_str = quote!(#field_type).to_string();
143
144    // Check if it's an Option type
145    let is_option = type_str.starts_with("Option <");
146
147    if is_option {
148        // Extract inner type from Option<T>
149        if type_str.contains("String") {
150            // Option<String>
151            quote! {
152                let #field_name = buf.get_string(#field_id, 0).ok();
153            }
154        } else if type_str.contains("i64") || type_str.contains("i32") || type_str.contains("long")
155        {
156            // Option<i64/i32>
157            quote! {
158                let #field_name = buf.get_long(#field_id, 0).ok().map(|v| v as _);
159            }
160        } else if type_str.contains("f64")
161            || type_str.contains("f32")
162            || type_str.contains("double")
163        {
164            // Option<f64/f32>
165            quote! {
166                let #field_name = buf.get_double(#field_id, 0).ok().map(|v| v as _);
167            }
168        } else if type_str.contains("bool") {
169            // Option<bool>
170            quote! {
171                let #field_name = if buf.is_present(#field_id, 0) { Some(true) } else { None };
172            }
173        } else {
174            // Option<NestedStruct> - try to parse, return None if fails
175            // Extract inner type by removing "Option <" and ">"
176            let inner_type_str = type_str
177                .trim_start_matches("Option <")
178                .trim_end_matches(">")
179                .trim();
180            let inner_type: proc_macro2::TokenStream =
181                inner_type_str.parse().expect("Failed to parse inner type");
182
183            quote! {
184                let #field_name = <#inner_type as ::endurox_sys::ubf_struct::UbfStruct>::from_ubf(buf).ok();
185            }
186        }
187    } else {
188        // Non-optional types
189        if type_str.contains("String") {
190            if let Some(default) = default_value {
191                quote! {
192                    let #field_name = buf.get_string(#field_id, 0)
193                        .unwrap_or_else(|_| #default.to_string());
194                }
195            } else {
196                quote! {
197                    let #field_name = buf.get_string(#field_id, 0)
198                        .map_err(|e| ::endurox_sys::ubf_struct::UbfError::FieldNotFound(
199                            format!("Field {} ({}): {}", stringify!(#field_name), #field_id, e)
200                        ))?;
201                }
202            }
203        } else if type_str.contains("i64") || type_str.contains("i32") || type_str.contains("long")
204        {
205            quote! {
206                let #field_name = buf.get_long(#field_id, 0)
207                    .map_err(|e| ::endurox_sys::ubf_struct::UbfError::FieldNotFound(
208                        format!("Field {} ({}): {}", stringify!(#field_name), #field_id, e)
209                    ))? as #field_type;
210            }
211        } else if type_str.contains("f64")
212            || type_str.contains("f32")
213            || type_str.contains("double")
214        {
215            quote! {
216                let #field_name = buf.get_double(#field_id, 0)
217                    .map_err(|e| ::endurox_sys::ubf_struct::UbfError::FieldNotFound(
218                        format!("Field {} ({}): {}", stringify!(#field_name), #field_id, e)
219                    ))? as #field_type;
220            }
221        } else if type_str.contains("bool") {
222            quote! {
223                let #field_name = buf.is_present(#field_id, 0);
224            }
225        } else {
226            // Assume it's a nested struct that implements UbfStruct
227            quote! {
228                let #field_name = <#field_type as ::endurox_sys::ubf_struct::UbfStruct>::from_ubf(buf)?;
229            }
230        }
231    }
232}
233
234fn generate_field_setter(
235    field_name: &syn::Ident,
236    field_type: &syn::Type,
237    field_id: proc_macro2::TokenStream,
238) -> proc_macro2::TokenStream {
239    let type_str = quote!(#field_type).to_string();
240
241    // Check if it's an Option type
242    let is_option = type_str.starts_with("Option <");
243
244    if is_option {
245        // Handle all Option<T> types
246        if type_str.contains("String") {
247            // Option<String>
248            quote! {
249                if let Some(ref value) = self.#field_name {
250                    buf.add_string(#field_id, value)
251                        .map_err(|e| ::endurox_sys::ubf_struct::UbfError::TypeError(
252                            format!("Field {}: {}", stringify!(#field_name), e)
253                        ))?;
254                }
255            }
256        } else if type_str.contains("i64") || type_str.contains("i32") || type_str.contains("long")
257        {
258            // Option<i64/i32>
259            quote! {
260                if let Some(value) = self.#field_name {
261                    buf.add_long(#field_id, value as i64)
262                        .map_err(|e| ::endurox_sys::ubf_struct::UbfError::TypeError(
263                            format!("Field {}: {}", stringify!(#field_name), e)
264                        ))?;
265                }
266            }
267        } else if type_str.contains("f64")
268            || type_str.contains("f32")
269            || type_str.contains("double")
270        {
271            // Option<f64/f32>
272            quote! {
273                if let Some(value) = self.#field_name {
274                    buf.add_double(#field_id, value as f64)
275                        .map_err(|e| ::endurox_sys::ubf_struct::UbfError::TypeError(
276                            format!("Field {}: {}", stringify!(#field_name), e)
277                        ))?;
278                }
279            }
280        } else if type_str.contains("bool") {
281            // Option<bool>
282            quote! {
283                if let Some(value) = self.#field_name {
284                    if value {
285                        buf.add_long(#field_id, 1)
286                            .map_err(|e| ::endurox_sys::ubf_struct::UbfError::TypeError(
287                                format!("Field {}: {}", stringify!(#field_name), e)
288                            ))?;
289                    }
290                }
291            }
292        } else {
293            // Option<NestedStruct>
294            quote! {
295                if let Some(ref nested) = self.#field_name {
296                    nested.update_ubf(buf)?;
297                }
298            }
299        }
300    } else {
301        // Non-optional types
302        if type_str.contains("String") {
303            quote! {
304                buf.add_string(#field_id, &self.#field_name)
305                    .map_err(|e| ::endurox_sys::ubf_struct::UbfError::TypeError(
306                        format!("Field {}: {}", stringify!(#field_name), e)
307                    ))?;
308            }
309        } else if type_str.contains("i64") || type_str.contains("i32") || type_str.contains("long")
310        {
311            quote! {
312                buf.add_long(#field_id, self.#field_name as i64)
313                    .map_err(|e| ::endurox_sys::ubf_struct::UbfError::TypeError(
314                        format!("Field {}: {}", stringify!(#field_name), e)
315                    ))?;
316            }
317        } else if type_str.contains("f64")
318            || type_str.contains("f32")
319            || type_str.contains("double")
320        {
321            quote! {
322                buf.add_double(#field_id, self.#field_name as f64)
323                    .map_err(|e| ::endurox_sys::ubf_struct::UbfError::TypeError(
324                        format!("Field {}: {}", stringify!(#field_name), e)
325                    ))?;
326            }
327        } else if type_str.contains("bool") {
328            quote! {
329                if self.#field_name {
330                    buf.add_long(#field_id, 1)
331                        .map_err(|e| ::endurox_sys::ubf_struct::UbfError::TypeError(
332                            format!("Field {}: {}", stringify!(#field_name), e)
333                        ))?;
334                }
335            }
336        } else {
337            // Assume it's a nested struct that implements UbfStruct
338            quote! {
339                self.#field_name.update_ubf(buf)?;
340            }
341        }
342    }
343}