From 43b0a3843bf34ec5303b55c003996281a9521823 Mon Sep 17 00:00:00 2001 From: Paskal Sitepu Date: Thu, 27 Apr 2023 23:27:13 +0700 Subject: [PATCH] glib: Add optional support for serialization and deserialization with serde This feature is gated as `serde` Supports both serialization and deserialization: - glib::ByteArray - glib::Bytes - glib::GString Supports serialization only: - glib::GStr - glib::StrV Collection types are also supported as long as the type parameters implement the necessary traits: - glib::Slice - glib::PtrSlice - glib::List - glib::SList --- .github/workflows/CI.yml | 1 + .github/workflows/windows.yml | 1 + glib/Cargo.toml | 13 ++- glib/src/lib.rs | 3 + glib/src/serde/byte_array.rs | 53 ++++++++++++ glib/src/serde/bytes.rs | 51 ++++++++++++ glib/src/serde/collections.rs | 146 ++++++++++++++++++++++++++++++++ glib/src/serde/gstring.rs | 48 +++++++++++ glib/src/serde/mod.rs | 151 ++++++++++++++++++++++++++++++++++ 9 files changed, 463 insertions(+), 4 deletions(-) create mode 100644 glib/src/serde/byte_array.rs create mode 100644 glib/src/serde/bytes.rs create mode 100644 glib/src/serde/collections.rs create mode 100644 glib/src/serde/gstring.rs create mode 100644 glib/src/serde/mod.rs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 85fd5cd41d53..24787eb18b67 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -24,6 +24,7 @@ jobs: - { name: "gdk-pixbuf", features: "v2_42", nightly: "--all-features", test_sys: true } - { name: "gio", features: "v2_78", nightly: "--all-features", test_sys: true } - { name: "glib", features: "v2_78", nightly: "--all-features", test_sys: true } + - { name: "glib", features: "v2_78,serde", nightly: "--all-features", test_sys: true } - { name: "graphene", features: "v1_12", nightly: "", test_sys: true } - { name: "pango", features: "v1_50", nightly: "--all-features", test_sys: true } - { name: "pangocairo", features: "", nightly: "--all-features", test_sys: true } diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 2a60bcebc89b..df7f8551c616 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -82,6 +82,7 @@ jobs: - { name: "gdk-pixbuf", test: true, args: "--features v2_42" } - { name: "gio", test: true, args: "--features v2_74" } - { name: "glib", test: true, args: "--features v2_74" } + - { name: "glib", test: true, args: "--features v2_74,serde" } - { name: "glib-build-tools", test: false, args: "" } - { name: "graphene", test: false, args: "" } - { name: "pango", test: true, args: "--features v1_50" } diff --git a/glib/Cargo.toml b/glib/Cargo.toml index ba43a0496fa6..e66b9da56232 100644 --- a/glib/Cargo.toml +++ b/glib/Cargo.toml @@ -9,9 +9,7 @@ version = "0.19.0" keywords = ["glib", "gtk-rs", "gnome", "GUI"] repository = "https://github.com/gtk-rs/gtk-rs-core" license = "MIT" -exclude = [ - "gir-files/*", -] +exclude = ["gir-files/*"] edition = "2021" rust-version = "1.70" @@ -31,16 +29,22 @@ ffi = { package = "glib-sys", path = "sys" } gobject_ffi = { package = "gobject-sys", path = "gobject-sys" } glib-macros = { path = "../glib-macros" } rs-log = { package = "log", version = "0.4", optional = true } -smallvec = { version = "1.11", features = ["union", "const_generics", "const_new"] } +smallvec = { version = "1.11", features = [ + "union", + "const_generics", + "const_new", +] } thiserror = "1" gio_ffi = { package = "gio-sys", path = "../gio/sys", optional = true } memchr = "2.7.1" +serde = { version = "1.0", optional = true } [dev-dependencies] tempfile = "3" gir-format-check = "^0.1" trybuild2 = "1" criterion = "0.5.1" +serde_json = "1.0" [features] default = ["gio"] @@ -59,6 +63,7 @@ log = ["rs-log"] log_macros = ["log"] compiletests = [] gio = ["gio_ffi"] +serde = ["dep:serde"] [package.metadata.docs.rs] all-features = true diff --git a/glib/src/lib.rs b/glib/src/lib.rs index b9f6b3df9056..ab893128e76d 100644 --- a/glib/src/lib.rs +++ b/glib/src/lib.rs @@ -227,6 +227,9 @@ pub use self::thread_pool::{ThreadHandle, ThreadPool}; pub mod thread_guard; +#[cfg(feature = "serde")] +mod serde; + // rustdoc-stripper-ignore-next /// This is the log domain used by the [`clone!`][crate::clone!] macro. If you want to use a custom /// logger (it prints to stdout by default), you can set your own logger using the corresponding diff --git a/glib/src/serde/byte_array.rs b/glib/src/serde/byte_array.rs new file mode 100644 index 000000000000..b53757493639 --- /dev/null +++ b/glib/src/serde/byte_array.rs @@ -0,0 +1,53 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use serde::Deserializer; + +use super::*; + +use crate::ByteArray; + +serialize_impl!(ByteArray, Bytes(b) => b); + +deserialize_impl! { + ByteArray, + "a sequence of bytes", + Deserializer::deserialize_seq => match impl { + Bytes(b) => Ok(ByteArray::from(b)), + ByteBuf(buf) => Ok(ByteArray::from(buf.as_slice())), + Seq(s) => { + // See https://docs.rs/serde/1.0.159/src/serde/de/impls.rs.html#1038 + // and https://docs.rs/serde/1.0.159/src/serde/private/size_hint.rs.html#13 + let mut bytes = Vec::with_capacity(min(s.size_hint().unwrap_or(0), 4096)); + + while let Some(byte) = s.next_element()? { + bytes.push(byte) + } + + Ok(ByteArray::from(bytes.as_slice())) + }, + } +} + +#[cfg(test)] +mod tests { + use crate::{gformat, ByteArray}; + + #[test] + fn serialization() { + let json = match serde_json::to_value(ByteArray::from( + gformat!("Lorem ipsum dolor sit amet").as_bytes(), + )) { + Ok(v) => Some(v), + Err(_) => None, + }; + + assert_ne!(json, None); + } + + #[test] + fn deserialization() { + let json_str = r#"[76,111,114,101,109,32,105,112,115,117,109,32,100,111,108,111,114,32,115,105,116,32,97,109,101]"#; + + serde_json::from_str::(json_str).unwrap(); + } +} diff --git a/glib/src/serde/bytes.rs b/glib/src/serde/bytes.rs new file mode 100644 index 000000000000..da9fe1d8d121 --- /dev/null +++ b/glib/src/serde/bytes.rs @@ -0,0 +1,51 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use serde::Deserializer; + +use super::*; + +use crate::Bytes; + +serialize_impl!(Bytes, Bytes(b) => b); + +deserialize_impl! { + Bytes, + "a sequence of bytes", + Deserializer::deserialize_seq => match impl { + Bytes(b) => Ok(Bytes::from_owned(b.to_owned())), + ByteBuf(buf) => Ok(Bytes::from_owned(buf)), + Seq(s) => { + let mut bytes = Vec::with_capacity(min(s.size_hint().unwrap_or(0), 4096)); + + while let Some(byte) = s.next_element()? { + bytes.push(byte) + } + + Ok(Bytes::from_owned(bytes)) + }, + } +} + +#[cfg(test)] +mod tests { + use crate::{gformat, Bytes}; + + #[test] + fn serialization() { + let json = match serde_json::to_value(Bytes::from_owned( + gformat!("Lorem ipsum dolor sit amet").into_bytes(), + )) { + Ok(v) => Some(v), + Err(_) => None, + }; + + assert_ne!(json, None); + } + + #[test] + fn deserialization() { + let json_str = r#"[76,111,114,101,109,32,105,112,115,117,109,32,100,111,108,111,114,32,115,105,116,32,97,109,101]"#; + + serde_json::from_str::(json_str).unwrap(); + } +} diff --git a/glib/src/serde/collections.rs b/glib/src/serde/collections.rs new file mode 100644 index 000000000000..1860bec136a0 --- /dev/null +++ b/glib/src/serde/collections.rs @@ -0,0 +1,146 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use super::*; + +use crate::{ + translate::{TransparentPtrType, TransparentType}, + List, PtrSlice, SList, Slice, StrV, +}; + +serialize_impl!(Slice, Sequence(iter) => iter); + +deserialize_impl! { + Slice, + "a sequence of GLib transparent values", + Deserializer::deserialize_seq => match impl { + Seq(s) => { + let mut slice = Slice::with_capacity(min(s.size_hint().unwrap_or(0), 4096)); + + while let Some(item) = s.next_element()? { + slice.push(item) + } + + Ok(slice) + }, + } +} + +serialize_impl!(PtrSlice, Sequence(iter) => iter); + +deserialize_impl! { + PtrSlice, + "a sequence of GLib transparent pointer values", + Deserializer::deserialize_seq => match impl { + Seq(s) => { + let mut slice = PtrSlice::with_capacity(min(s.size_hint().unwrap_or(0), 4096)); + + while let Some(item) = s.next_element()? { + slice.push(item) + } + + Ok(slice) + }, + } +} + +serialize_impl!(List, Sequence(iter) => iter.iter()); + +deserialize_impl! { + List, + "a sequence of GLib transparent pointer values", + Deserializer::deserialize_seq => match impl { + Seq(s) => { + let mut list = List::new(); + + while let Some(item) = s.next_element()? { + list.push_front(item) + } + list.reverse(); + + Ok(list) + }, + } +} + +serialize_impl!(SList, Sequence(iter) => iter.iter()); + +deserialize_impl! { + SList, + "a sequence of GLib transparent pointer values", + Deserializer::deserialize_seq => match impl { + Seq(s) => { + let mut list = SList::new(); + + while let Some(item) = s.next_element()? { + list.push_front(item) + } + list.reverse(); + + Ok(list) + }, + } +} + +serialize_impl!(StrV, Sequence(iter) => iter); + +#[cfg(test)] +mod tests { + use serde_json::json; + + use crate::{gformat, Bytes, List, PtrSlice, SList, Slice}; + + #[test] + fn serialization() { + let bytes = gformat!("Lorem ipsum dolor sit amet").into_bytes(); + + let slice = Slice::from([ + Bytes::from_owned(bytes[..].to_vec()), + Bytes::from_owned(bytes[1..].to_vec()), + Bytes::from_owned(bytes[2..].to_vec()), + Bytes::from_owned(bytes[3..].to_vec()), + ]); + + let ptr_slice = PtrSlice::from([ + Bytes::from_owned(bytes[..].to_vec()), + Bytes::from_owned(bytes[1..].to_vec()), + Bytes::from_owned(bytes[2..].to_vec()), + Bytes::from_owned(bytes[3..].to_vec()), + ]); + + let mut list = List::::new(); + list.push_front(Bytes::from_owned(bytes[..].to_vec())); + list.push_front(Bytes::from_owned(bytes[1..].to_vec())); + list.push_front(Bytes::from_owned(bytes[2..].to_vec())); + list.push_front(Bytes::from_owned(bytes[3..].to_vec())); + list.reverse(); + + let mut slist = SList::::new(); + slist.push_front(Bytes::from_owned(bytes[..].to_vec())); + slist.push_front(Bytes::from_owned(bytes[1..].to_vec())); + slist.push_front(Bytes::from_owned(bytes[2..].to_vec())); + slist.push_front(Bytes::from_owned(bytes[3..].to_vec())); + slist.reverse(); + + assert_eq!(json!(&slice), json!(&list)); + assert_eq!(json!(&slice), json!(&slist)); + assert_eq!(json!(&ptr_slice), json!(&list)); + assert_eq!(json!(&ptr_slice), json!(&slist)); + assert_eq!(json!(&slice), json!(&ptr_slice)); + assert_eq!(json!(&list), json!(&slist)); + } + + #[test] + fn deserialization() { + let json_str = r#" +[ + [76,111,114,101,109,32,105,112,115,117,109,32,100,111,108,111,114,32,115,105,116,32,97,109,101], + [111,114,101,109,32,105,112,115,117,109,32,100,111,108,111,114,32,115,105,116,32,97,109,101], + [114,101,109,32,105,112,115,117,109,32,100,111,108,111,114,32,115,105,116,32,97,109,101], + [101,109,32,105,112,115,117,109,32,100,111,108,111,114,32,115,105,116,32,97,109,101] +]"#; + serde_json::from_str::>(json_str).unwrap(); + serde_json::from_str::>(json_str).unwrap(); + serde_json::from_str::>(json_str).unwrap(); + serde_json::from_str::>(json_str).unwrap(); + } +} diff --git a/glib/src/serde/gstring.rs b/glib/src/serde/gstring.rs new file mode 100644 index 000000000000..6342c2e84fa3 --- /dev/null +++ b/glib/src/serde/gstring.rs @@ -0,0 +1,48 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use super::*; + +use crate::{gformat, GStr, GString, GStringPtr}; +use serde::de; + +serialize_impl!(GStr, str(s) => s.as_str()); + +serialize_impl!(GString, str(s) => s.as_str()); + +deserialize_impl! { + GString, + "a valid UTF-8 string", + Deserializer::deserialize_string => match impl { + str(s) => Ok(gformat!("{s}")), + String(s) => GString::from_string_checked(s).map_err(|e| de::Error::custom(e)), + } +} + +serialize_impl!(GStringPtr, str(s) => s.to_str()); + +#[cfg(test)] +mod tests { + use crate::{translate::ToGlibPtr, GString, StrV}; + use serde_json::json; + + use crate::gformat; + + #[test] + fn serialization() { + let gstring = gformat!("Lorem ipsum dolor sit amet"); + let gstr = gstring.as_gstr(); + let gstringptr = + &unsafe { StrV::from_glib_none(vec![gstring.to_owned()].to_glib_none().0) }[0]; + + assert_eq!(json!(&gstring), json!(gstr)); + assert_eq!(json!(&gstring), json!(gstringptr)); + assert_eq!(json!(gstr), json!(gstringptr)); + } + + #[test] + fn deserialization() { + let json_str = r#""Lorem ipsum dolor sit amet""#; + + serde_json::from_str::(json_str).unwrap(); + } +} diff --git a/glib/src/serde/mod.rs b/glib/src/serde/mod.rs new file mode 100644 index 000000000000..65036bd5f41a --- /dev/null +++ b/glib/src/serde/mod.rs @@ -0,0 +1,151 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use serde::Deserializer; +use std::cmp::min; + +macro_rules! serialize_impl { + ($ty:ty, Bytes($bind:ident) => $expr:expr) => { + impl ::serde::Serialize for $ty { + fn serialize(&self, serializer: S) -> Result + where + S: ::serde::Serializer, + { + let $bind = self; + + serializer.serialize_bytes($expr) + } + } + }; + ($ty:ty, str($bind:ident) => $expr:expr) => { + impl ::serde::Serialize for $ty { + fn serialize(&self, serializer: S) -> Result + where + S: ::serde::Serializer, + { + let $bind = self; + + serializer.serialize_str($expr) + } + } + }; + ($ty:ident$(<$($generic:ident $(: $bound:tt $(+ $bound2:tt)*)?),+>)?, Sequence($bind:ident) => $expr:expr) => { + impl$(<$($generic $(: ::serde::Serialize + $bound $(+ $bound2)*)?),+>)? ::serde::Serialize for $ty$(<$($generic),+>)? { + fn serialize(&self, serializer: S) -> Result + where + S: ::serde::Serializer + { + let $bind = self; + + serializer.collect_seq($expr) + } + } + }; +} + +macro_rules! deserialize_impl { + ( + $ty:ident$(<$($generic:ident$(: $bound:tt $(+ $bound2:tt)*)?),+>)?, + $expecting:literal, + $deserialize_target:expr => match impl { + $( + Bytes($bytes_arg:ident) => $visit_bytes:expr, + ByteBuf($byte_buf_arg:ident) => $visit_byte_buf:expr, + )? + $( + str($str_arg:ident) => $visit_str:expr, + String($string_arg:ident) => $visit_string:expr, + )? + $(Seq($seq_arg:ident) => $visit_seq:expr,)? + $( + @in_place($inplace_self:ident) => match impl { + $( + Bytes($inplace_bytes_arg:ident) => $inplace_visit_bytes:expr, + ByteBuf($inplace_byte_buf_arg:ident) => $inplace_visit_byte_buf:expr, + )? + $( + str($inplace_str_arg:ident) => $inplace_visit_str:expr, + String($inplace_string_arg:ident) => $inplace_visit_string:expr, + )? + $(Seq($inplace_seq_arg:ident) => $inplace_visit_seq:expr,)? + }, + )? + } + ) => { + impl<'de, $($($generic $(: ::serde::Deserialize<'de> + $bound $(+ $bound2)*)?),+)?> ::serde::Deserialize<'de> for $ty$(<$($generic),+>)? { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::Deserializer<'de>, + { + struct Visitor<'v$(, $($generic),+)?>(::std::marker::PhantomData<&'v ()>, $($(::std::marker::PhantomData $generic>),+)?) + $( + where + $($generic: ::serde::Deserialize<'v> $(+ $bound $(+ $bound2)*)?),+ + )?; + + impl<'a$(, $($generic),+)?> ::serde::de::Visitor<'a> for Visitor<'a, $($($generic),+)?> + $( + where + $($generic: ::serde::Deserialize<'a> $(+ $bound $(+ $bound2)*)?),+ + )? + { + type Value = $ty$(<$($generic),+>)?; + + fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + formatter.write_str($expecting) + } + + $( + fn visit_bytes(self, $bytes_arg: &[u8]) -> Result + where + E: ::serde::de::Error, + { + $visit_bytes + } + + fn visit_byte_buf(self, $byte_buf_arg: Vec) -> Result + where + E: ::serde::de::Error, + { + $visit_byte_buf + } + )? + + $( + fn visit_str(self, $str_arg: &str) -> Result + where + E: ::serde::de::Error, + { + $visit_str + } + + fn visit_string(self, $string_arg: String) -> Result + where + E: ::serde::de::Error, + { + $visit_string + } + )? + + $( + fn visit_seq(self, mut $seq_arg: A) -> Result + where + A: ::serde::de::SeqAccess<'a>, + { + $visit_seq + } + )? + } + + $deserialize_target(deserializer, Visitor(::std::marker::PhantomData, $($(::std::marker::PhantomData:: $generic>),+)?)) + } + } + }; +} + +mod byte_array; + +mod bytes; + +mod gstring; + +mod collections;