use std::collections::BTreeMap;
use base64::prelude::*;
use serde::{
Deserialize, Deserializer, Serialize,
de::{self, MapAccess, Visitor},
};
use serde_with::serde_as;
#[derive(Debug, thiserror::Error)]
pub enum DecodeError {
#[error("Parameter is not a byte array")]
WrongType,
#[error(transparent)]
Base64(#[from] base64::DecodeError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ParameterType {
ByteArray,
Float64,
Float64Array,
}
#[serde_as]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ParameterValue {
Integer(i64),
Float64(f64),
Bool(bool),
String(String),
Array(Vec<ParameterValue>),
Dict(BTreeMap<String, ParameterValue>),
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Parameter {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub r#type: Option<ParameterType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<ParameterValue>,
}
impl<'de> Deserialize<'de> for Parameter {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ParameterVisitor;
impl<'de> Visitor<'de> for ParameterVisitor {
type Value = Parameter;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a parameter object")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut name: Option<String> = None;
let mut r#type: Option<ParameterType> = None;
let mut value: Option<serde_json::Value> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"name" => {
name = Some(map.next_value()?);
}
"type" => {
r#type = Some(map.next_value()?);
}
"value" => {
value = Some(map.next_value()?);
}
_ => {
return Err(de::Error::unknown_field(&key, &["name", "type", "value"]));
}
}
}
let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
let parameter_value = match (r#type.as_ref(), value) {
(Some(ParameterType::Float64), Some(val)) => {
Some(convert_to_float64_value(val).map_err(de::Error::custom)?)
}
(Some(ParameterType::Float64Array), Some(val)) => {
Some(convert_to_float64_array_value(val).map_err(de::Error::custom)?)
}
(Some(ParameterType::ByteArray), Some(val)) => {
Some(convert_to_byte_array_value(val).map_err(de::Error::custom)?)
}
(_, Some(val)) => {
Some(convert_value_with_homogenization(val).map_err(de::Error::custom)?)
}
(_, None) => None,
};
Ok(Parameter {
name,
r#type,
value: parameter_value,
})
}
}
deserializer.deserialize_map(ParameterVisitor)
}
}
fn convert_to_float64_value(value: serde_json::Value) -> Result<ParameterValue, String> {
match value {
serde_json::Value::Number(n) => {
if let Some(f) = n.as_f64() {
Ok(ParameterValue::Float64(f))
} else {
Err("Invalid number for float64".to_string())
}
}
_ => {
Err("Non-numeric value had type set to float64".to_string())
}
}
}
fn convert_to_float64_array_value(value: serde_json::Value) -> Result<ParameterValue, String> {
match value {
serde_json::Value::Array(arr) => {
let mut float_values = Vec::new();
for item in arr {
match item {
serde_json::Value::Number(n) => {
if let Some(f) = n.as_f64() {
float_values.push(ParameterValue::Float64(f));
} else {
return Err("Invalid number in float64 array".to_string());
}
}
_ => {
return Err("Non-numeric value in float64 array".to_string());
}
}
}
Ok(ParameterValue::Array(float_values))
}
_ => {
Err("Value with type set to float64_array was not an array of numbers".to_string())
}
}
}
fn convert_to_byte_array_value(value: serde_json::Value) -> Result<ParameterValue, String> {
match value {
serde_json::Value::String(s) => {
if let Err(e) = BASE64_STANDARD.decode(&s) {
return Err(e.to_string());
}
Ok(ParameterValue::String(s))
}
_ => {
Err("Value with type set to byte_array was not a string".to_string())
}
}
}
fn convert_value_with_homogenization(value: serde_json::Value) -> Result<ParameterValue, String> {
match value {
serde_json::Value::Array(arr) => {
let mut has_int = false;
let mut has_float = false;
let mut has_other = false;
for item in &arr {
if item.is_i64() {
has_int = true;
} else if item.is_f64() {
has_float = true;
} else {
has_other = true;
}
}
if (has_float || has_int) && has_other {
Err("Array contains a mix of numeric and non-numeric-values".to_string())
} else if has_int && has_float {
let mut float_values = Vec::new();
for item in arr {
match item {
serde_json::Value::Number(n) => {
if let Some(f) = n.as_f64() {
float_values.push(ParameterValue::Float64(f));
} else {
return Err("Invalid number in mixed array".to_string());
}
}
_ => {
unreachable!()
}
}
}
Ok(ParameterValue::Array(float_values))
} else {
serde_json::from_value(serde_json::Value::Array(arr)).map_err(|e| e.to_string())
}
}
_ => {
serde_json::from_value(value).map_err(|e| e.to_string())
}
}
}
impl Parameter {
pub fn empty(name: impl Into<String>) -> Self {
Self {
name: name.into(),
r#type: None,
value: None,
}
}
pub fn float64(name: impl Into<String>, value: f64) -> Self {
Self {
name: name.into(),
r#type: Some(ParameterType::Float64),
value: Some(ParameterValue::Float64(value)),
}
}
pub fn integer(name: impl Into<String>, value: i64) -> Self {
Self {
name: name.into(),
r#type: None,
value: Some(ParameterValue::Integer(value)),
}
}
pub fn integer_array(name: impl Into<String>, values: impl IntoIterator<Item = i64>) -> Self {
Self {
name: name.into(),
r#type: None,
value: Some(ParameterValue::Array(
values.into_iter().map(ParameterValue::Integer).collect(),
)),
}
}
pub fn float64_array(name: impl Into<String>, values: impl IntoIterator<Item = f64>) -> Self {
Self {
name: name.into(),
r#type: Some(ParameterType::Float64Array),
value: Some(ParameterValue::Array(
values.into_iter().map(ParameterValue::Float64).collect(),
)),
}
}
pub fn string(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
r#type: None,
value: Some(ParameterValue::String(value.into())),
}
}
pub fn byte_array(name: impl Into<String>, data: &[u8]) -> Self {
let value = BASE64_STANDARD.encode(data);
Self {
name: name.into(),
r#type: Some(ParameterType::ByteArray),
value: Some(ParameterValue::String(value)),
}
}
pub fn bool(name: impl Into<String>, value: bool) -> Self {
Self {
name: name.into(),
r#type: None,
value: Some(ParameterValue::Bool(value)),
}
}
pub fn dict(name: impl Into<String>, value: BTreeMap<String, ParameterValue>) -> Self {
Self {
name: name.into(),
r#type: None,
value: Some(ParameterValue::Dict(value)),
}
}
pub fn decode_byte_array(&self) -> Result<Option<Vec<u8>>, DecodeError> {
match (self.r#type, self.value.as_ref()) {
(Some(ParameterType::ByteArray), Some(ParameterValue::String(s))) => {
Some(BASE64_STANDARD.decode(s).map_err(DecodeError::Base64)).transpose()
}
(_, None) => Ok(None),
_ => Err(DecodeError::WrongType),
}
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::*;
#[test]
fn test_empty() {
insta::assert_json_snapshot!(Parameter::empty("test"));
}
#[test]
fn test_float() {
insta::assert_json_snapshot!(Parameter::float64("f64", 1.23));
}
#[test]
fn test_float_array() {
insta::assert_json_snapshot!(Parameter::float64_array("f64[]", [1.23, 4.56]));
}
#[test]
fn test_integer() {
insta::assert_json_snapshot!(Parameter::integer("i64", 123));
}
#[test]
fn test_integer_array() {
insta::assert_json_snapshot!(Parameter::integer_array("i64[]", [123, 456]));
}
#[test]
fn test_string() {
insta::assert_json_snapshot!(Parameter::string("string", "howdy"));
}
#[test]
fn test_byte_array() {
insta::assert_json_snapshot!(Parameter::byte_array("byte[]", &[0x10, 0x20, 0x30]));
}
#[test]
fn test_deserialize_integer() {
let json = r#"{"name": "test", "value": 123}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Integer(_)));
assert_eq!(param.value.unwrap(), ParameterValue::Integer(123));
assert_eq!(param.r#type, None);
}
#[test]
fn test_deserialize_integer_array() {
let json = r#"{"name": "test", "value": [123, 456]}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Array(_)));
assert_eq!(
param.value.unwrap(),
ParameterValue::Array(vec![
ParameterValue::Integer(123),
ParameterValue::Integer(456)
])
);
}
#[test]
fn test_deserialize_integer_marked_as_float64() {
let json = r#"{"name": "test", "value": 123, "type": "float64"}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Float64(_)));
assert_eq!(param.value.unwrap(), ParameterValue::Float64(123.0));
assert_eq!(param.r#type, Some(ParameterType::Float64));
}
#[test]
fn test_deserialize_integer_array_marked_as_float64() {
let json = r#"{"name": "test", "value": [123, 456], "type": "float64_array"}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Array(_)));
assert_eq!(param.r#type, Some(ParameterType::Float64Array));
assert_eq!(
param.value.unwrap(),
ParameterValue::Array(vec![
ParameterValue::Float64(123.0),
ParameterValue::Float64(456.0),
])
);
}
#[test]
fn test_deserialize_float64() {
let json = r#"{"name": "test", "value": 1.23}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Float64(_)));
assert_eq!(param.value.unwrap(), ParameterValue::Float64(1.23));
}
#[test]
fn test_deserialize_float64_array() {
let json = r#"{"name": "test", "value": [1.23, 4.56]}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Array(_)));
assert_eq!(
param.value.unwrap(),
ParameterValue::Array(vec![
ParameterValue::Float64(1.23),
ParameterValue::Float64(4.56),
])
);
}
#[test]
fn test_deserialize_numeric_parameter_with_zero_fractional_part() {
let json = r#"{"name": "test", "value": 1.0}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Float64(_)));
assert_eq!(param.value.unwrap(), ParameterValue::Float64(1.0));
}
#[test]
fn test_deserialize_numeric_array_with_zero_fractional_part() {
let json = r#"{"name": "test", "value": [1.0, 2.0]}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Array(_)));
assert_eq!(
param.value.unwrap(),
ParameterValue::Array(vec![
ParameterValue::Float64(1.0),
ParameterValue::Float64(2.0)
])
);
}
#[test]
fn test_deserialize_numeric_array_with_heterogeneous_elements() {
let json = r#"{"name": "test", "value": [1, 2.0]}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Array(_)));
assert_eq!(
param.value.unwrap(),
ParameterValue::Array(vec![
ParameterValue::Float64(1.0),
ParameterValue::Float64(2.0)
])
);
}
#[test]
fn test_deserialize_boolean() {
let json = r#"{"name": "test", "value": true}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Bool(_)));
assert_eq!(param.value.unwrap(), ParameterValue::Bool(true));
}
#[test]
fn test_deserialize_boolean_array() {
let json = r#"{"name": "test", "value": [true, false]}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::Array(_)));
assert_eq!(
param.value.unwrap(),
ParameterValue::Array(vec![
ParameterValue::Bool(true),
ParameterValue::Bool(false)
])
);
}
#[test]
fn test_deserialize_byte_array() {
let json = r#"{"name": "test", "value": "Rm94Z2xvdmUgcnVsZXMh", "type": "byte_array"}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_matches!(param.value, Some(ParameterValue::String(_)));
assert_eq!(
param.decode_byte_array().unwrap().unwrap(),
b"Foxglove rules!".to_vec()
);
}
#[test]
fn test_deserialize_undefined_parameter() {
let json = r#"{"name": "test"}"#;
let param = serde_json::from_str::<Parameter>(json).unwrap();
assert_eq!(param.name, "test");
assert_matches!(param.value, None);
assert_matches!(param.r#type, None);
}
#[test]
fn test_deserialize_array_with_mixed_types() {
let json = r#"{"name": "test", "value": [1, 2.0, "three"]}"#;
let param_result = serde_json::from_str::<Parameter>(json);
assert_matches!(param_result, Err(_));
}
#[test]
fn test_deserialize_invalid_float64_array() {
let json = r#"{"name": "test", "value": [true, false, true], type: "float64_array"}"#;
let param_result = serde_json::from_str::<Parameter>(json);
assert_matches!(param_result, Err(_));
}
#[test]
fn test_deserialize_invalid_float64_value() {
let json = r#"{"name": "test", "value": "three point one four one five", type: "float64"}"#;
let param_result = serde_json::from_str::<Parameter>(json);
assert_matches!(param_result, Err(_));
}
#[test]
fn test_deserialize_invalid_byte_array() {
let json = r#"{"name": "test", "value": "!!!!", "type": "byte_array"}"#;
let param_result = serde_json::from_str::<Parameter>(json);
assert_matches!(param_result, Err(_));
}
#[test]
fn test_decode_byte_array() {
let param = Parameter::byte_array("bytes", b"123");
let decoded = param.decode_byte_array().unwrap().unwrap();
assert_eq!(decoded, b"123".to_vec());
let param = Parameter {
name: "invalid".into(),
r#type: Some(ParameterType::ByteArray),
value: Some(ParameterValue::String("!!".into())),
};
let result = param.decode_byte_array();
assert_matches!(result, Err(DecodeError::Base64(_)));
let param = Parameter::string("string", "eHl6enk=");
let result = param.decode_byte_array();
assert_matches!(result, Err(DecodeError::WrongType));
let param = Parameter {
name: "unset".into(),
r#type: Some(ParameterType::ByteArray),
value: None,
};
let result = param.decode_byte_array();
assert_matches!(result, Ok(None));
let param = Parameter {
name: "unset".into(),
r#type: None,
value: None,
};
let result = param.decode_byte_array();
assert_matches!(result, Ok(None));
}
#[test]
fn test_bool() {
insta::assert_json_snapshot!(Parameter::bool("bool", true));
}
#[test]
fn test_dict() {
insta::assert_json_snapshot!(Parameter::dict(
"outer",
maplit::btreemap! {
"bool".into() => ParameterValue::Bool(false),
"nested".into() => ParameterValue::Dict(
maplit::btreemap! {
"inner".into() => ParameterValue::Float64(1.0),
}
),
"float64".into() => ParameterValue::Float64(1.23),
}
));
}
}