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