mod container_attrs;
mod generators;
mod parsers;
mod type_utils;
use container_attrs::ContainerAttributes;
use proc_macro::TokenStream;
use syn::{Data, DeriveInput, 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 instructor_impl = 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! {
impl ::rstructor::model::Instructor for #name {
fn validate(&self) -> ::rstructor::error::Result<()> {
#validate_path(self)
}
}
}
} else {
quote::quote! {
impl ::rstructor::model::Instructor for #name {
fn validate(&self) -> ::rstructor::error::Result<()> {
::rstructor::error::Result::Ok(())
}
}
}
};
let combined = quote::quote! {
#schema_impl
#instructor_impl
};
combined.into()
}
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()
}