1#![cfg_attr(not(test), no_std)]
2#![feature(macro_attr, macro_derive, str_split_remainder)]
3
4extern crate alloc;
5
6use core::{fmt::Display, str::FromStr};
7
8use compact_str::{CompactString, ToCompactString};
9use serde::{Deserialize, Serialize};
10
11pub mod capabilities;
12pub mod macros;
13pub mod meta;
14
15#[doc(hidden)]
16pub use serde as _serde;
17
18mod property;
19pub use property::*;
20
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct EntityId(pub CompactString);
24
25impl EntityId {
26 pub const WILDCARD: Self = EntityId(CompactString::const_new("+"));
27
28 pub fn as_str(&self) -> &str {
29 &self.0
30 }
31}
32
33impl<T: AsRef<str>> From<T> for EntityId {
34 fn from(value: T) -> Self {
35 EntityId(value.as_ref().to_compact_string())
36 }
37}
38
39impl Display for EntityId {
40 fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
41 write!(f, "{}", self.0)
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum Topic {
47 EntityMeta {
48 entity: EntityId,
49 key: CompactString,
50 },
51 CapabilityMeta {
52 entity: EntityId,
53 capability: CompactString,
54 key: CompactString,
55 },
56 CapabilityData {
57 entity: EntityId,
58 capability: CompactString,
59 rest: CompactString,
60 },
61}
62
63impl Topic {
64 pub const CAPABILITY_DATA_WILDCARD: Self = Self::CapabilityData {
65 entity: EntityId::WILDCARD,
66 capability: CompactString::const_new("+"),
67 rest: CompactString::const_new("#"),
68 };
69}
70
71impl Display for Topic {
72 fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result {
73 match self {
74 Topic::EntityMeta { entity, key } => {
75 write!(f, "tanuki/entities/{}/$meta/{}", entity, key)
76 }
77 Topic::CapabilityMeta { entity, capability, key } => {
78 write!(f, "tanuki/entities/{}/{}/$meta/{}", entity, capability, key)
79 }
80 Topic::CapabilityData { entity, capability, rest } => {
81 write!(f, "tanuki/entities/{}/{}/{}", entity, capability, rest)
82 }
83 }
84 }
85}
86
87impl FromStr for Topic {
88 type Err = &'static str;
89
90 fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
91 let mut parts = s.split('/');
92 if parts.next() != Some("tanuki") {
93 return Err("does not start with tanuki/");
94 }
95
96 match parts.next() {
97 Some("entities") => match parts.next() {
98 Some(entity) => match parts.next() {
99 Some("$meta") => match parts.next() {
100 Some(key) if parts.next().is_none() => Ok(Topic::EntityMeta {
101 entity: EntityId::from(entity),
102 key: key.to_compact_string(),
103 }),
104 Some(_) => Err("tanuki/entities/{id}/$meta/{key}/..."),
105 _ => Err("tanuki/entities/{id}/$meta"),
106 },
107 Some(capability) => match parts.next() {
108 Some("$meta") => match parts.next() {
109 Some(key) if parts.next().is_none() => Ok(Topic::CapabilityMeta {
110 entity: EntityId::from(entity),
111 capability: capability.to_compact_string(),
112 key: key.to_compact_string(),
113 }),
114 Some(_) => Err("tanuki/entities/{id}/{cap}/$meta/{key}/..."),
115 _ => Err("tanuki/entities/{id}/{cap}/$meta"),
116 },
117 Some(rest) => Ok(Topic::CapabilityData {
118 entity: EntityId::from(entity),
119 capability: capability.to_compact_string(),
120 rest: match parts.remainder() {
121 Some(remainder) => rest.to_compact_string() + "/" + remainder,
122 None => rest.to_compact_string(),
123 },
124 }),
125 None => Err("tanuki/entities/{id}/{cap}"),
126 },
127 None => Err("tanuki/entities/{id}"),
128 },
129 None => Err("tanuki/entities"),
130 },
131 Some(_) => Err("tanuki/..."),
132 None => Err("tanuki"),
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn entity_id_serde() {
143 assert_eq!(
144 serde_json::to_string(&EntityId::from("test.entity")).unwrap(),
145 r#""test.entity""#
146 );
147
148 assert_eq!(
149 serde_json::from_str::<EntityId>(r#""test.entity""#).unwrap(),
150 EntityId::from("test.entity")
151 );
152 }
153
154 #[test]
155 fn topic_display() {
156 assert_eq!(
157 Topic::EntityMeta {
158 entity: EntityId::from("sensor.temperature"),
159 key: "status".to_compact_string(),
160 }
161 .to_string(),
162 "tanuki/entities/sensor.temperature/$meta/status"
163 );
164
165 assert_eq!(
166 Topic::CapabilityMeta {
167 entity: EntityId::from("sensor.temperature"),
168 capability: "temperature_sensor".to_compact_string(),
169 key: "version".to_compact_string(),
170 }
171 .to_string(),
172 "tanuki/entities/sensor.temperature/temperature_sensor/$meta/version"
173 );
174
175 assert_eq!(
176 Topic::CapabilityData {
177 entity: EntityId::from("sensor.temperature"),
178 capability: "temperature_sensor".to_compact_string(),
179 rest: "current".to_compact_string(),
180 }
181 .to_string(),
182 "tanuki/entities/sensor.temperature/temperature_sensor/current"
183 );
184 }
185
186 #[test]
187 fn topic_from_str() {
188 assert_eq!(
189 "tanuki/entities/sensor.temperature/$meta/status"
190 .parse::<Topic>()
191 .unwrap(),
192 Topic::EntityMeta {
193 entity: EntityId::from("sensor.temperature"),
194 key: "status".to_compact_string(),
195 }
196 );
197
198 assert_eq!(
199 "tanuki/entities/sensor.temperature/$meta/status/extra".parse::<Topic>(),
200 Err("tanuki/entities/{id}/$meta/{key}/...")
201 );
202
203 assert_eq!(
204 "tanuki/entities/sensor.temperature/temperature_sensor/$meta/version"
205 .parse::<Topic>()
206 .unwrap(),
207 Topic::CapabilityMeta {
208 entity: EntityId::from("sensor.temperature"),
209 capability: "temperature_sensor".to_compact_string(),
210 key: "version".to_compact_string(),
211 }
212 );
213
214 assert_eq!(
215 "tanuki/entities/sensor.temperature/temperature_sensor/$meta/version/extra"
216 .parse::<Topic>(),
217 Err("tanuki/entities/{id}/{cap}/$meta/{key}/...")
218 );
219
220 assert_eq!(
221 "tanuki/entities/sensor.temperature/temperature_sensor/current"
222 .parse::<Topic>()
223 .unwrap(),
224 Topic::CapabilityData {
225 entity: EntityId::from("sensor.temperature"),
226 capability: "temperature_sensor".to_compact_string(),
227 rest: "current".to_compact_string(),
228 }
229 );
230
231 assert_eq!(
232 "tanuki/entities/sensor.temperature/temperature_sensor/current/extra"
233 .parse::<Topic>()
234 .unwrap(),
235 Topic::CapabilityData {
236 entity: EntityId::from("sensor.temperature"),
237 capability: "temperature_sensor".to_compact_string(),
238 rest: "current/extra".to_compact_string(),
239 }
240 );
241 }
242}