diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 038be1907..1ebdbfced 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -169,11 +169,11 @@ jobs: godot-binary: godot.linuxbsd.editor.dev.double.x86_64 rust-extra-args: --features double-precision - - name: linux-threads + - name: linux-features os: ubuntu-20.04 artifact-name: linux godot-binary: godot.linuxbsd.editor.dev.x86_64 - rust-extra-args: --features threads + rust-extra-args: --features threads,serde - name: linux-nightly os: ubuntu-20.04 diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index 692fb9869..a98e4982e 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -21,10 +21,12 @@ 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 } # Reverse dev dependencies so doctests can use `godot::` prefix [dev-dependencies] godot = { path = "../godot" } +serde_json = "1.0" [build-dependencies] godot-codegen = { path = "../godot-codegen" } diff --git a/godot-core/src/builtin/aabb.rs b/godot-core/src/builtin/aabb.rs index 26dc97892..77eef9917 100644 --- a/godot-core/src/builtin/aabb.rs +++ b/godot-core/src/builtin/aabb.rs @@ -18,6 +18,7 @@ use super::Vector3; /// /// The 2D counterpart to `Aabb` is [`Rect2`](super::Rect2). #[derive(Default, Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Aabb { pub position: Vector3, @@ -101,3 +102,15 @@ impl std::fmt::Display for Aabb { unsafe impl GodotFfi for Aabb { ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } } + +#[cfg(test)] +mod test { + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let aabb = super::Aabb::default(); + let expected_json = "{\"position\":{\"x\":0.0,\"y\":0.0,\"z\":0.0},\"size\":{\"x\":0.0,\"y\":0.0,\"z\":0.0}}"; + + crate::builtin::test_utils::roundtrip(&aabb, expected_json); + } +} diff --git a/godot-core/src/builtin/basis.rs b/godot-core/src/builtin/basis.rs index 5c2d15353..f64bccc4d 100644 --- a/godot-core/src/builtin/basis.rs +++ b/godot-core/src/builtin/basis.rs @@ -23,6 +23,7 @@ use super::{real, RMat3, RQuat, RVec2, RVec3}; /// The basis vectors are the columns of the matrix, whereas the [`rows`](Self::rows) field represents /// the row vectors. #[derive(Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Basis { /// The rows of the matrix. These are *not* the basis vectors. @@ -835,4 +836,13 @@ mod test { "Basis with three components infinite should not be finite." ); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let basis = Basis::IDENTITY; + let expected_json = "{\"rows\":[{\"x\":1.0,\"y\":0.0,\"z\":0.0},{\"x\":0.0,\"y\":1.0,\"z\":0.0},{\"x\":0.0,\"y\":0.0,\"z\":1.0}]}"; + + crate::builtin::test_utils::roundtrip(&basis, expected_json); + } } diff --git a/godot-core/src/builtin/color.rs b/godot-core/src/builtin/color.rs index 31966d70e..aba364a43 100644 --- a/godot-core/src/builtin/color.rs +++ b/godot-core/src/builtin/color.rs @@ -16,6 +16,7 @@ use sys::{ffi_methods, GodotFfi}; /// values outside this range are explicitly allowed for e.g. High Dynamic Range (HDR). #[repr(C)] #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Color { /// The color's red component. pub r: f32, @@ -511,3 +512,15 @@ impl std::fmt::Display for Color { write!(f, "({}, {}, {}, {})", self.r, self.g, self.b, self.a) } } + +#[cfg(test)] +mod test { + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let color = super::Color::WHITE; + let expected_json = "{\"r\":1.0,\"g\":1.0,\"b\":1.0,\"a\":1.0}"; + + crate::builtin::test_utils::roundtrip(&color, expected_json); + } +} diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index b80e40b9e..3da822058 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -407,3 +407,22 @@ mod export { // TODO investigate whether Signal should impl Export at all, and if so, how // impl_export_by_clone!(Signal); } + +#[cfg(all(test, feature = "serde"))] +pub(crate) mod test_utils { + use serde::{Deserialize, Serialize}; + + pub(crate) fn 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" + ); + } +} diff --git a/godot-core/src/builtin/plane.rs b/godot-core/src/builtin/plane.rs index bd59fb2ec..df7f23472 100644 --- a/godot-core/src/builtin/plane.rs +++ b/godot-core/src/builtin/plane.rs @@ -23,6 +23,7 @@ use super::{is_equal_approx, real, Vector3}; /// unit length and will panic if this invariant is violated. This is not separately /// annotated for each method. #[derive(Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Plane { pub normal: Vector3, @@ -189,4 +190,16 @@ mod test { Vector3::new(0.0, 0.0, 2.0), ); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let plane = Plane { + normal: Vector3::ONE, + d: 0.0, + }; + let expected_json = "{\"normal\":{\"x\":1.0,\"y\":1.0,\"z\":1.0},\"d\":0.0}"; + + crate::builtin::test_utils::roundtrip(&plane, expected_json); + } } diff --git a/godot-core/src/builtin/projection.rs b/godot-core/src/builtin/projection.rs index 20f184753..b1fb3951e 100644 --- a/godot-core/src/builtin/projection.rs +++ b/godot-core/src/builtin/projection.rs @@ -25,6 +25,7 @@ use super::{real, RMat4, RealConv}; /// Note: The current implementation largely makes calls to godot for its /// methods and as such are not as performant as other types. #[derive(Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Projection { /// The columns of the projection matrix. @@ -960,6 +961,15 @@ mod test { } } } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let projection = Projection::IDENTITY; + let expected_json = "{\"cols\":[{\"x\":1.0,\"y\":0.0,\"z\":0.0,\"w\":0.0},{\"x\":0.0,\"y\":1.0,\"z\":0.0,\"w\":0.0},{\"x\":0.0,\"y\":0.0,\"z\":1.0,\"w\":0.0},{\"x\":0.0,\"y\":0.0,\"z\":0.0,\"w\":1.0}]}"; + + crate::builtin::test_utils::roundtrip(&projection, expected_json); + } } impl std::fmt::Display for Projection { diff --git a/godot-core/src/builtin/quaternion.rs b/godot-core/src/builtin/quaternion.rs index 1a1f34884..6b0da2234 100644 --- a/godot-core/src/builtin/quaternion.rs +++ b/godot-core/src/builtin/quaternion.rs @@ -15,6 +15,7 @@ use super::{real, RQuat}; use super::{Basis, EulerOrder}; #[derive(Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Quaternion { pub x: real, @@ -347,3 +348,15 @@ impl Neg for Quaternion { Self::new(-self.x, -self.y, -self.z, -self.w) } } + +#[cfg(test)] +mod test { + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let quaternion = super::Quaternion::new(1.0, 1.0, 1.0, 1.0); + let expected_json = "{\"x\":1.0,\"y\":1.0,\"z\":1.0,\"w\":1.0}"; + + crate::builtin::test_utils::roundtrip(&quaternion, expected_json); + } +} diff --git a/godot-core/src/builtin/rect2.rs b/godot-core/src/builtin/rect2.rs index aa5973c4b..b2a89ebf3 100644 --- a/godot-core/src/builtin/rect2.rs +++ b/godot-core/src/builtin/rect2.rs @@ -18,6 +18,7 @@ use super::{real, Rect2i, Vector2}; /// /// The 3D counterpart to `Rect2` is [`Aabb`](super::Aabb). #[derive(Default, Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Rect2 { pub position: Vector2, @@ -125,3 +126,15 @@ impl std::fmt::Display for Rect2 { write!(f, "[P: {}, S: {}]", self.position, self.size) } } + +#[cfg(test)] +mod test { + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let rect = super::Rect2::default(); + let expected_json = "{\"position\":{\"x\":0.0,\"y\":0.0},\"size\":{\"x\":0.0,\"y\":0.0}}"; + + crate::builtin::test_utils::roundtrip(&rect, expected_json); + } +} diff --git a/godot-core/src/builtin/rect2i.rs b/godot-core/src/builtin/rect2i.rs index cab59889b..de51a1a14 100644 --- a/godot-core/src/builtin/rect2i.rs +++ b/godot-core/src/builtin/rect2i.rs @@ -15,6 +15,7 @@ use super::{Rect2, RectSide, Vector2i}; /// `Rect2i` consists of a position, a size, and several utility functions. It is typically used for /// fast overlap tests. #[derive(Default, Copy, Clone, Eq, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Rect2i { /// The position of the rectangle. @@ -583,4 +584,13 @@ mod test { let rect = Rect2i::from_components(0, 0, -5, -5); Rect2i::default().merge(rect); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let rect = Rect2i::default(); + let expected_json = "{\"position\":{\"x\":0,\"y\":0},\"size\":{\"x\":0,\"y\":0}}"; + + crate::builtin::test_utils::roundtrip(&rect, expected_json); + } } diff --git a/godot-core/src/builtin/transform2d.rs b/godot-core/src/builtin/transform2d.rs index 12185298a..c6a892ff0 100644 --- a/godot-core/src/builtin/transform2d.rs +++ b/godot-core/src/builtin/transform2d.rs @@ -27,6 +27,7 @@ use super::{real, RAffine2, RMat2}; /// /// For methods that don't take translation into account, see [`Basis2D`]. #[derive(Default, Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Transform2D { /// The first basis vector. @@ -739,4 +740,13 @@ mod test { "let with: Transform2D three components infinite should not be finite.", ); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let transform = Transform2D::default(); + let expected_json = "{\"a\":{\"x\":0.0,\"y\":0.0},\"b\":{\"x\":0.0,\"y\":0.0},\"origin\":{\"x\":0.0,\"y\":0.0}}"; + + crate::builtin::test_utils::roundtrip(&transform, expected_json); + } } diff --git a/godot-core/src/builtin/transform3d.rs b/godot-core/src/builtin/transform3d.rs index ebcfcd2cb..ce48f0702 100644 --- a/godot-core/src/builtin/transform3d.rs +++ b/godot-core/src/builtin/transform3d.rs @@ -24,6 +24,7 @@ use super::{Aabb, Basis, Plane, Projection, Vector3}; /// [ a.z b.z c.z o.z ] /// ``` #[derive(Default, Copy, Clone, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Transform3D { /// The basis is a matrix containing 3 vectors as its columns. They can be @@ -459,4 +460,13 @@ mod test { "Transform3D with two components infinite should not be finite.", ); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let transform = Transform3D::default(); + let expected_json = "{\"basis\":{\"rows\":[{\"x\":1.0,\"y\":0.0,\"z\":0.0},{\"x\":0.0,\"y\":1.0,\"z\":0.0},{\"x\":0.0,\"y\":0.0,\"z\":1.0}]},\"origin\":{\"x\":0.0,\"y\":0.0,\"z\":0.0}}"; + + crate::builtin::test_utils::roundtrip(&transform, expected_json); + } } diff --git a/godot-core/src/builtin/vector2.rs b/godot-core/src/builtin/vector2.rs index 7ae14f9b6..c9c77374c 100644 --- a/godot-core/src/builtin/vector2.rs +++ b/godot-core/src/builtin/vector2.rs @@ -27,6 +27,7 @@ use super::{real, RAffine2, RVec2}; /// /// See [`Vector2i`] for its integer counterpart. #[derive(Debug, Default, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Vector2 { /// The vector's X component. @@ -369,4 +370,13 @@ mod test { Vector2::is_equal_approx ); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let vector = Vector2::default(); + let expected_json = "{\"x\":0.0,\"y\":0.0}"; + + crate::builtin::test_utils::roundtrip(&vector, expected_json); + } } diff --git a/godot-core/src/builtin/vector2i.rs b/godot-core/src/builtin/vector2i.rs index f127a78ee..01274b5e4 100644 --- a/godot-core/src/builtin/vector2i.rs +++ b/godot-core/src/builtin/vector2i.rs @@ -24,6 +24,7 @@ use super::IVec2; /// configured with an engine build option. Use `i64` or [`PackedInt64Array`] if 64-bit values are /// needed. #[derive(Debug, Default, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Vector2i { /// The vector's X component. @@ -140,4 +141,13 @@ mod test { assert_eq!(a.coord_min(b), Vector2i::new(0, 3)); assert_eq!(a.coord_max(b), Vector2i::new(1, 5)); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let vector = Vector2i::default(); + let expected_json = "{\"x\":0,\"y\":0}"; + + crate::builtin::test_utils::roundtrip(&vector, expected_json); + } } diff --git a/godot-core/src/builtin/vector3.rs b/godot-core/src/builtin/vector3.rs index 28ee21839..88bce1210 100644 --- a/godot-core/src/builtin/vector3.rs +++ b/godot-core/src/builtin/vector3.rs @@ -28,6 +28,7 @@ use super::{real, Basis, RVec3}; /// /// See [`Vector3i`] for its integer counterpart. #[derive(Debug, Default, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Vector3 { /// The vector's X component. @@ -432,4 +433,13 @@ mod test { Vector3::is_equal_approx ); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let vector = Vector3::default(); + let expected_json = "{\"x\":0.0,\"y\":0.0,\"z\":0.0}"; + + crate::builtin::test_utils::roundtrip(&vector, expected_json); + } } diff --git a/godot-core/src/builtin/vector3i.rs b/godot-core/src/builtin/vector3i.rs index 270cfef8f..f7c7c6e48 100644 --- a/godot-core/src/builtin/vector3i.rs +++ b/godot-core/src/builtin/vector3i.rs @@ -24,6 +24,7 @@ use super::IVec3; /// configured with an engine build option. Use `i64` or [`PackedInt64Array`] if 64-bit values are /// needed. #[derive(Debug, Default, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Vector3i { /// The vector's X component. @@ -151,4 +152,13 @@ mod test { assert_eq!(a.coord_min(b), Vector3i::new(0, 3, 2)); assert_eq!(a.coord_max(b), Vector3i::new(1, 5, 5)); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let vector = Vector3i::default(); + let expected_json = "{\"x\":0,\"y\":0,\"z\":0}"; + + crate::builtin::test_utils::roundtrip(&vector, expected_json); + } } diff --git a/godot-core/src/builtin/vector4.rs b/godot-core/src/builtin/vector4.rs index d59186b81..85901ac99 100644 --- a/godot-core/src/builtin/vector4.rs +++ b/godot-core/src/builtin/vector4.rs @@ -25,6 +25,7 @@ use super::{real, RVec4}; /// /// See [`Vector4i`] for its integer counterpart. #[derive(Debug, Default, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Vector4 { /// The vector's X component. @@ -160,4 +161,13 @@ mod test { Vector4::is_equal_approx ); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let vector = Vector4::default(); + let expected_json = "{\"x\":0.0,\"y\":0.0,\"z\":0.0,\"w\":0.0}"; + + crate::builtin::test_utils::roundtrip(&vector, expected_json); + } } diff --git a/godot-core/src/builtin/vector4i.rs b/godot-core/src/builtin/vector4i.rs index d4bd1e1da..454bf82fc 100644 --- a/godot-core/src/builtin/vector4i.rs +++ b/godot-core/src/builtin/vector4i.rs @@ -23,6 +23,7 @@ use super::IVec4; /// configured with an engine build option. Use `i64` or [`PackedInt64Array`] if 64-bit values are /// needed. #[derive(Debug, Default, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(C)] pub struct Vector4i { /// The vector's X component. @@ -138,4 +139,13 @@ mod test { assert_eq!(a.coord_min(b), Vector4i::new(0, 3, 2, 0),); assert_eq!(a.coord_max(b), Vector4i::new(1, 5, 5, 1)); } + + #[cfg(feature = "serde")] + #[test] + fn serde_roundtrip() { + let vector = Vector4i::default(); + let expected_json = "{\"x\":0,\"y\":0,\"z\":0,\"w\":0}"; + + crate::builtin::test_utils::roundtrip(&vector, expected_json); + } } diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 1813708bc..8011eb89d 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -13,6 +13,7 @@ formatted = ["godot-core/codegen-fmt"] double-precision = ["godot-core/double-precision"] custom-godot = ["godot-core/custom-godot"] threads = ["godot-core/threads"] +serde = ["godot-core/serde"] # Private features, they are under no stability guarantee codegen-full = ["godot-core/codegen-full"] diff --git a/itest/rust/Cargo.toml b/itest/rust/Cargo.toml index dd84f5c1e..2c873d600 100644 --- a/itest/rust/Cargo.toml +++ b/itest/rust/Cargo.toml @@ -13,6 +13,7 @@ default = [] trace = ["godot/trace"] double-precision = ["godot/double-precision"] threads = ["godot/threads"] +serde = ["godot/serde"] [dependencies] godot = { path = "../../godot", default-features = false, features = ["formatted"] }