Skip to main content

tp_lib_core/models/
netrelation.rs

1//! Network topology connection model
2
3use crate::errors::ProjectionError;
4use serde::{Deserialize, Serialize};
5
6/// Represents a navigability connection between two track segments
7///
8/// A NetRelation defines whether trains can travel from one netelement to another.
9/// Navigability may be unidirectional (e.g., one-way track) or bidirectional.
10///
11/// # Examples
12///
13/// ```
14/// use tp_lib_core::NetRelation;
15///
16/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
17/// // Bidirectional connection: trains can go from A to B and from B to A
18/// let relation = NetRelation::new(
19///     "NR001".to_string(),
20///     "NE_A".to_string(),
21///     "NE_B".to_string(),
22///     1,  // position_on_a: end of A
23///     0,  // position_on_b: start of B
24///     true,   // A → B allowed
25///     true,   // B → A allowed
26/// )?;
27///
28/// assert!(relation.is_bidirectional());
29/// # Ok(())
30/// # }
31/// ```
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub struct NetRelation {
34    /// Unique identifier for this netrelation
35    pub id: String,
36
37    /// ID of the source netelement (starting track segment)
38    pub from_netelement_id: String,
39
40    /// ID of the target netelement (destination track segment)
41    pub to_netelement_id: String,
42
43    /// Position on netelementA where the connection applies (0 = start, 1 = end)
44    pub position_on_a: u8,
45
46    /// Position on netelementB where the connection applies (0 = start, 1 = end)
47    pub position_on_b: u8,
48
49    /// Whether trains can navigate forward (from → to)
50    pub navigable_forward: bool,
51
52    /// Whether trains can navigate backward (to → from)
53    pub navigable_backward: bool,
54}
55
56impl NetRelation {
57    /// Create a new netrelation with validation
58    pub fn new(
59        id: String,
60        from_netelement_id: String,
61        to_netelement_id: String,
62        position_on_a: u8,
63        position_on_b: u8,
64        navigable_forward: bool,
65        navigable_backward: bool,
66    ) -> Result<Self, ProjectionError> {
67        let relation = Self {
68            id,
69            from_netelement_id,
70            to_netelement_id,
71            position_on_a,
72            position_on_b,
73            navigable_forward,
74            navigable_backward,
75        };
76
77        relation.validate()?;
78        Ok(relation)
79    }
80
81    /// Validate netrelation fields
82    pub fn validate(&self) -> Result<(), ProjectionError> {
83        // ID must be non-empty
84        if self.id.is_empty() {
85            return Err(ProjectionError::InvalidNetRelation(
86                "NetRelation ID must not be empty".to_string(),
87            ));
88        }
89
90        // Netelement IDs must be non-empty
91        if self.from_netelement_id.is_empty() {
92            return Err(ProjectionError::InvalidNetRelation(
93                "from_netelement_id must not be empty".to_string(),
94            ));
95        }
96
97        if self.to_netelement_id.is_empty() {
98            return Err(ProjectionError::InvalidNetRelation(
99                "to_netelement_id must not be empty".to_string(),
100            ));
101        }
102
103        // Position values must be 0 or 1
104        if self.position_on_a > 1 {
105            return Err(ProjectionError::InvalidNetRelation(format!(
106                "position_on_a must be 0 or 1, got {}",
107                self.position_on_a
108            )));
109        }
110
111        if self.position_on_b > 1 {
112            return Err(ProjectionError::InvalidNetRelation(format!(
113                "position_on_b must be 0 or 1, got {}",
114                self.position_on_b
115            )));
116        }
117
118        // Cannot connect to itself
119        if self.from_netelement_id == self.to_netelement_id {
120            return Err(ProjectionError::InvalidNetRelation(format!(
121                "NetRelation cannot connect netelement to itself: {}",
122                self.from_netelement_id
123            )));
124        }
125
126        Ok(())
127    }
128
129    /// Check if navigation is allowed in forward direction (from → to)
130    pub fn is_navigable_forward(&self) -> bool {
131        self.navigable_forward
132    }
133
134    /// Check if navigation is allowed in backward direction (to → from)
135    pub fn is_navigable_backward(&self) -> bool {
136        self.navigable_backward
137    }
138
139    /// Check if bidirectional (both directions navigable)
140    pub fn is_bidirectional(&self) -> bool {
141        self.navigable_forward && self.navigable_backward
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_valid_bidirectional() {
151        let relation = NetRelation::new(
152            "NR001".to_string(),
153            "NE_A".to_string(),
154            "NE_B".to_string(),
155            1,
156            0,
157            true,
158            true,
159        );
160
161        assert!(relation.is_ok());
162        let rel = relation.unwrap();
163        assert!(rel.is_bidirectional());
164        assert!(rel.is_navigable_forward());
165        assert!(rel.is_navigable_backward());
166    }
167
168    #[test]
169    fn test_valid_unidirectional() {
170        let relation = NetRelation::new(
171            "NR002".to_string(),
172            "NE_A".to_string(),
173            "NE_B".to_string(),
174            1,
175            0,
176            true,
177            false,
178        );
179
180        assert!(relation.is_ok());
181        let rel = relation.unwrap();
182        assert!(!rel.is_bidirectional());
183        assert!(rel.is_navigable_forward());
184        assert!(!rel.is_navigable_backward());
185    }
186
187    #[test]
188    fn test_invalid_position_on_a() {
189        let relation = NetRelation::new(
190            "NR003".to_string(),
191            "NE_A".to_string(),
192            "NE_B".to_string(),
193            2, // Invalid: must be 0 or 1
194            0,
195            true,
196            false,
197        );
198
199        assert!(relation.is_err());
200    }
201
202    #[test]
203    fn test_invalid_position_on_b() {
204        let relation = NetRelation::new(
205            "NR004".to_string(),
206            "NE_A".to_string(),
207            "NE_B".to_string(),
208            1,
209            5, // Invalid: must be 0 or 1
210            true,
211            false,
212        );
213
214        assert!(relation.is_err());
215    }
216
217    #[test]
218    fn test_self_reference() {
219        let relation = NetRelation::new(
220            "NR005".to_string(),
221            "NE_A".to_string(),
222            "NE_A".to_string(), // Invalid: same as from_netelement_id
223            1,
224            0,
225            true,
226            false,
227        );
228
229        assert!(relation.is_err());
230    }
231
232    #[test]
233    fn test_empty_id() {
234        let relation = NetRelation::new(
235            "".to_string(), // Invalid
236            "NE_A".to_string(),
237            "NE_B".to_string(),
238            1,
239            0,
240            true,
241            false,
242        );
243
244        assert!(relation.is_err());
245    }
246
247    #[test]
248    fn test_empty_from_id() {
249        let relation = NetRelation::new(
250            "NR006".to_string(),
251            "".to_string(), // Invalid
252            "NE_B".to_string(),
253            1,
254            0,
255            true,
256            false,
257        );
258
259        assert!(relation.is_err());
260    }
261
262    #[test]
263    fn test_empty_to_id() {
264        let relation = NetRelation::new(
265            "NR007".to_string(),
266            "NE_A".to_string(),
267            "".to_string(), // Invalid
268            1,
269            0,
270            true,
271            false,
272        );
273
274        assert!(relation.is_err());
275    }
276}