use crate::tool::doc::parse_doc_full;
use crate::tool::helpers::{
SeenKeys, expr_str, expr_u8, fresh_internal_ident, normalize_sdk_paths_in_derive_input,
resolve_generated_sdk_paths, to_kebab_case,
};
use crate::tool::ir::{
ErrorKindIr, ToolErrorIr, ToolErrorNoPayloadStyleIr, ToolErrorPayloadIr, ToolErrorVariantIr,
};
use crate::tool::synthesis::{doc_tokens, error_kind_tokens, wire_doc_tokens};
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::spanned::Spanned;
use syn::{Attribute, Data, DeriveInput, Error, Expr, Fields, Ident};
pub fn derive_tool_error_impl(input: TokenStream, golem_rust: &Ident, guest: bool) -> TokenStream {
let derive_input = syn::parse_macro_input!(input as DeriveInput);
let canonical_golem_rust = Ident::new("golem_rust", Span::call_site());
let preserved_golem_rust = fresh_internal_ident(
"e! { #derive_input },
&format!(
"__golem_preserved_golem_rust_identifier_for_{}",
derive_input.ident
),
derive_input.ident.span(),
);
let mut ir_input = derive_input.clone();
normalize_sdk_paths_in_derive_input(
&mut ir_input,
golem_rust,
&canonical_golem_rust,
&preserved_golem_rust,
);
match parse_tool_error(&ir_input) {
Ok(ir) => resolve_generated_sdk_paths(
synthesize_tool_error(&ir, guest),
golem_rust,
&canonical_golem_rust,
&preserved_golem_rust,
)
.into(),
Err(err) => err.to_compile_error().into(),
}
}
fn synthesize_tool_error(ir: &ToolErrorIr, guest: bool) -> proc_macro2::TokenStream {
let enum_ident = &ir.enum_ident;
let type_name = enum_ident.to_string();
let cases = ir.variants.iter().map(|variant| {
let name = to_kebab_case(&variant.variant_ident.to_string());
let doc = doc_tokens(&variant.doc);
let kind = error_kind_tokens(variant.kind);
let exit_code = variant.exit_code;
let payload = match &variant.payload {
ToolErrorPayloadIr::None { .. } => quote! { ::std::option::Option::None },
ToolErrorPayloadIr::Single { ty, .. } => {
let position = format!("error {name} payload");
quote! {
::std::option::Option::Some(
golem_rust::agentic::tool_value_schema::<#ty>(#position)?
)
}
}
};
quote! {
golem_rust::agentic::ExtendedErrorCase {
name: #name.to_string(),
doc: #doc,
kind: #kind,
exit_code: #exit_code,
payload: #payload,
}
}
});
let schema_cases = ir.variants.iter().map(|variant| {
let name = to_kebab_case(&variant.variant_ident.to_string());
let payload = match &variant.payload {
ToolErrorPayloadIr::None { .. } => quote! { ::std::option::Option::None },
ToolErrorPayloadIr::Single { ty, .. } => {
quote! { ::std::option::Option::Some(<#ty as golem_rust::IntoSchema>::register_in(__builder)) }
}
};
quote! {
golem_rust::schema::VariantCaseType {
name: #name.to_string(),
payload: #payload,
metadata: ::std::default::Default::default(),
}
}
});
let to_value_arms = ir.variants.iter().enumerate().map(|(idx, variant)| {
let variant_ident = &variant.variant_ident;
let idx = idx as u32;
match &variant.payload {
ToolErrorPayloadIr::None { style } => {
let pattern = no_payload_pattern(variant_ident, *style);
quote! {
#pattern => golem_rust::SchemaValue::Variant(
golem_rust::schema::VariantValuePayload {
case: #idx,
payload: ::std::option::Option::None,
}
)
}
}
ToolErrorPayloadIr::Single {
field_ident: None, ..
} => quote! {
Self::#variant_ident(__payload) => golem_rust::SchemaValue::Variant(
golem_rust::schema::VariantValuePayload {
case: #idx,
payload: ::std::option::Option::Some(::std::boxed::Box::new(
golem_rust::IntoSchema::to_value(__payload)
)),
}
)
},
ToolErrorPayloadIr::Single {
field_ident: Some(field_ident),
..
} => quote! {
Self::#variant_ident { #field_ident } => golem_rust::SchemaValue::Variant(
golem_rust::schema::VariantValuePayload {
case: #idx,
payload: ::std::option::Option::Some(::std::boxed::Box::new(
golem_rust::IntoSchema::to_value(#field_ident)
)),
}
)
},
}
});
let from_value_arms = ir.variants.iter().enumerate().map(|(idx, variant)| {
let variant_ident = &variant.variant_ident;
let idx = idx as u32;
match &variant.payload {
ToolErrorPayloadIr::None { style } => {
let constructor = no_payload_constructor(variant_ident, *style);
quote! {
#idx => {
if __payload.payload.is_some() {
return ::std::result::Result::Err(golem_rust::schema::FromSchemaError::custom(
"tool error variant unexpectedly carried a payload"
));
}
::std::result::Result::Ok(#constructor)
}
}
}
ToolErrorPayloadIr::Single { ty, field_ident: None } => quote! {
#idx => {
let __value = __payload.payload.as_deref().ok_or_else(|| {
golem_rust::schema::FromSchemaError::custom("tool error variant is missing its payload")
})?;
::std::result::Result::Ok(Self::#variant_ident(<#ty as golem_rust::FromSchema>::from_value(__value)?))
}
},
ToolErrorPayloadIr::Single { ty, field_ident: Some(field_ident) } => quote! {
#idx => {
let __value = __payload.payload.as_deref().ok_or_else(|| {
golem_rust::schema::FromSchemaError::custom("tool error variant is missing its payload")
})?;
::std::result::Result::Ok(Self::#variant_ident {
#field_ident: <#ty as golem_rust::FromSchema>::from_value(__value)?,
})
}
},
}
});
let to_error_payload_arms = ir.variants.iter().map(|variant| {
let variant_ident = &variant.variant_ident;
let name = to_kebab_case(&variant.variant_ident.to_string());
match &variant.payload {
ToolErrorPayloadIr::None { style } => {
let pattern = no_payload_pattern(variant_ident, *style);
quote! {
#pattern => {
golem_rust::IntoTypedSchemaValue::into_typed_schema_value(&())
.map(|__payload| (#name.to_string(), __payload)).map_err(|__err| __err.to_string())
}
}
}
ToolErrorPayloadIr::Single {
field_ident: None, ..
} => quote! {
Self::#variant_ident(__payload) => {
golem_rust::IntoTypedSchemaValue::into_typed_schema_value(__payload)
.map(|__payload| (#name.to_string(), __payload)).map_err(|__err| __err.to_string())
}
},
ToolErrorPayloadIr::Single {
field_ident: Some(field_ident),
..
} => quote! {
Self::#variant_ident { #field_ident } => {
golem_rust::IntoTypedSchemaValue::into_typed_schema_value(#field_ident)
.map(|__payload| (#name.to_string(), __payload)).map_err(|__err| __err.to_string())
}
},
}
});
let direct_error_payload_arms = ir.variants.iter().map(|variant| {
let variant_ident = &variant.variant_ident;
let name = to_kebab_case(&variant.variant_ident.to_string());
match &variant.payload {
ToolErrorPayloadIr::None { style } => {
let pattern = no_payload_pattern(variant_ident, *style);
quote! { #pattern => (#name.to_string(), golem_rust::agentic::encode_direct_tool_value(&()).await?) }
}
ToolErrorPayloadIr::Single { field_ident: None, .. } => quote! {
Self::#variant_ident(__payload) => (#name.to_string(), golem_rust::agentic::encode_direct_tool_value(__payload).await?)
},
ToolErrorPayloadIr::Single { field_ident: Some(field_ident), .. } => quote! {
Self::#variant_ident { #field_ident } => (#name.to_string(), golem_rust::agentic::encode_direct_tool_value(#field_ident).await?)
},
}
});
let from_error_payload_arms = ir.variants.iter().map(|variant| {
let variant_ident = &variant.variant_ident;
let name = to_kebab_case(&variant.variant_ident.to_string());
match &variant.payload {
ToolErrorPayloadIr::None { style } => {
let constructor = no_payload_constructor(variant_ident, *style);
quote! { #name => <() as golem_rust::FromSchema>::from_value(__value.value())
.map(|_| ::std::option::Option::Some(#constructor)).map_err(|__err| __err.to_string()), }
}
ToolErrorPayloadIr::Single { ty, field_ident: None } => quote! { #name =>
<#ty as golem_rust::FromSchema>::from_value(__value.value())
.map(|__payload| ::std::option::Option::Some(Self::#variant_ident(__payload)))
.map_err(|__err| __err.to_string()),
},
ToolErrorPayloadIr::Single { ty, field_ident: Some(field_ident) } => quote! { #name =>
<#ty as golem_rust::FromSchema>::from_value(__value.value())
.map(|__payload| ::std::option::Option::Some(Self::#variant_ident { #field_ident: __payload }))
.map_err(|__err| __err.to_string()),
},
}
});
let variant_count = ir.variants.len() as u32;
let direct_error_impl = guest.then(|| {
let cases = ir.variants.iter().map(|variant| {
let name = to_kebab_case(&variant.variant_ident.to_string());
let doc = wire_doc_tokens(&variant.doc);
let kind = match variant.kind {
ErrorKindIr::UsageError => quote! { golem_rust::schema::tool::wit::wire::ErrorKind::UsageError },
ErrorKindIr::RuntimeError => quote! { golem_rust::schema::tool::wit::wire::ErrorKind::RuntimeError },
};
let exit_code = variant.exit_code;
let payload = match &variant.payload {
ToolErrorPayloadIr::None { .. } => quote! { ::std::option::Option::None },
ToolErrorPayloadIr::Single { ty, .. } => quote! {
::std::option::Option::Some(<#ty as golem_rust::WireSchema>::append_schema(__builder))
},
};
quote! {
golem_rust::schema::tool::wit::wire::ErrorCase {
name: #name.to_string(), doc: #doc, kind: #kind,
exit_code: #exit_code, payload: #payload,
}
}
});
let decode_arms = ir.variants.iter().map(|variant| {
let name = to_kebab_case(&variant.variant_ident.to_string());
let ident = &variant.variant_ident;
let (ty, constructor) = match &variant.payload {
ToolErrorPayloadIr::None { style } => (
quote! { () },
no_payload_constructor(ident, *style),
),
ToolErrorPayloadIr::Single { ty, field_ident: None } => (
quote! { #ty }, quote! { Self::#ident(__payload) },
),
ToolErrorPayloadIr::Single { ty, field_ident: Some(field) } => (
quote! { #ty }, quote! { Self::#ident { #field: __payload } },
),
};
quote! {
#name => {
let __payload = <#ty as golem_rust::schema::wit::direct::FromWire>::read_wire(__reader, __root)
.map_err(|__error| __error.to_string())?;
::std::result::Result::Ok(::std::option::Option::Some(#constructor))
}
}
});
let error_names = ir.variants.iter().map(|variant| {
to_kebab_case(&variant.variant_ident.to_string())
});
quote! {
impl golem_rust::agentic::DirectToolError for #enum_ident {
fn wire_error_cases(
__builder: &mut golem_rust::schema::wit::direct::WireSchemaBuilder,
) -> ::std::vec::Vec<golem_rust::schema::tool::wit::wire::ErrorCase> {
::std::vec![#(#cases),*]
}
fn recognizes_error_name(__name: &str) -> bool {
::std::matches!(__name, #(#error_names)|*)
}
fn from_direct_error_payload(
__name: &str,
__value: golem_rust::schema::wit::wire::SchemaValueTree,
) -> ::std::result::Result<::std::option::Option<Self>, ::std::string::String> {
let __root = __value.root;
let mut __reader = golem_rust::schema::wit::direct::WireReader::new(__value.value_nodes);
let __result = Self::from_direct_error_reader(__name, &mut __reader, __root)?;
if __result.is_some() {
__reader.finish().map_err(|__error| __error.to_string())?;
}
Ok(__result)
}
fn from_direct_error_reader(
__name: &str,
__reader: &mut golem_rust::schema::wit::direct::WireReader,
__root: golem_rust::schema::wit::wire::ValueNodeIndex,
) -> ::std::result::Result<::std::option::Option<Self>, ::std::string::String> {
match __name {
#(#decode_arms),*,
_ => ::std::result::Result::Ok(::std::option::Option::None),
}
}
async fn direct_error_payload(&self) -> ::std::result::Result<
(::std::string::String, golem_rust::schema::wit::wire::TypedSchemaValue),
::std::string::String,
> {
::std::result::Result::Ok(match self { #(#direct_error_payload_arms),* })
}
}
}
});
quote! {
impl golem_rust::agentic::ToolErrorSchema for #enum_ident {
fn error_cases() -> ::std::result::Result<
::std::vec::Vec<golem_rust::agentic::ExtendedErrorCase>,
golem_rust::agentic::ToolBuildError,
> {
::std::result::Result::Ok(::std::vec![ #(#cases),* ])
}
fn to_error_payload_value(&self) -> ::std::result::Result<(::std::string::String, golem_rust::TypedSchemaValue), ::std::string::String> {
match self {
#(#to_error_payload_arms),*
}
}
fn from_error_payload_value(
__name: ::std::string::String,
__value: golem_rust::TypedSchemaValue,
) -> ::std::result::Result<::std::option::Option<Self>, ::std::string::String> {
match __name.as_str() {
#(#from_error_payload_arms)*
_ => ::std::result::Result::Ok(::std::option::Option::None),
}
}
}
#direct_error_impl
impl golem_rust::IntoSchema for #enum_ident {
fn type_id() -> golem_rust::schema::TypeId {
golem_rust::schema::TypeId::new(
golem_rust::schema::conversion::normalize_type_path(::std::any::type_name::<Self>())
)
}
fn register_in(__builder: &mut golem_rust::schema::SchemaBuilder) -> golem_rust::SchemaType {
let __id = <Self as golem_rust::IntoSchema>::type_id();
if __builder.is_registered(&__id) {
return golem_rust::SchemaType::Ref {
id: __id,
metadata: ::std::default::Default::default(),
};
}
__builder.reserve(__id.clone());
let __body = golem_rust::SchemaType::Variant {
cases: ::std::vec![ #(#schema_cases),* ],
metadata: ::std::default::Default::default(),
};
__builder.commit(
__id.clone(),
::std::option::Option::Some(#type_name.to_string()),
::std::default::Default::default(),
__body,
);
golem_rust::SchemaType::Ref {
id: __id,
metadata: ::std::default::Default::default(),
}
}
fn to_value(&self) -> golem_rust::SchemaValue {
match self {
#(#to_value_arms),*
}
}
}
impl golem_rust::FromSchema for #enum_ident {
fn from_value(__value: &golem_rust::SchemaValue) -> ::std::result::Result<Self, golem_rust::schema::FromSchemaError> {
match __value {
golem_rust::SchemaValue::Variant(__payload) => match __payload.case {
#(#from_value_arms),*,
__idx => ::std::result::Result::Err(golem_rust::schema::FromSchemaError::out_of_range(
__idx,
#variant_count,
"tool error variant",
)),
},
__other => ::std::result::Result::Err(golem_rust::schema::FromSchemaError::shape_mismatch(
"variant",
golem_rust::schema::conversion::value_kind(__other),
"tool error",
)),
}
}
}
}
}
pub fn parse_tool_error(input: &DeriveInput) -> Result<ToolErrorIr, Error> {
let Data::Enum(data) = &input.data else {
return Err(Error::new(
input.span(),
"#[derive(ToolError)] can only be applied to enums",
));
};
if let Some(attr) = find_tool_error_attr(&input.attrs)? {
return Err(Error::new(
attr.span(),
"#[tool_error(...)] may only be used on variants of a #[derive(ToolError)] enum",
));
}
if let Some(attr) = find_example_attr(&input.attrs) {
return Err(Error::new(
attr.span(),
"#[example] may only be used on variants of a #[derive(ToolError)] enum",
));
}
let mut variants = Vec::new();
for variant in &data.variants {
for field in variant.fields.iter() {
if let Some(attr) = find_tool_error_attr(&field.attrs)? {
return Err(Error::new(
attr.span(),
"#[tool_error(...)] may only be used on variants of a #[derive(ToolError)] enum",
));
}
if let Some(attr) = find_example_attr(&field.attrs) {
return Err(Error::new(
attr.span(),
"#[example] may only be used on variants of a #[derive(ToolError)] enum",
));
}
}
let attr = find_tool_error_attr(&variant.attrs)?.ok_or_else(|| {
Error::new(
variant.span(),
format!(
"variant `{}` is missing #[tool_error(kind = \"...\", exit_code = ...)]",
variant.ident
),
)
})?;
let (kind, exit_code) = parse_tool_error_attr(attr)?;
let payload = parse_payload(&variant.fields)?;
variants.push(ToolErrorVariantIr {
variant_ident: variant.ident.clone(),
doc: parse_doc_full(&variant.attrs)?,
kind,
exit_code,
payload,
});
}
Ok(ToolErrorIr {
enum_ident: input.ident.clone(),
variants,
})
}
fn parse_payload(fields: &Fields) -> Result<ToolErrorPayloadIr, Error> {
match fields {
Fields::Unit => Ok(ToolErrorPayloadIr::None {
style: ToolErrorNoPayloadStyleIr::Unit,
}),
Fields::Unnamed(f) if f.unnamed.is_empty() => Ok(ToolErrorPayloadIr::None {
style: ToolErrorNoPayloadStyleIr::Tuple,
}),
Fields::Named(f) if f.named.is_empty() => Ok(ToolErrorPayloadIr::None {
style: ToolErrorNoPayloadStyleIr::Struct,
}),
Fields::Unnamed(f) if f.unnamed.len() == 1 => Ok(ToolErrorPayloadIr::Single {
ty: f.unnamed.first().unwrap().ty.clone(),
field_ident: None,
}),
Fields::Named(f) if f.named.len() == 1 => {
let field = f.named.first().unwrap();
Ok(ToolErrorPayloadIr::Single {
ty: field.ty.clone(),
field_ident: field.ident.clone(),
})
}
other => Err(Error::new(
other.span(),
"a ToolError variant may have at most one field; wrap multiple values in a struct or tuple type",
)),
}
}
fn no_payload_pattern(
variant_ident: &syn::Ident,
style: ToolErrorNoPayloadStyleIr,
) -> proc_macro2::TokenStream {
match style {
ToolErrorNoPayloadStyleIr::Unit => quote! { Self::#variant_ident },
ToolErrorNoPayloadStyleIr::Tuple => quote! { Self::#variant_ident() },
ToolErrorNoPayloadStyleIr::Struct => quote! { Self::#variant_ident {} },
}
}
fn no_payload_constructor(
variant_ident: &syn::Ident,
style: ToolErrorNoPayloadStyleIr,
) -> proc_macro2::TokenStream {
match style {
ToolErrorNoPayloadStyleIr::Unit => quote! { Self::#variant_ident },
ToolErrorNoPayloadStyleIr::Tuple => quote! { Self::#variant_ident() },
ToolErrorNoPayloadStyleIr::Struct => quote! { Self::#variant_ident {} },
}
}
fn find_example_attr(attrs: &[Attribute]) -> Option<&Attribute> {
attrs.iter().find(|a| a.path().is_ident("example"))
}
fn find_tool_error_attr(attrs: &[Attribute]) -> Result<Option<&Attribute>, Error> {
let mut found: Option<&Attribute> = None;
for attr in attrs {
if attr.path().is_ident("tool_error") {
if found.is_some() {
return Err(Error::new(
attr.span(),
"duplicate #[tool_error(...)]; a variant may have at most one",
));
}
found = Some(attr);
}
}
Ok(found)
}
fn parse_tool_error_attr(attr: &Attribute) -> Result<(ErrorKindIr, u8), Error> {
use syn::punctuated::Punctuated;
let parser = Punctuated::<Expr, syn::Token![,]>::parse_terminated;
let exprs = attr.parse_args_with(parser)?;
let mut kind = None;
let mut exit_code = None;
let mut seen = SeenKeys::default();
for expr in exprs.iter() {
let Expr::Assign(assign) = expr else {
return Err(Error::new(
expr.span(),
"expected `kind = \"...\"` and `exit_code = N`",
));
};
let key = assign_left_ident(&assign.left)?;
seen.insert(&key)?;
match key.to_string().as_str() {
"kind" => kind = Some(parse_kind(&assign.right)?),
"exit_code" => exit_code = Some(expr_u8(&assign.right, "exit_code")?),
other => {
return Err(Error::new(
key.span(),
format!("unknown #[tool_error] key `{other}`"),
));
}
}
}
let kind = kind.ok_or_else(|| Error::new(attr.span(), "#[tool_error] is missing `kind`"))?;
let exit_code =
exit_code.ok_or_else(|| Error::new(attr.span(), "#[tool_error] is missing `exit_code`"))?;
Ok((kind, exit_code))
}
fn parse_kind(expr: &Expr) -> Result<ErrorKindIr, Error> {
match expr_str(expr, "kind")?.as_str() {
"usage" | "usage-error" => Ok(ErrorKindIr::UsageError),
"runtime" | "runtime-error" => Ok(ErrorKindIr::RuntimeError),
other => Err(Error::new(
expr.span(),
format!("invalid error kind `{other}`; expected `usage-error` or `runtime-error`"),
)),
}
}
fn assign_left_ident(left: &Expr) -> Result<syn::Ident, Error> {
if let Expr::Path(p) = left
&& let Some(ident) = p.path.get_ident()
{
return Ok(ident.clone());
}
Err(Error::new(
left.span(),
"expected an identifier on the left of `=`",
))
}
#[cfg(test)]
mod tests {
use super::*;
use test_r::test;
fn parse(src: &str) -> Result<ToolErrorIr, Error> {
let input: DeriveInput = syn::parse_str(src).unwrap();
parse_tool_error(&input)
}
#[test]
fn direct_error_encoding_is_only_emitted_for_guest_sdk() {
let ir = parse(
r#"enum Failure {
#[tool_error(kind = "usage-error", exit_code = 2)] Reason(String),
#[tool_error(kind = "runtime-error", exit_code = 1)] Empty,
}"#,
)
.unwrap();
let guest = synthesize_tool_error(&ir, true).to_string();
let native = synthesize_tool_error(&ir, false).to_string();
assert!(guest.contains("DirectToolError"));
assert!(guest.contains("encode_direct_tool_value"));
assert!(!native.contains("DirectToolError"));
assert!(!native.contains("encode_direct_tool_value"));
assert!(native.contains("ToolErrorSchema"));
}
#[test]
fn parses_variants() {
let ir = parse(
r#"
enum GrepError {
#[tool_error(kind = "usage-error", exit_code = 2)]
BadPattern(String),
#[tool_error(kind = "runtime-error", exit_code = 1)]
Io(String),
}
"#,
)
.unwrap();
assert_eq!(ir.enum_ident.to_string(), "GrepError");
assert_eq!(ir.variants.len(), 2);
assert_eq!(ir.variants[0].variant_ident.to_string(), "BadPattern");
assert_eq!(ir.variants[0].kind, ErrorKindIr::UsageError);
assert_eq!(ir.variants[0].exit_code, 2);
assert_eq!(ir.variants[1].kind, ErrorKindIr::RuntimeError);
assert_eq!(ir.variants[1].exit_code, 1);
assert!(matches!(
ir.variants[0].payload,
ToolErrorPayloadIr::Single { .. }
));
}
#[test]
fn payload_shapes() {
let ir = parse(
r#"
enum E {
#[tool_error(kind = "usage-error", exit_code = 1)]
Unit,
#[tool_error(kind = "usage-error", exit_code = 1)]
Tuple(String),
#[tool_error(kind = "usage-error", exit_code = 1)]
Named { reason: String },
}
"#,
)
.unwrap();
assert_eq!(
ir.variants[0].payload,
ToolErrorPayloadIr::None {
style: ToolErrorNoPayloadStyleIr::Unit,
}
);
assert!(matches!(
ir.variants[1].payload,
ToolErrorPayloadIr::Single { .. }
));
assert!(matches!(
ir.variants[2].payload,
ToolErrorPayloadIr::Single { .. }
));
}
#[test]
fn multi_field_variant_is_error() {
let err = parse(
r#"
enum E {
#[tool_error(kind = "usage-error", exit_code = 1)]
Bad { a: String, b: u32 },
}
"#,
)
.unwrap_err();
assert!(err.to_string().contains("at most one field"));
}
#[test]
fn legacy_kind_spellings() {
let ir = parse(
r#"
enum E {
#[tool_error(kind = "usage", exit_code = 129)]
A,
#[tool_error(kind = "runtime", exit_code = 128)]
B,
}
"#,
)
.unwrap();
assert_eq!(ir.variants[0].kind, ErrorKindIr::UsageError);
assert_eq!(ir.variants[1].kind, ErrorKindIr::RuntimeError);
}
#[test]
fn variant_doc_is_captured() {
let ir = parse(
r#"
enum E {
/// The pattern was bad.
#[tool_error(kind = "usage-error", exit_code = 2)]
BadPattern,
}
"#,
)
.unwrap();
assert_eq!(ir.variants[0].doc.summary, "The pattern was bad.");
}
#[test]
fn missing_attr_is_error() {
let err = parse(
r#"
enum E {
A,
}
"#,
)
.unwrap_err();
assert!(err.to_string().contains("missing #[tool_error"));
}
#[test]
fn duplicate_tool_error_attr_is_error() {
let err = parse(
r#"
enum E {
#[tool_error(kind = "usage-error", exit_code = 1)]
#[tool_error(kind = "runtime-error", exit_code = 2)]
A,
}
"#,
)
.unwrap_err();
assert!(err.to_string().contains("duplicate") || err.to_string().contains("at most one"));
}
#[test]
fn duplicate_tool_error_key_is_error() {
let err = parse(
r#"
enum E {
#[tool_error(kind = "usage-error", kind = "runtime-error", exit_code = 1)]
A,
}
"#,
)
.unwrap_err();
assert!(err.to_string().contains("duplicate"));
}
#[test]
fn enum_level_tool_error_attr_is_error() {
let err = parse(
r#"
#[tool_error(kind = "runtime-error", exit_code = 9)]
enum E {
#[tool_error(kind = "usage-error", exit_code = 1)]
A,
}
"#,
)
.unwrap_err();
assert!(err.to_string().contains("may only be used on variants"));
}
#[test]
fn field_level_tool_error_attr_is_error() {
let err = parse(
r#"
enum E {
#[tool_error(kind = "usage-error", exit_code = 1)]
A(
#[tool_error(kind = "runtime-error", exit_code = 2)]
String,
),
}
"#,
)
.unwrap_err();
assert!(err.to_string().contains("may only be used on variants"));
}
#[test]
fn misplaced_example_attr_in_tool_error_is_error() {
let enum_level_example_is_accepted = parse(
r#"
#[example(body = "ignored")]
enum E {
#[tool_error(kind = "usage-error", exit_code = 1)]
A,
}
"#,
)
.is_ok();
let field_level_example_is_accepted = parse(
r#"
enum E {
#[tool_error(kind = "usage-error", exit_code = 1)]
A(
#[example(body = "ignored")]
String,
),
}
"#,
)
.is_ok();
assert!(
!enum_level_example_is_accepted && !field_level_example_is_accepted,
"#[example] is accepted and silently ignored on ToolError enum/field positions"
);
}
#[test]
fn non_enum_is_error() {
let err = parse(r#"struct S { x: u32 }"#).unwrap_err();
assert!(err.to_string().contains("can only be applied to enums"));
}
#[test]
fn invalid_kind_is_error() {
let err = parse(
r#"
enum E {
#[tool_error(kind = "bogus", exit_code = 1)]
A,
}
"#,
)
.unwrap_err();
assert!(err.to_string().contains("invalid error kind"));
}
}