apollo-errors-derive 0.4.0

Proc macro for deriving apollo-errors::Error trait
Documentation
//! Field parsing

use syn::{Attribute, Result, Type};

use super::http_header::parse_http_header;
use crate::ir::{FieldDefinition, TransparentFieldDefinition};

/// Parse a field
pub(crate) fn parse_field(field: syn::Field) -> Result<FieldDefinition> {
    // Check if field has #[extension] attribute
    let is_extension = has_attribute(&field.attrs, "extension");

    // Check if field has #[from] attribute (implies #[source])
    let is_from = has_attribute(&field.attrs, "from");

    // Check if field has #[source] attribute (or #[from] which implies #[source])
    let is_source = has_attribute(&field.attrs, "source") || is_from;

    // Parse #[http_header("...")] attribute
    let http_header = parse_http_header(&field.attrs)?;

    let rust_name = field
        .ident
        .clone()
        .ok_or_else(|| syn::Error::new_spanned(&field, "fields must be named"))?;

    let output_name = rust_name.to_string();
    let is_option = is_option_type(&field.ty);

    Ok(FieldDefinition {
        rust_name,
        output_name,
        ty: field.ty,
        is_extension,
        is_source,
        is_from,
        is_option,
        http_header,
    })
}

pub(crate) fn parse_transparent_field(field: syn::Field) -> Result<TransparentFieldDefinition> {
    if field.ident.is_some() {
        unreachable!("parse_transparent_field() can only be called on unnamed fields");
    }

    if let Some(attr) = field
        .attrs
        .iter()
        .find(|attr| attr.path().is_ident("source"))
    {
        return Err(syn::Error::new_spanned(
            attr,
            "#[source] attribute is not necessary on transparent variants",
        ));
    }

    Ok(TransparentFieldDefinition {
        ty: field.ty,
        is_from: has_attribute(&field.attrs, "from"),
    })
}

/// Check if an attribute list contains a specific attribute
fn has_attribute(attrs: &[Attribute], name: &str) -> bool {
    attrs.iter().any(|attr| attr.path().is_ident(name))
}

/// Check if a type is `Option<T>`.
/// Handles: `Option<T>`, `std::option::Option<T>`, `core::option::Option<T>`
fn is_option_type(ty: &Type) -> bool {
    if let Type::Path(type_path) = ty {
        let path = &type_path.path;

        // Check for Option, std::option::Option, or core::option::Option
        path.segments
            .last()
            .is_some_and(|seg| seg.ident == "Option")
            && (path.segments.len() == 1
                || (path.segments.len() == 3
                    && (path.segments[0].ident == "std" || path.segments[0].ident == "core")
                    && path.segments[1].ident == "option"))
    } else {
        false
    }
}