Skip to main content

Crate typed_ident

Crate typed_ident 

Source
Expand description

§typed-ident

A Rust crate for type-safe identifier validation, inspection, and mutation.

Conforming to:

NOTE: This crate is currently in alpha. Until the crate enters beta, the crate has no stability promise and may contain breaking changes with every update.

§Using typed-ident

§When To Use

typed-ident is intended for values that represent programming identifiers and must obey a defined identifier syntax.

Use it when you need to:

  • Check that an input is a valid identifier
  • Distinguish between different identifier casing conventions at the type level
  • Inspect identifier boundaries, delimiters, or segments
  • Convert between different identifier casing conventions while preserving identifier semantics
  • Safely build or modify identifiers without accidentally producing invalid values

Typical use cases include:

  • Programming language implementations
  • Procedural macros and code generation
  • Compiler, formatter, or linter tooling
  • Schema, API, or serialization code that generates fields, types, or methods
  • Refactoring and rename tools

For ordinary strings that only need case conversion, use a general-purpose crate such as convert_case.

For URLs, REST paths, filesystem paths, or other domain-specific syntax, use a crate that understands that syntax instead.

§Basic Usage

The most common way to use this crate is through its predefined identifier presets. You can choose the preset that matches the syntax you want to enforce.

use typed_ident::presets::unicode::*;

// Examples of lower-camel identifiers:
assert!(LowerCamelIdent::new("iAm_123").is_ok());     // Delimiters         (allowed)
assert!(LowerCamelIdent::new("iAm_lower").is_ok());   // Consistent Chunks  (allowed)

// Examples of non-lower-camel identifiers:
assert!(LowerCamelIdent::new("").is_err());           // Empty              (disallowed)
assert!(LowerCamelIdent::new("2").is_err());          // Invalid Start      (disallowed)
assert!(LowerCamelIdent::new("UpperCamel").is_err()); // Upper Camel        (disallowed)
assert!(LowerCamelIdent::new("iAm_Mixed").is_err());  // Mixed Chunks       (disallowed)
assert!(LowerCamelIdent::new("kebab-case").is_err()); // Hyphen Delimiter   (disallowed)

If you know the general shape of the identifier, but do not want to enforce a specific casing convention, less-specific presets are also available.

use typed_ident::presets::unicode::*;

// Examples of camel identifiers:
assert!(CamelIdent::new("iAm_123").is_ok());     // Delimiters         (allowed)
assert!(CamelIdent::new("iAm_lower").is_ok());   // Consistent Chunks  (allowed)
assert!(CamelIdent::new("iAm_Mixed").is_ok());   // Mixed Chunks       (allowed)
assert!(CamelIdent::new("UpperCamel").is_ok());  // Upper Camel        (allowed)

// Examples of non-camel identifiers:
assert!(CamelIdent::new("").is_err());           // Empty              (disallowed)
assert!(CamelIdent::new("2").is_err());          // Invalid Start      (disallowed)
assert!(CamelIdent::new("kebab-case").is_err()); // Hyphen Delimiter   (disallowed)

§Advanced Usage

If the predefined presets do not match your use case, you can define custom identifiers with customized boundaries, delimiters, and character profiles. Prefer a predefined preset whenever one already matches your requirements.

use typed_ident::*;
use typed_ident::syntax::*;

// Defining your own custom delimiter (the `@` symbol).
#[derive(Copy, Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct AtDelimiter;
impl UnitDelimiter for AtDelimiter {
    const CHAR: char = '@';
    const STR: &'static str = "@";
}
impl SubsetOf<AtDelimiter> for AtDelimiter {}

// Defining your own custom type that uses the `@` delimiter.
// For this example, use a predefined boundary and character profile.
type CustomIdent = Ident<
    boundary::Standard,             // Use the standard boundary algorithm
    AtDelimiter,                    // The delimiter used by this identifier is `@`.
    profile::Lower<profile::Ascii>, // ASCII-only, disallowing uppercase and titlecase.
>;

// Examples of lower-at identifiers:
assert!(CustomIdent::new("iam@123").is_ok());     // Delimiters         (allowed)
assert!(CustomIdent::new("iam@lower").is_ok());   // Consistent Chunks  (allowed)
assert!(CustomIdent::new("onechunk").is_ok());    // One Chunk          (allowed)

