tiger_lib/
config_load.rs

1//! Loading and interpreting the config file.
2//!
3//! The config file is located at the top level of the mod and is named after the validator, for
4//! example `ck3-tiger.conf`.
5
6use std::path::PathBuf;
7
8use strum::IntoEnumIterator;
9
10use crate::block::{Block, BlockItem, Comparator, Eq::*, Field, BV};
11use crate::helpers::stringify_list;
12use crate::report::{
13    err, set_predicate, set_show_loaded_mods, set_show_vanilla, Confidence, ErrorKey, ErrorLoc,
14    FilterRule, PointedMessage, Severity,
15};
16
17/// Checks for legacy ignore blocks (that no longer work) and report an error if they are present.
18pub fn check_for_legacy_ignore(config: &Block) {
19    // First, report errors if legacy ignore blocks are detected:
20    let pointers: Vec<PointedMessage> = config
21        .get_keys("ignore")
22        .into_iter()
23        .map(|key| PointedMessage::new(key.into_loc()))
24        .collect();
25    if !pointers.is_empty() {
26        err(ErrorKey::Config)
27            .strong()
28            .msg("`ignore` is deprecated, consider using `filter` instead.")
29            .info("Check out the filter.md guide on GitHub for tips on how to migrate.")
30            .pointers(pointers)
31            .push();
32    }
33}
34
35/// Check if config file that was passed in with --conf argument is valid.
36/// If it is not valid let the user know, set it to None, and use the default one instead.
37pub fn validate_config_file(config: Option<PathBuf>) -> Option<PathBuf> {
38    match config {
39        Some(config) => {
40            if config.is_file() {
41                if config.extension().is_some_and(|s| s != "conf") {
42                    eprintln!(
43                        "{} is not a valid .conf file. Using the default conf file instead.",
44                        config.display()
45                    );
46                    None
47                } else {
48                    eprintln!("Using conf file: {}", config.display());
49                    Some(config)
50                }
51            } else {
52                eprintln!(
53                    "{} is not a valid file. Using the default conf file instead.",
54                    config.display()
55                );
56                None
57            }
58        }
59        None => None,
60    }
61}
62
63pub fn load_filter(config: &Block) {
64    assert_one_key("filter", config);
65    if let Some(filter) = config.get_field_block("filter") {
66        assert_one_key("trigger", filter);
67        assert_one_key("show_vanilla", filter);
68        assert_one_key("show_loaded_mods", filter);
69        set_show_vanilla(filter.get_field_bool("show_vanilla").unwrap_or(false));
70        set_show_loaded_mods(filter.get_field_bool("show_loaded_mods").unwrap_or(false));
71        if let Some(trigger) = filter.get_field_block("trigger") {
72            set_predicate(FilterRule::Conjunction(load_rules(trigger)));
73        } else {
74            set_predicate(FilterRule::default());
75        }
76    }
77}
78
79/// Load a vector of rules from the given block.
80fn load_rules(block: &Block) -> Vec<FilterRule> {
81    block.iter_items().filter_map(BlockItem::expect_field).filter_map(load_rule).collect()
82}
83
84/// Load a vector of rules from a value.
85/// This first checks that the value is a block. If so, it loads a `Vec` of `FilterRule`s.
86fn load_rules_from_bv(bv: &BV) -> Option<Vec<FilterRule>> {
87    match bv {
88        BV::Block(block) => Some(load_rules(block)),
89        BV::Value(_) => {
90            let msg = "Expected a trigger block. Example usage: `AND = { }`";
91            err(ErrorKey::Config).msg(msg).loc(bv).push();
92            None
93        }
94    }
95}
96
97/// Load a single rule.
98fn load_rule(field: &Field) -> Option<FilterRule> {
99    let Field(key, cmp, bv) = field;
100    let cmp = *cmp;
101    if !key.is("severity") && !key.is("confidence") && !matches!(cmp, Comparator::Equals(Single)) {
102        err(ErrorKey::Config)
103            .msg(format!("Unexpected operator `{cmp}`, only `=` is valid here."))
104            .loc(key)
105            .push();
106        return None;
107    }
108    match key.as_str() {
109        "severity" => load_rule_severity(cmp, bv),
110        "confidence" => load_rule_confidence(cmp, bv),
111        "key" => load_rule_key(bv),
112        "file" => load_rule_file(bv),
113        "text" => load_rule_text(bv),
114        "always" => load_rule_always(bv),
115        "ignore_keys_in_files" => load_ignore_keys_in_files(bv),
116        "NOT" => load_not(bv),
117        "AND" => Some(FilterRule::Conjunction(load_rules_from_bv(bv)?)),
118        "OR" => Some(FilterRule::Disjunction(load_rules_from_bv(bv)?)),
119        "NAND" => {
120            Some(FilterRule::Negation(Box::new(FilterRule::Conjunction(load_rules_from_bv(bv)?))))
121        }
122        "NOR" => {
123            Some(FilterRule::Negation(Box::new(FilterRule::Disjunction(load_rules_from_bv(bv)?))))
124        }
125        _ => {
126            err(ErrorKey::Config).msg("Unexpected key").loc(key).push();
127            None
128        }
129    }
130}
131
132/// This loads a NOT block.
133/// In paradox script, NOT is actually an implicit NOR.
134/// Load the children, if more than one exists, it returns a NOR block, otherwise a NOT.
135fn load_not(bv: &BV) -> Option<FilterRule> {
136    let mut children = load_rules_from_bv(bv)?;
137    if children.is_empty() {
138        err(ErrorKey::Config)
139            .msg("This NOT block contains no valid triggers. It will be ignored.")
140            .loc(bv)
141            .push();
142        None
143    } else if children.len() == 1 {
144        Some(FilterRule::Negation(Box::new(children.remove(0))))
145    } else {
146        Some(FilterRule::Negation(Box::new(FilterRule::Disjunction(children))))
147    }
148}
149
150fn load_rule_always(bv: &BV) -> Option<FilterRule> {
151    match bv {
152        BV::Block(_) => {
153            err(ErrorKey::Config)
154                .msg("`always` can't open a block. Valid values are `yes` and `no`.")
155                .loc(bv)
156                .push();
157            None
158        }
159        BV::Value(token) => match token.as_str() {
160            "yes" => Some(FilterRule::Tautology),
161            "no" => Some(FilterRule::Contradiction),
162            _ => {
163                err(ErrorKey::Config)
164                    .msg("`always` value not recognised. Valid values are `yes` and `no`.")
165                    .loc(bv)
166                    .push();
167                None
168            }
169        },
170    }
171}
172
173/// Loads the `ignore_keys_in_files` trigger.
174/// This is syntactic sugar for a NAND wrapping an OR of keys and an OR of files.
175fn load_ignore_keys_in_files(bv: &BV) -> Option<FilterRule> {
176    let Some(block) = bv.get_block() else {
177        err(ErrorKey::Config)
178            .strong()
179            .msg("This trigger should open a block.")
180            .info("Usage: ignore_keys_in_files = { keys = {} files = {} }")
181            .loc(bv)
182            .push();
183        return None;
184    };
185
186    let mut keys = None;
187    let mut files = None;
188
189    for item in block.iter_items() {
190        let Some(Field(key, cmp, bv)) = item.get_field() else {
191            err(ErrorKey::Config)
192                .strong()
193                .msg("Didn't expect a loose value here.")
194                .info("Usage: ignore_keys_in_files = { keys = {} files = {} }")
195                .loc(item)
196                .push();
197            return None;
198        };
199        let key_str = key.as_str();
200        if key_str != "keys" && key_str != "files" {
201            err(ErrorKey::Config)
202                .strong()
203                .msg("This key isn't valid here.")
204                .info("Usage: ignore_keys_in_files = { keys = {} files = {} }")
205                .loc(bv)
206                .push();
207            return None;
208        }
209        if !matches!(cmp, Comparator::Equals(Single)) {
210            err(ErrorKey::Config)
211                .strong()
212                .msg("Expected `=` here.")
213                .info("Usage: ignore_keys_in_files = { keys = {} files = {} }")
214                .loc(key)
215                .push();
216            return None;
217        }
218        if let BV::Value(_) = bv {
219            err(ErrorKey::Config)
220                .strong()
221                .msg("This should open a block.")
222                .info("Usage: ignore_keys_in_files = { keys = {} files = {} }")
223                .loc(bv)
224                .push();
225            return None;
226        }
227        let array_block = bv.expect_block().expect("Should be ok");
228        if key_str == "keys" {
229            keys = load_keys_array(array_block);
230        }
231        if key_str == "files" {
232            files = load_files_array(array_block);
233        }
234    }
235    if keys.is_none() {
236        err(ErrorKey::Config)
237            .strong()
238            .msg("There are no valid keys. This `ignore_keys_in_files` trigger will be ignored.")
239            .info(
240                "Add at least one key. Example: ignore_keys_in_files = { keys = { unknown-field }",
241            )
242            .loc(block)
243            .push();
244        None
245    } else if files.is_none() {
246        err(ErrorKey::Config)
247            .strong()
248            .msg("There are no valid files. This `ignore_keys_in_files` trigger will be ignored.")
249            .info("Add at least one file. Example: ignore_keys_in_files = { files = { common/ }")
250            .loc(block)
251            .push();
252        None
253    } else {
254        Some(FilterRule::Negation(Box::new(FilterRule::Conjunction(vec![
255            keys.expect("Should exist."),
256            files.expect("Should exist."),
257        ]))))
258    }
259}
260
261fn load_keys_array(array_block: &Block) -> Option<FilterRule> {
262    let keys: Vec<_> = array_block.iter_values_warn()
263        .filter_map(|token| {
264            if let Ok(error_key) = token.as_str().parse() {
265                Some(FilterRule::Key(error_key))
266            } else {
267                err(ErrorKey::Config).strong()
268                    .msg("Invalid key. In the output, keys are listed between parentheses on the first line of each report. For example, in `Warning(missing-item)`, the key is `missing-item`.")
269                    .loc(token)
270                    .push();
271                None
272            }
273        }).collect();
274    if keys.is_empty() {
275        None
276    } else {
277        Some(FilterRule::Disjunction(keys))
278    }
279}
280fn load_files_array(array_block: &Block) -> Option<FilterRule> {
281    let files: Vec<_> =
282        array_block.iter_values_warn().filter_map(FilterRule::file_from_token).collect();
283    if files.is_empty() {
284        None
285    } else {
286        Some(FilterRule::Disjunction(files))
287    }
288}
289
290fn load_rule_severity(comparator: Comparator, value: &BV) -> Option<FilterRule> {
291    match value {
292        BV::Block(_) => {
293            err(ErrorKey::Config)
294                .msg("`severity` can't open a block. Example usage: `severity >= Warning`")
295                .loc(value)
296                .push();
297            None
298        }
299        BV::Value(token) => {
300            if let Ok(severity) = token.as_str().to_ascii_lowercase().parse() {
301                Some(FilterRule::Severity(comparator, severity))
302            } else {
303                err(ErrorKey::Config)
304                    .msg(format!(
305                        "Invalid Severity value. Valid values: {}",
306                        stringify_list(&Severity::iter().map(Severity::into).collect::<Vec<_>>()),
307                    ))
308                    .loc(token)
309                    .push();
310                None
311            }
312        }
313    }
314}
315
316fn load_rule_confidence(comparator: Comparator, value: &BV) -> Option<FilterRule> {
317    match value {
318        BV::Block(_) => {
319            err(ErrorKey::Config)
320                .msg("`confidence` can't open a block. Example usage: `confidence >= Reasonable`")
321                .loc(value)
322                .push();
323            None
324        }
325        BV::Value(token) => {
326            if let Ok(confidence) = token.as_str().to_ascii_lowercase().parse() {
327                Some(FilterRule::Confidence(comparator, confidence))
328            } else {
329                err(ErrorKey::Config)
330                    .msg(format!(
331                        "Invalid Confidence value. Valid values are {}",
332                        stringify_list(
333                            &Confidence::iter().map(Confidence::into).collect::<Vec<_>>()
334                        )
335                    ))
336                    .loc(token)
337                    .push();
338                None
339            }
340        }
341    }
342}
343
344fn load_rule_key(value: &BV) -> Option<FilterRule> {
345    match value {
346        BV::Block(_) => {
347            err(ErrorKey::Config)
348                .msg("`key` can't open a block. Example usage: `key = missing-item`")
349                .loc(value)
350                .push();
351            None
352        }
353        BV::Value(token) => {
354            if let Ok(error_key) = token.as_str().parse() {
355                Some(FilterRule::Key(error_key))
356            } else {
357                err(ErrorKey::Config).msg(
358                    "Invalid key. In the output, keys are listed between parentheses on the first line of each report. For example, in `Warning(missing-item)`, the key is `missing-item`.",
359                ).loc(token).push();
360                None
361            }
362        }
363    }
364}
365
366fn load_rule_file(value: &BV) -> Option<FilterRule> {
367    match value {
368        BV::Block(_) => {
369            err(
370                ErrorKey::Config).msg(
371                "`file` can't open a block. Example usage: `file = common/traits/00_traits.txt`",
372            ).loc(value).push();
373            None
374        }
375        BV::Value(token) => FilterRule::file_from_token(token),
376    }
377}
378
379fn load_rule_text(bv: &BV) -> Option<FilterRule> {
380    match bv {
381        BV::Block(_) => {
382            err(
383                ErrorKey::Config).msg(
384                "`text` can't open a block. Example usage: `text = \"coat of arms is redefined\"`",
385            ).loc(bv).push();
386            None
387        }
388        BV::Value(token) => Some(FilterRule::Text(token.to_string())),
389    }
390}
391
392/// Assert that the given key occurs at most once within the given block.
393/// If the assertion fails, an error report will be created. No other action will be taken.
394pub fn assert_one_key(assert_key: &str, block: &Block) {
395    let keys: Vec<_> = block
396        .iter_items()
397        .filter_map(|item| {
398            if let BlockItem::Field(Field(key, _, _)) = item {
399                (key.as_str() == assert_key).then_some(key)
400            } else {
401                None
402            }
403        })
404        .collect();
405    if keys.len() > 1 {
406        let pointers = keys
407            .iter()
408            .enumerate()
409            .map(|(index, key)| PointedMessage {
410                loc: key.into_loc(),
411                length: 1,
412                msg: Some((if index == 0 { "It occurs here" } else { "and here" }).to_owned()),
413            })
414            .collect();
415        err(ErrorKey::Config)
416            .strong()
417            .msg(format!("Detected more than one `{assert_key}`: there can be only one here!"))
418            .pointers(pointers)
419            .push();
420    }
421}