How does heck::ToShoutySnakeCase transform identifiers for constant naming conventions?

ToShoutySnakeCase converts identifiers to SHOUTY_SNAKE_CASE—uppercase letters with underscores between words—making it ideal for generating Rust constant names, configuration keys, or any identifier following the convention that "constants should be SCREAMING_SNAKE_CASE". The trait handles multiple input formats including snake_case, camelCase, PascalCase, and kebab-case, automatically detecting word boundaries and transforming them into the uppercase underscore-separated format. This is particularly useful in code generation, macros, and configuration systems where you need to programmatically generate conventional constant names from various identifier styles.

Basic Transformation

use heck::ToShoutySnakeCase;
 
fn main() {
    // Convert from various input formats
    let snake = "user_account_id".to_shouty_snake_case();
    let camel = "userAccountId".to_shouty_snake_case();
    let pascal = "UserAccountId".to_shouty_snake_case();
    let kebab = "user-account-id".to_shouty_snake_case();
    
    // All produce the same output
    assert_eq!(snake, "USER_ACCOUNT_ID");
    assert_eq!(camel, "USER_ACCOUNT_ID");
    assert_eq!(pascal, "USER_ACCOUNT_ID");
    assert_eq!(kebab, "USER_ACCOUNT_ID");
    
    println!("{}", snake);  // USER_ACCOUNT_ID
}

ToShoutySnakeCase detects word boundaries automatically regardless of input style.

The Trait Definition

use heck::ToShoutySnakeCase;
 
fn main() {
    // ToShoutySnakeCase is a trait that extends str
    // It provides the to_shouty_snake_case() method
    
    // Works on &str
    let name: &str = "myConstantName";
    let shouty: String = name.to_shouty_snake_case();
    
    // Works on String
    let owned: String = String::from("myConstantName");
    let shouty: String = owned.to_shouty_snake_case();
    
    // Returns an owned String
    let result: String = "test".to_shouty_snake_case();
    
    println!("{}", result);
}

The trait is implemented for str and String, returning an owned String.

Word Boundary Detection

use heck::ToShoutySnakeCase;
 
fn main() {
    // Underscores are word boundaries
    assert_eq!("hello_world".to_shouty_snake_case(), "HELLO_WORLD");
    
    // CamelCase: uppercase letters start new words
    assert_eq!("helloWorld".to_shouty_snake_case(), "HELLO_WORLD");
    assert_eq!("XMLHttpRequest".to_shouty_snake_case(), "XML_HTTP_REQUEST");
    
    // PascalCase: similar to camelCase
    assert_eq!("HelloWorld".to_shouty_snake_case(), "HELLO_WORLD");
    
    // Kebab-case: hyphens are word boundaries
    assert_eq!("hello-world".to_shouty_snake_case(), "HELLO_WORLD");
    
    // Numbers are handled specially
    assert_eq!("version2Update".to_shouty_snake_case(), "VERSION_2_UPDATE");
    assert_eq!("http2_protocol".to_shouty_snake_case(), "HTTP_2_PROTOCOL");
    
    // Multiple consecutive separators
    assert_eq!("hello__world".to_shouty_snake_case(), "HELLO_WORLD");
    assert_eq!("hello--world".to_shouty_snake_case(), "HELLO_WORLD");
}

The transformation handles various conventions and edge cases.

Use in Procedural Macros

use heck::ToShoutySnakeCase;
 
// Simulating a derive macro that generates constants
fn generate_constants(struct_name: &str, field_names: &[&str]) -> String {
    let mut output = String::new();
    
    // Generate constant for struct name prefix
    let prefix = struct_name.to_shouty_snake_case();
    
    // Generate constants for each field
    for field in field_names {
        let const_name = format!("{}_{}", prefix, field).to_shouty_snake_case();
        output.push_str(&format!(
            "pub const {}: &str = \"{}.{}\";\n",
            const_name, struct_name, field
        ));
    }
    
    output
}
 
