Skip to content

Add serde support to messages #131

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

Merged
merged 1 commit into from
Apr 29, 2022
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,13 @@ file(MAKE_DIRECTORY "${_output_path}")
set(_target_suffix "__rs")

set(CRATES_DEPENDENCIES "rosidl_runtime_rs = \"*\"")
set(SERDE_FEATURES "[\"dep:serde\", \"rosidl_runtime_rs/serde\"")
foreach(_pkg_name ${rosidl_generate_interfaces_DEPENDENCY_PACKAGE_NAMES})
find_package(${_pkg_name} REQUIRED)
set(CRATES_DEPENDENCIES "${CRATES_DEPENDENCIES}\n${_pkg_name} = \"*\"")
set(SERDE_FEATURES "${SERDE_FEATURES}, \"${_pkg_name}/serde\"")
endforeach()
set(SERDE_FEATURES "${SERDE_FEATURES}]")
ament_index_register_resource("rust_packages")


Expand Down
6 changes: 5 additions & 1 deletion rosidl_generator_rs/resource/Cargo.toml.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ edition = "2021"

[dependencies]
libc = "0.2"
@CRATES_DEPENDENCIES@
serde = { version = "1", optional = true, features = ["derive"] }
@CRATES_DEPENDENCIES@

[features]
serde = @SERDE_FEATURES@
6 changes: 6 additions & 0 deletions rosidl_generator_rs/resource/msg.rs.em
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ from rosidl_parser.definition import UnboundedString
from rosidl_parser.definition import UnboundedWString
}@
pub mod rmw {
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@[for subfolder, msg_spec in msg_specs]@
@{
type_name = msg_spec.structure.namespaced_type.name
Expand All @@ -31,6 +33,7 @@ extern "C" {
@# it just calls the drop/fini functions of all fields
// Corresponds to @(package_name)__@(subfolder)__@(type_name)
#[repr(C)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct @(type_name) {
@[for member in msg_spec.structure.members]@
Expand Down Expand Up @@ -87,11 +90,14 @@ impl rosidl_runtime_rs::RmwMessage for @(type_name) where Self: Sized {
@# ############ Idiomatic message types ############
@# #################################################
@# These types use standard Rust containers where possible.
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@[for subfolder, msg_spec in msg_specs]@
@{
type_name = msg_spec.structure.namespaced_type.name
}@

#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct @(type_name) {
@[for member in msg_spec.structure.members]@
Expand Down
8 changes: 7 additions & 1 deletion rosidl_runtime_rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ path = "src/lib.rs"
[dependencies]
# Needed for FFI
libc = "0.2"
# Optional dependency for making it possible to convert messages to and from
# formats such as JSON, YAML, Pickle, etc.
serde = { version = "1", optional = true }

[dev-dependencies]
# Needed for writing property tests
quickcheck = "1"
quickcheck = "1"
# Needed for testing serde support
serde_json = "1"

18 changes: 17 additions & 1 deletion rosidl_runtime_rs/src/sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use std::hash::{Hash, Hasher};
use std::iter::{Extend, FromIterator, FusedIterator};
use std::ops::{Deref, DerefMut};

#[cfg(feature = "serde")]
mod serde;

use crate::traits::SequenceAlloc;

/// An unbounded sequence.
Expand Down Expand Up @@ -674,7 +677,20 @@ macro_rules! seq {
#[cfg(test)]
mod tests {
use super::*;
use quickcheck::quickcheck;
use quickcheck::{quickcheck, Arbitrary, Gen};

impl<T: Arbitrary + SequenceAlloc> Arbitrary for Sequence<T> {
fn arbitrary(g: &mut Gen) -> Self {
Vec::arbitrary(g).into()
}
}

impl<T: Arbitrary + SequenceAlloc> Arbitrary for BoundedSequence<T, 256> {
fn arbitrary(g: &mut Gen) -> Self {
let len = u8::arbitrary(g);
(0..len).map(|_| T::arbitrary(g)).collect()
}
}

quickcheck! {
fn test_extend(xs: Vec<i32>, ys: Vec<i32>) -> bool {
Expand Down
70 changes: 70 additions & 0 deletions rosidl_runtime_rs/src/sequence/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use serde::{de::Error, ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer};

use super::{BoundedSequence, Sequence};
use crate::traits::SequenceAlloc;

impl<'de, T: Deserialize<'de> + SequenceAlloc> Deserialize<'de> for Sequence<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let v: Vec<_> = Deserialize::deserialize(deserializer)?;
Ok(Self::from(v))
}
}

impl<T: Serialize + SequenceAlloc> Serialize for Sequence<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.len()))?;
for element in self.iter() {
seq.serialize_element(element)?;
}
seq.end()
}
}

impl<'de, T: Deserialize<'de> + SequenceAlloc, const N: usize> Deserialize<'de>
for BoundedSequence<T, N>
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let v: Vec<_> = Deserialize::deserialize(deserializer)?;
Self::try_from(v).map_err(D::Error::custom)
}
}

