1//! Rules as data, validated against a domain's vocabulary. 2//! 3//! The format is JSON, because every host already has a JSON parser and the 4//! error says where it stopped. A file is one object with a `rules` array, 5//! and the array's order is priority: 6//! 7//! ```json 8//! { "rules": [ 9//! { "name": "tests untouched", 10//! "when": [ { "fact": "touches code", "is": "yes" }, 11//! { "fact": "touches tests", "is": "no" } ], 12//! "then": { "note": "ask for tests" } }, 13//! { "name": "size known", 14//! "when": [ { "known": "lines changed" } ], 15//! "then": { "do": "review" } } 16//! ] } 17//! ``` 18//! 19//! A test is `{"fact": F, "is": V}` (the fact has that value) or `{"known": 20//! F}` (the fact is known, whatever it is). What a rule does is exactly one of 21//! `{"do": E}`, `{"end": E}` and `{"note": N}`. Every name is the domain's 22//! ([`Domain::fact_name`] and the others). A field that is not in this 23//! description is an error, so a misspelling is not silently ignored. 24 25use std::fmt; 26 27use serde::{Deserialize, Serialize}; 28 29use crate::{Domain, Rule, Test, Then}; 30 31/// 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} 42 43/// 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} 54 55/// A rule as it is written. 56#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 57#[serde(deny_unknown_fields)] 58pub struct Spec { 59 pub name: String, 60 pub when: Vec<TestSpec>, 61 pub then: ThenSpec, 62} 63 64#[derive(Deserialize)] 65#[serde(deny_unknown_fields)] 66struct File { 67 rules: Vec<Spec>, 68} 69 70/// A rule that passed validation. It owns what [`Rule`] borrows. 71#[derive(Clone, Debug)] 72pub struct LoadedRule<D: Domain> { 73 pub name: String, 74 pub when: Vec<Test<D>>, 75 pub then: Then<D>, 76} 77 78impl<D: Domain> LoadedRule<D> { 79 pub fn rule(&self) -> Rule<'_, D> { 80 Rule { name: &self.name, when: &self.when, then: self.then } 81 } 82} 83 84/// What is wrong with one rule. 85#[derive(Clone, Debug, PartialEq, Eq)] 86pub enum Problem { 87 EmptyName, 88 /// The name is another rule's, in this file or among those already in use. 89 DuplicateName, 90 /// A rule with no tests would hold always. 91 NoTests, 92 /// A test sets neither or both of `is` and `known`, or sets `is` without `fact`. 93 MalformedTest { test: usize }, 94 /// `then` is not exactly one of `do`, `end` and `note`. 95 MalformedThen, 96 UnknownFact { test: usize, fact: String }, 97 /// 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> }, 99 /// The domain does not allow this effect, end or note, and says why. 100 Refused { kind: &'static str, name: String, reason: String }, 101} 102 103/// A rule that cannot be loaded, or a file that cannot be read. 104#[derive(Clone, Debug, PartialEq, Eq)] 105pub enum LoadError { 106 /// The text is not the format: the parser's message, with where. 107 Syntax(String), 108 /// Rule number `rule` (from 0, in the file's order) has this problem. 109 Rule { rule: usize, name: String, problem: Problem }, 110} 111 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} 172 173/// Validates rules against the domain's vocabulary. Every problem in every 174/// rule is returned, not the first. `taken` is the names of rules already in 175/// 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} 207 208/// Reads a rules file (see the module's description) and validates it. 209pub fn load<D: Domain>(json: &str, taken: &[&str]) -> Result<Vec<LoadedRule<D>>, Vec<LoadError>> { 210 let file: File = serde_json::from_str(json).map_err(|error| vec![LoadError::Syntax(error.to_string())])?; 211 load_specs(&file.rules, taken) 212} 213 214/// Rules written in code, as they would be written as data: what a host 215/// 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}