diff --git a/Cargo.lock b/Cargo.lock index 05fbf8a3445..94483fd05ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1604,6 +1604,9 @@ version = "0.1.5" dependencies = [ "databake", "displaydoc", + "postcard", + "serde", + "serde_json", "writeable", "yoke", "zerofrom", diff --git a/utils/pattern/Cargo.toml b/utils/pattern/Cargo.toml index 463da670918..61a96314a93 100644 --- a/utils/pattern/Cargo.toml +++ b/utils/pattern/Cargo.toml @@ -24,16 +24,20 @@ all-features = true displaydoc = { version = "0.2.3", default-features = false } writeable = { workspace = true } databake = { workspace = true, features = ["derive"], optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } yoke = { workspace = true, features = ["derive"], optional = true } zerofrom = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] zerofrom = { workspace = true, features = ["alloc"] } zerovec = { workspace = true, features = ["databake", "serde"] } +serde_json = { version = "1.0" } +postcard = { version = "1.0", features = ["use-std"] } [features] alloc = [] std = ["alloc"] databake = ["dep:databake"] +serde = ["alloc", "dep:serde"] yoke = ["dep:yoke"] zerofrom = ["dep:zerofrom"] diff --git a/utils/pattern/src/common.rs b/utils/pattern/src/common.rs index 939717e2805..ecb030a58a9 100644 --- a/utils/pattern/src/common.rs +++ b/utils/pattern/src/common.rs @@ -32,7 +32,7 @@ pub enum PatternItem<'a, T> { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[allow(clippy::exhaustive_enums)] // Part of core data model #[cfg(feature = "alloc")] -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum PatternItemCow<'a, T> { /// A placeholder of the type specified on this [`PatternItemCow`]. Placeholder(T), @@ -45,6 +45,16 @@ pub enum PatternItemCow<'a, T> { Literal(Cow<'a, str>), } +#[cfg(feature = "alloc")] +impl<'a, T> From> for PatternItemCow<'a, T> { + fn from(value: PatternItem<'a, T>) -> Self { + match value { + PatternItem::Placeholder(t) => Self::Placeholder(t), + PatternItem::Literal(s) => Self::Literal(Cow::Borrowed(s)), + } + } +} + /// Types that implement backing data models for [`Pattern`] implement this trait. /// /// The trait has no public methods and is not implementable outside of this crate. diff --git a/utils/pattern/src/frontend/mod.rs b/utils/pattern/src/frontend/mod.rs index acca9b3866a..f4ff89ed6b3 100644 --- a/utils/pattern/src/frontend/mod.rs +++ b/utils/pattern/src/frontend/mod.rs @@ -4,6 +4,8 @@ #[cfg(feature = "databake")] mod databake; +#[cfg(feature = "serde")] +mod serde; use core::{ fmt::{self, Write}, diff --git a/utils/pattern/src/frontend/serde.rs b/utils/pattern/src/frontend/serde.rs new file mode 100644 index 00000000000..64b84a202a8 --- /dev/null +++ b/utils/pattern/src/frontend/serde.rs @@ -0,0 +1,162 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use super::*; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use core::fmt::Display; + +use ::serde::{Deserialize, Deserializer, Serialize, Serializer}; + +type HumanReadablePattern<'a, B> = Vec::PlaceholderKey>>; + +impl<'de, 'data, B, Store> Deserialize<'de> for Pattern +where + 'de: 'data, + B: PatternBackend, + B::Store: ToOwned + 'de, + &'de B::Store: Deserialize<'de>, + B::PlaceholderKey: Deserialize<'de>, + Store: TryFrom> + AsRef, + Store::Error: Display, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if deserializer.is_human_readable() { + let pattern_items = >::deserialize(deserializer)?; + let pattern_owned: Pattern::Owned> = + Pattern::try_from_items(pattern_items.into_iter()) + .map_err(::custom)?; + let pattern: Pattern = Pattern::from_store_unchecked( + Cow::::Owned(pattern_owned.take_store()) + .try_into() + .map_err(::custom)?, + ); + Ok(pattern) + } else { + let store = <&B::Store>::deserialize(deserializer)?; + let pattern = Self::try_from_store( + Cow::Borrowed(store) + .try_into() + .map_err(::custom)?, + ) + .map_err(::custom)?; + Ok(pattern) + } + } +} + +impl Serialize for Pattern +where + B: PatternBackend, + B::Store: Serialize, + B::PlaceholderKey: Serialize, + Store: AsRef, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if serializer.is_human_readable() { + let pattern_items: HumanReadablePattern = B::iter_items(self.store.as_ref()) + .map(|x| x.into()) + .collect(); + pattern_items.serialize(serializer) + } else { + let bytes = self.store.as_ref(); + bytes.serialize(serializer) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::SinglePlaceholderPattern; + + #[test] + fn test_json() { + let pattern_owned = SinglePlaceholderPattern::try_from_str("Hello, {0}!").unwrap(); + let pattern_cow: SinglePlaceholderPattern> = + SinglePlaceholderPattern::from_store_unchecked(Cow::Owned(pattern_owned.take_store())); + let pattern_json = serde_json::to_string(&pattern_cow).unwrap(); + assert_eq!( + pattern_json, + r#"[{"Literal":"Hello, "},{"Placeholder":"Singleton"},{"Literal":"!"}]"# + ); + let pattern_deserialized: SinglePlaceholderPattern> = + serde_json::from_str(&pattern_json).unwrap(); + assert_eq!(pattern_cow, pattern_deserialized); + assert!(matches!(pattern_deserialized.take_store(), Cow::Owned(_))); + } + + #[test] + fn test_postcard() { + let pattern_owned = SinglePlaceholderPattern::try_from_str("Hello, {0}!").unwrap(); + let pattern_cow: SinglePlaceholderPattern> = + SinglePlaceholderPattern::from_store_unchecked(Cow::Owned(pattern_owned.take_store())); + let pattern_postcard = postcard::to_stdvec(&pattern_cow).unwrap(); + assert_eq!(pattern_postcard, b"\x09\x08Hello, !"); + let pattern_deserialized: SinglePlaceholderPattern> = + postcard::from_bytes(&pattern_postcard).unwrap(); + assert_eq!(pattern_cow, pattern_deserialized); + assert!(matches!( + pattern_deserialized.take_store(), + Cow::Borrowed(_) + )); + } + + macro_rules! check_store { + ($store:expr, $ty:ty) => { + check_store!(@borrow, $store, $ty); + let json = serde_json::to_string::>( + &SinglePlaceholderPattern::from_store_unchecked($store.clone()), + ) + .unwrap(); + let de_json = serde_json::from_str::>(&json).unwrap(); + assert_eq!(de_json.take_store(), $store); + }; + (@borrow, $store:expr, $ty:ty) => { + let postcard = postcard::to_stdvec::>( + &SinglePlaceholderPattern::from_store_unchecked($store.clone()), + ) + .unwrap(); + let de_postcard = postcard::from_bytes::>(&postcard).unwrap(); + assert_eq!(de_postcard.take_store(), $store); + }; + } + + #[test] + fn test_serde_stores() { + let store = SinglePlaceholderPattern::try_from_str("Hello, {0}!") + .unwrap() + .take_store(); + + check_store!(Cow::Borrowed(store.as_str()), Cow); + check_store!(Cow::::Owned(store.clone()), Cow); + check_store!(store.clone(), String); + + /// A type implementing TryFrom> that returns an error if the Cow is Owned + #[derive(Debug, Clone, PartialEq, displaydoc::Display)] + struct MyStr<'a>(&'a str); + impl<'a> TryFrom> for MyStr<'a> { + type Error = &'static str; + fn try_from(input: Cow<'a, str>) -> Result, Self::Error> { + match input { + Cow::Borrowed(s) => Ok(MyStr(s)), + Cow::Owned(_) => Err("cannot borrow from a Cow with needed lifetime"), + } + } + } + impl AsRef for MyStr<'_> { + fn as_ref(&self) -> &str { + self.0 + } + } + + check_store!(@borrow, MyStr(store.as_str()), MyStr); + } +} diff --git a/utils/pattern/src/single.rs b/utils/pattern/src/single.rs index 0dd57565101..b26b6362942 100644 --- a/utils/pattern/src/single.rs +++ b/utils/pattern/src/single.rs @@ -41,6 +41,7 @@ use alloc::string::String; /// ); /// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[allow(clippy::exhaustive_enums)] // Singleton pub enum SinglePlaceholderKey { Singleton, diff --git a/utils/pattern/tests/derive_test.rs b/utils/pattern/tests/derive_test.rs index 5adf0de9803..5ad59cdc319 100644 --- a/utils/pattern/tests/derive_test.rs +++ b/utils/pattern/tests/derive_test.rs @@ -11,24 +11,56 @@ use icu_pattern::{Pattern, SinglePlaceholder}; #[cfg_attr(feature = "yoke", derive(yoke::Yokeable))] #[cfg_attr(feature = "zerofrom", derive(zerofrom::ZeroFrom))] -// #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "databake", derive(databake::Bake), databake(path = crate))] -struct DeriveTest_SinglePlaceholderPattern_ZeroVec<'data> { - // #[cfg_attr(feature = "serde", serde(borrow))] - _data: Pattern>, +#[derive(Debug, PartialEq)] +struct DeriveTest_SinglePlaceholderPattern_Cow<'data> { + #[cfg_attr(feature = "serde", serde(borrow))] + data: Pattern>, } #[test] #[cfg(all(feature = "databake", feature = "alloc"))] -fn bake_SinglePlaceholderPattern_ZeroVec() { +fn bake_SinglePlaceholderPattern_Cow() { use databake::*; extern crate std; test_bake!( - DeriveTest_SinglePlaceholderPattern_ZeroVec<'static>, - crate::DeriveTest_SinglePlaceholderPattern_ZeroVec { - _data: icu_pattern::Pattern::::from_store_unchecked( + DeriveTest_SinglePlaceholderPattern_Cow<'static>, + crate::DeriveTest_SinglePlaceholderPattern_Cow { + data: icu_pattern::Pattern::::from_store_unchecked( alloc::borrow::Cow::Borrowed(""), ) }, ); } + +#[test] +#[cfg(feature = "serde")] +fn json_SinglePlaceholderPattern_Cow() { + let pattern_owned = Pattern::::try_from_str("Hello, {0}!").unwrap(); + let pattern_cow: Pattern> = + Pattern::from_store_unchecked(Cow::Owned(pattern_owned.take_store())); + let data = DeriveTest_SinglePlaceholderPattern_Cow { data: pattern_cow }; + let data_json = serde_json::to_string(&data).unwrap(); + assert_eq!( + data_json, + r#"{"data":[{"Literal":"Hello, "},{"Placeholder":"Singleton"},{"Literal":"!"}]}"# + ); + let data_deserialized: DeriveTest_SinglePlaceholderPattern_Cow = + serde_json::from_str(&data_json).unwrap(); + assert_eq!(data, data_deserialized); +} + +#[test] +#[cfg(feature = "serde")] +fn postcard_SinglePlaceholderPattern_Cow() { + let pattern_owned = Pattern::::try_from_str("Hello, {0}!").unwrap(); + let pattern_cow: Pattern> = + Pattern::from_store_unchecked(Cow::Owned(pattern_owned.take_store())); + let data = DeriveTest_SinglePlaceholderPattern_Cow { data: pattern_cow }; + let data_postcard = postcard::to_stdvec(&data).unwrap(); + assert_eq!(data_postcard, b"\x09\x08Hello, !"); + let data_deserialized: DeriveTest_SinglePlaceholderPattern_Cow = + postcard::from_bytes(&data_postcard).unwrap(); + assert_eq!(data, data_deserialized); +}