diff --git a/src/macros.rs b/src/macros.rs index fbd3be004..95f7e859c 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -31,42 +31,6 @@ pub use time_macros::date; /// [`OffsetDateTime`]: crate::OffsetDateTime /// [`PrimitiveDateTime`]: crate::PrimitiveDateTime pub use time_macros::datetime; -/// Declares a custom format based on the provided string. -/// -/// The syntax accepted by this macro is the same as [`format_description::parse()`], which can -/// be found in [the book](https://time-rs.github.io/book/api/format-description.html). -/// -/// # Usage -/// -/// Invoked as -/// `declare_format_string!(mod_name, Date, "")`: puts -/// a module named `mod_name` in the current namespace that can be used to -/// format `Date` structs. -/// -/// # Examples -/// -/// ``` -/// # use time::OffsetDateTime; -/// # use time::macros::declare_format_string; -/// # use serde::{Serialize, Deserialize}; -/// // Makes a module `mod my_format { ... }`. -/// declare_format_string!(my_format, OffsetDateTime, "hour=[hour], minute=[minute]"); -/// -/// #[derive(Serialize, Deserialize)] -/// struct SerializesWithCustom { -/// #[serde(with = "my_format")] -/// dt: OffsetDateTime, -/// #[serde(with = "my_format::option")] -/// maybe_dt: Option, -/// } -/// # -/// # // otherwise rustdoc tests don't work because we put a module in `main()` -/// # fn main() {} -/// ``` -/// -/// [`format_description::parse()`]: crate::format_description::parse() -#[cfg(feature = "serde-human-readable")] -pub use time_macros::declare_format_string; /// Equivalent of performing [`format_description::parse()`] at compile time. /// /// Using the macro instead of the function results in a static slice rather than a [`Vec`], diff --git a/src/serde/mod.rs b/src/serde/mod.rs index bcb2e4833..0a5464e0b 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -25,6 +25,43 @@ use core::marker::PhantomData; #[cfg(feature = "serde-human-readable")] use serde::ser::Error as _; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +/// Generate a custom serializer and deserializer from the provided string. +/// +/// The syntax accepted by this macro is the same as [`format_description::parse()`], which can +/// be found in [the book](https://time-rs.github.io/book/api/format-description.html). +/// +/// # Usage +/// +/// Invoked as `serde::format_description!(mod_name, Date, "")`. This puts a +/// module named `mod_name` in the current scope that can be used to format `Date` structs. A +/// submodule (`mod_name::option`) is also generated for `Option`. Both modules are only +/// visible in the current scope. +/// +/// # Examples +/// +/// ``` +/// # use time::OffsetDateTime; +/// # use ::serde::{Serialize, Deserialize}; +/// use time::serde; +/// +/// // Makes a module `mod my_format { ... }`. +/// serde::format_description!(my_format, OffsetDateTime, "hour=[hour], minute=[minute]"); +/// +/// #[derive(Serialize, Deserialize)] +/// struct SerializesWithCustom { +/// #[serde(with = "my_format")] +/// dt: OffsetDateTime, +/// #[serde(with = "my_format::option")] +/// maybe_dt: Option, +/// } +/// # +/// # // otherwise rustdoc tests don't work because we put a module in `main()` +/// # fn main() {} +/// ``` +/// +/// [`format_description::parse()`]: crate::format_description::parse() +#[cfg(all(feature = "macros", feature = "serde-human-readable"))] +pub use time_macros::serde_format_description as format_description; use self::visitor::Visitor; #[cfg(feature = "parsing")] diff --git a/tests/integration/compile-fail/invalid_serializer.rs b/tests/integration/compile-fail/invalid_serializer.rs index 546c313d9..284d0b783 100644 --- a/tests/integration/compile-fail/invalid_serializer.rs +++ b/tests/integration/compile-fail/invalid_serializer.rs @@ -1,16 +1,16 @@ -use time::macros::declare_format_string; +use time::serde; -declare_format_string!(); // unexpected end of input -declare_format_string!("bad string", OffsetDateTime, "[year] [month]"); // module name is not ident -declare_format_string!(my_format: OffsetDateTime, "[year] [month]"); // not a comma -declare_format_string!(my_format,); // missing formattable and string -declare_format_string!(my_format, "[year] [month]"); // missing formattable -declare_format_string!(OffsetDateTime, "[year] [month]"); // missing ident -declare_format_string!(my_format, OffsetDateTime); // missing string format -declare_format_string!(my_format, OffsetDateTime,); // missing string format -declare_format_string!(my_format, OffsetDateTime "[year] [month]"); // missing comma -declare_format_string!(my_format, OffsetDateTime : "[year] [month]"); // not a comma -declare_format_string!(my_format, OffsetDateTime, "[bad]"); // bad component name -declare_format_string!(my_format, OffsetDateTime, not_string); // string format wrong type +serde::format_description!(); // unexpected end of input +serde::format_description!("bad string", OffsetDateTime, "[year] [month]"); // module name is not ident +serde::format_description!(my_format: OffsetDateTime, "[year] [month]"); // not a comma +serde::format_description!(my_format,); // missing formattable and string +serde::format_description!(my_format, "[year] [month]"); // missing formattable +serde::format_description!(OffsetDateTime, "[year] [month]"); // missing ident +serde::format_description!(my_format, OffsetDateTime); // missing string format +serde::format_description!(my_format, OffsetDateTime,); // missing string format +serde::format_description!(my_format, OffsetDateTime "[year] [month]"); // missing comma +serde::format_description!(my_format, OffsetDateTime : "[year] [month]"); // not a comma +serde::format_description!(my_format, OffsetDateTime, "[bad]"); // bad component name +serde::format_description!(my_format, OffsetDateTime, not_string); // string format wrong type fn main() {} diff --git a/tests/integration/compile-fail/invalid_serializer.stderr b/tests/integration/compile-fail/invalid_serializer.stderr index ff6888161..7b5e3db2c 100644 --- a/tests/integration/compile-fail/invalid_serializer.stderr +++ b/tests/integration/compile-fail/invalid_serializer.stderr @@ -1,79 +1,79 @@ error: unexpected end of input --> $DIR/invalid_serializer.rs:3:1 | -3 | declare_format_string!(); // unexpected end of input - | ^^^^^^^^^^^^^^^^^^^^^^^^ +3 | serde::format_description!(); // unexpected end of input + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: this error originates in the macro `declare_format_string` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `serde::format_description` (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected token: "bad string" - --> $DIR/invalid_serializer.rs:4:24 + --> $DIR/invalid_serializer.rs:4:28 | -4 | declare_format_string!("bad string", OffsetDateTime, "[year] [month]"); // module name is not ident - | ^^^^^^^^^^^^ +4 | serde::format_description!("bad string", OffsetDateTime, "[year] [month]"); // module name is not ident + | ^^^^^^^^^^^^ error: unexpected token: : - --> $DIR/invalid_serializer.rs:5:33 + --> $DIR/invalid_serializer.rs:5:37 | -5 | declare_format_string!(my_format: OffsetDateTime, "[year] [month]"); // not a comma - | ^ +5 | serde::format_description!(my_format: OffsetDateTime, "[year] [month]"); // not a comma + | ^ error: unexpected end of input --> $DIR/invalid_serializer.rs:6:1 | -6 | declare_format_string!(my_format,); // missing formattable and string - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +6 | serde::format_description!(my_format,); // missing formattable and string + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: this error originates in the macro `declare_format_string` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `serde::format_description` (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected token: "[year] [month]" - --> $DIR/invalid_serializer.rs:7:35 + --> $DIR/invalid_serializer.rs:7:39 | -7 | declare_format_string!(my_format, "[year] [month]"); // missing formattable - | ^^^^^^^^^^^^^^^^ +7 | serde::format_description!(my_format, "[year] [month]"); // missing formattable + | ^^^^^^^^^^^^^^^^ error: unexpected token: "[year] [month]" - --> $DIR/invalid_serializer.rs:8:40 + --> $DIR/invalid_serializer.rs:8:44 | -8 | declare_format_string!(OffsetDateTime, "[year] [month]"); // missing ident - | ^^^^^^^^^^^^^^^^ +8 | serde::format_description!(OffsetDateTime, "[year] [month]"); // missing ident + | ^^^^^^^^^^^^^^^^ error: unexpected end of input --> $DIR/invalid_serializer.rs:9:1 | -9 | declare_format_string!(my_format, OffsetDateTime); // missing string format - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +9 | serde::format_description!(my_format, OffsetDateTime); // missing string format + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: this error originates in the macro `declare_format_string` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `serde::format_description` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected string --> $DIR/invalid_serializer.rs:10:1 | -10 | declare_format_string!(my_format, OffsetDateTime,); // missing string format - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +10 | serde::format_description!(my_format, OffsetDateTime,); // missing string format + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: this error originates in the macro `declare_format_string` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `serde::format_description` (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected token: "[year] [month]" - --> $DIR/invalid_serializer.rs:11:50 + --> $DIR/invalid_serializer.rs:11:54 | -11 | declare_format_string!(my_format, OffsetDateTime "[year] [month]"); // missing comma - | ^^^^^^^^^^^^^^^^ +11 | serde::format_description!(my_format, OffsetDateTime "[year] [month]"); // missing comma + | ^^^^^^^^^^^^^^^^ error: unexpected token: : - --> $DIR/invalid_serializer.rs:12:50 + --> $DIR/invalid_serializer.rs:12:54 | -12 | declare_format_string!(my_format, OffsetDateTime : "[year] [month]"); // not a comma - | ^ +12 | serde::format_description!(my_format, OffsetDateTime : "[year] [month]"); // not a comma + | ^ error: invalid component name `bad` at byte index 1 - --> $DIR/invalid_serializer.rs:13:51 + --> $DIR/invalid_serializer.rs:13:55 | -13 | declare_format_string!(my_format, OffsetDateTime, "[bad]"); // bad component name - | ^^^^^^^ +13 | serde::format_description!(my_format, OffsetDateTime, "[bad]"); // bad component name + | ^^^^^^^ error: expected string - --> $DIR/invalid_serializer.rs:14:51 + --> $DIR/invalid_serializer.rs:14:55 | -14 | declare_format_string!(my_format, OffsetDateTime, not_string); // string format wrong type - | ^^^^^^^^^^ +14 | serde::format_description!(my_format, OffsetDateTime, not_string); // string format wrong type + | ^^^^^^^^^^ diff --git a/tests/integration/serde/macros.rs b/tests/integration/serde/macros.rs index 4c7fee22d..ab66df6b7 100644 --- a/tests/integration/serde/macros.rs +++ b/tests/integration/serde/macros.rs @@ -1,27 +1,23 @@ -use serde::{Deserialize, Serialize}; +use ::serde::{Deserialize, Serialize}; use serde_test::{ assert_de_tokens_error, assert_ser_tokens_error, assert_tokens, Configure, Token, }; -use time::macros::{date, datetime, declare_format_string, offset}; -use time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; +use time::macros::{date, datetime, offset}; +use time::{serde, Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; -declare_format_string!( +serde::format_description!( offset_dt_format, OffsetDateTime, "custom format: [year]-[month]-[day] [hour]:[minute]:[second] [offset_hour]:[offset_minute]" ); - -declare_format_string!( +serde::format_description!( primitive_dt_format, PrimitiveDateTime, "custom format: [year]-[month]-[day] [hour]:[minute]:[second]" ); - -declare_format_string!(time_format, Time, "custom format: [minute]:[second]"); - -declare_format_string!(date_format, Date, "custom format: [year]-[month]-[day]"); - -declare_format_string!( +serde::format_description!(time_format, Time, "custom format: [minute]:[second]"); +serde::format_description!(date_format, Date, "custom format: [year]-[month]-[day]"); +serde::format_description!( offset_format, UtcOffset, "custom format: [offset_hour]:[offset_minute]" @@ -131,9 +127,8 @@ fn custom_serialize_error() { ); } -// This format string has offset_hour and offset_minute, but is for formatting -// PrimitiveDateTime. -declare_format_string!( +// This format string has offset_hour and offset_minute, but is for formatting PrimitiveDateTime. +serde::format_description!( primitive_date_time_format_bad, PrimitiveDateTime, "[offset_hour]:[offset_minute]" diff --git a/time-macros/src/lib.rs b/time-macros/src/lib.rs index 572e789b8..9a127510f 100644 --- a/time-macros/src/lib.rs +++ b/time-macros/src/lib.rs @@ -40,11 +40,10 @@ mod error; mod format_description; mod helpers; mod offset; +mod serde_format_description; mod time; mod to_tokens; -use std::iter::FromIterator; - use proc_macro::{TokenStream, TokenTree}; use self::error::Error; @@ -90,127 +89,8 @@ pub fn format_description(input: TokenStream) -> TokenStream { .unwrap_or_else(|err: Error| err.to_compile_error()) } -fn make_serde_serializer_module( - mod_name: proc_macro::Ident, - items: impl to_tokens::ToTokens, - formattable: TokenStream, - format_string: &str, -) -> TokenStream { - let visitor_struct = quote! { - struct Visitor(::core::marker::PhantomData); - - impl<'a> ::serde::de::Visitor<'a> for Visitor<#(formattable.clone())> { - type Value = #(formattable.clone()); - - fn expecting(&self, formatter: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - // `write!` macro confuses `quote!` so format our message manually - formatter.write_str("a(n) `")?; - formatter.write_str(#(formattable.to_string()))?; - formatter.write_str("` in the format \"")?; - formatter.write_str(&FORMAT_STRING)?; - formatter.write_str("\"") - } - - fn visit_str( - self, - value: &str - ) -> Result { - #(formattable.clone())::parse(value, &DESCRIPTION).map_err(E::custom) - } - } - - impl<'a> ::serde::de::Visitor<'a> for Visitor> { - type Value = Option<#(formattable.clone())>; - - fn expecting(&self, formatter: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - // `write!` macro confuses `quote!` so format our message manually - formatter.write_str("an `Option<")?; - formatter.write_str(#(formattable.to_string()))?; - formatter.write_str(">` in the format \"")?; - formatter.write_str(&FORMAT_STRING)?; - formatter.write_str("\"") - } - - fn visit_some>( - self, - deserializer: D - ) -> Result { - let visitor = Visitor::<#(formattable.clone())>(::core::marker::PhantomData); - deserializer - .deserialize_any(visitor) - .map(Some) - } - - fn visit_none( - self - ) -> Result, E> { - Ok(None) - } - } - - }; - let serialize_fns = quote! { - pub fn serialize( - datetime: &#(formattable.clone()), - serializer: S, - ) -> Result { - use ::serde::Serialize; - datetime - .format(&DESCRIPTION) - .map_err(::time::error::Format::into_invalid_serde_value::)? - .serialize(serializer) - } - - pub fn deserialize<'a, D: ::serde::Deserializer<'a>>( - deserializer: D - ) -> Result<#(formattable.clone()), D::Error> { - use ::serde::Deserialize; - let visitor = Visitor::<#(formattable.clone())>(::core::marker::PhantomData); - deserializer.deserialize_any(visitor) - } - }; - let option_serialize_fns = quote! { - pub fn serialize( - option: &Option<#(formattable.clone())>, - serializer: S, - ) -> Result { - use ::serde::Serialize; - option.map(|datetime| datetime.format(&DESCRIPTION)) - .transpose() - .map_err(::time::error::Format::into_invalid_serde_value::)? - .serialize(serializer) - } - - pub fn deserialize<'a, D: ::serde::Deserializer<'a>>( - deserializer: D - ) -> Result, D::Error> { - use ::serde::Deserialize; - let visitor = Visitor::>(::core::marker::PhantomData); - deserializer.deserialize_option(visitor) - } - }; - - quote! {mod #(mod_name) { - use ::time::#(formattable.clone()); - - const DESCRIPTION: &[::time::format_description::FormatItem<'_>] = &[#(items)]; - const FORMAT_STRING: &str = #(format_string); - - #(visitor_struct) - - #(serialize_fns) - - pub mod option { - use super::{DESCRIPTION, #(formattable), Visitor}; - - #(option_serialize_fns) - } - } - } -} - #[proc_macro] -pub fn declare_format_string(input: TokenStream) -> TokenStream { +pub fn serde_format_description(input: TokenStream) -> TokenStream { (|| { let mut tokens = input.into_iter().peekable(); // First, an identifier (the desired module name) @@ -234,17 +114,16 @@ pub fn declare_format_string(input: TokenStream) -> TokenStream { helpers::consume_punct(',', &mut tokens)?; // Then, a string literal. - let input = TokenStream::from_iter(tokens); - let (span, format_string) = helpers::get_string_literal(input)?; + let (span, format_string) = helpers::get_string_literal(tokens.collect())?; let items = format_description::parse(&format_string, span)?; let items: TokenStream = items.into_iter().map(|item| quote! { #(item), }).collect(); - Ok(make_serde_serializer_module( + Ok(serde_format_description::build( mod_name, items, formattable.into(), - std::str::from_utf8(&format_string).unwrap(), + &String::from_utf8_lossy(&format_string), )) })() .unwrap_or_else(|err: Error| err.to_compile_error_standalone()) diff --git a/time-macros/src/quote.rs b/time-macros/src/quote.rs index 59255c1e7..a849f244e 100644 --- a/time-macros/src/quote.rs +++ b/time-macros/src/quote.rs @@ -96,6 +96,11 @@ macro_rules! quote_inner { ::proc_macro::Punct::new('?', ::proc_macro::Spacing::Alone) )), ])); + ([! $($tail:tt)*] -> [$($accum:tt)*]) => (quote_inner!([$($tail)*] -> [$($accum)* + ::proc_macro::TokenStream::from(::proc_macro::TokenTree::from( + ::proc_macro::Punct::new('!', ::proc_macro::Spacing::Alone) + )), + ])); ([| $($tail:tt)*] -> [$($accum:tt)*] ) => (quote_inner!([$($tail)*] -> [$($accum)* ::proc_macro::TokenStream::from(::proc_macro::TokenTree::from( ::proc_macro::Punct::new('|', ::proc_macro::Spacing::Alone) diff --git a/time-macros/src/serde_format_description.rs b/time-macros/src/serde_format_description.rs new file mode 100644 index 000000000..d05d9d9ad --- /dev/null +++ b/time-macros/src/serde_format_description.rs @@ -0,0 +1,131 @@ +use proc_macro::{Ident, TokenStream}; + +use crate::to_tokens; + +pub(crate) fn build( + mod_name: Ident, + items: impl to_tokens::ToTokens, + ty: TokenStream, + format_string: &str, +) -> TokenStream { + let visitor = quote! { + struct Visitor(::core::marker::PhantomData); + + impl<'a> ::serde::de::Visitor<'a> for Visitor<#(ty.clone())> { + type Value = #(ty.clone()); + + fn expecting(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!( + f, + concat!( + "a(n) `", + #(ty.to_string()), + "` in the format \"", + #(format_string), + "\"", + ) + ) + } + + fn visit_str( + self, + value: &str + ) -> Result { + #(ty.clone())::parse(value, &DESCRIPTION).map_err(E::custom) + } + } + + impl<'a> ::serde::de::Visitor<'a> for Visitor> { + type Value = Option<#(ty.clone())>; + + fn expecting(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!( + f, + concat!( + "an `Option<", + #(ty.to_string()), + ">` in the format \"", + #(format_string), + "\"", + ) + ) + } + + fn visit_some>( + self, + deserializer: D + ) -> Result { + let visitor = Visitor::<#(ty.clone())>(::core::marker::PhantomData); + deserializer + .deserialize_any(visitor) + .map(Some) + } + + fn visit_none( + self + ) -> Result, E> { + Ok(None) + } + } + + }; + + let primary_fns = quote! { + pub fn serialize( + datetime: &#(ty.clone()), + serializer: S, + ) -> Result { + use ::serde::Serialize; + datetime + .format(&DESCRIPTION) + .map_err(::time::error::Format::into_invalid_serde_value::)? + .serialize(serializer) + } + + pub fn deserialize<'a, D: ::serde::Deserializer<'a>>( + deserializer: D + ) -> Result<#(ty.clone()), D::Error> { + use ::serde::Deserialize; + let visitor = Visitor::<#(ty.clone())>(::core::marker::PhantomData); + deserializer.deserialize_any(visitor) + } + }; + + let options_fns = quote! { + pub fn serialize( + option: &Option<#(ty.clone())>, + serializer: S, + ) -> Result { + use ::serde::Serialize; + option.map(|datetime| datetime.format(&DESCRIPTION)) + .transpose() + .map_err(::time::error::Format::into_invalid_serde_value::)? + .serialize(serializer) + } + + pub fn deserialize<'a, D: ::serde::Deserializer<'a>>( + deserializer: D + ) -> Result, D::Error> { + use ::serde::Deserialize; + let visitor = Visitor::>(::core::marker::PhantomData); + deserializer.deserialize_option(visitor) + } + }; + + quote! { + mod #(mod_name) { + use ::time::#(ty.clone()); + + const DESCRIPTION: &[::time::format_description::FormatItem<'_>] = &[#(items)]; + + #(visitor) + #(primary_fns) + + pub(super) mod option { + use super::{DESCRIPTION, #(ty), Visitor}; + + #(options_fns) + } + } + } +}