Skip to content

Commit

Permalink
Add serde support for GString, StringName, NodePath and Array
Browse files Browse the repository at this point in the history
  • Loading branch information
kuruk-mm committed Nov 29, 2023
1 parent cf16a91 commit d30fc84
Show file tree
Hide file tree
Showing 12 changed files with 332 additions and 9 deletions.
4 changes: 4 additions & 0 deletions check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ for arg in "$@"; do
echo "$HELP_TEXT"
exit 0
;;
--use-serde)
extraCargoArgs+=("--features" "godot/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 @@ -27,6 +27,7 @@ godot-ffi = { path = "../godot-ffi" }
# See https://docs.rs/glam/latest/glam/index.html#feature-gates
glam = { version = "0.23", features = ["debug-glam-assert"] }
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }

# Reverse dev dependencies so doctests can use `godot::` prefix
[dev-dependencies]
Expand Down
74 changes: 66 additions & 8 deletions godot-core/src/builtin/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,15 @@ impl<T: GodotType> Array<T> {

#[doc(hidden)]
pub fn as_inner(&self) -> inner::InnerArray {
// SAFETY: The memory layout of `TypedArray<T>` does not depend on `T`.
// SAFETY: The memory layout of `Array<T>` does not depend on `T`.
inner::InnerArray::from_outer_typed(self)
}

/// Changes the generic type on this array, without changing its contents. Needed for API
/// functions that return a variant array even though we know its type, and for API functions
/// that take a variant array even though we want to pass a typed one.
///
/// This is marked `unsafe` since it can be used to break the invariant that a `TypedArray<T>`
/// This is marked `unsafe` since it can be used to break the invariant that a `Array<T>`
/// always holds a Godot array whose runtime type is `T`.
///
/// # Safety
Expand All @@ -236,7 +236,7 @@ impl<T: GodotType> Array<T> {
/// In the current implementation, both cases will produce a panic rather than undefined
/// behavior, but this should not be relied upon.
unsafe fn assume_type<U: GodotType>(self) -> Array<U> {
// SAFETY: The memory layout of `TypedArray<T>` does not depend on `T`.
// SAFETY: The memory layout of `Array<T>` does not depend on `T`.
unsafe { std::mem::transmute(self) }
}
}
Expand Down Expand Up @@ -276,7 +276,7 @@ impl<T: GodotType> Array<T> {
///
/// If specified, `step` is the relative index between source elements. It can be negative,
/// in which case `begin` must be higher than `end`. For example,
/// `TypedArray::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`.
/// `Array::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`.
///
/// Array elements are copied to the slice, but any reference types (such as `Array`,
/// `Dictionary` and `Object`) will still refer to the same value. To create a deep copy, use
Expand All @@ -292,7 +292,7 @@ impl<T: GodotType> Array<T> {
///
/// If specified, `step` is the relative index between source elements. It can be negative,
/// in which case `begin` must be higher than `end`. For example,
/// `TypedArray::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`.
/// `Array::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`.
///
/// All nested arrays and dictionaries are duplicated and will not be shared with the original
/// array. Note that any `Object`-derived elements will still be shallow copied. To create a
Expand Down Expand Up @@ -576,7 +576,7 @@ impl<T: GodotType + ToGodot> Array<T> {
let len = self.len();
assert!(
index <= len,
"TypedArray insertion index {index} is out of bounds: length is {len}",
"Array insertion index {index} is out of bounds: length is {len}",
);
self.as_inner().insert(to_i64(index), value.to_variant());
}
Expand Down Expand Up @@ -604,9 +604,9 @@ impl<T: GodotType + ToGodot> Array<T> {
// but `[NAN] == [NAN]` is `true`. If they decide to make all NaNs equal, we can implement `Eq` and
// `Ord`; if they decide to make all NaNs unequal, we can remove this comment.
//
// impl<T> Eq for TypedArray<T> {}
// impl<T> Eq for Array<T> {}
//
// impl<T> Ord for TypedArray<T> {
// impl<T> Ord for Array<T> {
// ...
// }

Expand Down Expand Up @@ -1063,3 +1063,61 @@ impl fmt::Debug for TypeInfo {
write!(f, "{:?}{}", self.variant_type, class_str)
}
}

#[cfg(feature = "serde")]
mod serialize {
use super::*;
use serde::{
de::{SeqAccess, Visitor},
ser::SerializeSeq,
Deserialize, Deserializer, Serialize, Serializer,
};
use std::marker::PhantomData;

impl<T: Serialize + GodotType> Serialize for Array<T> {
#[inline]
fn serialize<S>(&self, ser: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
where
S: Serializer,
{
let mut ser = ser.serialize_seq(Some(self.len()))?;
for e in self.iter_shared() {
ser.serialize_element(&e)?
}
ser.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))
}
}
}
50 changes: 50 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,53 @@ impl From<NodePath> for GString {
Self::from(&path)
}
}

#[cfg(feature = "serde")]
mod serialize {
use super::*;
use serde::{
de::{Error, Visitor},
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)
}
}
}
57 changes: 57 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,60 @@ impl From<StringName> for NodePath {
Self::from(GString::from(string_name))
}
}

#[cfg(feature = "serde")]
mod serialize {
use super::*;
use serde::{
de::{Error, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
use std::fmt::Formatter;

impl Serialize for NodePath {
#[inline]
fn serialize<S>(&self, ser: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
where
S: Serializer,
{
ser.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)
}
}
}
49 changes: 49 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,52 @@ impl From<NodePath> for StringName {
Self::from(GString::from(path))
}
}

#[cfg(feature = "serde")]
mod serialize {
use super::*;
use serde::{
de::{Error, Visitor},
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)
}
}
}
2 changes: 1 addition & 1 deletion godot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ default = ["codegen-full"]
custom-godot = ["godot-core/custom-godot"]
double-precision = ["godot-core/double-precision"]
formatted = ["godot-core/codegen-fmt"]
serde = ["godot-core/serde"]
serde = ["godot-core/serde", "godot-core/serde_json"]
lazy-function-tables = ["godot-core/codegen-lazy-fptrs"]
experimental-threads = ["godot-core/experimental-threads"]
experimental-godot-api = ["godot-core/experimental-godot-api"]
Expand Down
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"]
# 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
35 changes: 35 additions & 0 deletions itest/rust/src/builtin_tests/containers/array_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,38 @@ impl ArrayTest {
(1..(n + 1)).collect()
}
}

#[itest]
#[cfg(feature = "serde")]
fn serde_roundtrip() {
#[derive(serde::Deserialize, serde::Serialize, PartialEq, Debug)]
struct P {
sequence: Array<i32>,
names: Array<GString>,
vectors: Array<Vector2i>,
}
let value = P {
sequence: Array::from(&[1, 2, 3, 4, 5, 6]),
names: Array::from(&[
"Godot".into_godot(),
"Rust".into_godot(),
"Rocks".into_godot(),
]),
vectors: Array::from(&[
Vector2i::new(1, 1),
Vector2i::new(2, 2),
Vector2i::new(3, 3),
]),
};

let expected_json = r#"{"sequence":[1,2,3,4,5,6],"names":["Godot","Rust","Rocks"],"vectors":[{"x":1,"y":1},{"x":2,"y":2},{"x":3,"y":3}]}"#;

let json: String = serde_json::to_string(&value).unwrap();
let back: P = 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"
);
}

0 comments on commit d30fc84

Please sign in to comment.