Skip to main content

time_macros/
lib.rs

1#![allow(
2    clippy::missing_const_for_fn,
3    clippy::std_instead_of_core,
4    clippy::std_instead_of_alloc,
5    clippy::alloc_instead_of_core,
6    reason = "irrelevant for proc macros"
7)]
8#![allow(
9    clippy::missing_docs_in_private_items,
10    missing_docs,
11    reason = "may be removed eventually"
12)]
13
14#[allow(
15    unused_macros,
16    reason = "may not be used for all feature flag combinations"
17)]
18macro_rules! bug {
19    () => { compile_error!("provide an error message to help fix a possible bug") };
20    ($descr:literal $($rest:tt)?) => {
21        unreachable!(concat!("internal error: ", $descr) $($rest)?)
22    }
23}
24
25#[macro_use]
26mod quote;
27
28mod date;
29mod datetime;
30mod error;
31#[cfg(any(feature = "formatting", feature = "parsing"))]
32mod format_description;
33mod helpers;
34mod offset;
35#[cfg(all(feature = "serde", any(feature = "formatting", feature = "parsing")))]
36mod serde_format_description;
37mod time;
38mod to_tokens;
39mod utc_datetime;
40
41#[cfg(any(feature = "formatting", feature = "parsing"))]
42use std::iter::Peekable;
43
44#[cfg(all(feature = "serde", any(feature = "formatting", feature = "parsing")))]
45use proc_macro::Delimiter;
46use proc_macro::TokenStream;
47#[cfg(any(feature = "formatting", feature = "parsing"))]
48use proc_macro::TokenTree;
49
50use self::error::Error;
51
52macro_rules! impl_macros {
53    ($($name:ident)*) => {$(
54        #[proc_macro]
55        pub fn $name(input: TokenStream) -> TokenStream {
56            use crate::to_tokens::ToTokenStream;
57
58            let mut iter = input.into_iter().peekable();
59            match $name::parse(&mut iter) {
60                Ok(value) => match iter.peek() {
61                    Some(tree) => Error::UnexpectedToken { tree: tree.clone() }.to_compile_error(),
62                    None => quote_! { const { #S(value.into_token_stream()) } },
63                },
64                Err(err) => err.to_compile_error(),
65            }
66        }
67    )*};
68}
69
70impl_macros![date datetime utc_datetime offset time];
71
72#[cfg(any(feature = "formatting", feature = "parsing"))]
73type PeekableTokenStreamIter = Peekable<proc_macro::token_stream::IntoIter>;
74
75#[cfg(any(feature = "formatting", feature = "parsing"))]
76#[derive(Clone, Copy)]
77enum FormatDescriptionVersion {
78    V1,
79    V2,
80    V3,
81}
82
83#[cfg(any(feature = "formatting", feature = "parsing"))]
84impl FormatDescriptionVersion {
85    fn is_v1(self) -> bool {
86        match self {
87            Self::V1 => true,
88            Self::V2 | Self::V3 => false,
89        }
90    }
91
92    fn is_at_most_v2(self) -> bool {
93        match self {
94            Self::V1 | Self::V2 => true,
95            Self::V3 => false,
96        }
97    }
98
99    fn is_at_least_v2(self) -> bool {
100        match self {
101            Self::V1 => false,
102            Self::V2 | Self::V3 => true,
103        }
104    }
105
106    fn is_at_least_v3(self) -> bool {
107        match self {
108            Self::V1 | Self::V2 => false,
109            Self::V3 => true,
110        }
111    }
112}
113
114#[cfg(any(feature = "formatting", feature = "parsing"))]
115fn parse_format_description_version<const NO_EQUALS_IS_MOD_NAME: bool>(
116    iter: &mut PeekableTokenStreamIter,
117) -> Result<Option<FormatDescriptionVersion>, Error> {
118    let end_of_input_err = || {
119        if NO_EQUALS_IS_MOD_NAME {
120            Error::UnexpectedEndOfInput
121        } else {
122            Error::ExpectedString {
123                span_start: None,
124                span_end: None,
125            }
126        }
127    };
128    let version_ident = match iter.peek().ok_or_else(end_of_input_err)? {
129        version @ TokenTree::Ident(ident) if ident.to_string() == "version" => {
130            let version_ident = version.clone();
131            iter.next(); // consume `version`
132            version_ident
133        }
134        _ => return Ok(None),
135    };
136
137    match iter.peek() {
138        Some(TokenTree::Punct(punct)) if punct.as_char() == '=' => iter.next(),
139        _ if NO_EQUALS_IS_MOD_NAME => {
140            // Push the `version` ident to the front of the iterator.
141            *iter = std::iter::once(version_ident)
142                .chain(iter.clone())
143                .collect::<TokenStream>()
144                .into_iter()
145                .peekable();
146            return Ok(None);
147        }
148        Some(token) => {
149            return Err(Error::Custom {
150                message: "expected `=`".into(),
151                span_start: Some(token.span()),
152                span_end: Some(token.span()),
153            });
154        }
155        None => {
156            return Err(Error::Custom {
157                message: "expected `=`".into(),
158                span_start: None,
159                span_end: None,
160            });
161        }
162    };
163    let version_literal = match iter.next() {
164        Some(TokenTree::Literal(literal)) => literal,
165        Some(token) => {
166            return Err(Error::Custom {
167                message: "expected 1, 2, or 3".into(),
168                span_start: Some(token.span()),
169                span_end: Some(token.span()),
170            });
171        }
172        None => {
173            return Err(Error::Custom {
174                message: "expected 1, 2, or 3".into(),
175                span_start: None,
176                span_end: None,
177            });
178        }
179    };
180    let version = match version_literal.to_string().as_str() {
181        "1" => FormatDescriptionVersion::V1,
182        "2" => FormatDescriptionVersion::V2,
183        "3" => FormatDescriptionVersion::V3,
184        _ => {
185            return Err(Error::Custom {
186                message: "invalid format description version".into(),
187                span_start: Some(version_literal.span()),
188                span_end: Some(version_literal.span()),
189            });
190        }
191    };
192    helpers::consume_punct(',', iter)?;
193
194    Ok(Some(version))
195}
196
197#[cfg(all(feature = "serde", any(feature = "formatting", feature = "parsing")))]
198fn parse_visibility(iter: &mut PeekableTokenStreamIter) -> Result<TokenStream, Error> {
199    let mut visibility = match iter.peek().ok_or(Error::UnexpectedEndOfInput)? {
200        pub_ident @ TokenTree::Ident(ident) if ident.to_string() == "pub" => {
201            let visibility = quote_! { #(pub_ident.clone()) };
202            iter.next(); // consume `pub`
203            visibility
204        }
205        _ => return Ok(quote_! {}),
206    };
207
208    match iter.peek().ok_or(Error::UnexpectedEndOfInput)? {
209        group @ TokenTree::Group(path) if path.delimiter() == Delimiter::Parenthesis => {
210            visibility.extend(std::iter::once(group.clone()));
211            iter.next(); // consume parentheses and path
212        }
213        _ => {}
214    }
215
216    Ok(visibility)
217}
218
219#[cfg(any(feature = "formatting", feature = "parsing"))]
220#[proc_macro]
221pub fn format_description(input: TokenStream) -> TokenStream {
222    (|| {
223        let mut input = input.into_iter().peekable();
224        let version = parse_format_description_version::<false>(&mut input)?
225            .unwrap_or(FormatDescriptionVersion::V1);
226        let (span, string) = helpers::get_string_literal(input)?;
227        let items = format_description::parse_with_version(version, &string, span)?;
228
229        match version {
230            FormatDescriptionVersion::V1 | FormatDescriptionVersion::V2 => Ok(quote_! {
231                const {
232                    use ::time::format_description::{*, modifier::*};
233                    &[#S(
234                        items
235                            .into_iter()
236                            .map(|item| quote_! { #S(item), })
237                            .collect::<TokenStream>()
238                    )] as StaticFormatDescription
239                }
240            }),
241            FormatDescriptionVersion::V3 if items.len() == 1 => Ok(quote_! {
242                const {
243                    use ::time::format_description::__private::*;
244                    use ::time::format_description::modifier::*;
245                    #S(items[0].clone()).into_opaque()
246                }
247            }),
248            FormatDescriptionVersion::V3 => {
249                let inner = format_description::public::OwnedFormatItemInner::Compound(
250                    items.into_iter().map(|item| item.inner).collect(),
251                );
252                let item = format_description::public::OwnedFormatItem { version, inner };
253                Ok(quote_! {
254                    const {
255                        use ::time::format_description::__private::*;
256                        use ::time::format_description::modifier::*;
257                        #S(item).into_opaque()
258                    }
259                })
260            }
261        }
262    })()
263    .unwrap_or_else(|err: Error| err.to_compile_error())
264}
265
266#[cfg(all(feature = "serde", any(feature = "formatting", feature = "parsing")))]
267#[proc_macro]
268pub fn serde_format_description(input: TokenStream) -> TokenStream {
269    (|| {
270        let mut tokens = input.into_iter().peekable();
271
272        // First, the optional format description version.
273        let version = parse_format_description_version::<true>(&mut tokens)?
274            .unwrap_or(FormatDescriptionVersion::V1);
275
276        // Then, the visibility of the module.
277        let visibility = parse_visibility(&mut tokens)?;
278
279        // Next, an identifier (the desired module name)
280        let mod_name = match tokens.next() {
281            Some(TokenTree::Ident(ident)) => Ok(ident),
282            Some(tree) => Err(Error::UnexpectedToken { tree }),
283            None => Err(Error::UnexpectedEndOfInput),
284        }?;
285
286        // Followed by a comma
287        helpers::consume_punct(',', &mut tokens)?;
288
289        // Then, the type to create serde serializers for (e.g., `OffsetDateTime`).
290        let formattable = match tokens.next() {
291            Some(tree @ TokenTree::Ident(_)) => Ok(tree),
292            Some(tree) => Err(Error::UnexpectedToken { tree }),
293            None => Err(Error::UnexpectedEndOfInput),
294        }?;
295
296        // Another comma
297        helpers::consume_punct(',', &mut tokens)?;
298
299        // We now have two options. The user can either provide a format description as a string or
300        // they can provide a path to a format description. If the latter, all remaining tokens are
301        // assumed to be part of the path.
302        let (format, format_description_display) = match tokens.peek() {
303            // string literal
304            Some(TokenTree::Literal(_)) => {
305                let (span, format_string) = helpers::get_string_literal(tokens)?;
306                let items = format_description::parse_with_version(version, &format_string, span)?;
307                let items: TokenStream = items
308                    .into_iter()
309                    .map(|item| quote_! { #S(item), })
310                    .collect();
311                let items = quote_! {
312                    const {
313                        use ::time::format_description::{*, modifier::*};
314                        &[#S(items)] as StaticFormatDescription
315                    }
316                };
317
318                (items, String::from_utf8_lossy(&format_string).into_owned())
319            }
320            // path
321            Some(_) => {
322                let tokens = tokens.collect::<TokenStream>();
323                let tokens_string = tokens.to_string();
324                (tokens, tokens_string)
325            }
326            None => return Err(Error::UnexpectedEndOfInput),
327        };
328
329        Ok(serde_format_description::build(
330            visibility,
331            mod_name,
332            formattable,
333            format,
334            format_description_display,
335        ))
336    })()
337    .unwrap_or_else(|err: Error| err.to_compile_error_standalone())
338}