use esp_metadata::Chip;
use crate::template::{GeneratorOption, GeneratorOptionItem};
#[derive(Debug)]
pub struct ActiveConfiguration {
pub chip: Chip,
pub selected: Vec<usize>,
pub options: Vec<GeneratorOptionItem>,
pub flat_options: Vec<GeneratorOption>,
}
pub fn flatten_options(options: &[GeneratorOptionItem]) -> Vec<GeneratorOption> {
options
.iter()
.flat_map(|item| match item {
GeneratorOptionItem::Category(category) => flatten_options(&category.options),
GeneratorOptionItem::Option(option) => vec![option.clone()],
})
.collect()
}
impl ActiveConfiguration {
fn option_requires(option: &GeneratorOption, requirement: &str) -> bool {
option.requires.iter().any(|r| r == requirement)
}
pub fn option_is_method_specific_for_state(
option: &GeneratorOption,
method_option: &str,
method_option_selected: bool,
) -> bool {
if method_option_selected {
Self::option_requires(option, method_option)
} else {
let requirement = format!("!{method_option}");
Self::option_requires(option, &requirement)
}
}
pub fn can_switch_method_option(&self, method_option: &str) -> bool {
let mut has_selected_side = false;
let mut has_unselected_side = false;
let negated = format!("!{method_option}");
for option in &self.flat_options {
if Self::option_requires(option, method_option) {
has_selected_side = true;
}
if Self::option_requires(option, &negated) {
has_unselected_side = true;
}
if has_selected_side && has_unselected_side {
return true;
}
}
false
}
pub fn is_group_selected(&self, group: &str) -> bool {
self.selected
.iter()
.any(|s| self.flat_options[*s].selection_group == group)
}
pub fn is_selected(&self, option: &str) -> bool {
self.selected_index(option).is_some()
}
pub fn selected_index(&self, option: &str) -> Option<usize> {
self.selected
.iter()
.position(|s| self.flat_options[*s].name == option)
}
fn deselect_group(selected: &mut Vec<usize>, options: &[GeneratorOption], group: &str) -> bool {
if group.is_empty() {
return true;
}
if !selected.iter().copied().all(|s| {
let o = &options[s];
if o.selection_group == group {
Self::can_be_disabled_impl(selected, options, s, true)
} else {
true
}
}) {
return false;
}
selected.retain(|s| options[*s].selection_group != group);
true
}
pub fn select(&mut self, option: &str) {
let (index, _o) = find_option(option, &self.flat_options, self.chip).unwrap();
self.select_idx(index);
}
pub fn select_idx(&mut self, idx: usize) {
let o = &self.flat_options[idx];
if !self.is_option_active(o) {
return;
}
if !Self::deselect_group(&mut self.selected, &self.flat_options, &o.selection_group) {
return;
}
self.selected.push(idx);
}
pub fn is_active(&self, item: &GeneratorOptionItem) -> bool {
match item {
GeneratorOptionItem::Category(category) => {
if !self.requirements_met(&category.requires) {
return false;
}
for sub in category.options.iter() {
if self.is_active(sub) {
return true;
}
}
false
}
GeneratorOptionItem::Option(option) => self.is_option_active(option),
}
}
fn requirements_met(&self, requires: &[String]) -> bool {
for requirement in requires {
let (key, expected) = if let Some(requirement) = requirement.strip_prefix('!') {
(requirement, false)
} else {
(requirement.as_str(), true)
};
if self.is_selected(key) == expected {
continue;
}
let is_group = Self::group_exists(key, &self.flat_options);
if is_group && self.is_group_selected(key) == expected {
continue;
}
return false;
}
true
}
pub fn is_option_active(&self, option: &GeneratorOption) -> bool {
if !option.chips.is_empty() && !option.chips.contains(&self.chip) {
return false;
}
if !self.requirements_met(&option.requires) {
return false;
}
for selected in self.selected.iter().copied() {
let Some(selected_option) = self.flat_options.get(selected) else {
ratatui::restore();
panic!("selected option not found: {selected}");
};
for requirement in selected_option.requires.iter() {
if let Some(requirement) = requirement.strip_prefix('!') {
if requirement == option.name {
return false;
}
}
}
}
true
}
pub fn can_be_disabled(&self, option: &str) -> bool {
let (option, _) = find_option(option, &self.flat_options, self.chip).unwrap();
Self::can_be_disabled_impl(&self.selected, &self.flat_options, option, false)
}
fn can_be_disabled_impl(
selected: &[usize],
options: &[GeneratorOption],
option: usize,
allow_deselecting_group: bool,
) -> bool {
let op = &options[option];
for selected in selected.iter().copied() {
let selected_option = &options[selected];
if selected_option
.requires
.iter()
.any(|o| o == &op.name || (o == &op.selection_group && !allow_deselecting_group))
{
return false;
}
}
true
}
pub fn collect_relationships<'a>(
&'a self,
option: &'a GeneratorOptionItem,
) -> Relationships<'a> {
let mut requires = Vec::new();
let mut required_by = Vec::new();
let mut disabled_by = Vec::new();
self.selected.iter().for_each(|opt| {
let opt = &self.flat_options[*opt];
for o in opt.requires.iter() {
if let Some(disables) = o.strip_prefix("!") {
if disables == option.name() {
disabled_by.push(opt.name.as_str());
}
} else if o == option.name() {
required_by.push(opt.name.as_str());
}
}
});
for req in option.requires() {
if let Some(disables) = req.strip_prefix("!") {
if self.is_selected(disables) {
disabled_by.push(disables);
}
} else {
requires.push(req.as_str());
}
}
Relationships {
requires,
required_by,
disabled_by,
}
}
fn group_exists(key: &str, options: &[GeneratorOption]) -> bool {
options.iter().any(|o| o.selection_group == key)
}
}
pub struct Relationships<'a> {
pub requires: Vec<&'a str>,
pub required_by: Vec<&'a str>,
pub disabled_by: Vec<&'a str>,
}
pub fn find_option<'c>(
option: &str,
options: &'c [GeneratorOption],
chip: Chip,
) -> Option<(usize, &'c GeneratorOption)> {
options
.iter()
.enumerate()
.find(|(_, opt)| opt.name == option && (opt.chips.is_empty() || opt.chips.contains(&chip)))
}
#[cfg(test)]
mod test {
use esp_metadata::Chip;
use crate::{
config::*,
template::{GeneratorOption, GeneratorOptionCategory, GeneratorOptionItem},
};
#[test]
fn required_by_and_requires_pick_the_right_options() {
let options = vec![
GeneratorOptionItem::Option(GeneratorOption {
name: "option1".to_string(),
display_name: "Foobar".to_string(),
selection_group: "".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec!["option2".to_string()],
}),
GeneratorOptionItem::Option(GeneratorOption {
name: "option2".to_string(),
display_name: "Barfoo".to_string(),
selection_group: "".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec![],
}),
];
let active = ActiveConfiguration {
chip: Chip::Esp32,
selected: vec![0],
flat_options: flatten_options(&options),
options,
};
let rels = active.collect_relationships(&active.options[0]);
assert_eq!(rels.requires, &["option2"]);
assert_eq!(rels.required_by, <&[&str]>::default());
let rels = active.collect_relationships(&active.options[1]);
assert_eq!(rels.requires, <&[&str]>::default());
assert_eq!(rels.required_by, &["option1"]);
}
#[test]
fn selecting_one_in_group_deselects_other() {
let options = vec![
GeneratorOptionItem::Option(GeneratorOption {
name: "option1".to_string(),
display_name: "Foobar".to_string(),
selection_group: "group".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec![],
}),
GeneratorOptionItem::Option(GeneratorOption {
name: "option2".to_string(),
display_name: "Barfoo".to_string(),
selection_group: "group".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec![],
}),
GeneratorOptionItem::Option(GeneratorOption {
name: "option3".to_string(),
display_name: "Prevents deselecting option2".to_string(),
selection_group: "".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec!["option2".to_string()],
}),
];
let mut active = ActiveConfiguration {
chip: Chip::Esp32,
selected: vec![],
flat_options: flatten_options(&options),
options,
};
active.select("option1");
assert_eq!(active.selected, &[0]);
active.select("option2");
assert_eq!(active.selected, &[1]);
active.select("option3");
assert_eq!(active.selected, &[1, 2]);
active.select("option1");
assert_eq!(active.selected, &[1, 2]);
}
#[test]
fn depending_on_group_allows_changing_group_option() {
let options = vec![
GeneratorOptionItem::Category(GeneratorOptionCategory {
name: "group-options".to_string(),
display_name: "Group options".to_string(),
help: "".to_string(),
requires: vec![],
options: vec![
GeneratorOptionItem::Option(GeneratorOption {
name: "option1".to_string(),
display_name: "Foobar".to_string(),
selection_group: "group".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec![],
}),
GeneratorOptionItem::Option(GeneratorOption {
name: "option2".to_string(),
display_name: "Barfoo".to_string(),
selection_group: "group".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec![],
}),
],
}),
GeneratorOptionItem::Option(GeneratorOption {
name: "option3".to_string(),
display_name: "Requires any in group to be selected".to_string(),
selection_group: "".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec!["group".to_string()],
}),
GeneratorOptionItem::Option(GeneratorOption {
name: "option4".to_string(),
display_name: "Extra option that depends on something".to_string(),
selection_group: "".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec!["option3".to_string()],
}),
];
let mut active = ActiveConfiguration {
chip: Chip::Esp32,
selected: vec![],
flat_options: flatten_options(&options),
options,
};
active.select("option3");
assert_eq!(active.selected, empty());
active.select("option1");
assert_eq!(active.selected, &[0]);
active.select("option3");
assert_eq!(active.selected, &[0, 2]);
active.select("option4");
assert_eq!(active.selected, &[0, 2, 3]);
active.select("option2");
assert_eq!(active.selected, &[2, 3, 1]);
}
#[test]
fn depending_on_group_prevents_deselecting() {
let options = vec![
GeneratorOptionItem::Option(GeneratorOption {
name: "option1".to_string(),
display_name: "Foobar".to_string(),
selection_group: "group".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec![],
}),
GeneratorOptionItem::Option(GeneratorOption {
name: "option2".to_string(),
display_name: "Barfoo".to_string(),
selection_group: "".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec!["group".to_string()],
}),
];
let mut active = ActiveConfiguration {
chip: Chip::Esp32,
selected: vec![],
flat_options: flatten_options(&options),
options,
};
active.select("option1");
active.select("option2");
assert!(!active.can_be_disabled("option1"));
}
#[test]
fn requiring_not_option_only_rejects_existing_group() {
let options = vec![
GeneratorOptionItem::Option(GeneratorOption {
name: "option1".to_string(),
display_name: "Foobar".to_string(),
selection_group: "group".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec![],
}),
GeneratorOptionItem::Option(GeneratorOption {
name: "option2".to_string(),
display_name: "Barfoo".to_string(),
selection_group: "".to_string(),
help: "".to_string(),
chips: vec![Chip::Esp32],
requires: vec!["!option1".to_string()],
}),
];
let mut active = ActiveConfiguration {
chip: Chip::Esp32,
selected: vec![],
flat_options: flatten_options(&options),
options,
};
active.select("option1");
let (_, opt2) = find_option("option2", &active.flat_options, Chip::Esp32).unwrap();
assert!(!active.is_option_active(opt2));
}
fn empty() -> &'static [usize] {
&[]
}
}