use proc_macro::TokenStream;
use syn::{DeriveInput, Fields, Ident, parse_macro_input};
pub fn derive_config_schema(input: TokenStream, golem_rust_crate_ident: &Ident) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let fields = match &ast.data {
syn::Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => {
return syn::Error::new_spanned(
&ast.ident,
"ConfigSchema only supports structs with named fields",
)
.to_compile_error()
.into();
}
},
_ => {
return syn::Error::new_spanned(
&ast.ident,
"ConfigSchema can only be derived for structs",
)
.to_compile_error()
.into();
}
};
let struct_name = &ast.ident;
let rpc_struct_name = Ident::new(&format!("{struct_name}Rpc"), struct_name.span());
let config_schema_impl = generate_config_schema_impl(
struct_name,
&rpc_struct_name,
golem_rust_crate_ident,
fields,
);
let rpc_struct = generate_rpc_struct(&rpc_struct_name, golem_rust_crate_ident, fields);
let as_rpc_impl =
generate_into_rpc_config_param_impl(&rpc_struct_name, golem_rust_crate_ident, fields);
let expanded = quote::quote! {
#rpc_struct
#as_rpc_impl
#config_schema_impl
};
expanded.into()
}
fn generate_config_schema_impl(
struct_name: &Ident,
rpc_struct_name: &Ident,
golem_rust_crate_ident: &Ident,
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> proc_macro2::TokenStream {
let mut append_config_entries = Vec::new();
let mut append_wire_entries = Vec::new();
let mut load_entries = Vec::new();
for field in fields {
let field_ident = field.ident.as_ref().unwrap();
let field_name_str = field_ident.to_string();
let field_ty = &field.ty;
let wire_entry = if has_nested_attr(field) {
quote::quote! {
config_entries.extend(<#field_ty as #golem_rust_crate_ident::agentic::ConfigSchema>::describe_wire_config(&field_path, builder));
}
} else {
let (source, value_type) = if has_secret_attr(field) {
(
quote::quote! { Secret },
quote::quote! {{
let inner = <<#field_ty as #golem_rust_crate_ident::agentic::InnerTypeHelper>::Type as #golem_rust_crate_ident::WireSchema>::append_schema(builder);
builder.push(#golem_rust_crate_ident::schema::wit::wire::SchemaTypeBody::SecretType(
#golem_rust_crate_ident::schema::wit::wire::SecretSpec { inner, category: None }
))
}},
)
} else {
(
quote::quote! { Local },
quote::quote! { <#field_ty as #golem_rust_crate_ident::WireSchema>::append_schema(builder) },
)
};
quote::quote! {
config_entries.push(#golem_rust_crate_ident::golem_agentic::golem::agent::common::AgentConfigDeclaration {
source: #golem_rust_crate_ident::golem_agentic::golem::agent::common::AgentConfigSource::#source,
path: field_path,
value_type: #value_type,
});
}
};
append_wire_entries.push(quote::quote! {{
let mut field_path = path.to_vec();
field_path.push(#field_name_str.to_string());
#wire_entry
}});
if has_nested_attr(field) {
append_config_entries.push(quote::quote! {
{
let mut field_path = path.to_vec();
field_path.push(#field_name_str.to_string());
let mut nested_entries = <#field_ty as #golem_rust_crate_ident::agentic::ConfigSchema>::describe_config(&field_path);
config_entries.append(&mut nested_entries);
}
});
load_entries.push(quote::quote! {
#field_ident: {
let mut field_path = path.to_vec();
field_path.push(#field_name_str.to_string());
<#field_ty as #golem_rust_crate_ident::agentic::ConfigSchema>::load(&field_path)?
}
});
} else if has_secret_attr(field) {
append_config_entries.push(quote::quote! {
{
let mut field_path = path.to_vec();
field_path.push(#field_name_str.to_string());
let config_entry = #golem_rust_crate_ident::agentic::ExtendedAgentConfigDeclaration {
source: #golem_rust_crate_ident::golem_agentic::golem::agent::common::AgentConfigSource::Secret,
path: field_path,
value_type: #golem_rust_crate_ident::agentic::secret_schema_graph::<<#field_ty as #golem_rust_crate_ident::agentic::InnerTypeHelper>::Type>()
.expect("failed to build config schema graph"),
};
config_entries.push(config_entry);
}
});
load_entries.push(quote::quote! {
#field_ident: {
let mut field_path = path.to_vec();
field_path.push(#field_name_str.to_string());
#golem_rust_crate_ident::agentic::Secret::new(field_path)
}
});
} else {
append_config_entries.push(quote::quote! {
{
let mut field_path = path.to_vec();
field_path.push(#field_name_str.to_string());
let config_entry = #golem_rust_crate_ident::agentic::ExtendedAgentConfigDeclaration {
source: #golem_rust_crate_ident::golem_agentic::golem::agent::common::AgentConfigSource::Local,
path: field_path,
value_type: #golem_rust_crate_ident::schema::try_into_schema_graph::<#field_ty>()
.expect("failed to build config schema graph"),
};
config_entries.push(config_entry);
}
});
load_entries.push(quote::quote! {
#field_ident: {
let mut field_path = path.to_vec();
field_path.push(#field_name_str.to_string());
let graph = #golem_rust_crate_ident::schema::wit::direct::schema::<#field_ty>();
let value = #golem_rust_crate_ident::golem_agentic::golem::agent::host::get_config_value(
&field_path,
&graph,
)?;
#golem_rust_crate_ident::schema::wit::direct::decode::<#field_ty>(value)
.expect("failed deserializing config value")
}
});
}
}
quote::quote! {
impl #golem_rust_crate_ident::agentic::ConfigSchema for #struct_name {
type RpcType = #rpc_struct_name;
fn describe_config(path: &[String]) -> Vec<#golem_rust_crate_ident::agentic::ExtendedAgentConfigDeclaration> {
let mut config_entries = Vec::new();
#(#append_config_entries)*
config_entries
}
fn describe_wire_config(path: &[String], builder: &mut #golem_rust_crate_ident::schema::wit::direct::WireSchemaBuilder)
-> Vec<#golem_rust_crate_ident::golem_agentic::golem::agent::common::AgentConfigDeclaration> {
let mut config_entries = Vec::new();
#(#append_wire_entries)*
config_entries
}
fn load(path: &[String]) -> Result<Self, #golem_rust_crate_ident::agentic::ConfigValueError> {
Ok(Self {
#(#load_entries),*
})
}
}
}
}
fn generate_rpc_struct(
rpc_struct_name: &Ident,
golem_rust_crate_ident: &Ident,
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> proc_macro2::TokenStream {
let mut rpc_fields = Vec::new();
for field in fields {
let field_ident = field.ident.as_ref().unwrap();
let field_ty = &field.ty;
if has_secret_attr(field) {
continue; } else if has_nested_attr(field) {
rpc_fields.push(quote::quote! {
pub #field_ident: <#field_ty as #golem_rust_crate_ident::agentic::ConfigSchema>::RpcType
});
} else {
rpc_fields.push(quote::quote! {
pub #field_ident: ::std::option::Option<#field_ty>
});
}
}
quote::quote! {
#[derive(Default)]
pub struct #rpc_struct_name {
#(#rpc_fields),*
}
}
}
fn generate_into_rpc_config_param_impl(
rpc_struct_name: &Ident,
golem_rust_crate_ident: &Ident,
fields: &syn::punctuated::Punctuated<syn::Field, syn::token::Comma>,
) -> proc_macro2::TokenStream {
let mut field_processes = Vec::new();
for field in fields {
let field_ident = field.ident.as_ref().unwrap();
let field_name_str = field_ident.to_string();
let field_ty = &field.ty;
if has_secret_attr(field) {
continue; } else if has_nested_attr(field) {
field_processes.push(quote::quote! {
{
let mut field_path = path.to_vec();
field_path.push(#field_name_str.to_string());
let mut nested_fields_as_params = #golem_rust_crate_ident::agentic::IntoRpcConfigParam::into_rpc_param(self.#field_ident, &field_path);
result.append(&mut nested_fields_as_params);
}
});
} else {
field_processes.push(quote::quote! {
{
if let Some(value) = self.#field_ident.clone() {
let mut field_path = path.to_vec();
field_path.push(#field_name_str.to_string());
result.push(#golem_rust_crate_ident::golem_agentic::golem::agent::common::TypedAgentConfigValue {
path: field_path,
value: #golem_rust_crate_ident::schema::wit::wire::TypedSchemaValue {
graph: #golem_rust_crate_ident::schema::wit::direct::schema::<#field_ty>(),
value: #golem_rust_crate_ident::schema::wit::direct::encode(&value)
.expect("failed to encode config value"),
},
});
}
}
});
}
}
quote::quote! {
impl #golem_rust_crate_ident::agentic::IntoRpcConfigParam for #rpc_struct_name {
fn into_rpc_param(self, path: &[String]) -> Vec<#golem_rust_crate_ident::golem_agentic::golem::agent::common::TypedAgentConfigValue> {
let mut result = Vec::new();
#(#field_processes)*
result
}
}
}
}
fn has_secret_attr(field: &syn::Field) -> bool {
field.attrs.iter().any(|attr| {
attr.path().is_ident("config_schema")
&& attr
.parse_args::<Ident>()
.map(|i| i == "secret")
.unwrap_or(false)
})
}
fn has_nested_attr(field: &syn::Field) -> bool {
field.attrs.iter().any(|attr| {
attr.path().is_ident("config_schema")
&& attr
.parse_args::<Ident>()
.map(|i| i == "nested")
.unwrap_or(false)
})
}