use std::collections::BTreeSet;
use crate::pass::{Options, Pass};
pub trait Choice: Copy + PartialEq + 'static {
const VALUES: &'static [(&'static str, Self)];
fn text(self) -> &'static str {
Self::VALUES
.iter()
.find(|(_, v)| *v == self)
.map(|(t, _)| *t)
.expect("every value of a Choice is listed in its VALUES")
}
}
impl Choice for bool {
const VALUES: &'static [(&'static str, Self)] = &[("false", false), ("true", true)];
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Need {
Required,
OneOf(&'static str),
Optional,
}
#[derive(Clone, Copy, Debug)]
pub struct OptionSpec {
key: &'static str,
need: Need,
example: &'static str,
doc: &'static str,
absent: &'static str,
values: Option<fn() -> Vec<&'static str>>,
}
fn texts<T: Choice>() -> Vec<&'static str> {
T::VALUES.iter().map(|(t, _)| *t).collect()
}
impl OptionSpec {
pub const fn required(key: &'static str, example: &'static str, doc: &'static str) -> Self {
OptionSpec {
key,
need: Need::Required,
example,
doc,
absent: "",
values: None,
}
}
pub const fn optional(
key: &'static str,
example: &'static str,
doc: &'static str,
absent: &'static str,
) -> Self {
OptionSpec {
key,
need: Need::Optional,
example,
doc,
absent,
values: None,
}
}
pub const fn one_of(
group: &'static str,
key: &'static str,
example: &'static str,
doc: &'static str,
) -> Self {
OptionSpec {
key,
need: Need::OneOf(group),
example,
doc,
absent: "",
values: None,
}
}
pub const fn one_of_choice<T: Choice>(group: &'static str, key: &'static str, doc: &'static str) -> Self {
OptionSpec {
key,
need: Need::OneOf(group),
example: T::VALUES[0].0,
doc,
absent: "",
values: Some(texts::<T>),
}
}
pub const fn choice<T: Choice>(key: &'static str, doc: &'static str) -> Self {
OptionSpec {
key,
need: Need::Required,
example: T::VALUES[0].0,
doc,
absent: "",
values: Some(texts::<T>),
}
}
pub const fn optional_choice<T: Choice>(
key: &'static str,
doc: &'static str,
absent: &'static str,
) -> Self {
OptionSpec {
key,
need: Need::Optional,
example: T::VALUES[0].0,
doc,
absent,
values: Some(texts::<T>),
}
}
pub fn key(&self) -> &'static str {
self.key
}
pub fn example(&self) -> &'static str {
self.example
}
pub fn doc(&self) -> &'static str {
self.doc
}
pub fn absent(&self) -> Option<&'static str> {
(self.need == Need::Optional).then_some(self.absent)
}
pub fn is_needed(&self) -> bool {
self.need != Need::Optional
}
pub fn group(&self) -> Option<&'static str> {
match self.need {
Need::OneOf(g) => Some(g),
_ => None,
}
}
pub fn values(&self) -> Option<Vec<&'static str>> {
self.values.map(|f| f())
}
}
fn alternatives(items: &[&str]) -> String {
match items {
[] => String::new(),
[one] => (*one).to_owned(),
[rest @ .., last] => format!("{} or {last}", rest.join(", ")),
}
}
fn groups(declarations: &[OptionSpec]) -> Vec<(&'static str, Vec<&OptionSpec>)> {
let mut groups: Vec<(&'static str, Vec<&OptionSpec>)> = Vec::new();
for o in declarations {
if let Need::OneOf(g) = o.need {
match groups.iter_mut().find(|(name, _)| *name == g) {
Some((_, members)) => members.push(o),
None => groups.push((g, vec![o])),
}
}
}
groups
}
pub(crate) fn check_options(
name: &str,
declarations: &[OptionSpec],
options: &Options,
) -> Result<(), String> {
if let Some(k) = options
.keys()
.find(|k| !declarations.iter().any(|option| option.key == k.as_str()))
{
let keys: Vec<&str> = declarations.iter().map(|o| o.key).collect();
return Err(if keys.is_empty() {
format!("unknown option {k} ({} takes none)", name)
} else {
format!("unknown option {k} ({} takes {})", name, keys.join(", "))
});
}
if let Some(o) = declarations
.iter()
.find(|o| o.need == Need::Required && !options.contains_key(o.key))
{
return Err(format!("{} is required: {}", o.key, o.doc));
}
for (_, members) in groups(declarations) {
let keys: Vec<&str> = members.iter().map(|o| o.key).collect();
match members.iter().filter(|o| options.contains_key(o.key)).count() {
0 => {
let docs: Vec<String> = members.iter().map(|o| format!("{}: {}", o.key, o.doc)).collect();
return Err(format!(
"one of {} is required ({})",
alternatives(&keys),
docs.join("; ")
));
}
1 => {}
_ => return Err(format!("only one of {} may be given", alternatives(&keys))),
}
}
for (k, v) in options {
if let Some(values) = declarations
.iter()
.find(|option| option.key == k.as_str())
.and_then(OptionSpec::values)
&& !values.contains(&v.as_str())
{
return Err(format!("{k}={v} is none of {}", alternatives(&values)));
}
}
Ok(())
}
pub(crate) fn validate_options(name: &str, declarations: &[OptionSpec]) {
let mut keys = BTreeSet::new();
for o in declarations {
assert!(
keys.insert(o.key),
"pass {}: option {} declared twice",
name,
o.key
);
assert!(
o.absent().is_none_or(|a| !a.trim().is_empty()),
"pass {}: optional option {} says nothing of what its absence means",
name,
o.key
);
if let Some(values) = o.values() {
let distinct: BTreeSet<&str> = values.iter().copied().collect();
assert!(
!values.is_empty() && distinct.len() == values.len(),
"pass {}: option {}: a choice's values are none or one twice: {values:?}",
name,
o.key
);
}
}
for (g, members) in groups(declarations) {
assert!(
members.len() > 1,
"pass {}: the one-of group {g} has one option",
name
);
}
}
pub struct PassSpec {
pub name: &'static str,
pub options: &'static [OptionSpec],
pub make: fn(&Given) -> Result<Box<dyn Pass>, String>,
}
impl std::fmt::Debug for PassSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PassSpec")
.field("name", &self.name)
.field("options", &self.options)
.finish_non_exhaustive()
}
}
impl PassSpec {
fn groups(&self) -> Vec<(&'static str, Vec<&OptionSpec>)> {
groups(self.options)
}
pub fn requires_options(&self) -> bool {
self.options.iter().any(OptionSpec::is_needed)
}
pub fn example(&self) -> Options {
let mut seen = BTreeSet::new();
self.options
.iter()
.filter(|o| match o.need {
Need::Required => true,
Need::OneOf(g) => seen.insert(g),
Need::Optional => false,
})
.map(|o| (o.key.to_owned(), o.example.to_owned()))
.collect()
}
pub fn check(&self, options: &Options) -> Result<(), String> {
check_options(self.name, self.options, options)
}
pub fn instance(&self, options: &Options) -> Result<Box<dyn Pass>, String> {
self.check(options)?;
(self.make)(&Given::new(self.name, self.options, options))
}
pub fn describe(&self) -> String {
let mut out = String::from(self.name);
if !self.options.is_empty() {
let mut keys: Vec<String> = Vec::new();
for o in self.options {
match o.need {
Need::Required => keys.push(format!("{}*", o.key)),
Need::Optional => keys.push(o.key.to_owned()),
Need::OneOf(g) => {
if self
.groups()
.iter()
.any(|(name, m)| *name == g && m[0].key == o.key)
{
let members: Vec<&str> = self
.options
.iter()
.filter(|m| m.need == Need::OneOf(g))
.map(|m| m.key)
.collect();
keys.push(format!("({})*", members.join("|")));
}
}
}
}
out.push_str(&format!("{{{}}}", keys.join(",")));
}
for o in self.options {
let need = match o.need {
Need::Required => "required".to_owned(),
Need::OneOf(g) => format!("one of {g}"),
Need::Optional => "optional".to_owned(),
};
let values = o
.values()
.map(|v| format!("; {}", v.join(" | ")))
.unwrap_or_default();
out.push_str(&format!(
"\n {} ({need}{values}; e.g. {}): {}",
o.key, o.example, o.doc
));
if let Some(absent) = o.absent() {
out.push_str(&format!("; without it: {absent}"));
}
}
out
}
pub(crate) fn validate(&self) {
validate_options(self.name, self.options);
}
}
#[derive(Debug)]
pub struct Given<'a> {
name: &'a str,
declarations: &'a [OptionSpec],
options: &'a Options,
}
impl<'a> Given<'a> {
pub(crate) fn new(name: &'a str, declarations: &'a [OptionSpec], options: &'a Options) -> Self {
Given {
name,
declarations,
options,
}
}
fn declared(&self, key: &str) -> &'a OptionSpec {
self.declarations
.iter()
.find(|o| o.key == key)
.unwrap_or_else(|| {
panic!(
"{} reads option {key}, which its declaration does not have",
self.name
)
})
}
pub fn required(&self, key: &str) -> &'a str {
let o = self.declared(key);
assert!(
o.need == Need::Required,
"{} reads option {key} as required, which it declares optional or one of a group",
self.name
);
self.options
.get(key)
.map(String::as_str)
.expect("the declaration's check lets no required option be missing")
}
pub fn optional(&self, key: &str) -> Option<&'a str> {
self.declared(key);
self.options.get(key).map(String::as_str)
}
fn of<T: Choice>(&self, key: &str, text: &str) -> T {
T::VALUES
.iter()
.find(|(t, _)| *t == text)
.map(|(_, v)| *v)
.unwrap_or_else(|| {
panic!(
"{} reads option {key} as a choice its declaration does not give it",
self.name
)
})
}
pub fn choice<T: Choice>(&self, key: &str) -> T {
let text = self.required(key);
self.of(key, text)
}
pub fn optional_choice<T: Choice>(&self, key: &str) -> Option<T> {
self.optional(key).map(|text| self.of(key, text))
}
}
pub trait TextForm: Sized {
const NAME: &'static str;
const OPTIONS: &'static [OptionSpec];
fn make(given: &Given) -> Result<Self, String>;
fn options(&self) -> Options;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::World;
use crate::pass::PassLog;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Detail {
Plain,
High,
}
impl Choice for Detail {
const VALUES: &'static [(&'static str, Self)] = &[("plain", Detail::Plain), ("high", Detail::High)];
}
#[derive(Debug)]
struct Toy(Options);
impl Pass for Toy {
fn name(&self) -> &'static str {
"toy"
}
fn options(&self) -> Options {
self.0.clone()
}
fn run(&self, _w: &mut World, _log: &mut PassLog) -> Result<(), String> {
Ok(())
}
}
const TOY: PassSpec = PassSpec {
name: "toy",
options: &[
OptionSpec::required("cell", "4", "metres, above 0: the grid's cell"),
OptionSpec::choice::<Detail>("detail", "which collision is written"),
OptionSpec::one_of("by", "factor", "2", "the enlargement"),
OptionSpec::one_of("by", "size", "64", "the larger side to reach"),
OptionSpec::optional("halo", "1", "metres around each cell", "no halo"),
OptionSpec::optional_choice::<Detail>(
"fallback",
"the detail used where one is missing",
"no fallback",
),
],
make: |g| {
let _ = (
g.required("cell"),
g.choice::<Detail>("detail"),
g.optional("halo"),
);
let _ = (
g.optional("factor"),
g.optional("size"),
g.optional_choice::<Detail>("fallback"),
);
Ok(Box::new(Toy(Options::new())))
},
};
fn options(items: &[(&str, &str)]) -> Options {
items
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect()
}
#[test]
fn the_examples_come_from_the_declaration() {
TOY.validate();
assert!(TOY.requires_options());
assert_eq!(
TOY.example(),
options(&[("cell", "4"), ("detail", "plain"), ("factor", "2")])
);
assert!(TOY.instance(&TOY.example()).is_ok());
}
#[test]
fn the_declaration_refuses_before_the_pass_is_made() {
let refusal = |items: &[(&str, &str)]| TOY.check(&options(items)).unwrap_err();
let base = [("cell", "4"), ("detail", "plain"), ("factor", "2")];
let with = |extra: (&'static str, &'static str)| {
let mut o = base.to_vec();
o.push(extra);
o
};
assert_eq!(
refusal(&with(("colour", "red"))),
"unknown option colour (toy takes cell, detail, factor, size, halo, fallback)"
);
assert_eq!(
refusal(&[("detail", "plain"), ("factor", "2")]),
"cell is required: metres, above 0: the grid's cell"
);
assert_eq!(
refusal(&[("cell", "4"), ("detail", "plain")]),
"one of factor or size is required (factor: the enlargement; size: the larger side to reach)"
);
assert_eq!(
refusal(&with(("size", "64"))),
"only one of factor or size may be given"
);
assert_eq!(
refusal(&[("cell", "4"), ("detail", "fine"), ("factor", "2")]),
"detail=fine is none of plain or high"
);
assert_eq!(
refusal(&with(("fallback", "low"))),
"fallback=low is none of plain or high"
);
let none = PassSpec {
name: "bare",
options: &[],
make: |_| Ok(Box::new(Toy(Options::new()))),
};
assert_eq!(
none.check(&options(&[("x", "1")])).unwrap_err(),
"unknown option x (bare takes none)"
);
assert!(!none.requires_options());
}
#[test]
fn a_choice_is_read_as_its_enum() {
const READ: PassSpec = PassSpec {
name: "read",
options: &[OptionSpec::choice::<Detail>("detail", "which")],
make: |g| {
let detail = g.choice::<Detail>("detail");
Ok(Box::new(Toy(options(&[("detail", detail.text())]))))
},
};
let pass = READ.instance(&options(&[("detail", "high")])).unwrap();
assert_eq!(pass.options(), options(&[("detail", "high")]));
assert_eq!(Detail::High.text(), "high");
}
#[test]
fn a_one_of_choice_is_a_group_member_with_values() {
const SOURCE: PassSpec = PassSpec {
name: "source",
options: &[
OptionSpec::one_of_choice::<Detail>("source", "preset", "a preset detail"),
OptionSpec::one_of("source", "file", "d.txt", "a file stating the detail"),
],
make: |g| {
let printed = match (g.optional_choice::<Detail>("preset"), g.optional("file")) {
(Some(d), None) => options(&[("preset", d.text())]),
(None, Some(f)) => options(&[("file", f)]),
_ => unreachable!("the group gives one"),
};
Ok(Box::new(Toy(printed)))
},
};
SOURCE.validate();
assert_eq!(SOURCE.example(), options(&[("preset", "plain")]));
let pass = SOURCE.instance(&options(&[("preset", "high")])).unwrap();
assert_eq!(pass.options(), options(&[("preset", "high")]));
assert_eq!(
SOURCE.check(&options(&[("preset", "fine")])).unwrap_err(),
"preset=fine is none of plain or high"
);
assert_eq!(
SOURCE
.check(&options(&[("preset", "high"), ("file", "d.txt")]))
.unwrap_err(),
"only one of preset or file may be given"
);
assert!(
SOURCE
.describe()
.contains("\n preset (one of source; plain | high; e.g. plain): a preset detail"),
"{}",
SOURCE.describe()
);
}
#[test]
fn a_declaration_describes_its_options() {
let text = TOY.describe();
assert!(
text.starts_with("toy{cell*,detail*,(factor|size)*,halo,fallback}\n"),
"{text}"
);
assert!(
text.contains("\n detail (required; plain | high; e.g. plain): which collision is written"),
"{text}"
);
assert!(
text.contains("\n size (one of by; e.g. 64): the larger side to reach"),
"{text}"
);
assert!(
text.contains("\n halo (optional; e.g. 1): metres around each cell; without it: no halo"),
"{text}"
);
}
#[test]
#[should_panic(expected = "bad reads option other, which its declaration does not have")]
fn reading_an_undeclared_option_panics() {
const BAD: PassSpec = PassSpec {
name: "bad",
options: &[OptionSpec::optional("one", "1", "nothing", "nothing either")],
make: |g| {
let _ = g.optional("other");
Ok(Box::new(Toy(Options::new())))
},
};
let _ = BAD.instance(&Options::new());
}
#[test]
#[should_panic(expected = "pass silent: optional option halo says nothing of what its absence means")]
fn an_optional_option_states_its_absence() {
const SILENT: PassSpec = PassSpec {
name: "silent",
options: &[OptionSpec::optional("halo", "1", "metres around each cell", " ")],
make: |_| Ok(Box::new(Toy(Options::new()))),
};
SILENT.validate();
}
#[test]
#[should_panic(expected = "pass lonely: the one-of group by has one option")]
fn a_group_of_one_is_refused() {
const LONELY: PassSpec = PassSpec {
name: "lonely",
options: &[OptionSpec::one_of("by", "factor", "2", "the enlargement")],
make: |_| Ok(Box::new(Toy(Options::new()))),
};
LONELY.validate();
}
#[derive(Debug, PartialEq)]
struct Read {
name: Option<String>,
cell: f32,
detail: Detail,
}
impl TextForm for Read {
const NAME: &'static str = "read-toy";
const OPTIONS: &'static [OptionSpec] = &[
OptionSpec::optional("name", "a", "what is read", "the caller names it"),
OptionSpec::required("cell", "4", "metres, above 0: the grid's cell"),
OptionSpec::choice::<Detail>("detail", "which collision is read"),
];
fn make(given: &Given) -> Result<Self, String> {
let cell = given.required("cell");
Ok(Read {
name: given.optional("name").map(str::to_owned),
cell: cell
.parse()
.ok()
.filter(|c: &f32| *c > 0.)
.ok_or_else(|| format!("cell={cell} is not a number of metres above 0"))?,
detail: given.choice("detail"),
})
}
fn options(&self) -> Options {
let mut options = options(&[("cell", &self.cell.to_string()), ("detail", self.detail.text())]);
if let Some(name) = &self.name {
options.insert("name".into(), name.clone());
}
options
}
}
#[test]
fn a_text_form_reads_back_what_it_prints() {
use crate::pass::{parse_text_form, text_form, validate_text_form};
validate_text_form::<Read>();
for read in [
Read {
name: Some("m10".into()),
cell: 2.5,
detail: Detail::High,
},
Read {
name: None,
cell: 4.,
detail: Detail::Plain,
},
] {
let text = text_form(&read);
assert_eq!(parse_text_form::<Read>(&text).unwrap(), read, "{text}");
}
assert_eq!(
text_form(&Read {
name: Some("m10".into()),
cell: 2.5,
detail: Detail::High
}),
"read-toy{cell=2.5,detail=high,name=m10}"
);
let refusal = |text: &str| parse_text_form::<Read>(text).unwrap_err().to_string();
assert_eq!(
refusal("read-toy{detail=plain}"),
"read-toy: cell is required: metres, above 0: the grid's cell"
);
assert_eq!(
refusal("read-toy{cell=4,detail=fine}"),
"read-toy: detail=fine is none of plain or high"
);
assert_eq!(
refusal("read-toy{cell=4,detail=plain,colour=red}"),
"read-toy: unknown option colour (read-toy takes name, cell, detail)"
);
assert_eq!(
refusal("read-toy{cell=-1,detail=plain}"),
"read-toy: cell=-1 is not a number of metres above 0"
);
assert_eq!(refusal("toy{cell=4,detail=plain}"), "toy is not read-toy");
assert!(
refusal("read-toy{cell=4,detail=plain}, read-toy{cell=4,detail=plain}")
.contains("is not one read-toy")
);
assert!(refusal("").contains("is not one read-toy"));
}
}