// Examples of non-lower-at identifiers:
assert!(CustomIdent::new("").is_err());           // Empty              (disallowed)
assert!(CustomIdent::new("2").is_err());          // Invalid Start      (disallowed)
assert!(CustomIdent::new("iam@Mixed").is_err());  // Uppercase Char     (disallowed)
assert!(CustomIdent::new("kebab-case").is_err()); // Hyphen Delimiter   (disallowed)
assert!(CustomIdent::new("snake_case").is_err()); // Low Line Delimiter (disallowed)
assert!(CustomIdent::new("CamelCase").is_err());  // Camel Casing       (disallowed)
assert!(CustomIdent::new("UPPERCASE").is_err());  // Upper Casing       (disallowed)
assert!(CustomIdent::new("日本語").is_err());      // Non-ASCII          (disallowed)

Creating custom identifiers can be difficult. See the syntax module documentation for more details.

§Selecting a Preset

There are four predefined formats, each available with different case restrictions.

To break down the selection process, you can first start by considering the general form of the identifier you want.

Start by selecting one of…

FormatDelimitersCase restrictions apply to…
CamelIdentLow Line (_)…only chunk start characters.
HybridIdentFlat Line (_ / -)…only chunk start characters.
KebabIdentHyphen-Minus (-)…all characters.
SnakeIdentLow Line (_)…all characters.

Then select a case profile based on the restrictions you want to apply to the in-scope characters.

Finalize your selection based on your required case restrictions…

CasingCase Restrictions
Mixed (Unprefixed)No specific case restrictions.
CasedEither lower or upper, but not mixed.
LowerMust be lowercase or uncased.
UpperMust be uppercase or uncased.

All predefined presets follow this naming pattern. Select the strictest preset that matches your requirement.

// A basic camel identifier is the most permissive choice.
assert!(CamelIdent::new("lowerCamel").is_ok());
assert!(CamelIdent::new("UpperCamel").is_ok());
assert!(CamelIdent::new("lowerCamel_UpperCamel").is_ok());

// A cased-camel identifier must be consistently either lower- or upper-cased.
assert!(CasedCamelIdent::new("lowerCamel").is_ok());
assert!(CasedCamelIdent::new("UpperCamel").is_ok());
assert!(CasedCamelIdent::new("lowerCamel_UpperCamel").is_err());

// A lower-cased camel identifier requires that it's lowerCamelCase
assert!(LowerCamelIdent::new("lowerCamel").is_ok());
assert!(LowerCamelIdent::new("UpperCamel").is_err());
assert!(LowerCamelIdent::new("lowerCamel_UpperCamel").is_err());

// An upper-cased camel identifier requires that it's UpperCamelCase
assert!(UpperCamelIdent::new("lowerCamel").is_err());
assert!(UpperCamelIdent::new("UpperCamel").is_ok());
assert!(UpperCamelIdent::new("lowerCamel_UpperCamel").is_err());

§Crate Details

§Core Types

The predefined presets are configurations of more general types provided by this crate.

TypeBrief Description
IdentAn immutable, syntax-validated UTF-8 string slice representing an identifier.
FragmentAn immutable slice of an identifier, which itself may not be an identifier.
FragmentBufA mutable, owned buffer for constructing or modifying fragments and identifiers.
ChunkAn immutable slice of a Fragment which contains no delimiters.
SegmentA single chunk or delimiter from an Ident or Fragment.

There is no IdentBuf type. This is because a mutable identifier is able to be made invalid by clearing the buffer, and enforcing the buffer is non-empty makes such a type difficult to use.

For an owned Ident, use Box<Ident>.

§Boundary Definition

Though the boundary implementation can be customized, this crate provides a configurable predefined implementation with defaults based on Unicode Technical Standard #55.

This means that predefined identifiers will create the following segment boundaries.

  • A delimiter boundary places a boundary before and after any delimiter defined by the identifier
  • A camel boundary places a boundary after a lowercase or non-Greek titlecase letter, followed by an uppercase or titlecase letter
  • A hat boundary places a boundary before an uppercase or titlecase letter followed by a lowercase letter, or before a non-Greek titlecase letter

