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}