fn main() {
    let code = generate_constants("UserAccount", &["userId", "accountName", "createdAt"]);
    println!("{}", code);
    // Output:
    // pub const USER_ACCOUNT_USER_ID: &str = "UserAccount.userId";
    // pub const USER_ACCOUNT_ACCOUNT_NAME: &str = "UserAccount.accountName";
    // pub const USER_ACCOUNT_CREATED_AT: &str = "UserAccount.createdAt";
}

Procedural macros commonly use ToShoutySnakeCase for generating constant names.

Generating Configuration Keys

use heck::ToShoutySnakeCase;
 
fn main() {
    // Environment variable names use SHOUTY_SNAKE_CASE
    let config_keys = vec![
        "databaseUrl",
        "maxConnections",
        "sessionTimeout",
        "enableSsl",
    ];
    
    for key in config_keys {
        let env_var = key.to_shouty_snake_case();
        println!("export {}=<value>", env_var);
    }
    // Output:
    // export DATABASE_URL=<value>
    // export MAX_CONNECTIONS=<value>
    // export SESSION_TIMEOUT=<value>
    // export ENABLE_SSL=<value>
}

Environment variable naming follows the SHOUTY_SNAKE_CASE convention.

Related heck Traits

use heck::{
    ToShoutySnakeCase,
    ToSnakeCase,
    ToLowerCamelCase,
    ToUpperCamelCase,
    ToKebabCase,
    ToShoutyKebabCase,
};
 
fn main() {
    let input = "userAccountId";
    
    // SHOUTY_SNAKE_CASE: uppercase with underscores
    println!("shouty_snake: {}", input.to_shouty_snake_case());
    // USER_ACCOUNT_ID
    
    // snake_case: lowercase with underscores
    println!("snake: {}", input.to_snake_case());
    // user_account_id
    
    // lowerCamelCase: first word lowercase
    println!("lower_camel: {}", input.to_lower_camel_case());
    // userAccountId (unchanged)
    
    // UpperCamelCase (PascalCase): all words capitalized
    println!("upper_camel: {}", input.to_upper_camel_case());
    // UserAccountId
    
    // kebab-case: lowercase with hyphens
    println!("kebab: {}", input.to_kebab_case());
    // user-account-id
    
    // SHOUTY-KEBAB-CASE: uppercase with hyphens
    println!("shouty_kebab: {}", input.to_shouty_kebab_case());
    // USER-ACCOUNT-ID
}

heck provides a family of case transformation traits.

Edge Cases

use heck::ToShoutySnakeCase;
 
fn main() {
    // Already shouty snake case (idempotent)
    assert_eq!("USER_ACCOUNT_ID".to_shouty_snake_case(), "USER_ACCOUNT_ID");
    
    // Mixed case input
    assert_eq!("user_AccountID".to_shouty_snake_case(), "USER_ACCOUNT_ID");
    
    // Single word
    assert_eq!("user".to_shouty_snake_case(), "USER");
    
    // Empty string
    assert_eq!("".to_shouty_snake_case(), "");
    
    // Acronyms
    assert_eq!("parseXML".to_shouty_snake_case(), "PARSE_XML");
    assert_eq!("HTTPServer".to_shouty_snake_case(), "HTTP_SERVER");
    
    // Numbers within words
    assert_eq!("item42Status".to_shouty_snake_case(), "ITEM_42_STATUS");
    
    // Multiple underscores collapse
    assert_eq!("user___name".to_shouty_snake_case(), "USER_NAME");
}

The transformation handles edge cases sensibly.

Integration with serde for Serialization

use heck::ToShoutySnakeCase;
use serde::Serialize;
 
#[derive(Serialize)]
struct Config {
    #[serde(rename = "database_url")]
    database_url: String,
    #[serde(rename = "max_connections")]
    max_connections: u32,
}
 
