redactable-derive 0.7.1

Derive macros for the redactable crate
Documentation
//! Shared field transformation logic for struct and enum derivation.
//!
//! This module extracts the common code for generating field transformations,
//! which was previously duplicated between `derive_struct` and `derive_enum`.

use proc_macro2::{Ident, Span, TokenStream};
use quote::quote_spanned;
use syn::Result;

use crate::{
    crate_path,
    generics::collect_generics_from_type,
    strategy::Strategy,
    types::{is_ip_address_type, is_nonzero_type, is_phantom_data, is_scalar_type},
};

/// Accumulated state during field processing.
///
/// This struct groups the mutable vectors that collect generics and output tokens
/// during traversal of struct fields or enum variants.
pub(crate) struct DeriveContext<'a> {
    pub(crate) generics: &'a syn::Generics,
    pub(crate) container_path: &'a TokenStream,
    pub(crate) used_generics: &'a mut Vec<Ident>,
    pub(crate) policy_applicable_generics: &'a mut Vec<Ident>,
    pub(crate) debug_redacted_generics: &'a mut Vec<Ident>,
    pub(crate) debug_unredacted_generics: &'a mut Vec<Ident>,
}

/// Checks if a policy path refers to the `Secret` policy.
fn is_secret_policy(path: &syn::Path) -> bool {
    path.is_ident("Secret")
}

fn is_ip_address_policy(path: &syn::Path) -> bool {
    path.is_ident("IpAddress")
}

fn nonzero_policy_error(span: Span) -> syn::Error {
    syn::Error::new(
        span,
        "NonZero integer fields cannot use #[sensitive(Policy)] because redaction \
         may need to produce zero; use a nullable scalar or a policy-aware wrapper",
    )
}

/// Generates the transform token stream for a single field.
///
/// ## Field Transformation Rules
///
/// | Annotation              | Behavior                                             |
/// |-------------------------|------------------------------------------------------|
/// | None                    | Walk containers, scalars pass through                |
/// | `#[sensitive(Secret)]`  | Scalars redact to default; strings to "[REDACTED]"   |
/// | `#[sensitive(Policy)]`  | Apply policy recursively through wrappers            |
/// | `#[not_sensitive]`      | Explicit passthrough (no transformation)             |
pub(crate) fn generate_field_transform(
    ctx: &mut DeriveContext<'_>,
    ty: &syn::Type,
    binding: &Ident,
    span: Span,
    strategy: &Strategy,
) -> Result<TokenStream> {
    let container_path = ctx.container_path;

    match strategy {
        Strategy::WalkDefault => {
            // PhantomData<T> is a zero-sized marker that never contains data.
            // Scalars are primitive types that pass through unchanged.
            // Both cases: return empty token stream (passthrough) without collecting generics.
            if is_phantom_data(ty) || is_scalar_type(ty) {
                Ok(TokenStream::new())
            } else {
                collect_generics_from_type(ty, ctx.generics, ctx.used_generics);
                collect_generics_from_type(ty, ctx.generics, ctx.debug_redacted_generics);
                collect_generics_from_type(ty, ctx.generics, ctx.debug_unredacted_generics);
                Ok(quote_spanned! { span =>
                    let #binding = #container_path::redact_with(#binding, mapper);
                })
            }
        }
        Strategy::NotSensitive => {
            // Explicit opt-out: no transformation, passthrough unchanged.
            // This is useful for foreign types that don't implement RedactableWithMapper.
            // Still collect debug generics: the field is printed in generated Debug impls
            // even though it's not transformed, so its type needs a Debug bound.
            collect_generics_from_type(ty, ctx.generics, ctx.debug_redacted_generics);
            collect_generics_from_type(ty, ctx.generics, ctx.debug_unredacted_generics);
            Ok(TokenStream::new())
        }
        Strategy::Policy(policy_path) => {
            if is_nonzero_type(ty) {
                return Err(nonzero_policy_error(span));
            }
            if is_ip_address_type(ty) {
                if !is_ip_address_policy(policy_path) {
                    return Err(syn::Error::new(
                        span,
                        "IP address fields can only use #[sensitive(IpAddress)]",
                    ));
                }
                let policy = policy_path.clone();
                let sensitive_with_policy_path = crate_path("SensitiveWithPolicy");
                let redaction_policy_path = crate_path("RedactionPolicy");
                return Ok(quote_spanned! { span =>
                    let #binding = <#ty as #sensitive_with_policy_path<#policy>>::redact_with_policy(
                        #binding,
                        &<#policy as #redaction_policy_path>::policy(),
                    );
                });
            }
            if is_scalar_type(ty) {
                if is_secret_policy(policy_path) {
                    Ok(quote_spanned! { span =>
                        let #binding = mapper.map_scalar(#binding);
                    })
                } else {
                    Err(syn::Error::new(
                        span,
                        "scalar fields can only use #[sensitive(Secret)]; \
                         other policies are for string-like types",
                    ))
                }
            } else {
                collect_generics_from_type(ty, ctx.generics, ctx.policy_applicable_generics);
                collect_generics_from_type(ty, ctx.generics, ctx.debug_unredacted_generics);
                let policy = policy_path.clone();
                let policy_applicable_path = crate_path("PolicyApplicable");
                Ok(quote_spanned! { span =>
                    let #binding = #policy_applicable_path::apply_policy::<#policy, _>(#binding, mapper);
                })
            }
        }
    }
}