use std::fmt::Debug;
#[repr(u8)]
#[derive(Clone, PartialEq, Debug, Default, Copy)]
pub enum FormatIndicator {
#[default]
UnspecifiedBytes = 0,
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"
)),
}
}
}
impl From<FormatIndicator> for azure_iot_operations_mqtt::control_packet::PayloadFormatIndicator {
fn from(value: FormatIndicator) -> Self {
match value {
FormatIndicator::UnspecifiedBytes => {
azure_iot_operations_mqtt::control_packet::PayloadFormatIndicator::Unspecified
}
FormatIndicator::Utf8EncodedCharacterData => {
azure_iot_operations_mqtt::control_packet::PayloadFormatIndicator::UTF8
}
}
}
}
impl From<azure_iot_operations_mqtt::control_packet::PayloadFormatIndicator> for FormatIndicator {
fn from(value: azure_iot_operations_mqtt::control_packet::PayloadFormatIndicator) -> Self {
match value {
azure_iot_operations_mqtt::control_packet::PayloadFormatIndicator::Unspecified => {
FormatIndicator::UnspecifiedBytes
}
azure_iot_operations_mqtt::control_packet::PayloadFormatIndicator::UTF8 => {
FormatIndicator::Utf8EncodedCharacterData
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SerializedPayload {
pub content_type: String,
pub format_indicator: FormatIndicator,
pub payload: Vec<u8>,
}
pub trait PayloadSerialize: Clone {
type Error: Debug + Into<Box<dyn std::error::Error + Sync + Send + 'static>>;
fn serialize(self) -> Result<SerializedPayload, Self::Error>;
fn deserialize(
payload: &[u8],
content_type: Option<&String>,
format_indicator: &FormatIndicator,
) -> Result<Self, DeserializationError<Self::Error>>;
}
#[derive(thiserror::Error, Debug)]
pub enum DeserializationError<T: Debug + Into<Box<dyn std::error::Error + Sync + Send + 'static>>> {
#[error(transparent)]
InvalidPayload(#[from] T),
#[error("Unsupported content type: {0}")]
UnsupportedContentType(String),
}
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,
payload: payload.to_vec(),
})
}
}
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
&& 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)] 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)] fn deserialize<'a>(payload: &[u8], content_type: Option<&'a String>, format_indicator: &FormatIndicator) -> Result<Self, DeserializationError<String>>;
}
}
#[cfg(test)]
use std::sync::Mutex;
#[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 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());
}
#[test_case(FormatIndicator::UnspecifiedBytes; "UnspecifiedBytes")]
#[test_case(FormatIndicator::Utf8EncodedCharacterData; "Utf8EncodedCharacterData")]
fn test_to_from_mqtt_format_indicator(prop: FormatIndicator) {
assert_eq!(
prop,
FormatIndicator::from(
azure_iot_operations_mqtt::control_packet::PayloadFormatIndicator::from(prop)
)
);
}
}