use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueType {
String,
Float,
Integer,
Bool,
Any,
}
impl ValueType {
pub fn matches(&self, value: &Value) -> bool {
match self {
Self::String => value.is_string(),
Self::Float => value.is_f64(),
Self::Integer => value.is_i64() || value.is_u64(),
Self::Bool => value.is_boolean(),
Self::Any => true,
}
}
}
#[derive(Debug, Clone)]
pub enum PayloadSpec {
None,
Value(ValueType),
Fields {
fields: Vec<(String, ValueType)>,
required: Vec<String>,
},
}
impl PayloadSpec {
pub fn validate(&self, value: &Value) -> bool {
match self {
Self::None => value.is_null(),
Self::Value(vt) => vt.matches(value),
Self::Fields { fields, required } => {
let obj = match value.as_object() {
Some(o) => o,
None => return false,
};
let has_required = required.iter().all(|r| obj.contains_key(r));
let types_ok = fields
.iter()
.all(|(name, vt)| obj.get(name).map(|v| vt.matches(v)).unwrap_or(true));
has_required && types_ok
}
}
}
}
#[derive(Debug, Clone)]
pub struct EventSpec {
pub family: String,
pub payload: PayloadSpec,
}
#[derive(Debug, Clone)]
pub struct CommandSpec {
pub family: String,
pub payload: PayloadSpec,
}
pub trait WidgetCommandEncode {
fn to_wire(&self) -> (&'static str, crate::protocol::PropValue);
fn command_specs() -> Vec<CommandSpec>
where
Self: Sized;
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn none_spec_validates_null() {
assert!(PayloadSpec::None.validate(&Value::Null));
assert!(!PayloadSpec::None.validate(&json!("hello")));
assert!(!PayloadSpec::None.validate(&json!(42)));
}
#[test]
fn value_spec_validates_type() {
let spec = PayloadSpec::Value(ValueType::Float);
assert!(spec.validate(&json!(2.5)));
assert!(!spec.validate(&json!("hello")));
assert!(!spec.validate(&Value::Null));
let spec = PayloadSpec::Value(ValueType::String);
assert!(spec.validate(&json!("hello")));
assert!(!spec.validate(&json!(42)));
let spec = PayloadSpec::Value(ValueType::Bool);
assert!(spec.validate(&json!(true)));
assert!(!spec.validate(&json!("true")));
let spec = PayloadSpec::Value(ValueType::Any);
assert!(spec.validate(&json!(42)));
assert!(spec.validate(&json!("hello")));
assert!(spec.validate(&Value::Null));
}
#[test]
fn fields_spec_validates_structure() {
let spec = PayloadSpec::Fields {
fields: vec![
("x".into(), ValueType::Float),
("y".into(), ValueType::Float),
],
required: vec!["x".into(), "y".into()],
};
assert!(spec.validate(&json!({"x": 1.0, "y": 2.0})));
assert!(spec.validate(&json!({"x": 1.0, "y": 2.0, "extra": true})));
assert!(!spec.validate(&json!({"x": 1.0})));
assert!(!spec.validate(&json!({"x": "wrong", "y": 2.0})));
assert!(!spec.validate(&json!("not an object")));
}
#[test]
fn fields_spec_optional_fields() {
let spec = PayloadSpec::Fields {
fields: vec![
("x".into(), ValueType::Float),
("label".into(), ValueType::String),
],
required: vec!["x".into()],
};
assert!(spec.validate(&json!({"x": 1.0})));
assert!(spec.validate(&json!({"x": 1.0, "label": "hello"})));
assert!(!spec.validate(&json!({"label": "hello"})));
}
#[test]
fn integer_type_matches_both_signed_and_unsigned() {
let spec = PayloadSpec::Value(ValueType::Integer);
assert!(spec.validate(&json!(42)));
assert!(spec.validate(&json!(-5)));
assert!(!spec.validate(&json!(2.5)));
assert!(!spec.validate(&json!("42")));
}
}