Expand description
§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:
- A macro from Nightly Rust that allows to concatenate identifiers. It is limited in functionality and nowhere near to be stabilized: https://doc.rust-lang.org/std/macro.concat_idents.html
- A very similar macro that doesn’t support multiple aliases and is not maintained: https://github.com/DzenanJupic/concat-idents
- A macro that allows to define and refer to unique temporary variables: https://crates.io/crates/templest
§Development
The following standards are followed to maintain the project:
Macros§
- compose_
idents - Compose identifiers from the provided parts and replace their aliases in the code block.