use anyhow::Context as _;
use re_ros_msg::MessageSchema;
use super::timestamp::TimestampLocation;
use re_ros_msg::message_spec::{
ArraySize, BuiltInType, ComplexType, MessageSpecification, Type, message_package,
};
#[derive(Debug)]
pub(super) struct MessageDecodePlan {
schema_name: String,
messages: Vec<MessageLayout>,
timestamp_location: TimestampLocation,
}
impl MessageDecodePlan {
pub(super) const ROOT_ID: usize = 0;
pub(super) fn from_schema(schema: &MessageSchema) -> anyhow::Result<Self> {
let specs = std::iter::chain(std::iter::once(&schema.spec), &schema.dependencies)
.collect::<Vec<_>>();
let message_ids = specs
.iter()
.enumerate()
.map(|(id, spec)| (spec.name.as_str(), id))
.collect::<std::collections::HashMap<_, _>>();
let messages = specs
.iter()
.map(|spec| {
let fields = spec
.fields
.iter()
.map(|field| {
Ok(FieldLayout {
name: field.name.clone(),
value: ValueLayout::from_type(spec, &field.ty, &specs, &message_ids)
.with_context(|| {
format!("failed to resolve ROS message field `{}`", field.name)
})?,
})
})
.collect::<anyhow::Result<_>>()?;
Ok(MessageLayout { fields })
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(Self {
schema_name: schema.spec.name.clone(),
timestamp_location: TimestampLocation::from_messages(&messages, Self::ROOT_ID),
messages,
})
}
pub(super) fn schema_name(&self) -> &str {
&self.schema_name
}
pub(super) fn message(&self, id: usize) -> &MessageLayout {
&self.messages[id]
}
pub(super) fn timestamp_location(&self) -> &TimestampLocation {
&self.timestamp_location
}
}
#[derive(Debug)]
pub(super) struct MessageLayout {
fields: Vec<FieldLayout>,
}
impl MessageLayout {
pub(super) fn fields(&self) -> &[FieldLayout] {
&self.fields
}
}
#[derive(Debug)]
pub(super) struct FieldLayout {
name: String,
value: ValueLayout,
}
impl FieldLayout {
pub(super) fn name(&self) -> &str {
&self.name
}
pub(super) fn value(&self) -> &ValueLayout {
&self.value
}
}
#[derive(Debug)]
pub(super) enum ValueLayout {
BuiltIn(BuiltInType),
Message(usize),
Array { element: Box<Self>, size: ArraySize },
}
impl ValueLayout {
fn from_type(
scope: &MessageSpecification,
ty: &Type,
specs: &[&MessageSpecification],
message_ids: &std::collections::HashMap<&str, usize>,
) -> anyhow::Result<Self> {
Ok(match ty {
Type::BuiltIn(ty) => Self::BuiltIn(ty.clone()),
Type::Complex(complex_type) => {
let full_name = match complex_type {
ComplexType::Absolute { package, name } => format!("{package}/{name}"),
ComplexType::Relative { name } => match message_package(&scope.name) {
Some(package) => format!("{package}/{name}"),
None => name.clone(),
},
};
let id = *message_ids.get(full_name.as_str()).ok_or_else(|| match complex_type {
ComplexType::Absolute { .. } => {
anyhow::anyhow!("could not resolve complex type `{full_name}`")
}
ComplexType::Relative { name } => anyhow::anyhow!(
"relative ROS type `{name}` must resolve within the containing message package as `{full_name}`, but no such message definition was found"
),
})?;
let spec = specs[id];
if let Some(primitive_type) = spec.underlying_type_if_enum_like()? {
Self::BuiltIn(primitive_type.clone())
} else {
Self::Message(id)
}
}
Type::Array { ty, size } => Self::Array {
element: Box::new(Self::from_type(scope, ty, specs, message_ids)?),
size: size.clone(),
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::assert_matches;
#[test]
fn resolves_relative_message_types_once() {
let schema = MessageSchema::parse(
"test/msg/Outer",
r#"
Inner inner
================================================================================
MSG: test/Inner
uint32 value
"#,
)
.unwrap();
let plan = MessageDecodePlan::from_schema(&schema).unwrap();
let field = &plan.message(MessageDecodePlan::ROOT_ID).fields()[0];
assert_matches!(field.value(), ValueLayout::Message(1));
}
#[test]
fn resolves_enum_like_messages_to_their_underlying_scalar() {
let schema = MessageSchema::parse(
"test/Message",
r#"
test/Mode mode
================================================================================
MSG: test/Mode
int8 OFF=0
int8 ON=1
"#,
)
.unwrap();
let plan = MessageDecodePlan::from_schema(&schema).unwrap();
let field = &plan.message(MessageDecodePlan::ROOT_ID).fields()[0];
assert_matches!(field.value(), ValueLayout::BuiltIn(BuiltInType::Int8));
}
#[test]
fn rejects_unresolved_relative_message_types() {
let schema = MessageSchema::parse(
"test/msg/Message",
r#"
Time timestamp
================================================================================
MSG: other/Time
uint32 sec
uint32 nanosec
"#,
)
.unwrap();
let err = format!("{:#}", MessageDecodePlan::from_schema(&schema).unwrap_err());
assert!(err.contains("timestamp"));
assert!(err.contains("test/Time"));
}
}