Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add serde support for GString, StringName, NodePath and Array #508

Merged
merged 1 commit into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ for arg in "$@"; do
echo "$HELP_TEXT"
exit 0
;;
--use-serde)
extraCargoArgs+=("--features" "serde")
;;
--double)
extraCargoArgs+=("--features" "godot/double-precision")
;;
Expand Down
1 change: 1 addition & 0 deletions godot-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
59 changes: 59 additions & 0 deletions godot-core/src/builtin/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Serialize + GodotType> Serialize for Array<T> {
#[inline]
fn serialize<S>(
&self,
serializer: S,
) -> Result<<S as Serializer>::Ok, <S as Serializer>::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<T> {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
where
D: Deserializer<'de>,
{
struct ArrayVisitor<T>(PhantomData<T>);
impl<'de, T: Deserialize<'de> + GodotType> Visitor<'de> for ArrayVisitor<T> {
type Value = Array<T>;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> fmt::Result {
formatter.write_str(std::any::type_name::<Self::Value>())
}

fn visit_seq<A>(
self,
mut seq: A,
) -> Result<Self::Value, <A as SeqAccess<'de>>::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::<T>()? {
vec.push(val);
}
Ok(Self::Value::from(vec.as_slice()))
}
}

deserializer.deserialize_seq(ArrayVisitor::<T>(PhantomData))
}
}
}
48 changes: 48 additions & 0 deletions godot-core/src/builtin/string/gstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,51 @@ impl From<NodePath> 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<S>(
&self,
serializer: S,
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

#[cfg(feature = "serde")]
impl<'de> serialize::Deserialize<'de> for GString {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::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<E>(self, s: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(GString::from(s))
}
}

deserializer.deserialize_str(GStringVisitor)
}
}
}
58 changes: 58 additions & 0 deletions godot-core/src/builtin/string/node_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,61 @@ impl From<StringName> 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<S>(
&self,
serializer: S,
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
where
S: Serializer,
{
serializer.serialize_newtype_struct("NodePath", &*self.to_string())
}
}

impl<'de> Deserialize<'de> for NodePath {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, s: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(NodePath::from(s))
}

fn visit_newtype_struct<D>(
self,
deserializer: D,
) -> Result<Self::Value, <D as Deserializer<'de>>::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(self)
}
}

deserializer.deserialize_newtype_struct("NodePath", NodePathVisitor)
}
}
}
47 changes: 47 additions & 0 deletions godot-core/src/builtin/string/string_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,50 @@ impl From<NodePath> 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<S>(
&self,
serializer: S,
) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}

impl<'de> serialize::Deserialize<'de> for StringName {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::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<E>(self, s: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(StringName::from(s))
}
}

deserializer.deserialize_str(StringNameVisitor)
}
}
}
3 changes: 3 additions & 0 deletions itest/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions itest/rust/src/builtin_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ mod string {
mod color_test;

mod convert_test;

#[cfg(feature = "serde")]
mod serde_test;
79 changes: 79 additions & 0 deletions itest/rust/src/builtin_tests/serde_test.rs
Original file line number Diff line number Diff line change
@@ -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<T>(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<i32> = 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<GString> = 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<Vector2i> = 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)
}