diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 820048436acc..3eaaaa317253 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -23,7 +23,7 @@ jobs: - { name: "cairo", features: "png,pdf,svg,ps,use_glib,v1_18,freetype,script,xcb,xlib,win32-surface", nightly: "--features 'png,pdf,svg,ps,use_glib,v1_18,freetype,script,xcb,xlib,win32-surface'", test_sys: true } - { name: "gdk-pixbuf", features: "v2_42", nightly: "--all-features", test_sys: true } - { name: "gio", features: "v2_74", nightly: "--all-features", test_sys: true } - - { name: "glib", features: "v2_74", nightly: "--all-features", test_sys: true } + - { name: "glib", features: "v2_74,serde", nightly: "--all-features", test_sys: true } - { name: "graphene", features: "", 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 2a5acbf80420..8b6cf3b53fd2 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -126,7 +126,14 @@ jobs: with: command: test args: --manifest-path ${{ matrix.conf.name }}/sys/Cargo.toml ${{ matrix.conf.args }} - if: matrix.conf.name != 'examples' && matrix.conf.name != 'glib-build-tools' + if: matrix.conf.name != 'examples' && matrix.conf.name != 'glib' && matrix.conf.name != 'glib-build-tools' + + - name: test glib-sys + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path ${{ matrix.conf.name }}/sys/Cargo.toml --features v2_74 + if: matrix.conf.name == 'glib' - name: build uses: actions-rs/cargo@v1 diff --git a/glib/Cargo.toml b/glib/Cargo.toml index de8c41b7415b..55986e49706e 100644 --- a/glib/Cargo.toml +++ b/glib/Cargo.toml @@ -35,12 +35,14 @@ smallvec = { version = "1.0", features = ["union", "const_generics", "const_new" thiserror = "1" gio_ffi = { package = "gio-sys", path = "../gio/sys", optional = true } memchr = "2.5.0" +serde = { version = "1.0", optional = true } [dev-dependencies] tempfile = "3" gir-format-check = "^0.1" trybuild2 = "1" criterion = "0.4.0" +serde_json = "1.0" [features] default = ["gio"] @@ -59,6 +61,7 @@ log_macros = ["log"] dox = ["ffi/dox", "gobject_ffi/dox", "log_macros"] compiletests = [] gio = ["gio_ffi"] +serde = ["dep:serde"] [package.metadata.docs.rs] features = ["dox"] diff --git a/glib/src/lib.rs b/glib/src/lib.rs index eb276045ba5a..d3b1cf76648d 100644 --- a/glib/src/lib.rs +++ b/glib/src/lib.rs @@ -213,6 +213,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..79ffda7de6e8 --- /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 byte_vec = Vec::with_capacity(min(s.size_hint().unwrap_or(0), 4096)); + + while let Some(byte) = s.next_element()? { + byte_vec.push(byte) + } + + Ok(ByteArray::from(byte_vec.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..3262ba15f672 --- /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 byte_vec = Vec::with_capacity(min(s.size_hint().unwrap_or(0), 4096)); + + while let Some(byte) = s.next_element()? { + byte_vec.push(byte) + } + + Ok(Bytes::from_owned(byte_vec)) + }, + } +} + +#[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..2f4c63dc1a93 --- /dev/null +++ b/glib/src/serde/collections.rs @@ -0,0 +1,194 @@ +// 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) + }, + @in_place(this) => match impl { + Seq(s) => { + this.clear(); + this.reserve(min(s.size_hint().unwrap_or(0), 4096)); + + while let Some(item) = s.next_element()? { + this.push(item) + } + + Ok(()) + }, + }, + } +} + +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) + }, + @in_place(this) => match impl { + Seq(s) => { + this.clear(); + this.reserve(min(s.size_hint().unwrap_or(0), 4096)); + + while let Some(item) = s.next_element()? { + this.push(item) + } + + Ok(()) + }, + }, + } +} + +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) + }, + @in_place(this) => match impl { + Seq(s) => { + this.clear(); + + while let Some(item) = s.next_element()? { + this.push_front(item) + } + this.reverse(); + + Ok(()) + }, + }, + } +} + +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) + }, + @in_place(this) => match impl { + Seq(s) => { + this.clear(); + + while let Some(item) = s.next_element()? { + this.push_front(item) + } + this.reverse(); + + Ok(()) + }, + }, + } +} + +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_equality() { + 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..98f371aba873 --- /dev/null +++ b/glib/src/serde/gstring.rs @@ -0,0 +1,58 @@ +// 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)), + @in_place(this) => match impl { + str(s) => { + *this = gformat!("{s}"); + Ok(()) + }, + String(s) => { + *this = GString::from_string_checked(s).map_err(|e| de::Error::custom(e))?; + Ok(()) + }, + }, + } +} + +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_equality() { + 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..28280c7eafd9 --- /dev/null +++ b/glib/src/serde/mod.rs @@ -0,0 +1,265 @@ +// 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_in_place_gen { + ($ty:ident$(<$($generic:ident$(: $bound:tt $(+ $bound2:tt)*)?),+>)?, $expect:literal,) => {}; // No-op expansion + ( + $ty:ident$(<$($generic:ident$(: $bound:tt $(+ $bound2:tt)*)?),+>)?, + $expecting:literal, + $deserialize_target:expr => match impl { + $( + Bytes($self_bytes:ident, $bytes_arg:ident) => $visit_bytes:expr, + ByteBuf($self_byte_buf:ident, $byte_buf_arg:ident) => $visit_byte_buf:expr, + )? + $( + str($self_str:ident, $str_arg:ident) => $visit_str:expr, + String($self_string:ident, $string_arg:ident) => $visit_string:expr, + )? + $(Seq($self_seq:ident, $seq_arg:ident) => $visit_seq:expr,)? + } + ) => { + fn deserialize_in_place(deserializer: D, place: &mut Self) -> Result<(), D::Error> + where + D: ::serde::Deserializer<'de>, + { + struct InPlaceVisitor<'a, 'de$(, $($generic),+)?>(&'a mut $ty$(<$($generic),+>)?, ::std::marker::PhantomData<&'de ()>) + $( + where + $($generic: ::serde::Deserialize<'de> $(+ $bound $(+ $bound2)*)?),+ + )?; + + impl<'a, 'de$(, $($generic),+)?> ::serde::de::Visitor<'de> for InPlaceVisitor<'a, 'de$(, $($generic),+)?> + $( + where + $($generic: ::serde::Deserialize<'de> $(+ $bound $(+ $bound2)*)?),+ + )? + { + type Value = (); + + 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, + { + let $self_bytes = self.0; + + $visit_bytes + } + + fn visit_byte_buf(self, $byte_buf_arg: Vec) -> Result + where + E: ::serde::de::Error, + { + let $self_byte_buf = self.0; + + $visit_byte_buf + } + )? + + $( + fn visit_str(self, $str_arg: &str) -> Result + where + E: ::serde::de::Error, + { + let $self_str = self.0; + + $visit_str + } + + fn visit_string(self, $string_arg: String) -> Result + where + E: ::serde::de::Error, + { + let $self_string = self.0; + + $visit_string + } + )? + + $( + fn visit_seq(self, mut $seq_arg: A) -> Result + where + A: ::serde::de::SeqAccess<'de>, + { + let $self_seq = self.0; + + $visit_seq + } + )? + } + + $deserialize_target(deserializer, InPlaceVisitor(place, ::std::marker::PhantomData)) + } + }; +} + +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>),+)?)) + } + + deserialize_in_place_gen! { + $ty$(<$($generic$(: $bound $(+ $bound2)*)?),+>)?, + $expecting, + $( + $deserialize_target => match impl { + $( + Bytes($inplace_self, $inplace_bytes_arg) => $inplace_visit_bytes, + ByteBuf($inplace_self, $inplace_byte_buf_arg) => $inplace_visit_byte_buf, + )? + $( + str($inplace_self, $inplace_str_arg) => $inplace_visit_str, + String($inplace_self, $inplace_string_arg) => $inplace_visit_string, + )? + $(Seq($inplace_self, $inplace_seq_arg) => $inplace_visit_seq,)? + } + )? + } + } + }; +} + +mod byte_array; + +mod bytes; + +mod gstring; + +mod collections;