fn main() {
    // Generate environment variable names from struct fields
    let fields = vec!["databaseUrl", "maxConnections"];
    
    for field in fields {
        let env_var = field.to_shouty_snake_case();
        println!("Config field '{}' -> env var '{}'", field, env_var);
    }
    
    // Combine with serde for consistent naming
    let config = Config {
        database_url: "localhost".to_string(),
        max_connections: 10,
    };
    let json = serde_json::to_string(&config).unwrap();
    println!("Serialized: {}", json);
}

Consistent naming conventions help maintain coherence across configuration systems.

Build Script Usage

// In build.rs
use heck::ToShoutySnakeCase;
 
fn main() {
    // Generate constants from configuration
    let features = vec!["enableFeatureX", "enableFeatureY"];
    
    let mut constants = String::new();
    for feature in features {
        let const_name = feature.to_shouty_snake_case();
        constants.push_str(&format!(
            "pub const {}: &str = \"{}\";\n",
            const_name, feature
        ));
    }
    
    // Write to generated file
    std::fs::write(
        std::path::Path::new("src/generated/constants.rs"),
        format!("// Auto-generated\n\n{}", constants),
    ).unwrap();
}

Build scripts can generate constants with proper naming conventions.

Comparison with Manual Conversion

use heck::ToShoutySnakeCase;
 
fn manual_to_shouty_snake_case(input: &str) -> String {
    let mut result = String::new();
    let mut prev_char = ' ';
    
    for c in input.chars() {
        if c.is_uppercase() && result.len() > 0 && prev_char != '_' {
            result.push('_');
        } else if c == '-' || c == ' ' {
            if prev_char != '_' {
                result.push('_');
            }
            prev_char = '_';
            continue;
        } else if c == '_' {
            if prev_char != '_' {
                result.push('_');
            }
            prev_char = '_';
            continue;
        }
        result.push(c.to_ascii_uppercase());
        prev_char = c;
    }
    
    result
}
 
fn main() {
    let inputs = vec![
        "userAccountId",
        "XMLHttpRequest",
        "my-kebab-case",
        "simple",
    ];
    
    for input in inputs {
        let expected = input.to_shouty_snake_case();
        let manual = manual_to_shouty_snake_case(input);
        println!("Input: {}", input);
        println!("  heck:  {}", expected);
        println!("  manual: {}", manual);
        assert_eq!(expected, manual);
    }
}

Using heck avoids reinventing case conversion logic with subtle bugs.

Working with Enums

use heck::ToShoutySnakeCase;
 
enum Status {
    PendingApproval,
    InProgress,
    Completed,
    Failed,
}
 
fn main() {
    // Convert enum variant names to constants
    let variants = vec!["PendingApproval", "InProgress", "Completed", "Failed"];
    
    for variant in variants {
        let constant = variant.to_shouty_snake_case();
        println!("pub const STATUS_{}: &str = \"{}\";", constant, variant);
    }
    
    // Output:
    // pub const STATUS_PENDING_APPROVAL: &str = "PendingApproval";
    // pub const STATUS_IN_PROGRESS: &str = "InProgress";
    // pub const STATUS_COMPLETED: &str = "Completed";
    // pub const STATUS_FAILED: &str = "Failed";
}

Enum variant names commonly need conversion for serialization or external APIs.

CLI Tool Example

use heck::ToShoutySnakeCase;
use std::io::{self, BufRead, Write};
 
fn main() {
    // Convert stdin to SHOUTY_SNAKE_CASE
    let stdin = io::stdin();
    let stdout = io::stdout();
    let mut stdout = stdout.lock();
    
    for line in stdin.lock().lines() {
        let line = line.unwrap();
        let converted = line.to_shouty_snake_case();
        writeln!(stdout, "{}", converted).unwrap();
    }
}
 
// Usage:
// $ echo "myConstantName" | cargo run
// MY_CONSTANT_NAME

CLI tools can transform identifiers for various purposes.

Integration with strum

use heck::ToShoutySnakeCase;
 
// strum provides enum iteration; heck provides case conversion
// They work well together
 
