1use std::collections::BTreeSet;
13
14use serde::{Deserialize, Serialize};
15
16#[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#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Label {
27 pub title: String,
28 pub subtitle: Option<String>,
29}
30
31#[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#[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 #[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 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 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#[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 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#[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}