use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{
parse2, Attribute, Data, DataEnum, DeriveInput, Fields, LitInt, LitStr, Variant as SynVariant,
};
fn err(span: Span, msg: &str, spec_anchor: &str) -> syn::Error {
syn::Error::new(span, format!("taut_rpc: {msg}\n see SPEC §{spec_anchor}"))
}
pub(crate) fn expand(input: TokenStream) -> syn::Result<TokenStream> {
let derive_input: DeriveInput = parse2(input)?;
reject_generics(&derive_input)?;
let ident = derive_input.ident.clone();
let data_enum = match &derive_input.data {
Data::Enum(e) => e,
Data::Struct(s) => {
return Err(err(
s.struct_token.span(),
"#[derive(TautError)] can only be applied to enums",
"3.3",
));
}
Data::Union(u) => {
return Err(err(
u.union_token.span(),
"#[derive(TautError)] can only be applied to enums",
"3.3",
));
}
};
let (code_arms, status_arms) = expand_enum(data_enum)?;
Ok(quote! {
impl ::taut_rpc::TautError for #ident {
fn code(&self) -> &'static str {
match self {
#( #code_arms )*
}
}
fn http_status(&self) -> u16 {
match self {
#( #status_arms )*
}
}
}
})
}
fn reject_generics(input: &DeriveInput) -> syn::Result<()> {
if input.generics.params.is_empty() {
return Ok(());
}
Err(err(
input.generics.span(),
"generic types are not yet supported in v0.1; please monomorphize manually for now",
"3.3",
))
}
fn expand_enum(e: &DataEnum) -> syn::Result<(Vec<TokenStream>, Vec<TokenStream>)> {
let mut code_arms = Vec::with_capacity(e.variants.len());
let mut status_arms = Vec::with_capacity(e.variants.len());
for v in &e.variants {
let attrs = VariantAttrs::parse(&v.attrs)?;
let variant_ident = &v.ident;
let default_code = variant_name_to_default_code(&variant_ident.to_string());
let code = attrs.code.unwrap_or(default_code);
let status: u16 = attrs.status.unwrap_or(400);
let code_lit = LitStr::new(&code, variant_ident.span());
let status_lit = LitInt::new(&status.to_string(), variant_ident.span());
let pattern = variant_pattern(v);
code_arms.push(quote_spanned! {v.span()=>
#pattern => #code_lit,
});
status_arms.push(quote_spanned! {v.span()=>
#pattern => #status_lit,
});
}
Ok((code_arms, status_arms))
}
fn variant_pattern(v: &SynVariant) -> TokenStream {
let ident = &v.ident;
match &v.fields {
Fields::Unit => quote! { Self::#ident },
Fields::Unnamed(_) => quote! { Self::#ident(..) },
Fields::Named(_) => quote! { Self::#ident { .. } },
}
}
#[derive(Debug, Default)]
struct VariantAttrs {
code: Option<String>,
status: Option<u16>,
}
impl VariantAttrs {
fn parse(attrs: &[Attribute]) -> syn::Result<Self> {
let mut out = VariantAttrs::default();
for attr in attrs {
if !attr.path().is_ident("taut") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("code") {
let s: LitStr = meta.value()?.parse()?;
out.code = Some(s.value());
Ok(())
} else if meta.path.is_ident("status") {
let n: LitInt = meta.value()?.parse()?;
let parsed: u16 = n.base10_parse()?;
out.status = Some(parsed);
Ok(())
} else {
consume_foreign(&meta)
}
})?;
}
Ok(out)
}
}
fn consume_foreign(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
let input = meta.input;
if input.peek(syn::Token![=]) {
let value = meta.value()?;
let _: syn::Expr = value.parse()?;
Ok(())
} else if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
let _: proc_macro2::TokenStream = content.parse()?;
Ok(())
} else {
Ok(())
}
}
fn variant_name_to_default_code(name: &str) -> String {
let mut out = String::new();
for (i, ch) in name.chars().enumerate() {
if ch.is_uppercase() && i > 0 {
out.push('_');
}
out.extend(ch.to_lowercase());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
#[test]
fn variant_name_to_default_code_handles_camel_case() {
assert_eq!(variant_name_to_default_code("NotFound"), "not_found");
}
#[test]
fn variant_name_to_default_code_handles_runs_of_uppercase() {
assert_eq!(variant_name_to_default_code("ABC"), "a_b_c");
}
#[test]
fn variant_name_to_default_code_passes_through_lowercase() {
assert_eq!(variant_name_to_default_code("lowercase"), "lowercase");
}
#[test]
fn variant_attrs_parses_code_and_status() {
let attrs: Vec<Attribute> = vec![parse_quote!(#[taut(code = "x", status = 401)])];
let parsed = VariantAttrs::parse(&attrs).expect("parse");
assert_eq!(parsed.code.as_deref(), Some("x"));
assert_eq!(parsed.status, Some(401));
}
#[test]
fn variant_attrs_silently_consumes_foreign_keys() {
let attrs: Vec<Attribute> = vec![
parse_quote!(#[taut(rename = "Other", code = "x", status = 401)]),
parse_quote!(#[taut(unknown_future_key)]),
parse_quote!(#[taut(length(min = 1, max = 10))]),
];
let parsed = VariantAttrs::parse(&attrs).expect("foreign keys must be consumed");
assert_eq!(parsed.code.as_deref(), Some("x"));
assert_eq!(parsed.status, Some(401));
}
#[test]
fn expand_handles_phase5_combined_attrs_example() {
let input: TokenStream = quote! {
enum AppError {
#[taut(status = 401, code = "auth_required")]
NotAuthed,
#[taut(status = 400)]
BadInput { #[taut(length(min = 1))] msg: String },
}
};
let out = expand(input).expect("expansion must succeed").to_string();
assert!(
out.contains("\"auth_required\""),
"missing override code: {out}"
);
assert!(out.contains("401"), "missing override status: {out}");
assert!(
out.contains("\"bad_input\""),
"missing default snake_case code for BadInput: {out}"
);
assert!(out.contains("400"), "missing default status: {out}");
}
#[test]
fn expand_emits_expected_match_arms() {
let input: TokenStream = quote! {
enum E {
A,
#[taut(code = "auth_required", status = 401)]
B(u32),
C { x: u64 },
}
};
let out = expand(input).expect("expansion succeeds").to_string();
assert!(
out.contains("impl :: taut_rpc :: TautError for E"),
"missing impl header in: {out}"
);
assert!(out.contains("\"a\""), "missing default code 'a': {out}");
assert!(
out.contains("\"auth_required\""),
"missing override code 'auth_required': {out}"
);
assert!(out.contains("\"c\""), "missing default code 'c': {out}");
assert!(out.contains("400"), "missing default status 400: {out}");
assert!(out.contains("401"), "missing override status 401: {out}");
assert!(
out.contains("Self :: A"),
"missing unit-variant pattern: {out}"
);
assert!(
out.contains("Self :: B (..)"),
"missing tuple-variant pattern: {out}"
);
assert!(
out.contains("Self :: C { .. }"),
"missing struct-variant pattern: {out}"
);
}
#[test]
fn expand_rejects_struct() {
let input: TokenStream = quote! {
struct S { x: u64 }
};
let err = expand(input).expect_err("structs must be rejected");
let msg = err.to_string();
assert!(
msg.contains("can only be applied to enums"),
"error message was: {msg}"
);
}
#[test]
fn expand_rejects_generics() {
let input: TokenStream = quote! {
enum E<T> { A(T) }
};
let err = expand(input).expect_err("generics must be rejected");
let msg = err.to_string();
assert!(
msg.contains("generic types are not yet supported"),
"error message was: {msg}"
);
}
}