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}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum Direction {
43 Up,
44 Down,
45 Left,
46 Right,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum TouchArea {
52 Top,
53 Bottom,
54 Left,
55 Right,
56}
57
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61#[serde(tag = "type", rename_all = "snake_case")]
62pub enum Intent {
63 Play,
64 Pause,
65 PlayPause,
66 Stop,
67 Next,
68 Previous,
69 VolumeChange { delta: f64 },
70 VolumeSet { value: f64 },
71 Mute,
72 Unmute,
73 SeekRelative { seconds: f64 },
74 SeekAbsolute { seconds: f64 },
75 BrightnessChange { delta: f64 },
76 BrightnessSet { value: f64 },
77 ColorTemperatureChange { delta: f64 },
78 PowerToggle,
79 PowerOn,
80 PowerOff,
81}
82
83impl Intent {
84 pub fn split(&self) -> (String, serde_json::Value) {
90 match serde_json::to_value(self) {
91 Ok(serde_json::Value::Object(mut map)) => {
92 let name = map
93 .remove("type")
94 .and_then(|v| v.as_str().map(str::to_string))
95 .unwrap_or_else(|| "unknown".to_string());
96 (name, serde_json::Value::Object(map))
97 }
98 _ => ("unknown".to_string(), serde_json::json!({})),
99 }
100 }
101
102 pub fn reassemble(intent: &str, params: &serde_json::Value) -> Result<Self, serde_json::Error> {
107 let value = match params {
108 serde_json::Value::Object(map) => {
109 let mut m = map.clone();
110 m.insert(
111 "type".to_string(),
112 serde_json::Value::String(intent.to_string()),
113 );
114 serde_json::Value::Object(m)
115 }
116 _ => serde_json::json!({ "type": intent }),
117 };
118 serde_json::from_value(value)
119 }
120}
121
122impl InputPrimitive {
123 pub fn matches_route(&self, route_input: &str) -> bool {
126 match (self, route_input) {
127 (InputPrimitive::Rotate { .. }, "rotate") => true,
128 (InputPrimitive::Press, "press") => true,
129 (InputPrimitive::Release, "release") => true,
130 (InputPrimitive::LongPress, "long_press") => true,
131 (InputPrimitive::Slide { .. }, "slide") => true,
132 (InputPrimitive::Hover { .. }, "hover") => true,
133 (InputPrimitive::Swipe { direction }, s) => matches!(
134 (direction, s),
135 (Direction::Up, "swipe_up")
136 | (Direction::Down, "swipe_down")
137 | (Direction::Left, "swipe_left")
138 | (Direction::Right, "swipe_right")
139 ),
140 (InputPrimitive::Touch { area }, s) => matches!(
141 (area, s),
142 (TouchArea::Top, "touch_top")
143 | (TouchArea::Bottom, "touch_bottom")
144 | (TouchArea::Left, "touch_left")
145 | (TouchArea::Right, "touch_right")
146 ),
147 (InputPrimitive::LongTouch { area }, s) => matches!(
148 (area, s),
149 (TouchArea::Top, "long_touch_top")
150 | (TouchArea::Bottom, "long_touch_bottom")
151 | (TouchArea::Left, "long_touch_left")
152 | (TouchArea::Right, "long_touch_right")
153 ),
154 (InputPrimitive::Button { id }, s) => s
155 .strip_prefix("button_")
156 .and_then(|n| n.parse::<u8>().ok())
157 .is_some_and(|parsed| parsed == *id),
158 _ => false,
159 }
160 }
161
162 pub fn continuous_value(&self) -> Option<f64> {
164 match self {
165 InputPrimitive::Rotate { delta } | InputPrimitive::Slide { value: delta } => {
166 Some(*delta)
167 }
168 _ => None,
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn rotate_matches_rotate_route() {
179 let r = InputPrimitive::Rotate { delta: 0.03 };
180 assert!(r.matches_route("rotate"));
181 assert!(!r.matches_route("press"));
182 }
183
184 #[test]
185 fn swipe_direction_matters() {
186 let s = InputPrimitive::Swipe {
187 direction: Direction::Right,
188 };
189 assert!(s.matches_route("swipe_right"));
190 assert!(!s.matches_route("swipe_left"));
191 assert!(!s.matches_route("swipe_up"));
192 }
193
194 #[test]
195 fn button_id_matches_numbered_route() {
196 let b = InputPrimitive::Button { id: 1 };
197 assert!(b.matches_route("button_1"));
198 assert!(!b.matches_route("button_2"));
199 assert!(!b.matches_route("button_"));
200 assert!(!b.matches_route("press"));
201 }
202
203 #[test]
204 fn button_route_rejects_non_numeric_suffix() {
205 let b = InputPrimitive::Button { id: 3 };
206 assert!(!b.matches_route("button_x"));
207 assert!(!b.matches_route("button_3a"));
208 assert!(b.matches_route("button_3"));
209 }
210
211 #[test]
212 fn split_payloadless_intent_yields_empty_params() {
213 let (name, params) = Intent::PlayPause.split();
214 assert_eq!(name, "play_pause");
215 assert_eq!(params, serde_json::json!({}));
216 }
217
218 #[test]
219 fn split_continuous_intent_carries_params() {
220 let (name, params) = Intent::VolumeChange { delta: 0.25 }.split();
221 assert_eq!(name, "volume_change");
222 assert_eq!(params, serde_json::json!({ "delta": 0.25 }));
223 }
224
225 #[test]
226 fn reassemble_payloadless_round_trips() {
227 let (name, params) = Intent::Play.split();
228 let recovered = Intent::reassemble(&name, ¶ms).unwrap();
229 assert_eq!(recovered, Intent::Play);
230 }
231
232 #[test]
233 fn reassemble_continuous_round_trips() {
234 let (name, params) = Intent::SeekRelative { seconds: -5.0 }.split();
235 let recovered = Intent::reassemble(&name, ¶ms).unwrap();
236 assert_eq!(recovered, Intent::SeekRelative { seconds: -5.0 });
237 }
238
239 #[test]
240 fn reassemble_unknown_name_errors() {
241 let err = Intent::reassemble("definitely_not_an_intent", &serde_json::json!({}));
242 assert!(err.is_err());
243 }
244}