1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4use crate::errors::{CoreError, Result};
5
6#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
7#[serde(transparent)]
8pub struct EntityPath(String);
9
10impl EntityPath {
11 pub fn new(path: impl Into<String>) -> Result<Self> {
12 let s: String = path.into();
13 if !s.starts_with('/') {
14 return Err(CoreError::InvalidEntityPath {
15 path: s,
16 reason: "must start with '/'",
17 });
18 }
19 if s.len() == 1 {
20 return Err(CoreError::InvalidEntityPath {
21 path: s,
22 reason: "must contain at least one path segment",
23 });
24 }
25 if s.contains("//") {
26 return Err(CoreError::InvalidEntityPath {
27 path: s,
28 reason: "empty segments are not allowed",
29 });
30 }
31 Ok(Self(s))
32 }
33
34 pub fn as_str(&self) -> &str {
35 &self.0
36 }
37}
38
39impl fmt::Display for EntityPath {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.write_str(&self.0)
42 }
43}
44
45#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
46#[serde(transparent)]
47pub struct Timeline(String);
48
49impl Timeline {
50 pub const WALL: &'static str = "wall_time";
51 pub const SIM: &'static str = "sim_time";
52
53 pub fn new(name: impl Into<String>) -> Self {
54 Self(name.into())
55 }
56
57 pub fn wall() -> Self {
58 Self(Self::WALL.to_string())
59 }
60
61 pub fn sim() -> Self {
62 Self(Self::SIM.to_string())
63 }
64
65 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68}
69
70impl fmt::Display for Timeline {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 f.write_str(&self.0)
73 }
74}
75
76#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
77#[serde(transparent)]
78pub struct ComponentName(String);
79
80impl ComponentName {
81 pub fn new(name: impl Into<String>) -> Self {
82 Self(name.into())
83 }
84
85 pub fn as_str(&self) -> &str {
86 &self.0
87 }
88}
89
90impl fmt::Display for ComponentName {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 f.write_str(&self.0)
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn entity_path_requires_leading_slash() {
102 assert!(EntityPath::new("robot/base").is_err());
103 assert!(EntityPath::new("/").is_err());
104 assert!(EntityPath::new("/robot//base").is_err());
105 assert!(EntityPath::new("/robot/base").is_ok());
106 }
107
108 #[test]
109 fn entity_path_sorts_lexicographically() {
110 let mut paths = [
111 EntityPath::new("/robot/10").unwrap(),
112 EntityPath::new("/robot/2").unwrap(),
113 EntityPath::new("/agent/1").unwrap(),
114 ];
115 paths.sort();
116 assert_eq!(paths[0].as_str(), "/agent/1");
117 assert_eq!(paths[1].as_str(), "/robot/10");
118 assert_eq!(paths[2].as_str(), "/robot/2");
119 }
120}