use crate::Error;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Rubric {
what: String,
examples: Vec<String>,
counterexamples: Vec<String>,
}
impl Rubric {
pub fn new(what: impl Into<String>) -> Self {
Self {
what: what.into(),
examples: Vec::new(),
counterexamples: Vec::new(),
}
}
pub fn example(mut self, example: impl Into<String>) -> Self {
self.examples.push(example.into());
self
}
pub fn counterexample(mut self, counterexample: impl Into<String>) -> Self {
self.counterexamples.push(counterexample.into());
self
}
pub fn render(&self) -> Result<String, Error> {
if self.what.trim().is_empty()
&& !(self.examples.is_empty() && self.counterexamples.is_empty())
{
return Err(Error::Config {
detail: "examples need a non-empty rubric to attach to".into(),
});
}
check_clause(&self.examples, "example")?;
check_clause(&self.counterexamples, "counterexample")?;
for example in &self.examples {
if self.counterexamples.contains(example) {
return Err(Error::Config {
detail: format!(
"{example:?} is both an example and a counterexample; it cannot be in and out of the same option"
),
});
}
}
let mut out = self.what.clone();
if !self.examples.is_empty() {
out.push_str("\nExamples: ");
out.push_str(&self.examples.join("; "));
}
if !self.counterexamples.is_empty() {
out.push_str("\nNot this option: ");
out.push_str(&self.counterexamples.join("; "));
}
Ok(out)
}
}
pub(crate) fn render_pair(yes: &Rubric, no: &Rubric) -> Result<(String, String), Error> {
for example in &yes.examples {
if no.examples.contains(example) {
return Err(Error::Config {
detail: format!(
"{example:?} is an example of both yes and no; an input belongs to one option"
),
});
}
}
Ok((yes.render()?, no.render()?))
}
fn check_clause(items: &[String], kind: &str) -> Result<(), Error> {
for (i, item) in items.iter().enumerate() {
if item.trim().is_empty() {
return Err(Error::Config {
detail: format!("an {kind} must not be empty"),
});
}
if item.contains(['\n', '\r']) {
return Err(Error::Config {
detail: format!(
"an {kind} may not contain a line break (U+000A or U+000D): {item:?}"
),
});
}
if items[..i].contains(item) {
return Err(Error::Config {
detail: format!("duplicate {kind} {item:?}"),
});
}
}
Ok(())
}
mod sealed {
pub trait Sealed {}
}
pub trait IntoRubric: sealed::Sealed {
fn into_rubric(self) -> Rubric;
}
impl<T: Into<String>> sealed::Sealed for T {}
impl<T: Into<String>> IntoRubric for T {
fn into_rubric(self) -> Rubric {
Rubric::new(self)
}
}
impl sealed::Sealed for Rubric {}
impl IntoRubric for Rubric {
fn into_rubric(self) -> Rubric {
self
}
}