use core::fmt::Write as _;
use zerodds_ast::facet::Extensibility;
use zerodds_ast::{Ast, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::Id;
use zerodds_ast_layout::key::{Encoding, Endianness, LayoutKey, Purpose};
use crate::{encoding_for, has_codec};
pub(crate) fn dds_type_impls(
ast: &Ast,
xcdr1: &[Id<TypeNode>],
key_for: &dyn Fn(Encoding) -> Result<LayoutKey, String>,
) -> Result<String, String> {
let mut out = String::new();
for (id, node) in ast.types() {
if node.name.is_empty() || !has_codec(&node.kind) {
continue;
}
if matches!(node.kind, TypeKind::Bitmask { .. }) {
let _ = write!(
out,
"\n// `{}`: a bitmask is a `u32` alias in Rust, so a `DdsType` \
impl for it\n// would be an impl for `u32` and that is `error[E0117]`. It is a member type,\n// never a topic type; the aggregate that carries it has the impl.\n",
node.name
);
continue;
}
if matches!(
node.kind,
TypeKind::Enumeration { .. } | TypeKind::Bitset { .. }
) {
let _ = write!(
out,
"\n// `{}`: {} is a member type. DDS-XTypes 1.3 \u{a7}7.6.2 allows only the \
aggregated types as topic types,\n// so there is no `DdsType` impl for it; the \
aggregate that carries it has one.\n",
node.name,
if matches!(node.kind, TypeKind::Enumeration { .. }) {
"an enumeration"
} else {
"a bitset"
}
);
continue;
}
out.push_str(&one_impl(ast, id, node, xcdr1.contains(&id), key_for)?);
}
Ok(out)
}
fn one_impl(
ast: &Ast,
id: Id<TypeNode>,
node: &TypeNode,
has_xcdr1: bool,
key_for: &dyn Fn(Encoding) -> Result<LayoutKey, String>,
) -> Result<String, String> {
let Ok(path) = zerodds_lang_rules::adapter::cst::path_from_file_root(ast, id) else {
return Ok(format!(
"\n// `{}`: the model has no module chain for it, so it cannot be named \
from the file root and gets no `DdsType` impl.\n",
node.name
));
};
let type_name = qualified_idl_name(ast, id, node);
let mut body = String::new();
let _ = write!(
body,
" /// The peer's topic type name, which XTypes 1.3 \u{a7}7.6.3 \
matches\n /// by strict equality \u{2014} the IDL scoped name, \
without the\n /// leading `::` that Cyclone, Fast DDS and RTI all \
omit.\n const TYPE_NAME: &'static str = \"{type_name}\";\n"
);
let _ = write!(
body,
" const EXTENSIBILITY: zerodds_dcps::Extensibility =\n \
zerodds_dcps::Extensibility::{};\n",
extensibility_variant(ast.effective_extensibility(id))
);
if is_nested(node) {
let _ = write!(
body,
" /// `@nested`, XTypes 1.3 \u{a7}7.4.6.3.5: a member type and not \
a topic\n /// type. `create_topic` refuses it with \
`PreconditionNotMet`.\n const IS_NESTED: bool = true;\n"
);
}
body.push_str(&key_items(ast, id, &path, key_for)?);
body.push_str(&identity_items(ast, id));
body.push_str(&codec_items(
&path,
ast.effective_extensibility(id),
has_xcdr1,
&key_for(encoding_for(ast, id))?,
&key_for(xcdr1_encoding(ast.effective_extensibility(id)))?,
));
body.push_str(&field_value_item(ast, node));
let mut out = format!("\nimpl zerodds_dcps::DdsType for {path} {{\n{body}}}\n");
if !has_xcdr1 {
out.push_str(&format!(
"// `{}`: no XCDR1 codec could be planned for this type, so its four \
XCDR1\n// methods stay the trait's, which refuse. A pair that \
delegated to the XCDR2\n// codec would put version-2 octets under a \
`CDR_LE`/`CDR_BE` encapsulation, and\n// no peer can tell that from a \
sample it is supposed to read.\n",
node.name
));
}
Ok(out)
}
fn key_items(
ast: &Ast,
id: Id<TypeNode>,
path: &str,
key_for: &dyn Fn(Encoding) -> Result<LayoutKey, String>,
) -> Result<String, String> {
if zerodds_ast_layout::keyhash::union_key_discriminator(ast, id).is_some() {
return Err(format!(
"DdsType key holder for `{path}`: a keyed union discriminator requires \
Rust key_holder_be/key_from_holder_be methods, which the union codec \
does not yet generate"
));
}
let data_key = key_for(encoding_for(ast, id))?;
let key = LayoutKey {
purpose: Purpose::Key,
endianness: Endianness::Big,
..data_key
};
let members = zerodds_ast_layout::keyhash::key_members_with(
ast,
id,
key.key_holder_nesting,
key.key_holder_order,
)
.map_err(|error| format!("DdsType key holder for `{path}`: {error}"))?;
if members.is_empty() {
return Ok(String::new());
}
let form = zerodds_ast_layout::keyhash::key_hash_form_for(ast, id, key)
.map_err(|error| format!("DdsType key holder for `{path}`: {error}"))?;
let max_size = match form {
zerodds_ast_layout::keyhash::KeyHashForm::ZeroPadded => "Some(16)",
zerodds_ast_layout::keyhash::KeyHashForm::Md5 => "None",
};
let mut items = format!(
" const HAS_KEY: bool = true;\n \
/// XTypes 1.3 \u{a7}7.6.8 step 5, from the holder's maximum size: \
at most\n /// sixteen octets means the octets themselves, \
zero-padded; a larger or unbounded\n /// maximum means their MD5.\n \
const KEY_HOLDER_MAX_SIZE: Option<usize> = {max_size};\n\n \
// The refusal below is a `panic!`, which this tree's lints deny \
by\n // default; the allow is narrower than asking every consumer \
of a\n // generated file to turn the lint off.\n \
#[allow(clippy::panic)]\n \
fn encode_key_holder_be(&self, holder: &mut zerodds_dcps::dds_type::PlainCdr2BeKeyHolder) \
{{\n \
match {path}::key_holder_be(self) {{\n \
Ok(bytes) => holder.write_bytes(&bytes),\n \
// The generated holder refuses exactly the values the IDL's own \
bounds\n // forbid \u{2014} a `@key char` above U+00FF, a \
`@key string<8>` holding\n // nine. The trait method has no \
way to report, and leaving the holder\n // empty would give \
every such instance one KeyHash and silently merge\n // \
instances the application keeps apart. Refusing by name is the \
lesser\n // of the two.\n \
Err(error) => panic!(\n \
\"the key holder of `{path}` refused this instance's key: {{error}}\"\n \
),\n }}\n }}\n\n"
);
items.push_str(&decode_fn(
path,
"decode_key_holder_be",
"key_from_holder_be",
"Big",
key.key_holder_alignment.max_alignment(key.encoding),
"",
));
Ok(items)
}
fn identity_items(ast: &Ast, id: Id<TypeNode>) -> String {
let mut out = String::new();
match zerodds_types::from_ast::type_identifier(ast, id, zerodds_types::from_ast::Form::Complete)
{
Ok(zerodds_types::TypeIdentifier::EquivalenceHashComplete(hash)) => {
let _ = write!(
out,
" /// The complete equivalence hash, XTypes 1.3 \u{a7}7.3.4.6, \
computed\n /// from the same model the codec was built \
from.\n const TYPE_IDENTIFIER: zerodds_types::TypeIdentifier \
=\n zerodds_types::TypeIdentifier::EquivalenceHashComplete(\n \
zerodds_types::EquivalenceHash({}),\n );\n",
octet_array(&hash.0)
);
}
Ok(zerodds_types::TypeIdentifier::StronglyConnectedComponent(scc))
if scc.equivalence_kind == zerodds_types::type_identifier::kinds::EK_COMPLETE =>
{
let _ = write!(
out,
" /// The identifier of a type in a strongly connected component, XTypes \
1.3\n /// \u{a7}7.3.4.9.3: the component's complete hash, its length and the \
type's\n /// position in it.\n const TYPE_IDENTIFIER: \
zerodds_types::TypeIdentifier =\n \
zerodds_types::TypeIdentifier::StronglyConnectedComponent(\n \
zerodds_types::StronglyConnectedComponentId {{\n \
equivalence_kind: 0x{:02X},\n hash: \
zerodds_types::EquivalenceHash({}),\n scc_length: {},\n \
scc_index: {},\n }},\n );\n",
scc.equivalence_kind,
octet_array(&scc.hash.0),
scc.scc_length,
scc.scc_index
);
out.push_str(&component_items(ast, id));
}
Ok(other) => {
let _ = writeln!(
out,
" // No `TYPE_IDENTIFIER`: {other:?} is not an equivalence hash, \
and matching falls back to the type name."
);
}
Err(error) => {
let _ = writeln!(
out,
" // No `TYPE_IDENTIFIER`: the identifier could not be computed \
\u{2014} {error:?}."
);
}
}
let object = zerodds_types::from_ast::complete_type_object(ast, id)
.ok()
.map(zerodds_types::type_object::TypeObject::Complete)
.and_then(|object| object.to_bytes_le().ok())
.filter(|bytes| zerodds_types::type_object::TypeObject::from_bytes_le(bytes).is_ok());
if let Some(bytes) = object {
let _ = write!(
out,
"\n /// The `CompleteTypeObject` this endpoint registers with the \
TypeLookup\n /// server, so a peer that asks for \
`TYPE_IDENTIFIER` gets an answer.\n /// XCDR2-LE, \
XTypes 1.3 \u{a7}7.3.4.\n fn type_object() -> \
Option<zerodds_types::type_object::TypeObject> {{\n \
zerodds_types::type_object::TypeObject::from_bytes_le(&{})\n \
.ok()\n }}\n",
octet_array(&bytes)
);
let dependencies: Vec<Vec<u8>> = zerodds_types::from_ast::dependency_type_objects(ast, id)
.unwrap_or_default()
.into_iter()
.map(zerodds_types::type_object::TypeObject::Complete)
.filter_map(|object| object.to_bytes_le().ok())
.filter(|bytes| zerodds_types::type_object::TypeObject::from_bytes_le(bytes).is_ok())
.collect();
if !dependencies.is_empty() {
let arrays: Vec<String> = dependencies
.iter()
.map(|bytes| format!(" &{},\n", octet_array(bytes)))
.collect();
let _ = write!(
out,
"\n /// The `CompleteTypeObject`s the one above names by hash, \
transitively:\n /// base type, member aggregates, enumerations, \
aliases. Registered beside\n /// it so the closure resolves locally \
and `getTypeDependencies` is\n /// answerable. XTypes 1.3 \
\u{a7}7.3.4.6, \u{a7}7.6.3.3.4.\n fn type_object_dependencies() -> \
Vec<zerodds_types::type_object::TypeObject> {{\n \
let blobs: &[&[u8]] = &[\n{} ];\n blobs\n \
.iter()\n .filter_map(|bytes| \
zerodds_types::type_object::TypeObject::from_bytes_le(bytes).ok())\n \
.collect()\n }}\n",
arrays.concat()
);
}
}
out
}
fn component_items(ast: &Ast, id: Id<TypeNode>) -> String {
let Ok(Some(component)) = zerodds_types::from_ast::type_component(ast, id) else {
return String::from(
"\n // No `type_component()`: the component could not be built, and the \
runtime\n // cannot announce a TypeInformation for a type it cannot serve.\n",
);
};
let encode = |object: zerodds_types::type_object::TypeObject| object.to_bytes_le().ok();
let complete: Option<Vec<Vec<u8>>> = component
.complete
.iter()
.cloned()
.map(|object| encode(zerodds_types::type_object::TypeObject::Complete(object)))
.collect();
let minimal: Option<Vec<Vec<u8>>> = component
.minimal
.iter()
.cloned()
.map(|object| encode(zerodds_types::type_object::TypeObject::Minimal(object)))
.collect();
let (Some(complete), Some(minimal)) = (complete, minimal) else {
return String::from(
"\n // No `type_component()`: a member of the component did not encode.\n",
);
};
let arrays = |blobs: &[Vec<u8>]| -> String {
blobs
.iter()
.map(|bytes| format!(" &{},\n", octet_array(bytes)))
.collect::<String>()
};
format!(
"\n /// The component this type belongs to, in both forms, XTypes 1.3 \
\u{a7}7.3.4.9.2.\n /// Registered whole: the hash identifies the component, not a \
member.\n fn type_component() -> \
Option<zerodds_types::resolve::TypeComponent> {{\n \
use zerodds_types::type_object::TypeObject;\n \
let complete_blobs: &[&[u8]] = &[\n{complete_blobs} ];\n \
let minimal_blobs: &[&[u8]] = &[\n{minimal_blobs} ];\n \
let complete: Vec<_> = complete_blobs\n .iter()\n \
.filter_map(|bytes| match TypeObject::from_bytes_le(bytes).ok()? {{\n \
TypeObject::Complete(object) => Some(object),\n _ => None,\n \
}})\n .collect();\n \
let minimal: Vec<_> = minimal_blobs\n .iter()\n \
.filter_map(|bytes| match TypeObject::from_bytes_le(bytes).ok()? {{\n \
TypeObject::Minimal(object) => Some(object),\n _ => None,\n \
}})\n .collect();\n \
if complete.len() != {length} || minimal.len() != {length} {{\n \
return None;\n }}\n \
Some(zerodds_types::resolve::TypeComponent {{\n complete_hash: \
zerodds_types::EquivalenceHash({complete_hash}),\n minimal_hash: \
zerodds_types::EquivalenceHash({minimal_hash}),\n complete,\n \
minimal,\n }})\n }}\n",
complete_blobs = arrays(&complete),
minimal_blobs = arrays(&minimal),
length = component.complete.len(),
complete_hash = octet_array(&component.complete_hash.0),
minimal_hash = octet_array(&component.minimal_hash.0),
)
}
fn codec_items(
path: &str,
extensibility: Extensibility,
has_xcdr1: bool,
v2_key: &LayoutKey,
v1_key: &LayoutKey,
) -> String {
let second = zerodds_lang_rules::encode::SECONDARY_SUFFIX;
let mut out = String::new();
let v2 = Encoding::PlainCdr2.max_alignment();
let (v2_write, v2_read) = (wire_settings(v2_key, true), wire_settings(v2_key, false));
for (name, order) in [("encode", "Little"), ("encode_be", "Big")] {
out.push_str(&encode_fn(path, name, "encode", order, v2, &v2_write));
}
for (name, order) in [("decode", "Little"), ("decode_be", "Big")] {
out.push_str(&decode_fn(path, name, "decode", order, v2, &v2_read));
}
let mut v1_continuing = None;
if has_xcdr1 {
let v1 = xcdr1_encoding(extensibility).max_alignment();
let (v1_write, v1_read) = (wire_settings(v1_key, true), wire_settings(v1_key, false));
v1_continuing = Some((v1, v1_write.clone(), v1_read.clone()));
for (name, order) in [("encode_xcdr1", "Little"), ("encode_xcdr1_be", "Big")] {
out.push_str(&encode_fn(
path,
name,
&format!("encode{second}"),
order,
v1,
&v1_write,
));
}
for (name, order) in [("decode_xcdr1", "Little"), ("decode_xcdr1_be", "Big")] {
out.push_str(&decode_fn(
path,
name,
&format!("decode{second}"),
order,
v1,
&v1_read,
));
}
}
out.push_str(&continuing_fns(
path,
second,
(v2, &v2_write, &v2_read),
v1_continuing
.as_ref()
.map(|(align, write, read)| (*align, write.as_str(), read.as_str())),
));
out
}
fn continuing_fns(
path: &str,
second: &str,
v2: (u8, &str, &str),
v1: Option<(u8, &str, &str)>,
) -> String {
let (v2_align, v2_write, v2_read) = v2;
let (encode_v1, decode_v1) = match v1 {
Some((v1_align, v1_write, v1_read)) => (
format!(
"let mut w = zerodds_wire::Writer::continuing(\n \
zerodds_wire::Endianness::Little,\n {v1_align},\n \
core::mem::take(prefix),\n ){v1_write};\n \
let written = {path}::encode{second}(self, &mut w).and_then(|_| w.check_deferred());\n \
*prefix = w.finish();\n written"
),
format!(
"let mut r = zerodds_wire::Reader::with_max_align(\n bytes,\n \
zerodds_wire::Endianness::Little,\n {v1_align},\n ){v1_read};\n \
r.skip_to(offset)?;\n \
Ok({path}::decode{second}(&mut r)?)"
),
),
None => (
"return Err(zerodds_dcps::EncodeError::Invalid {\n \
what: \"XCDR1 encoding not implemented for this type\",\n })"
.to_string(),
"Err(zerodds_dcps::DecodeError::Invalid {\n \
what: \"XCDR1 decoding not implemented for this type\",\n })"
.to_string(),
),
};
format!(
"\n fn encode_continuing(\n &self,\n prefix: &mut Vec<u8>,\n xcdr1: bool,\n \
) -> Result<(), zerodds_dcps::EncodeError> {{\n \
let start = prefix.len();\n \
let written = if xcdr1 {{\n {encode_v1}\n }} else {{\n \
let mut w = zerodds_wire::Writer::continuing(\n \
zerodds_wire::Endianness::Little,\n {v2_align},\n \
core::mem::take(prefix),\n ){v2_write};\n \
let written = {path}::encode(self, &mut w).and_then(|_| w.check_deferred());\n \
*prefix = w.finish();\n written\n }};\n \
if written.is_err() {{\n prefix.truncate(start);\n }}\n \
written?;\n Ok(())\n }}\n\n \
fn decode_continuing(\n bytes: &[u8],\n offset: usize,\n xcdr1: bool,\n \
) -> Result<Self, zerodds_dcps::DecodeError> {{\n \
if xcdr1 {{\n {decode_v1}\n }} else {{\n \
let mut r = zerodds_wire::Reader::with_max_align(\n bytes,\n \
zerodds_wire::Endianness::Little,\n {v2_align},\n ){v2_read};\n \
r.skip_to(offset)?;\n \
Ok({path}::decode(&mut r)?)\n }}\n }}\n"
)
}
fn wire_settings(key: &LayoutKey, writer: bool) -> String {
use zerodds_ast_layout::key::{PidFlagPolicy, WcharFraming, WcharWidth, WstringUnitOrder};
let default = LayoutKey::data(key.encoding, key.endianness);
let mut out = String::new();
let framing = key.pl_cdr1_framing();
if framing != default.pl_cdr1_framing() {
let pid_flags = match framing.pid_flags {
PidFlagPolicy::SentinelMustUnderstand => "SentinelMustUnderstand",
PidFlagPolicy::Bare => "Bare",
PidFlagPolicy::ImplExtensionOnMembers => "ImplExtensionOnMembers",
};
let _ = write!(
out,
"\n .with_pl_cdr1_framing(zerodds_wire::PlCdr1Framing {{\n \
pid_flags: zerodds_wire::PidFlagPolicy::{pid_flags},\n \
highest_short_id: {},\n \
pad_declared_length: {},\n \
pad_declared_optional_length: {},\n \
member_must_understand: {},\n \
require_extended: {},\n \
pad_after_value: {},\n }})",
framing.highest_short_id,
framing.pad_declared_length,
framing.pad_declared_optional_length,
framing.member_must_understand,
framing.require_extended,
framing.pad_after_value,
);
}
if !writer && key.pl_cdr1_reading != default.pl_cdr1_reading {
out.push_str("\n .with_pl_cdr1_reading(zerodds_wire::PlCdr1Reading::Tolerant)");
}
if key.wchar_width != default.wchar_width {
let width = match key.wchar_width {
WcharWidth::Two => "Two",
WcharWidth::Four => "Four",
};
let _ = write!(
out,
"\n .with_wchar_width(zerodds_wire::WcharWidth::{width})"
);
}
if key.wchar_framing != default.wchar_framing {
let framing = match key.wchar_framing {
WcharFraming::Fixed => "Fixed",
WcharFraming::GiopLengthPrefixed => "GiopLengthPrefixed",
WcharFraming::GiopLengthPrefixedMemberOnly => "GiopLengthPrefixedMemberOnly",
};
let _ = write!(
out,
"\n .with_wchar_framing(zerodds_wire::WcharFraming::{framing})"
);
}
if key.wstring_unit_order != default.wstring_unit_order {
let order = match key.wstring_unit_order {
WstringUnitOrder::Stream => "Stream",
WstringUnitOrder::BigEndianAlways => "BigEndianAlways",
WstringUnitOrder::ByteOrderMark => "ByteOrderMark",
WstringUnitOrder::HostUnswapped => "HostUnswapped",
};
let _ = write!(
out,
"\n .with_wstring_unit_order(zerodds_wire::WstringUnitOrder::{order})"
);
}
if writer
&& key.length_codes.short_code_where_it_fits()
!= default.length_codes.short_code_where_it_fits()
{
let _ = write!(
out,
"\n .with_short_length_codes({})",
key.length_codes.short_code_where_it_fits()
);
}
out
}
fn encode_fn(
path: &str,
name: &str,
inherent: &str,
order: &str,
max_align: u8,
settings: &str,
) -> String {
format!(
"\n fn {name}(&self, out: &mut Vec<u8>) -> Result<(), zerodds_dcps::EncodeError> {{\n \
let start = out.len();\n \
let mut w = zerodds_wire::Writer::appending(\n \
zerodds_wire::Endianness::{order},\n {max_align},\n \
core::mem::take(out),\n ){settings};\n \
let written = {path}::{inherent}(self, &mut w).and_then(|_| w.check_deferred());\n \
*out = w.finish();\n \
if written.is_err() {{\n out.truncate(start);\n }}\n \
written?;\n Ok(())\n }}\n"
)
}
fn decode_fn(
path: &str,
name: &str,
inherent: &str,
order: &str,
max_align: u8,
settings: &str,
) -> String {
format!(
"\n fn {name}(bytes: &[u8]) -> Result<Self, zerodds_dcps::DecodeError> {{\n \
let mut r = zerodds_wire::Reader::with_max_align(\n bytes,\n \
zerodds_wire::Endianness::{order},\n {max_align},\n ){settings};\n \
Ok({path}::{inherent}(&mut r)?)\n }}\n"
)
}
fn field_value_item(ast: &Ast, node: &TypeNode) -> String {
let TypeKind::Structure { members, .. } = &node.kind else {
return String::new();
};
let mut arms = String::new();
for member in members {
let Ok(member_node) = ast.member(*member) else {
continue;
};
let optional = member_node
.facets
.xtypes
.as_ref()
.is_some_and(|x| x.is_optional);
let Some(target) = member_node
.type_
.resolved()
.map(|id| resolve_alias(ast, id))
else {
continue;
};
let Ok(target_node) = ast.type_(target) else {
continue;
};
let field = zerodds_lang_cst::rust::escape_keyword(&member_node.name);
if optional {
let external = member_node
.facets
.xtypes
.as_ref()
.is_some_and(|x| x.is_external);
let deref = if external { "**v" } else { "*v" };
let name = &member_node.name;
let present = match &target_node.kind {
TypeKind::Primitive(kind) => {
scalar_value(*kind, "v").map(|value| format!("{{ let v = {deref}; {value} }}"))
}
TypeKind::String { .. } => Some(String::from(
"zerodds_dcps::FilterValue::String(String::clone(v))",
)),
_ => None,
};
if let Some(present) = present {
let _ = writeln!(
arms,
" (\"{name}\", None) => Some(match &self.{field} {{\n \
Some(v) => {present},\n \
None => zerodds_dcps::FilterValue::Null,\n }}),"
);
}
continue;
}
let value = if member_node
.facets
.xtypes
.as_ref()
.is_some_and(|x| x.is_external)
{
format!("(*self.{field})")
} else {
format!("self.{field}")
};
let name = &member_node.name;
match &target_node.kind {
TypeKind::Primitive(kind) => {
if let Some(value) = scalar_value(*kind, &value) {
let _ = writeln!(arms, " (\"{name}\", None) => Some({value}),");
}
}
TypeKind::String { .. } => {
let _ = writeln!(
arms,
" (\"{name}\", None) => \
Some(zerodds_dcps::FilterValue::String({value}.clone())),"
);
}
TypeKind::Structure { .. } | TypeKind::Union { .. } => {
let _ = writeln!(
arms,
" (\"{name}\", Some(rest)) => \
zerodds_dcps::DdsType::field_value(&{value}, rest),"
);
}
_ => {}
}
}
if arms.is_empty() {
return String::new();
}
format!(
"\n fn field_value(&self, path: &str) -> Option<zerodds_dcps::FilterValue> {{\n \
let (head, rest) = match path.split_once('.') {{\n \
Some((head, rest)) => (head, Some(rest)),\n \
None => (path, None),\n }};\n \
match (head, rest) {{\n{arms} _ => None,\n }}\n }}\n"
)
}
fn scalar_value(kind: PrimitiveKind, field: &str) -> Option<String> {
let bare = field
.strip_prefix('(')
.and_then(|f| f.strip_suffix(')'))
.filter(|f| f.starts_with("*self."))
.unwrap_or(field);
Some(match kind {
PrimitiveKind::Boolean => {
format!("zerodds_dcps::FilterValue::Bool({bare})")
}
PrimitiveKind::Octet
| PrimitiveKind::UInt8
| PrimitiveKind::Int8
| PrimitiveKind::Int16
| PrimitiveKind::UInt16
| PrimitiveKind::Int32
| PrimitiveKind::UInt32
| PrimitiveKind::Int64 => {
format!("zerodds_dcps::FilterValue::Int(i64::from({bare}))")
}
PrimitiveKind::Float32 | PrimitiveKind::Float64 => {
format!("zerodds_dcps::FilterValue::Float(f64::from({bare}))")
}
PrimitiveKind::Char | PrimitiveKind::WChar => {
format!("zerodds_dcps::FilterValue::String({field}.to_string())")
}
PrimitiveKind::UInt64 | PrimitiveKind::Float128 => return None,
})
}
fn resolve_alias(ast: &Ast, mut id: Id<TypeNode>) -> Id<TypeNode> {
for _ in 0..64 {
match ast.type_(id).map(|node| &node.kind) {
Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
Some(next) => id = next,
None => return id,
},
_ => return id,
}
}
id
}
pub(crate) const fn xcdr1_encoding(extensibility: Extensibility) -> Encoding {
match extensibility {
Extensibility::Final | Extensibility::Appendable => Encoding::PlainCdr1,
Extensibility::Mutable => Encoding::PlCdr1,
}
}
fn qualified_idl_name(ast: &Ast, id: Id<TypeNode>, node: &TypeNode) -> String {
ast.scoped_name_of(zerodds_ast::AnyId::Type(id, node.node_id))
.map(|scoped| {
let text = scoped.to_string();
text.strip_prefix("::").unwrap_or(&text).to_string()
})
.unwrap_or_else(|| node.name.clone())
}
fn is_nested(node: &TypeNode) -> bool {
node.facets
.xtypes
.as_ref()
.is_some_and(|xtypes| xtypes.is_nested)
}
const fn extensibility_variant(extensibility: Extensibility) -> &'static str {
match extensibility {
Extensibility::Final => "Final",
Extensibility::Appendable => "Appendable",
Extensibility::Mutable => "Mutable",
}
}
fn octet_array(bytes: &[u8]) -> String {
let octets: Vec<String> = bytes.iter().map(|b| format!("0x{b:02x}")).collect();
format!("[{}]", octets.join(", "))
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
fn default_key_for(encoding: Encoding) -> Result<LayoutKey, String> {
Ok(LayoutKey::data(encoding, Endianness::Little))
}
fn model(source: &str) -> Ast {
let dir = std::env::temp_dir().join(format!(
"zerodds-build-dds-type-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
std::fs::create_dir_all(&dir).expect("scratch");
let path = dir.join("Probe.idl");
std::fs::write(&path, source).expect("write");
let composed = zerodds_idl_compose::compose_with(
&path.to_string_lossy(),
&zerodds_idl_compose::PathLoader::new(Vec::new()),
zerodds_idl_compose::ComposeOptions::default(),
)
.expect("compose");
assert!(!composed.failed(), "{:?}", composed.diagnostics);
composed.ast
}
#[test]
fn a_carried_union_discriminator_cannot_acquire_keyless_dds_defaults() {
let mut ast = Ast::new();
let discriminator = ast
.add_type(
ast.root_module(),
"Long",
TypeKind::Primitive(PrimitiveKind::Int32),
)
.expect("discriminator");
let union = ast
.add_type(
ast.root_module(),
"Command",
TypeKind::Union {
discriminator: zerodds_ast_core::Ref::Resolved(discriminator),
cases: Vec::new(),
default_case: None,
},
)
.expect("union");
let zero = ast.add_int_expr(0).expect("label");
ast.add_case(
union,
vec![zerodds_ast::CaseLabel::Value(zero)],
zerodds_ast::MemberSpec::new("value", zerodds_ast_core::Ref::Resolved(discriminator)),
)
.expect("case");
let any = ast.any_of_type(union).expect("union id");
ast.facets_mut(any)
.expect("facets")
.xtypes_mut()
.discriminator_is_key = true;
assert!(
zerodds_ast_layout::keyhash::key_members_with(
&ast,
union,
Default::default(),
Default::default()
)
.expect("members")
.is_empty(),
"the discriminator is deliberately not a MemberNode"
);
let error = dds_type_impls(&ast, &[], &default_key_for)
.expect_err("a carried discriminator must not become keyless");
assert!(
error.contains("DdsType key holder for `Command`"),
"{error}"
);
assert!(error.contains("keyed union discriminator"), "{error}");
assert!(
error.contains("key_holder_be/key_from_holder_be"),
"{error}"
);
ast.facets_mut(any)
.expect("facets")
.xtypes_mut()
.discriminator_is_key = false;
let keyless =
dds_type_impls(&ast, &[], &default_key_for).expect("same union without a key");
assert!(keyless.contains("impl zerodds_dcps::DdsType for Command"));
assert!(!keyless.contains("const HAS_KEY: bool = true"));
}
#[test]
fn an_unplannable_key_refuses_the_impl_instead_of_becoming_keyless() {
let ast = model("@final struct Broken { @key any id; };");
let error = dds_type_impls(&ast, &[], &default_key_for)
.expect_err("a key without a DDS wire form must refuse");
assert!(error.contains("DdsType key holder for `Broken`"), "{error}");
assert!(error.contains("any"), "{error}");
}
#[test]
fn a_genuinely_keyless_type_keeps_the_trait_defaults() {
let ast = model("@final struct Plain { long id; };");
let code = dds_type_impls(&ast, &[], &default_key_for).expect("keyless is not an error");
assert!(code.contains("impl zerodds_dcps::DdsType for Plain"));
assert!(!code.contains("const HAS_KEY: bool = true"));
}
#[test]
fn without_an_xcdr1_plan_the_four_methods_are_left_refusing() {
let ast = model("@final struct Pose { int32 id; double x; };");
let with = dds_type_impls(
&ast,
&ast.types().map(|(id, _)| id).collect::<Vec<_>>(),
&default_key_for,
)
.unwrap();
assert!(with.contains("fn encode_xcdr1("), "{with}");
assert!(with.contains("fn decode_xcdr1_be("), "{with}");
let without = dds_type_impls(&ast, &[], &default_key_for).unwrap();
assert!(
!without.contains("fn encode_xcdr1("),
"no XCDR1 codec means no override: {without}"
);
assert!(
!without.contains("fn decode_xcdr1_be("),
"no XCDR1 codec means no override: {without}"
);
assert!(
without.contains("// `Pose`: no XCDR1 codec could be planned"),
"the reason is named where the overrides would have been: {without}"
);
assert!(without.contains("fn encode_be("), "{without}");
}
}