1use serde_json::Value;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ValueType {
25 String,
27 Float,
29 Integer,
31 Bool,
33 Any,
35}
36
37impl ValueType {
38 pub fn matches(&self, value: &Value) -> bool {
40 match self {
41 Self::String => value.is_string(),
42 Self::Float => value.is_f64(),
43 Self::Integer => value.is_i64() || value.is_u64(),
44 Self::Bool => value.is_boolean(),
45 Self::Any => true,
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
52pub enum PayloadSpec {
53 None,
55 Value(ValueType),
57 Fields {
59 fields: Vec<(String, ValueType)>,
61 required: Vec<String>,
63 },
64}
65
66impl PayloadSpec {
67 pub fn validate(&self, value: &Value) -> bool {
71 match self {
72 Self::None => value.is_null(),
73 Self::Value(vt) => vt.matches(value),
74 Self::Fields { fields, required } => {
75 let obj = match value.as_object() {
76 Some(o) => o,
77 None => return false,
78 };
79
80 let has_required = required.iter().all(|r| obj.contains_key(r));
82
83 let types_ok = fields
85 .iter()
86 .all(|(name, vt)| obj.get(name).map(|v| vt.matches(v)).unwrap_or(true));
87
88 has_required && types_ok
89 }
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
96pub struct EventSpec {
97 pub family: String,
99 pub payload: PayloadSpec,
101}
102
103#[derive(Debug, Clone)]
105pub struct CommandSpec {
106 pub family: String,
108 pub payload: PayloadSpec,
110}
111
112pub trait WidgetCommandEncode {
117 fn to_wire(&self) -> (&'static str, crate::protocol::PropValue);
121
122 fn command_specs() -> Vec<CommandSpec>
124 where
125 Self: Sized;
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use serde_json::json;
132
133 #[test]
134 fn none_spec_validates_null() {
135 assert!(PayloadSpec::None.validate(&Value::Null));
136 assert!(!PayloadSpec::None.validate(&json!("hello")));
137 assert!(!PayloadSpec::None.validate(&json!(42)));
138 }
139
140 #[test]
141 fn value_spec_validates_type() {
142 let spec = PayloadSpec::Value(ValueType::Float);
143 assert!(spec.validate(&json!(2.5)));
144 assert!(!spec.validate(&json!("hello")));
145 assert!(!spec.validate(&Value::Null));
146
147 let spec = PayloadSpec::Value(ValueType::String);
148 assert!(spec.validate(&json!("hello")));
149 assert!(!spec.validate(&json!(42)));
150
151 let spec = PayloadSpec::Value(ValueType::Bool);
152 assert!(spec.validate(&json!(true)));
153 assert!(!spec.validate(&json!("true")));
154
155 let spec = PayloadSpec::Value(ValueType::Any);
156 assert!(spec.validate(&json!(42)));
157 assert!(spec.validate(&json!("hello")));
158 assert!(spec.validate(&Value::Null));
159 }
160
161 #[test]
162 fn fields_spec_validates_structure() {
163 let spec = PayloadSpec::Fields {
164 fields: vec![
165 ("x".into(), ValueType::Float),
166 ("y".into(), ValueType::Float),
167 ],
168 required: vec!["x".into(), "y".into()],
169 };
170
171 assert!(spec.validate(&json!({"x": 1.0, "y": 2.0})));
172 assert!(spec.validate(&json!({"x": 1.0, "y": 2.0, "extra": true})));
173 assert!(!spec.validate(&json!({"x": 1.0})));
174 assert!(!spec.validate(&json!({"x": "wrong", "y": 2.0})));
175 assert!(!spec.validate(&json!("not an object")));
176 }
177
178 #[test]
179 fn fields_spec_optional_fields() {
180 let spec = PayloadSpec::Fields {
181 fields: vec![
182 ("x".into(), ValueType::Float),
183 ("label".into(), ValueType::String),
184 ],
185 required: vec!["x".into()],
186 };
187
188 assert!(spec.validate(&json!({"x": 1.0})));
189 assert!(spec.validate(&json!({"x": 1.0, "label": "hello"})));
190 assert!(!spec.validate(&json!({"label": "hello"})));
191 }
192
193 #[test]
194 fn integer_type_matches_both_signed_and_unsigned() {
195 let spec = PayloadSpec::Value(ValueType::Integer);
196 assert!(spec.validate(&json!(42)));
197 assert!(spec.validate(&json!(-5)));
198 assert!(!spec.validate(&json!(2.5)));
199 assert!(!spec.validate(&json!("42")));
200 }
201}