use crate::tool::doc::parse_doc;
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, format_ident, quote};
use std::hash::{Hash, Hasher};
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream, Parser};
use syn::punctuated::Punctuated;
use syn::{
Error, Expr, ExprLit, FnArg, GenericArgument, Ident, ImplItem, Item, ItemFn, ItemImpl, Lit,
LitStr, Pat, Path, PathArguments, ReturnType, Token, Type,
};
enum MiddlewareAttributeValue {
Expr(Expr),
Type(Type),
}
impl ToTokens for MiddlewareAttributeValue {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
Self::Expr(value) => value.to_tokens(tokens),
Self::Type(value) => value.to_tokens(tokens),
}
}
}
struct MiddlewareNameValue {
path: Path,
value: MiddlewareAttributeValue,
}
impl Parse for MiddlewareNameValue {
fn parse(input: ParseStream) -> syn::Result<Self> {
let path = input.parse::<Path>()?;
input.parse::<Token![=]>()?;
let value = if path.is_ident("parameters") {
MiddlewareAttributeValue::Type(input.parse()?)
} else {
MiddlewareAttributeValue::Expr(input.parse()?)
};
Ok(Self { path, value })
}
}
pub fn tool_middleware_impl(
attrs: TokenStream,
item: TokenStream,
golem_rust: &Ident,
) -> TokenStream {
expand_tool_middleware(attrs.into(), item.into(), golem_rust)
.unwrap_or_else(Error::into_compile_error)
.into()
}
pub fn universal_tool_middleware_impl(
attrs: TokenStream,
item: TokenStream,
golem_rust: &Ident,
) -> TokenStream {
expand_universal_tool_middleware(attrs.into(), item.into(), golem_rust)
.unwrap_or_else(Error::into_compile_error)
.into()
}
struct MonomorphicArgs {
name: LitStr,
version: LitStr,
constructor: Path,
parameters: Option<Type>,
}
struct UniversalArgs {
name: LitStr,
version: LitStr,
parameters: Option<Type>,
}
fn parse_monomorphic_args(attrs: TokenStream2) -> syn::Result<MonomorphicArgs> {
let values = parse_name_values(attrs)?;
let mut name = None;
let mut version = None;
let mut constructor = None;
let mut parameters = None;
for value in values {
let Some(key) = value.path.get_ident() else {
return Err(Error::new_spanned(
value.path,
"tool middleware attribute keys must be identifiers",
));
};
match key.to_string().as_str() {
"name" => set_once(&mut name, parse_string(value.value, "name")?, key)?,
"version" => set_once(&mut version, parse_string(value.value, "version")?, key)?,
"constructor" => {
let MiddlewareAttributeValue::Expr(Expr::Path(path)) = value.value else {
return Err(Error::new_spanned(
value.value,
"`constructor` must be a synchronous zero-argument function path",
));
};
if path.qself.is_some() {
return Err(Error::new_spanned(
path,
"`constructor` must be a synchronous zero-argument function path",
));
}
set_once(&mut constructor, path.path, key)?;
}
"parameters" => {
let MiddlewareAttributeValue::Type(Type::Path(path)) = value.value else {
return Err(Error::new_spanned(
value.value,
"`parameters` must be a type path",
));
};
set_once(&mut parameters, Type::Path(path), key)?;
}
other => {
return Err(Error::new_spanned(
key,
format!(
"unknown #[tool_middleware] key `{other}`; expected `name`, `version`, `constructor`, or `parameters`"
),
));
}
}
}
let name = name.ok_or_else(|| {
Error::new(
proc_macro2::Span::call_site(),
"#[tool_middleware] is missing `name`",
)
})?;
validate_middleware_name(&name)?;
let constructor = constructor.ok_or_else(|| {
Error::new(
proc_macro2::Span::call_site(),
"#[tool_middleware] is missing `constructor`",
)
})?;
let version = version.unwrap_or_else(|| LitStr::new("0.0.0", name.span()));
Ok(MonomorphicArgs {
name,
version,
constructor,
parameters,
})
}
fn parse_universal_args(attrs: TokenStream2) -> syn::Result<UniversalArgs> {
let values = parse_name_values(attrs)?;
let mut name = None;
let mut version = None;
let mut parameters = None;
for value in values {
let Some(key) = value.path.get_ident() else {
return Err(Error::new_spanned(
value.path,
"universal tool middleware attribute keys must be identifiers",
));
};
match key.to_string().as_str() {
"name" => set_once(&mut name, parse_string(value.value, "name")?, key)?,
"version" => set_once(&mut version, parse_string(value.value, "version")?, key)?,
"parameters" => {
let MiddlewareAttributeValue::Type(Type::Path(path)) = value.value else {
return Err(Error::new_spanned(
value.value,
"`parameters` must be a type path",
));
};
set_once(&mut parameters, Type::Path(path), key)?;
}
other => {
return Err(Error::new_spanned(
key,
format!(
"unknown #[universal_tool_middleware] key `{other}`; expected `name`, `version`, or `parameters`"
),
));
}
}
}
let name = name.ok_or_else(|| {
Error::new(
proc_macro2::Span::call_site(),
"#[universal_tool_middleware] is missing `name`",
)
})?;
validate_middleware_name(&name)?;
let version = version.unwrap_or_else(|| LitStr::new("0.0.0", name.span()));
Ok(UniversalArgs {
name,
version,
parameters,
})
}
fn parse_name_values(
attrs: TokenStream2,
) -> syn::Result<Punctuated<MiddlewareNameValue, Token![,]>> {
Punctuated::<MiddlewareNameValue, Token![,]>::parse_terminated.parse2(attrs)
}
fn parse_string(value: MiddlewareAttributeValue, key: &str) -> syn::Result<LitStr> {
let MiddlewareAttributeValue::Expr(expr) = value else {
return Err(Error::new_spanned(
value,
format!("`{key}` must be a string literal"),
));
};
let Expr::Lit(ExprLit {
lit: Lit::Str(value),
..
}) = expr
else {
return Err(Error::new_spanned(
expr,
format!("`{key}` must be a string literal"),
));
};
Ok(value)
}
fn set_once<T>(slot: &mut Option<T>, value: T, key: &Ident) -> syn::Result<()> {
if slot.replace(value).is_some() {
return Err(Error::new_spanned(
key,
format!("duplicate tool middleware attribute key `{key}`"),
));
}
Ok(())
}
fn validate_middleware_name(name: &LitStr) -> syn::Result<()> {
let value = name.value();
let mut previous_dash = false;
let valid = !value.is_empty()
&& value.chars().enumerate().all(|(index, character)| {
let character_valid = if index == 0 {
character.is_ascii_lowercase()
} else {
character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-'
};
let dash_valid = !(character == '-' && previous_dash);
previous_dash = character == '-';
character_valid && dash_valid
})
&& !value.ends_with('-');
if !valid {
return Err(Error::new_spanned(
name,
"tool middleware names must match `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`",
));
}
Ok(())
}
fn expand_tool_middleware(
attrs: TokenStream2,
item: TokenStream2,
golem_rust: &Ident,
) -> syn::Result<TokenStream2> {
let args = parse_monomorphic_args(attrs)?;
let Item::Impl(mut item_impl) = syn::parse2::<Item>(item)? else {
return Err(Error::new(
proc_macro2::Span::call_site(),
"#[tool_middleware] must be applied to a trait implementation (`impl ToolMiddleware for Type`)",
));
};
let trait_path = validate_monomorphic_impl(&item_impl)?;
let self_ty = &item_impl.self_ty;
let constructor = &args.constructor;
let name = &args.name;
let version = &args.version;
let parameter_ty: Type = args
.parameters
.clone()
.unwrap_or_else(|| syn::parse_quote!(#golem_rust::tool::EmptyMiddlewareParameters));
let doc = parse_doc(&item_impl.attrs);
let summary = doc.summary;
let description = doc.description;
let suffix = registration_suffix(self_ty, &trait_path, &name.value());
let trait_name = trait_path
.segments
.last()
.expect("validated middleware trait path")
.ident
.unraw()
.to_string()
.to_lowercase();
let descriptor_ident =
format_ident!("__golem_tool_middleware_descriptor_{trait_name}_{suffix:016x}");
let invoker_ident = format_ident!("__golem_tool_middleware_invoke_{trait_name}_{suffix:016x}");
let constructor_trait_ident =
format_ident!("__GolemToolMiddlewareConstructor_{trait_name}_{suffix:016x}");
let constructor_result_trait_ident =
format_ident!("__GolemToolMiddlewareConstructorResult_{trait_name}_{suffix:016x}");
let assert_constructor_ident = format_ident!(
"__golem_tool_middleware_constructor_must_be_synchronous_infallible_zero_argument_and_return_self_{trait_name}_{suffix:016x}"
);
let assert_constructor_result_ident = format_ident!(
"__golem_tool_middleware_constructor_must_be_synchronous_infallible_zero_argument_and_return_self_result_{trait_name}_{suffix:016x}"
);
let (constructor_bound, constructor_call, constructor_diagnostic) = if args.parameters.is_some()
{
(
quote! { ::std::ops::FnOnce(#parameter_ty) -> Output },
quote! { constructor(parameters) },
"tool middleware `constructor` must be synchronous, infallible, accept the declared parameters type, and return the middleware implementation type (`fn(Parameters) -> Self`)",
)
} else {
(
quote! { ::std::ops::FnOnce() -> Output },
quote! { constructor() },
"tool middleware `constructor` must be synchronous, infallible, zero-argument, and return the middleware implementation type (`fn() -> Self`)",
)
};
let construct_middleware = quote! {
{
let constructor = #assert_constructor_ident::<_, _>(#constructor);
#assert_constructor_result_ident::<#self_ty, _>(#constructor_call)
}
};
let register_ident =
format_ident!("__golem_register_tool_middleware_{trait_name}_{suffix:016x}");
let annotation: ImplItem = syn::parse_quote! {
#[doc(hidden)]
fn __golem_tool_middleware_annotation() where Self: Sized {}
};
item_impl.items.push(annotation);
Ok(quote! {
#item_impl
#[doc(hidden)]
#[allow(non_camel_case_types)]
#[diagnostic::on_unimplemented(
message = #constructor_diagnostic
)]
trait #constructor_trait_ident<Output> {}
impl<Constructor, Output> #constructor_trait_ident<Output> for Constructor
where
Constructor: #constructor_bound,
{}
#[doc(hidden)]
fn #assert_constructor_ident<Output, Constructor>(constructor: Constructor) -> Constructor
where
Constructor: #constructor_trait_ident<Output>,
{
constructor
}
#[doc(hidden)]
#[allow(non_camel_case_types)]
#[diagnostic::on_unimplemented(
message = #constructor_diagnostic
)]
trait #constructor_result_trait_ident<Expected> {
fn into_expected(self) -> Expected;
}
impl<Expected> #constructor_result_trait_ident<Expected> for Expected {
fn into_expected(self) -> Expected {
self
}
}
#[doc(hidden)]
fn #assert_constructor_result_ident<Expected, Actual>(actual: Actual) -> Expected
where
Actual: #constructor_result_trait_ident<Expected>,
{
<Actual as #constructor_result_trait_ident<Expected>>::into_expected(actual)
}
#[doc(hidden)]
fn #descriptor_ident() -> #golem_rust::tool::ToolMiddleware {
#golem_rust::tool::ToolMiddleware {
name: #name.to_string(),
version: #version.to_string(),
aliases: ::std::vec::Vec::new(),
doc: #golem_rust::schema::tool::Doc {
summary: #summary.to_string(),
description: #description.to_string(),
examples: ::std::vec::Vec::new(),
},
scope: #golem_rust::tool::ToolMiddlewareScope::Monomorphic(
::std::boxed::Box::new(#golem_rust::tool::MonomorphicToolMiddlewareScope {
presented: <#self_ty as #trait_path>::__golem_presented_tool_descriptor(),
expected: ::std::option::Option::Some(
<#self_ty as #trait_path>::__golem_expected_tool_descriptor()
),
})
),
parameter_schema: #golem_rust::schema::try_into_schema_graph::<#parameter_ty>()
.expect("tool middleware parameter schema build failed"),
}
}
#[doc(hidden)]
fn #invoker_ident(
_tool_name: ::std::string::String,
_tool_metadata: #golem_rust::tool::Tool,
parameters: #golem_rust::TypedSchemaValue,
command_path: ::std::vec::Vec<::std::string::String>,
input: #golem_rust::TypedSchemaValue,
stdin: ::std::option::Option<#golem_rust::tool::InputStream>,
stdout: ::std::option::Option<#golem_rust::tool::OutputStream>,
stderr: ::std::option::Option<#golem_rust::tool::OutputStream>,
principal: #golem_rust::tool::Principal,
underlying: #golem_rust::tool::UnderlyingTool,
) -> #golem_rust::tool::ToolMiddlewareInvokeFuture {
::std::boxed::Box::pin(async move {
let parameters = #golem_rust::tool::decode_middleware_parameters::<#parameter_ty>(parameters)?;
let middleware = #construct_middleware;
<#self_ty as #trait_path>::__golem_invoke_tool_middleware(
&middleware,
command_path,
input,
stdin,
stdout,
stderr,
principal,
underlying,
)
.await
})
}
#golem_rust::ctor::__support::ctor_parse!(
#[ctor] fn #register_ident() {
#golem_rust::tool::install_middleware_exports();
#golem_rust::tool::register_tool_middleware(
#descriptor_ident(),
#invoker_ident,
);
}
);
})
}
fn validate_monomorphic_impl(item_impl: &ItemImpl) -> syn::Result<Path> {
if !item_impl.generics.params.is_empty() {
return Err(Error::new_spanned(
&item_impl.generics,
"#[tool_middleware] requires a concrete, non-generic implementation",
));
}
let Some((negative, trait_path, _)) = &item_impl.trait_ else {
return Err(Error::new_spanned(
&item_impl.self_ty,
"#[tool_middleware] must be applied to a generated middleware trait implementation",
));
};
if negative.is_some() {
return Err(Error::new_spanned(
trait_path,
"#[tool_middleware] does not support negative trait implementations",
));
}
let segment = trait_path
.segments
.last()
.ok_or_else(|| Error::new_spanned(trait_path, "middleware trait path must not be empty"))?;
let trait_name = segment.ident.unraw().to_string();
if trait_name
.strip_suffix("Middleware")
.is_none_or(str::is_empty)
{
return Err(Error::new_spanned(
&segment.ident,
"#[tool_middleware] requires a generated `<Tool>Middleware` trait",
));
}
match &segment.arguments {
PathArguments::None => {}
PathArguments::AngleBracketed(arguments) => {
if arguments.args.len() != 1 {
return Err(Error::new_spanned(
arguments,
"a generated middleware trait accepts exactly one underlying proxy type",
));
}
let Some(GenericArgument::Type(underlying)) = arguments.args.first() else {
return Err(Error::new_spanned(
arguments,
"the middleware trait argument must be a generated `<Tool>Underlying` type",
));
};
let Some(underlying_name) = type_last_ident(underlying) else {
return Err(Error::new_spanned(
underlying,
"the middleware trait argument must be a generated `<Tool>Underlying` type",
));
};
if underlying_name
.strip_suffix("Underlying")
.is_none_or(str::is_empty)
{
return Err(Error::new_spanned(
underlying,
"the middleware trait argument must be a generated `<Tool>Underlying` type",
));
}
}
PathArguments::Parenthesized(arguments) => {
return Err(Error::new_spanned(
arguments,
"generated middleware traits do not use parenthesized arguments",
));
}
}
Ok(trait_path.clone())
}
fn registration_suffix(self_ty: &Type, trait_path: &Path, name: &str) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
self_ty.to_token_stream().to_string().hash(&mut hasher);
trait_path.to_token_stream().to_string().hash(&mut hasher);
name.hash(&mut hasher);
hasher.finish()
}
fn expand_universal_tool_middleware(
attrs: TokenStream2,
item: TokenStream2,
golem_rust: &Ident,
) -> syn::Result<TokenStream2> {
let args = parse_universal_args(attrs)?;
let Item::Fn(item_fn) = syn::parse2::<Item>(item)? else {
return Err(Error::new(
proc_macro2::Span::call_site(),
"#[universal_tool_middleware] must be applied to an async free function",
));
};
validate_universal_function(&item_fn)?;
let expected_parameter_count = if args.parameters.is_some() { 10 } else { 9 };
if item_fn.sig.inputs.len() != expected_parameter_count {
return Err(Error::new_spanned(
&item_fn.sig.inputs,
"the universal middleware function parameter list must match its `parameters` declaration",
));
}
let function_ident = &item_fn.sig.ident;
let name = &args.name;
let version = &args.version;
let parameter_ty: Type = args
.parameters
.clone()
.unwrap_or_else(|| syn::parse_quote!(#golem_rust::tool::EmptyMiddlewareParameters));
let parameter_binding = if args.parameters.is_some() {
quote! { let parameters = #golem_rust::tool::decode_middleware_parameters::<#parameter_ty>(parameters)?; }
} else {
quote! { #golem_rust::tool::decode_middleware_parameters::<#parameter_ty>(parameters)?; }
};
let parameter_argument = args.parameters.as_ref().map(|_| quote! { parameters, });
let doc = parse_doc(&item_fn.attrs);
let summary = doc.summary;
let description = doc.description;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
function_ident.unraw().to_string().hash(&mut hasher);
name.value().hash(&mut hasher);
let suffix = hasher.finish();
let function_name = function_ident.unraw().to_string().to_lowercase();
let descriptor_ident =
format_ident!("__golem_universal_tool_middleware_descriptor_{function_name}_{suffix:016x}");
let invoker_ident =
format_ident!("__golem_universal_tool_middleware_invoke_{function_name}_{suffix:016x}");
let register_ident =
format_ident!("__golem_register_universal_tool_middleware_{function_name}_{suffix:016x}");
Ok(quote! {
#item_fn
#[doc(hidden)]
fn #descriptor_ident() -> #golem_rust::tool::ToolMiddleware {
#golem_rust::tool::ToolMiddleware {
name: #name.to_string(),
version: #version.to_string(),
aliases: ::std::vec::Vec::new(),
doc: #golem_rust::schema::tool::Doc {
summary: #summary.to_string(),
description: #description.to_string(),
examples: ::std::vec::Vec::new(),
},
scope: #golem_rust::tool::ToolMiddlewareScope::Universal,
parameter_schema: #golem_rust::schema::try_into_schema_graph::<#parameter_ty>()
.expect("tool middleware parameter schema build failed"),
}
}
#[doc(hidden)]
fn #invoker_ident(
tool_name: ::std::string::String,
tool_metadata: #golem_rust::tool::Tool,
parameters: #golem_rust::TypedSchemaValue,
command_path: ::std::vec::Vec<::std::string::String>,
input: #golem_rust::TypedSchemaValue,
stdin: ::std::option::Option<#golem_rust::tool::InputStream>,
stdout: ::std::option::Option<#golem_rust::tool::OutputStream>,
stderr: ::std::option::Option<#golem_rust::tool::OutputStream>,
principal: #golem_rust::tool::Principal,
underlying: #golem_rust::tool::UnderlyingTool,
) -> #golem_rust::tool::ToolMiddlewareInvokeFuture {
::std::boxed::Box::pin(async move {
#parameter_binding
#function_ident(
#parameter_argument
tool_name,
tool_metadata,
command_path,
input,
stdin,
stdout,
stderr,
principal,
underlying,
).await
})
}
#golem_rust::ctor::__support::ctor_parse!(
#[ctor] fn #register_ident() {
#golem_rust::tool::install_middleware_exports();
#golem_rust::tool::register_tool_middleware(
#descriptor_ident(),
#invoker_ident,
);
}
);
})
}
fn validate_universal_function(item_fn: &ItemFn) -> syn::Result<()> {
let signature = &item_fn.sig;
if signature.asyncness.is_none() {
return Err(Error::new_spanned(
signature,
"#[universal_tool_middleware] requires an async function",
));
}
if !signature.generics.params.is_empty() || signature.generics.where_clause.is_some() {
return Err(Error::new_spanned(
&signature.generics,
"#[universal_tool_middleware] does not support generic functions",
));
}
if signature.constness.is_some()
|| signature.unsafety.is_some()
|| signature.abi.is_some()
|| signature.variadic.is_some()
{
return Err(Error::new_spanned(
signature,
"#[universal_tool_middleware] requires a safe Rust async function",
));
}
let inputs = signature
.inputs
.iter()
.map(|argument| {
let FnArg::Typed(argument) = argument else {
return Err(Error::new_spanned(
argument,
"universal tool middleware functions cannot have a receiver",
));
};
if !matches!(argument.pat.as_ref(), Pat::Ident(_)) {
return Err(Error::new_spanned(
&argument.pat,
"universal tool middleware parameters must use identifier patterns",
));
}
Ok(argument.ty.as_ref())
})
.collect::<syn::Result<Vec<_>>>()?;
let has_parameters = inputs.len() == 10;
if inputs.len() != 9 && !has_parameters {
return Err(Error::new_spanned(
&signature.inputs,
"universal tool middleware functions require nine parameters, or ten when `parameters` is declared",
));
}
let offset = usize::from(has_parameters);
let expected = [
type_is_ident(inputs[offset], "String"),
type_is_ident(inputs[offset + 1], "Tool"),
type_is_container(inputs[offset + 2], "Vec", |inner| {
type_is_ident(inner, "String")
}),
type_is_ident(inputs[offset + 3], "TypedSchemaValue"),
type_is_container(inputs[offset + 4], "Option", |inner| {
type_is_ident(inner, "InputStream")
}),
type_is_container(inputs[offset + 5], "Option", |inner| {
type_is_ident(inner, "OutputStream")
}),
type_is_container(inputs[offset + 6], "Option", |inner| {
type_is_ident(inner, "OutputStream")
}),
type_is_ident(inputs[offset + 7], "Principal"),
type_is_ident(inputs[offset + 8], "UnderlyingTool"),
];
if let Some((index, _)) = expected.iter().enumerate().find(|(_, valid)| !**valid) {
return Err(Error::new_spanned(
inputs[index],
format!(
"universal tool middleware parameter {} has the wrong SDK-owned type",
index + 1
),
));
}
let ReturnType::Type(_, output) = &signature.output else {
return Err(Error::new_spanned(
&signature.output,
"universal tool middleware functions must return `Result<InvocationResult, ToolInvokeError<RawCustomToolError>>`",
));
};
if !result_type_is_exact(output) {
return Err(Error::new_spanned(
output,
"universal tool middleware functions must return `Result<InvocationResult, ToolInvokeError<RawCustomToolError>>`",
));
}
Ok(())
}
fn type_is_ident(ty: &Type, expected: &str) -> bool {
matches!(ty, Type::Path(path) if path.qself.is_none() && path.path.segments.last().is_some_and(|segment| segment.ident == expected && matches!(segment.arguments, PathArguments::None)))
}
fn type_is_container(
ty: &Type,
expected: &str,
validate_first: impl FnOnce(&Type) -> bool,
) -> bool {
let Type::Path(path) = ty else {
return false;
};
let Some(segment) = path.path.segments.last() else {
return false;
};
if segment.ident != expected {
return false;
}
let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
return false;
};
if arguments.args.len() != 1 {
return false;
}
let Some(GenericArgument::Type(first)) = arguments.args.first() else {
return false;
};
validate_first(first)
}
fn result_type_is_exact(ty: &Type) -> bool {
let Type::Path(path) = ty else {
return false;
};
let Some(segment) = path.path.segments.last() else {
return false;
};
let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
return false;
};
if segment.ident != "Result" || arguments.args.len() != 2 {
return false;
}
let Some(GenericArgument::Type(result)) = arguments.args.first() else {
return false;
};
let Some(GenericArgument::Type(error)) = arguments.args.iter().nth(1) else {
return false;
};
type_is_ident(result, "InvocationResult")
&& type_is_container(error, "ToolInvokeError", |inner| {
type_is_ident(inner, "RawCustomToolError")
})
}
fn type_last_ident(ty: &Type) -> Option<String> {
let Type::Path(path) = ty else {
return None;
};
path.path
.segments
.last()
.map(|segment| segment.ident.unraw().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use test_r::test;
fn sdk() -> Ident {
Ident::new("golem_rust", proc_macro2::Span::call_site())
}
#[test]
fn middleware_parameters_accept_generic_type_paths() {
let monomorphic = parse_monomorphic_args(quote! {
name = "policy",
constructor = Policy::new,
parameters = crate::config::Config<Vec<String>, Result<u32, crate::Error>>
})
.expect("a generic Rust type path is a valid parameter type");
let universal = parse_universal_args(quote! {
name = "audit",
parameters = ::config::Config<Vec<String>, Result<u32, ::config::Error>>
})
.expect("a qualified generic Rust type path is valid for both middleware macros");
let monomorphic = monomorphic.parameters.expect("parameters should be parsed");
let universal = universal.parameters.expect("parameters should be parsed");
assert_eq!(
quote!(#monomorphic).to_string(),
"crate :: config :: Config < Vec < String > , Result < u32 , crate :: Error > >"
);
assert_eq!(
quote!(#universal).to_string(),
":: config :: Config < Vec < String > , Result < u32 , :: config :: Error > >"
);
let invalid = parse_monomorphic_args(quote! {
name = "policy",
constructor = Policy::new,
parameters = (String, u32)
})
.err()
.expect("non-path parameter types must be rejected");
assert!(
invalid
.to_string()
.contains("`parameters` must be a type path")
);
}
#[test]
fn parameterized_monomorphic_impl_expands_with_constructor_assertions() {
let expanded = expand_tool_middleware(
quote! {
name = "path-policy",
constructor = PathPolicy::new,
parameters = crate::Config<Vec<String>, Result<u32, crate::Error>>
},
quote! { impl FileToolMiddleware for PathPolicy {} },
&sdk(),
)
.unwrap();
syn::parse2::<syn::File>(expanded.clone()).unwrap();
let text = expanded.to_string();
assert!(text.contains("FnOnce (crate :: Config < Vec < String > , Result"));
assert!(text.contains("< PathPolicy , _ > (constructor (parameters))"));
assert!(text.contains("accept the declared parameters type"));
let universal = expand_universal_tool_middleware(
quote! {
name = "audit",
parameters = ::config::Config<Vec<String>, Result<u32, ::config::Error>>
},
quote! {
async fn audit(
_parameters: ::config::Config<Vec<String>, Result<u32, ::config::Error>>,
_tool_name: String,
_tool_metadata: golem_rust::tool::Tool,
command_path: Vec<String>,
input: golem_rust::TypedSchemaValue,
stdin: Option<golem_rust::tool::InputStream>,
_stdout: Option<golem_rust::tool::OutputStream>,
_stderr: Option<golem_rust::tool::OutputStream>,
_principal: golem_rust::tool::Principal,
underlying: golem_rust::tool::UnderlyingTool,
) -> Result<
golem_rust::tool::InvocationResult,
golem_rust::tool::ToolInvokeError<golem_rust::tool::RawCustomToolError>,
> {
underlying.invoke(command_path, input, stdin).await
}
},
&sdk(),
)
.unwrap();
syn::parse2::<syn::File>(universal.clone()).unwrap();
assert!(universal.to_string().contains(
"decode_middleware_parameters :: < :: config :: Config < Vec < String > , Result"
));
}
#[test]
fn transparent_monomorphic_impl_expands_with_constructor_assertion() {
let expanded = expand_tool_middleware(
quote! { name = "path-policy", constructor = PathPolicy::new },
quote! {
impl FileToolMiddleware for PathPolicy {
async fn read(
&self,
underlying: &FileToolUnderlying,
path: String,
) -> Result<Vec<u8>, golem_rust::tool::ToolInvokeError<FileError>> {
underlying.read(path).await
}
}
},
&sdk(),
)
.unwrap();
syn::parse2::<syn::File>(expanded.clone()).unwrap();
let text = expanded.to_string();
assert!(text.contains("Constructor : :: std :: ops :: FnOnce () -> Output"));
assert!(text.contains("constructor_must_be_synchronous_infallible_zero_argument"));
assert!(text.contains("< PathPolicy , _ > (constructor ())"));
assert!(text.contains("constructor` must be synchronous, infallible, zero-argument"));
assert!(text.contains("__golem_tool_middleware_annotation"));
assert!(text.contains("ToolMiddlewareScope :: Monomorphic"));
}
#[test]
fn adapter_underlying_type_is_accepted() {
let expanded = expand_tool_middleware(
quote! { name = "grep-via-ripgrep", constructor = Adapter::new },
quote! { impl GrepMiddleware<tools::RipgrepUnderlying> for Adapter {} },
&sdk(),
)
.unwrap();
syn::parse2::<syn::File>(expanded).unwrap();
}
#[test]
fn monomorphic_attributes_are_required_and_closed() {
let missing_name = expand_tool_middleware(
quote! { constructor = Policy::new },
quote! { impl FileMiddleware for Policy {} },
&sdk(),
)
.unwrap_err();
assert!(missing_name.to_string().contains("missing `name`"));
let missing = expand_tool_middleware(
quote! { name = "policy" },
quote! { impl FileMiddleware for Policy {} },
&sdk(),
)
.unwrap_err();
assert!(missing.to_string().contains("missing `constructor`"));
let unknown = expand_tool_middleware(
quote! { name = "policy", constructor = Policy::new, aliases = [] },
quote! { impl FileMiddleware for Policy {} },
&sdk(),
)
.unwrap_err();
assert!(
unknown
.to_string()
.contains("unknown #[tool_middleware] key `aliases`")
);
}
#[test]
fn monomorphic_target_must_be_concrete_generated_trait_impl() {
let inherent = expand_tool_middleware(
quote! { name = "policy", constructor = Policy::new },
quote! { impl Policy {} },
&sdk(),
)
.unwrap_err();
assert!(
inherent
.to_string()
.contains("generated middleware trait implementation")
);
let generic = expand_tool_middleware(
quote! { name = "policy", constructor = Policy::<T>::new },
quote! { impl<T> FileMiddleware for Policy<T> {} },
&sdk(),
)
.unwrap_err();
assert!(generic.to_string().contains("concrete, non-generic"));
let wrong_trait = expand_tool_middleware(
quote! { name = "policy", constructor = Policy::new },
quote! { impl FileTool for Policy {} },
&sdk(),
)
.unwrap_err();
assert!(
wrong_trait
.to_string()
.contains("generated `<Tool>Middleware` trait")
);
}
#[test]
fn middleware_name_uses_tool_identifier_grammar() {
let error = expand_tool_middleware(
quote! { name = "Not Valid", constructor = Policy::new },
quote! { impl FileMiddleware for Policy {} },
&sdk(),
)
.unwrap_err();
assert!(error.to_string().contains("must match"));
}
#[test]
fn valid_universal_function_expands() {
let expanded = expand_universal_tool_middleware(
quote! { name = "audit" },
quote! {
async fn audit(
tool_name: String,
tool_metadata: golem_rust::tool::Tool,
command_path: Vec<String>,
input: golem_rust::TypedSchemaValue,
stdin: Option<golem_rust::tool::InputStream>,
stdout: Option<golem_rust::tool::OutputStream>,
stderr: Option<golem_rust::tool::OutputStream>,
principal: golem_rust::tool::Principal,
underlying: golem_rust::tool::UnderlyingTool,
) -> Result<
golem_rust::tool::InvocationResult,
golem_rust::tool::ToolInvokeError<golem_rust::tool::RawCustomToolError>,
> {
underlying.invoke(command_path, input, stdin).await
}
},
&sdk(),
)
.unwrap();
syn::parse2::<syn::File>(expanded.clone()).unwrap();
assert!(
expanded
.to_string()
.contains("ToolMiddlewareScope :: Universal")
);
assert!(
!expanded
.to_string()
.contains("tool :: register_universal_tool_middleware")
);
}
#[test]
fn authoring_expansions_use_resolved_sdk_name_and_common_invoker_abi() {
let renamed_sdk = Ident::new("sdk_alias", proc_macro2::Span::call_site());
let monomorphic = expand_tool_middleware(
quote! { name = "policy", constructor = Policy::new },
quote! { impl FileMiddleware for Policy {} },
&renamed_sdk,
)
.unwrap();
let universal = expand_universal_tool_middleware(
quote! { name = "audit" },
universal_function(true, false),
&renamed_sdk,
)
.unwrap();
for expansion in [monomorphic, universal] {
let file = syn::parse2::<syn::File>(expansion.clone()).unwrap();
let invoker = file
.items
.iter()
.filter_map(|item| match item {
Item::Fn(item) if item.sig.ident.to_string().contains("invoke") => Some(item),
_ => None,
})
.next()
.expect("expansion should contain an invoker adapter");
assert_eq!(invoker.sig.inputs.len(), 10);
let text = expansion.to_string();
assert!(text.contains("sdk_alias :: tool :: register_tool_middleware"));
}
}
#[test]
fn universal_generic_arity_is_exact() {
let too_many_vec_args: Type = syn::parse_quote! { Vec<String, String> };
assert!(!type_is_container(&too_many_vec_args, "Vec", |inner| {
type_is_ident(inner, "String")
}));
let too_many_result_args: Type = syn::parse_quote! {
Result<InvocationResult, ToolInvokeError<TypedSchemaValue>, TypedSchemaValue>
};
assert!(!result_type_is_exact(&too_many_result_args));
let too_many_error_args: Type = syn::parse_quote! {
Result<InvocationResult, ToolInvokeError<TypedSchemaValue, TypedSchemaValue>>
};
assert!(!result_type_is_exact(&too_many_error_args));
}
#[test]
fn universal_target_and_signature_are_validated() {
let not_function = expand_universal_tool_middleware(
quote! { name = "audit" },
quote! { struct Audit; },
&sdk(),
)
.unwrap_err();
assert!(not_function.to_string().contains("async free function"));
let not_async = expand_universal_tool_middleware(
quote! { name = "audit" },
universal_function(false, false),
&sdk(),
)
.unwrap_err();
assert!(not_async.to_string().contains("requires an async function"));
let generic = expand_universal_tool_middleware(
quote! { name = "audit" },
universal_function(true, true),
&sdk(),
)
.unwrap_err();
assert!(
generic
.to_string()
.contains("does not support generic functions")
);
}
#[test]
fn universal_attributes_are_closed() {
let error = expand_universal_tool_middleware(
quote! { name = "audit", constructor = Audit::new },
universal_function(true, false),
&sdk(),
)
.unwrap_err();
assert!(
error
.to_string()
.contains("unknown #[universal_tool_middleware] key `constructor`")
);
}
fn universal_function(is_async: bool, generic: bool) -> TokenStream2 {
let asyncness = is_async.then(|| quote! { async });
let generics = generic.then(|| quote! { <T> });
quote! {
#asyncness fn audit #generics(
tool_name: String,
tool_metadata: Tool,
command_path: Vec<String>,
input: TypedSchemaValue,
stdin: Option<InputStream>,
stdout: Option<OutputStream>,
stderr: Option<OutputStream>,
principal: Principal,
underlying: UnderlyingTool,
) -> Result<InvocationResult, ToolInvokeError<RawCustomToolError>> {
unimplemented!()
}
}
}
}