load.rsannotatedload.rssource240 lines · 9.9 KB · raw

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;
27use serde::{Deserialize, Serialize};
28
29use crate::{Domain, Rule, Test, Then};

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.

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}
64#[derive(Deserialize)]
65#[serde(deny_unknown_fields)]
66struct File {
67    rules: Vec<Spec>,
68}

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}
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}

What is wrong with one rule.

85#[derive(Clone, Debug, PartialEq, Eq)]
86pub enum Problem {
87    EmptyName,

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.

95    MalformedThen,
96    UnknownFact { test: usize, fact: String },

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.

100    Refused { kind: &'static str, name: String, reason: String },
101}

A rule that cannot be loaded, or a file that cannot be read.

104#[derive(Clone, Debug, PartialEq, Eq)]
105pub enum LoadError {

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.

109    Rule { rule: usize, name: String, problem: Problem },
110}
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.

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}

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}