Skip to content
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
248 changes: 244 additions & 4 deletions crates/generators/rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::{collections::HashMap, fs, io, path::Path};

use blueberry_ast::{
Commented, ConstDef, ConstValue, Definition, EnumDef, MessageDef, ModuleDef, StructDef, Type,
TypeDef,
Annotation, AnnotationParam, Commented, ConstDef, ConstValue, Definition, EnumDef, MessageDef,
ModuleDef, StructDef, Type, TypeDef,
};
use blueberry_codegen_core::{CodegenError, GeneratedFile};
use genco::lang::rust::Tokens;
Expand Down Expand Up @@ -145,13 +145,22 @@ impl RustGenerator {

fn runtime_module_items(&self) -> Tokens {
quote! {
use core::marker::PhantomData;
use cdr::{deserialize, serialize, CdrLe, Infinite};
use serde::{de::DeserializeOwned, Serialize};
use zenoh::bytes::ZBytes;
use zenoh::handlers::fifo::FifoChannelHandler;
use zenoh::pubsub::Publisher;
use zenoh::{sample::Sample, Error as ZenohError};

pub type DefaultSubscriber =
zenoh::pubsub::Subscriber<FifoChannelHandler<Sample>>;

#[derive(Debug)]
pub enum Error {
Cdr(cdr::Error),
InvalidEnum { name: &'static str, value: i64 },
Zenoh(ZenohError),
}

impl From<cdr::Error> for Error {
Expand All @@ -160,6 +169,12 @@ impl RustGenerator {
}
}

impl From<ZenohError> for Error {
fn from(err: ZenohError) -> Self {
Error::Zenoh(err)
}
}

pub trait CdrEncoding: Serialize + DeserializeOwned {
fn to_payload(&self) -> Result<Vec<u8>, Error> {
serialize::<_, _, CdrLe>(self, Infinite).map_err(Error::Cdr)
Expand All @@ -174,6 +189,84 @@ impl RustGenerator {
}

impl<T> CdrEncoding for T where T: Serialize + DeserializeOwned {}

pub fn format_topic(template: &str) -> String {
template.to_string()
}

pub fn from_sample<T: CdrEncoding>(sample: &Sample) -> Result<T, Error> {
let payload = sample.payload().to_bytes();
T::from_payload(&payload)
}

pub fn to_zbytes<T: CdrEncoding>(value: &T) -> Result<ZBytes, Error> {
value.to_payload().map(ZBytes::from)
}

pub async fn publish_with<T: CdrEncoding>(
publisher: &Publisher<'_>,
value: &T,
) -> Result<(), Error> {
let payload = to_zbytes(value)?;
publisher.put(payload).await.map_err(Error::from)
}

pub async fn recv_decoded<T: CdrEncoding>(
subscriber: &DefaultSubscriber,
) -> Result<T, Error> {
let sample = subscriber.recv_async().await?;
from_sample(&sample)
}

pub struct TypedPublisher<'a, T> {
inner: Publisher<'a>,
_marker: PhantomData<&'a T>,
}

impl<'a, T> TypedPublisher<'a, T>
where
T: CdrEncoding,
{
pub fn new(inner: Publisher<'a>) -> Self {
Self {
inner,
_marker: PhantomData,
}
}

pub fn inner(&self) -> &Publisher<'a> {
&self.inner
}

pub async fn publish(&self, value: &T) -> Result<(), Error> {
publish_with(&self.inner, value).await
}
}

pub struct TypedSubscriber<T> {
inner: DefaultSubscriber,
_marker: PhantomData<T>,
}

impl<T> TypedSubscriber<T>
where
T: CdrEncoding,
{
pub fn new(inner: DefaultSubscriber) -> Self {
Self {
inner,
_marker: PhantomData,
}
}

pub fn inner(&self) -> &DefaultSubscriber {
&self.inner
}

pub async fn recv(&self) -> Result<T, Error> {
recv_decoded(&self.inner).await
}
}
}
}

Expand Down Expand Up @@ -322,22 +415,35 @@ impl RustGenerator {
let mut path = scope.to_vec();
path.push(struct_def.node.name.clone());
let members = self.registry.collect_struct_members(&path);
self.emit_struct_like(&ident, &members, &struct_def.comments, scope)
self.emit_struct_like(
&ident,
&members,
&struct_def.comments,
&struct_def.annotations,
scope,
)
}

fn emit_message(&self, message_def: &Commented<MessageDef>, scope: &[String]) -> Tokens {
let ident = message_def.node.name.clone();
let mut path = scope.to_vec();
path.push(message_def.node.name.clone());
let members = self.registry.collect_message_members(&path);
self.emit_struct_like(&ident, &members, &message_def.comments, scope)
self.emit_struct_like(
&ident,
&members,
&message_def.comments,
&message_def.annotations,
scope,
)
}

fn emit_struct_like(
&self,
ident: &str,
members: &[ResolvedMember],
comments: &[String],
annotations: &[Annotation],
scope: &[String],
) -> Tokens {
let fields: Vec<Tokens> = members
Expand All @@ -353,6 +459,7 @@ impl RustGenerator {
})
.collect();
let docs = self.doc_attributes(comments);
let topic_impl = self.topic_impl(ident, annotations);

quote! {
$(for doc in docs => $doc)
Expand All @@ -367,6 +474,96 @@ impl RustGenerator {
pub struct $ident {
$(for f in fields => $f)
}

$topic_impl
}
}

fn topic_impl(&self, ident: &str, annotations: &[Annotation]) -> Tokens {
let Some(topic) = self.topic_value(annotations) else {
return Tokens::new();
};
let topic_literal = format!("{topic:?}");

quote! {
impl $ident {
pub const TOPIC_TEMPLATE: &'static str = $topic_literal;
pub const SCHEMA: &'static str = concat!("blueberry.", stringify!($ident));

pub fn topic() -> String {
runtime::format_topic(Self::TOPIC_TEMPLATE)
}

pub async fn declare_publisher<'a>(
session: &'a zenoh::Session,
) -> Result<runtime::TypedPublisher<'a, Self>, runtime::Error> {
let topic = Self::topic();
let encoding = zenoh::bytes::Encoding::APPLICATION_CDR
.with_schema(Self::SCHEMA);
let publisher = session
.declare_publisher(topic)
.encoding(encoding)
.await
.map_err(runtime::Error::from)?;

Ok(runtime::TypedPublisher::new(publisher))
}

pub async fn publish_with<'a>(
&self,
publisher: &runtime::TypedPublisher<'a, Self>,
) -> Result<(), runtime::Error> {
publisher.publish(self).await
}

pub async fn declare_subscriber<'a>(
session: &'a zenoh::Session,
) -> Result<runtime::TypedSubscriber<Self>, runtime::Error> {
let topic = Self::topic();
let subscriber = session
.declare_subscriber(topic)
.await
.map_err(runtime::Error::from)?;

Ok(runtime::TypedSubscriber::new(subscriber))
}

pub async fn recv(
subscriber: &runtime::TypedSubscriber<Self>,
) -> Result<Self, runtime::Error> {
subscriber.recv().await
}
}
}
}

