use super::CommandObject;
use std::borrow::Cow;
use bacnet_types::enums::PropertyIdentifier as P;
use crate::property_metadata::{
PropertyConformance::{Optional, RequiredRead, RequiredWrite},
PropertyMetadata,
PropertyWriteCapability::{Always, ReadOnly},
};
const BASE: &[PropertyMetadata] = &[
PropertyMetadata::new(P::OBJECT_IDENTIFIER, RequiredRead, None, ReadOnly),
PropertyMetadata::new(P::OBJECT_NAME, RequiredRead, None, ReadOnly),
PropertyMetadata::new(P::DESCRIPTION, Optional, None, Always),
PropertyMetadata::new(P::OBJECT_TYPE, RequiredRead, None, ReadOnly),
PropertyMetadata::new(P::PRESENT_VALUE, RequiredWrite, None, Always),
PropertyMetadata::new(P::IN_PROCESS, RequiredRead, None, ReadOnly),
PropertyMetadata::new(P::ALL_WRITES_SUCCESSFUL, RequiredRead, None, ReadOnly),
PropertyMetadata::new(P::ACTION, RequiredRead, None, ReadOnly),
PropertyMetadata::new(P::STATUS_FLAGS, Optional, None, ReadOnly),
PropertyMetadata::new(P::RELIABILITY, Optional, None, ReadOnly),
PropertyMetadata::new(P::PROPERTY_LIST, RequiredRead, None, ReadOnly),
];
pub(super) fn for_object(object: &CommandObject) -> Cow<'_, [PropertyMetadata]> {
if object.action_text.is_none() {
return Cow::Borrowed(BASE);
}
let mut rows = BASE.to_vec();
let after_action = rows
.iter()
.position(|row| row.property_identifier == P::ACTION)
.expect("Action row")
+ 1;
rows.insert(
after_action,
PropertyMetadata::new(P::ACTION_TEXT, Optional, None, ReadOnly),
);
Cow::Owned(rows)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BACnetObject;
use bacnet_types::enums::{ErrorClass, ErrorCode};
use bacnet_types::error::Error;
use bacnet_types::primitives::PropertyValue;
use std::collections::HashSet;
fn assert_error(error: Error, expected: ErrorCode) {
assert!(
matches!(error, Error::Protocol { class, code }
if class == ErrorClass::PROPERTY.to_raw() as u32
&& code == expected.to_raw() as u32),
"expected {expected:?}, got {error:?}"
);
}
#[test]
fn property_metadata_command_exact_sets_readable_rows_and_indexed_list() {
let object = CommandObject::new(1, "CMD-1").unwrap();
let all = [
P::OBJECT_IDENTIFIER,
P::OBJECT_NAME,
P::DESCRIPTION,
P::OBJECT_TYPE,
P::PRESENT_VALUE,
P::IN_PROCESS,
P::ALL_WRITES_SUCCESSFUL,
P::ACTION,
P::STATUS_FLAGS,
P::RELIABILITY,
];
let required = [
P::OBJECT_IDENTIFIER,
P::OBJECT_NAME,
P::OBJECT_TYPE,
P::PRESENT_VALUE,
P::IN_PROCESS,
P::ALL_WRITES_SUCCESSFUL,
P::ACTION,
P::PROPERTY_LIST,
];
let metadata = object.property_metadata();
assert!(matches!(metadata, Cow::Borrowed(_)));
assert_eq!(metadata.len(), 11);
assert_eq!(object.property_list().as_ref(), all);
assert_eq!(object.required_properties().as_ref(), required);
assert_eq!(
metadata
.iter()
.map(|row| row.property_identifier)
.collect::<HashSet<_>>()
.len(),
metadata.len()
);
assert!(!object.is_createable());
assert!(object.is_deleteable());
assert!(!object.supports_cov());
for row in metadata.iter() {
assert_eq!(row.presence_condition, None);
let expected = if row.property_identifier == P::PRESENT_VALUE {
RequiredWrite
} else if required.contains(&row.property_identifier) {
RequiredRead
} else {
Optional
};
assert_eq!(row.conformance, expected, "{:?}", row.property_identifier);
object.read_property(row.property_identifier, None).unwrap();
}
assert_eq!(
object.read_property(P::PRESENT_VALUE, None).unwrap(),
PropertyValue::Unsigned(0)
);
let whole = PropertyValue::List(vec![]);
assert_eq!(object.read_property(P::ACTION, None).unwrap(), whole);
assert!(object.is_array_property(P::ACTION));
let wire: Vec<_> = all
.iter()
.filter(|&&p| !matches!(p, P::OBJECT_IDENTIFIER | P::OBJECT_NAME | P::OBJECT_TYPE))
.map(|p| PropertyValue::Enumerated(p.to_raw()))
.collect();
assert_eq!(wire.len(), 7);
assert!(object.is_array_property(P::PROPERTY_LIST));
assert_eq!(
object.read_property(P::PROPERTY_LIST, None).unwrap(),
PropertyValue::List(wire.clone())
);
assert_eq!(
object.read_property(P::PROPERTY_LIST, Some(0)).unwrap(),
PropertyValue::Unsigned(7)
);
for (index, value) in wire.iter().enumerate() {
assert_eq!(
object
.read_property(P::PROPERTY_LIST, Some(index as u32 + 1))
.unwrap(),
*value
);
}
for index in [8, u32::MAX] {
assert_error(
object
.read_property(P::PROPERTY_LIST, Some(index))
.unwrap_err(),
ErrorCode::INVALID_ARRAY_INDEX,
);
}
}
#[test]
fn property_metadata_command_write_capabilities_match_dispatch() {
let mut object = CommandObject::new(1, "CMD-1").unwrap();
object
.set_action(vec![
bacnet_types::constructed::BACnetActionList::default();
3
])
.unwrap();
let original = object.property_metadata().into_owned();
for row in &original {
let p = row.property_identifier;
let capability = match p {
P::DESCRIPTION | P::PRESENT_VALUE => Always,
_ => ReadOnly,
};
assert_eq!(row.write_capability, capability, "{p:?}");
assert_eq!(
object.is_writable_property(p),
capability.is_writable(),
"{p:?}"
);
let value = if p == P::PRESENT_VALUE {
PropertyValue::Unsigned(3)
} else {
object.read_property(p, None).unwrap()
};
let result = object.write_property(p, None, value, None);
if capability.is_writable() {
result.unwrap();
} else {
assert_error(result.unwrap_err(), ErrorCode::WRITE_ACCESS_DENIED);
}
}
object
.write_property(P::PRESENT_VALUE, None, PropertyValue::Unsigned(3), None)
.unwrap();
assert_eq!(
object.read_property(P::PRESENT_VALUE, None).unwrap(),
PropertyValue::Unsigned(3)
);
assert_error(
object
.write_property(P::PRESENT_VALUE, None, PropertyValue::Real(1.0), None)
.unwrap_err(),
ErrorCode::INVALID_DATA_TYPE,
);
assert_eq!(
object.read_property(P::PRESENT_VALUE, None).unwrap(),
PropertyValue::Unsigned(3)
);
assert!(!object.is_writable_property(P::ACTION));
assert_error(
object
.write_property(
P::ACTION,
None,
PropertyValue::OctetString(vec![1, 2, 3]),
None,
)
.unwrap_err(),
ErrorCode::WRITE_ACCESS_DENIED,
);
assert!(!object.is_writable_property(P::OBJECT_NAME));
assert_error(
object
.write_property(
P::OBJECT_NAME,
None,
PropertyValue::CharacterString("CMD-2".into()),
None,
)
.unwrap_err(),
ErrorCode::WRITE_ACCESS_DENIED,
);
for p in [P::IN_PROCESS, P::ALL_WRITES_SUCCESSFUL] {
let value = object.read_property(p, None).unwrap();
assert_error(
object.write_property(p, None, value, None).unwrap_err(),
ErrorCode::WRITE_ACCESS_DENIED,
);
assert!(!object.is_writable_property(p));
}
assert_error(
object
.write_property(P::DESCRIPTION, None, PropertyValue::Unsigned(1), None)
.unwrap_err(),
ErrorCode::INVALID_DATA_TYPE,
);
for p in [P::ACTION_TEXT, P::EVENT_STATE, P::OUT_OF_SERVICE] {
assert!(!object.is_writable_property(p));
assert_error(
object.read_property(p, None).unwrap_err(),
ErrorCode::UNKNOWN_PROPERTY,
);
assert_error(
object
.write_property(p, None, PropertyValue::Null, None)
.unwrap_err(),
ErrorCode::UNKNOWN_PROPERTY,
);
}
assert_eq!(object.property_metadata().as_ref(), original);
}
#[test]
fn property_metadata_command_action_text_row_follows_action_once_set() {
let mut object = CommandObject::new(1, "CMD-1").unwrap();
object
.set_action(vec![
bacnet_types::constructed::BACnetActionList::default();
2
])
.unwrap();
object
.set_action_text(vec!["Unoccupied".into(), "Occupied".into()])
.unwrap();
let metadata = object.property_metadata();
assert!(matches!(metadata, Cow::Owned(_)));
let at = metadata
.iter()
.position(|row| row.property_identifier == P::ACTION_TEXT)
.unwrap();
assert_eq!(metadata[at - 1].property_identifier, P::ACTION);
assert_eq!(metadata[at].conformance, Optional);
assert_eq!(metadata[at].write_capability, ReadOnly);
assert!(object.property_list().contains(&P::ACTION_TEXT));
assert!(object.is_array_property(P::ACTION_TEXT));
assert!(!object.is_writable_property(P::ACTION_TEXT));
for index in [None, Some(1)] {
assert_error(
object
.write_property(
P::ACTION_TEXT,
index,
PropertyValue::CharacterString("x".into()),
None,
)
.unwrap_err(),
ErrorCode::WRITE_ACCESS_DENIED,
);
}
}
}