Skip to main content

bacnet_objects/
database.rs

1//! ObjectDatabase — stores and retrieves BACnet objects by identifier.
2
3use std::collections::HashMap;
4
5use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType};
6use bacnet_types::error::Error;
7use bacnet_types::primitives::ObjectIdentifier;
8
9use crate::traits::BACnetObject;
10
11/// A collection of BACnet objects, keyed by ObjectIdentifier.
12///
13/// Enforces BACnet Clause 12.11.12: Object_Name must be unique within a device.
14/// Maintains secondary indexes for O(1) name lookup and O(1) type lookup.
15pub struct ObjectDatabase {
16    objects: HashMap<ObjectIdentifier, Box<dyn BACnetObject>>,
17    /// Reverse index: object name → ObjectIdentifier for uniqueness enforcement.
18    name_index: HashMap<String, ObjectIdentifier>,
19    /// Type index: object type → set of ObjectIdentifiers for fast enumeration.
20    type_index: HashMap<ObjectType, Vec<ObjectIdentifier>>,
21}
22
23impl Default for ObjectDatabase {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl ObjectDatabase {
30    /// Create an empty database.
31    pub fn new() -> Self {
32        Self {
33            objects: HashMap::new(),
34            name_index: HashMap::new(),
35            type_index: HashMap::new(),
36        }
37    }
38
39    /// Add an object to the database.
40    ///
41    /// Returns `Err` if another object already has the same `object_name()`
42    /// (BACnet Clause 12.11.12 requires unique names within a device).
43    /// Replacing an object with the same OID is allowed (the old object is removed).
44    pub fn add(&mut self, object: Box<dyn BACnetObject>) -> Result<(), Error> {
45        let oid = object.object_identifier();
46        let name = object.object_name().to_string();
47
48        // Check for name collision with a *different* object
49        if let Some(&existing_oid) = self.name_index.get(&name) {
50            if existing_oid != oid {
51                return Err(Error::Protocol {
52                    class: ErrorClass::OBJECT.to_raw() as u32,
53                    code: ErrorCode::DUPLICATE_NAME.to_raw() as u32,
54                });
55            }
56        }
57
58        // If replacing an existing object, remove its old name from the index
59        if let Some(old) = self.objects.get(&oid) {
60            let old_name = old.object_name().to_string();
61            self.name_index.remove(&old_name);
62        }
63
64        self.name_index.insert(name, oid);
65        let is_new = !self.objects.contains_key(&oid);
66        self.objects.insert(oid, object);
67        if is_new {
68            self.type_index
69                .entry(oid.object_type())
70                .or_default()
71                .push(oid);
72        }
73        Ok(())
74    }
75
76    /// Look up an object by its name. O(1) via the name index.
77    pub fn find_by_name(&self, name: &str) -> Option<&dyn BACnetObject> {
78        let oid = self.name_index.get(name)?;
79        self.objects.get(oid).map(|o| o.as_ref())
80    }
81
82    /// Check whether `new_name` is available for object `oid`.
83    ///
84    /// Returns `Ok(())` if the name is unused or already belongs to `oid`.
85    /// Returns `Err(DUPLICATE_NAME)` if another object owns the name.
86    pub fn check_name_available(
87        &self,
88        oid: &ObjectIdentifier,
89        new_name: &str,
90    ) -> Result<(), Error> {
91        if let Some(&owner) = self.name_index.get(new_name) {
92            if owner != *oid {
93                return Err(Error::Protocol {
94                    class: ErrorClass::OBJECT.to_raw() as u32,
95                    code: ErrorCode::DUPLICATE_NAME.to_raw() as u32,
96                });
97            }
98        }
99        Ok(())
100    }
101
102    /// Update the name index after a successful Object_Name write.
103    ///
104    /// Call this after `write_property(OBJECT_NAME, …)` succeeds.
105    pub fn update_name_index(&mut self, oid: &ObjectIdentifier) {
106        if let Some(obj) = self.objects.get(oid) {
107            // Remove any old name mapping for this OID
108            self.name_index.retain(|_, v| v != oid);
109            // Insert the current name
110            self.name_index.insert(obj.object_name().to_string(), *oid);
111        }
112    }
113
114    /// Get a shared reference to an object by identifier.
115    pub fn get(&self, oid: &ObjectIdentifier) -> Option<&dyn BACnetObject> {
116        self.objects.get(oid).map(|o| o.as_ref())
117    }
118
119    /// Get a mutable reference to an object by identifier.
120    pub fn get_mut(&mut self, oid: &ObjectIdentifier) -> Option<&mut Box<dyn BACnetObject>> {
121        self.objects.get_mut(oid)
122    }
123
124    /// Remove an object by identifier.
125    pub fn remove(&mut self, oid: &ObjectIdentifier) -> Option<Box<dyn BACnetObject>> {
126        if let Some(obj) = self.objects.remove(oid) {
127            self.name_index.remove(obj.object_name());
128            if let Some(type_set) = self.type_index.get_mut(&oid.object_type()) {
129                type_set.retain(|o| o != oid);
130            }
131            Some(obj)
132        } else {
133            None
134        }
135    }
136
137    /// List all object identifiers in the database.
138    pub fn list_objects(&self) -> Vec<ObjectIdentifier> {
139        self.objects.keys().copied().collect()
140    }
141
142    /// Find all objects of a given type.
143    ///
144    /// Returns a `Vec` of `ObjectIdentifier`s whose object type matches `object_type`.
145    /// Useful for WhoHas, object enumeration, and similar queries.
146    pub fn find_by_type(&self, object_type: ObjectType) -> Vec<ObjectIdentifier> {
147        self.type_index
148            .get(&object_type)
149            .cloned()
150            .unwrap_or_default()
151    }
152
153    /// Iterate over all `(ObjectIdentifier, &dyn BACnetObject)` pairs.
154    ///
155    /// Avoids the double-lookup pattern of `list_objects()` followed by `get()`.
156    pub fn iter_objects(&self) -> impl Iterator<Item = (ObjectIdentifier, &dyn BACnetObject)> {
157        self.objects.iter().map(|(&oid, obj)| (oid, obj.as_ref()))
158    }
159
160    /// Number of objects in the database.
161    pub fn len(&self) -> usize {
162        self.objects.len()
163    }
164
165    /// Whether the database is empty.
166    pub fn is_empty(&self) -> bool {
167        self.objects.is_empty()
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use std::borrow::Cow;
174
175    use super::*;
176    use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
177    use bacnet_types::error::Error;
178    use bacnet_types::primitives::PropertyValue;
179
180    /// Minimal test object.
181    struct TestObject {
182        oid: ObjectIdentifier,
183        name: String,
184    }
185
186    impl BACnetObject for TestObject {
187        fn object_identifier(&self) -> ObjectIdentifier {
188            self.oid
189        }
190
191        fn object_name(&self) -> &str {
192            &self.name
193        }
194
195        fn read_property(
196            &self,
197            property: PropertyIdentifier,
198            _array_index: Option<u32>,
199        ) -> Result<PropertyValue, Error> {
200            if property == PropertyIdentifier::OBJECT_NAME {
201                Ok(PropertyValue::CharacterString(self.name.clone()))
202            } else {
203                Err(Error::Protocol {
204                    class: ErrorClass::PROPERTY.to_raw() as u32,
205                    code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
206                })
207            }
208        }
209
210        fn write_property(
211            &mut self,
212            _property: PropertyIdentifier,
213            _array_index: Option<u32>,
214            _value: PropertyValue,
215            _priority: Option<u8>,
216        ) -> Result<(), Error> {
217            Err(Error::Protocol {
218                class: ErrorClass::PROPERTY.to_raw() as u32,
219                code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
220            })
221        }
222
223        fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
224            Cow::Borrowed(&[PropertyIdentifier::OBJECT_NAME])
225        }
226    }
227
228    fn make_test_object(instance: u32) -> Box<dyn BACnetObject> {
229        Box::new(TestObject {
230            oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, instance).unwrap(),
231            name: format!("AI-{instance}"),
232        })
233    }
234
235    fn make_test_object_typed(
236        object_type: ObjectType,
237        instance: u32,
238        name: &str,
239    ) -> Box<dyn BACnetObject> {
240        Box::new(TestObject {
241            oid: ObjectIdentifier::new(object_type, instance).unwrap(),
242            name: name.to_string(),
243        })
244    }
245
246    #[test]
247    fn add_and_get() {
248        let mut db = ObjectDatabase::new();
249        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
250        db.add(make_test_object(1)).unwrap();
251        assert_eq!(db.len(), 1);
252
253        let obj = db.get(&oid).unwrap();
254        assert_eq!(obj.object_name(), "AI-1");
255    }
256
257    #[test]
258    fn get_nonexistent_returns_none() {
259        let db = ObjectDatabase::new();
260        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 99).unwrap();
261        assert!(db.get(&oid).is_none());
262    }
263
264    #[test]
265    fn read_property_via_database() {
266        let mut db = ObjectDatabase::new();
267        db.add(make_test_object(1)).unwrap();
268        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
269        let obj = db.get(&oid).unwrap();
270        let val = obj
271            .read_property(PropertyIdentifier::OBJECT_NAME, None)
272            .unwrap();
273        assert_eq!(val, PropertyValue::CharacterString("AI-1".into()));
274    }
275
276    #[test]
277    fn remove_object() {
278        let mut db = ObjectDatabase::new();
279        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
280        db.add(make_test_object(1)).unwrap();
281        assert_eq!(db.len(), 1);
282        let removed = db.remove(&oid);
283        assert!(removed.is_some());
284        assert_eq!(db.len(), 0);
285    }
286
287    #[test]
288    fn list_objects() {
289        let mut db = ObjectDatabase::new();
290        db.add(make_test_object(1)).unwrap();
291        db.add(make_test_object(2)).unwrap();
292        let oids = db.list_objects();
293        assert_eq!(oids.len(), 2);
294    }
295
296    #[test]
297    fn find_by_type_returns_matching_objects() {
298        let mut db = ObjectDatabase::new();
299        db.add(make_test_object_typed(ObjectType::ANALOG_INPUT, 1, "AI-1"))
300            .unwrap();
301        db.add(make_test_object_typed(ObjectType::ANALOG_INPUT, 2, "AI-2"))
302            .unwrap();
303        db.add(make_test_object_typed(ObjectType::BINARY_INPUT, 1, "BI-1"))
304            .unwrap();
305        db.add(make_test_object_typed(ObjectType::ANALOG_OUTPUT, 1, "AO-1"))
306            .unwrap();
307
308        let ai_oids = db.find_by_type(ObjectType::ANALOG_INPUT);
309        assert_eq!(ai_oids.len(), 2);
310        for oid in &ai_oids {
311            assert_eq!(oid.object_type(), ObjectType::ANALOG_INPUT);
312        }
313
314        let bi_oids = db.find_by_type(ObjectType::BINARY_INPUT);
315        assert_eq!(bi_oids.len(), 1);
316        assert_eq!(bi_oids[0].object_type(), ObjectType::BINARY_INPUT);
317        assert_eq!(bi_oids[0].instance_number(), 1);
318
319        let ao_oids = db.find_by_type(ObjectType::ANALOG_OUTPUT);
320        assert_eq!(ao_oids.len(), 1);
321    }
322
323    #[test]
324    fn find_by_type_returns_empty_for_no_matches() {
325        let mut db = ObjectDatabase::new();
326        db.add(make_test_object_typed(ObjectType::ANALOG_INPUT, 1, "AI-1"))
327            .unwrap();
328
329        let results = db.find_by_type(ObjectType::BINARY_VALUE);
330        assert!(results.is_empty());
331    }
332
333    #[test]
334    fn find_by_type_on_empty_database() {
335        let db = ObjectDatabase::new();
336        let results = db.find_by_type(ObjectType::ANALOG_INPUT);
337        assert!(results.is_empty());
338    }
339
340    #[test]
341    fn iter_objects_yields_all_entries() {
342        let mut db = ObjectDatabase::new();
343        db.add(make_test_object_typed(ObjectType::ANALOG_INPUT, 1, "AI-1"))
344            .unwrap();
345        db.add(make_test_object_typed(ObjectType::BINARY_INPUT, 1, "BI-1"))
346            .unwrap();
347
348        let items: Vec<_> = db.iter_objects().collect();
349        assert_eq!(items.len(), 2);
350
351        // Verify we can access object data without a second lookup
352        for (oid, obj) in &items {
353            assert_eq!(oid.object_type(), obj.object_identifier().object_type());
354            assert!(!obj.object_name().is_empty());
355        }
356    }
357
358    #[test]
359    fn iter_objects_on_empty_database() {
360        let db = ObjectDatabase::new();
361        assert_eq!(db.iter_objects().count(), 0);
362    }
363
364    #[test]
365    fn duplicate_name_rejected() {
366        let mut db = ObjectDatabase::new();
367        db.add(make_test_object_typed(
368            ObjectType::ANALOG_INPUT,
369            1,
370            "Sensor",
371        ))
372        .unwrap();
373        // Different OID, same name → must fail
374        let result = db.add(make_test_object_typed(
375            ObjectType::ANALOG_INPUT,
376            2,
377            "Sensor",
378        ));
379        assert!(result.is_err());
380        assert_eq!(db.len(), 1); // original still there
381    }
382
383    #[test]
384    fn replace_same_oid_allowed() {
385        let mut db = ObjectDatabase::new();
386        db.add(make_test_object_typed(
387            ObjectType::ANALOG_INPUT,
388            1,
389            "Sensor",
390        ))
391        .unwrap();
392        // Same OID, same or different name → allowed (replacement)
393        db.add(make_test_object_typed(
394            ObjectType::ANALOG_INPUT,
395            1,
396            "Sensor-v2",
397        ))
398        .unwrap();
399        assert_eq!(db.len(), 1);
400        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
401        assert_eq!(db.get(&oid).unwrap().object_name(), "Sensor-v2");
402    }
403
404    #[test]
405    fn find_by_name_works() {
406        let mut db = ObjectDatabase::new();
407        db.add(make_test_object_typed(ObjectType::ANALOG_INPUT, 1, "Temp"))
408            .unwrap();
409        db.add(make_test_object_typed(ObjectType::BINARY_INPUT, 1, "Alarm"))
410            .unwrap();
411
412        let obj = db.find_by_name("Temp").unwrap();
413        assert_eq!(obj.object_identifier().instance_number(), 1);
414        assert_eq!(
415            obj.object_identifier().object_type(),
416            ObjectType::ANALOG_INPUT
417        );
418
419        assert!(db.find_by_name("NonExistent").is_none());
420    }
421
422    #[test]
423    fn remove_frees_name() {
424        let mut db = ObjectDatabase::new();
425        let oid = ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap();
426        db.add(make_test_object_typed(
427            ObjectType::ANALOG_INPUT,
428            1,
429            "Sensor",
430        ))
431        .unwrap();
432        db.remove(&oid);
433        // Name should now be available for a different object
434        db.add(make_test_object_typed(
435            ObjectType::ANALOG_INPUT,
436            2,
437            "Sensor",
438        ))
439        .unwrap();
440        assert_eq!(db.len(), 1);
441    }
442}