tryparse_derive/
lib.rs

1//! Derive macros for tryparse
2//!
3//! This crate provides the `LlmDeserialize` derive macro for automatically
4//! generating fuzzy deserialization logic from Rust types.
5
6mod attributes;
7mod enum_gen;
8mod struct_gen;
9mod union_gen;
10
11use proc_macro::TokenStream;
12use quote::quote;
13use syn::{parse_macro_input, Data, DeriveInput};
14
15use attributes::has_union_attribute;
16use enum_gen::generate_enum_deserialize;
17use struct_gen::generate_struct_deserialize;
18use union_gen::generate_union_deserialize;
19
20/// Derives the `LlmDeserialize` trait for structs and enums.
21///
22/// This macro generates a custom deserialization implementation using BAML's
23/// algorithms for fuzzy field matching and type coercion.
24///
25/// # Features
26///
27/// - **Fuzzy field matching**: Handles different naming conventions (userName ↔ user_name)
28/// - **Fuzzy enum matching**: Case-insensitive, substring, and edit-distance matching for variants
29/// - **Union types**: Score-based variant selection with `#[llm(union)]`
30/// - **Optional fields**: Automatic handling of `Option<T>` fields
31/// - **Transformation tracking**: Records all coercions applied during parsing
32///
33/// # Example
34///
35/// ```ignore
36/// use tryparse::deserializer::LlmDeserialize;
37///
38/// #[derive(LlmDeserialize)]
39/// struct User {
40///     name: String,
41///     age: u32,
42///     email: Option<String>, // Optional field
43/// }
44///
45/// // Handles messy input like:
46/// // {"userName": "Alice", "age": "30"}  // camelCase + string number
47/// ```
48///
49/// # Union Types
50///
51/// ```ignore
52/// #[derive(LlmDeserialize)]
53/// #[llm(union)]
54/// enum Value {
55///     Number(i64),
56///     Text(String),
57/// }
58///
59/// // Automatically picks the best matching variant
60/// ```
61#[proc_macro_derive(LlmDeserialize, attributes(llm))]
62pub fn derive_llm_deserialize(input: TokenStream) -> TokenStream {
63    let input = parse_macro_input!(input as DeriveInput);
64
65    let name = &input.ident;
66    let generics = &input.generics;
67    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
68
69    match &input.data {
70        Data::Struct(data_struct) => {
71            let deserialize_impl = generate_struct_deserialize(name, data_struct);
72            let name_str = name.to_string();
73
74            let expanded = quote! {
75                // Compile-time check: LlmDeserialize requires serde::Deserialize
76                const _: () = {
77                    fn __assert_deserialize_impl<__T: ::serde::de::DeserializeOwned>() {}
78                    fn __check_deserialize_bound() {
79                        __assert_deserialize_impl::<#name #ty_generics>();
80                    }
81
82                    // Provide a helpful error message
83                    #[doc = concat!(
84                        "LlmDeserialize requires serde::Deserialize. ",
85                        "Add `#[derive(serde::Deserialize)]` to `", #name_str, "`."
86                    )]
87                    const __LLMDESERIALIZE_REQUIRES_SERDE: () = ();
88                };
89
90                impl #impl_generics ::tryparse::deserializer::LlmDeserialize for #name #ty_generics #where_clause {
91                    #deserialize_impl
92                }
93            };
94
95            TokenStream::from(expanded)
96        }
97        Data::Enum(data_enum) => {
98            // Check if this is a union enum (has #[llm(union)] attribute)
99            let is_union = has_union_attribute(&input.attrs);
100
101            let deserialize_impl = if is_union {
102                generate_union_deserialize(name, data_enum, &input.attrs)
103            } else {
104                generate_enum_deserialize(name, data_enum, &input.attrs)
105            };
106
107            let name_str = name.to_string();
108
109            let expanded = quote! {
110                // Compile-time check: LlmDeserialize requires serde::Deserialize
111                const _: () = {
112                    fn __assert_deserialize_impl<__T: ::serde::de::DeserializeOwned>() {}
113                    fn __check_deserialize_bound() {
114                        __assert_deserialize_impl::<#name #ty_generics>();
115                    }
116
117                    // Provide a helpful error message
118                    #[doc = concat!(
119                        "LlmDeserialize requires serde::Deserialize. ",
120                        "Add `#[derive(serde::Deserialize)]` to `", #name_str, "`."
121                    )]
122                    const __LLMDESERIALIZE_REQUIRES_SERDE: () = ();
123                };
124
125                impl #impl_generics ::tryparse::deserializer::LlmDeserialize for #name #ty_generics #where_clause {
126                    #deserialize_impl
127                }
128            };
129
130            TokenStream::from(expanded)
131        }
132        Data::Union(_) => {
133            syn::Error::new_spanned(input, "LlmDeserialize cannot be derived for unions")
134                .to_compile_error()
135                .into()
136        }
137    }
138}