field_name 0.2.0

A proc-macro for exposing a struct's field names at runtime.
Documentation
use std::collections::HashSet;
use field_name::FieldNames;

#[derive(FieldNames)]
struct DbModel {
    id: u64,
    user_email: String,
    created_at: String,
    #[field_name(skip)] // Database internal field
    row_version: u32,
}

#[derive(FieldNames)]
struct JsonApiDto {
    id: u64,
    // The API uses camelCase, but the DB uses snake_case.
    // We want to ensure the *field exists*, even if the name on wire differs.
    #[field_name(rename = "userEmail")]
    user_email: String,

    #[field_name(rename = "createdAt")]
    created_at: String,
}

/// This test ensures that the `DbModel` and `JsonApiDto` have the same logical fields,
/// specifically checking the individual constant *names* (via code usage) and
/// checking field counts.
#[test]
fn ensure_db_and_api_models_are_aligned() {
    // 1. Check Field Counts (Accounting for skipped internal fields)
    // DbModel has 4 fields, 1 skipped = 3 exposed.
    // JsonApiDto has 3 fields.
    assert_eq!(DbModel::FIELDS.len(), JsonApiDto::FIELDS.len());

    // 2. Compile-Time Refactor Safety Check
    // If someone renames `user_email` to `email` in `DbModel` but forgets `JsonApiDto`,
    // the following line will stop compiling if you change the macro to also
    // generate a "verify" function, or we can simply rely on the existence of constants.

    // We can simulate a "mapping check".
    // We assert that for every key field in DB, we have a corresponding key in API.
    // Since the values differ ("user_email" vs "userEmail"), we can't compare the arrays directly.
    // However, we can compare the generated Constant Names if we write a macro helper,
    // OR we can manually verify the mapping here:

    let _ = DbModel::USER_EMAIL;
    let _ = JsonApiDto::USER_EMAIL; // If field is removed from DTO, this fails to compile.

    let _ = DbModel::CREATED_AT;
    let _ = JsonApiDto::CREATED_AT; // If field is removed from DTO, this fails to compile.
}

/// This test enforces that two structs meant to be identical (e.g., Raw vs Validated)
/// have the exact same serialized field names.
#[test]
fn ensure_raw_and_validated_structs_match_exact_wire_format() {
    #[derive(FieldNames)]
    struct ValidatedUser {
        #[field_name(rename = "uuid")]
        id: String,
        username: String,
    }

    #[derive(FieldNames)]
    struct RawUserImport {
        #[field_name(rename = "uuid")] // If this rename is missing, test fails
        id: String,
        username: String,
        #[field_name(skip)]
        extra_garbage: String, // We skip this in check
    }

    let valid_set: HashSet<_> = ValidatedUser::FIELDS.iter().collect();
    let raw_set: HashSet<_> = RawUserImport::FIELDS.iter().collect();

    assert_eq!(valid_set, raw_set, "Raw import struct definition does not match Validated struct wire format!");
}