dterror-derive 0.1.0

Derive macro for the dterror FromContext trait
Documentation
//! Field-level marker classification and `#[context(borrow = ...)]` parsing, shared by the
//! struct and enum expansion paths.

use syn::{Attribute, Field, Ident, Type};

/// The role a field plays when constructing the error from its context.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum FieldRole {
    /// Assigned the `source` argument of `from_context`.
    Source,
    /// Assigned the `location` argument of `from_context`.
    Location,
    /// Lives on the generated `{Error}Ctx` struct.
    Context,
}

impl FieldRole {
    pub(crate) fn classify(field: &Field) -> syn::Result<FieldRole> {
        let is_source = field
            .attrs
            .iter()
            .any(|attr| attr.path().is_ident("source") || attr.path().is_ident("from"));
        let is_location = field
            .attrs
            .iter()
            .any(|attr| attr.path().is_ident("location"));

        if (is_source || is_location)
            && field
                .attrs
                .iter()
                .any(|attr| attr.path().is_ident("context"))
        {
            return Err(syn::Error::new_spanned(
                field,
                "`#[context(...)]` cannot be combined with a `#[source]`/`#[from]` or \
                 `#[location]` marker",
            ));
        }

        match (is_source, is_location) {
            (true, true) => Err(syn::Error::new_spanned(
                field,
                "a field cannot carry both a `#[source]`/`#[from]` marker and a `#[location]` marker",
            )),
            (true, false) => Ok(FieldRole::Source),
            (false, true) => Ok(FieldRole::Location),
            (false, false) => Ok(FieldRole::Context),
        }
    }
}

/// A context field paired with optional borrow metadata parsed from `#[context(borrow = "...")]`.
pub(crate) struct CtxField<'a> {
    /// The original syn field definition.
    field: &'a Field,
    /// If `Some`, the generated `{Error}Ctx` stores `&'ctx TargetType` instead of owning;
    /// the value is converted back to owned via `.into()` inside `from_context`.
    pub(crate) borrow_target: Option<Type>,
}

impl<'a> CtxField<'a> {
    pub(crate) fn parse_borrow(field: &'a Field) -> syn::Result<Self> {
        let context_attrs: Vec<&Attribute> = field
            .attrs
            .iter()
            .filter(|attr| attr.path().is_ident("context"))
            .collect();

        if let Some(duplicate) = context_attrs.get(1) {
            return Err(syn::Error::new_spanned(
                duplicate,
                "a field may carry at most one `#[context(...)]` attribute",
            ));
        }

        let mut borrow_target = None;

        for attr in &context_attrs {
            attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("borrow") {
                    if borrow_target.is_some() {
                        return Err(
                            meta.error("duplicate `#[context(borrow = ...)]` on the same field")
                        );
                    }
                    let Ok(value_stream) = meta.value() else {
                        return Err(meta.error(
                            "`#[context(borrow)]` requires an explicit target type, e.g. \
                             `#[context(borrow = Path)]`",
                        ));
                    };
                    borrow_target = Some(value_stream.parse::<Type>()?);
                } else {
                    return Err(meta.error("unknown `#[context(...)]` helper; expected `borrow`"));
                }
                Ok(())
            })?;
        }

        Ok(Self {
            field,
            borrow_target,
        })
    }

    pub(crate) fn ident(&self) -> Option<&Ident> {
        self.field.ident.as_ref()
    }

    pub(crate) fn ty(&self) -> &Type {
        &self.field.ty
    }

    pub(crate) fn vis(&self) -> &syn::Visibility {
        &self.field.vis
    }
}

/// The classified fields of one scope: a struct or an enum variant.
pub(crate) struct ScopeFields<'a> {
    /// The field marked `#[source]`/`#[from]`, if any.
    pub(crate) source_field: Option<&'a Field>,
    /// The field marked `#[location]`, if any.
    pub(crate) location_field: Option<&'a Field>,
    /// The context fields, in declaration order.
    pub(crate) context_fields: Vec<CtxField<'a>>,
}

impl<'a> ScopeFields<'a> {
    /// Classify every named field of the scope, enforcing the per-scope marker rules.
    pub(crate) fn classify(fields: impl IntoIterator<Item = &'a Field>) -> syn::Result<Self> {
        let mut source_field: Option<&Field> = None;
        let mut location_field: Option<&Field> = None;
        let mut context_fields: Vec<CtxField> = Vec::new();

        for field in fields {
            match FieldRole::classify(field)? {
                FieldRole::Source => {
                    if source_field.is_some() {
                        return Err(syn::Error::new_spanned(
                            field,
                            "at most one field may be marked `#[source]`/`#[from]`",
                        ));
                    }
                    source_field = Some(field);
                }
                FieldRole::Location => {
                    if location_field.is_some() {
                        return Err(syn::Error::new_spanned(
                            field,
                            "at most one field may be marked `#[location]`",
                        ));
                    }
                    location_field = Some(field);
                }
                FieldRole::Context => context_fields.push(CtxField::parse_borrow(field)?),
            }
        }

        Ok(Self {
            source_field,
            location_field,
            context_fields,
        })
    }
}