use super::level::normalise_field_name;
use crate::formats;
use crate::walker::{DecodedElement, ElementDecoder, HandleIndex, InstanceField};
use crate::{Error, Result};
macro_rules! simple_decoder {
($Struct:ident, $name:literal) => {
pub struct $Struct;
impl ElementDecoder for $Struct {
fn class_name(&self) -> &'static str {
$name
}
fn decode(
&self,
bytes: &[u8],
schema: &formats::ClassEntry,
_index: &HandleIndex,
) -> Result<DecodedElement> {
if schema.name != $name {
return Err(Error::BasicFileInfo(format!(
"{} received wrong schema: {}",
stringify!($Struct),
schema.name
)));
}
Ok(crate::walker::decode_instance(bytes, 0, schema))
}
}
};
}
simple_decoder!(ParameterElementDecoder, "ParameterElement");
simple_decoder!(SharedParameterDecoder, "SharedParameter");
simple_decoder!(APropertyDecoder, "AProperty");
simple_decoder!(APropertyBooleanDecoder, "APropertyBoolean");
simple_decoder!(APropertyIntegerDecoder, "APropertyInteger");
simple_decoder!(APropertyEnumDecoder, "APropertyEnum");
simple_decoder!(APropertyDouble1Decoder, "APropertyDouble1");
simple_decoder!(APropertyDouble3Decoder, "APropertyDouble3");
simple_decoder!(APropertyFloatDecoder, "APropertyFloat");
simple_decoder!(APropertyFloat3Decoder, "APropertyFloat3");
simple_decoder!(APropertyStringDecoder, "APropertyString");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StorageType {
#[default]
None,
Integer,
Double,
String,
ElementId,
Other,
}
impl StorageType {
pub fn from_code(code: u32) -> Self {
match code {
0 => Self::None,
1 => Self::Integer,
2 => Self::Double,
3 => Self::String,
4 => Self::ElementId,
_ => Self::Other,
}
}
pub fn is_numeric(self) -> bool {
matches!(self, Self::Integer | Self::Double)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ParameterValueSource {
#[default]
UserSet,
Calculated,
Reporting,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ParameterElement {
pub name: Option<String>,
pub parameter_group: Option<u32>,
pub storage_type: Option<StorageType>,
pub unit_type: Option<u32>,
pub is_shared: Option<bool>,
pub visible: Option<bool>,
pub value_source: Option<ParameterValueSource>,
}
impl ParameterElement {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self::default();
let mut is_calculated = None;
let mut is_reporting = None;
for (field_name, value) in &decoded.fields {
match (normalise_field_name(field_name).as_str(), value) {
("name", InstanceField::String(s)) => out.name = Some(s.clone()),
("parametergroup" | "group", InstanceField::Integer { value, .. }) => {
out.parameter_group = Some(*value as u32);
}
("storagetype" | "storage", InstanceField::Integer { value, .. }) => {
out.storage_type = Some(StorageType::from_code(*value as u32));
}
("unittype" | "unit", InstanceField::Integer { value, .. }) => {
out.unit_type = Some(*value as u32);
}
("isshared" | "shared", InstanceField::Bool(b)) => {
out.is_shared = Some(*b);
}
("visible", InstanceField::Bool(b)) => out.visible = Some(*b),
(
"iscalculated" | "calculated" | "isformula" | "formula",
InstanceField::Bool(b),
) => {
is_calculated = Some(*b);
}
("isreporting" | "reporting", InstanceField::Bool(b)) => {
is_reporting = Some(*b);
}
_ => {}
}
}
out.value_source = match (is_calculated, is_reporting) {
(_, Some(true)) => Some(ParameterValueSource::Reporting),
(Some(true), _) => Some(ParameterValueSource::Calculated),
(Some(false), Some(false)) | (Some(false), None) | (None, Some(false)) => {
Some(ParameterValueSource::UserSet)
}
(None, None) => None,
};
out
}
pub fn is_user_writable(&self) -> bool {
!matches!(
self.value_source,
Some(ParameterValueSource::Calculated | ParameterValueSource::Reporting)
)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct SharedParameter {
pub base: ParameterElement,
pub guid: Option<[u8; 16]>,
pub description: Option<String>,
}
impl SharedParameter {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self {
base: ParameterElement::from_decoded(decoded),
..Self::default()
};
for (field_name, value) in &decoded.fields {
match (normalise_field_name(field_name).as_str(), value) {
("guid", InstanceField::Guid(bytes)) => out.guid = Some(*bytes),
("description", InstanceField::String(s)) => {
out.description = Some(s.clone());
}
_ => {}
}
}
out
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ParameterValue {
Boolean(bool),
Integer(i64),
Enum(u32),
Double(f64),
Double3([f64; 3]),
Float(f32),
Float3([f32; 3]),
String(String),
Other {
class_name: String,
raw_bytes: Vec<u8>,
},
}
impl ParameterValue {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let find_value = |names: &[&str]| -> Option<&InstanceField> {
for (name, field) in &decoded.fields {
let normalised = normalise_field_name(name);
if names.iter().any(|wanted| normalised == *wanted) {
return Some(field);
}
}
None
};
match decoded.class.as_str() {
"APropertyBoolean" => {
if let Some(InstanceField::Bool(b)) = find_value(&["value"]) {
return ParameterValue::Boolean(*b);
}
}
"APropertyInteger" => {
if let Some(InstanceField::Integer { value, .. }) = find_value(&["value"]) {
return ParameterValue::Integer(*value);
}
}
"APropertyEnum" => {
if let Some(InstanceField::Integer { value, .. }) = find_value(&["value"]) {
return ParameterValue::Enum(*value as u32);
}
}
"APropertyDouble1" => {
if let Some(InstanceField::Float { value, .. }) = find_value(&["value"]) {
return ParameterValue::Double(*value);
}
}
"APropertyFloat" => {
if let Some(InstanceField::Float { value, .. }) = find_value(&["value"]) {
return ParameterValue::Float(*value as f32);
}
}
"APropertyDouble3" => {
if let Some(InstanceField::Vector(components)) = find_value(&["value"]) {
if let Some(tuple) = vector_to_f64_3(components) {
return ParameterValue::Double3(tuple);
}
}
}
"APropertyFloat3" => {
if let Some(InstanceField::Vector(components)) = find_value(&["value"]) {
if let Some(tuple) = vector_to_f32_3(components) {
return ParameterValue::Float3(tuple);
}
}
}
"APropertyString" => {
if let Some(InstanceField::String(s)) = find_value(&["value"]) {
return ParameterValue::String(s.clone());
}
}
_ => {}
}
let raw_bytes = decoded
.fields
.iter()
.find_map(|(_, f)| {
if let InstanceField::Bytes(b) = f {
Some(b.clone())
} else {
None
}
})
.unwrap_or_default();
ParameterValue::Other {
class_name: decoded.class.clone(),
raw_bytes,
}
}
pub fn storage_type(&self) -> StorageType {
match self {
ParameterValue::Boolean(_) | ParameterValue::Integer(_) | ParameterValue::Enum(_) => {
StorageType::Integer
}
ParameterValue::Double(_)
| ParameterValue::Double3(_)
| ParameterValue::Float(_)
| ParameterValue::Float3(_) => StorageType::Double,
ParameterValue::String(_) => StorageType::String,
ParameterValue::Other { .. } => StorageType::Other,
}
}
pub fn to_json_value(&self) -> serde_json::Value {
match self {
ParameterValue::Boolean(v) => serde_json::json!({
"kind": "boolean",
"value": v,
}),
ParameterValue::Integer(v) => serde_json::json!({
"kind": "integer",
"value": v,
}),
ParameterValue::Enum(v) => serde_json::json!({
"kind": "enum",
"value": v,
}),
ParameterValue::Double(v) => serde_json::json!({
"kind": "double",
"value": v,
}),
ParameterValue::Double3(v) => serde_json::json!({
"kind": "double3",
"value": v,
}),
ParameterValue::Float(v) => serde_json::json!({
"kind": "float",
"value": v,
}),
ParameterValue::Float3(v) => serde_json::json!({
"kind": "float3",
"value": v,
}),
ParameterValue::String(v) => serde_json::json!({
"kind": "string",
"value": v,
}),
ParameterValue::Other {
class_name,
raw_bytes,
} => serde_json::json!({
"kind": "other",
"class_name": class_name,
"raw_len": raw_bytes.len(),
}),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParameterEntry {
pub name: Option<String>,
pub value: ParameterValue,
pub source_class: String,
}
impl ParameterEntry {
pub fn to_json_value(&self) -> serde_json::Value {
let mut obj = serde_json::Map::new();
match &self.name {
Some(n) => {
obj.insert("name".into(), serde_json::Value::String(n.clone()));
}
None => {
obj.insert("name".into(), serde_json::Value::Null);
}
}
obj.insert(
"source_class".into(),
serde_json::Value::String(self.source_class.clone()),
);
if let serde_json::Value::Object(map) = self.value.to_json_value() {
for (k, v) in map {
obj.insert(k, v);
}
}
serde_json::Value::Object(obj)
}
}
pub fn is_aproperty_class(class_name: &str) -> bool {
matches!(
class_name,
"AProperty"
| "APropertyBoolean"
| "APropertyInteger"
| "APropertyEnum"
| "APropertyDouble1"
| "APropertyDouble3"
| "APropertyFloat"
| "APropertyFloat3"
| "APropertyString"
)
}
pub fn parameter_entries_from_decoded(decoded: &DecodedElement) -> Vec<ParameterEntry> {
if !is_aproperty_class(&decoded.class) {
return Vec::new();
}
let value = ParameterValue::from_decoded(decoded);
if matches!(
&value,
ParameterValue::Other { raw_bytes, .. } if raw_bytes.is_empty()
) && decoded.class == "AProperty"
{
return Vec::new();
}
let name = decoded.fields.iter().find_map(|(n, f)| {
let norm = normalise_field_name(n);
if matches!(norm.as_str(), "name" | "sname" | "parametername") {
if let InstanceField::String(s) = f {
return Some(s.clone());
}
}
None
});
vec![ParameterEntry {
name,
value,
source_class: decoded.class.clone(),
}]
}
fn vector_to_f64_3(components: &[InstanceField]) -> Option<[f64; 3]> {
if components.len() < 3 {
return None;
}
let to_f64 = |f: &InstanceField| match f {
InstanceField::Float { value, .. } => Some(*value),
InstanceField::Integer { value, .. } => Some(*value as f64),
_ => None,
};
Some([
to_f64(&components[0])?,
to_f64(&components[1])?,
to_f64(&components[2])?,
])
}
fn vector_to_f32_3(components: &[InstanceField]) -> Option<[f32; 3]> {
let tuple_f64 = vector_to_f64_3(components)?;
Some([
tuple_f64[0] as f32,
tuple_f64[1] as f32,
tuple_f64[2] as f32,
])
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ParameterBundle {
values: std::collections::BTreeMap<String, ParameterValue>,
}
impl ParameterBundle {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, name: impl Into<String>, value: ParameterValue) {
self.values.insert(name.into(), value);
}
pub fn get(&self, name: &str) -> Option<&ParameterValue> {
self.values.get(name)
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &ParameterValue)> {
self.values.iter().map(|(k, v)| (k.as_str(), v))
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.values.keys().map(String::as_str)
}
}
pub fn effective_value<'a>(
instance: &'a ParameterBundle,
type_: &'a ParameterBundle,
name: &str,
) -> Option<&'a ParameterValue> {
instance.get(name).or_else(|| type_.get(name))
}
pub fn merge_effective(instance: &ParameterBundle, type_: &ParameterBundle) -> ParameterBundle {
let mut out = type_.clone();
for (name, value) in instance.iter() {
out.insert(name.to_string(), value.clone());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formats::ClassEntry;
fn wrong_schema() -> ClassEntry {
ClassEntry {
name: "Wall".into(),
offset: 0,
fields: vec![],
tag: None,
parent: None,
declared_field_count: None,
was_parent_only: false,
ancestor_tag: None,
}
}
#[test]
fn parameter_element_rejects_wrong_schema() {
assert!(
ParameterElementDecoder
.decode(&[], &wrong_schema(), &HandleIndex::new())
.is_err()
);
}
#[test]
fn shared_parameter_rejects_wrong_schema() {
assert!(
SharedParameterDecoder
.decode(&[], &wrong_schema(), &HandleIndex::new())
.is_err()
);
}
#[test]
fn storage_type_mapping() {
assert_eq!(StorageType::from_code(0), StorageType::None);
assert_eq!(StorageType::from_code(1), StorageType::Integer);
assert_eq!(StorageType::from_code(2), StorageType::Double);
assert_eq!(StorageType::from_code(3), StorageType::String);
assert_eq!(StorageType::from_code(4), StorageType::ElementId);
assert_eq!(StorageType::from_code(99), StorageType::Other);
assert!(StorageType::Integer.is_numeric());
assert!(StorageType::Double.is_numeric());
assert!(!StorageType::String.is_numeric());
assert!(!StorageType::ElementId.is_numeric());
}
#[test]
fn parameter_element_from_decoded() {
let fields = vec![
("m_name".into(), InstanceField::String("Head Height".into())),
(
"m_parameter_group".into(),
InstanceField::Integer {
value: 7,
signed: false,
size: 4,
},
),
(
"m_storage_type".into(),
InstanceField::Integer {
value: 2,
signed: false,
size: 4,
},
),
("m_is_shared".into(), InstanceField::Bool(false)),
("m_visible".into(), InstanceField::Bool(true)),
];
let decoded = DecodedElement {
id: None,
class: "ParameterElement".into(),
fields,
byte_range: 0..0,
provenance: Default::default(),
};
let p = ParameterElement::from_decoded(&decoded);
assert_eq!(p.name.as_deref(), Some("Head Height"));
assert_eq!(p.parameter_group, Some(7));
assert_eq!(p.storage_type, Some(StorageType::Double));
assert_eq!(p.is_shared, Some(false));
assert_eq!(p.visible, Some(true));
}
#[test]
fn shared_parameter_from_decoded_carries_base_fields() {
let guid_bytes = [
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65,
0x43, 0x21,
];
let fields = vec![
("m_name".into(), InstanceField::String("Fire Rating".into())),
(
"m_storage_type".into(),
InstanceField::Integer {
value: 3,
signed: false,
size: 4,
},
),
("m_is_shared".into(), InstanceField::Bool(true)),
("m_guid".into(), InstanceField::Guid(guid_bytes)),
(
"m_description".into(),
InstanceField::String("Hourly fire resistance rating".into()),
),
];
let decoded = DecodedElement {
id: None,
class: "SharedParameter".into(),
fields,
byte_range: 0..0,
provenance: Default::default(),
};
let sp = SharedParameter::from_decoded(&decoded);
assert_eq!(sp.base.name.as_deref(), Some("Fire Rating"));
assert_eq!(sp.base.storage_type, Some(StorageType::String));
assert_eq!(sp.base.is_shared, Some(true));
assert_eq!(sp.guid, Some(guid_bytes));
assert_eq!(
sp.description.as_deref(),
Some("Hourly fire resistance rating")
);
}
#[test]
fn empty_tolerance() {
let empty = DecodedElement {
id: None,
class: "ParameterElement".into(),
fields: vec![],
byte_range: 0..0,
provenance: Default::default(),
};
assert!(ParameterElement::from_decoded(&empty).name.is_none());
assert!(SharedParameter::from_decoded(&empty).guid.is_none());
}
#[test]
fn class_names() {
assert_eq!(ParameterElementDecoder.class_name(), "ParameterElement");
assert_eq!(SharedParameterDecoder.class_name(), "SharedParameter");
}
fn mk_decoded(class: &str, fields: Vec<(String, InstanceField)>) -> DecodedElement {
DecodedElement {
id: None,
class: class.into(),
fields,
byte_range: 0..0,
provenance: Default::default(),
}
}
#[test]
fn aproperty_boolean_decodes_to_parameter_value() {
let d = mk_decoded(
"APropertyBoolean",
vec![("m_value".into(), InstanceField::Bool(true))],
);
assert_eq!(
ParameterValue::from_decoded(&d),
ParameterValue::Boolean(true)
);
}
#[test]
fn aproperty_integer_decodes_to_parameter_value() {
let d = mk_decoded(
"APropertyInteger",
vec![(
"m_value".into(),
InstanceField::Integer {
value: 42,
signed: true,
size: 4,
},
)],
);
assert_eq!(
ParameterValue::from_decoded(&d),
ParameterValue::Integer(42)
);
}
#[test]
fn aproperty_enum_decodes_to_parameter_value() {
let d = mk_decoded(
"APropertyEnum",
vec![(
"m_value".into(),
InstanceField::Integer {
value: 7,
signed: false,
size: 4,
},
)],
);
assert_eq!(ParameterValue::from_decoded(&d), ParameterValue::Enum(7));
}
#[test]
fn aproperty_double1_decodes_to_parameter_value() {
let d = mk_decoded(
"APropertyDouble1",
vec![(
"m_value".into(),
InstanceField::Float {
value: 3.5,
size: 8,
},
)],
);
assert_eq!(
ParameterValue::from_decoded(&d),
ParameterValue::Double(3.5)
);
}
#[test]
fn aproperty_double3_decodes_to_parameter_value() {
let components = vec![
InstanceField::Float {
value: 1.0,
size: 8,
},
InstanceField::Float {
value: 2.0,
size: 8,
},
InstanceField::Float {
value: 3.0,
size: 8,
},
];
let d = mk_decoded(
"APropertyDouble3",
vec![("m_value".into(), InstanceField::Vector(components))],
);
assert_eq!(
ParameterValue::from_decoded(&d),
ParameterValue::Double3([1.0, 2.0, 3.0])
);
}
#[test]
fn aproperty_float_decodes_to_parameter_value() {
let d = mk_decoded(
"APropertyFloat",
vec![(
"m_value".into(),
InstanceField::Float {
value: 0.5,
size: 4,
},
)],
);
assert_eq!(ParameterValue::from_decoded(&d), ParameterValue::Float(0.5));
}
#[test]
fn aproperty_float3_decodes_to_parameter_value() {
let components = vec![
InstanceField::Float {
value: 0.1,
size: 4,
},
InstanceField::Float {
value: 0.2,
size: 4,
},
InstanceField::Float {
value: 0.3,
size: 4,
},
];
let d = mk_decoded(
"APropertyFloat3",
vec![("m_value".into(), InstanceField::Vector(components))],
);
match ParameterValue::from_decoded(&d) {
ParameterValue::Float3([x, y, z]) => {
assert!((x - 0.1).abs() < 1e-6);
assert!((y - 0.2).abs() < 1e-6);
assert!((z - 0.3).abs() < 1e-6);
}
other => panic!("expected Float3, got {other:?}"),
}
}
#[test]
fn aproperty_string_decodes_to_parameter_value() {
let d = mk_decoded(
"APropertyString",
vec![("m_value".into(), InstanceField::String("Mark-A".into()))],
);
assert_eq!(
ParameterValue::from_decoded(&d),
ParameterValue::String("Mark-A".into())
);
assert_eq!(
ParameterValue::String("Mark-A".into()).storage_type(),
StorageType::String
);
}
#[test]
fn parameter_entries_from_aproperty_are_non_empty() {
let d = mk_decoded(
"APropertyInteger",
vec![(
"m_value".into(),
InstanceField::Integer {
value: 7,
signed: true,
size: 4,
},
)],
);
let entries = parameter_entries_from_decoded(&d);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].source_class, "APropertyInteger");
assert_eq!(entries[0].value, ParameterValue::Integer(7));
let json = entries[0].to_json_value();
assert_eq!(json["kind"], "integer");
assert_eq!(json["value"], 7);
}
#[test]
fn parameter_entries_from_host_element_are_honestly_empty() {
let d = mk_decoded(
"Floor",
vec![(
"m_level_id".into(),
InstanceField::ElementId { tag: 0, id: 42 },
)],
);
assert!(parameter_entries_from_decoded(&d).is_empty());
}
#[test]
fn aproperty_unknown_falls_through_to_other() {
let d = mk_decoded(
"APropertyNewVariantFromFutureRevit",
vec![("m_value".into(), InstanceField::Bytes(vec![0x01, 0x02]))],
);
match ParameterValue::from_decoded(&d) {
ParameterValue::Other {
class_name,
raw_bytes,
} => {
assert_eq!(class_name, "APropertyNewVariantFromFutureRevit");
assert_eq!(raw_bytes, vec![0x01, 0x02]);
}
other => panic!("expected Other, got {other:?}"),
}
}
#[test]
fn aproperty_missing_value_field_falls_through_to_other() {
let d = mk_decoded("APropertyBoolean", vec![]);
assert!(matches!(
ParameterValue::from_decoded(&d),
ParameterValue::Other { .. }
));
}
#[test]
fn parameter_value_storage_type_mapping() {
assert_eq!(
ParameterValue::Boolean(false).storage_type(),
StorageType::Integer
);
assert_eq!(
ParameterValue::Integer(0).storage_type(),
StorageType::Integer
);
assert_eq!(ParameterValue::Enum(0).storage_type(), StorageType::Integer);
assert_eq!(
ParameterValue::Double(0.0).storage_type(),
StorageType::Double
);
assert_eq!(
ParameterValue::Double3([0.0; 3]).storage_type(),
StorageType::Double
);
assert_eq!(
ParameterValue::Float(0.0).storage_type(),
StorageType::Double
);
assert_eq!(
ParameterValue::Float3([0.0; 3]).storage_type(),
StorageType::Double
);
assert_eq!(
ParameterValue::Other {
class_name: String::new(),
raw_bytes: vec![],
}
.storage_type(),
StorageType::Other
);
}
#[test]
fn parameter_element_value_source_user_set_when_flags_absent() {
let decoded = mk_decoded(
"ParameterElement",
vec![("m_name".into(), InstanceField::String("Height".into()))],
);
let p = ParameterElement::from_decoded(&decoded);
assert_eq!(p.value_source, None);
assert!(p.is_user_writable());
}
#[test]
fn parameter_element_value_source_calculated() {
let decoded = mk_decoded(
"ParameterElement",
vec![
("m_name".into(), InstanceField::String("TotalArea".into())),
("m_is_calculated".into(), InstanceField::Bool(true)),
("m_is_reporting".into(), InstanceField::Bool(false)),
],
);
let p = ParameterElement::from_decoded(&decoded);
assert_eq!(p.value_source, Some(ParameterValueSource::Calculated));
assert!(!p.is_user_writable());
}
#[test]
fn parameter_element_value_source_reporting() {
let decoded = mk_decoded(
"ParameterElement",
vec![
("m_name".into(), InstanceField::String("DimLength".into())),
("m_is_reporting".into(), InstanceField::Bool(true)),
],
);
let p = ParameterElement::from_decoded(&decoded);
assert_eq!(p.value_source, Some(ParameterValueSource::Reporting));
assert!(!p.is_user_writable());
}
#[test]
fn parameter_element_value_source_reporting_wins_over_calculated() {
let decoded = mk_decoded(
"ParameterElement",
vec![
("m_is_calculated".into(), InstanceField::Bool(true)),
("m_is_reporting".into(), InstanceField::Bool(true)),
],
);
let p = ParameterElement::from_decoded(&decoded);
assert_eq!(p.value_source, Some(ParameterValueSource::Reporting));
}
#[test]
fn parameter_element_value_source_explicit_user_set() {
let decoded = mk_decoded(
"ParameterElement",
vec![
("m_is_calculated".into(), InstanceField::Bool(false)),
("m_is_reporting".into(), InstanceField::Bool(false)),
],
);
let p = ParameterElement::from_decoded(&decoded);
assert_eq!(p.value_source, Some(ParameterValueSource::UserSet));
assert!(p.is_user_writable());
}
#[test]
fn parameter_element_value_source_accepts_formula_alias() {
let decoded = mk_decoded(
"ParameterElement",
vec![("m_is_formula".into(), InstanceField::Bool(true))],
);
let p = ParameterElement::from_decoded(&decoded);
assert_eq!(p.value_source, Some(ParameterValueSource::Calculated));
}
#[test]
fn parameter_bundle_insert_get_roundtrip() {
let mut b = ParameterBundle::new();
assert!(b.is_empty());
b.insert("Width", ParameterValue::Double(2.0));
assert_eq!(b.len(), 1);
assert_eq!(b.get("Width"), Some(&ParameterValue::Double(2.0)));
assert!(b.get("Nonexistent").is_none());
}
#[test]
fn parameter_bundle_iteration_is_sorted() {
let mut b = ParameterBundle::new();
b.insert("Zebra", ParameterValue::Boolean(true));
b.insert("Alpha", ParameterValue::Integer(1));
b.insert("Mango", ParameterValue::Double(5.5));
let names: Vec<&str> = b.names().collect();
assert_eq!(names, vec!["Alpha", "Mango", "Zebra"]);
}
#[test]
fn effective_value_prefers_instance_over_type() {
let mut type_ = ParameterBundle::new();
type_.insert("Height", ParameterValue::Double(6.8));
let mut instance = ParameterBundle::new();
instance.insert("Height", ParameterValue::Double(7.0));
assert_eq!(
effective_value(&instance, &type_, "Height"),
Some(&ParameterValue::Double(7.0))
);
}
#[test]
fn effective_value_falls_back_to_type_when_instance_missing() {
let mut type_ = ParameterBundle::new();
type_.insert("Width", ParameterValue::Double(2.0));
let instance = ParameterBundle::new();
assert_eq!(
effective_value(&instance, &type_, "Width"),
Some(&ParameterValue::Double(2.0))
);
}
#[test]
fn effective_value_returns_none_for_unknown_name() {
let type_ = ParameterBundle::new();
let instance = ParameterBundle::new();
assert!(effective_value(&instance, &type_, "DoesNotExist").is_none());
}
#[test]
fn merge_effective_union_with_instance_precedence() {
let mut type_ = ParameterBundle::new();
type_.insert("Width", ParameterValue::Double(2.0));
type_.insert("Height", ParameterValue::Double(6.8));
type_.insert("Material", ParameterValue::Enum(3));
let mut instance = ParameterBundle::new();
instance.insert("Height", ParameterValue::Double(7.0));
instance.insert("Mark", ParameterValue::Integer(42));
let effective = merge_effective(&instance, &type_);
assert_eq!(effective.len(), 4);
assert_eq!(effective.get("Width"), Some(&ParameterValue::Double(2.0)));
assert_eq!(effective.get("Mark"), Some(&ParameterValue::Integer(42)));
assert_eq!(effective.get("Height"), Some(&ParameterValue::Double(7.0)));
assert_eq!(effective.get("Material"), Some(&ParameterValue::Enum(3)));
}
#[test]
fn merge_effective_preserves_input_bundles() {
let mut type_ = ParameterBundle::new();
type_.insert("A", ParameterValue::Integer(1));
let mut instance = ParameterBundle::new();
instance.insert("A", ParameterValue::Integer(2));
let _ = merge_effective(&instance, &type_);
assert_eq!(type_.get("A"), Some(&ParameterValue::Integer(1)));
assert_eq!(instance.get("A"), Some(&ParameterValue::Integer(2)));
}
#[test]
fn aproperty_decoders_reject_wrong_schema() {
assert!(
APropertyBooleanDecoder
.decode(&[], &wrong_schema(), &HandleIndex::new())
.is_err()
);
assert!(
APropertyDouble3Decoder
.decode(&[], &wrong_schema(), &HandleIndex::new())
.is_err()
);
}
}