Skip to content

Commit

Permalink
Implement Serde for Pattern (#4665)
Browse files Browse the repository at this point in the history
  • Loading branch information
sffc authored Mar 8, 2024
1 parent a51fa55 commit 8387180
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 9 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions utils/pattern/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
12 changes: 11 additions & 1 deletion utils/pattern/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -45,6 +45,16 @@ pub enum PatternItemCow<'a, T> {
Literal(Cow<'a, str>),
}

#[cfg(feature = "alloc")]
impl<'a, T> From<PatternItem<'a, T>> 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.
Expand Down
2 changes: 2 additions & 0 deletions utils/pattern/src/frontend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#[cfg(feature = "databake")]
mod databake;
#[cfg(feature = "serde")]
mod serde;

use core::{
fmt::{self, Write},
Expand Down
162 changes: 162 additions & 0 deletions utils/pattern/src/frontend/serde.rs
Original file line number Diff line number Diff line change
@@ -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<PatternItemCow<'a, <B as PatternBackend>::PlaceholderKey>>;

impl<'de, 'data, B, Store> Deserialize<'de> for Pattern<B, Store>
where
'de: 'data,
B: PatternBackend,
B::Store: ToOwned + 'de,
&'de B::Store: Deserialize<'de>,
B::PlaceholderKey: Deserialize<'de>,
Store: TryFrom<Cow<'data, B::Store>> + AsRef<B::Store>,
Store::Error: Display,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
if deserializer.is_human_readable() {
let pattern_items = <HumanReadablePattern<B>>::deserialize(deserializer)?;
let pattern_owned: Pattern<B, <B::Store as ToOwned>::Owned> =
Pattern::try_from_items(pattern_items.into_iter())
.map_err(<D::Error as ::serde::de::Error>::custom)?;
let pattern: Pattern<B, Store> = Pattern::from_store_unchecked(
Cow::<B::Store>::Owned(pattern_owned.take_store())
.try_into()
.map_err(<D::Error as ::serde::de::Error>::custom)?,
);
Ok(pattern)
} else {
let store = <&B::Store>::deserialize(deserializer)?;
let pattern = Self::try_from_store(
Cow::Borrowed(store)
.try_into()
.map_err(<D::Error as ::serde::de::Error>::custom)?,
)
.map_err(<D::Error as ::serde::de::Error>::custom)?;
Ok(pattern)
}
}
}

impl<B, Store> Serialize for Pattern<B, Store>
where
B: PatternBackend,
B::Store: Serialize,
B::PlaceholderKey: Serialize,
Store: AsRef<B::Store>,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if serializer.is_human_readable() {
let pattern_items: HumanReadablePattern<B> = 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<Cow<str>> =
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<Cow<str>> =
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<Cow<str>> =
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<Cow<str>> =
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<$ty>>(
&SinglePlaceholderPattern::from_store_unchecked($store.clone()),
)
.unwrap();
let de_json = serde_json::from_str::<SinglePlaceholderPattern<$ty>>(&json).unwrap();
assert_eq!(de_json.take_store(), $store);
};
(@borrow, $store:expr, $ty:ty) => {
let postcard = postcard::to_stdvec::<SinglePlaceholderPattern<$ty>>(
&SinglePlaceholderPattern::from_store_unchecked($store.clone()),
)
.unwrap();
let de_postcard = postcard::from_bytes::<SinglePlaceholderPattern<$ty>>(&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<str>);
check_store!(Cow::<str>::Owned(store.clone()), Cow<str>);
check_store!(store.clone(), String);

/// A type implementing TryFrom<Cow<str>> that returns an error if the Cow is Owned
#[derive(Debug, Clone, PartialEq, displaydoc::Display)]
struct MyStr<'a>(&'a str);
impl<'a> TryFrom<Cow<'a, str>> for MyStr<'a> {
type Error = &'static str;
fn try_from(input: Cow<'a, str>) -> Result<MyStr<'a>, Self::Error> {
match input {
Cow::Borrowed(s) => Ok(MyStr(s)),
Cow::Owned(_) => Err("cannot borrow from a Cow with needed lifetime"),
}
}
}
impl AsRef<str> for MyStr<'_> {
fn as_ref(&self) -> &str {
self.0
}
}

check_store!(@borrow, MyStr(store.as_str()), MyStr);
}
}
1 change: 1 addition & 0 deletions utils/pattern/src/single.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
48 changes: 40 additions & 8 deletions utils/pattern/tests/derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SinglePlaceholder, Cow<'data, str>>,
#[derive(Debug, PartialEq)]
struct DeriveTest_SinglePlaceholderPattern_Cow<'data> {
#[cfg_attr(feature = "serde", serde(borrow))]
data: Pattern<SinglePlaceholder, Cow<'data, str>>,
}

#[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::<icu_pattern::SinglePlaceholder, _>::from_store_unchecked(
DeriveTest_SinglePlaceholderPattern_Cow<'static>,
crate::DeriveTest_SinglePlaceholderPattern_Cow {
data: icu_pattern::Pattern::<icu_pattern::SinglePlaceholder, _>::from_store_unchecked(
alloc::borrow::Cow::Borrowed(""),
)
},
);
}

#[test]
#[cfg(feature = "serde")]
fn json_SinglePlaceholderPattern_Cow() {
let pattern_owned = Pattern::<SinglePlaceholder, String>::try_from_str("Hello, {0}!").unwrap();
let pattern_cow: Pattern<SinglePlaceholder, Cow<str>> =
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::<SinglePlaceholder, String>::try_from_str("Hello, {0}!").unwrap();
let pattern_cow: Pattern<SinglePlaceholder, Cow<str>> =
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);
}

0 comments on commit 8387180

Please sign in to comment.