actix_failwrap 1.0.3

A micro-package that enables ergonomic error propagation (using thiserror) inside actix-web route handlers.
Documentation
//! [`ErrorResponse`] Input Module
//!
//! Declares meta structs that implement
//! parsing methods for the [`ErrorResponse`]
//! macro.

use std::fmt::Write;

use quote::format_ident;
use syn::parse::{Parse, ParseStream};
use syn::{
    Error as SynError,
    Ident,
    ItemEnum,
    LitInt,
    Result as SynResult,
    Variant as EnumVariant,
};

use crate::helpers::status_codes::{
    allowed_status_pairs,
    closest_status,
    code_to_status,
    is_status_supported,
};
use crate::helpers::unique_attr::get_single_attr;

/// **`ErrorResponse`**
///
/// Meta for the error response macro.
#[derive(Debug)]
pub struct ErrorResponse {
    enum_name: Ident,
    default_status_code: Ident,        // by default 500. Dynamic
    transform_response: Option<Ident>, // an onscope reference Fn(HttpStatusCode, &str)
    variants: Vec<ErrorResponseVariant>,
}

/// **`ErrorResponseVariant`**
///
/// Meta for each error response annotated enum variant.
#[derive(Debug)]
pub struct ErrorResponseVariant {
    status_code: Option<Ident>,
    variant: EnumVariant,
}

/// **`StatusCode`**
///
/// Parsed meta for HTTP status codes.
///
/// XXX: Consider abstracting this.
#[derive(Debug)]
pub struct StatusCode(Ident);

impl ErrorResponse {
    /// **`ErrorResponse.enum_name`**
    ///
    /// The enum name for which this macro was applied.
    #[inline]
    pub fn enum_name(&self) -> &Ident {
        &self.enum_name
    }

    /// **`ErrorResponse.default_status_code`**
    ///
    /// The parsed default status code from `#[default_status_code(..)]`
    /// if not found `InternalServerError`.
    #[inline]
    pub fn default_status_code(&self) -> &Ident {
        &self.default_status_code
    }

    /// **`ErrorResponse.transform_response`**
    ///
    /// The provided function identifier to transform the HTTP response
    /// if any.
    #[inline]
    pub const fn transform_response(&self) -> Option<&Ident> {
        self.transform_response
            .as_ref()
    }

    /// **`ErrorResponse.variants`**
    ///
    /// The annotated error enum variants.
    #[inline]
    pub fn variants(&self) -> &[ErrorResponseVariant] {
        &self.variants
    }
}

impl Parse for ErrorResponse {
    fn parse(input: ParseStream) -> SynResult<Self> {
        let input = input.parse::<ItemEnum>()?;

        let enum_name = input.ident;

        let default_status_code = get_single_attr(
            input
                .attrs
                .clone(),
            "default_status_code",
        )?
        .map(|attr| attr.parse_args::<StatusCode>())
        .transpose()?
        .map(StatusCode::into_inner)
        .unwrap_or(format_ident!("InternalServerError"));

        let transform_response = get_single_attr(input.attrs, "transform_response")?
            .map(|attr| attr.parse_args::<Ident>())
            .transpose()?;

        let variants = input
            .variants
            .into_iter()
            .map(|variant| {
                Ok(ErrorResponseVariant {
                    status_code: get_single_attr(
                        variant
                            .attrs
                            .clone(),
                        "status_code",
                    )?
                    .map(|attr| attr.parse_args::<StatusCode>())
                    .transpose()?
                    .map(StatusCode::into_inner),
                    variant,
                })
            })
            .collect::<SynResult<Vec<_>>>()?;

        if variants.is_empty() {
            return Err(SynError::new_spanned(
                enum_name,
                "Expected at least 1 variant in this error type.",
            ));
        }

        Ok(Self {
            enum_name,
            default_status_code,
            transform_response,
            variants,
        })
    }
}

impl ErrorResponseVariant {
    /// **`ErrorResponseVariant.status_code`**
    ///
    /// The status code override for the specific variant,
    /// if None the default should be used.
    #[inline]
    pub const fn status_code(&self) -> Option<&Ident> {
        self.status_code
            .as_ref()
    }

    /// **`ErrorResponseVariant.variant`**
    ///
    /// The original enum variant from the AST.
    #[inline]
    pub fn variant(&self) -> &EnumVariant {
        &self.variant
    }
}

impl StatusCode {
    /// **`StatusCode.into_inner()`**
    ///
    /// Get the parsed HTTP status code as an ident
    /// compatible with appending to `actix_web::HttpResponse::`.
    #[inline]
    pub fn into_inner(self) -> Ident {
        self.0
    }
}

impl Parse for StatusCode {
    fn parse(input: ParseStream) -> SynResult<Self> {
        if let Ok(integer) = input.parse::<LitInt>() {
            let parsed = integer
                .base10_parse::<usize>()
                .map_err(|error| {
                    SynError::new(error.span(), "Expected a usize value for number variant.")
                })?;

            let status = code_to_status(parsed).ok_or_else(|| {
                SynError::new_spanned(
                    integer,
                    format!(
                        concat!(
                            "Only HTTP error status codes are allowed, ",
                            "The allowed status codes are:\n{}"
                        ),
                        allowed_status_pairs()
                            .iter()
                            .fold(String::new(), |mut res, (code, status)| {
                                writeln!(res, "{code} -> {status}").ok();
                                res
                            })
                    ),
                )
            })?;

            return Ok(StatusCode(format_ident!("{status}")));
        }

        if let Ok(ident) = input.parse::<Ident>() {
            let ident_string = ident.to_string();
            return if is_status_supported(&ident_string) {
                Ok(StatusCode(ident))
            } else {
                Err(SynError::new_spanned(
                    ident,
                    format!(
                        concat!(
                            "Only HTTP error statuses are allowed. ",
                            "{} is not a valid status code, did you mean {}?"
                        ),
                        &ident_string,
                        closest_status(&ident_string)
                    ),
                ))
            };
        }

        Err(SynError::new(
            input.span(),
            "Only HTTP status codes (usize) and refrences (Ident) are allowed.",
        ))
    }
}