time_macros/
lib.rs

1#![allow(
2    clippy::missing_const_for_fn, // irrelevant for proc macros
3    clippy::missing_docs_in_private_items, // TODO remove
4    clippy::std_instead_of_core, // irrelevant for proc macros
5    clippy::std_instead_of_alloc, // irrelevant for proc macros
6    clippy::alloc_instead_of_core, // irrelevant for proc macros
7    missing_docs, // TODO remove
8)]
9
10#[allow(unused_macros)]
11macro_rules! bug {
12    () => { compile_error!("provide an error message to help fix a possible bug") };
13    ($descr:literal $($rest:tt)?) => {
14        unreachable!(concat!("internal error: ", $descr) $($rest)?)
15    }
16}
17
18#[macro_use]
19mod quote;
20
21mod date;
22mod datetime;
23mod error;
24#[cfg(any(feature = "formatting", feature = "parsing"))]
25mod format_description;
26mod helpers;
27mod offset;
28#[cfg(all(feature = "serde", any(feature = "formatting", feature = "parsing")))]
29mod serde_format_description;
30mod time;
31mod to_tokens;
32mod utc_datetime;
33
34#[cfg(any(feature = "formatting", feature = "parsing"))]
35use std::iter::Peekable;
36
37use proc_macro::TokenStream;
38#[cfg(any(feature = "formatting", feature = "parsing"))]
39use proc_macro::{Ident, TokenTree};
40
41use self::error::Error;
42
43macro_rules! impl_macros {
44    ($($name:ident)*) => {$(
45        #[proc_macro]
46        pub fn $name(input: TokenStream) -> TokenStream {
47            use crate::to_tokens::ToTokenTree;
48
49            let mut iter = input.into_iter().peekable();
50            match $name::parse(&mut iter) {
51                Ok(value) => match iter.peek() {
52                    Some(tree) => Error::UnexpectedToken { tree: tree.clone() }.to_compile_error(),
53                    None => TokenStream::from(value.into_token_tree()),
54                },
55                Err(err) => err.to_compile_error(),
56            }
57        }
58    )*};
59}
60
61impl_macros![date datetime utc_datetime offset time];
62
63#[cfg(any(feature = "formatting", feature = "parsing"))]
64enum FormatDescriptionVersion {
65    V1,
66    V2,
67}
68
69#[cfg(any(feature = "formatting", feature = "parsing"))]
70enum VersionOrModuleName {
71    Version(FormatDescriptionVersion),
72    #[cfg_attr(not(feature = "serde"), allow(dead_code))]
73    ModuleName(Ident),
74}
75
76#[cfg(any(feature = "formatting", feature = "parsing"))]
77fn parse_format_description_version<const NO_EQUALS_IS_MOD_NAME: bool>(
78    iter: &mut Peekable<proc_macro::token_stream::IntoIter>,
79) -> Result<Option<VersionOrModuleName>, Error> {
80    let version_ident = match iter.peek() {
81        Some(TokenTree::Ident(ident)) if ident.to_string() == "version" => match iter.next() {
82            Some(TokenTree::Ident(ident)) => ident,
83            _ => unreachable!(),
84        },
85        _ => return Ok(None),
86    };
87    match iter.peek() {
88        Some(TokenTree::Punct(punct)) if punct.as_char() == '=' => iter.next(),
89        _ if NO_EQUALS_IS_MOD_NAME => {
90            return Ok(Some(VersionOrModuleName::ModuleName(version_ident)));
91        }
92        Some(token) => {
93            return Err(Error::Custom {
94                message: "expected `=`".into(),
95                span_start: Some(token.span()),
96                span_end: Some(token.span()),
97            });
98        }
99        None => {
100            return Err(Error::Custom {
101                message: "expected `=`".into(),
102                span_start: None,
103                span_end: None,
104            });
105        }
106    };
107    let version_literal = match iter.next() {
108        Some(TokenTree::Literal(literal)) => literal,
109        Some(token) => {
110            return Err(Error::Custom {
111                message: "expected 1 or 2".into(),
112                span_start: Some(token.span()),
113                span_end: Some(token.span()),
114            });
115        }
116        None => {
117            return Err(Error::Custom {
118                message: "expected 1 or 2".into(),
119                span_start: None,
120                span_end: None,
121            });
122        }
123    };
124    let version = match version_literal.to_string().as_str() {
125        "1" => FormatDescriptionVersion::V1,
126        "2" => FormatDescriptionVersion::V2,
127        _ => {
128            return Err(Error::Custom {
129                message: "invalid format description version".into(),
130                span_start: Some(version_literal.span()),
131                span_end: Some(version_literal.span()),
132            });
133        }
134    };
135    helpers::consume_punct(',', iter)?;
136
137    Ok(Some(VersionOrModuleName::Version(version)))
138}
139
140#[cfg(any(feature = "formatting", feature = "parsing"))]
141#[proc_macro]
142pub fn format_description(input: TokenStream) -> TokenStream {
143    (|| {
144        let mut input = input.into_iter().peekable();
145        let version = match parse_format_description_version::<false>(&mut input)? {
146            Some(VersionOrModuleName::Version(version)) => Some(version),
147            None => None,
148            // This branch should never occur here, as `false` is the provided as a const parameter.
149            Some(VersionOrModuleName::ModuleName(_)) => bug!("branch should never occur"),
150        };
151        let (span, string) = helpers::get_string_literal(input)?;
152        let items = format_description::parse_with_version(version, &string, span)?;
153
154        Ok(quote! {{
155            const DESCRIPTION: &[::time::format_description::BorrowedFormatItem<'_>] = &[#S(
156                items
157                    .into_iter()
158                    .map(|item| quote! { #S(item), })
159                    .collect::<TokenStream>()
160            )];
161            DESCRIPTION
162        }})
163    })()
164    .unwrap_or_else(|err: Error| err.to_compile_error())
165}
166
167#[cfg(all(feature = "serde", any(feature = "formatting", feature = "parsing")))]
168#[proc_macro]
169pub fn serde_format_description(input: TokenStream) -> TokenStream {
170    (|| {
171        let mut tokens = input.into_iter().peekable();
172
173        // First, the optional format description version.
174        let version = parse_format_description_version::<true>(&mut tokens)?;
175        let (version, mod_name) = match version {
176            Some(VersionOrModuleName::ModuleName(module_name)) => (None, Some(module_name)),
177            Some(VersionOrModuleName::Version(version)) => (Some(version), None),
178            None => (None, None),
179        };
180
181        // Next, an identifier (the desired module name)
182        // Only parse this if it wasn't parsed when attempting to get the version.
183        let mod_name = match mod_name {
184            Some(mod_name) => mod_name,
185            None => match tokens.next() {
186                Some(TokenTree::Ident(ident)) => Ok(ident),
187                Some(tree) => Err(Error::UnexpectedToken { tree }),
188                None => Err(Error::UnexpectedEndOfInput),
189            }?,
190        };
191
192        // Followed by a comma
193        helpers::consume_punct(',', &mut tokens)?;
194
195        // Then, the type to create serde serializers for (e.g., `OffsetDateTime`).
196        let formattable = match tokens.next() {
197            Some(tree @ TokenTree::Ident(_)) => Ok(tree),
198            Some(tree) => Err(Error::UnexpectedToken { tree }),
199            None => Err(Error::UnexpectedEndOfInput),
200        }?;
201
202        // Another comma
203        helpers::consume_punct(',', &mut tokens)?;
204
205        // We now have two options. The user can either provide a format description as a string or
206        // they can provide a path to a format description. If the latter, all remaining tokens are
207        // assumed to be part of the path.
208        let (format, format_description_display) = match tokens.peek() {
209            // string literal
210            Some(TokenTree::Literal(_)) => {
211                let (span, format_string) = helpers::get_string_literal(tokens)?;
212                let items = format_description::parse_with_version(version, &format_string, span)?;
213                let items: TokenStream =
214                    items.into_iter().map(|item| quote! { #S(item), }).collect();
215                let items = quote! {
216                    const ITEMS: &[::time::format_description::BorrowedFormatItem<'_>]
217                        = &[#S(items)];
218                    ITEMS
219                };
220
221                (items, String::from_utf8_lossy(&format_string).into_owned())
222            }
223            // path
224            Some(_) => {
225                let tokens = tokens.collect::<TokenStream>();
226                let tokens_string = tokens.to_string();
227                (tokens, tokens_string)
228            }
229            None => return Err(Error::UnexpectedEndOfInput),
230        };
231
232        Ok(serde_format_description::build(
233            mod_name,
234            formattable,
235            format,
236            format_description_display,
237        ))
238    })()
239    .unwrap_or_else(|err: Error| err.to_compile_error_standalone())
240}