bacnet_objects/property_metadata.rs
1//! Executable property metadata and compatibility projections.
2
3use std::borrow::Cow;
4
5use bacnet_types::enums::PropertyIdentifier;
6
7/// A property's base conformance code in its Clause 12 object table.
8///
9/// Conditions attached to a base code are represented separately by
10/// [`PropertyPresenceCondition`]. This keeps the table code available to RPM
11/// and PICS while preserving why an implemented conditional row is present.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum PropertyConformance {
14 /// Required and readable (`R`).
15 RequiredRead,
16 /// Required and writable (`W`).
17 RequiredWrite,
18 /// Optional (`O`), including an implemented conditionally present row.
19 Optional,
20}
21
22impl PropertyConformance {
23 /// Whether the table code classifies this property as required.
24 pub const fn is_required(self) -> bool {
25 matches!(self, Self::RequiredRead | Self::RequiredWrite)
26 }
27}
28
29/// Why a conditionally present property row is implemented.
30///
31/// Metadata describes effective rows that are already present. These values
32/// retain the conformance reason; they are not predicates evaluated at runtime.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum PropertyPresenceCondition {
36 /// The row is present because the object is commandable.
37 Commandable,
38 /// The object reports intrinsically, and its table's footnotes make this
39 /// row required of such an object: the event configuration, Event_Enable,
40 /// Acked_Transitions, Notify_Type, Event_Time_Stamps and
41 /// Event_Detection_Enable, for example.
42 IntrinsicReportingRequired,
43 /// The object reports intrinsically, and its table's footnotes only let
44 /// this row be present for such an object without requiring it:
45 /// Event_Message_Texts, Event_Message_Texts_Config, the
46 /// Event_Algorithm_Inhibit pair and Time_Delay_Normal.
47 IntrinsicReportingOptional,
48 /// Audit Reporting is active, making its recipient required and writable.
49 AuditReporting,
50 /// An optional object-owned Audit setting is provisioned.
51 ObjectAuditReporting,
52 /// An optional Audit priority filter on a commandable reporting object.
53 CommandableAuditReporting,
54 /// The Value_Source mechanism is implemented, making its source required.
55 ValueSourceTracking,
56 /// Source tracking on a commandable object requires its source array and time.
57 CommandableValueSourceTracking,
58 /// The paired Active_Text and Inactive_Text option is implemented.
59 PairedText,
60}
61
62/// The write capability implemented by an object's property-write routes.
63///
64/// A conditional capability remains writable for PICS purposes even when the
65/// current object state causes a particular request to be denied.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67#[non_exhaustive]
68pub enum PropertyWriteCapability {
69 /// No network property-write route is implemented.
70 ReadOnly,
71 /// The property-write route is available whatever Out_Of_Service and
72 /// command ownership say. The object's own checks may still refuse a
73 /// value, or a write in some state: an Audit Log refuses a Buffer_Size
74 /// write while Log_Enable is TRUE, for example.
75 Always,
76 /// The property-write route is available only while Out_Of_Service is true.
77 WhenOutOfService,
78 /// Correction is restricted to the original command owner at that priority.
79 WhenCommandOwner,
80 /// The property has no write route of its own, but a write of the named
81 /// property changes it too: State_Text written whole, or its size at
82 /// index 0, sets a multi-state object's Number_Of_States (#1443). A
83 /// WriteProperty naming this property itself is refused, so it doesn't
84 /// count as writable.
85 Through(PropertyIdentifier),
86}
87
88impl PropertyWriteCapability {
89 /// Whether a network property-write route naming this property is
90 /// implemented. [`Self::Through`] has none.
91 pub const fn is_writable(self) -> bool {
92 !matches!(self, Self::ReadOnly | Self::Through(_))
93 }
94
95 /// The property whose writes change this one, for [`Self::Through`].
96 pub const fn written_through(self) -> Option<PropertyIdentifier> {
97 match self {
98 Self::Through(property) => Some(property),
99 _ => None,
100 }
101 }
102}
103
104/// Canonical metadata for one effective property row on a BACnet object.
105///
106/// A migrated object's canonical set includes `PROPERTY_LIST` itself. The
107/// legacy [`BACnetObject::property_list`](crate::traits::BACnetObject::property_list)
108/// projection omits that identifier, and the encoded BACnet Property_List
109/// value additionally omits Object_Identifier, Object_Name, and Object_Type.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111#[non_exhaustive]
112pub struct PropertyMetadata {
113 /// The standard property identifier.
114 pub property_identifier: PropertyIdentifier,
115 /// The base `R`, `W`, or `O` table classification.
116 pub conformance: PropertyConformance,
117 /// The reason a conditionally present row is implemented.
118 pub presence_condition: Option<PropertyPresenceCondition>,
119 /// The write route implemented for this property.
120 pub write_capability: PropertyWriteCapability,
121}
122
123impl PropertyMetadata {
124 /// Whether this effective row is required, including the rows intrinsic
125 /// reporting, enabled Audit Reporting and command-source mechanisms
126 /// require while retaining optional base table codes.
127 pub const fn is_required(self) -> bool {
128 self.conformance.is_required()
129 || matches!(
130 self.presence_condition,
131 Some(
132 PropertyPresenceCondition::IntrinsicReportingRequired
133 | PropertyPresenceCondition::AuditReporting
134 | PropertyPresenceCondition::ValueSourceTracking
135 | PropertyPresenceCondition::CommandableValueSourceTracking
136 )
137 )
138 }
139
140 /// Construct one canonical metadata row.
141 ///
142 /// This constructor is the stable construction path as the non-exhaustive
143 /// metadata model gains conditions needed by later object migrations.
144 pub const fn new(
145 property_identifier: PropertyIdentifier,
146 conformance: PropertyConformance,
147 presence_condition: Option<PropertyPresenceCondition>,
148 write_capability: PropertyWriteCapability,
149 ) -> Self {
150 Self {
151 property_identifier,
152 conformance,
153 presence_condition,
154 write_capability,
155 }
156 }
157}
158
159/// Derive the legacy object property-list projection from canonical metadata.
160///
161/// The projection preserves metadata order and omits only `PROPERTY_LIST`.
162pub fn property_list_from_metadata(
163 metadata: &[PropertyMetadata],
164) -> Cow<'static, [PropertyIdentifier]> {
165 Cow::Owned(
166 metadata
167 .iter()
168 .filter_map(|row| {
169 (row.property_identifier != PropertyIdentifier::PROPERTY_LIST)
170 .then_some(row.property_identifier)
171 })
172 .collect(),
173 )
174}
175
176/// Derive all effectively required identifiers from canonical metadata.
177///
178/// Unlike the legacy property-list projection, this includes `PROPERTY_LIST`.
179/// Consumers such as RPM apply their own service-specific exclusion.
180pub fn required_properties_from_metadata(
181 metadata: &[PropertyMetadata],
182) -> Cow<'static, [PropertyIdentifier]> {
183 Cow::Owned(
184 metadata
185 .iter()
186 .filter_map(|row| row.is_required().then_some(row.property_identifier))
187 .collect(),
188 )
189}
190
191/// Project implemented write capability for one property identifier.
192pub fn is_writable_in_metadata(
193 metadata: &[PropertyMetadata],
194 property_identifier: PropertyIdentifier,
195) -> bool {
196 metadata.iter().any(|row| {
197 row.property_identifier == property_identifier && row.write_capability.is_writable()
198 })
199}