Skip to main content

tanuki_common/
lib.rs

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}