aws_iot_device_sdk/
shadow.rs

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/// A shadow topic string takes one of the two forms,
12/// in the case of an unnamed ("Classic") shadow.
13/// Or, in the case of a named shadow
14/// The shadow_name part is None when unnamed shadow.
15#[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/// Each of these values describes the type of a shadow message.
23/// https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html
24#[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
39/// Assemble shadow topic string when Thing Name or Shadow Name is only known at run time.
40///
41/// # Example
42/// ```
43/// use aws_iot_device_sdk::shadow::Topic::*;
44/// use aws_iot_device_sdk::{shadow};
45/// use arrayvec::{ArrayString, ArrayVec};
46///
47/// let topic = shadow::assemble_topic(shadow::Topic::Get, "chloe", None).unwrap();
48/// assert_eq!("$aws/things/chloe/shadow/get", topic.as_str())
49/// ```
50
51pub 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        // Classic shadow topic
62        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        // Named shadow topic
69        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
98/// Given the topic string of an incoming message, determine whether it is
99/// related to a device shadow;
100///
101/// If it is, return information about the type of device shadow message,
102/// and the Thing Name and Shadow Name inside of the topic string.
103///
104/// # Example
105/// ```
106/// use aws_iot_device_sdk::shadow::Topic::*;
107/// use aws_iot_device_sdk::{shadow};
108///
109/// let topic = "$aws/things/chloe/shadow/name/common/update";
110/// let shadow = shadow::match_topic(topic).unwrap();
111///
112/// assert_eq!(shadow.thing_name, "chloe");
113/// assert_eq!(shadow.shadow_name.unwrap(), "common");
114/// assert_eq!(shadow.shadow_op, shadow::Topic::Update);
115///
116/// let topic = "$aws/things/chloe/shadow/name/common/update/delta";
117/// let shadow = shadow::match_topic(topic).unwrap();
118///
119/// assert_eq!(shadow.thing_name, "chloe");
120/// assert_eq!(shadow.shadow_name.unwrap(), "common");
121/// assert_eq!(shadow.shadow_op, shadow::Topic::UpdateDelta);
122/// ```
123pub 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 topic
137        [_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 topic without suffix
146        [_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        // Classic shadow topic
155        [op, suffix] => Ok(ThingShadow {
156            thing_name,
157            shadow_name: None,
158            shadow_op: find_message_type(op, Some(suffix))?,
159        }),
160        // Classic shadow topic without suffix
161        [op] => Ok(ThingShadow {
162            thing_name,
163            shadow_name: None,
164            shadow_op: find_message_type(op, None)?,
165        }),
166        // Not shadow topic
167        _ => 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}