#![allow( clippy::missing_const_for_fn, // irrelevant for proc macros clippy::missing_docs_in_private_items, // TODO remove clippy::std_instead_of_core, // irrelevant for proc macros clippy::std_instead_of_alloc, // irrelevant for proc macros clippy::alloc_instead_of_core, // irrelevant for proc macros missing_docs, // TODO remove )] #[allow(unused_macros)] macro_rules! bug { () => { compile_error!("provide an error message to help fix a possible bug") }; ($descr:literal $($rest:tt)?) => { unreachable!(concat!("internal error: ", $descr) $($rest)?) } } #[macro_use] mod quote; mod date; mod datetime; mod error; #[cfg(any(feature = "formatting", feature = "parsing"))] mod format_description; mod helpers; mod offset; #[cfg(all(feature = "serde", any(feature = "formatting", feature = "parsing")))] mod serde_format_description; mod time; mod to_tokens; #[cfg(any(feature = "formatting", feature = "parsing"))] use std::iter::Peekable; use proc_macro::TokenStream; #[cfg(any(feature = "formatting", feature = "parsing"))] use proc_macro::{Ident, TokenTree}; use self::error::Error; macro_rules! impl_macros { ($($name:ident)*) => {$( #[proc_macro] pub fn $name(input: TokenStream) -> TokenStream { use crate::to_tokens::ToTokenTree; let mut iter = input.into_iter().peekable(); match $name::parse(&mut iter) { Ok(value) => match iter.peek() { Some(tree) => Error::UnexpectedToken { tree: tree.clone() }.to_compile_error(), None => TokenStream::from(value.into_token_tree()), }, Err(err) => err.to_compile_error(), } } )*}; } impl_macros![date datetime offset time]; #[cfg(any(feature = "formatting", feature = "parsing"))] enum FormatDescriptionVersion { V1, V2, } #[cfg(any(feature = "formatting", feature = "parsing"))] enum VersionOrModuleName { Version(FormatDescriptionVersion), #[cfg_attr(not(feature = "serde"), allow(unused_tuple_struct_fields))] ModuleName(Ident), } #[cfg(any(feature = "formatting", feature = "parsing"))] fn parse_format_description_version( iter: &mut Peekable, ) -> Result, Error> { let version_ident = match iter.peek() { Some(TokenTree::Ident(ident)) if ident.to_string() == "version" => match iter.next() { Some(TokenTree::Ident(ident)) => ident, _ => unreachable!(), }, _ => return Ok(None), }; match iter.peek() { Some(TokenTree::Punct(punct)) if punct.as_char() == '=' => iter.next(), _ if NO_EQUALS_IS_MOD_NAME => { return Ok(Some(VersionOrModuleName::ModuleName(version_ident))); } Some(token) => { return Err(Error::Custom { message: "expected `=`".into(), span_start: Some(token.span()), span_end: Some(token.span()), }); } None => { return Err(Error::Custom { message: "expected `=`".into(), span_start: None, span_end: None, }); } }; let version_literal = match iter.next() { Some(TokenTree::Literal(literal)) => literal, Some(token) => { return Err(Error::Custom { message: "expected 1 or 2".into(), span_start: Some(token.span()), span_end: Some(token.span()), }); } None => { return Err(Error::Custom { message: "expected 1 or 2".into(), span_start: None, span_end: None, }); } }; let version = match version_literal.to_string().as_str() { "1" => FormatDescriptionVersion::V1, "2" => FormatDescriptionVersion::V2, _ => { return Err(Error::Custom { message: "invalid format description version".into(), span_start: Some(version_literal.span()), span_end: Some(version_literal.span()), }); } }; helpers::consume_punct(',', iter)?; Ok(Some(VersionOrModuleName::Version(version))) } #[cfg(any(feature = "formatting", feature = "parsing"))] #[proc_macro] pub fn format_description(input: TokenStream) -> TokenStream { (|| { let mut input = input.into_iter().peekable(); let version = match parse_format_description_version::(&mut input)? { Some(VersionOrModuleName::Version(version)) => Some(version), None => None, // This branch should never occur here, as `false` is the provided as a const parameter. Some(VersionOrModuleName::ModuleName(_)) => bug!("branch should never occur"), }; let (span, string) = helpers::get_string_literal(input)?; let items = format_description::parse_with_version(version, &string, span)?; Ok(quote! {{ const DESCRIPTION: &[::time::format_description::FormatItem<'_>] = &[#S( items .into_iter() .map(|item| quote! { #S(item), }) .collect::() )]; DESCRIPTION }}) })() .unwrap_or_else(|err: Error| err.to_compile_error()) } #[cfg(all(feature = "serde", any(feature = "formatting", feature = "parsing")))] #[proc_macro] pub fn serde_format_description(input: TokenStream) -> TokenStream { (|| { let mut tokens = input.into_iter().peekable(); // First, the optional format description version. let version = parse_format_description_version::(&mut tokens)?; let (version, mod_name) = match version { Some(VersionOrModuleName::ModuleName(module_name)) => (None, Some(module_name)), Some(VersionOrModuleName::Version(version)) => (Some(version), None), None => (None, None), }; // Next, an identifier (the desired module name) // Only parse this if it wasn't parsed when attempting to get the version. let mod_name = match mod_name { Some(mod_name) => mod_name, None => match tokens.next() { Some(TokenTree::Ident(ident)) => Ok(ident), Some(tree) => Err(Error::UnexpectedToken { tree }), None => Err(Error::UnexpectedEndOfInput), }?, }; // Followed by a comma helpers::consume_punct(',', &mut tokens)?; // Then, the type to create serde serializers for (e.g., `OffsetDateTime`). let formattable = match tokens.next() { Some(tree @ TokenTree::Ident(_)) => Ok(tree), Some(tree) => Err(Error::UnexpectedToken { tree }), None => Err(Error::UnexpectedEndOfInput), }?; // Another comma helpers::consume_punct(',', &mut tokens)?; // We now have two options. The user can either provide a format description as a string or // they can provide a path to a format description. If the latter, all remaining tokens are // assumed to be part of the path. let (format, format_description_display) = match tokens.peek() { // string literal Some(TokenTree::Literal(_)) => { let (span, format_string) = helpers::get_string_literal(tokens)?; let items = format_description::parse_with_version(version, &format_string, span)?; let items: TokenStream = items.into_iter().map(|item| quote! { #S(item), }).collect(); let items = quote! { const ITEMS: &[::time::format_description::FormatItem<'_>] = &[#S(items)]; ITEMS }; (items, String::from_utf8_lossy(&format_string).into_owned()) } // path Some(_) => { let tokens = tokens.collect::(); let tokens_string = tokens.to_string(); ( quote! {{ // We can't just do `super::path` because the path could be an absolute // path. In that case, we'd be generating `super::::path`, which is invalid. // Even if we took that into account, it's not possible to know if it's an // external crate, which would just require emitting `path` directly. By // taking this approach, we can leave it to the compiler to do the actual // resolution. mod __path_hack { pub(super) use super::super::*; pub(super) use #S(tokens) as FORMAT; } __path_hack::FORMAT }}, tokens_string, ) } None => return Err(Error::UnexpectedEndOfInput), }; Ok(serde_format_description::build( mod_name, formattable, format, format_description_display, )) })() .unwrap_or_else(|err: Error| err.to_compile_error_standalone()) }