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"))]
220fn expand_format_description(item: format_description::public::OwnedFormatItem) -> TokenStream {
221    match item.version {
222        FormatDescriptionVersion::V1 | FormatDescriptionVersion::V2 => match &item.inner {
223            format_description::public::OwnedFormatItemInner::Compound(items) => {
224                let items = items
225                    .iter()
226                    .map(|inner_item| {
227                        quote_! {
228                            #S(format_description::public::OwnedFormatItem {
229                                version: item.version,
230                                inner: inner_item.clone(),
231                            }),
232                        }
233                    })
234                    .collect::<TokenStream>();
235                quote_! {
236                    const {
237                        use ::time::format_description::{*, modifier::*};
238                        &[#S(items)] as StaticFormatDescription
239                    }
240                }
241            }
242            _ => quote_! {
243                const {
244                    use ::time::format_description::{*, modifier::*};
245                    &[#S(item)] as StaticFormatDescription
246                }
247            },
248        },
249        FormatDescriptionVersion::V3 => quote_! {
250            const {
251                use ::time::format_description::__private::*;
252                use ::time::format_description::modifier::*;
253                #S(item).into_opaque()
254            }
255        },
256    }
257}
258
259#[cfg(any(feature = "formatting", feature = "parsing"))]
260#[proc_macro]
261pub fn format_description(input: TokenStream) -> TokenStream {
262    (|| {
263        let mut input = input.into_iter().peekable();
264        let version = parse_format_description_version::<false>(&mut input)?
265            .unwrap_or(FormatDescriptionVersion::V1);
266        let (span, string) = helpers::get_string_literal(input)?;
267        let items = format_description::parse_with_version(version, &string, span)?;
268
269        Ok(expand_format_description(items))
270    })()
271    .unwrap_or_else(|err: Error| err.to_compile_error())
272}
273
274#[cfg(all(feature = "serde", any(feature = "formatting", feature = "parsing")))]
275#[proc_macro]
276pub fn serde_format_description(input: TokenStream) -> TokenStream {
277    (|| {
278        let mut tokens = input.into_iter().peekable();
279
280        // First, the optional format description version.
281        let version = parse_format_description_version::<true>(&mut tokens)?
282            .unwrap_or(FormatDescriptionVersion::V1);
283
284        // Then, the visibility of the module.
285        let visibility = parse_visibility(&mut tokens)?;
286
287        let (mod_name, ty) = match version {
288            FormatDescriptionVersion::V1 | FormatDescriptionVersion::V2 => {
289                // Next, an identifier (the desired module name)
290                let mod_name = match tokens.next() {
291                    Some(TokenTree::Ident(ident)) => Ok(ident),
292                    Some(tree) => Err(Error::UnexpectedToken { tree }),
293                    None => Err(Error::UnexpectedEndOfInput),
294                }?;
295
296                // Followed by a comma
297                helpers::consume_punct(',', &mut tokens)?;
298
299                // Then, the type to create serde serializers for (e.g., `OffsetDateTime`).
300                let ty = match tokens.next() {
301                    Some(tree @ TokenTree::Ident(_)) => Ok(tree.into()),
302                    Some(tree) => Err(Error::UnexpectedToken { tree }),
303                    None => Err(Error::UnexpectedEndOfInput),
304                }?;
305
306                // Another comma
307                helpers::consume_punct(',', &mut tokens)?;
308
309                (mod_name, ty)
310            }
311            FormatDescriptionVersion::V3 => {
312                // Next, the `mod` token.
313                match tokens.next() {
314                    Some(TokenTree::Ident(ident)) if ident.to_string() == "mod" => {}
315                    Some(tree) => {
316                        return Err(Error::Custom {
317                            message: "expected `mod`".into(),
318                            span_start: Some(tree.span().start()),
319                            span_end: Some(tree.span().end()),
320                        });
321                    }
322                    None => return Err(Error::UnexpectedEndOfInput),
323                };
324
325                // Next, an identifier (the desired module name)
326                let mod_name = match tokens.next() {
327                    Some(TokenTree::Ident(ident)) => Ok(ident),
328                    Some(tree) => Err(Error::UnexpectedToken { tree }),
329                    None => Err(Error::UnexpectedEndOfInput),
330                }?;
331
332                // Followed by the type to create implementations for, placed in brackets.
333                let ty = match tokens.next() {
334                    Some(TokenTree::Group(group)) if group.delimiter() == Delimiter::Bracket => {
335                        Ok(group.stream())
336                    }
337                    Some(tree) => Err(Error::UnexpectedToken { tree }),
338                    None => Err(Error::UnexpectedEndOfInput),
339                }?;
340
341                // Then an equals sign
342                helpers::consume_punct('=', &mut tokens)?;
343
344                (mod_name, ty)
345            }
346        };
347
348        // We now have two options. The user can either provide a format description as a
349        // string or they can provide a path to a format description. If the
350        // latter, all remaining tokens are assumed to be part of the path.
351        let (format, format_description_display) = match tokens.peek() {
352            // string literal
353            Some(TokenTree::Literal(_)) => {
354                let (span, format_string) = helpers::get_string_literal(tokens)?;
355                let items = format_description::parse_with_version(version, &format_string, span)?;
356                let items = expand_format_description(items);
357
358                (items, String::from_utf8_lossy(&format_string).into_owned())
359            }
360            // path
361            Some(_) => {
362                let tokens = tokens.collect::<TokenStream>();
363                let tokens_string = tokens.to_string();
364                (tokens, tokens_string)
365            }
366            None => return Err(Error::UnexpectedEndOfInput),
367        };
368
369        Ok(serde_format_description::build(
370            version,
371            visibility,
372            mod_name,
373            ty,
374            format,
375            format_description_display,
376        ))
377    })()
378    .unwrap_or_else(|err: Error| err.to_compile_error_standalone())
379}