aws_iot_device_sdk/
jobs.rs

1use crate::common::*;
2use arrayvec::{ArrayString, ArrayVec};
3
4use self::Topic::*;
5
6const API_JOBSCHANGED: &str = "notify";
7const API_NEXTJOBCHANGED: &str = "notify-next";
8const API_GETPENDING: &str = "get";
9const API_STARTNEXT: &str = "start-next";
10const API_DESCRIBE: &str = "get";
11const API_UPDATE: &str = "update";
12const API_JOBID_NEXT: &str = "$next";
13
14/// The struct outputs which API the topic is for. It also outputs
15/// the thing name in the given topic.
16pub struct ThingJobs<'a> {
17    pub thing_name: &'a str,
18    pub api: Topic,
19    pub id: Option<ArrayString<JOBID_MAX_LENGTH>>,
20}
21
22///
23/// Topic values for subscription requests.
24///
25#[derive(Debug, PartialEq, PartialOrd)]
26pub enum Topic {
27    JobsChanged,
28    NextJobChanged,
29    GetPendingSuccess,
30    GetPendingFailed,
31    StartNextSuccess,
32    StartNextFailed,
33    /* Topics below use a job ID. */
34    DescribeSuccess,
35    DescribeFailed,
36    UpdateSuccess,
37    UpdateFailed,
38}
39
40/// Populate a topic string for a subscription request.
41///
42/// # Example
43/// ```
44/// use aws_iot_device_sdk::jobs::Topic::*;
45/// use aws_iot_device_sdk::{jobs};
46///
47/// let jobs = jobs::match_topic("$aws/things/chloe/jobs/notify-next").unwrap();
48/// assert_eq!(jobs.api, jobs::Topic::NextJobChanged);
49/// assert_eq!(jobs.id, None);
50/// ```
51pub fn assemble_topic(
52    thing_name: &str,
53    api: Topic,
54) -> Result<ArrayString<JOBS_TOPIC_MAX_LENGTH>, Error> {
55    is_valid_thing_name(thing_name)?;
56    let mut s = ArrayString::<JOBS_TOPIC_MAX_LENGTH>::new();
57    s.push_str(AWS_THINGS_PREFIX);
58    s.push_str(thing_name);
59    s.push_str(JOBS_API_BRIDGE);
60    s.push_str(id(&api));
61    s.push_str(op(&api));
62    s.push_str(suffix(&api));
63
64    Ok(s)
65}
66
67fn id(api: &Topic) -> &str {
68    match api {
69        DescribeSuccess | DescribeFailed | UpdateSuccess | UpdateFailed => "+/",
70        _ => "",
71    }
72}
73
74fn op(api: &Topic) -> &str {
75    match api {
76        JobsChanged => API_JOBSCHANGED,
77        NextJobChanged => API_NEXTJOBCHANGED,
78        GetPendingSuccess => API_GETPENDING,
79        GetPendingFailed => API_GETPENDING,
80        StartNextSuccess => API_STARTNEXT,
81        StartNextFailed => API_STARTNEXT,
82        /* Topics below use a => job ID. */
83        DescribeSuccess => API_DESCRIBE,
84        DescribeFailed => API_DESCRIBE,
85        UpdateSuccess => API_UPDATE,
86        UpdateFailed => API_UPDATE,
87    }
88}
89
90fn suffix(topic_type: &Topic) -> &str {
91    match topic_type {
92        GetPendingSuccess | StartNextSuccess | DescribeSuccess | UpdateSuccess => SUFFIX_ACCEPTED,
93        GetPendingFailed | StartNextFailed | DescribeFailed | UpdateFailed => SUFFIX_REJECTED,
94        _ => "",
95    }
96}
97/// Output a topic value if a Jobs API topic string is present.
98/// Optionally, output a jobID and thing name within the topic.
99///
100/// # Example
101/// ```
102/// use aws_iot_device_sdk::jobs::Topic::*;
103/// use aws_iot_device_sdk::{jobs};
104///
105/// let jobs = jobs::match_topic("$aws/things/chloe/jobs/$next/get/accepted").unwrap();
106/// assert_eq!(jobs.api, jobs::Topic::DescribeSuccess);
107/// let id = jobs.id.unwrap();
108/// assert_eq!(&id[..], "$next")
109///
110/// ```
111pub fn match_topic(topic: &str) -> Result<ThingJobs, Error> {
112    is_valid_mqtt_topic(topic)?;
113
114    let s = is_valid_prefix(topic, AWS_THINGS_PREFIX)?;
115
116    let mid = s.find('/').ok_or(Error::FAIL);
117    let (thing_name, mut s) = s.split_at(mid?);
118    is_valid_thing_name(thing_name)?;
119
120    s = is_valid_bridge(s, JOBS_API_BRIDGE)?;
121
122    let v: ArrayVec<&str, 16> = s.split('/').collect();
123    let api: Topic;
124    let jobs_id;
125    match v[..] {
126        // ~$aws/things/MyThing/jobs/~<operation>
127        // $aws/things/MyThing/jobs/notify (or $aws/things/MyThing/jobs/notify-next)
128        [op] => {
129            if op == API_JOBSCHANGED {
130                api = JobsChanged;
131            } else {
132                api = NextJobChanged;
133            }
134            Ok(ThingJobs {
135                thing_name,
136                api,
137                id: None,
138            })
139        }
140        // $aws/things/MyThing/jobs/<operation>/<suffix>
141        [op, suffix] => {
142            match (op, suffix) {
143                (API_GETPENDING, ACCEPTED) => api = GetPendingSuccess,
144                (API_GETPENDING, REJECTED) => api = GetPendingFailed,
145                (API_STARTNEXT, ACCEPTED) => api = StartNextSuccess,
146                (API_STARTNEXT, REJECTED) => api = StartNextFailed,
147                _ => return Err(Error::NoMatch),
148            }
149            Ok(ThingJobs {
150                thing_name,
151                api,
152                id: None,
153            })
154        }
155        // $aws/things/MyThing/jobs/<jobs-id>/<operation>/<suffix>
156        [id, op, suffix] => {
157            match (op, suffix) {
158                (API_DESCRIBE, ACCEPTED) => api = DescribeSuccess,
159                (API_DESCRIBE, REJECTED) => api = DescribeFailed,
160                (API_UPDATE, ACCEPTED) => api = UpdateSuccess,
161                (API_UPDATE, REJECTED) => api = UpdateFailed,
162                _ => return Err(Error::NoMatch),
163            }
164            jobs_id = Some(ArrayString::<JOBID_MAX_LENGTH>::from(id).unwrap());
165            Ok(ThingJobs {
166                thing_name,
167                api,
168                id: jobs_id,
169            })
170        }
171        // Not jobs topic
172        _ => Err(Error::NoMatch),
173    }
174}
175/// Populate a topic string for a GetPendingJobExecutions request.
176///
177pub fn get_pending(thing_name: &str) -> Result<ArrayString<THINGNAME_MAX_LENGTH>, Error> {
178    is_valid_thing_name(thing_name)?;
179    let mut s = ArrayString::<THINGNAME_MAX_LENGTH>::new();
180    s.push_str(AWS_THINGS_PREFIX);
181    s.push_str(thing_name);
182    s.push_str(JOBS_API_BRIDGE);
183    s.push_str(API_GETPENDING);
184
185    Ok(s)
186}
187/// Populate a topic string for a StartNextPendingJobExecution request.
188///
189pub fn start_next(thing_name: &str) -> Result<ArrayString<THINGNAME_MAX_LENGTH>, Error> {
190    is_valid_thing_name(thing_name)?;
191    let mut s = ArrayString::<THINGNAME_MAX_LENGTH>::new();
192    s.push_str(AWS_THINGS_PREFIX);
193    s.push_str(thing_name);
194    s.push_str(JOBS_API_BRIDGE);
195    s.push_str(API_STARTNEXT);
196
197    Ok(s)
198}
199/// Populate a topic string for a DescribeJobExecution request.
200///
201/// # Example
202/// ```
203/// use aws_iot_device_sdk::jobs::Topic::*;
204/// use aws_iot_device_sdk::{jobs};
205///
206/// let topic = jobs::describe("chloe", "$next").unwrap();
207/// assert_eq!(&topic[..], "$aws/things/chloe/jobs/$next/get")
208///
209/// ```
210pub fn describe(thing_name: &str, id: &str) -> Result<ArrayString<THINGNAME_MAX_LENGTH>, Error> {
211    is_valid_thing_name(thing_name)?;
212    if id != API_JOBID_NEXT {
213        is_valid_job_id(id)?
214    };
215    let mut s = ArrayString::<THINGNAME_MAX_LENGTH>::new();
216    s.push_str(AWS_THINGS_PREFIX);
217    s.push_str(thing_name);
218    s.push_str(JOBS_API_BRIDGE);
219    s.push_str(id);
220    s.push_str("/");
221    s.push_str(API_DESCRIBE);
222
223    Ok(s)
224}
225/// Populate a topic string for an UpdateJobExecution request.
226///
227pub fn update(thing_name: &str, id: &str) -> Result<ArrayString<THINGNAME_MAX_LENGTH>, Error> {
228    is_valid_thing_name(thing_name)?;
229    is_valid_job_id(id)?;
230    let mut s = ArrayString::<THINGNAME_MAX_LENGTH>::new();
231    s.push_str(AWS_THINGS_PREFIX);
232    s.push_str(thing_name);
233    s.push_str(JOBS_API_BRIDGE);
234    s.push_str(id);
235    s.push_str("/");
236    s.push_str(API_UPDATE);
237
238    Ok(s)
239}
240
241#[cfg(test)]
242mod tests {
243    use crate::jobs;
244    #[test]
245    fn assemble_topic_notify_next() {
246        let topic = jobs::assemble_topic("chloe", jobs::Topic::NextJobChanged).unwrap();
247        assert_eq!(&topic[..], "$aws/things/chloe/jobs/notify-next");
248    }
249
250    #[test]
251    fn assemble_topic_get_rejected() {
252        let topic = jobs::assemble_topic("chloe", jobs::Topic::GetPendingFailed).unwrap();
253        assert_eq!(&topic[..], "$aws/things/chloe/jobs/get/rejected");
254    }
255
256    #[test]
257    fn assemble_topic_id_update_rejected() {
258        let topic = jobs::assemble_topic("chloe", jobs::Topic::UpdateFailed).unwrap();
259        assert_eq!(&topic[..], "$aws/things/chloe/jobs/+/update/rejected");
260    }
261
262    #[test]
263    fn match_topic() {
264        let jobs = jobs::match_topic("$aws/things/chloe/jobs/notify-next").unwrap();
265        assert_eq!(jobs.api, jobs::Topic::NextJobChanged);
266        assert_eq!(jobs.id, None);
267    }
268    #[test]
269    fn match_topic_with_op() {
270        let jobs = jobs::match_topic("$aws/things/chloe/jobs/get/rejected").unwrap();
271        assert_eq!(jobs.api, jobs::Topic::GetPendingFailed);
272        assert_eq!(jobs.id, None);
273    }
274    #[test]
275    fn match_topic_with_id_op() {
276        let jobs = jobs::match_topic("$aws/things/chloe/jobs/example-job-01/get/accepted").unwrap();
277        assert_eq!(jobs.api, jobs::Topic::DescribeSuccess);
278        let id = jobs.id.unwrap();
279        assert_eq!(&id[..], "example-job-01");
280    }
281    #[test]
282    fn get_pending() {
283        let topic = jobs::get_pending("chloe").unwrap();
284        assert_eq!(&topic[..], "$aws/things/chloe/jobs/get");
285    }
286    #[test]
287    fn start_next() {
288        let topic = jobs::start_next("chloe").unwrap();
289        assert_eq!(&topic[..], "$aws/things/chloe/jobs/start-next");
290    }
291    #[test]
292    fn update() {
293        let topic = jobs::update("chloe", "example-job-01").unwrap();
294        assert_eq!(&topic[..], "$aws/things/chloe/jobs/example-job-01/update");
295    }
296}