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}