1use std::collections::HashMap;
2
3use serde_derive::{Deserialize, Serialize};
4
5#[derive(Debug, PartialEq, Eq, Hash, Clone, Deserialize, Serialize)]
7#[serde(transparent)]
8pub struct MenuId(String);
9
10pub type Graph = HashMap<MenuId, Menu>;
11
12#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
13pub struct Data {
14 pub root: Root,
15 pub graph: Graph,
16}
17
18impl Data {
19 const JSON: &'static str = include_str!("data.json");
21
22 pub fn load() -> Self {
24 serde_json::from_str(Self::JSON).unwrap()
25 }
26}
27
28#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
29pub struct Root {
30 #[serde(rename = "State")]
31 pub state: State,
32 #[serde(rename = "Menu")]
33 pub menu: Menu,
34}
35
36#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
37pub struct State {
38 #[serde(rename = "Tags")]
39 tags: HashMap<MenuId, String>,
40}
41
42impl State {
43 pub fn update(&mut self, action: &Action) {
44 self.tags.extend(action.set_tags.clone());
46 self.tags.retain(|k, _| !action.unset_tags.contains(k));
47 if !action.set_tags.is_empty() || !action.unset_tags.is_empty() {
48 eprintln!(
49 "Updated tags: {:#?}",
50 self.tags.keys().map(|tag| &tag.0).collect::<Vec<_>>()
51 );
52 }
53 }
54}
55
56#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
57pub struct Action {
58 #[serde(rename = "setTags")]
59 set_tags: HashMap<MenuId, String>,
60 #[serde(rename = "unsetTags")]
61 unset_tags: Vec<MenuId>,
62}
63
64#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
65pub struct MenuItem {
66 icon: Option<String>,
68 pub label: String,
70 pub display: Conditional,
72 pub active: Conditional,
74 pub reaction: Reaction,
76}
77
78#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
79#[serde(tag = "tag")]
80pub enum Conditional {
81 Always,
82 TagSet { contents: MenuId },
83 TagUnset { contents: MenuId },
84 TLNot { contents: Box<Conditional> },
85 TLAnd { contents: Vec<Conditional> },
86 TLOr { contents: Vec<Conditional> },
87}
88
89impl Conditional {
90 pub fn evaluate(&self, state: &State) -> bool {
91 match self {
92 Self::Always => true,
93 Self::TagSet { contents } => state.tags.contains_key(contents),
94 Self::TagUnset { contents } => !state.tags.contains_key(contents),
95 Self::TLNot { contents } => !contents.evaluate(state),
96 Self::TLAnd { contents } => contents.iter().all(|item| item.evaluate(state)),
97 Self::TLOr { contents } => contents.iter().any(|item| item.evaluate(state)),
98 }
99 }
100}
101
102#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
103#[serde(tag = "tag")]
104pub enum Reaction {
105 SubMenu {
106 #[serde(rename = "onAction")]
107 on_hover: Action,
108 #[serde(flatten)]
109 submenu: SubMenu,
110 },
111 #[serde(rename = "Action")]
112 ClickAction {
113 #[serde(rename = "onAction")]
114 on_action: Action,
115 act: Option<ClickAction>,
116 },
117}
118
119#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
120pub struct SubMenu {
121 #[serde(rename = "subMenu")]
122 sub_menu: MenuId,
123 #[serde(rename = "subIdPostfix")]
124 sub_id_postfix: Option<MenuId>,
125}
126
127impl SubMenu {
128 pub fn id(&self, state: &State) -> MenuId {
129 if let Some(postfix_id) = &self.sub_id_postfix {
130 if let Some(postfix) = state.tags.get(postfix_id) {
131 MenuId(format!("{}{}", self.sub_menu.0, postfix))
132 } else {
133 self.sub_menu.clone()
135 }
136 } else {
137 self.sub_menu.clone()
138 }
139 }
140}
141
142#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
143#[serde(tag = "tag")]
144pub enum ClickAction {
145 ColapseMenu,
146 Nav {
147 url: String,
148 },
149 Download {
150 url: String,
151 filename: String,
152 },
153 JSCall {
154 #[serde(rename = "jsCall")]
155 js_call: String,
156 },
157}
158
159#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
160pub struct Menu {
161 pub id: MenuId,
162 #[serde(rename = "onLeave")]
163 pub on_leave: Action,
164 pub entries: Vec<MenuItem>,
165}
166
167#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn everything_is_parsed() {
174 let expected: serde_json::Value = serde_json::from_str(Data::JSON).unwrap();
175
176 let parsed = Data::load();
177 let after_roundtrip = serde_json::to_value(parsed).unwrap();
178
179 assert_eq!(expected, after_roundtrip);
180 }
181
182 #[test]
183 fn no_icons() {
184 let data = Data::load();
185
186 assert!(data
187 .graph
188 .values()
189 .flat_map(|menu| &menu.entries)
190 .all(|entry| entry.icon.is_none()));
191 }
192
193 #[test]
194 fn root_menu_is_same_as_in_graph() {
195 let data = Data::load();
196
197 assert_eq!(data.root.menu, data.graph[&data.root.menu.id]);
198 }
199
200 #[test]
201 fn root_menu_has_no_on_leave() {
202 let data = Data::load();
203
204 assert_eq!(data.root.menu.on_leave, Action::default());
205 }
206
207 #[test]
208 fn root_menu_entries_has_no_active() {
209 let data = Data::load();
210
211 assert!(data
212 .root
213 .menu
214 .entries
215 .iter()
216 .all(|entry| entry.active == Conditional::Always));
217 }
218
219 #[test]
220 fn root_menu_entries_is_submenu_and_has_no_hover() {
221 let data = Data::load();
222
223 assert!(data.root.menu.entries.iter().all(|entry| matches!(
224 &entry.reaction,
225 Reaction::SubMenu {
226 on_hover,
227 ..
228 } if on_hover == &Action::default()
229 )));
230 }
231}