rustcrates.git / rete / src / load.rs
load.rsannotatedload.rssource240 lines · 9.9 KB · raw
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}