Skip to main content

opys_bundle/
option.rs

1//! Options: what a bundle lets whoever launches it choose.
2//!
3//! A manifest's rules test features and its strings name variables, and
4//! nothing in it says which of those are a player's to set. A bundle's head
5//! does: a tree of options, each with what a launcher needs to draw it. An
6//! option fills a variable, except a `feature`, which switches a feature on
7//! and holds the options that only matter while it is.
8//!
9//! This is the schema alone. What was chosen, and turning that into the
10//! `vars` and `features` an install takes, belongs to whoever shows the form.
11
12use std::collections::BTreeSet;
13
14use serde::{Deserialize, Serialize};
15
16/// One value a `select` offers, and what to call it.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct Choice {
20    pub value: String,
21    pub label: String,
22}
23
24/// What every option says about itself to the person choosing.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Label {
27    pub title: String,
28    pub subtitle: Option<String>,
29}
30
31/// One option. `name` is the variable it fills or, for a `Feature`, the
32/// feature it switches.
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34#[serde(try_from = "OptionWire", into = "OptionWire")]
35pub enum OptionDef {
36    Slider {
37        name: String,
38        label: Label,
39        min: f64,
40        max: f64,
41        step: f64,
42        default: f64,
43        unit: Option<String>,
44    },
45    Select {
46        name: String,
47        label: Label,
48        choices: Vec<Choice>,
49        default: String,
50    },
51    Text {
52        name: String,
53        label: Label,
54        placeholder: Option<String>,
55        default: Option<String>,
56    },
57    File {
58        name: String,
59        label: Label,
60    },
61    Directory {
62        name: String,
63        label: Label,
64    },
65    Feature {
66        name: String,
67        label: Label,
68        default: bool,
69        options: Vec<OptionDef>,
70    },
71}
72
73impl OptionDef {
74    pub fn name(&self) -> &str {
75        match self {
76            OptionDef::Slider { name, .. }
77            | OptionDef::Select { name, .. }
78            | OptionDef::Text { name, .. }
79            | OptionDef::File { name, .. }
80            | OptionDef::Directory { name, .. }
81            | OptionDef::Feature { name, .. } => name,
82        }
83    }
84
85    pub fn label(&self) -> &Label {
86        match self {
87            OptionDef::Slider { label, .. }
88            | OptionDef::Select { label, .. }
89            | OptionDef::Text { label, .. }
90            | OptionDef::File { label, .. }
91            | OptionDef::Directory { label, .. }
92            | OptionDef::Feature { label, .. } => label,
93        }
94    }
95}
96
97/// Discriminated by which field is present, like every wire shape here, and
98/// that field holds the name: `{ "slider": "xmx", … }`. A struct of options
99/// rather than an untagged enum, so a field on the wrong kind is named.
100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub(crate) struct OptionWire {
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    slider: Option<String>,
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    select: Option<String>,
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    text: Option<String>,
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    file: Option<String>,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    directory: Option<String>,
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    feature: Option<String>,
115
116    title: String,
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    subtitle: Option<String>,
119
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    min: Option<f64>,
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    max: Option<f64>,
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    step: Option<f64>,
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    unit: Option<String>,
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    choices: Option<Vec<Choice>>,
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    placeholder: Option<String>,
132    /// A number on a slider, a string on a select or a text, a boolean on a
133    /// feature: the one field whose type depends on the kind.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    default: Option<serde_json::Value>,
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    options: Option<Vec<OptionDef>>,
138}
139
140const KINDS: &str = "`slider`, `select`, `text`, `file`, `directory`, `feature`";
141
142impl TryFrom<OptionWire> for OptionDef {
143    type Error = String;
144
145    fn try_from(raw: OptionWire) -> Result<Self, Self::Error> {
146        let kinds = [
147            ("slider", &raw.slider),
148            ("select", &raw.select),
149            ("text", &raw.text),
150            ("file", &raw.file),
151            ("directory", &raw.directory),
152            ("feature", &raw.feature),
153        ];
154        let mut named = kinds
155            .iter()
156            .filter_map(|(kind, name)| name.as_ref().map(|name| (*kind, name.clone())));
157        let (kind, name) = named
158            .next()
159            .ok_or_else(|| format!("an option is one of {KINDS}, and this names none"))?;
160        if let Some((other, _)) = named.next() {
161            return Err(format!(
162                "an option is one kind, and this is both `{kind}` and `{other}`"
163            ));
164        }
165
166        // Each kind takes what is its own and refuses the rest, so a `min` on
167        // a select is an error rather than a field nothing reads.
168        let present = [
169            ("min", raw.min.is_some()),
170            ("max", raw.max.is_some()),
171            ("step", raw.step.is_some()),
172            ("unit", raw.unit.is_some()),
173            ("choices", raw.choices.is_some()),
174            ("placeholder", raw.placeholder.is_some()),
175            ("default", raw.default.is_some()),
176            ("options", raw.options.is_some()),
177        ];
178        let allowed: &[&str] = match kind {
179            "slider" => &["min", "max", "step", "unit", "default"],
180            "select" => &["choices", "default"],
181            "text" => &["placeholder", "default"],
182            "feature" => &["default", "options"],
183            _ => &[],
184        };
185        if let Some((field, _)) = present
186            .iter()
187            .find(|(field, is)| *is && !allowed.contains(field))
188        {
189            return Err(format!("`{field}` does not belong on a {kind} (`{name}`)"));
190        }
191
192        let missing = |field: &str| format!("{kind} `{name}` has no `{field}`");
193        let label = Label {
194            title: raw.title,
195            subtitle: raw.subtitle,
196        };
197        Ok(match kind {
198            "slider" => OptionDef::Slider {
199                min: raw.min.ok_or_else(|| missing("min"))?,
200                max: raw.max.ok_or_else(|| missing("max"))?,
201                step: raw.step.ok_or_else(|| missing("step"))?,
202                default: raw
203                    .default
204                    .ok_or_else(|| missing("default"))?
205                    .as_f64()
206                    .ok_or_else(|| format!("the `default` of slider `{name}` is a number"))?,
207                unit: raw.unit,
208                name,
209                label,
210            },
211            "select" => OptionDef::Select {
212                choices: raw.choices.ok_or_else(|| missing("choices"))?,
213                default: match raw.default.ok_or_else(|| missing("default"))? {
214                    serde_json::Value::String(value) => value,
215                    _ => return Err(format!("the `default` of select `{name}` is a string")),
216                },
217                name,
218                label,
219            },
220            "text" => OptionDef::Text {
221                placeholder: raw.placeholder,
222                default: match raw.default {
223                    None => None,
224                    Some(serde_json::Value::String(value)) => Some(value),
225                    Some(_) => return Err(format!("the `default` of text `{name}` is a string")),
226                },
227                name,
228                label,
229            },
230            "file" => OptionDef::File { name, label },
231            "directory" => OptionDef::Directory { name, label },
232            _ => OptionDef::Feature {
233                default: match raw.default {
234                    None => false,
235                    Some(serde_json::Value::Bool(value)) => value,
236                    Some(_) => {
237                        return Err(format!("the `default` of feature `{name}` is a boolean"))
238                    }
239                },
240                options: raw.options.unwrap_or_default(),
241                name,
242                label,
243            },
244        })
245    }
246}
247
248impl From<OptionDef> for OptionWire {
249    fn from(option: OptionDef) -> Self {
250        let labelled = |label: Label| OptionWire {
251            title: label.title,
252            subtitle: label.subtitle,
253            ..OptionWire::default()
254        };
255        match option {
256            OptionDef::Slider {
257                name,
258                label,
259                min,
260                max,
261                step,
262                default,
263                unit,
264            } => OptionWire {
265                slider: Some(name),
266                min: Some(min),
267                max: Some(max),
268                step: Some(step),
269                default: Some(default.into()),
270                unit,
271                ..labelled(label)
272            },
273            OptionDef::Select {
274                name,
275                label,
276                choices,
277                default,
278            } => OptionWire {
279                select: Some(name),
280                choices: Some(choices),
281                default: Some(default.into()),
282                ..labelled(label)
283            },
284            OptionDef::Text {
285                name,
286                label,
287                placeholder,
288                default,
289            } => OptionWire {
290                text: Some(name),
291                placeholder,
292                default: default.map(Into::into),
293                ..labelled(label)
294            },
295            OptionDef::File { name, label } => OptionWire {
296                file: Some(name),
297                ..labelled(label)
298            },
299            OptionDef::Directory { name, label } => OptionWire {
300                directory: Some(name),
301                ..labelled(label)
302            },
303            // What a feature leaves out reads back as what it was: off, and
304            // holding nothing.
305            OptionDef::Feature {
306                name,
307                label,
308                default,
309                options,
310            } => OptionWire {
311                feature: Some(name),
312                default: default.then_some(true.into()),
313                options: (!options.is_empty()).then_some(options),
314                ..labelled(label)
315            },
316        }
317    }
318}
319
320/// A bundle's options, checked as a whole: a tree that has been read is one a
321/// form can be drawn from without looking at it twice.
322#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
323#[serde(try_from = "Vec<OptionDef>", into = "Vec<OptionDef>")]
324pub struct Options(Vec<OptionDef>);
325
326impl Options {
327    /// Refuses a tree that would mean two things or nothing: a variable or a
328    /// feature named twice, a slider with no range, a select whose default is
329    /// not among its choices.
330    pub fn new(options: Vec<OptionDef>) -> Result<Self, String> {
331        let mut seen = Seen::default();
332        options
333            .iter()
334            .try_for_each(|option| check(option, &mut seen))?;
335        Ok(Options(options))
336    }
337
338    pub fn is_empty(&self) -> bool {
339        self.0.is_empty()
340    }
341
342    pub fn as_slice(&self) -> &[OptionDef] {
343        &self.0
344    }
345}
346
347impl TryFrom<Vec<OptionDef>> for Options {
348    type Error = String;
349
350    fn try_from(options: Vec<OptionDef>) -> Result<Self, Self::Error> {
351        Options::new(options)
352    }
353}
354
355impl From<Options> for Vec<OptionDef> {
356    fn from(options: Options) -> Self {
357        options.0
358    }
359}
360
361/// The names taken so far. A variable and a feature are named apart, since
362/// nothing reads one where it reads the other.
363#[derive(Default)]
364struct Seen<'a> {
365    vars: BTreeSet<&'a str>,
366    features: BTreeSet<&'a str>,
367}
368
369fn check<'a>(option: &'a OptionDef, seen: &mut Seen<'a>) -> Result<(), String> {
370    let name = option.name();
371    if name.is_empty() {
372        return Err("an option has an empty name".to_owned());
373    }
374    let (taken, what) = match option {
375        OptionDef::Feature { .. } => (&mut seen.features, "feature"),
376        _ => (&mut seen.vars, "variable"),
377    };
378    if !taken.insert(name) {
379        return Err(format!("{what} `{name}` is an option twice"));
380    }
381
382    match option {
383        OptionDef::Slider {
384            min,
385            max,
386            step,
387            default,
388            ..
389        } => {
390            if ![min, max, step, default].iter().all(|n| n.is_finite()) {
391                return Err(format!("slider `{name}` has a number that is not finite"));
392            }
393            if min >= max {
394                return Err(format!("slider `{name}` has no range: {min} to {max}"));
395            }
396            if *step <= 0.0 {
397                return Err(format!("slider `{name}` has a step of {step}"));
398            }
399            if default < min || default > max {
400                return Err(format!(
401                    "the default of slider `{name}`, {default}, is outside {min} to {max}"
402                ));
403            }
404        }
405        OptionDef::Select {
406            choices, default, ..
407        } => {
408            let mut values = BTreeSet::new();
409            if let Some(twice) = choices.iter().find(|c| !values.insert(c.value.as_str())) {
410                return Err(format!("select `{name}` offers `{}` twice", twice.value));
411            }
412            if !values.contains(default.as_str()) {
413                return Err(format!(
414                    "the default of select `{name}`, `{default}`, is not one of its choices"
415                ));
416            }
417        }
418        OptionDef::Feature { options, .. } => {
419            options.iter().try_for_each(|child| check(child, seen))?;
420        }
421        OptionDef::Text { .. } | OptionDef::File { .. } | OptionDef::Directory { .. } => {}
422    }
423    Ok(())
424}