summaryrefslogtreecommitdiffstats
path: root/crates/core/c8y_smartrest/src
diff options
context:
space:
mode:
authorLukasz Woznicki <75632179+makr11st@users.noreply.github.com>2021-11-24 20:54:56 +0000
committerGitHub <noreply@github.com>2021-11-24 20:54:56 +0000
commita4ffeccf60090e4456755bc53a6e3b8c8038e855 (patch)
tree9583f187114913a92866571920dd3bb205bd50a3 /crates/core/c8y_smartrest/src
parent8217e80670e76dbf9168780f5e0545355a39f8f3 (diff)
Restructure directories of the workspace (#559)
* Restructure directories of the workspace * Rename c8y_translator_lib to c8y_translator * Update comment on how to get dummy plugin path Signed-off-by: Lukasz Woznicki <lukasz.woznicki@softwareag.com>
Diffstat (limited to 'crates/core/c8y_smartrest/src')
-rw-r--r--crates/core/c8y_smartrest/src/error.rs35
-rw-r--r--crates/core/c8y_smartrest/src/lib.rs3
-rw-r--r--crates/core/c8y_smartrest/src/smartrest_deserializer.rs542
-rw-r--r--crates/core/c8y_smartrest/src/smartrest_serializer.rs367
4 files changed, 947 insertions, 0 deletions
diff --git a/crates/core/c8y_smartrest/src/error.rs b/crates/core/c8y_smartrest/src/error.rs
new file mode 100644
index 00000000..03fd5125
--- /dev/null
+++ b/crates/core/c8y_smartrest/src/error.rs
@@ -0,0 +1,35 @@
+use json_sm::SoftwareUpdateResponse;
+
+#[derive(thiserror::Error, Debug)]
+pub enum SmartRestSerializerError {
+ #[error("The operation status is not supported. {response:?}")]
+ UnsupportedOperationStatus { response: SoftwareUpdateResponse },
+
+ #[error("Failed to serialize SmartREST.")]
+ InvalidCsv(#[from] csv::Error),
+
+ #[error(transparent)]
+ FromCsvWriter(#[from] csv::IntoInnerError<csv::Writer<Vec<u8>>>),
+
+ #[error(transparent)]
+ FromUtf8Error(#[from] std::string::FromUtf8Error),
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum SmartRestDeserializerError {
+ #[error("The received SmartREST message ID {id} is unsupported.")]
+ UnsupportedOperation { id: String },
+
+ #[error("Failed to deserialize SmartREST.")]
+ InvalidCsv(#[from] csv::Error),
+
+ #[error("Jwt response contains incorrect ID: {0}")]
+ InvalidMessageId(u16),
+
+ #[error("Parameter {parameter} is not recognized. {hint}")]
+ InvalidParameter {
+ operation: String,
+ parameter: String,
+ hint: String,
+ },
+}
diff --git a/crates/core/c8y_smartrest/src/lib.rs b/crates/core/c8y_smartrest/src/lib.rs
new file mode 100644
index 00000000..596af9be
--- /dev/null
+++ b/crates/core/c8y_smartrest/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod error;
+pub mod smartrest_deserializer;
+pub mod smartrest_serializer;
diff --git a/crates/core/c8y_smartrest/src/smartrest_deserializer.rs b/crates/core/c8y_smartrest/src/smartrest_deserializer.rs
new file mode 100644
index 00000000..841915b7
--- /dev/null
+++ b/crates/core/c8y_smartrest/src/smartrest_deserializer.rs
@@ -0,0 +1,542 @@
+use crate::error::SmartRestDeserializerError;
+use chrono::{DateTime, FixedOffset};
+use csv::ReaderBuilder;
+use json_sm::{DownloadInfo, SoftwareModule, SoftwareModuleUpdate, SoftwareUpdateRequest};
+use serde::de::Error;
+use serde::{Deserialize, Deserializer, Serialize};
+use std::convert::{TryFrom, TryInto};
+
+#[derive(Debug)]
+enum CumulocitySoftwareUpdateActions {
+ Install,
+ Delete,
+}
+
+impl TryFrom<String> for CumulocitySoftwareUpdateActions {
+ type Error = SmartRestDeserializerError;
+
+ fn try_from(action: String) -> Result<Self, Self::Error> {
+ match action.as_str() {
+ "install" => Ok(Self::Install),
+ "delete" => Ok(Self::Delete),
+ param => Err(SmartRestDeserializerError::InvalidParameter {
+ parameter: param.into(),
+ operation: "c8y_SoftwareUpdate".into(),
+ hint: "It must be install or delete.".into(),
+ }),
+ }
+ }
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct SmartRestUpdateSoftware {
+ pub message_id: String,
+ pub external_id: String,
+ pub update_list: Vec<SmartRestUpdateSoftwareModule>,
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct SmartRestUpdateSoftwareModule {
+ pub software: String,
+ pub version: Option<String>,
+ pub url: Option<String>,
+ pub action: String,
+}
+
+impl SmartRestUpdateSoftware {
+ pub fn new() -> Self {
+ Self {
+ message_id: "528".into(),
+ external_id: "".into(),
+ update_list: vec![],
+ }
+ }
+
+ pub fn from_smartrest(&self, smartrest: &str) -> Result<Self, SmartRestDeserializerError> {
+ let mut message_id = smartrest.to_string();
+ let () = message_id.truncate(3);
+
+ let mut rdr = ReaderBuilder::new()
+ .has_headers(false)
+ .flexible(true)
+ .from_reader(smartrest.as_bytes());
+ let mut record: Self = Self::new();
+
+ for result in rdr.deserialize() {
+ record = result?;
+ }
+
+ Ok(record)
+ }
+
+ pub fn to_thin_edge_json(&self) -> Result<SoftwareUpdateRequest, SmartRestDeserializerError> {
+ let request = self.map_to_software_update_request(SoftwareUpdateRequest::new())?;
+ Ok(request)
+ }
+
+ pub fn modules(&self) -> Vec<SmartRestUpdateSoftwareModule> {
+ let mut modules = vec![];
+ for module in &self.update_list {
+ modules.push(SmartRestUpdateSoftwareModule {
+ software: module.software.clone(),
+ version: module.version.clone(),
+ url: module.url.clone(),
+ action: module.action.clone(),
+ });
+ }
+ modules
+ }
+
+ fn map_to_software_update_request(
+ &self,
+ mut request: SoftwareUpdateRequest,
+ ) -> Result<SoftwareUpdateRequest, SmartRestDeserializerError> {
+ for module in &self.modules() {
+ match module.action.clone().try_into()? {
+ CumulocitySoftwareUpdateActions::Install => {
+ request.add_update(SoftwareModuleUpdate::Install {
+ module: SoftwareModule {
+ module_type: module.get_module_version_and_type().1,
+ name: module.software.clone(),
+ version: module.get_module_version_and_type().0,
+ url: module.get_url(),
+ file_path: None,
+ },
+ });
+ }
+ CumulocitySoftwareUpdateActions::Delete => {
+ request.add_update(SoftwareModuleUpdate::Remove {
+ module: SoftwareModule {
+ module_type: module.get_module_version_and_type().1,
+ name: module.software.clone(),
+ version: module.get_module_version_and_type().0,
+ url: None,
+ file_path: None,
+ },
+ });
+ }
+ }
+ }
+ Ok(request)
+ }
+}
+
+impl SmartRestUpdateSoftwareModule {
+ fn get_module_version_and_type(&self) -> (Option<String>, Option<String>) {
+ let split;
+ match &self.version {
+ Some(version) => {
+ if version.matches("::").count() > 1 {
+ split = version.rsplit_once("::");
+ } else {
+ split = version.split_once("::");
+ }
+
+ match split {
+ Some((v, t)) => {
+ if v.is_empty() {
+ (None, Some(t.into())) // ::debian
+ } else if !t.is_empty() {
+ (Some(v.into()), Some(t.into())) // 1.0::debian
+ } else {
+ (Some(v.into()), None)
+ }
+ }
+ None => {
+ if version == " " {
+ (None, None) // as long as c8y UI forces version input
+ } else {
+ (Some(version.into()), None) // 1.0
+ }
+ }
+ }
+ }
+
+ None => (None, None), // (empty)
+ }
+ }
+
+ fn get_url(&self) -> Option<DownloadInfo> {
+ match &self.url {
+ Some(url) if url.trim().is_empty() => None,
+ Some(url) => Some(DownloadInfo::new(url.as_str())),
+ None => None,
+ }
+ }
+}
+
+fn to_datetime<'de, D>(deserializer: D) -> Result<DateTime<FixedOffset>, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ // NOTE `NaiveDateTime` is used here because c8y uses for log requests a date time string which
+ // does not exactly equal `chrono::DateTime::parse_from_rfc3339`
+ // c8y result:
+ // 2021-10-23T19:03:26+0100
+ // rfc3339 expected:
+ // 2021-10-23T19:03:26+01:00
+ // so we add a ':'
+ let mut date_string: String = Deserialize::deserialize(deserializer)?;
+ let str_size = date_string.len();
+ // check if `date_string` does not have a colon.
+ let date_string_end = &date_string.split('+').last();
+ date_string = match date_string_end {
+ Some(string) if !string.contains(':') => {
+ date_string[0..str_size - 2].to_string()
+ + ":"
+ + &date_string[str_size - 2..str_size].to_string()
+ }
+ _ => date_string,
+ };
+
+ match DateTime::parse_from_rfc3339(&date_string) {
+ Ok(result) => Ok(result),
+ Err(e) => Err(D::Error::custom(&format!("Error: {}", e))),
+ }
+}
+
+pub enum SmartRestVariant {
+ SmartRestLogRequest,
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct SmartRestLogRequest {
+ pub message_id: String,
+ pub device: String,
+ pub log_type: String,
+ #[serde(deserialize_with = "to_datetime")]
+ pub date_from: DateTime<FixedOffset>,
+ #[serde(deserialize_with = "to_datetime")]
+ pub date_to: DateTime<FixedOffset>,
+ pub needle: Option<String>,
+ pub lines: usize,
+}
+
+impl SmartRestLogRequest {
+ pub fn from_smartrest(smartrest: &str) -> Result<Self, SmartRestDeserializerError> {
+ let mut rdr = ReaderBuilder::new()
+ .has_headers(false)
+ .flexible(true)
+ .from_reader(smartrest.as_bytes());
+
+ match rdr.deserialize().next() {
+ Some(Ok(record)) => Ok(record),
+ Some(Err(err)) => Err(err)?,
+ None => panic!("empty request"),
+ }
+ }
+}
+
+type JwtToken = String;
+
+#[derive(Debug, Deserialize, PartialEq)]
+pub struct SmartRestJwtResponse {
+ id: u16,
+ token: JwtToken,
+}
+
+impl SmartRestJwtResponse {
+ pub fn new() -> Self {
+ Self {
+ id: 71,
+ token: "".into(),
+ }
+ }
+
+ pub fn try_new(to_parse: &str) -> Result<Self, SmartRestDeserializerError> {
+ let mut csv = csv::ReaderBuilder::new()
+ .has_headers(false)
+ .from_reader(to_parse.as_bytes());
+
+ let mut jwt = Self::new();
+ for result in csv.deserialize() {
+ jwt = result.unwrap();
+ }
+
+ if jwt.id != 71 {
+ return Err(SmartRestDeserializerError::InvalidMessageId(jwt.id));
+ }
+
+ Ok(jwt)
+ }
+
+ pub fn token(&self) -> JwtToken {
+ self.token.clone()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use assert_json_diff::*;
+ use json_sm::*;
+ use serde_json::json;
+ use test_case::test_case;
+
+ // To avoid using an ID randomly generated, which is not convenient for testing.
+ impl SmartRestUpdateSoftware {
+ fn to_thin_edge_json_with_id(
+ &self,
+ id: &str,
+ ) -> Result<SoftwareUpdateRequest, SmartRestDeserializerError> {
+ let request = SoftwareUpdateRequest::new_with_id(id);
+ self.map_to_software_update_request(request)
+ }
+ }
+
+ #[test]
+ fn jwt_token_create_new() {
+ let jwt = SmartRestJwtResponse::new();
+
+ assert!(jwt.token.is_empty());
+ }
+
+ #[test]
+ fn jwt_token_deserialize_correct_id_returns_token() {
+ let test_response = "71,123456";
+ let jwt = SmartRestJwtResponse::try_new(test_response).unwrap();
+
+ assert_eq!(jwt.token(), "123456");
+ }
+
+ #[test]
+ fn jwt_token_deserialize_incorrect_id_returns_error() {
+ let test_response = "42,123456";
+
+ let jwt = SmartRestJwtResponse::try_new(test_response);
+
+ assert!(jwt.is_err());
+ assert_matches::assert_matches!(jwt, Err(SmartRestDeserializerError::InvalidMessageId(42)));
+ }
+
+ #[test]
+ fn verify_get_module_version_and_type() {
+ let mut module = SmartRestUpdateSoftwareModule {
+ software: "software1".into(),
+ version: None,
+ url: None,
+ action: "install".into(),
+ }; // ""
+ assert_eq!(module.get_module_version_and_type(), (None, None));
+
+ module.version = Some(" ".into()); // " " (space)
+ assert_eq!(module.get_module_version_and_type(), (None, None));
+
+ module.version = Some("::debian".into());
+ assert_eq!(
+ module.get_module_version_and_type(),
+ (None, Some("debian".to_string()))
+ );
+
+ module.version = Some("1.0.0::debian".into());
+ assert_eq!(
+ module.get_module_version_and_type(),
+ (Some("1.0.0".to_string()), Some("debian".to_string()))
+ );
+
+ module.version = Some("1.0.0::1::debian".into());
+ assert_eq!(
+ module.get_module_version_and_type(),
+ (Some("1.0.0::1".to_string()), Some("debian".to_string()))
+ );
+
+ module.version = Some("1.0.0::1::".into());
+ assert_eq!(
+ module.get_module_version_and_type(),
+ (Some("1.0.0::1".to_string()), None)
+ );
+
+ module.version = Some("1.0.0".into());
+ assert_eq!(
+ module.get_module_version_and_type(),
+ (Some("1.0.0".to_string()), None)
+ );
+ }
+
+ #[test]
+ fn deserialize_smartrest_update_software() {
+ let smartrest =
+ String::from("528,external_id,software1,version1,url1,install,software2,,,delete");
+ let update_software = SmartRestUpdateSoftware::new()
+ .from_smartrest(&smartrest)
+ .unwrap();
+
+ let expected_update_software = SmartRestUpdateSoftware {
+ message_id: "528".into(),
+ external_id: "external_id".into(),
+ update_list: vec![
+ SmartRestUpdateSoftwareModule {
+ software: "software1".into(),
+ version: Some("version1".into()),
+ url: Some("url1".into()),
+ action: "install".into(),
+ },
+ SmartRestUpdateSoftwareModule {
+ software: "software2".into(),
+ version: None,
+ url: None,
+ action: "delete".into(),
+ },
+ ],
+ };
+
+ assert_eq!(update_software, expected_update_software);
+ }
+
+ #[test]
+ fn deserialize_incorrect_smartrest_message_id() {
+ let smartrest = String::from("516,external_id");
+ assert!(SmartRestUpdateSoftware::new()
+ .from_smartrest(&smartrest)
+ .is_err());
+ }
+
+ #[test]
+ fn deserialize_incorrect_smartrest_action() {
+ let smartrest =
+ String::from("528,external_id,software1,version1,url1,action,software2,,,remove");
+ assert!(SmartRestUpdateSoftware::new()
+ .from_smartrest(&smartrest)
+ .unwrap()
+ .to_thin_edge_json()
+ .is_err());
+ }
+
+ #[test]
+ fn from_smartrest_update_software_to_software_update_request() {
+ let smartrest_obj = SmartRestUpdateSoftware {
+ message_id: "528".into(),
+ external_id: "external_id".into(),
+ update_list: vec![
+ SmartRestUpdateSoftwareModule {
+ software: "software1".into(),
+ version: Some("version1::debian".into()),
+ url: Some("url1".into()),
+ action: "install".into(),
+ },
+ SmartRestUpdateSoftwareModule {
+ software: "software2".into(),
+ version: None,
+ url: None,
+ action: "delete".into(),
+ },
+ ],
+ };
+ let thin_edge_json = smartrest_obj.to_thin_edge_json_with_id("123").unwrap();
+
+ let mut expected_thin_edge_json = SoftwareUpdateRequest::new_with_id("123");
+ let () =
+ expected_thin_edge_json.add_update(SoftwareModuleUpdate::install(SoftwareModule {
+ module_type: Some("debian".to_string()),
+ name: "software1".to_string(),
+ version: Some("version1".to_string()),
+ url: Some("url1".into()),
+ file_path: None,
+ }));
+ let () = expected_thin_edge_json.add_update(SoftwareModuleUpdate::remove(SoftwareModule {
+ module_type: Some("".to_string()),
+ name: "software2".to_string(),
+ version: None,
+ url: None,
+ file_path: None,
+ }));
+
+ assert_eq!(thin_edge_json, expected_thin_edge_json);
+ }
+
+ #[test]
+ fn from_smartrest_update_software_to_json() {
+ let smartrest =
+ String::from("528,external_id,nodered,1.0.0::debian,,install,\
+ collectd,5.7::debian,https://collectd.org/download/collectd-tarballs/collectd-5.12.0.tar.bz2,install,\
+ nginx,1.21.0::docker,,install,mongodb,4.4.6::docker,,delete");
+ let update_software = SmartRestUpdateSoftware::new();
+ let software_update_request = update_software
+ .from_smartrest(&smartrest)
+ .unwrap()
+ .to_thin_edge_json_with_id("123");
+ let output_json = software_update_request.unwrap().to_json().unwrap();
+
+ let expected_json = json!({
+ "id": "123",
+ "updateList": [
+ {
+ "type": "debian",
+ "modules": [
+ {
+ "name": "nodered",
+ "version": "1.0.0",
+ "action": "install"
+ },
+ {
+ "name": "collectd",
+ "version": "5.7",
+ "url": "https://collectd.org/download/collectd-tarballs/collectd-5.12.0.tar.bz2",
+ "action": "install"
+ }
+ ]
+ },
+ {
+ "type": "docker",
+ "modules": [
+ {
+ "name": "nginx",
+ "version": "1.21.0",
+ "action": "install"
+ },
+ {
+ "name": "mongodb",
+ "version": "4.4.6",
+ "action": "remove"
+ }
+ ]
+ }
+ ]
+ });
+ assert_json_eq!(
+ serde_json::from_str::<serde_json::Value>(output_json.as_str()).unwrap(),
+ expected_json
+ );
+ }
+
+ #[test]
+ fn access_smartrest_update_modules() {
+ let smartrest =
+ String::from("528,external_id,software1,version1,url1,install,software2,,,delete");
+ let update_software = SmartRestUpdateSoftware::new();
+ let vec = update_software
+ .from_smartrest(&smartrest)
+ .unwrap()
+ .modules();
+
+ let expected_vec = vec![
+ SmartRestUpdateSoftwareModule {
+ software: "software1".into(),
+ version: Some("version1".into()),
+ url: Some("url1".into()),
+ action: "install".into(),
+ },
+ SmartRestUpdateSoftwareModule {
+ software: "software2".into(),
+ version: None,
+ url: None,
+ action: "delete".into(),
+ },
+ ];
+
+ assert_eq!(vec, expected_vec);
+ }
+
+ #[test_case("2021-09-21T11:40:27+0200", "2021-09-22T11:40:27+0200"; "c8y expected")]
+ #[test_case("2021-09-21T11:40:27+02:00", "2021-09-22T11:40:27+02:00"; "with colon both")]
+ #[test_case("2021-09-21T11:40:27+02:00", "2021-09-22T11:40:27+0200"; "with colon date from")]
+ #[test_case("2021-09-21T11:40:27+0200", "2021-09-22T11:40:27+02:00"; "with colon date to")]
+ fn deserialize_smartrest_log_file_request_operation(date_from: &str, date_to: &str) {
+ let smartrest = String::from(&format!(
+ "522,DeviceSerial,syslog,{},{},ERROR,1000",
+ date_from, date_to
+ ));
+ let log = SmartRestLogRequest::from_smartrest(&smartrest);
+ assert!(log.is_ok());
+ }
+}
diff --git a/crates/core/c8y_smartrest/src/smartrest_serializer.rs b/crates/core/c8y_smartrest/src/smartrest_serializer.rs
new file mode 100644
index 00000000..81e7ed4f
--- /dev/null
+++ b/crates/core/c8y_smartrest/src/smartrest_serializer.rs
@@ -0,0 +1,367 @@
+use crate::error::SmartRestSerializerError;
+use csv::{QuoteStyle, WriterBuilder};
+use json_sm::{SoftwareOperationStatus, SoftwareUpdateResponse};
+use serde::{Deserialize, Serialize, Serializer};
+
+type SmartRest = String;
+
+#[derive(Debug)]
+pub enum CumulocitySupportedOperations {
+ C8ySoftwareUpdate,
+ C8yLogFileRequest,
+}
+
+impl From<CumulocitySupportedOperations> for &'static str {
+ fn from(op: CumulocitySupportedOperations) -> Self {
+ match op {
+ CumulocitySupportedOperations::C8ySoftwareUpdate => "c8y_SoftwareUpdate",
+ CumulocitySupportedOperations::C8yLogFileRequest => "c8y_LogfileRequest",
+ }
+ }
+}
+
+pub trait SmartRestSerializer<'a>
+where
+ Self: Serialize,
+{
+ fn to_smartrest(&self) -> Result<SmartRest, SmartRestSerializerError> {
+ serialize_smartrest(self)
+ }
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct SmartRestSetSupportedLogType {
+ pub message_id: &'static str,
+ pub supported_operations: Vec<&'static str>,
+}
+
+impl Default for SmartRestSetSupportedLogType {
+ fn default() -> Self {
+ Self {
+ message_id: "118",
+ supported_operations: vec!["software-management".into()],
+ }
+ }
+}
+
+impl<'a> SmartRestSerializer<'a> for SmartRestSetSupportedLogType {}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct SmartRestSetSupportedOperations {
+ pub message_id: &'static str,
+ pub supported_operations: Vec<&'static str>,
+}
+
+impl Default for SmartRestSetSupportedOperations {
+ fn default() -> Self {
+ Self {
+ message_id: "114",
+ supported_operations: vec![
+ CumulocitySupportedOperations::C8ySoftwareUpdate.into(),
+ CumulocitySupportedOperations::C8yLogFileRequest.into(),
+ ],
+ }
+ }
+}
+
+impl<'a> SmartRestSerializer<'a> for SmartRestSetSupportedOperations {}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct SmartRestSoftwareModuleItem {
+ pub software: String,
+ pub version: Option<String>,
+ pub url: Option<String>,
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct SmartRestGetPendingOperations {
+ pub id: &'static str,
+}
+
+impl Default for SmartRestGetPendingOperations {
+ fn default() -> Self {
+ Self { id: "500" }
+ }
+}
+
+impl<'a> SmartRestSerializer<'a> for SmartRestGetPendingOperations {}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct SmartRestSetOperationToExecuting {
+ pub message_id: &'static str,
+ pub operation: &'static str,
+}
+
+impl SmartRestSetOperationToExecuting {
+ pub fn new(operation: CumulocitySupportedOperations) -> Self {
+ Self {
+ message_id: "501",
+ operation: operation.into(),
+ }
+ }
+
+ pub fn from_thin_edge_json(
+ response: SoftwareUpdateResponse,
+ ) -> Result<Self, SmartRestSerializerError> {
+ match response.status() {
+ SoftwareOperationStatus::Executing => {
+ Ok(Self::new(CumulocitySupportedOperations::C8ySoftwareUpdate))
+ }
+ _ => Err(SmartRestSerializerError::UnsupportedOperationStatus { response }),
+ }
+ }
+}
+
+impl<'a> SmartRestSerializer<'a> for SmartRestSetOperationToExecuting {}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct SmartRestSetOperationToSuccessful {
+ pub message_id: &'static str,
+ pub operation: &'static str,
+ pub operation_parameter: Option<String>,
+}
+
+impl SmartRestSetOperationToSuccessful {
+ pub fn new(operation: CumulocitySupportedOperations) -> Self {
+ Self {
+ message_id: "503",
+ operation: operation.into(),
+ operation_parameter: None,
+ }
+ }
+
+ pub fn with_response_parameter(self, response_parameter: &str) -> Self {
+ Self {
+ operation_parameter: Some(response_parameter.into()),
+ ..self
+ }
+ }
+
+ pub fn from_thin_edge_json(
+ response: SoftwareUpdateResponse,
+ ) -> Result<Self, SmartRestSerializerError> {
+ match response.status() {
+ SoftwareOperationStatus::Successful => {
+ Ok(Self::new(CumulocitySupportedOperations::C8ySoftwareUpdate))
+ }
+ _ => Err(SmartRestSerializerError::UnsupportedOperationStatus { response }),
+ }
+ }
+}
+
+impl<'a> SmartRestSerializer<'a> for SmartRestSetOperationToSuccessful {}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq)]
+pub struct SmartRestSetOperationToFailed {
+ pub message_id: &'static str,
+ pub operation: &'static str,
+ #[serde(serialize_with = "reason_to_string_with_quotes")]
+ pub reason: String,
+}
+
+impl SmartRestSetOperationToFailed {
+ fn new(operation: CumulocitySupportedOperations, reason: String) -> Self {
+ Self {
+ message_id: "502",
+ operation: operation.into(),
+ reason,
+ }
+ }
+
+ pub fn from_thin_edge_json(
+ response: SoftwareUpdateResponse,
+ ) -> Result<Self, SmartRestSerializerError> {
+ match &response.status() {
+ SoftwareOperationStatus::Failed => Ok(Self::new(
+ CumulocitySupportedOperations::C8ySoftwareUpdate,
+ response.error().unwrap_or_else(|| "".to_string()),
+ )),
+ _ => Err(SmartRestSerializerError::UnsupportedOperationStatus { response }),
+ }
+ }
+}
+
+impl<'a> SmartRestSerializer<'a> for SmartRestSetOperationToFailed {}
+
+fn reason_to_string_with_quotes<S>(reason: &str, serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ let s = format!("\"{}\"", reason);
+ serializer.serialize_str(&s)
+}
+
+fn serialize_smartrest<S: Serialize>(record: S) -> Result<String, SmartRestSerializerError> {
+ let mut wtr = WriterBuilder::new()
+ .has_headers(false)
+ .quote_style(QuoteStyle::Never)
+ .double_quote(false)
+ .from_writer(vec![]);
+ wtr.serialize(record)?;
+ let csv = String::from_utf8(wtr.into_inner()?)?;
+ Ok(csv)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use json_sm::*;
+
+ #[test]
+ fn serialize_smartrest_supported_operations() {
+ let smartrest = SmartRestSetSupportedOperations::default()
+ .to_smartrest()
+ .unwrap();
+ assert_eq!(smartrest, "114,c8y_SoftwareUpdate,c8y_LogfileRequest\n");
+ }
+
+ #[test]
+ fn serialize_smartrest_get_pending_operations() {
+ let smartrest = SmartRestGetPendingOperations::default()
+ .to_smartrest()
+ .unwrap();
+ assert_eq!(smartrest, "500\n");
+ }
+
+ #[test]
+ fn serialize_smartrest_set_operation_to_executing() {
+ let smartrest =
+ SmartRestSetOperationToExecuting::new(CumulocitySupportedOperations::C8ySoftwareUpdate)
+ .to_smartrest()
+ .unwrap();
+ assert_eq!(smartrest, "501,c8y_SoftwareUpdate\n");
+ }
+
+ #[test]
+ fn from_thin_edge_json_to_smartrest_set_operation_to_executing() {
+ let json_response = r#"{
+ "id": "123",
+ "status": "executing"
+ }"#;
+ let response = SoftwareUpdateResponse::from_json(json_response).unwrap();
+ let smartrest_obj =
+ SmartRestSetOperationToExecuting::from_thin_edge_json(response).unwrap();
+
+ let expected_smartrest_obj = SmartRestSetOperationToExecuting {
+ message_id: "501",
+ operation: "c8y_SoftwareUpdate",
+ };
+ assert_eq!(smartrest_obj, expected_smartrest_obj);
+ }
+
+ #[test]
+ fn serialize_smartrest_set_operation_to_successful() {
+ let smartrest = SmartRestSetOperationToSuccessful::new(
+ CumulocitySupportedOperations::C8ySoftwareUpdate,
+ )
+ .to_smartrest()
+ .unwrap();
+ assert_eq!(smartrest, "503,c8y_SoftwareUpdate,\n");
+ }
+
+ #[test]
+ fn from_thin_edge_json_to_smartrest_set_operation_to_successful() {
+ let json_response = r#"{
+ "id":"1",
+ "status":"successful",
+ "currentSoftwareList":[]
+ }"#;
+ let response = SoftwareUpdateResponse::from_json(json_response).unwrap();
+ let smartrest_obj =
+ SmartRestSetOperationToSuccessful::from_thin_edge_json(response).unwrap();
+
+ let expected_smartrest_obj = SmartRestSetOperationToSuccessful {
+ message_id: "503",
+ operation: "c8y_SoftwareUpdate",
+ operation_parameter: None,
+ };
+ assert_eq!(smartrest_obj, expected_smartrest_obj);
+ }
+
+ #[test]
+ fn serialize_smartrest_set_operation_to_failed() {
+ let smartrest = SmartRestSetOperationToFailed::new(
+ CumulocitySupportedOperations::C8ySoftwareUpdate,
+ "Failed due to permission.".into(),
+ )
+ .to_smartrest()
+ .unwrap();
+ assert_eq!(
+ smartrest,
+ "502,c8y_SoftwareUpdate,\"Failed due to permission.\"\n"
+ );
+ }
+
+ #[test]
+ fn serialize_smartrest_set_operation_to_failed_with_comma_reason() {
+ let smartrest = SmartRestSetOperationToFailed::new(
+ CumulocitySupportedOperations::C8ySoftwareUpdate,