padded_number_macros/
lib.rs

1//! # `padded-number-macros` - Macros for compile time `padded-number` constructs
2
3use proc_macro::TokenStream;
4use proc_macro2::TokenStream as TokenStream2;
5use quote::quote;
6use syn::{
7    LitInt, LitStr,
8    parse::{Parse, ParseStream},
9    parse_macro_input,
10    token::Comma,
11};
12
13/// Construct a `PaddedNumber` at compile time
14///
15/// May be seend as shorthand for writing `PaddedNumber::<1, {
16/// u8::MAX}>::try_new("001").unwrap()`, but compile time error reporting.
17///
18/// Errors if provided string is not within the provided length bounds, or it it
19/// constrains anything but ASCII digits.
20///
21/// Works in const context:
22///
23/// ```no_compile
24/// const PADDED_NUMBER: PaddedNumber = padded_number!("001");
25/// ```
26#[proc_macro]
27pub fn padded_number(token_stream: TokenStream) -> TokenStream {
28    let number_literal = parse_macro_input!(token_stream as LitStr);
29
30    let args = Args { min: 1, max: u8::MAX, number_literal };
31
32    padded_number_impl(args).into()
33}
34
35/// Construct a bound `PaddedNumber` at compile time, similar to
36/// `padded_number!`
37///
38/// First and second parameter denote min and max length bounds respectively,
39/// both inclusive.
40///
41/// Works in const context:
42///
43/// ```no_compile
44/// const PADDED_NUMBER: PaddedNumber<1, 3> = bound_padded_number!(1, 3, "001");
45/// ```
46#[proc_macro]
47pub fn bound_padded_number(token_stream: TokenStream) -> TokenStream {
48    let args = parse_macro_input!(token_stream as Args);
49    padded_number_impl(args).into()
50}
51
52struct Args {
53    min: u8,
54    max: u8,
55    number_literal: LitStr,
56}
57
58impl Parse for Args {
59    fn parse(input: ParseStream) -> syn::Result<Self> {
60        let min = input.parse::<LitInt>()?.base10_parse()?;
61        let _comma = input.parse::<Comma>()?;
62        let max = input.parse::<LitInt>()?.base10_parse()?;
63        let _comma = input.parse::<Comma>()?;
64        let number_string = input.parse::<LitStr>()?;
65
66        Ok(Args { min, max, number_literal: number_string })
67    }
68}
69
70fn padded_number_impl(args: Args) -> TokenStream2 {
71    let Args { min, max, number_literal } = args;
72    let number_str = number_literal.value();
73
74    match padded_number_internal::parse(min, max, &number_str) {
75        Ok((leading_zeros, remaining_number)) => {
76            quote! {
77                // SAFETY: invariants verified by proc macro
78                unsafe {
79                    padded_number::PaddedNumber::<#min, #max>::new_unchecked(
80                        #leading_zeros,
81                        #remaining_number
82                    )
83                }
84            }
85        }
86        Err(error) => syn::Error::new(number_literal.span(), error.to_string()).into_compile_error(),
87    }
88}