1use std::collections::BTreeSet;
17
18use crate::pass::{Options, Pass};
19
20pub trait Choice: Copy + PartialEq + 'static {
23 const VALUES: &'static [(&'static str, Self)];
25
26 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42enum Need {
43 Required,
45 OneOf(&'static str),
47 Optional,
50}
51
52#[derive(Clone, Copy, Debug)]
55pub struct OptionSpec {
56 key: &'static str,
57 need: Need,
58 example: &'static str,
59 doc: &'static str,
60 absent: &'static str,
62 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 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 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 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 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 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 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 pub fn absent(&self) -> Option<&'static str> {
174 (self.need == Need::Optional).then_some(self.absent)
175 }
176
177 pub fn is_needed(&self) -> bool {
179 self.need != Need::Optional
180 }
181
182 pub fn group(&self) -> Option<&'static str> {
184 match self.need {
185 Need::OneOf(g) => Some(g),
186 _ => None,
187 }
188 }
189
190 pub fn values(&self) -> Option<Vec<&'static str>> {
192 self.values.map(|f| f())
193 }
194}
195
196fn 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
302pub 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 fn groups(&self) -> Vec<(&'static str, Vec<&OptionSpec>)> {
322 groups(self.options)
323 }
324
325 pub fn requires_options(&self) -> bool {
327 self.options.iter().any(OptionSpec::is_needed)
328 }
329
330 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 pub fn check(&self, options: &Options) -> Result<(), String> {
348 check_options(self.name, self.options, options)
349 }
350
351 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 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 pub(crate) fn validate(&self) {
410 validate_options(self.name, self.options);
411 }
412}
413
414#[derive(Debug)]
418pub struct Given<'a> {
419 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 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 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 pub fn choice<T: Choice>(&self, key: &str) -> T {
481 let text = self.required(key);
482 self.of(key, text)
483 }
484
485 pub fn optional_choice<T: Choice>(&self, key: &str) -> Option<T> {
487 self.optional(key).map(|text| self.of(key, text))
488 }
489}
490
491pub trait TextForm: Sized {
497 const NAME: &'static str;
499 const OPTIONS: &'static [OptionSpec];
501
502 fn make(given: &Given) -> Result<Self, String>;
504
505 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}