aws_iot_device_sdk_embedded_rust/
shadow.rs

1use crate::common::*;
2use arrayvec::{ArrayString, ArrayVec};
3
4use self::Topic::*;
5
6const API_BRIDGE: &str = "/shadow/";
7const API_BRIDGE_NAME: &str = "/shadow/name/";
8const OP_GET: &str = "get";
9const OP_DELETE: &str = "delete";
10const OP_UPDATE: &str = "update";
11const SUFFIX_DOCUMENTS: &str = "/documents";
12const SUFFIX_DELTA: &str = "/delta";
13/// A shadow topic string takes one of the two forms,
14/// in the case of an unnamed ("Classic") shadow.
15/// Or, in the case of a named shadow
16/// The shadow_name part is None when unnamed shadow.
17#[derive(Debug)]
18pub struct ThingShadow<'a> {
19    pub thing_name: &'a str,
20    pub shadow_name: Option<&'a str>,
21    pub shadow_op: Topic,
22}
23
24/// Each of these values describes the type of a shadow message.
25// https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-mqtt.html
26#[derive(Debug, PartialEq)]
27pub enum Topic {
28    Get = 0,
29    GetAccepted,
30    GetRejected,
31    Delete,
32    DeleteAccepted,
33    DeleteRejected,
34    Update,
35    UpdateAccepted,
36    UpdateRejected,
37    UpdateDocuments,
38    UpdateDelta,
39}
40
41/// Assemble shadow topic string when Thing Name or Shadow Name is only known at run time.
42///
43/// # Example
44/// ```
45/// use aws_iot_device_sdk_embedded_rust::shadow::Topic::*;
46/// use aws_iot_device_sdk_embedded_rust::{shadow};
47/// use arrayvec::{ArrayString, ArrayVec};
48///
49/// let topic = shadow::get_topic(shadow::Topic::Get, "chloe", None).unwrap();
50/// assert_eq!("$aws/things/chloe/shadow/get", topic.as_str())
51/// ```
52
53pub fn get_topic(
54    topic_type: Topic,
55    thing_name: &str,
56    named: Option<&str>,
57) -> Result<ArrayString<SHADOW_TOPIC_MAX_LENGTH>, Error> {
58    is_valid_thing_name(thing_name)?;
59    let mut s = ArrayString::<SHADOW_TOPIC_MAX_LENGTH>::new();
60    s.push_str(AWS_THINGS_PREFIX);
61    s.push_str(thing_name);
62    match named {
63        // Classic shadow topic
64        None => {
65            s.push_str(API_BRIDGE);
66            s.push_str(op(&topic_type));
67            s.push_str(suffix(&topic_type));
68            Ok(s)
69        }
70        // Named shadow topic
71        Some(shadow_name) => {
72            is_valid_shadow_name(shadow_name)?;
73            s.push_str(API_BRIDGE_NAME);
74            s.push_str(shadow_name);
75            s.push_str("/");
76            s.push_str(op(&topic_type));
77            s.push_str(suffix(&topic_type));
78            Ok(s)
79        }
80    }
81}
82
83fn op(topic_type: &Topic) -> &str {
84    match topic_type {
85        Get | GetAccepted | GetRejected => OP_GET,
86        Delete | DeleteAccepted | DeleteRejected => OP_DELETE,
87        Update | UpdateAccepted | UpdateRejected | UpdateDocuments | UpdateDelta => OP_UPDATE,
88    }
89}
90fn suffix(topic_type: &Topic) -> &str {
91    match topic_type {
92        GetAccepted | DeleteAccepted | UpdateAccepted => SUFFIX_ACCEPTED,
93        GetRejected | DeleteRejected | UpdateRejected => SUFFIX_REJECTED,
94        UpdateDocuments => SUFFIX_DOCUMENTS,
95        UpdateDelta => SUFFIX_DELTA,
96        _ => "",
97    }
98}
99
100/// Given the topic string of an incoming message, determine whether it is
101/// related to a device shadow;
102///
103/// If it is, return information about the type of device shadow message,
104/// and the Thing Name and Shadow Name inside of the topic string.
105///
106/// # Example
107/// ```
108/// use aws_iot_device_sdk_embedded_rust::shadow::Topic::*;
109/// use aws_iot_device_sdk_embedded_rust::{shadow};
110///
111/// let topic = "$aws/things/chloe/shadow/name/common/get/rejected";
112/// let shadow = shadow::match_topic(topic).unwrap();
113///
114/// assert_eq!(shadow.thing_name, "chloe");
115/// assert_eq!(shadow.shadow_name.unwrap(), "common");
116/// assert_eq!(shadow.shadow_op, shadow::Topic::GetRejected);
117/// ```
118pub fn match_topic<'a>(topic: &'a str) -> Result<ThingShadow, Error> {
119    is_valid_mqtt_topic(topic)?;
120
121    let s = is_valid_prefix(topic, AWS_THINGS_PREFIX)?;
122
123    let mid = s.find('/').ok_or(Error::NoMatch);
124    let (thing_name, s) = s.split_at(mid?);
125    is_valid_thing_name(thing_name)?;
126
127    let s = is_valid_bridge(s, API_BRIDGE)?;
128
129    let v: ArrayVec<&str, 16> = s.split('/').collect();
130    match v[..] {
131        // Named shadow topic
132        [_named, shadow_name, op, suffix] => {
133            is_valid_shadow_name(shadow_name)?;
134            Ok(ThingShadow {
135                thing_name,
136                shadow_name: Some(shadow_name),
137                shadow_op: find_message_type(op, suffix)?,
138            })
139        }
140        // Classic shadow topic
141        [op, suffix] => Ok(ThingShadow {
142            thing_name,
143            shadow_name: None,
144            shadow_op: find_message_type(op, suffix)?,
145        }),
146        // Not shadow topic
147        _ => Err(Error::NoMatch),
148    }
149}
150
151fn find_message_type(op: &str, suffix: &str) -> Result<Topic, Error> {
152    match (op, suffix) {
153        ("get", "accepted") => Ok(GetAccepted),
154        ("get", "rejected") => Ok(GetRejected),
155        ("delete", "accepted") => Ok(DeleteAccepted),
156        ("delete", "rejected") => Ok(DeleteRejected),
157        ("update", "accepted") => Ok(UpdateAccepted),
158        ("update", "rejected") => Ok(UpdateRejected),
159        ("update", "documents") => Ok(UpdateDocuments),
160        ("update", "delta") => Ok(UpdateDelta),
161        _ => Err(Error::MessageTypeParseFailed),
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use crate::shadow;
168    #[test]
169    fn assemble_named_topic_string() {
170        let topic = shadow::get_topic(shadow::Topic::Get, "chloe", Some("common")).unwrap();
171        assert_eq!("$aws/things/chloe/shadow/name/common/get", topic.as_str());
172    }
173    #[test]
174    fn assemble_classic_topic_string() {
175        let topic = shadow::get_topic(shadow::Topic::Get, "chloe", None).unwrap();
176        assert_eq!("$aws/things/chloe/shadow/get", topic.as_str());
177    }
178    #[test]
179    fn assemble_classic_topic_string_suffix() {
180        let topic = shadow::get_topic(shadow::Topic::GetAccepted, "chloe", None).unwrap();
181        assert_eq!("$aws/things/chloe/shadow/get/accepted", topic.as_str());
182    }
183    #[test]
184    fn match_classic_shadow_topic_string() {
185        let topic = "$aws/things/chloe/shadow/get/accepted";
186        let shadow = shadow::match_topic(topic).unwrap();
187        assert_eq!(shadow.thing_name, "chloe");
188        assert_eq!(shadow.shadow_name, None);
189        assert_eq!(shadow.shadow_op, shadow::Topic::GetAccepted);
190    }
191    #[test]
192    fn match_named_shadow_topic_string() {
193        let topic = "$aws/things/chloe/shadow/name/common/get/rejected";
194        let shadow = shadow::match_topic(topic).unwrap();
195        assert_eq!(shadow.thing_name, "chloe");
196        assert_eq!(shadow.shadow_name.unwrap(), "common");
197        assert_eq!(shadow.shadow_op, shadow::Topic::GetRejected);
198    }
199}