azure_iot_operations_protocol/common/
payload_serialize.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::fmt::Debug;

/// Format indicator for serialization and deserialization.
#[repr(u8)]
#[derive(Clone, PartialEq, Debug, Default)]
pub enum FormatIndicator {
    /// Unspecified Bytes
    #[default]
    UnspecifiedBytes = 0,
    /// UTF-8 Encoded Character Data (such as JSON)
    Utf8EncodedCharacterData = 1,
}

impl TryFrom<Option<u8>> for FormatIndicator {
    type Error = String;

    fn try_from(value: Option<u8>) -> Result<Self, Self::Error> {
        match value {
            Some(0) | None => Ok(FormatIndicator::default()),
            Some(1) => Ok(FormatIndicator::Utf8EncodedCharacterData),
            Some(_) => Err(format!(
                "Invalid format indicator value: {value:?}. Must be 0 or 1"
            )),
        }
    }
}

/// Struct that specifies the content type, format indicator, and payload for a serialized payload.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SerializedPayload {
    /// The content type of the payload
    pub content_type: String,
    /// The format indicator of the payload
    pub format_indicator: FormatIndicator,
    /// The payload as a serialized byte vector
    pub payload: Vec<u8>,
}

/// Trait for serializing and deserializing payloads.
/// # Examples
/// ```
/// use azure_iot_operations_protocol::common::payload_serialize::{PayloadSerialize, DeserializationError, FormatIndicator, SerializedPayload};
/// #[derive(Clone, Debug)]
/// pub struct CarLocationResponse {
///   latitude: f64,
///   longitude: f64,
/// }
/// impl PayloadSerialize for CarLocationResponse {
///   type Error = String;
///   fn serialize(self) -> Result<SerializedPayload, String> {
///     let response = format!("{{\"latitude\": {}, \"longitude\": {}}}", self.latitude, self.longitude);
///     Ok(SerializedPayload {
///         payload: response.as_bytes().to_vec(),
///         content_type: "application/json".to_string(),
///         format_indicator: FormatIndicator::Utf8EncodedCharacterData,
///     })
///   }
///   fn deserialize(payload: &[u8],
///     content_type: Option<&String>,
///     _format_indicator: &FormatIndicator,
///   ) -> Result<Self, DeserializationError<String>> {
///     if let Some(content_type) = content_type {
///            if content_type != "application/json" {
///                return Err(DeserializationError::UnsupportedContentType(format!(
///                    "Invalid content type: '{content_type:?}'. Must be 'application/json'"
///                )));
///            }
///        }
///     // mock deserialization here for brevity
///     let _payload = String::from_utf8(payload.to_vec()).unwrap();
///     Ok(CarLocationResponse {latitude: 12.0, longitude: 35.0})
///   }
/// }
/// ```
pub trait PayloadSerialize: Clone {
    /// The type returned in the event of a serialization/deserialization error
    type Error: Debug + Into<Box<dyn std::error::Error + Sync + Send + 'static>>;

    /// Serializes the payload from the generic type to a byte vector and specifies the content type and format indicator.
    /// The content type and format indicator could be the same every time or dynamic per payload.
    ///
    /// # Errors
    /// Returns a [`PayloadSerialize::Error`] if the serialization fails.
    fn serialize(self) -> Result<SerializedPayload, Self::Error>;

    /// Deserializes the payload from a byte vector to the generic type
    ///
    /// # Errors
    /// Returns a [`DeserializationError::InvalidPayload`] over type [`PayloadSerialize::Error`] if the deserialization fails.
    ///
    /// Returns a [`DeserializationError::UnsupportedContentType`] if the content type isn't supported by this deserialization implementation.
    fn deserialize(
        payload: &[u8],
        content_type: Option<&String>,
        format_indicator: &FormatIndicator,
    ) -> Result<Self, DeserializationError<Self::Error>>;
}

/// Enum to describe the type of error that occurred during payload deserialization.
#[derive(thiserror::Error, Debug)]
pub enum DeserializationError<T: Debug + Into<Box<dyn std::error::Error + Sync + Send + 'static>>> {
    /// An error occurred while deserializing.
    #[error(transparent)]
    InvalidPayload(#[from] T),
    /// The content type received is not supported by the deserialization implementation.
    #[error("Unsupported content type: {0}")]
    UnsupportedContentType(String),
}