NOTE: UTS #55 distinguishes Greek and non-Greek titlecase characters when determining identifier chunks. Greek titlecase characters are treated as visually uppercase (ᾈ, ᾨ, ῌ, ᾚ, etc), while non-Greek titlecase characters are treated as beginning uppercase and continuing lowercase within the same scalar value (Dž, Lj, Nj, Dz, etc).

// A camel boundary splits `Camel` from `Boundary`.
let segments: Vec<_> = CamelIdent::new("CamelBoundary")?
    .segments()
    .type_erased()
    .collect();
assert_eq!(segments, [
    StrSegment::Chunk("Camel"),
    StrSegment::Chunk("Boundary")
]);

// A hat boundary splits `HAT` from `Boundary`.
let segments: Vec<_> = CamelIdent::new("HATBoundary")?
    .segments()
    .type_erased()
    .collect();
assert_eq!(segments, [
    StrSegment::Chunk("HAT"),
    StrSegment::Chunk("Boundary")
]);

// A delimiter boundary splits `delimiter` from `_`, and `_` from `boundary`.
let segments: Vec<_> = CamelIdent::new("delimiter_boundary")?
    .segments()
    .type_erased()
    .collect();
assert_eq!(segments, [
    StrSegment::Chunk("delimiter"),
    StrSegment::Delim('_'),
    StrSegment::Chunk("boundary")
]);

See the boundary module documentation for information on configuring this predefined boundary implementation, or how to create your own boundary implementation.

§Character Profiles

Though the character profile can be customized, this crate provides a predefined Unicode character profile based on Unicode Standard Annex #31. It also provides an ASCII profile that restrict the Unicode profile to only ASCII characters.

A character profile must be combined with a case profile to form a cased profile, which is then usable for identifier validation.

  • A mixed profile allows any casing supported by the character profile
  • A lower profile allows only lowercase and uncased characters
  • An upper profile allows only uppercase, uncased, and Greek titlecase characters
  • A lower-camel profile allows only lowercase and uncased for the first character after a delimiter
  • An upper-camel profile allows only uppercase, uncased, and titlecase for the first character after a delimiter

NOTE: The specific handling of titlecase here comes from how UTS #55 defines where boundaries are introduced. For consistency, we either consider a titlecase character entirely visually uppercase (Greek), or that it visually start uppercase but ends lowercase (non-Greek).

// A mixed profile allows any casing of valid characters.
assert!(Mixed::<Ascii>::is_chunk_start('a'));
assert!(Mixed::<Ascii>::is_chunk_start('A'));
assert!(Mixed::<Ascii>::is_chunk_start('0'));
assert!(Mixed::<Ascii>::is_chunk_continue('a'));
assert!(Mixed::<Ascii>::is_chunk_continue('A'));
assert!(Mixed::<Ascii>::is_chunk_continue('0'));

// A lower profile allows only lower or uncased valid characters.
assert!(Lower::<Ascii>::is_chunk_start('a'));
assert!(!Lower::<Ascii>::is_chunk_start('A'));
assert!(Lower::<Ascii>::is_chunk_start('0'));
assert!(Lower::<Ascii>::is_chunk_continue('a'));
assert!(!Lower::<Ascii>::is_chunk_continue('A'));
assert!(Lower::<Ascii>::is_chunk_continue('0'));

// An upper profile allows only upper-like or uncased valid characters.
assert!(!Upper::<Ascii>::is_chunk_start('a'));
assert!(Upper::<Ascii>::is_chunk_start('A'));
assert!(Upper::<Ascii>::is_chunk_start('0'));
assert!(!Upper::<Ascii>::is_chunk_continue('a'));
assert!(Upper::<Ascii>::is_chunk_continue('A'));
assert!(Upper::<Ascii>::is_chunk_continue('0'));

// A lower camel profile restricts only the first character of each chunk.
assert!(LowerCamel::<Ascii>::is_chunk_start('a'));
assert!(!LowerCamel::<Ascii>::is_chunk_start('A'));
assert!(LowerCamel::<Ascii>::is_chunk_start('0'));
assert!(LowerCamel::<Ascii>::is_chunk_continue('a'));
assert!(LowerCamel::<Ascii>::is_chunk_continue('A'));
assert!(LowerCamel::<Ascii>::is_chunk_continue('0'));

