mod container_attrs;
mod generators;
mod parsers;
mod type_utils;
use container_attrs::ContainerAttributes;
use proc_macro::TokenStream;
use syn::{Data, DeriveInput, Fields, parse_macro_input};
#[proc_macro_derive(Instructor, attributes(llm))]
pub fn derive_instructor(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let container_attrs = extract_container_attributes(&input.attrs);
let schema_impl = match &input.data {
Data::Struct(data_struct) => {
generators::generate_struct_schema(name, data_struct, &container_attrs)
}
Data::Enum(data_enum) => {
generators::generate_enum_schema(name, data_enum, &container_attrs)
}
_ => panic!("Instructor can only be derived for structs and enums"),
};
let field_validation = generate_field_validation(&input.data);
let container_validate = if let Some(validate_fn) = &container_attrs.validate {
let validate_path: syn::Path =
syn::parse_str(validate_fn).expect("validate attribute must be a valid function path");
quote::quote! { #validate_path(self)?; }
} else {
quote::quote! {}
};
let instructor_impl = quote::quote! {
impl ::rstructor::model::Instructor for #name {
fn validate(&self) -> ::rstructor::error::Result<()> {
#[allow(unused_imports)]
use ::rstructor::model::__private::ProbeFallback as _;
#field_validation
#container_validate
::rstructor::error::Result::Ok(())
}
}
};
let combined = quote::quote! {
#schema_impl
#instructor_impl
};
combined.into()
}
fn generate_field_validation(data: &Data) -> proc_macro2::TokenStream {
match data {
Data::Struct(data_struct) => match &data_struct.fields {
Fields::Named(named) => {
let probes = named.named.iter().map(|f| {
let ident = f.ident.as_ref().unwrap();
quote::quote! {
::rstructor::model::__private::Probe(&self.#ident).rstructor_probe()?;
}
});
quote::quote! { #(#probes)* }
}
Fields::Unnamed(unnamed) => {
let probes = unnamed.unnamed.iter().enumerate().map(|(i, _)| {
let index = syn::Index::from(i);
quote::quote! {
::rstructor::model::__private::Probe(&self.#index).rstructor_probe()?;
}
});
quote::quote! { #(#probes)* }
}
Fields::Unit => quote::quote! {},
},
Data::Enum(data_enum) => {
let arms = data_enum.variants.iter().map(|variant| {
let vname = &variant.ident;
match &variant.fields {
Fields::Named(named) => {
let binds: Vec<_> = named
.named
.iter()
.map(|f| f.ident.clone().unwrap())
.collect();
quote::quote! {
Self::#vname { #(#binds),* } => {
#( ::rstructor::model::__private::Probe(#binds).rstructor_probe()?; )*
}
}
}
Fields::Unnamed(unnamed) => {
let binds: Vec<_> = (0..unnamed.unnamed.len())
.map(|i| quote::format_ident!("field{}", i))
.collect();
quote::quote! {
Self::#vname( #(#binds),* ) => {
#( ::rstructor::model::__private::Probe(#binds).rstructor_probe()?; )*
}
}
}
Fields::Unit => quote::quote! { Self::#vname => {} },
}
});
quote::quote! {
match self {
#(#arms)*
}
}
}
_ => quote::quote! {},
}
}
use quote::ToTokens;
fn extract_container_attributes(attrs: &[syn::Attribute]) -> ContainerAttributes {
let mut description = None;
let mut title = None;
let mut examples = Vec::new();
let mut serde_rename_all = None;
let mut validate = None;
let mut serde_tag = None;
let mut serde_content = None;
let mut serde_untagged = false;
for attr in attrs {
if attr.path().is_ident("llm") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("description") {
let value = meta.value()?;
let content: syn::LitStr = value.parse()?;
description = Some(content.value());
} else if meta.path.is_ident("title") {
let value = meta.value()?;
let content: syn::LitStr = value.parse()?;
title = Some(content.value());
} else if meta.path.is_ident("validate") {
let value = meta.value()?;
let content: syn::LitStr = value.parse()?;
validate = Some(content.value());
} else if meta.path.is_ident("examples") {
let value = meta.value()?;
if let Ok(syn::Expr::Array(array)) = value.parse::<syn::Expr>() {
for elem in array.elems.iter() {
if let syn::Expr::Lit(lit_expr) = elem {
if let syn::Lit::Str(lit_str) = &lit_expr.lit {
let str_val = lit_str.value();
let json_str = quote::quote! {
::serde_json::Value::String(#str_val.to_string())
};
examples.push(json_str);
} else {
examples.push(elem.to_token_stream());
}
} else {
examples.push(elem.to_token_stream());
}
}
}
}
Ok(())
});
}
}
for attr in attrs {
if attr.path().is_ident("serde") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
let value = meta.value()?;
let content: syn::LitStr = value.parse()?;
serde_rename_all = Some(content.value());
} else if meta.path.is_ident("tag") {
let value = meta.value()?;
let content: syn::LitStr = value.parse()?;
serde_tag = Some(content.value());
} else if meta.path.is_ident("content") {
let value = meta.value()?;
let content: syn::LitStr = value.parse()?;
serde_content = Some(content.value());
} else if meta.path.is_ident("untagged") {
serde_untagged = true;
}
Ok(())
});
}
}
ContainerAttributes::builder()
.description(description)
.title(title)
.examples(examples)
.serde_rename_all(serde_rename_all)
.validate(validate)
.serde_tag(serde_tag)
.serde_content(serde_content)
.serde_untagged(serde_untagged)
.build()
}