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}