peace_full_spec_id_macro/
lib.rs

1use quote::quote;
2use syn::{parse_macro_input, LitStr};
3
4/// Returns a `const FullSpecId` validated at compile time.
5///
6/// # Examples
7///
8/// Instantiate a valid `FullSpecId` at compile time:
9///
10/// ```rust
11/// # use peace_full_spec_id_macro::full_spec_id;
12/// // use peace::cfg::{full_spec_id, FullSpecId};
13///
14/// let _my_full_spec_id: FullSpecId = full_spec_id!("valid_id"); // Ok!
15///
16/// # struct FullSpecId(&'static str);
17/// # impl FullSpecId {
18/// #     fn new_unchecked(s: &'static str) -> Self { Self(s) }
19/// # }
20/// ```
21///
22/// If the ID is invalid, a compilation error is produced:
23///
24/// ```rust,compile_fail
25/// # use peace_full_spec_id_macro::full_spec_id;
26/// // use peace::cfg::{full_spec_id, FullSpecId};
27///
28/// let _my_full_spec_id: FullSpecId = full_spec_id!("-invalid_id"); // Compile error
29/// //                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30/// // error: "-invalid_id" is not a valid `FullSpecId`.
31/// //        `FullSpecId`s must begin with a letter or underscore, and contain only letters, numbers, or underscores.
32/// #
33/// # struct FullSpecId(&'static str);
34/// # impl FullSpecId {
35/// #     fn new_unchecked(s: &'static str) -> Self { Self(s) }
36/// # }
37/// ```
38#[proc_macro]
39pub fn full_spec_id(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
40    let proposed_id = parse_macro_input!(input as LitStr).value();
41
42    if is_valid_id(&proposed_id) {
43        quote!( FullSpecId::new_unchecked( #proposed_id )).into()
44    } else {
45        let message = format!(
46            "\"{proposed_id}\" is not a valid `FullSpecId`.\n\
47            `FullSpecId`s must begin with a letter or underscore, and contain only letters, numbers, or underscores."
48        );
49        quote! {
50            compile_error!(#message)
51        }
52        .into()
53    }
54}
55
56fn is_valid_id(proposed_id: &str) -> bool {
57    let mut chars = proposed_id.chars();
58    let first_char = chars.next();
59    let first_char_valid = first_char
60        .map(|c| c.is_ascii_alphabetic() || c == '_')
61        .unwrap_or(false);
62    let remainder_chars_valid =
63        chars.all(|c| c.is_ascii_alphabetic() || c == '_' || c.is_ascii_digit());
64
65    first_char_valid && remainder_chars_valid
66}