tiger_lib/validator/
value_validator.rs

1use std::borrow::Cow;
2use std::fmt::{Debug, Display, Error, Formatter};
3use std::ops::{Bound, RangeBounds};
4use std::str::FromStr;
5
6use crate::context::ScopeContext;
7use crate::date::Date;
8use crate::everything::Everything;
9#[cfg(feature = "hoi4")]
10use crate::hoi4::variables::validate_variable;
11use crate::item::Item;
12use crate::report::{ErrorKey, Severity, report};
13use crate::scopes::Scopes;
14use crate::token::Token;
15use crate::trigger::validate_target;
16#[cfg(feature = "imperator")]
17use crate::trigger::validate_target_ok_this;
18use crate::validate::validate_identifier;
19
20/// A validator for one `Token`.
21/// The intended usage is that the block-level [`Validator`](crate::validator::Validator) wraps the `Token`
22/// in a `ValueValidator`, then you call one or more of the validation functions on it.
23/// If the `ValueValidator` goes out of scope without having been validated, it will report an error.
24///
25/// Calling multiple validation functions is only supported when starting with the `maybe_`
26/// variants. Calling one of the definite validation methods will either accept the value or warn
27/// about it, and it's considered validated after that.
28pub struct ValueValidator<'a> {
29    /// The value being validated
30    value: Cow<'a, Token>,
31    /// A link to all the loaded and processed CK3 and mod files
32    data: &'a Everything,
33    /// Whether the value has been validated. If true, it means either the value was accepted as
34    /// correct or it was warned about.
35    validated: bool,
36    /// Maximum severity of problems reported by this `ValueValidator`. Defaults to `Error`.
37    /// This is intended to be set lower by validators for less-important items.
38    /// As an exception, `Fatal` severity reports will still always be logged as `Fatal`.
39    max_severity: Severity,
40}
41
42impl Debug for ValueValidator<'_> {
43    /// Roll our own `Debug` implementation in order to leave out the `data` field.
44    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
45        f.debug_struct("ValueValidator")
46            .field("value", &self.value)
47            .field("validated", &self.validated)
48            .field("max_severity", &self.max_severity)
49            .finish()
50    }
51}
52
53impl<'a> ValueValidator<'a> {
54    /// Construct a new `ValueValidator` for a `&Token`.
55    pub fn new(value: &'a Token, data: &'a Everything) -> Self {
56        Self { value: Cow::Borrowed(value), data, validated: false, max_severity: Severity::Fatal }
57    }
58
59    /// Construct a new `ValueValidator` for an owned `Token`.
60    #[allow(dead_code)]
61    pub fn new_owned(value: Token, data: &'a Everything) -> Self {
62        Self { value: Cow::Owned(value), data, validated: false, max_severity: Severity::Fatal }
63    }
64
65    #[allow(dead_code)]
66    pub fn data(&self) -> &Everything {
67        self.data
68    }
69
70    /// Maximum severity of problems reported by this `ValueValidator`. Defaults to `Error`.
71    /// This is intended to be set lower by validators for less-important items.
72    /// As an exception, `Fatal` severity reports will still always be logged as `Fatal`.
73    pub fn set_max_severity(&mut self, max_severity: Severity) {
74        self.max_severity = max_severity;
75    }
76
77    /// Make a descendant validator from this one, usually for a sub-token.
78    #[allow(dead_code)]
79    fn value_validator(&self, token: Token) -> Self {
80        let mut vd = ValueValidator::new_owned(token, self.data);
81        vd.set_max_severity(self.max_severity);
82        vd
83    }
84
85    /// Access the value that this `ValueValidator` validates.
86    pub fn value(&self) -> &Token {
87        &self.value
88    }
89
90    /// Mark this value as valid, without placing any restrictions on it.
91    pub fn accept(&mut self) {
92        self.validated = true;
93    }
94
95    #[allow(dead_code)]
96    pub fn identifier(&mut self, kind: &'static str) {
97        if self.validated {
98            return;
99        }
100        self.validated = true;
101        validate_identifier(&self.value, kind, Severity::Error.at_most(self.max_severity));
102    }
103
104    /// Expect the value to be the key of an `itype` item in the game database.
105    /// The item is looked up and must exist.
106    pub fn item(&mut self, itype: Item) {
107        if self.validated {
108            return;
109        }
110        self.validated = true;
111        self.data.verify_exists_max_sev(itype, &self.value, self.max_severity);
112    }
113
114    // Expect the value to be the name of a file (possibly with suffix) in the defined path.
115    #[cfg(feature = "ck3")]
116    #[allow(dead_code)]
117    pub fn icon(&mut self, define: &str, suffix: &str) {
118        if self.validated {
119            return;
120        }
121        self.validated = true;
122        self.data.verify_icon(define, &self.value, suffix);
123    }
124
125    /// Add the given suffix to the value and mark that as a used item, without doing any validation.
126    /// This is used for very weakly required localization, for example, where no warning is warranted.
127    #[cfg(feature = "ck3")] // silence dead code warning
128    pub fn item_used_with_suffix(&mut self, itype: Item, sfx: &str) {
129        let implied = format!("{}{sfx}", self.value);
130        self.data.mark_used(itype, &implied);
131    }
132
133    /// Validate a localization whose key is derived from the value, in the given [`ScopeContext`].
134    #[allow(dead_code)]
135    pub fn implied_localization_sc(&mut self, pfx: &str, sfx: &str, sc: &mut ScopeContext) {
136        let implied = format!("{pfx}{}{sfx}", self.value);
137        self.data.validate_localization_sc(&implied, sc);
138    }
139
140    /// Check if the value is the key of an `itype` item the game database.
141    /// The item is looked up, and if it exists then this validator is considered validated.
142    /// Return whether the item exists.
143    #[allow(dead_code)]
144    pub fn maybe_item(&mut self, itype: Item) -> bool {
145        if self.data.item_exists(itype, self.value.as_str()) {
146            self.validated = true;
147            true
148        } else {
149            false
150        }
151    }
152
153    /// Check if the value is be the key of an `itype` item the game database, after removing the prefix `pfx`.
154    /// The item is looked up, and if it exists then this validator is considered validated.
155    /// Return whether the item exists.
156    #[cfg(feature = "vic3")] // silence dead code warning
157    pub fn maybe_prefix_item(&mut self, pfx: &str, itype: Item) -> bool {
158        if let Some(value) = self.value.as_str().strip_prefix(pfx) {
159            if self.data.item_exists(itype, value) {
160                self.validated = true;
161                return true;
162            }
163        }
164        false
165    }
166
167    /// Expect the value to be the name of a file under the directory given here.
168    #[cfg(feature = "ck3")] // silence dead code warning
169    pub fn dir_file(&mut self, path: &str) {
170        if self.validated {
171            return;
172        }
173        self.validated = true;
174        let pathname = format!("{path}/{}", self.value);
175        // TODO: pass max_severity here
176        self.data.verify_exists_implied(Item::File, &pathname, &self.value);
177    }
178
179    #[must_use]
180    #[allow(dead_code)]
181    pub fn split(&mut self, c: char) -> Vec<ValueValidator<'_>> {
182        self.validated = true;
183        self.value.split(c).into_iter().map(|value| self.value_validator(value)).collect()
184    }
185
186    /// Expect the value to be a (possibly single-element) scope chain which evaluates to a scope type in `outscopes`.
187    ///
188    /// The value is evaluated in the scope context `sc`, so for example if the value does `scope:actor` but there is
189    /// no named scope "actor" in the scope context, then a warning is emitted.
190    ///
191    /// Also emits a warning if the value is simply "`this`", because that is almost never correct.
192    #[allow(dead_code)] // not used yet
193    pub fn target(&mut self, sc: &mut ScopeContext, outscopes: Scopes) {
194        if self.validated {
195            return;
196        }
197        self.validated = true;
198        // TODO: pass max_severity here
199        validate_target(&self.value, self.data, sc, outscopes);
200    }
201
202    /// Just like [`ValueValidator::target`], but allows the value to be simply "`this`".
203    /// It is expected to be used judiciously in cases where "`this`" can be correct.
204    #[cfg(feature = "imperator")]
205    pub fn target_ok_this(&mut self, sc: &mut ScopeContext, outscopes: Scopes) {
206        if self.validated {
207            return;
208        }
209        self.validated = true;
210        // TODO: pass max_severity here
211        validate_target_ok_this(&self.value, self.data, sc, outscopes);
212    }
213
214    /// This is a combination of [`ValueValidator::item`] and [`ValueValidator::target`]. If the field is present
215    /// and is not a known `itype` item, then it is evaluated as a target.
216    #[allow(dead_code)] // not used yet
217    pub fn item_or_target(&mut self, sc: &mut ScopeContext, itype: Item, outscopes: Scopes) {
218        if self.validated {
219            return;
220        }
221        self.validated = true;
222        if !self.data.item_exists(itype, self.value.as_str()) {
223            // TODO: pass max_severity here
224            validate_target(&self.value, self.data, sc, outscopes);
225        }
226    }
227
228    /// Expect the value to be just `yes` or `no`.
229    #[allow(dead_code)]
230    pub fn bool(&mut self) {
231        if self.validated {
232            return;
233        }
234        let sev = Severity::Error.at_most(self.max_severity);
235        self.validated = true;
236        if !self.value.lowercase_is("yes") && !self.value.lowercase_is("no") {
237            report(ErrorKey::Validation, sev).msg("expected yes or no").loc(self).push();
238        }
239    }
240
241    /// Allow the value to be just `yes` or `no`, but don't mandate it.
242    #[allow(dead_code)]
243    pub fn maybe_bool(&mut self) {
244        if self.validated {
245            return;
246        }
247        if self.value.lowercase_is("yes") || self.value.lowercase_is("no") {
248            self.validated = true;
249        }
250    }
251
252    /// Expect the value to be an integer.
253    pub fn integer(&mut self) {
254        if self.validated {
255            return;
256        }
257        self.validated = true;
258        // TODO: pass max_severity here
259        self.value.expect_integer();
260    }
261
262    /// Allow the value to be an integer, but don't mandate it.
263    #[allow(dead_code)]
264    pub fn maybe_integer(&mut self) {
265        if self.validated {
266            return;
267        }
268        if self.value.is_integer() {
269            self.validated = true;
270        }
271    }
272
273    /// Expect the value to be an integer between `low` and `high` (inclusive).
274    #[allow(dead_code)]
275    pub fn integer_range<R: RangeBounds<i64>>(&mut self, range: R) {
276        if self.validated {
277            return;
278        }
279        let sev = Severity::Error.at_most(self.max_severity);
280        self.validated = true;
281        // TODO: pass max_severity here
282        if let Some(i) = self.value.expect_integer() {
283            if !range.contains(&i) {
284                let low = match range.start_bound() {
285                    Bound::Unbounded => None,
286                    Bound::Included(&n) => Some(n),
287                    Bound::Excluded(&n) => Some(n + 1),
288                };
289                let high = match range.end_bound() {
290                    Bound::Unbounded => None,
291                    Bound::Included(&n) => Some(n),
292                    Bound::Excluded(&n) => Some(n - 1),
293                };
294                let msg;
295                if let (Some(low), Some(high)) = (low, high) {
296                    msg = format!("should be between {low} and {high} (inclusive)");
297                } else if let Some(low) = low {
298                    msg = format!("should be at least {low}");
299                } else if let Some(high) = high {
300                    msg = format!("should be at most {high}");
301                } else {
302                    unreachable!(); // could not have failed the contains check
303                }
304                report(ErrorKey::Range, sev).msg(msg).loc(self).push();
305            }
306        }
307    }
308
309    /// Expect the value to be a number with up to 5 decimals.
310    /// (5 decimals is the limit accepted by the game engine in most contexts).
311    #[allow(dead_code)] // not used yet
312    pub fn numeric(&mut self) {
313        if self.validated {
314            return;
315        }
316        self.validated = true;
317        // TODO: pass max_severity here
318        self.value.expect_number();
319    }
320
321    /// Expect the value to be a number with up to 5 decimals within the `range` provided.
322    /// (5 decimals is the limit accepted by the game engine in most contexts).
323    #[cfg(feature = "vic3")]
324    pub fn numeric_range<R: RangeBounds<f64>>(&mut self, range: R) {
325        if self.validated {
326            return;
327        }
328        let sev = Severity::Error.at_most(self.max_severity);
329        self.validated = true;
330        // TODO: pass max_severity here
331        if let Some(f) = self.value.expect_number() {
332            if !range.contains(&f) {
333                let low = match range.start_bound() {
334                    Bound::Unbounded => None,
335                    Bound::Included(&f) => Some(format!("{f} (inclusive)")),
336                    Bound::Excluded(&f) => Some(format!("{f}")),
337                };
338                let high = match range.end_bound() {
339                    Bound::Unbounded => None,
340                    Bound::Included(&f) => Some(format!("{f} (inclusive)")),
341                    Bound::Excluded(&f) => Some(format!("{f}")),
342                };
343                let msg;
344                if let (Some(low), Some(high)) = (low.as_ref(), high.as_ref()) {
345                    msg = format!("should be between {low} and {high}");
346                } else if let Some(low) = low {
347                    msg = format!("should be at least {low}");
348                } else if let Some(high) = high {
349                    msg = format!("should be at most {high}");
350                } else {
351                    unreachable!(); // could not have failed the contains check
352                }
353                report(ErrorKey::Range, sev).msg(msg).loc(self).push();
354            }
355        }
356    }
357
358    /// Expect the value to be a number with any number of decimals.
359    #[allow(dead_code)] // not used yet
360    pub fn precise_numeric(&mut self) {
361        if self.validated {
362            return;
363        }
364        self.validated = true;
365        // TODO: pass max_severity here
366        self.value.expect_precise_number();
367    }
368
369    /// Expect the value to be a date.
370    /// The format of dates is very flexible, from a single number (the year), to a year.month or year.month.day.
371    /// No checking is done on the validity of the date as a date (so January 42nd is okay).
372    #[allow(dead_code)] // not used yet
373    pub fn date(&mut self) {
374        if self.validated {
375            return;
376        }
377        let sev = Severity::Error.at_most(self.max_severity);
378        self.validated = true;
379        if Date::from_str(self.value.as_str()).is_err() {
380            let msg = "expected date value";
381            report(ErrorKey::Validation, sev).msg(msg).loc(self).push();
382        }
383    }
384
385    /// Expect the value to be one of the listed strings in `choices`.
386    #[allow(dead_code)]
387    pub fn choice(&mut self, choices: &[&str]) {
388        if self.validated {
389            return;
390        }
391        self.validated = true;
392        let sev = Severity::Error.at_most(self.max_severity);
393        if !choices.contains(&self.value.as_str()) {
394            let msg = format!("expected one of {}", choices.join(", "));
395            report(ErrorKey::Choice, sev).msg(msg).loc(self).push();
396        }
397    }
398
399    /// Expect the value to be a variable reference
400    #[cfg(feature = "hoi4")]
401    pub fn variable(&mut self, sc: &mut ScopeContext) {
402        if self.validated {
403            return;
404        }
405        self.validated = true;
406        let sev = Severity::Error.at_most(self.max_severity);
407        validate_variable(&self.value, self.data, sc, sev);
408    }
409
410    /// Check if the value is equal to the given string.
411    /// If it is, mark this value as validated.
412    /// Return whether the value matched the string.
413    pub fn maybe_is(&mut self, s: &str) -> bool {
414        if self.value.is(s) {
415            self.validated = true;
416            true
417        } else {
418            false
419        }
420    }
421
422    /// Tells the `ValueValidator` to report a warning if the value is still unvalidated.
423    pub fn warn_unvalidated(&mut self) {
424        if !self.validated {
425            let sev = Severity::Error.at_most(self.max_severity);
426            let msg = format!("unknown value `{}`", self.value);
427            report(ErrorKey::Validation, sev).msg(msg).loc(self).push();
428        }
429    }
430}
431
432impl Drop for ValueValidator<'_> {
433    fn drop(&mut self) {
434        self.warn_unvalidated();
435    }
436}
437
438impl Display for ValueValidator<'_> {
439    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
440        Display::fmt(&self.value, f)
441    }
442}