Skip to main content

cobre_core/
entity_id.rs

1//! Strongly-typed entity identifier used across all entity collections.
2//!
3//! [`EntityId`] wraps an `i32` (the type used in JSON input schemas) and
4//! prevents accidental confusion between entity IDs and collection indices.
5
6use core::fmt;
7
8/// Strongly-typed entity identifier.
9///
10/// Wraps the `i32` identifier from JSON input files. The newtype pattern prevents
11/// accidental confusion between entity IDs and collection indices (`usize`), which
12/// is a common source of bugs in systems with both ID-based lookup and index-based
13/// access. `EntityId` is used as the key in `HashMap<EntityId, usize>` lookup tables
14/// and as the value in cross-reference fields (e.g., `Hydro::bus_id`, `Line::source_bus_id`).
15///
16/// Why `i32` and not `String`: All JSON entity schemas use integer IDs (`i32`). Integer
17/// keys are cheaper to hash, compare, and copy than strings — important because
18/// `EntityId` appears in every lookup table and cross-reference field. If a future
19/// input format requires string IDs, the newtype boundary isolates the change to
20/// `EntityId`'s internal representation and its `From`/`Into` impls.
21///
22/// # Examples
23///
24/// ```
25/// use cobre_core::EntityId;
26///
27/// let id: EntityId = EntityId::from(42);
28/// assert_eq!(id.to_string(), "42");
29///
30/// let raw: i32 = i32::from(id);
31/// assert_eq!(raw, 42);
32/// ```
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
36pub struct EntityId(pub i32);
37
38impl fmt::Display for EntityId {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(f, "{}", self.0)
41    }
42}
43
44impl From<i32> for EntityId {
45    fn from(value: i32) -> Self {
46        Self(value)
47    }
48}
49
50impl From<EntityId> for i32 {
51    fn from(id: EntityId) -> Self {
52        id.0
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use core::hash::{Hash, Hasher};
59    use std::collections::hash_map::DefaultHasher;
60
61    use super::EntityId;
62
63    #[test]
64    fn test_equality() {
65        let a = EntityId(1);
66        let b = EntityId(1);
67        let c = EntityId(2);
68        assert_eq!(a, b);
69        assert_ne!(a, c);
70    }
71
72    #[test]
73    fn test_copy() {
74        let a = EntityId(10);
75        let b = a;
76        assert_eq!(a, b);
77        assert_eq!(a.0, 10);
78    }
79
80    #[test]
81    fn test_hash_consistency() {
82        let mut hasher_a = DefaultHasher::new();
83        EntityId(99).hash(&mut hasher_a);
84        let hash_a = hasher_a.finish();
85
86        let mut hasher_b = DefaultHasher::new();
87        EntityId(99).hash(&mut hasher_b);
88        let hash_b = hasher_b.finish();
89
90        assert_eq!(hash_a, hash_b);
91    }
92
93    #[test]
94    fn test_display() {
95        assert_eq!(EntityId(42).to_string(), "42");
96        assert_eq!(EntityId(0).to_string(), "0");
97        assert_eq!(EntityId(-1).to_string(), "-1");
98    }
99
100    #[test]
101    fn test_from_i32() {
102        let id = EntityId::from(5);
103        assert_eq!(id, EntityId(5));
104    }
105
106    #[test]
107    fn test_into_i32() {
108        let raw: i32 = i32::from(EntityId(7));
109        assert_eq!(raw, 7);
110    }
111
112    #[cfg(feature = "serde")]
113    #[test]
114    fn test_entity_id_serde_roundtrip() {
115        let id = EntityId(42);
116        let json = serde_json::to_string(&id).unwrap();
117        let deserialized: EntityId = serde_json::from_str(&json).unwrap();
118        assert_eq!(id, deserialized);
119    }
120}