Skip to main content

ravenrs_app/
validation.rs

1//! What a record has to meet before it is kept.
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::rc::Rc;
6
7use ravenrs_app_i18n::{Language, Texts};
8use ravenrs_criteria::{Criteria, Environment, Scope, Value};
9
10use crate::{EntityInfo, ModelError, Object, ObjectSpace, Query};
11
12/// The context of keeping a record. Rules are in it unless they say otherwise.
13pub const CONTEXT_SAVE: &str = "Save";
14/// The context of taking a record away.
15pub const CONTEXT_DELETE: &str = "Delete";
16
17/// How a [`RuleKind::Text`] compares.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19#[non_exhaustive]
20#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
21pub enum TextComparison {
22    /// The field holds exactly the text.
23    Equals,
24    /// The field holds something else.
25    NotEquals,
26    /// The text is somewhere in the field.
27    Contains,
28    /// The field starts with the text.
29    StartsWith,
30    /// The field ends with the text.
31    EndsWith,
32}
33
34/// How a [`RuleKind::Compare`] compares.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36#[non_exhaustive]
37#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
38pub enum Comparison {
39    /// The same.
40    Equal,
41    /// Not the same.
42    NotEqual,
43    /// Below.
44    Less,
45    /// Below or the same.
46    LessOrEqual,
47    /// Above.
48    Greater,
49    /// Above or the same.
50    GreaterOrEqual,
51}
52
53/// What a [`RuleKind::Compare`] compares with.
54#[derive(Clone, Debug, PartialEq)]
55#[non_exhaustive]
56#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
57pub enum Operand {
58    /// This value.
59    Value(Value),
60    /// What this other field holds.
61    Field(String),
62}
63
64/// How much not meeting a rule matters.
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
66#[non_exhaustive]
67#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
68pub enum Severity {
69    /// It stops what was being done until it is fixed.
70    #[default]
71    Error,
72    /// People are told, and can go on anyway.
73    Warning,
74    /// It goes on, with a note on the field.
75    Information,
76}
77
78#[cfg_attr(feature = "live", ravenrs_live::expose)]
79impl Severity {
80    fn from_name(name: &str) -> Option<Self> {
81        match name {
82            "Error" => Some(Self::Error),
83            "Warning" => Some(Self::Warning),
84            "Information" => Some(Self::Information),
85            _ => None,
86        }
87    }
88}
89
90type CustomRule = Rc<dyn Fn(&Object, &dyn ObjectSpace) -> Result<(), String>>;
91
92/// What a [`Rule`] asks for.
93#[derive(Clone)]
94#[non_exhaustive]
95#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
96pub enum RuleKind {
97    /// The field has something in it.
98    Required,
99    /// The field is within these limits.
100    Range {
101        /// The lowest it can be.
102        min: Option<f64>,
103        /// The highest it can be.
104        max: Option<f64>,
105    },
106    /// The field matches this regular expression.
107    Matches(String),
108    /// The field compares with this text.
109    Text {
110        /// How.
111        comparison: TextComparison,
112        /// With what.
113        text: String,
114        /// Whether case counts for nothing.
115        ignore_case: bool,
116    },
117    /// The field compares with a value or another field.
118    Compare {
119        /// How.
120        comparison: Comparison,
121        /// With what.
122        with: Operand,
123    },
124    /// The record meets this expression.
125    Criteria(String),
126    /// The field is true, as with an agreement.
127    IsTrue,
128    /// No other record holds the same in the field.
129    Unique,
130    /// No other record holds the same in all these fields together.
131    UniqueTogether(Vec<String>),
132    /// Some record of this entity meets this expression.
133    Exists {
134        /// The entity.
135        entity: String,
136        /// The expression, or none for any record at all.
137        criteria: Option<String>,
138    },
139    /// Some record of this entity points to this one through this field.
140    Referenced {
141        /// The entity.
142        entity: String,
143        /// Its field that holds this record's key.
144        field: String,
145    },
146    /// Anything else.
147    Custom(CustomRule),
148}
149
150impl fmt::Debug for RuleKind {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.write_str(match self {
153            Self::Required => "Required",
154            Self::Range { .. } => "Range",
155            Self::Matches(_) => "Matches",
156            Self::Text { .. } => "Text",
157            Self::Compare { .. } => "Compare",
158            Self::Criteria(_) => "Criteria",
159            Self::IsTrue => "IsTrue",
160            Self::Unique => "Unique",
161            Self::UniqueTogether(_) => "UniqueTogether",
162            Self::Exists { .. } => "Exists",
163            Self::Referenced { .. } => "Referenced",
164            Self::Custom(_) => "Custom",
165        })
166    }
167}
168
169/// One thing a record has to meet, in some contexts, and perhaps only when it fits.
170#[derive(Clone, Debug)]
171#[non_exhaustive]
172#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
173pub struct Rule {
174    /// What it is known by.
175    pub id: String,
176    /// The field it is about, where the error shows.
177    pub field: Option<String>,
178    /// What it asks for.
179    pub kind: RuleKind,
180    /// When it applies.
181    pub contexts: Vec<String>,
182    /// It applies only to records that meet this.
183    pub target_criteria: Option<String>,
184    /// What to say, instead of the usual words.
185    pub message: Option<String>,
186    /// Whether an empty field passes, as it does for all but [`RuleKind::Required`].
187    pub skip_empty: bool,
188    /// How much not meeting it matters.
189    pub severity: Severity,
190}
191
192impl PartialEq for Rule {
193    fn eq(&self, other: &Self) -> bool {
194        self.id == other.id
195    }
196}
197
198#[cfg_attr(feature = "live", ravenrs_live::expose)]
199impl Rule {
200    /// A rule set in the model, under an entity's `Rules`: `Kind` is `Required`, `Range`, `Matches`,
201    /// `Criteria` or `Unique`, with `Field`, `Min`, `Max`, `Pattern`, `Expression`, `Message`,
202    /// `Target` and `Contexts`, separated by commas, as each asks for.
203    pub(crate) fn from_model(node: &crate::ModelNode) -> Result<Self, crate::ModelError> {
204        let wrong = |what: &str| crate::ModelError(format!("the rule {} {what}", node.id));
205        let text = |name: &str| node.text(name).map(str::to_owned);
206        let field = || text("Field").ok_or_else(|| wrong("says nothing about its field"));
207        let kind = node.text("Kind").ok_or_else(|| wrong("says nothing about its kind"))?;
208        let mut rule = match kind {
209            "Required" => Self::on(field()?, RuleKind::Required),
210            "Unique" => Self::on(field()?, RuleKind::Unique),
211            "Range" => {
212                Self::on(field()?, RuleKind::Range { min: node.number("Min"), max: node.number("Max") })
213            }
214            "Matches" => {
215                let pattern = text("Pattern").ok_or_else(|| wrong("says nothing about its pattern"))?;
216                Self::on(field()?, RuleKind::Matches(pattern))
217            }
218            "Criteria" => {
219                let expression = text("Expression").ok_or_else(|| wrong("has no expression"))?;
220                Self::new(node.id.clone(), RuleKind::Criteria(expression))
221            }
222            other => return Err(wrong(&format!("is of an unknown kind: {other}"))),
223        };
224        rule.id = node.id.clone();
225        rule.message = text("Message");
226        rule.target_criteria = text("Target");
227        if let Some(name) = node.text("Severity") {
228            rule.severity = Severity::from_name(name)
229                .ok_or_else(|| wrong(&format!("has an unknown severity: {name}")))?;
230        }
231        if let Some(contexts) = node.text("Contexts") {
232            rule.contexts = contexts.split(',').map(|context| context.trim().to_owned()).collect();
233        }
234        Ok(rule)
235    }
236
237    /// A rule about the whole record.
238    pub fn new(id: impl Into<String>, kind: RuleKind) -> Self {
239        Self {
240            id: id.into(),
241            field: None,
242            kind,
243            contexts: vec![CONTEXT_SAVE.to_owned()],
244            target_criteria: None,
245            message: None,
246            skip_empty: true,
247            severity: Severity::Error,
248        }
249    }
250
251    /// A rule about a field.
252    pub fn on(field: impl Into<String>, kind: RuleKind) -> Self {
253        let field = field.into();
254        let skip_empty = !matches!(kind, RuleKind::Required | RuleKind::IsTrue);
255        Self { skip_empty, ..Self::new(format!("{field}.{kind:?}"), kind) }.about(field)
256    }
257
258    fn about(mut self, field: String) -> Self {
259        self.field = Some(field);
260        self
261    }
262
263    /// The field has to have something in it.
264    pub fn required(field: impl Into<String>) -> Self {
265        Self::on(field, RuleKind::Required)
266    }
267
268    /// No other record can hold the same in the field.
269    pub fn unique(field: impl Into<String>) -> Self {
270        Self::on(field, RuleKind::Unique)
271    }
272
273    /// The field has to be within these limits.
274    pub fn range(field: impl Into<String>, min: Option<f64>, max: Option<f64>) -> Self {
275        Self::on(field, RuleKind::Range { min, max })
276    }
277
278    /// The field has to match this regular expression.
279    pub fn matches(field: impl Into<String>, pattern: impl Into<String>) -> Self {
280        Self::on(field, RuleKind::Matches(pattern.into()))
281    }
282
283    /// The record has to meet this expression.
284    pub fn criteria(id: impl Into<String>, expression: impl Into<String>) -> Self {
285        Self::new(id, RuleKind::Criteria(expression.into()))
286    }
287
288    /// Applies in these contexts, instead of on saving.
289    pub fn contexts(mut self, contexts: impl IntoIterator<Item = impl Into<String>>) -> Self {
290        self.contexts = contexts.into_iter().map(Into::into).collect();
291        self
292    }
293
294    /// Applies only to records that meet this.
295    pub fn target_criteria(mut self, expression: impl Into<String>) -> Self {
296        self.target_criteria = Some(expression.into());
297        self
298    }
299
300    /// Says this when it fails. `{field}` is what a field of the record holds, as it shows, and
301    /// `{reference.field}` the same of the record a reference points to. `{$field}` is what the
302    /// rule's field is called, `{$entity}` the entity, `{$min}` and `{$max}` the limits.
303    pub fn message(mut self, message: impl Into<String>) -> Self {
304        self.message = Some(message.into());
305        self
306    }
307
308    /// How much not meeting it matters.
309    pub fn severity(mut self, severity: Severity) -> Self {
310        self.severity = severity;
311        self
312    }
313
314    /// People are told, and can go on anyway.
315    pub fn warning(self) -> Self {
316        self.severity(Severity::Warning)
317    }
318
319    /// It goes on, with a note on the field.
320    pub fn information(self) -> Self {
321        self.severity(Severity::Information)
322    }
323
324    /// Whether finding out takes reading other records.
325    fn reads_records(&self) -> bool {
326        matches!(
327            self.kind,
328            RuleKind::Unique
329                | RuleKind::UniqueTogether(_)
330                | RuleKind::Exists { .. }
331                | RuleKind::Referenced { .. }
332                | RuleKind::Custom(_)
333        )
334    }
335
336    /// Whether an empty field passes.
337    pub fn skip_empty(mut self, skip: bool) -> Self {
338        self.skip_empty = skip;
339        self
340    }
341
342    /// Says what is wrong with how it was declared.
343    pub(crate) fn check(&self, entity: &EntityInfo) -> Result<(), ModelError> {
344        let wrong = |what: String| Err(ModelError(format!("{}: the rule {}: {what}", entity.name, self.id)));
345        if let Some(field) = &self.field
346            && entity.field_named(field).is_none()
347        {
348            return wrong(format!("there is no field {field}"));
349        }
350        let parses = |text: &str| {
351            Criteria::parse(text)
352                .map(|_| ())
353                .map_err(|e| ModelError(format!("{}: the rule {}: {e}", entity.name, self.id)))
354        };
355        if let Some(text) = &self.target_criteria {
356            parses(text)?;
357        }
358        match &self.kind {
359            RuleKind::Criteria(text) => parses(text)?,
360            RuleKind::Exists { criteria: Some(text), .. } => parses(text)?,
361            RuleKind::Matches(pattern) => {
362                if let Err(e) = regex_lite::Regex::new(pattern) {
363                    return wrong(e.to_string());
364                }
365            }
366            RuleKind::UniqueTogether(fields) => {
367                if let Some(missing) = fields.iter().find(|f| entity.field_named(f).is_none()) {
368                    return wrong(format!("there is no field {missing}"));
369                }
370            }
371            RuleKind::Compare { with: Operand::Field(other), .. } if entity.field_named(other).is_none() => {
372                return wrong(format!("there is no field {other}"));
373            }
374            _ => {}
375        }
376        Ok(())
377    }
378}
379
380/// The words of what fails, where `{0}` is the name of the field.
381#[derive(Clone, Debug)]
382#[non_exhaustive]
383#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
384pub struct RuleWords {
385    /// [`RuleKind::Required`].
386    pub required: String,
387    /// [`RuleKind::Range`], below the limit, where `{1}` is the limit.
388    pub too_low: String,
389    /// [`RuleKind::Range`], above the limit, where `{1}` is the limit.
390    pub too_high: String,
391    /// [`RuleKind::Matches`] and [`RuleKind::Text`].
392    pub invalid: String,
393    /// [`RuleKind::Compare`].
394    pub comparison: String,
395    /// [`RuleKind::Criteria`] and [`RuleKind::Custom`], with no field.
396    pub record: String,
397    /// [`RuleKind::IsTrue`].
398    pub must_be_true: String,
399    /// [`RuleKind::Unique`] and [`RuleKind::UniqueTogether`].
400    pub duplicate: String,
401    /// [`RuleKind::Exists`].
402    pub none_exists: String,
403    /// [`RuleKind::Referenced`].
404    pub not_referenced: String,
405    /// Before decimals, where a message shows money.
406    pub decimal: char,
407}
408
409impl Default for RuleWords {
410    fn default() -> Self {
411        Self::from_texts(&Texts::new(Language::English))
412    }
413}
414
415#[cfg_attr(feature = "live", ravenrs_live::expose)]
416impl RuleWords {
417    /// The texts of a catalog, in its language. What it does not have comes from English.
418    pub fn from_texts(texts: &Texts) -> Self {
419        Self {
420            required: texts.get("rules.required"),
421            too_low: texts.get("rules.too_low"),
422            too_high: texts.get("rules.too_high"),
423            invalid: texts.get("rules.invalid"),
424            comparison: texts.get("rules.comparison"),
425            record: texts.get("rules.record"),
426            must_be_true: texts.get("rules.must_be_true"),
427            duplicate: texts.get("rules.duplicate"),
428            none_exists: texts.get("rules.none_exists"),
429            not_referenced: texts.get("rules.not_referenced"),
430            decimal: texts.char("format.decimal", '.'),
431        }
432    }
433
434    /// In Brazilian Portuguese.
435    pub fn pt_br() -> Self {
436        Self::from_texts(&Texts::new(Language::BrazilianPortuguese))
437    }
438}
439
440/// A rule that was not met.
441#[derive(Clone, Debug, PartialEq, Eq)]
442#[non_exhaustive]
443#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
444pub struct Violation {
445    /// The rule.
446    pub rule: String,
447    /// The field it is about.
448    pub field: Option<String>,
449    /// In words for people.
450    pub message: String,
451    /// How much it matters.
452    pub severity: Severity,
453}
454
455#[cfg_attr(feature = "live", ravenrs_live::expose)]
456impl Violation {
457    /// Whether it stops what was being done.
458    pub fn is_error(&self) -> bool {
459        self.severity == Severity::Error
460    }
461}
462
463fn is_empty(value: &Value) -> bool {
464    match value {
465        Value::Null => true,
466        Value::Text(text) => text.trim().is_empty(),
467        _ => false,
468    }
469}
470
471fn text_of(value: &Value) -> String {
472    match value {
473        Value::Text(text) => text.clone(),
474        Value::Number(number) if number.fract() == 0.0 => format!("{}", *number as i64),
475        Value::Number(number) => number.to_string(),
476        Value::Bool(on) => on.to_string(),
477        Value::Null => String::new(),
478        other => format!("{other:?}"),
479    }
480}
481
482fn same(a: &Value, b: &Value) -> bool {
483    text_of(a) == text_of(b)
484}
485
486fn compares(value: &Value, comparison: Comparison, other: &Value) -> bool {
487    let order = value.compare(other);
488    match comparison {
489        Comparison::Equal => order == Ordering::Equal,
490        Comparison::NotEqual => order != Ordering::Equal,
491        Comparison::Less => order == Ordering::Less,
492        Comparison::LessOrEqual => order != Ordering::Greater,
493        Comparison::Greater => order == Ordering::Greater,
494        Comparison::GreaterOrEqual => order != Ordering::Less,
495    }
496}
497
498fn meets(object: &Object, expression: &str, environment: &Environment) -> bool {
499    Criteria::parse(expression).is_ok_and(|parsed| Scope::new(environment).row(object).matches(&parsed))
500}
501
502/// Whether another record holds the same in all these fields. Only records storage already has
503/// or this space is changing can be found, so this needs a space that reads.
504fn duplicated(entity: &EntityInfo, object: &Object, fields: &[&str], space: &dyn ObjectSpace) -> bool {
505    let key = entity.key_field().map(|f| f.name.as_str()).unwrap_or_default();
506    let found = space.query(&entity.name, &Query::all()).unwrap_or_default();
507    found.iter().any(|other| {
508        !same(&other.get(key), &object.get(key))
509            && fields.iter().all(|field| same(&other.get(field), &object.get(field)))
510    })
511}
512
513/// What a value is, as a message says it.
514fn shown(field: Option<&crate::FieldInfo>, value: &Value, words: &RuleWords) -> String {
515    match (field.map(|field| &field.kind), value) {
516        (Some(crate::FieldKind::Money), Value::Number(cents)) => {
517            format!("{:.2}", cents / 100.0).replace('.', &words.decimal.to_string())
518        }
519        (Some(crate::FieldKind::Enum(options)), Value::Text(code)) => options
520            .iter()
521            .find(|option| option.code == *code)
522            .map_or_else(|| code.clone(), |option| option.caption.clone()),
523        (_, Value::Date(moment)) => moment.to_string(),
524        _ => text_of(value),
525    }
526}
527
528/// What `{name}` in a message stands for.
529fn message_part(
530    name: &str,
531    rule: &Rule,
532    entity: &EntityInfo,
533    object: &Object,
534    space: &dyn ObjectSpace,
535    words: &RuleWords,
536) -> Option<String> {
537    let number = |limit: Option<f64>| limit.map_or_else(String::new, |n| text_of(&Value::Number(n)));
538    match name {
539        "$entity" => return Some(entity.caption.clone()),
540        "$field" => {
541            let field = rule.field.as_deref().and_then(|field| entity.field_named(field));
542            return Some(field.map_or_else(String::new, |field| field.caption.clone()));
543        }
544        "$min" | "$max" => {
545            let RuleKind::Range { min, max } = &rule.kind else { return Some(String::new()) };
546            return Some(number(if name == "$min" { *min } else { *max }));
547        }
548        _ => {}
549    }
550    if let Some((reference, rest)) = name.split_once('.') {
551        let field = entity.field_named(reference)?;
552        let crate::FieldKind::Reference(target) = &field.kind else { return None };
553        let other = space.entities().into_iter().find(|e| e.name == *target)?;
554        let record = space.find(target, &object.get(reference)).ok().flatten();
555        let Some(record) = record else { return Some(String::new()) };
556        return message_part(rest, rule, &other, &record, space, words);
557    }
558    let field = entity.field_named(name)?;
559    Some(shown(Some(field), &object.get(name), words))
560}
561
562/// A rule's message with what it refers to filled in. What it does not know stays as written.
563fn filled_message(
564    text: &str,
565    rule: &Rule,
566    entity: &EntityInfo,
567    object: &Object,
568    space: &dyn ObjectSpace,
569    words: &RuleWords,
570) -> String {
571    let mut filled = String::with_capacity(text.len());
572    let mut rest = text;
573    while let Some(start) = rest.find('{') {
574        filled.push_str(&rest[..start]);
575        let Some(end) = rest[start..].find('}') else {
576            rest = &rest[start..];
577            break;
578        };
579        let inside = &rest[start + 1..start + end];
580        match message_part(inside, rule, entity, object, space, words) {
581            Some(part) => filled.push_str(&part),
582            None => filled.push_str(&rest[start..=start + end]),
583        }
584        rest = &rest[start + end + 1..];
585    }
586    filled.push_str(rest);
587    filled
588}
589
590/// What a rule says, or nothing if it is met.
591fn broken(
592    rule: &Rule,
593    entity: &EntityInfo,
594    object: &Object,
595    space: &dyn ObjectSpace,
596    words: &RuleWords,
597) -> Option<String> {
598    let environment = crate::environment();
599    let caption = rule
600        .field
601        .as_deref()
602        .and_then(|name| entity.field_named(name))
603        .map_or_else(String::new, |field| field.caption.clone());
604    let say = |text: &str, limit: Option<f64>| {
605        let limit = limit.map_or_else(String::new, |n| text_of(&Value::Number(n)));
606        text.replace("{0}", &caption).replace("{1}", &limit)
607    };
608    let value = rule.field.as_deref().map_or(Value::Null, |name| object.get(name));
609    if rule.field.is_some() && rule.skip_empty && is_empty(&value) {
610        return None;
611    }
612    let failed = match &rule.kind {
613        RuleKind::Required => is_empty(&value).then(|| say(&words.required, None)),
614        RuleKind::Range { min, max } => {
615            let number = value.as_number()?;
616            if min.is_some_and(|min| number < min) {
617                Some(say(&words.too_low, *min))
618            } else if max.is_some_and(|max| number > max) {
619                Some(say(&words.too_high, *max))
620            } else {
621                None
622            }
623        }
624        RuleKind::Matches(pattern) => {
625            let fits = regex_lite::Regex::new(pattern).is_ok_and(|regex| regex.is_match(&text_of(&value)));
626            (!fits).then(|| say(&words.invalid, None))
627        }
628        RuleKind::Text { comparison, text, ignore_case } => {
629            let fold = |s: &str| if *ignore_case { s.to_lowercase() } else { s.to_owned() };
630            let (held, wanted) = (fold(&text_of(&value)), fold(text));
631            let fits = match comparison {
632                TextComparison::NotEquals => held != wanted,
633                TextComparison::Contains => held.contains(&wanted),
634                TextComparison::StartsWith => held.starts_with(&wanted),
635                TextComparison::EndsWith => held.ends_with(&wanted),
636                _ => held == wanted,
637            };
638            (!fits).then(|| say(&words.invalid, None))
639        }
640        RuleKind::Compare { comparison, with } => {
641            let other = match with {
642                Operand::Value(other) => other.clone(),
643                Operand::Field(name) => object.get(name),
644            };
645            (!compares(&value, *comparison, &other)).then(|| say(&words.comparison, None))
646        }
647        RuleKind::Criteria(expression) => {
648            (!meets(object, expression, &environment)).then(|| words.record.clone())
649        }
650        RuleKind::IsTrue => (!matches!(value, Value::Bool(true))).then(|| say(&words.must_be_true, None)),
651        RuleKind::Unique => {
652            let field = rule.field.as_deref()?;
653            duplicated(entity, object, &[field], space).then(|| say(&words.duplicate, None))
654        }
655        RuleKind::UniqueTogether(fields) => {
656            let names: Vec<&str> = fields.iter().map(String::as_str).collect();
657            let captions: Vec<&str> = fields
658                .iter()
659                .filter_map(|name| entity.field_named(name))
660                .map(|field| field.caption.as_str())
661                .collect();
662            duplicated(entity, object, &names, space)
663                .then(|| words.duplicate.replace("{0}", &captions.join(", ")))
664        }
665        RuleKind::Exists { entity: other, criteria } => {
666            let found = space.query(other, &Query::all()).unwrap_or_default();
667            let any = found.iter().any(|record| {
668                criteria.as_deref().is_none_or(|expression| meets(record, expression, &environment))
669            });
670            (!any).then(|| words.none_exists.clone())
671        }
672        RuleKind::Referenced { entity: other, field } => {
673            let key = entity.key_field().map(|f| object.get(&f.name)).unwrap_or_default();
674            let found = space.query(other, &Query::all()).unwrap_or_default();
675            (!found.iter().any(|record| same(&record.get(field), &key))).then(|| words.not_referenced.clone())
676        }
677        RuleKind::Custom(check) => check(object, space).err(),
678    };
679    failed.map(|text| match &rule.message {
680        Some(message) => filled_message(message, rule, entity, object, space, words),
681        None => text,
682    })
683}
684
685/// Checks a record against the rules of its entity that apply in this context. It does not
686/// run by itself: forms call it before keeping, and anything else that writes calls it itself.
687pub fn validate(
688    entity: &EntityInfo,
689    object: &Object,
690    context: &str,
691    space: &dyn ObjectSpace,
692    words: &RuleWords,
693) -> Vec<Violation> {
694    checked(entity, object, context, space, words, false)
695}
696
697/// The same, leaving out rules that have to read other records, for checking every row of a list.
698pub fn validate_in_place(
699    entity: &EntityInfo,
700    object: &Object,
701    context: &str,
702    space: &dyn ObjectSpace,
703    words: &RuleWords,
704) -> Vec<Violation> {
705    checked(entity, object, context, space, words, true)
706}
707
708fn checked(
709    entity: &EntityInfo,
710    object: &Object,
711    context: &str,
712    space: &dyn ObjectSpace,
713    words: &RuleWords,
714    in_place: bool,
715) -> Vec<Violation> {
716    let mut object = object.clone();
717    let environment = crate::environment();
718    entity.compute(&mut object, &environment);
719    entity
720        .rules
721        .iter()
722        .filter(|rule| rule.contexts.iter().any(|c| c == context))
723        .filter(|rule| !(in_place && rule.reads_records()))
724        .filter(|rule| {
725            rule.target_criteria.as_deref().is_none_or(|expression| meets(&object, expression, &environment))
726        })
727        .filter_map(|rule| {
728            broken(rule, entity, &object, space, words).map(|message| Violation {
729                rule: rule.id.clone(),
730                field: rule.field.clone(),
731                message,
732                severity: rule.severity,
733            })
734        })
735        .collect()
736}