aws_iot_device_sdk_embedded_rust/
jobs.rs

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