use std::collections::BTreeMap;
use serde_json::Value;
use zerodds_ast::Ast;
use zerodds_ast_core::ScopedName;
use zerodds_ast_layout::{
Dialect, Encoding, Endianness, EnumHolderWidth, EnumeratorValue, FixedDigitCount,
LengthCodePolicy, MapEntryOrder, MutableUnionFraming, PidFlagPolicy, ShortPidRange,
UnionLabelMatch, WstringLengthPolicy, WstringUnitOrder,
};
use crate::canonical::{canonicalise, text};
use crate::decode::decode;
use crate::encode::{encode, hex, model_from_idl_in_dialect};
use crate::value::{Emit, MapLengthPolicy, ProbeRequest};
pub const ENCODINGS: [(&str, Encoding); 5] = [
("plain-cdr1", Encoding::PlainCdr1),
("pl-cdr1", Encoding::PlCdr1),
("plain-cdr2", Encoding::PlainCdr2),
("delimited-cdr", Encoding::DelimitedCdr),
("pl-cdr2", Encoding::PlCdr2),
];
pub const ORDERS: [(&str, Endianness); 2] = [("le", Endianness::Little), ("be", Endianness::Big)];
pub struct Configuration {
pub name: &'static str,
pub dialect: Dialect,
pub length_codes: Option<LengthCodePolicy>,
pub wstring_length: Option<WstringLengthPolicy>,
pub wstring_unit_order: Option<WstringUnitOrder>,
pub pid_flags: Option<PidFlagPolicy>,
pub enum_holder: Option<EnumHolderWidth>,
pub union_label: Option<UnionLabelMatch>,
pub map_entries: Option<MapEntryOrder>,
pub enumerator_value: Option<EnumeratorValue>,
pub union_framing: Option<MutableUnionFraming>,
pub short_pid_range: Option<ShortPidRange>,
pub fixed_digits: Option<FixedDigitCount>,
pub map_length: Option<MapLengthPolicy>,
}
impl Configuration {
#[must_use]
pub fn request(
&self,
type_name: &str,
encoding: Encoding,
endianness: Endianness,
) -> ProbeRequest {
let mut request = ProbeRequest::in_dialect(type_name, encoding, endianness, self.dialect);
if let Some(policy) = self.length_codes {
request.length_codes = policy;
}
if let Some(policy) = self.wstring_length {
request.wstring_length = policy;
}
if let Some(order) = self.wstring_unit_order {
request.wstring_unit_order = order;
}
if let Some(policy) = self.pid_flags {
request.pid_flags = policy;
}
if let Some(policy) = self.enum_holder {
request.enum_holder = policy;
}
if let Some(policy) = self.union_label {
request.union_label = policy;
}
if let Some(policy) = self.map_entries {
request.map_entries = policy;
}
if let Some(policy) = self.enumerator_value {
request.enumerator_value = policy;
}
if let Some(policy) = self.union_framing {
request.union_framing = policy;
}
if let Some(policy) = self.short_pid_range {
request.short_pid_range = policy;
}
if let Some(policy) = self.fixed_digits {
request.fixed_digits = policy;
}
if let Some(policy) = self.map_length {
request.map_length = policy;
}
request
}
#[must_use]
pub fn flags(&self) -> Vec<String> {
let mut out = vec![String::from("--dialect"), self.dialect.as_str().to_string()];
let mut push = |flag: &str, value: String| {
out.push(flag.to_string());
out.push(value);
};
if let Some(policy) = self.length_codes {
push("--length-codes", policy.as_str().to_string());
}
if let Some(policy) = self.wstring_length {
push("--wstring-length", policy.as_str().to_string());
}
if let Some(order) = self.wstring_unit_order {
push("--wstring-units", order.as_str().to_string());
}
if let Some(policy) = self.pid_flags {
push("--pid-flags", policy.as_str().to_string());
}
if let Some(policy) = self.enum_holder {
push("--enum-holder", policy.as_str().to_string());
}
if let Some(policy) = self.union_label {
push("--union-labels", policy.as_str().to_string());
}
if let Some(policy) = self.map_entries {
push("--map-entries", policy.as_str().to_string());
}
if let Some(policy) = self.enumerator_value {
push("--enumerator-value", policy.as_str().to_string());
}
if let Some(policy) = self.union_framing {
push("--union-framing", policy.as_str().to_string());
}
if let Some(policy) = self.short_pid_range {
push("--short-pid-range", policy.as_str().to_string());
}
if let Some(policy) = self.fixed_digits {
push("--fixed-digits", policy.as_str().to_string());
}
if let Some(policy) = self.map_length {
push("--map-length", policy.as_str().to_string());
}
out
}
}
pub const CONFIGURATIONS: [Configuration; 12] = [
configuration("specification", Dialect::Specification),
configuration("cyclone", Dialect::Cyclone),
configuration("rti", Dialect::Rti),
configuration("fastdds", Dialect::FastDds),
configuration("omniorb", Dialect::OmniOrb),
configuration("tao", Dialect::Tao),
configuration("jacorb", Dialect::JacOrb),
configuration("opendds", Dialect::OpenDds),
configuration("ros-fastrtps", Dialect::RosFastRtps),
configuration("ros-cyclone", Dialect::RosCyclone),
Configuration {
length_codes: Some(LengthCodePolicy::UniversalNextInt),
..configuration("universal-nextint", Dialect::Specification)
},
Configuration {
map_length: Some(MapLengthPolicy::Rule15Omits),
..configuration("rule-15-omits", Dialect::Specification)
},
];
const fn configuration(name: &'static str, dialect: Dialect) -> Configuration {
Configuration {
name,
dialect,
length_codes: None,
wstring_length: None,
wstring_unit_order: None,
pid_flags: None,
enum_holder: None,
union_label: None,
map_entries: None,
enumerator_value: None,
union_framing: None,
short_pid_range: None,
fixed_digits: None,
map_length: None,
}
}
pub struct Case {
pub name: String,
pub type_name: String,
pub idl: String,
pub values: Value,
}
#[must_use]
pub fn row(case: &Case) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
for settings in &CONFIGURATIONS {
let ast = match model_from_idl_in_dialect(
&case.idl,
&format!("{}/type.idl", case.name),
settings.dialect,
) {
Ok(ast) => ast,
Err(error) => {
refuse_configuration(
&mut out,
settings.name,
&format!("the IDL does not lower: {error}"),
);
continue;
}
};
let expected = expected_text(&ast, case);
for (encoding_name, encoding) in ENCODINGS {
for (order_name, endianness) in ORDERS {
let request = settings.request(&case.type_name, encoding, endianness);
let stem = format!("{}/{encoding_name}/{order_name}", settings.name);
let payload = encode(&ast, &request, &case.values);
out.insert(
format!("{stem}+enc"),
match encode(
&ast,
&request.clone().emitting(Emit::PayloadWithEncapsulation),
&case.values,
) {
Ok(bytes) => hex(&bytes),
Err(error) => format!("!{error}"),
},
);
out.insert(
format!("{stem}/read"),
match &payload {
Ok(bytes) => read_back(
&ast,
&request,
bytes,
expected.as_deref().map_err(String::as_str),
),
Err(error) => format!("!unwritten: {error}"),
},
);
out.insert(
stem,
match payload {
Ok(bytes) => hex(&bytes),
Err(error) => format!("!{error}"),
},
);
}
let request = settings
.request(&case.type_name, encoding, Endianness::Big)
.emitting(Emit::KeyHash);
out.insert(
format!("{}/{encoding_name}/keyhash", settings.name),
match encode(&ast, &request, &case.values) {
Ok(bytes) => hex(&bytes),
Err(error) => format!("!{error}"),
},
);
}
}
out
}
pub fn refuse_configuration(out: &mut BTreeMap<String, String>, name: &str, message: &str) {
let unwritten = format!("!unwritten: {message}");
for (encoding_name, _) in ENCODINGS {
for (order_name, _) in ORDERS {
let stem = format!("{name}/{encoding_name}/{order_name}");
out.insert(format!("{stem}+enc"), format!("!{message}"));
out.insert(format!("{stem}/read"), unwritten.clone());
out.insert(stem, format!("!{message}"));
}
out.insert(
format!("{name}/{encoding_name}/keyhash"),
format!("!{message}"),
);
}
}
pub const SLOTS_PER_CASE: usize = CONFIGURATIONS.len() * ENCODINGS.len() * (ORDERS.len() * 3 + 1);
fn expected_text(ast: &Ast, case: &Case) -> Result<String, String> {
let name = ScopedName::absolute(
case.type_name
.split("::")
.filter(|segment| !segment.is_empty())
.map(std::string::ToString::to_string)
.collect(),
);
let type_ = ast
.find_by_scoped_name(&name)
.map_err(|lookup| format!("the model has no {}: {lookup:?}", case.type_name))?
.as_type()
.ok_or_else(|| format!("{} names something that is not a type", case.type_name))?;
canonicalise(ast, type_, &case.values, "sample")
.map(|value| text(&value))
.map_err(|error| format!("the sample does not canonicalise: {error}"))
}
fn read_back(
ast: &Ast,
request: &ProbeRequest,
bytes: &[u8],
expected: Result<&str, &str>,
) -> String {
let expected = match expected {
Ok(text) => text,
Err(why) => return format!("!{why}"),
};
match decode(ast, request, bytes) {
Ok(value) if text(&value) == expected => "same".to_string(),
Ok(value) => format!("different: {} for {expected}", text(&value)),
Err(error) => format!("!{error}"),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
#[test]
fn the_twelve_are_named_once_each_and_in_the_gate_s_order() {
let names: Vec<&str> = CONFIGURATIONS.iter().map(|entry| entry.name).collect();
assert_eq!(
names,
[
"specification",
"cyclone",
"rti",
"fastdds",
"omniorb",
"tao",
"jacorb",
"opendds",
"ros-fastrtps",
"ros-cyclone",
"universal-nextint",
"rule-15-omits",
],
"the order is the three-column gate's; a column keyed by position \
would otherwise be measured under the wrong dialect"
);
}
#[test]
fn a_row_holds_four_hundred_and_twenty_slots() {
assert_eq!(SLOTS_PER_CASE, 420);
}
#[test]
fn every_override_reaches_the_flags() {
let base = configuration("probe", Dialect::Specification);
let with = Configuration {
length_codes: Some(LengthCodePolicy::UniversalNextInt),
map_length: Some(MapLengthPolicy::Rule15Omits),
..base
};
let flags = with.flags();
assert!(flags.iter().any(|flag| flag == "--length-codes"));
assert!(flags.iter().any(|flag| flag == "--map-length"));
assert!(
flags.iter().any(|flag| flag == "--dialect"),
"the dialect is a preset and goes first, or the overrides are discarded"
);
}
}