1use esp_metadata::Chip;
2
3use crate::template::{GeneratorOption, GeneratorOptionItem};
4
5#[derive(Debug)]
6pub struct ActiveConfiguration {
7 pub chip: Chip,
9 pub selected: Vec<String>,
11 pub options: Vec<GeneratorOptionItem>,
13}
14
15impl ActiveConfiguration {
16 pub fn is_group_selected(&self, group: &str) -> bool {
17 self.selected.iter().any(|s| {
18 let option = find_option(s, &self.options).unwrap();
19 option.selection_group == group
20 })
21 }
22
23 pub fn is_selected(&self, option: &str) -> bool {
24 self.selected_index(option).is_some()
25 }
26
27 pub fn selected_index(&self, option: &str) -> Option<usize> {
28 self.selected.iter().position(|s| s == option)
29 }
30
31 fn deselect_group(
34 selected: &mut Vec<String>,
35 options: &[GeneratorOptionItem],
36 group: &str,
37 ) -> bool {
38 if group.is_empty() {
40 return true;
41 }
42
43 if !selected.iter().all(|s| {
45 let o = find_option(s, options).unwrap();
46 if o.selection_group == group {
47 Self::can_be_disabled_impl(selected, options, s, true)
50 } else {
51 true
52 }
53 }) {
54 return false;
55 }
56
57 selected.retain(|s| {
58 let option = find_option(s, options).unwrap();
59 option.selection_group != group
60 });
61
62 true
63 }
64
65 pub fn select(&mut self, option: String) {
66 let o = find_option(&option, &self.options).unwrap();
67 if !self.is_option_active(o) {
68 return;
69 }
70 if !Self::deselect_group(&mut self.selected, &self.options, &o.selection_group) {
71 return;
72 }
73 self.selected.push(option);
74 }
75
76 pub fn is_active(&self, item: &GeneratorOptionItem) -> bool {
80 match item {
81 GeneratorOptionItem::Category(category) => {
82 if !self.requirements_met(&category.requires) {
83 return false;
84 }
85 for sub in category.options.iter() {
86 if self.is_active(sub) {
87 return true;
88 }
89 }
90 false
91 }
92 GeneratorOptionItem::Option(option) => self.is_option_active(option),
93 }
94 }
95
96 fn requirements_met(&self, requires: &[String]) -> bool {
107 for requirement in requires {
108 let (key, expected) = if let Some(requirement) = requirement.strip_prefix('!') {
109 (requirement, false)
110 } else {
111 (requirement.as_str(), true)
112 };
113
114 if self.is_selected(key) == expected {
116 continue;
117 }
118
119 let is_group = Self::group_exists(key, &self.options);
121 if is_group && self.is_group_selected(key) == expected {
122 continue;
123 }
124
125 return false;
126 }
127
128 true
129 }
130
131 pub fn is_option_active(&self, option: &GeneratorOption) -> bool {
136 if !option.chips.is_empty() && !option.chips.contains(&self.chip) {
137 return false;
138 }
139
140 if !self.requirements_met(&option.requires) {
142 return false;
143 }
144
145 for selected in self.selected.iter() {
147 let Some(selected_option) = find_option(selected, &self.options) else {
148 ratatui::restore();
149 panic!("selected option not found: {selected}");
150 };
151
152 for requirement in selected_option.requires.iter() {
153 if let Some(requirement) = requirement.strip_prefix('!') {
154 if requirement == option.name {
155 return false;
156 }
157 }
158 }
159 }
160
161 true
162 }
163
164 pub fn can_be_disabled(&self, option: &str) -> bool {
166 Self::can_be_disabled_impl(&self.selected, &self.options, option, false)
167 }
168
169 fn can_be_disabled_impl(
170 selected: &[String],
171 options: &[GeneratorOptionItem],
172 option: &str,
173 allow_deselecting_group: bool,
174 ) -> bool {
175 let op = find_option(option, options).unwrap();
176 for selected in selected.iter() {
177 let selected_option = find_option(selected, options).unwrap();
178 if selected_option
179 .requires
180 .iter()
181 .any(|o| o == option || (o == &op.selection_group && !allow_deselecting_group))
182 {
183 return false;
184 }
185 }
186 true
187 }
188
189 pub fn collect_relationships<'a>(
190 &'a self,
191 option: &'a GeneratorOptionItem,
192 ) -> Relationships<'a> {
193 let mut requires = Vec::new();
194 let mut required_by = Vec::new();
195 let mut disabled_by = Vec::new();
196
197 self.selected.iter().for_each(|opt| {
198 let opt = find_option(opt.as_str(), &self.options).unwrap();
199 for o in opt.requires.iter() {
200 if let Some(disables) = o.strip_prefix("!") {
201 if disables == option.name() {
202 disabled_by.push(opt.name.as_str());
203 }
204 } else if o == option.name() {
205 required_by.push(opt.name.as_str());
206 }
207 }
208 });
209 for req in option.requires() {
210 if let Some(disables) = req.strip_prefix("!") {
211 if self.is_selected(disables) {
212 disabled_by.push(disables);
213 }
214 } else {
215 requires.push(req.as_str());
216 }
217 }
218
219 Relationships {
220 requires,
221 required_by,
222 disabled_by,
223 }
224 }
225
226 fn group_exists(key: &str, options: &[GeneratorOptionItem]) -> bool {
227 options.iter().any(|o| match o {
228 GeneratorOptionItem::Option(o) => o.selection_group == key,
229 GeneratorOptionItem::Category(c) => Self::group_exists(key, &c.options),
230 })
231 }
232}
233
234pub struct Relationships<'a> {
235 pub requires: Vec<&'a str>,
236 pub required_by: Vec<&'a str>,
237 pub disabled_by: Vec<&'a str>,
238}
239
240pub fn find_option<'c>(
241 option: &str,
242 options: &'c [GeneratorOptionItem],
243) -> Option<&'c GeneratorOption> {
244 for item in options {
245 match item {
246 GeneratorOptionItem::Category(category) => {
247 let found_option = find_option(option, &category.options);
248 if found_option.is_some() {
249 return found_option;
250 }
251 }
252 GeneratorOptionItem::Option(item) => {
253 if item.name == option {
254 return Some(item);
255 }
256 }
257 }
258 }
259 None
260}
261
262#[cfg(test)]
263mod test {
264 use esp_metadata::Chip;
265
266 use crate::{
267 config::{ActiveConfiguration, find_option},
268 template::{GeneratorOption, GeneratorOptionCategory, GeneratorOptionItem},
269 };
270
271 #[test]
272 fn required_by_and_requires_pick_the_right_options() {
273 let options = vec![
274 GeneratorOptionItem::Option(GeneratorOption {
275 name: "option1".to_string(),
276 display_name: "Foobar".to_string(),
277 selection_group: "".to_string(),
278 help: "".to_string(),
279 chips: vec![Chip::Esp32],
280 requires: vec!["option2".to_string()],
281 }),
282 GeneratorOptionItem::Option(GeneratorOption {
283 name: "option2".to_string(),
284 display_name: "Barfoo".to_string(),
285 selection_group: "".to_string(),
286 help: "".to_string(),
287 chips: vec![Chip::Esp32],
288 requires: vec![],
289 }),
290 ];
291 let active = ActiveConfiguration {
292 chip: Chip::Esp32,
293 selected: vec!["option1".to_string()],
294 options,
295 };
296
297 let rels = active.collect_relationships(&active.options[0]);
298 assert_eq!(rels.requires, &["option2"]);
299 assert_eq!(rels.required_by, <&[&str]>::default());
300
301 let rels = active.collect_relationships(&active.options[1]);
302 assert_eq!(rels.requires, <&[&str]>::default());
303 assert_eq!(rels.required_by, &["option1"]);
304 }
305
306 #[test]
307 fn selecting_one_in_group_deselects_other() {
308 let options = vec![
309 GeneratorOptionItem::Option(GeneratorOption {
310 name: "option1".to_string(),
311 display_name: "Foobar".to_string(),
312 selection_group: "group".to_string(),
313 help: "".to_string(),
314 chips: vec![Chip::Esp32],
315 requires: vec![],
316 }),
317 GeneratorOptionItem::Option(GeneratorOption {
318 name: "option2".to_string(),
319 display_name: "Barfoo".to_string(),
320 selection_group: "group".to_string(),
321 help: "".to_string(),
322 chips: vec![Chip::Esp32],
323 requires: vec![],
324 }),
325 GeneratorOptionItem::Option(GeneratorOption {
326 name: "option3".to_string(),
327 display_name: "Prevents deselecting option2".to_string(),
328 selection_group: "".to_string(),
329 help: "".to_string(),
330 chips: vec![Chip::Esp32],
331 requires: vec!["option2".to_string()],
332 }),
333 ];
334 let mut active = ActiveConfiguration {
335 chip: Chip::Esp32,
336 selected: vec![],
337 options,
338 };
339
340 active.select("option1".to_string());
341 assert_eq!(active.selected, &["option1"]);
342
343 active.select("option2".to_string());
344 assert_eq!(active.selected, &["option2"]);
345
346 active.select("option3".to_string());
348 assert_eq!(active.selected, &["option2", "option3"]);
349
350 active.select("option1".to_string());
351 assert_eq!(active.selected, &["option2", "option3"]);
352 }
353
354 #[test]
355 fn depending_on_group_allows_changing_group_option() {
356 let options = vec![
357 GeneratorOptionItem::Category(GeneratorOptionCategory {
358 name: "group-options".to_string(),
359 display_name: "Group options".to_string(),
360 help: "".to_string(),
361 requires: vec![],
362 options: vec![
363 GeneratorOptionItem::Option(GeneratorOption {
364 name: "option1".to_string(),
365 display_name: "Foobar".to_string(),
366 selection_group: "group".to_string(),
367 help: "".to_string(),
368 chips: vec![Chip::Esp32],
369 requires: vec![],
370 }),
371 GeneratorOptionItem::Option(GeneratorOption {
372 name: "option2".to_string(),
373 display_name: "Barfoo".to_string(),
374 selection_group: "group".to_string(),
375 help: "".to_string(),
376 chips: vec![Chip::Esp32],
377 requires: vec![],
378 }),
379 ],
380 }),
381 GeneratorOptionItem::Option(GeneratorOption {
382 name: "option3".to_string(),
383 display_name: "Requires any in group to be selected".to_string(),
384 selection_group: "".to_string(),
385 help: "".to_string(),
386 chips: vec![Chip::Esp32],
387 requires: vec!["group".to_string()],
388 }),
389 GeneratorOptionItem::Option(GeneratorOption {
390 name: "option4".to_string(),
391 display_name: "Extra option that depends on something".to_string(),
392 selection_group: "".to_string(),
393 help: "".to_string(),
394 chips: vec![Chip::Esp32],
395 requires: vec!["option3".to_string()],
396 }),
397 ];
398 let mut active = ActiveConfiguration {
399 chip: Chip::Esp32,
400 selected: vec![],
401 options,
402 };
403
404 active.select("option3".to_string());
406 assert_eq!(active.selected, empty());
407
408 active.select("option1".to_string());
409 assert_eq!(active.selected, &["option1"]);
410
411 active.select("option3".to_string());
412 assert_eq!(active.selected, &["option1", "option3"]);
413
414 active.select("option4".to_string());
418 assert_eq!(active.selected, &["option1", "option3", "option4"]);
419
420 active.select("option2".to_string());
421 assert_eq!(active.selected, &["option3", "option4", "option2"]);
422 }
423
424 #[test]
425 fn depending_on_group_prevents_deselecting() {
426 let options = vec![
427 GeneratorOptionItem::Option(GeneratorOption {
428 name: "option1".to_string(),
429 display_name: "Foobar".to_string(),
430 selection_group: "group".to_string(),
431 help: "".to_string(),
432 chips: vec![Chip::Esp32],
433 requires: vec![],
434 }),
435 GeneratorOptionItem::Option(GeneratorOption {
436 name: "option2".to_string(),
437 display_name: "Barfoo".to_string(),
438 selection_group: "".to_string(),
439 help: "".to_string(),
440 chips: vec![Chip::Esp32],
441 requires: vec!["group".to_string()],
442 }),
443 ];
444 let mut active = ActiveConfiguration {
445 chip: Chip::Esp32,
446 selected: vec![],
447 options,
448 };
449
450 active.select("option1".to_string());
451 active.select("option2".to_string());
452
453 assert!(!active.can_be_disabled("option1"));
455 }
456
457 #[test]
458 fn requiring_not_option_only_rejects_existing_group() {
459 let options = vec![
460 GeneratorOptionItem::Option(GeneratorOption {
461 name: "option1".to_string(),
462 display_name: "Foobar".to_string(),
463 selection_group: "group".to_string(),
464 help: "".to_string(),
465 chips: vec![Chip::Esp32],
466 requires: vec![],
467 }),
468 GeneratorOptionItem::Option(GeneratorOption {
469 name: "option2".to_string(),
470 display_name: "Barfoo".to_string(),
471 selection_group: "".to_string(),
472 help: "".to_string(),
473 chips: vec![Chip::Esp32],
474 requires: vec!["!option1".to_string()],
475 }),
476 ];
477 let mut active = ActiveConfiguration {
478 chip: Chip::Esp32,
479 selected: vec![],
480 options,
481 };
482
483 active.select("option1".to_string());
484 let opt2 = find_option("option2", &active.options).unwrap();
485 assert!(!active.is_option_active(opt2));
486 }
487
488 fn empty() -> &'static [&'static str] {
489 &[]
490 }
491}