use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::localized::Localized;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Choice {
pub value: String,
pub label: Localized,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Label {
pub title: Localized,
pub subtitle: Option<Localized>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "OptionWire", into = "OptionWire")]
pub enum OptionDef {
Slider {
name: String,
label: Label,
min: f64,
max: f64,
step: f64,
default: f64,
unit: Option<Localized>,
},
Select {
name: String,
label: Label,
choices: Vec<Choice>,
default: String,
},
Text {
name: String,
label: Label,
placeholder: Option<Localized>,
default: Option<String>,
},
File {
name: String,
label: Label,
},
Directory {
name: String,
label: Label,
},
Feature {
name: String,
label: Label,
default: bool,
options: Vec<OptionDef>,
},
}
impl OptionDef {
pub fn name(&self) -> &str {
match self {
OptionDef::Slider { name, .. }
| OptionDef::Select { name, .. }
| OptionDef::Text { name, .. }
| OptionDef::File { name, .. }
| OptionDef::Directory { name, .. }
| OptionDef::Feature { name, .. } => name,
}
}
pub fn label(&self) -> &Label {
match self {
OptionDef::Slider { label, .. }
| OptionDef::Select { label, .. }
| OptionDef::Text { label, .. }
| OptionDef::File { label, .. }
| OptionDef::Directory { label, .. }
| OptionDef::Feature { label, .. } => label,
}
}
pub fn texts(&self) -> Vec<&Localized> {
let label = self.label();
let mut texts: Vec<&Localized> = std::iter::once(&label.title)
.chain(label.subtitle.as_ref())
.collect();
match self {
OptionDef::Slider { unit, .. } => texts.extend(unit),
OptionDef::Select { choices, .. } => texts.extend(choices.iter().map(|c| &c.label)),
OptionDef::Text { placeholder, .. } => texts.extend(placeholder),
OptionDef::Feature { options, .. } => {
texts.extend(options.iter().flat_map(OptionDef::texts))
}
OptionDef::File { .. } | OptionDef::Directory { .. } => {}
}
texts
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct OptionWire {
#[serde(default, skip_serializing_if = "Option::is_none")]
slider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
select: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
directory: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
feature: Option<String>,
title: Localized,
#[serde(default, skip_serializing_if = "Option::is_none")]
subtitle: Option<Localized>,
#[serde(default, skip_serializing_if = "Option::is_none")]
min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
step: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
unit: Option<Localized>,
#[serde(default, skip_serializing_if = "Option::is_none")]
choices: Option<Vec<Choice>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
placeholder: Option<Localized>,
#[serde(default, skip_serializing_if = "Option::is_none")]
default: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
options: Option<Vec<OptionDef>>,
}
const KINDS: &str = "`slider`, `select`, `text`, `file`, `directory`, `feature`";
impl TryFrom<OptionWire> for OptionDef {
type Error = String;
fn try_from(raw: OptionWire) -> Result<Self, Self::Error> {
let kinds = [
("slider", &raw.slider),
("select", &raw.select),
("text", &raw.text),
("file", &raw.file),
("directory", &raw.directory),
("feature", &raw.feature),
];
let mut named = kinds
.iter()
.filter_map(|(kind, name)| name.as_ref().map(|name| (*kind, name.clone())));
let (kind, name) = named
.next()
.ok_or_else(|| format!("an option is one of {KINDS}, and this names none"))?;
if let Some((other, _)) = named.next() {
return Err(format!(
"an option is one kind, and this is both `{kind}` and `{other}`"
));
}
let present = [
("min", raw.min.is_some()),
("max", raw.max.is_some()),
("step", raw.step.is_some()),
("unit", raw.unit.is_some()),
("choices", raw.choices.is_some()),
("placeholder", raw.placeholder.is_some()),
("default", raw.default.is_some()),
("options", raw.options.is_some()),
];
let allowed: &[&str] = match kind {
"slider" => &["min", "max", "step", "unit", "default"],
"select" => &["choices", "default"],
"text" => &["placeholder", "default"],
"feature" => &["default", "options"],
_ => &[],
};
if let Some((field, _)) = present
.iter()
.find(|(field, is)| *is && !allowed.contains(field))
{
return Err(format!("`{field}` does not belong on a {kind} (`{name}`)"));
}
let missing = |field: &str| format!("{kind} `{name}` has no `{field}`");
let label = Label {
title: raw.title,
subtitle: raw.subtitle,
};
Ok(match kind {
"slider" => OptionDef::Slider {
min: raw.min.ok_or_else(|| missing("min"))?,
max: raw.max.ok_or_else(|| missing("max"))?,
step: raw.step.ok_or_else(|| missing("step"))?,
default: raw
.default
.ok_or_else(|| missing("default"))?
.as_f64()
.ok_or_else(|| format!("the `default` of slider `{name}` is a number"))?,
unit: raw.unit,
name,
label,
},
"select" => OptionDef::Select {
choices: raw.choices.ok_or_else(|| missing("choices"))?,
default: match raw.default.ok_or_else(|| missing("default"))? {
serde_json::Value::String(value) => value,
_ => return Err(format!("the `default` of select `{name}` is a string")),
},
name,
label,
},
"text" => OptionDef::Text {
placeholder: raw.placeholder,
default: match raw.default {
None => None,
Some(serde_json::Value::String(value)) => Some(value),
Some(_) => return Err(format!("the `default` of text `{name}` is a string")),
},
name,
label,
},
"file" => OptionDef::File { name, label },
"directory" => OptionDef::Directory { name, label },
_ => OptionDef::Feature {
default: match raw.default {
None => false,
Some(serde_json::Value::Bool(value)) => value,
Some(_) => {
return Err(format!("the `default` of feature `{name}` is a boolean"))
}
},
options: raw.options.unwrap_or_default(),
name,
label,
},
})
}
}
impl From<OptionDef> for OptionWire {
fn from(option: OptionDef) -> Self {
let labelled = |label: Label| OptionWire {
title: label.title,
subtitle: label.subtitle,
slider: None,
select: None,
text: None,
file: None,
directory: None,
feature: None,
min: None,
max: None,
step: None,
unit: None,
choices: None,
placeholder: None,
default: None,
options: None,
};
match option {
OptionDef::Slider {
name,
label,
min,
max,
step,
default,
unit,
} => OptionWire {
slider: Some(name),
min: Some(min),
max: Some(max),
step: Some(step),
default: Some(default.into()),
unit,
..labelled(label)
},
OptionDef::Select {
name,
label,
choices,
default,
} => OptionWire {
select: Some(name),
choices: Some(choices),
default: Some(default.into()),
..labelled(label)
},
OptionDef::Text {
name,
label,
placeholder,
default,
} => OptionWire {
text: Some(name),
placeholder,
default: default.map(Into::into),
..labelled(label)
},
OptionDef::File { name, label } => OptionWire {
file: Some(name),
..labelled(label)
},
OptionDef::Directory { name, label } => OptionWire {
directory: Some(name),
..labelled(label)
},
OptionDef::Feature {
name,
label,
default,
options,
} => OptionWire {
feature: Some(name),
default: default.then_some(true.into()),
options: (!options.is_empty()).then_some(options),
..labelled(label)
},
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "Vec<OptionDef>", into = "Vec<OptionDef>")]
pub struct Options(Vec<OptionDef>);
impl Options {
pub fn new(options: Vec<OptionDef>) -> Result<Self, String> {
let mut seen = Seen::default();
options
.iter()
.try_for_each(|option| check(option, &mut seen))?;
Ok(Options(options))
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn as_slice(&self) -> &[OptionDef] {
&self.0
}
pub fn texts(&self) -> Vec<&Localized> {
self.0.iter().flat_map(OptionDef::texts).collect()
}
}
impl TryFrom<Vec<OptionDef>> for Options {
type Error = String;
fn try_from(options: Vec<OptionDef>) -> Result<Self, Self::Error> {
Options::new(options)
}
}
impl From<Options> for Vec<OptionDef> {
fn from(options: Options) -> Self {
options.0
}
}
#[derive(Default)]
struct Seen<'a> {
vars: BTreeSet<&'a str>,
features: BTreeSet<&'a str>,
}
fn check<'a>(option: &'a OptionDef, seen: &mut Seen<'a>) -> Result<(), String> {
let name = option.name();
if name.is_empty() {
return Err("an option has an empty name".to_owned());
}
let (taken, what) = match option {
OptionDef::Feature { .. } => (&mut seen.features, "feature"),
_ => (&mut seen.vars, "variable"),
};
if !taken.insert(name) {
return Err(format!("{what} `{name}` is an option twice"));
}
match option {
OptionDef::Slider {
min,
max,
step,
default,
..
} => {
if ![min, max, step, default].iter().all(|n| n.is_finite()) {
return Err(format!("slider `{name}` has a number that is not finite"));
}
if min >= max {
return Err(format!("slider `{name}` has no range: {min} to {max}"));
}
if *step <= 0.0 {
return Err(format!("slider `{name}` has a step of {step}"));
}
if default < min || default > max {
return Err(format!(
"the default of slider `{name}`, {default}, is outside {min} to {max}"
));
}
}
OptionDef::Select {
choices, default, ..
} => {
let mut values = BTreeSet::new();
if let Some(twice) = choices.iter().find(|c| !values.insert(c.value.as_str())) {
return Err(format!("select `{name}` offers `{}` twice", twice.value));
}
if !values.contains(default.as_str()) {
return Err(format!(
"the default of select `{name}`, `{default}`, is not one of its choices"
));
}
}
OptionDef::Feature { options, .. } => {
options.iter().try_for_each(|child| check(child, seen))?;
}
OptionDef::Text { .. } | OptionDef::File { .. } | OptionDef::Directory { .. } => {}
}
Ok(())
}