fn main() {
    // Simulating strum's IntoEnumIterator
    #[derive(Debug)]
    enum Color {
        Red,
        Green,
        Blue,
    }
    
    let colors = vec!["Red", "Green", "Blue"];
    
    for color in colors {
        // Convert enum variant name to constant format
        let constant_name = color.to_shouty_snake_case();
        println!("pub const COLOR_{}: &str = \"{}\";", constant_name, color);
    }
    
    // Generate serialization helper
    println!("\n// String conversion:");
    for color in &["Red", "Green", "Blue"] {
        let shouty = color.to_shouty_snake_case();
        println!("\"{}\" => Color::{},", shouty.to_lowercase(), color);
    }
}

heck complements other ecosystem crates for code generation.

Real-World Example: Configuration Generator

use heck::ToShoutySnakeCase;
use std::collections::HashMap;
 
struct ConfigField {
    name: String,
    default_value: String,
    description: String,
}
 
fn generate_config_code(fields: &[ConfigField]) -> String {
    let mut output = String::new();
    
    output.push_str("// Auto-generated configuration constants\n\n");
    
    // Generate environment variable constants
    for field in fields {
        let env_var = field.name.to_shouty_snake_case();
        output.push_str(&format!(
            "/// {}\npub const ENV_{}: &str = \"{}\";\n\n",
            field.description, env_var, field.name.to_shouty_snake_case()
        ));
    }
    
    // Generate default values
    output.push_str("pub fn defaults() -> HashMap<&'static str, String> {\n");
    output.push_str("    let mut map = HashMap::new();\n");
    for field in fields {
        output.push_str(&format!(
            "    map.insert(ENV_{}, String::from(\"{}\"));\n",
            field.name.to_shouty_snake_case(),
            field.default_value
        ));
    }
    output.push_str("    map\n}\n");
    
    output
}
 
fn main() {
    let fields = vec![
        ConfigField {
            name: "databaseUrl".to_string(),
            default_value: "localhost:5432".to_string(),
            description: "Database connection URL".to_string(),
        },
        ConfigField {
            name: "maxConnections".to_string(),
            default_value: "10".to_string(),
            description: "Maximum number of database connections".to_string(),
        },
        ConfigField {
            name: "sessionTimeout".to_string(),
            default_value: "3600".to_string(),
            description: "Session timeout in seconds".to_string(),
        },
    ];
    
    let code = generate_config_code(&fields);
    println!("{}", code);
}

Automatic generation of configuration constants with proper naming conventions.

Synthesis

Quick reference:

use heck::ToShoutySnakeCase;
 
fn main() {
    // Basic conversion
    assert_eq!("userId".to_shouty_snake_case(), "USER_ID");
    assert_eq!("user_account_id".to_shouty_snake_case(), "USER_ACCOUNT_ID");
    assert_eq!("user-account-id".to_shouty_snake_case(), "USER_ACCOUNT_ID");
    assert_eq!("UserAccountId".to_shouty_snake_case(), "USER_ACCOUNT_ID");
    
    // Word boundary detection
    // - Underscores separate words
    // - Hyphens separate words
    // - Uppercase letters start new words
    // - Numbers start new words
    
    // Common use cases:
    // 1. Generating constant names from field names
    // 2. Creating environment variable names
    // 3. Procedural macro code generation
    // 4. Configuration key generation
    
    // Related traits in heck:
    // - ToSnakeCase: user_account_id
    // - ToLowerCamelCase: userAccountId
    // - ToUpperCamelCase: UserAccountId
    // - ToKebabCase: user-account-id
    // - ToShoutyKebabCase: USER-ACCOUNT-ID
}

Key insight: ToShoutySnakeCase solves the common problem of generating conventional Rust constant names from various identifier styles without manually implementing word boundary detection. It handles the subtle cases like acronyms (XMLHttpRequestXML_HTTP_REQUEST), numbers (version2VERSION_2), and mixed conventions that make manual implementation error-prone. Use it in procedural macros, build scripts, and code generators to maintain consistent SHOUTY_SNAKE_CASE naming across your project. The trait is part of a family of case-conversion utilities in heck that work together for identifier transformation needs.