// An upper camel profile restricts only the first character of each chunk.
assert!(!UpperCamel::<Ascii>::is_chunk_start('a'));
assert!(UpperCamel::<Ascii>::is_chunk_start('A'));
assert!(UpperCamel::<Ascii>::is_chunk_start('0'));
assert!(UpperCamel::<Ascii>::is_chunk_continue('a'));
assert!(UpperCamel::<Ascii>::is_chunk_continue('A'));
assert!(UpperCamel::<Ascii>::is_chunk_continue('0'));

See the profile module documentation for information on other less-common predefined case profiles, or how to create your own case and character profile implementations.

§How Validation Works

Validation works on one Unicode scalar value (char) at a time, and checks if each character is either a valid delimiter or within the profile selected. Some case checks are interdependent, particularly for the Cased* types, but this is a useful high-level explanation.

  • There must be one or more characters to form a valid identifier.
  • Every character belonging to the target Delimiter is valid.
  • Non-delimiter characters must match based on the following definitions:

Validation can be described generally with the following pseudo-code:

// First, we must define some target syntax definitions.
type TargetDelimiter = LowLine;
type TargetProfile = Mixed<Unicode>;

// Then we can validate some target string.
let mut chars = "targetString".char_indices();

// The first character is handled specially.
let Some((_, first)) = chars.next() else {
    return Err(SyntaxError::Format(0));
};
let first_is_delim = TargetDelimiter::is_ident_start_delim(first);
if !first_is_delim && !TargetProfile::is_ident_start_char(first) {
    return Err(SyntaxError::Format(0));
}

// Remaining characters are handled generally.
let mut last_was_delim = first_is_delim;
for (idx, c) in chars {
    let profile_check = match last_was_delim {
        true => TargetProfile::is_chunk_start,
        false => TargetProfile::is_chunk_continue,
    };
    last_was_delim = TargetDelimiter::is_chunk_delim(c);
    if last_was_delim {
        continue;
    }
    if !profile_check(c) {
        return Err(SyntaxError::Format(idx));
    }
}

§Normalization

This crate does not provide utilities for Unicode normalization.

Normalization is optional, but if canonical-equivalent spellings should be treated identically, normalize the input before validation.

NFC (for storage and general use)

NFC composes canonically equivalent sequences into a standard canonical form. It is commonly useful for storing and comparing text when canonical equivalence should be treated as equality. It does not resolve case differences, compatibility equivalences, or visually confusable characters.

NFKC (for compatibility normalization)

NFKC converts text to a canonical form while also removing compatibility distinctions, such as mapping full-width 1 to ASCII 1. Because this can discard distinctions that may matter, it should be used only when compatibility equivalence is desired.

For caseless, compatibility-insensitive matching, use Unicode’s NFKC_Casefold operation, preferably through a library API that implements it directly.

Modules§

alloc
A module for defining typed identifier functionality that relies on allocation.
core
Core type definitions that make up this crate.
presets
Presets to set commonly used identifier formats.
syntax
Syntax definitions for identifiers.

Structs§

Chunk
An immutable UTF-8 encoded slice of an Ident which contains no delimiters.
Error
An error when performing some operation on a core type.
Fragment
An immutable UTF-8 encoded slice of an Ident.
FragmentBuf
A dynamic, growable Fragment.
Ident
An immutable, UTF-8 encoded, valid identifier string slice.

Enums§

Segment
A part of an identifier returned during segmentation.

Constants§

UNICODE_VERSION
The minimal common Unicode version (lesser of UNICODE_DEPENDENCIES_VERSION and UNICODE_GENERATED_VERSION).

Traits§

ConvertibleIdentifier
A convenience trait for identifiers which support all preset conversion operations (which are all valid identifiers).
FormattableIdentifier
A convenience trait for identifiers which support all preset formatting operations (which are all valid identifiers).
Identifier
A trait for dealing with identifiers in a generic way.

Type Aliases§

StrSegment
A borrowed, type-erased representation of a Segment.
StringSegment
An owned, type-erased representation of a Segment.