impl<T: Serialize + SequenceAlloc, const N: usize> Serialize for BoundedSequence<T, N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.inner.serialize(serializer)
}
}

#[cfg(test)]
mod tests {
use crate::{BoundedSequence, Sequence};
use quickcheck::quickcheck;

quickcheck! {
fn test_json_roundtrip_sequence(xs: Sequence<i32>) -> bool {
let value = serde_json::to_value(xs.clone()).unwrap();
let recovered = serde_json::from_value(value).unwrap();
xs == recovered
}
}

quickcheck! {
fn test_json_roundtrip_bounded_sequence(xs: BoundedSequence<i32, 256>) -> bool {
let value = serde_json::to_value(xs.clone()).unwrap();
let recovered = serde_json::from_value(value).unwrap();
xs == recovered
}
}
}
37 changes: 37 additions & 0 deletions rosidl_runtime_rs/src/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use std::fmt::{self, Debug, Display};
use std::hash::{Hash, Hasher};
use std::ops::{Deref, DerefMut};

#[cfg(feature = "serde")]
mod serde;

use crate::sequence::Sequence;
use crate::traits::SequenceAlloc;

Expand Down Expand Up @@ -449,3 +452,37 @@ impl Display for StringExceedsBoundsError {
}

impl std::error::Error for StringExceedsBoundsError {}

#[cfg(test)]
mod tests {
use super::*;
use quickcheck::{Arbitrary, Gen};

impl Arbitrary for String {
fn arbitrary(g: &mut Gen) -> Self {
std::string::String::arbitrary(g).as_str().into()
}
}

impl Arbitrary for WString {
fn arbitrary(g: &mut Gen) -> Self {
std::string::String::arbitrary(g).as_str().into()
}
}

impl Arbitrary for BoundedString<256> {
fn arbitrary(g: &mut Gen) -> Self {
let len = u8::arbitrary(g);
let s: std::string::String = (0..len).map(|_| char::arbitrary(g)).collect();
s.as_str().try_into().unwrap()
}
}

impl Arbitrary for BoundedWString<256> {
fn arbitrary(g: &mut Gen) -> Self {
let len = u8::arbitrary(g);
let s: std::string::String = (0..len).map(|_| char::arbitrary(g)).collect();
s.as_str().try_into().unwrap()
}
}
}
120 changes: 120 additions & 0 deletions rosidl_runtime_rs/src/string/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
use std::ops::Deref;

use super::{BoundedString, BoundedWString, String, WString};

impl<'de> Deserialize<'de> for String {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
std::string::String::deserialize(deserializer).map(|s| Self::from(s.as_str()))
}
}

impl Serialize for String {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Not particularly efficient
let s = std::string::String::from_utf8_lossy(self.deref());
serializer.serialize_str(&s)
}
}

impl<'de> Deserialize<'de> for WString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
std::string::String::deserialize(deserializer).map(|s| Self::from(s.as_str()))
}
}

impl Serialize for WString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Not particularly efficient
let s = std::string::String::from_utf16_lossy(self.deref());
serializer.serialize_str(&s)
}
}

impl<'de, const N: usize> Deserialize<'de> for BoundedString<N> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
std::string::String::deserialize(deserializer)
.and_then(|s| Self::try_from(s.as_str()).map_err(D::Error::custom))
}
}

impl<const N: usize> Serialize for BoundedString<N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.inner.serialize(serializer)
}
}

impl<'de, const N: usize> Deserialize<'de> for BoundedWString<N> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
std::string::String::deserialize(deserializer)
.and_then(|s| Self::try_from(s.as_str()).map_err(D::Error::custom))
}
}

impl<const N: usize> Serialize for BoundedWString<N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.inner.serialize(serializer)
}
}

#[cfg(test)]
mod tests {
use crate::{BoundedString, BoundedWString, String, WString};
use quickcheck::quickcheck;

quickcheck! {
fn test_json_roundtrip_string(s: String) -> bool {
let value = serde_json::to_value(s.clone()).unwrap();
let recovered = serde_json::from_value(value).unwrap();
s == recovered
}
}

quickcheck! {
fn test_json_roundtrip_wstring(s: WString) -> bool {
let value = serde_json::to_value(s.clone()).unwrap();
let recovered = serde_json::from_value(value).unwrap();
s == recovered
}
}

quickcheck! {
fn test_json_roundtrip_bounded_string(s: BoundedString<256>) -> bool {
let value = serde_json::to_value(s.clone()).unwrap();
let recovered = serde_json::from_value(value).unwrap();
s == recovered
}
}

quickcheck! {
fn test_json_roundtrip_bounded_wstring(s: BoundedWString<256>) -> bool {
let value = serde_json::to_value(s.clone()).unwrap();
let recovered = serde_json::from_value(value).unwrap();
s == recovered
}
}
}