Skip to main content

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 proc_macro2::{Span, TokenStream as TokenStream2};
13use proc_macro_crate::{crate_name, FoundCrate};
14use quote::quote;
15use syn::{parse_macro_input, Data, DeriveInput, Ident};
16
17use attributes::has_union_attribute;
18use enum_gen::generate_enum_deserialize;
19use struct_gen::generate_struct_deserialize;
20use union_gen::generate_union_deserialize;
21
22/// Finds the path to the tryparse crate, checking both direct dependency
23/// and re-exports through other crates (like radkit).
24fn get_tryparse_crate() -> TokenStream2 {
25    // First, try to find tryparse directly
26    if let Ok(found) = crate_name("tryparse") {
27        return match found {
28            // FoundCrate::Itself means we're compiling within the tryparse crate itself.
29            // However, doc tests can't use `crate::` paths - they need fully qualified paths.
30            // So we use `::tryparse` which works for both regular builds and doc tests.
31            FoundCrate::Itself => quote!(::tryparse),
32            FoundCrate::Name(name) => {
33                let ident = Ident::new(&name, Span::call_site());
34                quote!(::#ident)
35            }
36        };
37    }
38
39    // If not found directly, look for radkit which re-exports tryparse
40    if let Ok(found) = crate_name("radkit") {
41        match found {
42            FoundCrate::Itself => return quote!(crate::__private_tryparse),
43            FoundCrate::Name(name) => {
44                let ident = Ident::new(&name, Span::call_site());
45                return quote!(::#ident::__private_tryparse);
46            }
47        }
48    }
49
50    // Fallback to direct path (will fail at compile time with helpful error)
51    quote!(::tryparse)
52}
53
54/// Derives the `LlmDeserialize` trait for structs and enums.
55///
56/// This macro generates a custom deserialization implementation using BAML's
57/// algorithms for fuzzy field matching and type coercion.
58///
59/// # Features
60///
61/// - **Fuzzy field matching**: Handles different naming conventions (userName ↔ user_name)
62/// - **Fuzzy enum matching**: Case-insensitive, substring, and edit-distance matching for variants
63/// - **Union types**: Score-based variant selection with `#[llm(union)]`
64/// - **Optional fields**: Automatic handling of `Option<T>` fields
65/// - **Transformation tracking**: Records all coercions applied during parsing
66///
67/// # Example
68///
69/// ```ignore
70/// use tryparse::deserializer::LlmDeserialize;
71///
72/// #[derive(LlmDeserialize)]
73/// struct User {
74///     name: String,
75///     age: u32,
76///     email: Option<String>, // Optional field
77/// }
78///
79/// // Handles messy input like:
80/// // {"userName": "Alice", "age": "30"}  // camelCase + string number
81/// ```
82///
83/// # Union Types
84///
85/// ```ignore
86/// #[derive(LlmDeserialize)]
87/// #[llm(union)]
88/// enum Value {
89///     Number(i64),
90///     Text(String),
91/// }
92///
93/// // Automatically picks the best matching variant
94/// ```
95#[proc_macro_derive(LlmDeserialize, attributes(llm))]
96pub fn derive_llm_deserialize(input: TokenStream) -> TokenStream {
97    let input = parse_macro_input!(input as DeriveInput);
98    let tryparse_crate = get_tryparse_crate();
99
100    let name = &input.ident;
101    let generics = &input.generics;
102    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
103
104    match &input.data {
105        Data::Struct(data_struct) => {
106            let deserialize_impl = generate_struct_deserialize(name, data_struct, &tryparse_crate);
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_crate::deserializer::LlmDeserialize for #name #ty_generics #where_clause {
126                    #deserialize_impl
127                }
128            };
129
130            TokenStream::from(expanded)
131        }
132        Data::Enum(data_enum) => {
133            // Check if this is a union enum (has #[llm(union)] attribute)
134            let is_union = has_union_attribute(&input.attrs);
135
136            let deserialize_impl = if is_union {
137                generate_union_deserialize(name, data_enum, &input.attrs, &tryparse_crate)
138            } else {
139                generate_enum_deserialize(name, data_enum, &input.attrs, &tryparse_crate)
140            };
141
142            let name_str = name.to_string();
143
144            let expanded = quote! {
145                // Compile-time check: LlmDeserialize requires serde::Deserialize
146                const _: () = {
147                    fn __assert_deserialize_impl<__T: ::serde::de::DeserializeOwned>() {}
148                    fn __check_deserialize_bound() {
149                        __assert_deserialize_impl::<#name #ty_generics>();
150                    }
151
152                    // Provide a helpful error message
153                    #[doc = concat!(
154                        "LlmDeserialize requires serde::Deserialize. ",
155                        "Add `#[derive(serde::Deserialize)]` to `", #name_str, "`."
156                    )]
157                    const __LLMDESERIALIZE_REQUIRES_SERDE: () = ();
158                };
159
160                impl #impl_generics #tryparse_crate::deserializer::LlmDeserialize for #name #ty_generics #where_clause {
161                    #deserialize_impl
162                }
163            };
164
165            TokenStream::from(expanded)
166        }
167        Data::Union(_) => {
168            syn::Error::new_spanned(input, "LlmDeserialize cannot be derived for unions")
169                .to_compile_error()
170                .into()
171        }
172    }
173}