esp_generate/
config.rs

1use esp_metadata::Chip;
2
3use crate::template::{GeneratorOption, GeneratorOptionItem};
4
5#[derive(Debug)]
6pub struct ActiveConfiguration<'c> {
7    /// The chip that is configured for
8    pub chip: Chip,
9    /// The names of the selected options
10    pub selected: Vec<String>,
11    /// All available options
12    pub options: &'c [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    /// Tries to deselect all options in a selection group. Returns false if it's prevented by some
32    /// requirement.
33    fn deselect_group(
34        selected: &mut Vec<String>,
35        options: &[GeneratorOptionItem],
36        group: &str,
37    ) -> bool {
38        // No group, nothing to deselect
39        if group.is_empty() {
40            return true;
41        }
42
43        // Avoid deselecting some options then failing.
44        if !selected.iter().all(|s| {
45            let o = find_option(s, options).unwrap();
46            if o.selection_group == group {
47                // We allow deselecting group options because we are changing the options in the
48                // group, so after this operation the group have a selected item still.
49                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    /// Returns whether an item is active (can be selected).
77    ///
78    /// This function is different from `is_option_active` in that it handles categories as well.
79    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    /// Returns whether all requirements are met.
97    ///
98    /// A requirement may be:
99    /// - an `option`
100    /// - the absence of an `!option`
101    /// - a `selection_group`, which means one option in that selection group must be selected
102    /// - the absence of a `!selection_group`, which means no option in that selection group must
103    ///   be selected
104    ///
105    /// A selection group must not have the same name as an option.
106    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            // Requirement is an option that must be selected?
115            if self.is_selected(key) == expected {
116                continue;
117            }
118
119            // Requirement is a group that must have a selected option?
120            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    /// Returns whether an option is active (can be selected).
132    ///
133    /// This involves checking if the option is available for the current chip, if it's not
134    /// disabled by any other selected option, and if all its requirements are met.
135    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        // Are this option's requirements met?
141        if !self.requirements_met(&option.requires) {
142            return false;
143        }
144
145        // Does any of the enabled options have a requirement against this one?
146        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    // An option can only be disabled if it's not required by any other selected option.
165    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 = &[
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(&options[0]);
298        assert_eq!(rels.requires, &["option2"]);
299        assert_eq!(rels.required_by, <&[&str]>::default());
300
301        let rels = active.collect_relationships(&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 = &[
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        // Enable option3, which prevents deselecting option2, which disallows selecting option1
347        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 = &[
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        // Nothing is selected in group, so option3 can't be selected
405        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        // The rejection algorithm must not trigger on unrelated options. This option is
415        // meant to test the group filtering. It prevents disabling "option3" but it does not
416        // belong to `group`, so it should not prevent selecting between "option1" or "option2".
417        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 = &[
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        // Option1 can't be deselected because option2 requires that a `group` option is selected
454        assert!(!active.can_be_disabled("option1"));
455    }
456
457    #[test]
458    fn requiring_not_option_only_rejects_existing_group() {
459        let options = &[
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", options).unwrap();
485        assert!(!active.is_option_active(opt2));
486    }
487
488    fn empty() -> &'static [&'static str] {
489        &[]
490    }
491}