Skip to main content

primase_core/
pass_options.rs

1//! A pass's options declared once ([`PassSpec`]): each option's key, whether a pipeline must give it, an example value
2//! and what it means ([`OptionSpec`]). The registry derives the rest from the declaration
3//! ([`crate::pass::PassRegistry::add`]): it refuses an undeclared key, a missing required option and a value outside a
4//! [`Choice`]'s, each in one form, before the pass is made; it makes the pass from the examples to read its declarations
5//! (a pass with a required option is never run so); and it checks at registration that the pass made from the
6//! examples prints back what it was made with. The pass reads what the declaration let through ([`Given`]): a required
7//! option is there, a choice is its enum.
8//!
9//! What stays the pass's: parsing and checking a value (numbers, `;` lists, values the data names), and how options
10//! constrain each other beyond one-of groups (all four of a rectangle or none; one value that needs another).
11//!
12//! A program's choices that are not a pass (how a reader reads) take the same form ([`TextForm`]): a name and declared
13//! options, checked by the same declaration and read through the same [`Given`], so a program can state them as text
14//! another program runs as written.
15
16use std::collections::BTreeSet;
17
18use crate::pass::{Options, Pass};
19
20/// The values of an option that are a fixed set of names, listed once: the registry refuses any other and names these;
21/// the pass reads the enum ([`Given::choice`]), never the text, so there is no second list to drift from.
22pub trait Choice: Copy + PartialEq + 'static {
23    /// Each value's text and the value; the first is the option's example.
24    const VALUES: &'static [(&'static str, Self)];
25
26    /// The text of `self`, as a pass prints it back ([`Pass::options`]).
27    fn text(self) -> &'static str {
28        Self::VALUES
29            .iter()
30            .find(|(_, v)| *v == self)
31            .map(|(t, _)| *t)
32            .expect("every value of a Choice is listed in its VALUES")
33    }
34}
35
36impl Choice for bool {
37    const VALUES: &'static [(&'static str, Self)] = &[("false", false), ("true", true)];
38}
39
40/// Whether a pipeline must give an option.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42enum Need {
43    /// It must: nothing is assumed for the caller.
44    Required,
45    /// Exactly one of the options sharing this group name must be given.
46    OneOf(&'static str),
47    /// It may: its absence has the meaning its declaration states ([`OptionSpec::absent`]; never a choice made for the
48    /// caller: CONTRIBUTING "Drift").
49    Optional,
50}
51
52/// One option a pass takes: its key, whether it is needed, an example value and what it means. Built only by its
53/// constructors, so a required option always has its example and an optional one always says what its absence means.
54#[derive(Clone, Copy, Debug)]
55pub struct OptionSpec {
56    key: &'static str,
57    need: Need,
58    example: &'static str,
59    doc: &'static str,
60    /// What no value means, for an optional option (empty for the others).
61    absent: &'static str,
62    /// The texts of a [`Choice`]'s values, for a choice option.
63    values: Option<fn() -> Vec<&'static str>>,
64}
65
66fn texts<T: Choice>() -> Vec<&'static str> {
67    T::VALUES.iter().map(|(t, _)| *t).collect()
68}
69
70impl OptionSpec {
71    /// An option a pipeline must give.
72    pub const fn required(key: &'static str, example: &'static str, doc: &'static str) -> Self {
73        OptionSpec {
74            key,
75            need: Need::Required,
76            example,
77            doc,
78            absent: "",
79            values: None,
80        }
81    }
82
83    /// An option a pipeline may give; `absent` says what no value means (the registry refuses an empty one). Its
84    /// example only documents it: optional options may exclude each other (`at=centre` or `east` and `north`), so the
85    /// registry makes no pass from them.
86    pub const fn optional(
87        key: &'static str,
88        example: &'static str,
89        doc: &'static str,
90        absent: &'static str,
91    ) -> Self {
92        OptionSpec {
93            key,
94            need: Need::Optional,
95            example,
96            doc,
97            absent,
98            values: None,
99        }
100    }
101
102    /// One of the options of `group`, of which a pipeline gives exactly one.
103    pub const fn one_of(
104        group: &'static str,
105        key: &'static str,
106        example: &'static str,
107        doc: &'static str,
108    ) -> Self {
109        OptionSpec {
110            key,
111            need: Need::OneOf(group),
112            example,
113            doc,
114            absent: "",
115            values: None,
116        }
117    }
118
119    /// One of the options of `group`, of which a pipeline gives exactly one, whose value is one of `T`'s; the example
120    /// is the first. Read by [`Given::optional_choice`].
121    pub const fn one_of_choice<T: Choice>(group: &'static str, key: &'static str, doc: &'static str) -> Self {
122        OptionSpec {
123            key,
124            need: Need::OneOf(group),
125            example: T::VALUES[0].0,
126            doc,
127            absent: "",
128            values: Some(texts::<T>),
129        }
130    }
131
132    /// A required option whose value is one of `T`'s; the example is the first.
133    pub const fn choice<T: Choice>(key: &'static str, doc: &'static str) -> Self {
134        OptionSpec {
135            key,
136            need: Need::Required,
137            example: T::VALUES[0].0,
138            doc,
139            absent: "",
140            values: Some(texts::<T>),
141        }
142    }
143
144    /// An optional option whose value, when given, is one of `T`'s; `absent` says what no value means.
145    pub const fn optional_choice<T: Choice>(
146        key: &'static str,
147        doc: &'static str,
148        absent: &'static str,
149    ) -> Self {
150        OptionSpec {
151            key,
152            need: Need::Optional,
153            example: T::VALUES[0].0,
154            doc,
155            absent,
156            values: Some(texts::<T>),
157        }
158    }
159
160    pub fn key(&self) -> &'static str {
161        self.key
162    }
163
164    pub fn example(&self) -> &'static str {
165        self.example
166    }
167
168    pub fn doc(&self) -> &'static str {
169        self.doc
170    }
171
172    /// What no value means, for an optional option.
173    pub fn absent(&self) -> Option<&'static str> {
174        (self.need == Need::Optional).then_some(self.absent)
175    }
176
177    /// Whether a pipeline must give it (alone, or as one of its group).
178    pub fn is_needed(&self) -> bool {
179        self.need != Need::Optional
180    }
181
182    /// The group it is one of, if it is.
183    pub fn group(&self) -> Option<&'static str> {
184        match self.need {
185            Need::OneOf(g) => Some(g),
186            _ => None,
187        }
188    }
189
190    /// The texts of its values, for a choice option.
191    pub fn values(&self) -> Option<Vec<&'static str>> {
192        self.values.map(|f| f())
193    }
194}
195
196/// `items` as `a`, `a or b`, `a, b or c`.
197fn alternatives(items: &[&str]) -> String {
198    match items {
199        [] => String::new(),
200        [one] => (*one).to_owned(),
201        [rest @ .., last] => format!("{} or {last}", rest.join(", ")),
202    }
203}
204
205fn groups(declarations: &[OptionSpec]) -> Vec<(&'static str, Vec<&OptionSpec>)> {
206    let mut groups: Vec<(&'static str, Vec<&OptionSpec>)> = Vec::new();
207    for o in declarations {
208        if let Need::OneOf(g) = o.need {
209            match groups.iter_mut().find(|(name, _)| *name == g) {
210                Some((_, members)) => members.push(o),
211                None => groups.push((g, vec![o])),
212            }
213        }
214    }
215    groups
216}
217
218pub(crate) fn check_options(
219    name: &str,
220    declarations: &[OptionSpec],
221    options: &Options,
222) -> Result<(), String> {
223    if let Some(k) = options
224        .keys()
225        .find(|k| !declarations.iter().any(|option| option.key == k.as_str()))
226    {
227        let keys: Vec<&str> = declarations.iter().map(|o| o.key).collect();
228        return Err(if keys.is_empty() {
229            format!("unknown option {k} ({} takes none)", name)
230        } else {
231            format!("unknown option {k} ({} takes {})", name, keys.join(", "))
232        });
233    }
234    if let Some(o) = declarations
235        .iter()
236        .find(|o| o.need == Need::Required && !options.contains_key(o.key))
237    {
238        return Err(format!("{} is required: {}", o.key, o.doc));
239    }
240    for (_, members) in groups(declarations) {
241        let keys: Vec<&str> = members.iter().map(|o| o.key).collect();
242        match members.iter().filter(|o| options.contains_key(o.key)).count() {
243            0 => {
244                let docs: Vec<String> = members.iter().map(|o| format!("{}: {}", o.key, o.doc)).collect();
245                return Err(format!(
246                    "one of {} is required ({})",
247                    alternatives(&keys),
248                    docs.join("; ")
249                ));
250            }
251            1 => {}
252            _ => return Err(format!("only one of {} may be given", alternatives(&keys))),
253        }
254    }
255    for (k, v) in options {
256        if let Some(values) = declarations
257            .iter()
258            .find(|option| option.key == k.as_str())
259            .and_then(OptionSpec::values)
260            && !values.contains(&v.as_str())
261        {
262            return Err(format!("{k}={v} is none of {}", alternatives(&values)));
263        }
264    }
265    Ok(())
266}
267
268pub(crate) fn validate_options(name: &str, declarations: &[OptionSpec]) {
269    let mut keys = BTreeSet::new();
270    for o in declarations {
271        assert!(
272            keys.insert(o.key),
273            "pass {}: option {} declared twice",
274            name,
275            o.key
276        );
277        assert!(
278            o.absent().is_none_or(|a| !a.trim().is_empty()),
279            "pass {}: optional option {} says nothing of what its absence means",
280            name,
281            o.key
282        );
283        if let Some(values) = o.values() {
284            let distinct: BTreeSet<&str> = values.iter().copied().collect();
285            assert!(
286                !values.is_empty() && distinct.len() == values.len(),
287                "pass {}: option {}: a choice's values are none or one twice: {values:?}",
288                name,
289                o.key
290            );
291        }
292    }
293    for (g, members) in groups(declarations) {
294        assert!(
295            members.len() > 1,
296            "pass {}: the one-of group {g} has one option",
297            name
298        );
299    }
300}
301
302/// A pass as the registry knows it: its name, the options it takes, and how it is made from what the declaration let
303/// through.
304pub struct PassSpec {
305    pub name: &'static str,
306    pub options: &'static [OptionSpec],
307    pub make: fn(&Given) -> Result<Box<dyn Pass>, String>,
308}
309
310impl std::fmt::Debug for PassSpec {
311    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312        f.debug_struct("PassSpec")
313            .field("name", &self.name)
314            .field("options", &self.options)
315            .finish_non_exhaustive()
316    }
317}
318
319impl PassSpec {
320    /// The groups of one-of options, in the order they are first declared, each with its members.
321    fn groups(&self) -> Vec<(&'static str, Vec<&OptionSpec>)> {
322        groups(self.options)
323    }
324
325    /// Whether a pipeline must give it options: a required option or a one-of group.
326    pub fn requires_options(&self) -> bool {
327        self.options.iter().any(OptionSpec::is_needed)
328    }
329
330    /// The options it is made with to read its declarations: each required option's example and the first option's of
331    /// each group.
332    pub fn example(&self) -> Options {
333        let mut seen = BTreeSet::new();
334        self.options
335            .iter()
336            .filter(|o| match o.need {
337                Need::Required => true,
338                Need::OneOf(g) => seen.insert(g),
339                Need::Optional => false,
340            })
341            .map(|o| (o.key.to_owned(), o.example.to_owned()))
342            .collect()
343    }
344
345    /// `options` against the declaration: the refusal of the first undeclared key, missing required option, one-of
346    /// group given none or more than one of, or choice value outside its values.
347    pub fn check(&self, options: &Options) -> Result<(), String> {
348        check_options(self.name, self.options, options)
349    }
350
351    /// The pass made from `options`, once [`PassSpec::check`] lets them through.
352    pub fn instance(&self, options: &Options) -> Result<Box<dyn Pass>, String> {
353        self.check(options)?;
354        (self.make)(&Given::new(self.name, self.options, options))
355    }
356
357    /// Its text form and what each option means: `translate-world{by*}` (`*` a required option, `|` between the
358    /// options of a group), then a line per option, an optional one's ending with what its absence means.
359    pub fn describe(&self) -> String {
360        let mut out = String::from(self.name);
361        if !self.options.is_empty() {
362            let mut keys: Vec<String> = Vec::new();
363            for o in self.options {
364                match o.need {
365                    Need::Required => keys.push(format!("{}*", o.key)),
366                    Need::Optional => keys.push(o.key.to_owned()),
367                    Need::OneOf(g) => {
368                        if self
369                            .groups()
370                            .iter()
371                            .any(|(name, m)| *name == g && m[0].key == o.key)
372                        {
373                            let members: Vec<&str> = self
374                                .options
375                                .iter()
376                                .filter(|m| m.need == Need::OneOf(g))
377                                .map(|m| m.key)
378                                .collect();
379                            keys.push(format!("({})*", members.join("|")));
380                        }
381                    }
382                }
383            }
384            out.push_str(&format!("{{{}}}", keys.join(",")));
385        }
386        for o in self.options {
387            let need = match o.need {
388                Need::Required => "required".to_owned(),
389                Need::OneOf(g) => format!("one of {g}"),
390                Need::Optional => "optional".to_owned(),
391            };
392            let values = o
393                .values()
394                .map(|v| format!("; {}", v.join(" | ")))
395                .unwrap_or_default();
396            out.push_str(&format!(
397                "\n  {} ({need}{values}; e.g. {}): {}",
398                o.key, o.example, o.doc
399            ));
400            if let Some(absent) = o.absent() {
401                out.push_str(&format!("; without it: {absent}"));
402            }
403        }
404        out
405    }
406
407    /// Panics where the declaration itself is wrong: a key twice, a group of one, a choice with no values or a value
408    /// twice, an optional option that says nothing of its absence.
409    pub(crate) fn validate(&self) {
410        validate_options(self.name, self.options);
411    }
412}
413
414/// The options a pass (or a [`TextForm`]) is made from, after its declaration let them through ([`PassSpec::check`]).
415/// Reading a key the declaration does not have, or reading it as what it is not declared as, is a bug in the pass: a
416/// panic.
417#[derive(Debug)]
418pub struct Given<'a> {
419    /// The name of what is made, for the panics.
420    name: &'a str,
421    declarations: &'a [OptionSpec],
422    options: &'a Options,
423}
424
425impl<'a> Given<'a> {
426    pub(crate) fn new(name: &'a str, declarations: &'a [OptionSpec], options: &'a Options) -> Self {
427        Given {
428            name,
429            declarations,
430            options,
431        }
432    }
433
434    fn declared(&self, key: &str) -> &'a OptionSpec {
435        self.declarations
436            .iter()
437            .find(|o| o.key == key)
438            .unwrap_or_else(|| {
439                panic!(
440                    "{} reads option {key}, which its declaration does not have",
441                    self.name
442                )
443            })
444    }
445
446    /// A required option's value: always there.
447    pub fn required(&self, key: &str) -> &'a str {
448        let o = self.declared(key);
449        assert!(
450            o.need == Need::Required,
451            "{} reads option {key} as required, which it declares optional or one of a group",
452            self.name
453        );
454        self.options
455            .get(key)
456            .map(String::as_str)
457            .expect("the declaration's check lets no required option be missing")
458    }
459
460    /// An optional option's value, or a one-of option's, if given.
461    pub fn optional(&self, key: &str) -> Option<&'a str> {
462        self.declared(key);
463        self.options.get(key).map(String::as_str)
464    }
465
466    fn of<T: Choice>(&self, key: &str, text: &str) -> T {
467        T::VALUES
468            .iter()
469            .find(|(t, _)| *t == text)
470            .map(|(_, v)| *v)
471            .unwrap_or_else(|| {
472                panic!(
473                    "{} reads option {key} as a choice its declaration does not give it",
474                    self.name
475                )
476            })
477    }
478
479    /// A required choice's value.
480    pub fn choice<T: Choice>(&self, key: &str) -> T {
481        let text = self.required(key);
482        self.of(key, text)
483    }
484
485    /// An optional choice's value, if given.
486    pub fn optional_choice<T: Choice>(&self, key: &str) -> Option<T> {
487        self.optional(key).map(|text| self.of(key, text))
488    }
489}
490
491/// A program's choices that are not a pass, stated as a pass is (`read-fromsoft-area{area=m10_00_00_00,margin=30}`): a
492/// name, the options it takes declared once, made from what the declaration let through, and printing back what it was
493/// made with. [`crate::pass::parse_text_form`] reads it and [`crate::pass::text_form`] writes it, with the declaration's
494/// refusals (an undeclared key, a missing required option, a value outside a choice's) in a pass's words;
495/// [`crate::pass::validate_text_form`] checks the declaration as [`crate::pass::PassRegistry::add`] checks a pass's.
496pub trait TextForm: Sized {
497    /// The name its text starts with.
498    const NAME: &'static str;
499    /// The options it takes.
500    const OPTIONS: &'static [OptionSpec];
501
502    /// It, from what the declaration let through; the refusal names the option whose value it cannot take.
503    fn make(given: &Given) -> Result<Self, String>;
504
505    /// Its options, as text: what it was made with.
506    fn options(&self) -> Options;
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512    use crate::World;
513    use crate::pass::PassLog;
514
515    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
516    enum Detail {
517        Plain,
518        High,
519    }
520
521    impl Choice for Detail {
522        const VALUES: &'static [(&'static str, Self)] = &[("plain", Detail::Plain), ("high", Detail::High)];
523    }
524
525    /// A pass that prints back what it was made with.
526    #[derive(Debug)]
527    struct Toy(Options);
528
529    impl Pass for Toy {
530        fn name(&self) -> &'static str {
531            "toy"
532        }
533        fn options(&self) -> Options {
534            self.0.clone()
535        }
536        fn run(&self, _w: &mut World, _log: &mut PassLog) -> Result<(), String> {
537            Ok(())
538        }
539    }
540
541    const TOY: PassSpec = PassSpec {
542        name: "toy",
543        options: &[
544            OptionSpec::required("cell", "4", "metres, above 0: the grid's cell"),
545            OptionSpec::choice::<Detail>("detail", "which collision is written"),
546            OptionSpec::one_of("by", "factor", "2", "the enlargement"),
547            OptionSpec::one_of("by", "size", "64", "the larger side to reach"),
548            OptionSpec::optional("halo", "1", "metres around each cell", "no halo"),
549            OptionSpec::optional_choice::<Detail>(
550                "fallback",
551                "the detail used where one is missing",
552                "no fallback",
553            ),
554        ],
555        make: |g| {
556            // every way of reading what the declaration let through
557            let _ = (
558                g.required("cell"),
559                g.choice::<Detail>("detail"),
560                g.optional("halo"),
561            );
562            let _ = (
563                g.optional("factor"),
564                g.optional("size"),
565                g.optional_choice::<Detail>("fallback"),
566            );
567            Ok(Box::new(Toy(Options::new())))
568        },
569    };
570
571    fn options(items: &[(&str, &str)]) -> Options {
572        items
573            .iter()
574            .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
575            .collect()
576    }
577
578    /// The example is each required option's and the first of the group's; a choice's example is its first value.
579    #[test]
580    fn the_examples_come_from_the_declaration() {
581        TOY.validate();
582        assert!(TOY.requires_options());
583        assert_eq!(
584            TOY.example(),
585            options(&[("cell", "4"), ("detail", "plain"), ("factor", "2")])
586        );
587        assert!(TOY.instance(&TOY.example()).is_ok());
588    }
589
590    /// Each refusal in its one form, before the pass is made.
591    #[test]
592    fn the_declaration_refuses_before_the_pass_is_made() {
593        let refusal = |items: &[(&str, &str)]| TOY.check(&options(items)).unwrap_err();
594        let base = [("cell", "4"), ("detail", "plain"), ("factor", "2")];
595        let with = |extra: (&'static str, &'static str)| {
596            let mut o = base.to_vec();
597            o.push(extra);
598            o
599        };
600        assert_eq!(
601            refusal(&with(("colour", "red"))),
602            "unknown option colour (toy takes cell, detail, factor, size, halo, fallback)"
603        );
604        assert_eq!(
605            refusal(&[("detail", "plain"), ("factor", "2")]),
606            "cell is required: metres, above 0: the grid's cell"
607        );
608        assert_eq!(
609            refusal(&[("cell", "4"), ("detail", "plain")]),
610            "one of factor or size is required (factor: the enlargement; size: the larger side to reach)"
611        );
612        assert_eq!(
613            refusal(&with(("size", "64"))),
614            "only one of factor or size may be given"
615        );
616        assert_eq!(
617            refusal(&[("cell", "4"), ("detail", "fine"), ("factor", "2")]),
618            "detail=fine is none of plain or high"
619        );
620        assert_eq!(
621            refusal(&with(("fallback", "low"))),
622            "fallback=low is none of plain or high"
623        );
624        let none = PassSpec {
625            name: "bare",
626            options: &[],
627            make: |_| Ok(Box::new(Toy(Options::new()))),
628        };
629        assert_eq!(
630            none.check(&options(&[("x", "1")])).unwrap_err(),
631            "unknown option x (bare takes none)"
632        );
633        assert!(!none.requires_options());
634    }
635
636    /// A choice reads back as its enum and prints as its text.
637    #[test]
638    fn a_choice_is_read_as_its_enum() {
639        const READ: PassSpec = PassSpec {
640            name: "read",
641            options: &[OptionSpec::choice::<Detail>("detail", "which")],
642            make: |g| {
643                let detail = g.choice::<Detail>("detail");
644                Ok(Box::new(Toy(options(&[("detail", detail.text())]))))
645            },
646        };
647        let pass = READ.instance(&options(&[("detail", "high")])).unwrap();
648        assert_eq!(pass.options(), options(&[("detail", "high")]));
649        assert_eq!(Detail::High.text(), "high");
650    }
651
652    /// A one-of choice is one of its group, refused outside its values, and read as its enum when given.
653    #[test]
654    fn a_one_of_choice_is_a_group_member_with_values() {
655        const SOURCE: PassSpec = PassSpec {
656            name: "source",
657            options: &[
658                OptionSpec::one_of_choice::<Detail>("source", "preset", "a preset detail"),
659                OptionSpec::one_of("source", "file", "d.txt", "a file stating the detail"),
660            ],
661            make: |g| {
662                let printed = match (g.optional_choice::<Detail>("preset"), g.optional("file")) {
663                    (Some(d), None) => options(&[("preset", d.text())]),
664                    (None, Some(f)) => options(&[("file", f)]),
665                    _ => unreachable!("the group gives one"),
666                };
667                Ok(Box::new(Toy(printed)))
668            },
669        };
670        SOURCE.validate();
671        assert_eq!(SOURCE.example(), options(&[("preset", "plain")]));
672        let pass = SOURCE.instance(&options(&[("preset", "high")])).unwrap();
673        assert_eq!(pass.options(), options(&[("preset", "high")]));
674        assert_eq!(
675            SOURCE.check(&options(&[("preset", "fine")])).unwrap_err(),
676            "preset=fine is none of plain or high"
677        );
678        assert_eq!(
679            SOURCE
680                .check(&options(&[("preset", "high"), ("file", "d.txt")]))
681                .unwrap_err(),
682            "only one of preset or file may be given"
683        );
684        assert!(
685            SOURCE
686                .describe()
687                .contains("\n  preset (one of source; plain | high; e.g. plain): a preset detail"),
688            "{}",
689            SOURCE.describe()
690        );
691    }
692
693    /// The text form marks required options and groups, and each option's line says its need, values and example.
694    #[test]
695    fn a_declaration_describes_its_options() {
696        let text = TOY.describe();
697        assert!(
698            text.starts_with("toy{cell*,detail*,(factor|size)*,halo,fallback}\n"),
699            "{text}"
700        );
701        assert!(
702            text.contains("\n  detail (required; plain | high; e.g. plain): which collision is written"),
703            "{text}"
704        );
705        assert!(
706            text.contains("\n  size (one of by; e.g. 64): the larger side to reach"),
707            "{text}"
708        );
709        assert!(
710            text.contains("\n  halo (optional; e.g. 1): metres around each cell; without it: no halo"),
711            "{text}"
712        );
713    }
714
715    /// Reading an option the declaration does not have is a bug in the pass.
716    #[test]
717    #[should_panic(expected = "bad reads option other, which its declaration does not have")]
718    fn reading_an_undeclared_option_panics() {
719        const BAD: PassSpec = PassSpec {
720            name: "bad",
721            options: &[OptionSpec::optional("one", "1", "nothing", "nothing either")],
722            make: |g| {
723                let _ = g.optional("other");
724                Ok(Box::new(Toy(Options::new())))
725            },
726        };
727        let _ = BAD.instance(&Options::new());
728    }
729
730    /// An optional option that says nothing of its absence is a wrong declaration: what no value means is part of
731    /// declaring it, as a required option's example is.
732    #[test]
733    #[should_panic(expected = "pass silent: optional option halo says nothing of what its absence means")]
734    fn an_optional_option_states_its_absence() {
735        const SILENT: PassSpec = PassSpec {
736            name: "silent",
737            options: &[OptionSpec::optional("halo", "1", "metres around each cell", " ")],
738            make: |_| Ok(Box::new(Toy(Options::new()))),
739        };
740        SILENT.validate();
741    }
742
743    /// A group of one is a wrong declaration.
744    #[test]
745    #[should_panic(expected = "pass lonely: the one-of group by has one option")]
746    fn a_group_of_one_is_refused() {
747        const LONELY: PassSpec = PassSpec {
748            name: "lonely",
749            options: &[OptionSpec::one_of("by", "factor", "2", "the enlargement")],
750            make: |_| Ok(Box::new(Toy(Options::new()))),
751        };
752        LONELY.validate();
753    }
754
755    /// A reader's choices in a pass's form: a cell size and a detail, and a name it may leave out.
756    #[derive(Debug, PartialEq)]
757    struct Read {
758        name: Option<String>,
759        cell: f32,
760        detail: Detail,
761    }
762
763    impl TextForm for Read {
764        const NAME: &'static str = "read-toy";
765        const OPTIONS: &'static [OptionSpec] = &[
766            OptionSpec::optional("name", "a", "what is read", "the caller names it"),
767            OptionSpec::required("cell", "4", "metres, above 0: the grid's cell"),
768            OptionSpec::choice::<Detail>("detail", "which collision is read"),
769        ];
770        fn make(given: &Given) -> Result<Self, String> {
771            let cell = given.required("cell");
772            Ok(Read {
773                name: given.optional("name").map(str::to_owned),
774                cell: cell
775                    .parse()
776                    .ok()
777                    .filter(|c: &f32| *c > 0.)
778                    .ok_or_else(|| format!("cell={cell} is not a number of metres above 0"))?,
779                detail: given.choice("detail"),
780            })
781        }
782        fn options(&self) -> Options {
783            let mut options = options(&[("cell", &self.cell.to_string()), ("detail", self.detail.text())]);
784            if let Some(name) = &self.name {
785                options.insert("name".into(), name.clone());
786            }
787            options
788        }
789    }
790
791    /// A text form reads back what it prints, field by field, and refuses as a pass's declaration does, naming the
792    /// option; a text that is not one item of its name is refused.
793    #[test]
794    fn a_text_form_reads_back_what_it_prints() {
795        use crate::pass::{parse_text_form, text_form, validate_text_form};
796        validate_text_form::<Read>();
797        for read in [
798            Read {
799                name: Some("m10".into()),
800                cell: 2.5,
801                detail: Detail::High,
802            },
803            Read {
804                name: None,
805                cell: 4.,
806                detail: Detail::Plain,
807            },
808        ] {
809            let text = text_form(&read);
810            assert_eq!(parse_text_form::<Read>(&text).unwrap(), read, "{text}");
811        }
812        assert_eq!(
813            text_form(&Read {
814                name: Some("m10".into()),
815                cell: 2.5,
816                detail: Detail::High
817            }),
818            "read-toy{cell=2.5,detail=high,name=m10}"
819        );
820        let refusal = |text: &str| parse_text_form::<Read>(text).unwrap_err().to_string();
821        assert_eq!(
822            refusal("read-toy{detail=plain}"),
823            "read-toy: cell is required: metres, above 0: the grid's cell"
824        );
825        assert_eq!(
826            refusal("read-toy{cell=4,detail=fine}"),
827            "read-toy: detail=fine is none of plain or high"
828        );
829        assert_eq!(
830            refusal("read-toy{cell=4,detail=plain,colour=red}"),
831            "read-toy: unknown option colour (read-toy takes name, cell, detail)"
832        );
833        assert_eq!(
834            refusal("read-toy{cell=-1,detail=plain}"),
835            "read-toy: cell=-1 is not a number of metres above 0"
836        );
837        assert_eq!(refusal("toy{cell=4,detail=plain}"), "toy is not read-toy");
838        assert!(
839            refusal("read-toy{cell=4,detail=plain}, read-toy{cell=4,detail=plain}")
840                .contains("is not one read-toy")
841        );
842        assert!(refusal("").contains("is not one read-toy"));
843    }
844}