1use std::collections::BTreeSet;
13
14use serde::{Deserialize, Serialize};
15
16use crate::localized::Localized;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct Choice {
22 pub value: String,
23 pub label: Localized,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Label {
29 pub title: Localized,
30 pub subtitle: Option<Localized>,
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[serde(try_from = "OptionWire", into = "OptionWire")]
37pub enum OptionDef {
38 Slider {
39 name: String,
40 label: Label,
41 min: f64,
42 max: f64,
43 step: f64,
44 default: f64,
45 unit: Option<Localized>,
46 },
47 Select {
48 name: String,
49 label: Label,
50 choices: Vec<Choice>,
51 default: String,
52 },
53 Text {
54 name: String,
55 label: Label,
56 placeholder: Option<Localized>,
57 default: Option<String>,
58 },
59 File {
60 name: String,
61 label: Label,
62 },
63 Directory {
64 name: String,
65 label: Label,
66 },
67 Feature {
68 name: String,
69 label: Label,
70 default: bool,
71 options: Vec<OptionDef>,
72 },
73}
74
75impl OptionDef {
76 pub fn name(&self) -> &str {
77 match self {
78 OptionDef::Slider { name, .. }
79 | OptionDef::Select { name, .. }
80 | OptionDef::Text { name, .. }
81 | OptionDef::File { name, .. }
82 | OptionDef::Directory { name, .. }
83 | OptionDef::Feature { name, .. } => name,
84 }
85 }
86
87 pub fn label(&self) -> &Label {
88 match self {
89 OptionDef::Slider { label, .. }
90 | OptionDef::Select { label, .. }
91 | OptionDef::Text { label, .. }
92 | OptionDef::File { label, .. }
93 | OptionDef::Directory { label, .. }
94 | OptionDef::Feature { label, .. } => label,
95 }
96 }
97
98 pub fn texts(&self) -> Vec<&Localized> {
100 let label = self.label();
101 let mut texts: Vec<&Localized> = std::iter::once(&label.title)
102 .chain(label.subtitle.as_ref())
103 .collect();
104 match self {
105 OptionDef::Slider { unit, .. } => texts.extend(unit),
106 OptionDef::Select { choices, .. } => texts.extend(choices.iter().map(|c| &c.label)),
107 OptionDef::Text { placeholder, .. } => texts.extend(placeholder),
108 OptionDef::Feature { options, .. } => {
109 texts.extend(options.iter().flat_map(OptionDef::texts))
110 }
111 OptionDef::File { .. } | OptionDef::Directory { .. } => {}
112 }
113 texts
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(deny_unknown_fields)]
122pub(crate) struct OptionWire {
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 slider: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 select: Option<String>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 text: Option<String>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 file: Option<String>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 directory: Option<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 feature: Option<String>,
135
136 title: Localized,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 subtitle: Option<Localized>,
139
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 min: Option<f64>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 max: Option<f64>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 step: Option<f64>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 unit: Option<Localized>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
149 choices: Option<Vec<Choice>>,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 placeholder: Option<Localized>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
155 default: Option<serde_json::Value>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 options: Option<Vec<OptionDef>>,
158}
159
160const KINDS: &str = "`slider`, `select`, `text`, `file`, `directory`, `feature`";
161
162impl TryFrom<OptionWire> for OptionDef {
163 type Error = String;
164
165 fn try_from(raw: OptionWire) -> Result<Self, Self::Error> {
166 let kinds = [
167 ("slider", &raw.slider),
168 ("select", &raw.select),
169 ("text", &raw.text),
170 ("file", &raw.file),
171 ("directory", &raw.directory),
172 ("feature", &raw.feature),
173 ];
174 let mut named = kinds
175 .iter()
176 .filter_map(|(kind, name)| name.as_ref().map(|name| (*kind, name.clone())));
177 let (kind, name) = named
178 .next()
179 .ok_or_else(|| format!("an option is one of {KINDS}, and this names none"))?;
180 if let Some((other, _)) = named.next() {
181 return Err(format!(
182 "an option is one kind, and this is both `{kind}` and `{other}`"
183 ));
184 }
185
186 let present = [
189 ("min", raw.min.is_some()),
190 ("max", raw.max.is_some()),
191 ("step", raw.step.is_some()),
192 ("unit", raw.unit.is_some()),
193 ("choices", raw.choices.is_some()),
194 ("placeholder", raw.placeholder.is_some()),
195 ("default", raw.default.is_some()),
196 ("options", raw.options.is_some()),
197 ];
198 let allowed: &[&str] = match kind {
199 "slider" => &["min", "max", "step", "unit", "default"],
200 "select" => &["choices", "default"],
201 "text" => &["placeholder", "default"],
202 "feature" => &["default", "options"],
203 _ => &[],
204 };
205 if let Some((field, _)) = present
206 .iter()
207 .find(|(field, is)| *is && !allowed.contains(field))
208 {
209 return Err(format!("`{field}` does not belong on a {kind} (`{name}`)"));
210 }
211
212 let missing = |field: &str| format!("{kind} `{name}` has no `{field}`");
213 let label = Label {
214 title: raw.title,
215 subtitle: raw.subtitle,
216 };
217 Ok(match kind {
218 "slider" => OptionDef::Slider {
219 min: raw.min.ok_or_else(|| missing("min"))?,
220 max: raw.max.ok_or_else(|| missing("max"))?,
221 step: raw.step.ok_or_else(|| missing("step"))?,
222 default: raw
223 .default
224 .ok_or_else(|| missing("default"))?
225 .as_f64()
226 .ok_or_else(|| format!("the `default` of slider `{name}` is a number"))?,
227 unit: raw.unit,
228 name,
229 label,
230 },
231 "select" => OptionDef::Select {
232 choices: raw.choices.ok_or_else(|| missing("choices"))?,
233 default: match raw.default.ok_or_else(|| missing("default"))? {
234 serde_json::Value::String(value) => value,
235 _ => return Err(format!("the `default` of select `{name}` is a string")),
236 },
237 name,
238 label,
239 },
240 "text" => OptionDef::Text {
241 placeholder: raw.placeholder,
242 default: match raw.default {
243 None => None,
244 Some(serde_json::Value::String(value)) => Some(value),
245 Some(_) => return Err(format!("the `default` of text `{name}` is a string")),
246 },
247 name,
248 label,
249 },
250 "file" => OptionDef::File { name, label },
251 "directory" => OptionDef::Directory { name, label },
252 _ => OptionDef::Feature {
253 default: match raw.default {
254 None => false,
255 Some(serde_json::Value::Bool(value)) => value,
256 Some(_) => {
257 return Err(format!("the `default` of feature `{name}` is a boolean"))
258 }
259 },
260 options: raw.options.unwrap_or_default(),
261 name,
262 label,
263 },
264 })
265 }
266}
267
268impl From<OptionDef> for OptionWire {
269 fn from(option: OptionDef) -> Self {
270 let labelled = |label: Label| OptionWire {
271 title: label.title,
272 subtitle: label.subtitle,
273 slider: None,
274 select: None,
275 text: None,
276 file: None,
277 directory: None,
278 feature: None,
279 min: None,
280 max: None,
281 step: None,
282 unit: None,
283 choices: None,
284 placeholder: None,
285 default: None,
286 options: None,
287 };
288 match option {
289 OptionDef::Slider {
290 name,
291 label,
292 min,
293 max,
294 step,
295 default,
296 unit,
297 } => OptionWire {
298 slider: Some(name),
299 min: Some(min),
300 max: Some(max),
301 step: Some(step),
302 default: Some(default.into()),
303 unit,
304 ..labelled(label)
305 },
306 OptionDef::Select {
307 name,
308 label,
309 choices,
310 default,
311 } => OptionWire {
312 select: Some(name),
313 choices: Some(choices),
314 default: Some(default.into()),
315 ..labelled(label)
316 },
317 OptionDef::Text {
318 name,
319 label,
320 placeholder,
321 default,
322 } => OptionWire {
323 text: Some(name),
324 placeholder,
325 default: default.map(Into::into),
326 ..labelled(label)
327 },
328 OptionDef::File { name, label } => OptionWire {
329 file: Some(name),
330 ..labelled(label)
331 },
332 OptionDef::Directory { name, label } => OptionWire {
333 directory: Some(name),
334 ..labelled(label)
335 },
336 OptionDef::Feature {
339 name,
340 label,
341 default,
342 options,
343 } => OptionWire {
344 feature: Some(name),
345 default: default.then_some(true.into()),
346 options: (!options.is_empty()).then_some(options),
347 ..labelled(label)
348 },
349 }
350 }
351}
352
353#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
356#[serde(try_from = "Vec<OptionDef>", into = "Vec<OptionDef>")]
357pub struct Options(Vec<OptionDef>);
358
359impl Options {
360 pub fn new(options: Vec<OptionDef>) -> Result<Self, String> {
364 let mut seen = Seen::default();
365 options
366 .iter()
367 .try_for_each(|option| check(option, &mut seen))?;
368 Ok(Options(options))
369 }
370
371 pub fn is_empty(&self) -> bool {
372 self.0.is_empty()
373 }
374
375 pub fn as_slice(&self) -> &[OptionDef] {
376 &self.0
377 }
378
379 pub fn texts(&self) -> Vec<&Localized> {
381 self.0.iter().flat_map(OptionDef::texts).collect()
382 }
383}
384
385impl TryFrom<Vec<OptionDef>> for Options {
386 type Error = String;
387
388 fn try_from(options: Vec<OptionDef>) -> Result<Self, Self::Error> {
389 Options::new(options)
390 }
391}
392
393impl From<Options> for Vec<OptionDef> {
394 fn from(options: Options) -> Self {
395 options.0
396 }
397}
398
399#[derive(Default)]
402struct Seen<'a> {
403 vars: BTreeSet<&'a str>,
404 features: BTreeSet<&'a str>,
405}
406
407fn check<'a>(option: &'a OptionDef, seen: &mut Seen<'a>) -> Result<(), String> {
408 let name = option.name();
409 if name.is_empty() {
410 return Err("an option has an empty name".to_owned());
411 }
412 let (taken, what) = match option {
413 OptionDef::Feature { .. } => (&mut seen.features, "feature"),
414 _ => (&mut seen.vars, "variable"),
415 };
416 if !taken.insert(name) {
417 return Err(format!("{what} `{name}` is an option twice"));
418 }
419
420 match option {
421 OptionDef::Slider {
422 min,
423 max,
424 step,
425 default,
426 ..
427 } => {
428 if ![min, max, step, default].iter().all(|n| n.is_finite()) {
429 return Err(format!("slider `{name}` has a number that is not finite"));
430 }
431 if min >= max {
432 return Err(format!("slider `{name}` has no range: {min} to {max}"));
433 }
434 if *step <= 0.0 {
435 return Err(format!("slider `{name}` has a step of {step}"));
436 }
437 if default < min || default > max {
438 return Err(format!(
439 "the default of slider `{name}`, {default}, is outside {min} to {max}"
440 ));
441 }
442 }
443 OptionDef::Select {
444 choices, default, ..
445 } => {
446 let mut values = BTreeSet::new();
447 if let Some(twice) = choices.iter().find(|c| !values.insert(c.value.as_str())) {
448 return Err(format!("select `{name}` offers `{}` twice", twice.value));
449 }
450 if !values.contains(default.as_str()) {
451 return Err(format!(
452 "the default of select `{name}`, `{default}`, is not one of its choices"
453 ));
454 }
455 }
456 OptionDef::Feature { options, .. } => {
457 options.iter().try_for_each(|child| check(child, seen))?;
458 }
459 OptionDef::Text { .. } | OptionDef::File { .. } | OptionDef::Directory { .. } => {}
460 }
461 Ok(())
462}