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