orion-error-derive 0.8.0

Derive macros for orion-error
Documentation
#[derive(Default)]
struct OrionAttrs {
    message: Option<Expr>,
    code: Option<Expr>,
    identity: Option<Expr>,
    category: Option<TokenStream2>,
    transparent: bool,
}

impl OrionAttrs {
    fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
        let mut out = Self::default();
        for attr in attrs {
            if !attr.path().is_ident("orion_error") {
                continue;
            }

            attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("transparent") {
                    out.transparent = true;
                    return Ok(());
                }

                if meta.path.is_ident("code") {
                    out.code = Some(meta.value()?.parse()?);
                    return Ok(());
                }

                if meta.path.is_ident("message") {
                    out.message = Some(meta.value()?.parse()?);
                    return Ok(());
                }

                if meta.path.is_ident("identity") {
                    out.identity = Some(meta.value()?.parse()?);
                    return Ok(());
                }

                if meta.path.is_ident("category") {
                    let expr: Expr = meta.value()?.parse()?;
                    out.category = Some(category_expr(expr)?);
                    return Ok(());
                }

                Err(meta.error("unsupported orion_error attribute"))
            })?;
        }
        Ok(out)
    }

    fn display_message(&self) -> Option<LitStr> {
        self.message
            .as_ref()
            .and_then(expr_lit_str)
            .cloned()
            .or_else(|| {
                self.identity
                    .as_ref()
                    .and_then(expr_lit_str)
                    .map(message_from_identity)
            })
    }

    fn error_category(&self) -> Result<Option<TokenStream2>> {
        if let Some(category) = self.category.clone() {
            return Ok(Some(category));
        }

        let Some(identity) = self.identity.as_ref().and_then(expr_lit_str) else {
            return Ok(None);
        };

        identity_category(identity).transpose()
    }
}

fn expr_lit_str(expr: &Expr) -> Option<&LitStr> {
    match expr {
        Expr::Lit(ExprLit {
            lit: Lit::Str(lit), ..
        }) => Some(lit),
        _ => None,
    }
}

fn message_from_identity(identity: &LitStr) -> LitStr {
    let message = identity
        .value()
        .rsplit('.')
        .next()
        .unwrap_or_default()
        .replace('_', " ");
    LitStr::new(&message, identity.span())
}

fn identity_category(identity: &LitStr) -> Option<Result<TokenStream2>> {
    let value = identity.value();
    let prefix = value.split('.').next().unwrap_or_default();
    match prefix {
        "conf" => Some(Ok(quote! { ::orion_error::reason::ErrorCategory::Conf })),
        "biz" => Some(Ok(quote! { ::orion_error::reason::ErrorCategory::Biz })),
        "logic" => Some(Ok(quote! { ::orion_error::reason::ErrorCategory::Logic })),
        "sys" => Some(Ok(quote! { ::orion_error::reason::ErrorCategory::Sys })),
        value => Some(Err(Error::new(
            identity.span(),
            format!(
                "unknown identity category prefix `{value}`; expected one of: conf, biz, logic, sys"
            ),
        ))),
    }
}

fn category_expr(expr: Expr) -> Result<TokenStream2> {
    match expr {
        Expr::Lit(ExprLit {
            lit: Lit::Str(lit), ..
        }) => match lit.value().as_str() {
            "conf" => Ok(quote! { ::orion_error::reason::ErrorCategory::Conf }),
            "biz" => Ok(quote! { ::orion_error::reason::ErrorCategory::Biz }),
            "logic" => Ok(quote! { ::orion_error::reason::ErrorCategory::Logic }),
            "sys" => Ok(quote! { ::orion_error::reason::ErrorCategory::Sys }),
            value => Err(Error::new(
                lit.span(),
                format!("unknown error category `{value}`; expected one of: conf, biz, logic, sys"),
            )),
        },
        Expr::Path(ExprPath { path, .. })
            if path.leading_colon.is_none() && path.segments.len() == 1 =>
        {
            let ident = &path.segments[0].ident;
            match ident.to_string().as_str() {
                "Conf" => Ok(quote! { ::orion_error::reason::ErrorCategory::Conf }),
                "Biz" => Ok(quote! { ::orion_error::reason::ErrorCategory::Biz }),
                "Logic" => Ok(quote! { ::orion_error::reason::ErrorCategory::Logic }),
                "Sys" => Ok(quote! { ::orion_error::reason::ErrorCategory::Sys }),
                _ => Ok(path.to_token_stream()),
            }
        }
        other => Ok(other.to_token_stream()),
    }
}