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());
}
}