Rules as data, validated against a domain's vocabulary.
The format is JSON, because every host already has a JSON parser and the
error says where it stopped. A file is one object with a rules array,
and the array's order is priority:
{ "rules": [
{ "name": "tests untouched",
"when": [ { "fact": "touches code", "is": "yes" },
{ "fact": "touches tests", "is": "no" } ],
"then": { "note": "ask for tests" } },
{ "name": "size known",
"when": [ { "known": "lines changed" } ],
"then": { "do": "review" } }
] }
A test is {"fact": F, "is": V} (the fact has that value) or {"known": F} (the fact is known, whatever it is). What a rule does is exactly one of
{"do": E}, {"end": E} and {"note": N}. Every name is the domain's
([Domain::fact_name] and the others). A field that is not in this
description is an error, so a misspelling is not silently ignored.
25use std::fmt;
A test as it is written.
32#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] 33#[serde(deny_unknown_fields)] 34pub struct TestSpec { 35 #[serde(default, skip_serializing_if = "Option::is_none")] 36 pub fact: Option<String>, 37 #[serde(default, skip_serializing_if = "Option::is_none")] 38 pub is: Option<String>, 39 #[serde(default, skip_serializing_if = "Option::is_none")] 40 pub known: Option<String>, 41}
What a rule does, as it is written: exactly one field is set.
44#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] 45#[serde(deny_unknown_fields)] 46pub struct ThenSpec { 47 #[serde(default, rename = "do", skip_serializing_if = "Option::is_none")] 48 pub effect: Option<String>, 49 #[serde(default, skip_serializing_if = "Option::is_none")] 50 pub end: Option<String>, 51 #[serde(default, skip_serializing_if = "Option::is_none")] 52 pub note: Option<String>, 53}
A rule as it is written.
A rule that passed validation. It owns what [Rule] borrows.
What is wrong with one rule.
The name is another rule's, in this file or among those already in use.
89 DuplicateName,
A rule with no tests would hold always.
91 NoTests,
A test sets neither or both of is and known, or sets is without fact.
93 MalformedTest { test: usize },
then is not exactly one of do, end and note.
The value is not one the fact can have: allowed is what it can.
98 WrongValue { test: usize, fact: String, value: String, allowed: Vec<String> },
The domain does not allow this effect, end or note, and says why.
A rule that cannot be loaded, or a file that cannot be read.
The text is not the format: the parser's message, with where.
107 Syntax(String),
Rule number rule (from 0, in the file's order) has this problem.
112impl fmt::Display for LoadError { 113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 114 match self { 115 LoadError::Syntax(message) => write!(f, "not a rules file: {message}"), 116 LoadError::Rule { rule, name, problem } => { 117 write!(f, "rule {rule} ({name:?}): ")?; 118 match problem { 119 Problem::EmptyName => write!(f, "it has no name"), 120 Problem::DuplicateName => write!(f, "another rule has this name"), 121 Problem::NoTests => write!(f, "it has no tests, so it would hold always"), 122 Problem::MalformedTest { test } => { 123 write!(f, "test {test} must be {{\"fact\", \"is\"}} or {{\"known\"}}") 124 } 125 Problem::MalformedThen => write!(f, "`then` must have exactly one of `do`, `end` and `note`"), 126 Problem::UnknownFact { test, fact } => write!(f, "test {test}: there is no fact {fact:?}"), 127 Problem::WrongValue { test, fact, value, allowed } => { 128 write!(f, "test {test}: {fact:?} cannot be {value:?}, only one of {allowed:?}") 129 } 130 Problem::Refused { kind, name, reason } => write!(f, "{kind} {name:?} is refused: {reason}"), 131 } 132 } 133 } 134 } 135} 136 137impl std::error::Error for LoadError {} 138 139fn fact<D: Domain>(name: &str) -> Option<D::Fact> { 140 D::facts().iter().copied().find(|fact| D::fact_name(*fact) == name) 141} 142 143fn test<D: Domain>(index: usize, spec: &TestSpec) -> Result<Test<D>, Problem> { 144 let unknown = |fact: &str| Problem::UnknownFact { test: index, fact: fact.to_owned() }; 145 match (&spec.fact, &spec.is, &spec.known) { 146 (Some(name), Some(value), None) => { 147 let fact = fact::<D>(name).ok_or_else(|| unknown(name))?; 148 let value = D::values(fact).iter().copied().find(|v| D::value_name(*v) == value).ok_or_else(|| { 149 Problem::WrongValue { 150 test: index, 151 fact: name.clone(), 152 value: value.clone(), 153 allowed: D::values(fact).iter().map(|v| D::value_name(*v).to_owned()).collect(), 154 } 155 })?; 156 Ok(Test::Is(fact, value)) 157 } 158 (None, None, Some(name)) => Ok(Test::Known(fact::<D>(name).ok_or_else(|| unknown(name))?)), 159 _ => Err(Problem::MalformedTest { test: index }), 160 } 161} 162 163fn then<D: Domain>(spec: &ThenSpec) -> Result<Then<D>, Problem> { 164 let refused = |kind, name: &String, reason| Problem::Refused { kind, name: name.clone(), reason }; 165 match (&spec.effect, &spec.end, &spec.note) { 166 (Some(name), None, None) => D::parse_effect(name).map(Then::Do).map_err(|reason| refused("effect", name, reason)), 167 (None, Some(name), None) => D::parse_end(name).map(Then::End).map_err(|reason| refused("end", name, reason)), 168 (None, None, Some(name)) => D::parse_note(name).map(Then::Note).map_err(|reason| refused("note", name, reason)), 169 _ => Err(Problem::MalformedThen), 170 } 171}
Validates rules against the domain's vocabulary. Every problem in every
rule is returned, not the first. taken is the names of rules already in
use, such as the ones compiled in, which a loaded rule may not repeat.
176pub fn load_specs<D: Domain>(specs: &[Spec], taken: &[&str]) -> Result<Vec<LoadedRule<D>>, Vec<LoadError>> { 177 let mut rules = Vec::new(); 178 let mut errors = Vec::new(); 179 for (index, spec) in specs.iter().enumerate() { 180 let mut problems = Vec::new(); 181 if spec.name.trim().is_empty() { 182 problems.push(Problem::EmptyName); 183 } else if taken.contains(&spec.name.as_str()) || specs[..index].iter().any(|before| before.name == spec.name) { 184 problems.push(Problem::DuplicateName); 185 } 186 if spec.when.is_empty() { 187 problems.push(Problem::NoTests); 188 } 189 let mut when = Vec::new(); 190 for (at, written) in spec.when.iter().enumerate() { 191 match test::<D>(at, written) { 192 Ok(test) => when.push(test), 193 Err(problem) => problems.push(problem), 194 } 195 } 196 let then = then::<D>(&spec.then); 197 if let Err(problem) = &then { 198 problems.push(problem.clone()); 199 } 200 errors.extend(problems.into_iter().map(|problem| LoadError::Rule { rule: index, name: spec.name.clone(), problem })); 201 if let (Ok(then), true) = (then, errors.is_empty()) { 202 rules.push(LoadedRule { name: spec.name.clone(), when, then }); 203 } 204 } 205 if errors.is_empty() { Ok(rules) } else { Err(errors) } 206}
Reads a rules file (see the module's description) and validates it.
Rules written in code, as they would be written as data: what a host shows a user to start from, and what the tests hold the loader to.
216pub fn export<D: Domain>(rules: &[Rule<'_, D>]) -> Vec<Spec> { 217 rules 218 .iter() 219 .map(|rule| Spec { 220 name: rule.name.to_owned(), 221 when: rule 222 .when 223 .iter() 224 .map(|test| match *test { 225 Test::Is(fact, value) => TestSpec { 226 fact: Some(D::fact_name(fact).to_owned()), 227 is: Some(D::value_name(value).to_owned()), 228 known: None, 229 }, 230 Test::Known(fact) => TestSpec { known: Some(D::fact_name(fact).to_owned()), ..TestSpec::default() }, 231 }) 232 .collect(), 233 then: match rule.then { 234 Then::Do(effect) => ThenSpec { effect: Some(D::effect_name(effect)), ..ThenSpec::default() }, 235 Then::End(end) => ThenSpec { end: Some(D::end_name(end)), ..ThenSpec::default() }, 236 Then::Note(note) => ThenSpec { note: Some(D::note_name(note)), ..ThenSpec::default() }, 237 }, 238 }) 239 .collect() 240}