fn topic_value(&self, annotations: &[Annotation]) -> Option<String> {
annotations
.iter()
.find(|annotation| {
annotation
.name
.last()
.map(|segment| segment.eq_ignore_ascii_case("topic"))
.unwrap_or(false)
})
.and_then(|annotation| self.annotation_string(annotation))
}

fn annotation_string(&self, annotation: &Annotation) -> Option<String> {
annotation.params.iter().find_map(|param| match param {
AnnotationParam::Named { name, value } if name.eq_ignore_ascii_case("value") => {
self.const_string(value)
}
AnnotationParam::Positional(value) => self.const_string(value),
AnnotationParam::Named { value, .. } => self.const_string(value),
})
}

fn const_string(&self, value: &ConstValue) -> Option<String> {
match value {
ConstValue::String(text) => Some(text.clone()),
_ => None,
}
}

Expand Down Expand Up @@ -750,4 +947,47 @@ mod tests {
other => panic!("expected scoped name, got {other:?}"),
}
}

#[test]
fn emits_topic_helpers_for_topic_annotations() {
let definitions = load_fixture("crates/parser/tests/fixtures/message_default.idl");
let generator = RustGenerator::new(&definitions);
let root = generator.generate_root(&definitions);

assert!(
root.contains("TOPIC_TEMPLATE"),
"generated code should include topic template"
);
assert!(
root.contains("blueberry/devices/status"),
"topic literal should be embedded"
);
assert!(
root.contains("declare_publisher"),
"publisher helper should be generated"
);
assert!(
root.contains("declare_subscriber"),
"subscriber helper should be generated"
);
assert!(
root.contains("TypedSubscriber"),
"subscriber helpers should be typed"
);
}

#[test]
fn runtime_includes_zenoh_helpers() {
let generator = RustGenerator::new(&[]);
let runtime = generator.generate_runtime();

assert!(
runtime.contains("format_topic"),
"runtime should expose topic formatter"
);
assert!(
runtime.contains("publish_with"),
"runtime should expose publish helper"
);
}
}
1 change: 1 addition & 0 deletions crates/generators/rust/tests/compile_generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ edition = "2024"
[dependencies]
serde = { version = "1", features = ["derive"] }
cdr = "0.2"
zenoh = "1.6.2"
"#,
)
.expect("failed to write Cargo.toml");
Expand Down
13 changes: 13 additions & 0 deletions crates/parser/tests/fixtures/message_default.idl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@topic(value = "blueberry/devices/status")
struct Person {
@default("Potato")
string name;
@default(0x1234)
long age;
@default(1)
boolean isActive;
@default("maisquenada")
string<16> codename;
@default(0, 1, 2, 3, 4, 0xffff, 6, 7)
long readings[8];
};