compose-idents 0.1.0

A Rust macro for generating new identifiers (names of variables, functions, traits, etc) by concatenating one or more arbitrary parts and applying other manipulations.
Documentation

Build Crates.io Version docs.rs

compose-idents

A macro for generating new identifiers (names of variables, functions, traits, etc.) by concatenating one or more arbitrary parts and applying other manipulations.

It was created as an alternative to macro_rules! that doesn't allow creating new identifiers from the macro arguments and concat_idents! macro from the nightly Rust, which is limited in capabilities and has not been stabilized since 2015.

Features

  • Identifier generation

    Identifiers can be generated via concatenation of multiple parts. Arguments of the outer macro definitions and literals are supported for identifier definitions.

  • Functions

    Functions can be applied when defining new identifiers for changing case and style.

  • String formatting

    Strings can be formatted with %alias% syntax, which is useful for generating doc-attributes.

  • Unique identifier generation

    Unique identifiers can be deterministically generated by using hash() function which is uniquely seeded each invocation of the macro. This might be useful for generating unique global variables.

Usage

This section contains various usage examples. For even more usage examples look into tests/ directory of the repository.

Quick start

compose_idents! works by accepting definitions of aliases and a code block where aliases could be used as normal identifiers. When the macro is expanded, the aliases are replaced with their definitions:

use compose_idents::compose_idents;

// We generate separate const-functions for each type as a workaround
// since Rust doesn't allow us to use `core::ops::Add` in `const fn`.

macro_rules! gen_const_add {
    ($T:ty) => {
        compose_idents!(
            Type = [upper($T)],   // Alias for the type - make it uppercase in addition
            add_fn = [add_, $T],  // Alias for the function name
            {
                // Strings (including in doc-attributes) can be formatted with %alias% syntax.
                #[doc = "Adds two arguments of type `%Type%` at compile time."]
                pub const fn add_fn(a: $T, b: $T) -> $T {  // Aliases are used as normal identifiers
                    a + b
                }
            }
        );
    };
}

gen_const_add!(u32);  // Expands into `add_u32()` function.
gen_const_add!(u64);  // Expands into `add_u64()` function.

assert_eq!(add_u32(2_u32, 2_u32), 4_u32);
assert_eq!(add_u64(2_u64, 2_u64), 4_u64);

Generating tests for different types

A practical example for how to auto-generate names for macro-generated tests for different data types:

use compose_idents::compose_idents;

pub trait Frobnicate {
  type Output;

  fn frobnicate(&self, value: Self) -> Self::Output;
}

impl Frobnicate for u32 {
  type Output = u32;

  fn frobnicate(&self, value: Self) -> Self::Output {
    self + value
  }
}

impl Frobnicate for &'static str {
  type Output = String;

  fn frobnicate(&self, value: Self) -> Self::Output {
    format!("{}_{}", self, value)
  }
}

macro_rules! generate_frobnicate_test {
    ($type:ty, $initial:expr, $input:expr, $expected:expr) => {
        // Notice - we are using normalize() to make `&'static str` fit for
        // being part of the test function's identifier.
        compose_idents!(test_fn = [test_frobnicate_, normalize($type)], {
          fn test_fn() {
            let actual = ($initial as $type).frobnicate($input);
            let expected = $expected;

            assert_eq!(actual, expected);
          }
        });
    };
}

// Generates tests for u32 and &'static str types
generate_frobnicate_test!(u32, 0, 42_u32, 42_u32);
generate_frobnicate_test!(&'static str, "foo", "bar", "foo_bar".to_string());

test_frobnicate_u32();
// Notice - "&'static str" has been turned into just "static_str"
test_frobnicate_static_str();

Reference example

This example includes all the features of the macro:

use compose_idents::compose_idents;

compose_idents!(
    // Literal strings are accepted as arguments and their content is parsed.
    my_fn_1 = [foo, _, "baz"],
    // So as literal integers and underscores (or free-form token sequences).
    my_fn_2 = [spam, _, 1, _, eggs],
    // Functions can be applied to the arguments.
    my_const = [upper(foo), _, lower(BAR)],
    // Function calls can be arbitrarily nested and combined.
    my_static = [upper(lower(BAR))],
    MY_SNAKE_CASE_STATIC = [snake_case(snakeCase)],
    MY_CAMEL_CASE_STATIC = [camel_case(camel_case)],
    MY_PASCAL_CASE_STATIC = [pascal_case(pascal_case)],
    // normalize() allows to turn an arbitrary sequence of tokens into a valid identifier.
    MY_NORMALIZED_ALIAS = [my, _, normalize(&'static str)],
    // This function is useful to create identifiers that are unique across multiple macro invocations.
    // `hash(0b11001010010111)` will generate the same value even if called twice in the same macro call,
    // but will be different in different macro calls.
    MY_UNIQUE_STATIC = [hash(0b11001010010111)],
    MY_FORMATTED_STR = [FOO, _, BAR],
    MY_REUSED_ALIAS = [REUSED, _, FOO, _, my_static],
    {
        fn my_fn_1() -> u32 {
            123
        }

        // You can use %alias% syntax to replace aliases with their replacements
        // in string literals and doc-attributes.
        #[doc = "This is a docstring for %my_fn_2%"]
        fn my_fn_2() -> u32 {
            321
        }

        const my_const: u32 = 42;
        static my_static: u32 = 42;
        static MY_SNAKE_CASE_STATIC: u32 = 42;
        static MY_CAMEL_CASE_STATIC: u32 = 42;
        static MY_PASCAL_CASE_STATIC: u32 = 42;
        static MY_NORMALIZED_ALIAS: &'static str = "This alias is made from a normalized argument";
        static MY_UNIQUE_STATIC: u32 = 42;
        // This is an example of string literal formatting.
        static MY_FORMATTED_STR: &str = "This is %MY_FORMATTED_STR%";
        static MY_REUSED_ALIAS: u32 = 42;
    }
);

// It's possible to use arguments of declarative macros as parts of the identifiers.
macro_rules! outer_macro {
    ($name:tt) => {
        compose_idents!(my_nested_fn = [nested, _, $name], {
            fn my_nested_fn() -> u32 {
                42
            }
        });
    };
}

outer_macro!(foo);

macro_rules! global_var_macro {
    () => {
        // `my_static` is going to be unique in each invocation of `global_var_macro!()`.
        // But within the same invocation `hash(1)` will yield the same result.
        compose_idents!(my_static = [foo, _, hash(1)], {
            static my_static: u32 = 42;
        });
    };
}

global_var_macro!();
global_var_macro!();

assert_eq!(foo_baz(), 123);
assert_eq!(spam_1_eggs(), 321);
assert_eq!(nested_foo(), 42);
assert_eq!(FOO_bar, 42);
assert_eq!(BAR, 42);
assert_eq!(snake_case, 42);
assert_eq!(camelCase, 42);
assert_eq!(PascalCase, 42);
assert_eq!(
    my_static_str,
    "This alias is made from a normalized argument"
);
assert_eq!(FOO_BAR, "This is FOO_BAR");
assert_eq!(REUSED_FOO_BAR, 42);

Functions

Function Description
upper(arg) Converts the arg to upper case.
lower(arg) Converts the arg to lower case.
snake_case(arg) Converts the arg to snake_case.
camel_case(arg) Converts the arg to camelCase.
pascal_case(arg) Converts the arg to PascalCase.
normalize(tokens) Transforms a random sequence of tokens tokens into a valid identifier.
hash(arg) Hashes the arg deterministically within a single macro invocation.

Alternatives

There some other tools and projects dedicated to identifier manipulation:

Development

The following standards are followed to maintain the project: