1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub enum InputPrimitive {
9 Rotate {
10 delta: f64,
11 },
12 Press,
13 Release,
14 LongPress,
15 Swipe {
16 direction: Direction,
17 },
18 Slide {
19 value: f64,
20 },
21 Hover {
22 proximity: f64,
23 },
24 Touch {
25 area: TouchArea,
26 },
27 LongTouch {
28 area: TouchArea,
29 },
30 KeyPress {
31 key: u32,
32 },
33 Button {
36 id: u8,
37 },
38 Fly {
45 direction: Direction,
46 },
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum Direction {
52 Up,
53 Down,
54 Left,
55 Right,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum TouchArea {
61 Top,
62 Bottom,
63 Left,
64 Right,
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70#[serde(tag = "type", rename_all = "snake_case")]
71pub enum Intent {
72 Play,
73 Pause,
74 PlayPause,
75 Stop,
76 Next,
77 Previous,
78 VolumeChange { delta: f64 },
79 VolumeSet { value: f64 },
80 Mute,
81 Unmute,
82 SeekRelative { seconds: f64 },
83 SeekAbsolute { seconds: f64 },
84 BrightnessChange { delta: f64 },
85 BrightnessSet { value: f64 },
86 ColorTemperatureChange { delta: f64 },
87 PowerToggle,
88 PowerOn,
89 PowerOff,
90}
91
92impl Intent {
93 pub fn split(&self) -> (String, serde_json::Value) {
99 match serde_json::to_value(self) {
100 Ok(serde_json::Value::Object(mut map)) => {
101 let name = map
102 .remove("type")
103 .and_then(|v| v.as_str().map(str::to_string))
104 .unwrap_or_else(|| "unknown".to_string());
105 (name, serde_json::Value::Object(map))
106 }
107 _ => ("unknown".to_string(), serde_json::json!({})),
108 }
109 }
110
111 pub fn reassemble(intent: &str, params: &serde_json::Value) -> Result<Self, serde_json::Error> {
116 let value = match params {
117 serde_json::Value::Object(map) => {
118 let mut m = map.clone();
119 m.insert(
120 "type".to_string(),
121 serde_json::Value::String(intent.to_string()),
122 );
123 serde_json::Value::Object(m)
124 }
125 _ => serde_json::json!({ "type": intent }),
126 };
127 serde_json::from_value(value)
128 }
129}
130
131impl InputPrimitive {
132 pub fn matches_route(&self, route_input: &str) -> bool {
135 match (self, route_input) {
136 (InputPrimitive::Rotate { .. }, "rotate") => true,
137 (InputPrimitive::Press, "press") => true,
138 (InputPrimitive::Release, "release") => true,
139 (InputPrimitive::LongPress, "long_press") => true,
140 (InputPrimitive::Slide { .. }, "slide") => true,
141 (InputPrimitive::Hover { .. }, "hover") => true,
142 (InputPrimitive::Swipe { direction }, s) => matches!(
143 (direction, s),
144 (Direction::Up, "swipe_up")
145 | (Direction::Down, "swipe_down")
146 | (Direction::Left, "swipe_left")
147 | (Direction::Right, "swipe_right")
148 ),
149 (InputPrimitive::Touch { area }, s) => matches!(
150 (area, s),
151 (TouchArea::Top, "touch_top")
152 | (TouchArea::Bottom, "touch_bottom")
153 | (TouchArea::Left, "touch_left")
154 | (TouchArea::Right, "touch_right")
155 ),
156 (InputPrimitive::LongTouch { area }, s) => matches!(
157 (area, s),
158 (TouchArea::Top, "long_touch_top")
159 | (TouchArea::Bottom, "long_touch_bottom")
160 | (TouchArea::Left, "long_touch_left")
161 | (TouchArea::Right, "long_touch_right")
162 ),
163 (InputPrimitive::Button { id }, s) => s
164 .strip_prefix("button_")
165 .and_then(|n| n.parse::<u8>().ok())
166 .is_some_and(|parsed| parsed == *id),
167 (InputPrimitive::Fly { direction }, s) => matches!(
168 (direction, s),
169 (Direction::Left, "fly_left") | (Direction::Right, "fly_right")
170 ),
171 _ => false,
172 }
173 }
174
175 pub fn continuous_value(&self) -> Option<f64> {
177 match self {
178 InputPrimitive::Rotate { delta } | InputPrimitive::Slide { value: delta } => {
179 Some(*delta)
180 }
181 _ => None,
182 }
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn rotate_matches_rotate_route() {
192 let r = InputPrimitive::Rotate { delta: 0.03 };
193 assert!(r.matches_route("rotate"));
194 assert!(!r.matches_route("press"));
195 }
196
197 #[test]
198 fn swipe_direction_matters() {
199 let s = InputPrimitive::Swipe {
200 direction: Direction::Right,
201 };
202 assert!(s.matches_route("swipe_right"));
203 assert!(!s.matches_route("swipe_left"));
204 assert!(!s.matches_route("swipe_up"));
205 }
206
207 #[test]
208 fn button_id_matches_numbered_route() {
209 let b = InputPrimitive::Button { id: 1 };
210 assert!(b.matches_route("button_1"));
211 assert!(!b.matches_route("button_2"));
212 assert!(!b.matches_route("button_"));
213 assert!(!b.matches_route("press"));
214 }
215
216 #[test]
217 fn button_route_rejects_non_numeric_suffix() {
218 let b = InputPrimitive::Button { id: 3 };
219 assert!(!b.matches_route("button_x"));
220 assert!(!b.matches_route("button_3a"));
221 assert!(b.matches_route("button_3"));
222 }
223
224 #[test]
225 fn fly_left_matches_only_fly_left_route() {
226 let f = InputPrimitive::Fly {
227 direction: Direction::Left,
228 };
229 assert!(f.matches_route("fly_left"));
230 assert!(!f.matches_route("fly_right"));
231 assert!(!f.matches_route("swipe_left"));
232 }
233
234 #[test]
235 fn fly_right_matches_only_fly_right_route() {
236 let f = InputPrimitive::Fly {
237 direction: Direction::Right,
238 };
239 assert!(f.matches_route("fly_right"));
240 assert!(!f.matches_route("fly_left"));
241 assert!(!f.matches_route("swipe_right"));
242 }
243
244 #[test]
245 fn fly_up_down_directions_do_not_match_any_fly_route() {
246 for dir in [Direction::Up, Direction::Down] {
251 let f = InputPrimitive::Fly { direction: dir };
252 assert!(!f.matches_route("fly_up"));
253 assert!(!f.matches_route("fly_down"));
254 assert!(!f.matches_route("fly_left"));
255 assert!(!f.matches_route("fly_right"));
256 }
257 }
258
259 #[test]
260 fn fly_does_not_collide_with_swipe_route() {
261 let fly = InputPrimitive::Fly {
262 direction: Direction::Left,
263 };
264 let swipe = InputPrimitive::Swipe {
265 direction: Direction::Left,
266 };
267 assert!(fly.matches_route("fly_left"));
268 assert!(!fly.matches_route("swipe_left"));
269 assert!(swipe.matches_route("swipe_left"));
270 assert!(!swipe.matches_route("fly_left"));
271 }
272
273 #[test]
274 fn split_payloadless_intent_yields_empty_params() {
275 let (name, params) = Intent::PlayPause.split();
276 assert_eq!(name, "play_pause");
277 assert_eq!(params, serde_json::json!({}));
278 }
279
280 #[test]
281 fn split_continuous_intent_carries_params() {
282 let (name, params) = Intent::VolumeChange { delta: 0.25 }.split();
283 assert_eq!(name, "volume_change");
284 assert_eq!(params, serde_json::json!({ "delta": 0.25 }));
285 }
286
287 #[test]
288 fn reassemble_payloadless_round_trips() {
289 let (name, params) = Intent::Play.split();
290 let recovered = Intent::reassemble(&name, ¶ms).unwrap();
291 assert_eq!(recovered, Intent::Play);
292 }
293
294 #[test]
295 fn reassemble_continuous_round_trips() {
296 let (name, params) = Intent::SeekRelative { seconds: -5.0 }.split();
297 let recovered = Intent::reassemble(&name, ¶ms).unwrap();
298 assert_eq!(recovered, Intent::SeekRelative { seconds: -5.0 });
299 }
300
301 #[test]
302 fn reassemble_unknown_name_errors() {
303 let err = Intent::reassemble("definitely_not_an_intent", &serde_json::json!({}));
304 assert!(err.is_err());
305 }
306}