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
73 let expanded = quote! {
74 impl #impl_generics ::tryparse::deserializer::LlmDeserialize for #name #ty_generics #where_clause {
75 #deserialize_impl
76 }
77 };
78
79 TokenStream::from(expanded)
80 }
81 Data::Enum(data_enum) => {
82 // Check if this is a union enum (has #[llm(union)] attribute)
83 let is_union = has_union_attribute(&input.attrs);
84
85 let deserialize_impl = if is_union {
86 generate_union_deserialize(name, data_enum, &input.attrs)
87 } else {
88 generate_enum_deserialize(name, data_enum, &input.attrs)
89 };
90
91 let expanded = quote! {
92 impl #impl_generics ::tryparse::deserializer::LlmDeserialize for #name #ty_generics #where_clause {
93 #deserialize_impl
94 }
95 };
96
97 TokenStream::from(expanded)
98 }
99 Data::Union(_) => {
100 syn::Error::new_spanned(input, "LlmDeserialize cannot be derived for unions")
101 .to_compile_error()
102 .into()
103 }
104 }
105}