1use crate::common::*;
2use arrayvec::{ArrayString, ArrayVec};
3
4use self::Topic::*;
5
6const OP_GET: &str = "get";
7const OP_DELETE: &str = "delete";
8const OP_UPDATE: &str = "update";
9const SUFFIX_DOCUMENTS: &str = "/documents";
10const SUFFIX_DELTA: &str = "/delta";
11#[derive(Debug)]
16pub struct ThingShadow<'a> {
17 pub thing_name: &'a str,
18 pub shadow_name: Option<&'a str>,
19 pub shadow_op: Topic,
20}
21
22#[derive(Debug, PartialEq)]
25pub enum Topic {
26 Get = 0,
27 GetAccepted,
28 GetRejected,
29 Delete,
30 DeleteAccepted,
31 DeleteRejected,
32 Update,
33 UpdateAccepted,
34 UpdateRejected,
35 UpdateDocuments,
36 UpdateDelta,
37}
38
39pub fn assemble_topic(
52 topic_type: Topic,
53 thing_name: &str,
54 named: Option<&str>,
55) -> Result<ArrayString<SHADOW_TOPIC_MAX_LENGTH>, Error> {
56 is_valid_thing_name(thing_name)?;
57 let mut s = ArrayString::<SHADOW_TOPIC_MAX_LENGTH>::new();
58 s.push_str(AWS_THINGS_PREFIX);
59 s.push_str(thing_name);
60 match named {
61 None => {
63 s.push_str(SHADOW_API_BRIDGE);
64 s.push_str(op(&topic_type));
65 s.push_str(suffix(&topic_type));
66 Ok(s)
67 }
68 Some(shadow_name) => {
70 is_valid_shadow_name(shadow_name)?;
71 s.push_str(NAMED_SHADOW_API_BRIDGE);
72 s.push_str(shadow_name);
73 s.push_str("/");
74 s.push_str(op(&topic_type));
75 s.push_str(suffix(&topic_type));
76 Ok(s)
77 }
78 }
79}
80
81fn op(topic_type: &Topic) -> &str {
82 match topic_type {
83 Get | GetAccepted | GetRejected => OP_GET,
84 Delete | DeleteAccepted | DeleteRejected => OP_DELETE,
85 Update | UpdateAccepted | UpdateRejected | UpdateDocuments | UpdateDelta => OP_UPDATE,
86 }
87}
88fn suffix(topic_type: &Topic) -> &str {
89 match topic_type {
90 GetAccepted | DeleteAccepted | UpdateAccepted => SUFFIX_ACCEPTED,
91 GetRejected | DeleteRejected | UpdateRejected => SUFFIX_REJECTED,
92 UpdateDocuments => SUFFIX_DOCUMENTS,
93 UpdateDelta => SUFFIX_DELTA,
94 _ => "",
95 }
96}
97
98pub fn match_topic<'a>(topic: &'a str) -> Result<ThingShadow, Error> {
124 is_valid_mqtt_topic(topic)?;
125
126 let s = is_valid_prefix(topic, AWS_THINGS_PREFIX)?;
127
128 let mid = s.find('/').ok_or(Error::NoMatch);
129 let (thing_name, s) = s.split_at(mid?);
130 is_valid_thing_name(thing_name)?;
131
132 let s = is_valid_bridge(s, SHADOW_API_BRIDGE)?;
133
134 let v: ArrayVec<&str, 16> = s.split('/').collect();
135 match v[..] {
136 [_named, shadow_name, op, suffix] => {
138 is_valid_shadow_name(shadow_name)?;
139 Ok(ThingShadow {
140 thing_name,
141 shadow_name: Some(shadow_name),
142 shadow_op: find_message_type(op, Some(suffix))?,
143 })
144 }
145 [_named, shadow_name, op] => {
147 is_valid_shadow_name(shadow_name)?;
148 Ok(ThingShadow {
149 thing_name,
150 shadow_name: Some(shadow_name),
151 shadow_op: find_message_type(op, None)?,
152 })
153 }
154 [op, suffix] => Ok(ThingShadow {
156 thing_name,
157 shadow_name: None,
158 shadow_op: find_message_type(op, Some(suffix))?,
159 }),
160 [op] => Ok(ThingShadow {
162 thing_name,
163 shadow_name: None,
164 shadow_op: find_message_type(op, None)?,
165 }),
166 _ => Err(Error::NoMatch),
168 }
169}
170
171fn find_message_type(op: &str, suffix: Option<&str>) -> Result<Topic, Error> {
172 match (op, suffix) {
173 ("get", None) => Ok(Get),
174 ("get", Some("accepted")) => Ok(GetAccepted),
175 ("get", Some("rejected")) => Ok(GetRejected),
176 ("delete", None) => Ok(Delete),
177 ("delete", Some("accepted")) => Ok(DeleteAccepted),
178 ("delete", Some("rejected")) => Ok(DeleteRejected),
179 ("update", None) => Ok(Update),
180 ("update", Some("accepted")) => Ok(UpdateAccepted),
181 ("update", Some("rejected")) => Ok(UpdateRejected),
182 ("update", Some("documents")) => Ok(UpdateDocuments),
183 ("update", Some("delta")) => Ok(UpdateDelta),
184 _ => Err(Error::MessageTypeParseFailed),
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use crate::shadow;
191 #[test]
192 fn assemble_named_topic_string() {
193 let topic = shadow::assemble_topic(shadow::Topic::Get, "chloe", Some("common")).unwrap();
194 assert_eq!("$aws/things/chloe/shadow/name/common/get", topic.as_str());
195 }
196 #[test]
197 fn assemble_classic_topic_string() {
198 let topic = shadow::assemble_topic(shadow::Topic::Get, "chloe", None).unwrap();
199 assert_eq!("$aws/things/chloe/shadow/get", topic.as_str());
200 }
201 #[test]
202 fn assemble_classic_topic_string_suffix() {
203 let topic = shadow::assemble_topic(shadow::Topic::GetAccepted, "chloe", None).unwrap();
204 assert_eq!("$aws/things/chloe/shadow/get/accepted", topic.as_str());
205 }
206 #[test]
207 fn match_classic_shadow_topic_string() {
208 let topic = "$aws/things/chloe/shadow/get/accepted";
209 let shadow = shadow::match_topic(topic).unwrap();
210 assert_eq!(shadow.thing_name, "chloe");
211 assert_eq!(shadow.shadow_name, None);
212 assert_eq!(shadow.shadow_op, shadow::Topic::GetAccepted);
213 }
214 #[test]
215 fn match_named_shadow_topic_string() {
216 let topic = "$aws/things/chloe/shadow/name/common/get/rejected";
217 let shadow = shadow::match_topic(topic).unwrap();
218 assert_eq!(shadow.thing_name, "chloe");
219 assert_eq!(shadow.shadow_name.unwrap(), "common");
220 assert_eq!(shadow.shadow_op, shadow::Topic::GetRejected);
221 }
222}