use std::cell::RefCell;
use std::sync::Arc;
use zerodds_ast::Ast;
use zerodds_ast_layout::{Dialect, Encoding, Endianness};
use zerodds_dcps::dds_type::{DdsType, DecodeError, EncodeError};
use zerodds_types::dynamic::DynamicData;
use zerodds_wire_codec::ProbeRequest;
thread_local! {
static DECODE_CONTEXT: RefCell<Option<(Arc<Ast>, String, Dialect)>> = const { RefCell::new(None) };
}
pub fn with_decode_context<R>(
ast: &Arc<Ast>,
type_name: &str,
dialect: Dialect,
f: impl FnOnce() -> R,
) -> R {
let previous =
DECODE_CONTEXT.with(|c| c.replace(Some((Arc::clone(ast), type_name.to_string(), dialect))));
let result = f();
DECODE_CONTEXT.with(|c| *c.borrow_mut() = previous);
result
}
#[derive(Clone)]
pub struct DynamicRpcValue {
pub ast: Arc<Ast>,
pub type_name: String,
pub data: DynamicData,
pub dialect: Dialect,
}
impl std::fmt::Debug for DynamicRpcValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DynamicRpcValue")
.field("type_name", &self.type_name)
.field("data", &self.data)
.finish_non_exhaustive()
}
}
impl DynamicRpcValue {
#[must_use]
pub fn new(ast: Arc<Ast>, type_name: String, data: DynamicData) -> Self {
Self {
ast,
type_name,
data,
dialect: Dialect::default(),
}
}
#[must_use]
pub fn in_dialect(mut self, dialect: Dialect) -> Self {
self.dialect = dialect;
self
}
fn probe_request_in(&self, order: Endianness, encoding: Encoding) -> ProbeRequest {
ProbeRequest::in_dialect(&self.type_name, encoding, order, self.dialect)
}
fn encode_in(
&self,
order: Endianness,
encoding: Encoding,
start: usize,
out: &mut Vec<u8>,
) -> Result<(), EncodeError> {
let bytes = zerodds_wire_codec::encode_continuing(
&self.ast,
&self.probe_request_in(order, encoding),
&self.data,
start,
)
.map_err(|_| EncodeError::Invalid {
what: "dynamic RPC payload refused by the wire codec",
})?;
out.extend(bytes);
Ok(())
}
fn decode_in(
order: Endianness,
encoding: Encoding,
bytes: &[u8],
start: usize,
) -> Result<Self, DecodeError> {
let (ast, type_name, dialect) =
DECODE_CONTEXT
.with(|c| c.borrow().clone())
.ok_or(DecodeError::Invalid {
what: "no decode context set — see crate::dynamic_rpc::with_decode_context",
})?;
let request = ProbeRequest::in_dialect(&type_name, encoding, order, dialect);
let data =
zerodds_wire_codec::decode_continuing::<DynamicData>(&ast, &request, bytes, start)
.map_err(|_| DecodeError::Invalid {
what: "dynamic RPC payload refused by the wire codec",
})?;
Ok(Self {
ast,
type_name,
data,
dialect,
})
}
}
impl DdsType for DynamicRpcValue {
const TYPE_NAME: &'static str =
"zerodds::webdds_bridge::DynamicRpcValue (requires crate::dynamic_rpc's decode context)";
fn encode(&self, out: &mut Vec<u8>) -> Result<(), EncodeError> {
self.encode_in(Endianness::Little, Encoding::PlainCdr2, 0, out)
}
fn encode_be(&self, out: &mut Vec<u8>) -> Result<(), EncodeError> {
self.encode_in(Endianness::Big, Encoding::PlainCdr2, 0, out)
}
fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
Self::decode_in(Endianness::Little, Encoding::PlainCdr2, bytes, 0)
}
fn decode_be(bytes: &[u8]) -> Result<Self, DecodeError> {
Self::decode_in(Endianness::Big, Encoding::PlainCdr2, bytes, 0)
}
fn encode_xcdr1(&self, out: &mut Vec<u8>) -> Result<(), EncodeError> {
self.encode_in(Endianness::Little, Encoding::PlainCdr1, 0, out)
}
fn decode_xcdr1(bytes: &[u8]) -> Result<Self, DecodeError> {
Self::decode_in(Endianness::Little, Encoding::PlainCdr1, bytes, 0)
}
fn encode_continuing(&self, prefix: &mut Vec<u8>, xcdr1: bool) -> Result<(), EncodeError> {
let encoding = if xcdr1 {
Encoding::PlainCdr1
} else {
Encoding::PlainCdr2
};
self.encode_in(Endianness::Little, encoding, prefix.len(), prefix)
}
fn decode_continuing(bytes: &[u8], offset: usize, xcdr1: bool) -> Result<Self, DecodeError> {
let encoding = if xcdr1 {
Encoding::PlainCdr1
} else {
Encoding::PlainCdr2
};
Self::decode_in(Endianness::Little, encoding, bytes, offset)
}
fn field_value(&self, path: &str) -> Option<zerodds_dcps::FilterValue> {
use zerodds_dcps::FilterValue as Value;
let mut value = &self.data;
for component in path.split('.') {
value = value.member(component)?;
}
match value {
DynamicData::Boolean(v) => Some(Value::Bool(*v)),
DynamicData::Integer { value, .. } => i64::try_from(*value).ok().map(Value::Int),
DynamicData::Float { value, .. } => Some(Value::Float(*value)),
DynamicData::Text(v) | DynamicData::Character { value: v, .. } => {
Some(Value::String(v.clone()))
}
_ => None,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use zerodds_types::dynamic::DynamicType;
fn model(idl: &str, type_name: &str) -> (Arc<Ast>, DynamicType) {
let ast = Arc::new(zerodds_wire_probe::encode::model_from_idl(idl, "t.idl").unwrap());
let dt = DynamicType::find(&ast, type_name).unwrap();
(ast, dt)
}
#[test]
fn encode_needs_no_external_context() {
let (ast, _dt) = model(
"module M { @final struct Params { long a; long b; }; };",
"M::Params",
);
let data = DynamicData::Aggregate(vec![
(
"a".to_string(),
DynamicData::integer(zerodds_ast::PrimitiveKind::Int32, 3),
),
(
"b".to_string(),
DynamicData::integer(zerodds_ast::PrimitiveKind::Int32, 4),
),
]);
let value = DynamicRpcValue::new(ast, "M::Params".to_string(), data);
let mut out = Vec::new();
value.encode(&mut out).expect("encode");
assert!(!out.is_empty());
}
#[test]
fn the_codec_dialect_reaches_the_octets_of_a_call() {
let (ast, _dt) = model(
"module M { @mutable struct Call { @key long id; long v; }; };",
"M::Call",
);
let data = DynamicData::Aggregate(vec![
(
"id".to_string(),
DynamicData::integer(zerodds_ast::PrimitiveKind::Int32, 7),
),
(
"v".to_string(),
DynamicData::integer(zerodds_ast::PrimitiveKind::Int32, 9),
),
]);
let spec = DynamicRpcValue::new(Arc::clone(&ast), "M::Call".to_string(), data);
let fastdds = spec.clone().in_dialect(Dialect::FastDds);
let (mut a, mut b) = (Vec::new(), Vec::new());
spec.encode(&mut a).expect("encode");
fastdds.encode(&mut b).expect("encode");
assert_eq!(a[4..8], [0x00, 0x00, 0x00, 0xa0], "{a:02x?}");
assert_eq!(b[4..8], [0x00, 0x00, 0x00, 0x20], "{b:02x?}");
let decoded = with_decode_context(&ast, "M::Call", Dialect::FastDds, || {
DynamicRpcValue::decode(&b)
})
.expect("decode");
assert_eq!(decoded.dialect, Dialect::FastDds);
assert_eq!(decoded.data.member("id"), fastdds.data.member("id"));
assert_eq!(decoded.data.member("v"), fastdds.data.member("v"));
}
#[test]
fn decode_without_context_is_a_named_error_not_a_panic() {
let err = DynamicRpcValue::decode(&[0, 0, 0, 0]).unwrap_err();
assert!(matches!(err, DecodeError::Invalid { .. }));
}
#[test]
fn round_trips_through_encode_and_decode_with_context_set() {
let (ast, _dt) = model(
"module M { @final struct Params { long a; long b; }; };",
"M::Params",
);
let data = DynamicData::Aggregate(vec![
(
"a".to_string(),
DynamicData::integer(zerodds_ast::PrimitiveKind::Int32, 7),
),
(
"b".to_string(),
DynamicData::integer(zerodds_ast::PrimitiveKind::Int32, 9),
),
]);
let value = DynamicRpcValue::new(Arc::clone(&ast), "M::Params".to_string(), data);
let mut bytes = Vec::new();
value.encode(&mut bytes).expect("encode");
let decoded = with_decode_context(&ast, "M::Params", Dialect::default(), || {
DynamicRpcValue::decode(&bytes)
})
.expect("decode");
assert_eq!(decoded.data.member("a"), value.data.member("a"));
assert_eq!(decoded.data.member("b"), value.data.member("b"));
}
#[test]
fn context_is_restored_after_the_call_not_leaked() {
let (ast, _dt) = model(
"module M { @final struct Params { long a; }; };",
"M::Params",
);
let data = DynamicData::Aggregate(vec![(
"a".to_string(),
DynamicData::integer(zerodds_ast::PrimitiveKind::Int32, 1),
)]);
let value = DynamicRpcValue::new(Arc::clone(&ast), "M::Params".to_string(), data);
let mut bytes = Vec::new();
value.encode(&mut bytes).unwrap();
let _ = with_decode_context(&ast, "M::Params", Dialect::default(), || {
DynamicRpcValue::decode(&bytes)
});
let err = DynamicRpcValue::decode(&bytes).unwrap_err();
assert!(matches!(err, DecodeError::Invalid { .. }));
}
}