use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Type, Meta};
#[proc_macro_derive(LlmSchema, attributes(llmschem))]
pub fn derive_llm_schema(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let schema = generate_schema(&ast);
let expanded = quote! {
impl #name {
pub fn llm_schema() -> ::serde_json::Value {
#schema
}
}
};
TokenStream::from(expanded)
}
fn generate_schema(ast: &DeriveInput) -> proc_macro2::TokenStream {
let name = &ast.ident;
let fields = match &ast.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => fields.named.iter(),
_ => unimplemented!("Only named fields are supported"),
},
_ => unimplemented!("Only structs are supported"),
};
let mut properties = quote! {};
let mut required_fields = Vec::new();
for field in fields {
let field_name = &field.ident;
let field_name_str = field_name.as_ref().unwrap().to_string();
let mut is_required = false; for attr in &field.attrs {
if attr.path().is_ident("llmschem") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("require") {
is_required = true;
}
Ok(())
}).unwrap_or_default();
}
}
if let Type::Path(type_path) = &field.ty {
if let Some(segment) = type_path.path.segments.last() {
if segment.ident == "Option" {
is_required = false;
}
}
}
let type_def = get_type_definition(&field.ty);
properties.extend(quote! {
#field_name_str: {
"type": #type_def
},
});
if is_required {
required_fields.push(field_name_str);
}
}
let required_array = if !required_fields.is_empty() {
let required = required_fields.iter().map(|s| quote! { #s });
quote! {
schema["required"] = ::serde_json::json!([#(#required),*]);
}
} else {
quote! {}
};
quote! {
{
let mut schema = ::serde_json::json!({
"type": "object",
"properties": {
#properties
}
});
#required_array
schema
}
}
}
fn get_type_definition(ty: &Type) -> &'static str {
match ty {
Type::Path(type_path) => {
if let Some(segment) = type_path.path.segments.last() {
match segment.ident.to_string().as_str() {
"String" => "string",
"str" => "string",
"bool" => "boolean",
"f32" | "f64" | "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" => "number",
"Option" => "null", _ => panic!("Unsupported type for LLM schema"),
}
} else {
panic!("Invalid type path");
}
}
_ => panic!("Only simple types are supported"),
}
}