// Provided convenience implementations

/// A provided convenience struct for bypassing serialization and deserialization,
/// but having dynamic content type and format indicator.
pub type BypassPayload = SerializedPayload;

impl PayloadSerialize for BypassPayload {
    type Error = String;
    fn serialize(self) -> Result<SerializedPayload, String> {
        Ok(SerializedPayload {
            payload: self.payload,
            content_type: self.content_type,
            format_indicator: self.format_indicator,
        })
    }

    fn deserialize(
        payload: &[u8],
        content_type: Option<&String>,
        format_indicator: &FormatIndicator,
    ) -> Result<Self, DeserializationError<String>> {
        let ct = match content_type {
            Some(ct) => ct.clone(),
            None => String::default(),
        };
        Ok(BypassPayload {
            content_type: ct,
            format_indicator: format_indicator.clone(),
            payload: payload.to_vec(),
        })
    }
}

/// Provided convenience implementation for sending raw bytes as `content_type` "application/octet-stream".
impl PayloadSerialize for Vec<u8> {
    type Error = String;
    fn serialize(self) -> Result<SerializedPayload, String> {
        Ok(SerializedPayload {
            payload: self,
            content_type: "application/octet-stream".to_string(),
            format_indicator: FormatIndicator::UnspecifiedBytes,
        })
    }

    fn deserialize(
        payload: &[u8],
        content_type: Option<&String>,
        _format_indicator: &FormatIndicator,
    ) -> Result<Self, DeserializationError<String>> {
        if let Some(content_type) = content_type {
            if content_type != "application/octet-stream" {
                return Err(DeserializationError::UnsupportedContentType(format!(
                    "Invalid content type: '{content_type:?}'. Must be 'application/octet-stream'"
                )));
            }
        }
        Ok(payload.to_vec())
    }
}

#[cfg(test)]
use mockall::mock;
#[cfg(test)]
mock! {
    #[allow(clippy::ref_option_ref)]    // NOTE: This may not be required if mockall gets updated for 2024 edition
    pub Payload{}
    impl Clone for Payload {
        fn clone(&self) -> Self;
    }
    impl PayloadSerialize for Payload {
        type Error = String;
        fn serialize(self) -> Result<SerializedPayload, String>;
        #[allow(clippy::ref_option_ref)]    // NOTE: This may not be required if mockall gets updated for 2024 edition
        fn deserialize<'a>(payload: &[u8], content_type: Option<&'a String>, format_indicator: &FormatIndicator) -> Result<Self, DeserializationError<String>>;
    }
}
#[cfg(test)]
use std::sync::Mutex;

// TODO: Remove this mutex. Find a better way to control test ordering
/// Mutex needed to check mock calls of static method `PayloadSerialize::deserialize`,
#[cfg(test)]
pub static DESERIALIZE_MTX: Mutex<()> = Mutex::new(());

#[cfg(test)]
mod tests {
    use test_case::test_case;

    use crate::common::payload_serialize::FormatIndicator;

    #[test_case(&FormatIndicator::UnspecifiedBytes; "UnspecifiedBytes")]
    #[test_case(&FormatIndicator::Utf8EncodedCharacterData; "Utf8EncodedCharacterData")]
    fn test_to_from_u8(prop: &FormatIndicator) {
        assert_eq!(
            prop,
            &FormatIndicator::try_from(Some(prop.clone() as u8)).unwrap()
        );
    }

    #[test_case(Some(0), &FormatIndicator::UnspecifiedBytes; "0_to_UnspecifiedBytes")]
    #[test_case(Some(1), &FormatIndicator::Utf8EncodedCharacterData; "1_to_Utf8EncodedCharacterData")]
    #[test_case(None, &FormatIndicator::UnspecifiedBytes; "None_to_UnspecifiedBytes")]
    fn test_from_option_u8_success(value: Option<u8>, expected: &FormatIndicator) {
        let res = FormatIndicator::try_from(value);
        assert!(res.is_ok());
        assert_eq!(expected, &res.unwrap());
    }

    #[test_case(Some(2); "2")]
    #[test_case(Some(255); "255")]
    fn test_from_option_u8_failure(value: Option<u8>) {
        assert!(&FormatIndicator::try_from(value).is_err());
    }
}