diff --git a/check.sh b/check.sh index 75ab87704..4fd4c7878 100755 --- a/check.sh +++ b/check.sh @@ -171,6 +171,9 @@ for arg in "$@"; do echo "$HELP_TEXT" exit 0 ;; + --use-serde) + extraCargoArgs+=("--features" "serde") + ;; --double) extraCargoArgs+=("--features" "godot/double-precision") ;; diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index 652a50428..67d25c978 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -31,6 +31,7 @@ serde = { version = "1", features = ["derive"], optional = true } # Reverse dev dependencies so doctests can use `godot::` prefix [dev-dependencies] godot = { path = "../godot" } +serde_json = { version = "1.0" } [build-dependencies] godot-bindings = { path = "../godot-bindings" } diff --git a/godot-core/src/builtin/array.rs b/godot-core/src/builtin/array.rs index bcf1084e8..8fe258f29 100644 --- a/godot-core/src/builtin/array.rs +++ b/godot-core/src/builtin/array.rs @@ -1063,3 +1063,62 @@ impl fmt::Debug for TypeInfo { write!(f, "{:?}{}", self.variant_type, class_str) } } + +#[cfg(feature = "serde")] +mod serialize { + use super::*; + use serde::de::{SeqAccess, Visitor}; + use serde::ser::SerializeSeq; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::marker::PhantomData; + + impl Serialize for Array { + #[inline] + fn serialize( + &self, + serializer: S, + ) -> Result<::Ok, ::Error> + where + S: Serializer, + { + let mut sequence = serializer.serialize_seq(Some(self.len()))?; + for e in self.iter_shared() { + sequence.serialize_element(&e)? + } + sequence.end() + } + } + + impl<'de, T: Deserialize<'de> + GodotType> Deserialize<'de> for Array { + #[inline] + fn deserialize(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + struct ArrayVisitor(PhantomData); + impl<'de, T: Deserialize<'de> + GodotType> Visitor<'de> for ArrayVisitor { + type Value = Array; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> fmt::Result { + formatter.write_str(std::any::type_name::()) + } + + fn visit_seq( + self, + mut seq: A, + ) -> Result>::Error> + where + A: SeqAccess<'de>, + { + let mut vec = seq.size_hint().map_or_else(Vec::new, Vec::with_capacity); + while let Some(val) = seq.next_element::()? { + vec.push(val); + } + Ok(Self::Value::from(vec.as_slice())) + } + } + + deserializer.deserialize_seq(ArrayVisitor::(PhantomData)) + } + } +} diff --git a/godot-core/src/builtin/string/gstring.rs b/godot-core/src/builtin/string/gstring.rs index de77a0f6e..bcf9974b7 100644 --- a/godot-core/src/builtin/string/gstring.rs +++ b/godot-core/src/builtin/string/gstring.rs @@ -313,3 +313,51 @@ impl From for GString { Self::from(&path) } } + +#[cfg(feature = "serde")] +mod serialize { + use super::*; + use serde::de::{Error, Visitor}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::fmt::Formatter; + + impl Serialize for GString { + #[inline] + fn serialize( + &self, + serializer: S, + ) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } + } + + #[cfg(feature = "serde")] + impl<'de> serialize::Deserialize<'de> for GString { + #[inline] + fn deserialize(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + struct GStringVisitor; + impl<'de> Visitor<'de> for GStringVisitor { + type Value = GString; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("a GString") + } + + fn visit_str(self, s: &str) -> Result + where + E: Error, + { + Ok(GString::from(s)) + } + } + + deserializer.deserialize_str(GStringVisitor) + } + } +} diff --git a/godot-core/src/builtin/string/node_path.rs b/godot-core/src/builtin/string/node_path.rs index 66cccdc4a..2c9363e2a 100644 --- a/godot-core/src/builtin/string/node_path.rs +++ b/godot-core/src/builtin/string/node_path.rs @@ -150,3 +150,61 @@ impl From for NodePath { Self::from(GString::from(string_name)) } } + +#[cfg(feature = "serde")] +mod serialize { + use super::*; + use serde::de::{Error, Visitor}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::fmt::Formatter; + + impl Serialize for NodePath { + #[inline] + fn serialize( + &self, + serializer: S, + ) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.serialize_newtype_struct("NodePath", &*self.to_string()) + } + } + + impl<'de> Deserialize<'de> for NodePath { + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct NodePathVisitor; + + impl<'de> Visitor<'de> for NodePathVisitor { + type Value = NodePath; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("a NodePath") + } + + fn visit_str(self, s: &str) -> Result + where + E: Error, + { + Ok(NodePath::from(s)) + } + + fn visit_newtype_struct( + self, + deserializer: D, + ) -> Result>::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(self) + } + } + + deserializer.deserialize_newtype_struct("NodePath", NodePathVisitor) + } + } +} diff --git a/godot-core/src/builtin/string/string_name.rs b/godot-core/src/builtin/string/string_name.rs index df28f11de..801a713d0 100644 --- a/godot-core/src/builtin/string/string_name.rs +++ b/godot-core/src/builtin/string/string_name.rs @@ -245,3 +245,50 @@ impl From for StringName { Self::from(GString::from(path)) } } + +#[cfg(feature = "serde")] +mod serialize { + use super::*; + use serde::de::{Error, Visitor}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::fmt::Formatter; + + impl Serialize for StringName { + #[inline] + fn serialize( + &self, + serializer: S, + ) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } + } + + impl<'de> serialize::Deserialize<'de> for StringName { + #[inline] + fn deserialize(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + struct StringNameVisitor; + impl<'de> Visitor<'de> for StringNameVisitor { + type Value = StringName; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("a StringName") + } + + fn visit_str(self, s: &str) -> Result + where + E: Error, + { + Ok(StringName::from(s)) + } + } + + deserializer.deserialize_str(StringNameVisitor) + } + } +} diff --git a/itest/rust/Cargo.toml b/itest/rust/Cargo.toml index db7cce8a6..02b14d2c3 100644 --- a/itest/rust/Cargo.toml +++ b/itest/rust/Cargo.toml @@ -10,11 +10,14 @@ crate-type = ["cdylib"] [features] default = [] +serde = ["dep:serde", "dep:serde_json", "godot/serde"] # Do not add features here that are 1:1 forwarded to the `godot` crate. # Instead, compile itest with `--features godot/my-feature`. [dependencies] godot = { path = "../../godot", default-features = false } +serde = { version = "1", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } [build-dependencies] godot-bindings = { path = "../../godot-bindings" } # emit_godot_version_cfg diff --git a/itest/rust/src/builtin_tests/mod.rs b/itest/rust/src/builtin_tests/mod.rs index 27a5101c6..5afa18002 100644 --- a/itest/rust/src/builtin_tests/mod.rs +++ b/itest/rust/src/builtin_tests/mod.rs @@ -35,3 +35,6 @@ mod string { mod color_test; mod convert_test; + +#[cfg(feature = "serde")] +mod serde_test; diff --git a/itest/rust/src/builtin_tests/serde_test.rs b/itest/rust/src/builtin_tests/serde_test.rs new file mode 100644 index 000000000..04a069559 --- /dev/null +++ b/itest/rust/src/builtin_tests/serde_test.rs @@ -0,0 +1,79 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::framework::itest; +use godot::builtin::{array, Array, GString, NodePath, StringName, Vector2i}; +use serde::{Deserialize, Serialize}; + +fn serde_roundtrip(value: &T, expected_json: &str) +where + T: for<'a> Deserialize<'a> + Serialize + PartialEq + std::fmt::Debug, +{ + let json: String = serde_json::to_string(value).unwrap(); + let back: T = serde_json::from_str(json.as_str()).unwrap(); + + assert_eq!(back, *value, "serde round-trip changes value"); + assert_eq!( + json, expected_json, + "value does not conform to expected JSON" + ); +} + +#[itest] +fn serde_gstring() { + let value = GString::from("hello world"); + + let expected_json = "\"hello world\""; + + serde_roundtrip(&value, expected_json); +} + +#[itest] +fn serde_node_path() { + let value = NodePath::from("res://icon.png"); + let expected_json = "\"res://icon.png\""; + + serde_roundtrip(&value, expected_json); +} + +#[itest] +fn serde_string_name() { + let value = StringName::from("hello world"); + let expected_json = "\"hello world\""; + + serde_roundtrip(&value, expected_json); +} + +#[itest] +fn serde_array_rust_native_type() { + let value: Array = array![1, 2, 3, 4, 5, 6]; + + let expected_json = r#"[1,2,3,4,5,6]"#; + + serde_roundtrip(&value, expected_json) +} + +#[itest] +fn serde_array_godot_builtin_type() { + let value: Array = array!["Godot".into(), "Rust".into(), "Rocks".into()]; + + let expected_json = r#"["Godot","Rust","Rocks"]"#; + + serde_roundtrip(&value, expected_json) +} + +#[itest] +fn serde_array_godot_type() { + let value: Array = array![ + Vector2i::new(1, 1), + Vector2i::new(2, 2), + Vector2i::new(3, 3) + ]; + + let expected_json = r#"[{"x":1,"y":1},{"x":2,"y":2},{"x":3,"y":3}]"#; + + serde_roundtrip(&value, expected_json) +}