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::{BV, Block, BlockItem, Comparator, Eq::*, Field};
11use crate::helpers::stringify_list;
12use crate::report::{
13    Confidence, ErrorKey, ErrorLoc, FilterRule, PointedMessage, Severity, err, set_predicate,
14    set_show_loaded_mods, set_show_vanilla,
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 let Some(keys) = keys {
236        if let Some(files) = files {
237            Some(FilterRule::Negation(Box::new(FilterRule::Conjunction(vec![keys, files]))))
238        } else {
239            err(ErrorKey::Config)
240            .strong()
241            .msg("There are no valid files. This `ignore_keys_in_files` trigger will be ignored.")
242            .info("Add at least one file. Example: ignore_keys_in_files = { files = { common/ }")
243            .loc(block)
244            .push();
245            None
246        }
247    } else {
248        err(ErrorKey::Config)
249            .strong()
250            .msg("There are no valid keys. This `ignore_keys_in_files` trigger will be ignored.")
251            .info(
252                "Add at least one key. Example: ignore_keys_in_files = { keys = { unknown-field }",
253            )
254            .loc(block)
255            .push();
256        None
257    }
258}
259
260fn load_keys_array(array_block: &Block) -> Option<FilterRule> {
261    let keys: Vec<_> = array_block.iter_values_warn()
262        .filter_map(|token| {
263            if let Ok(error_key) = token.as_str().parse() {
264                Some(FilterRule::Key(error_key))
265            } else {
266                err(ErrorKey::Config).strong()
267                    .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`.")
268                    .loc(token)
269                    .push();
270                None
271            }
272        }).collect();
273    if keys.is_empty() { None } else { Some(FilterRule::Disjunction(keys)) }
274}
275fn load_files_array(array_block: &Block) -> Option<FilterRule> {
276    let files: Vec<_> =
277        array_block.iter_values_warn().filter_map(FilterRule::file_from_token).collect();
278    if files.is_empty() { None } else { Some(FilterRule::Disjunction(files)) }
279}
280
281fn load_rule_severity(comparator: Comparator, value: &BV) -> Option<FilterRule> {
282    match value {
283        BV::Block(_) => {
284            err(ErrorKey::Config)
285                .msg("`severity` can't open a block. Example usage: `severity >= Warning`")
286                .loc(value)
287                .push();
288            None
289        }
290        BV::Value(token) => {
291            if let Ok(severity) = token.as_str().to_ascii_lowercase().parse() {
292                Some(FilterRule::Severity(comparator, severity))
293            } else {
294                err(ErrorKey::Config)
295                    .msg(format!(
296                        "Invalid Severity value. Valid values: {}",
297                        stringify_list(&Severity::iter().map(Severity::into).collect::<Vec<_>>()),
298                    ))
299                    .loc(token)
300                    .push();
301                None
302            }
303        }
304    }
305}
306
307fn load_rule_confidence(comparator: Comparator, value: &BV) -> Option<FilterRule> {
308    match value {
309        BV::Block(_) => {
310            err(ErrorKey::Config)
311                .msg("`confidence` can't open a block. Example usage: `confidence >= Reasonable`")
312                .loc(value)
313                .push();
314            None
315        }
316        BV::Value(token) => {
317            if let Ok(confidence) = token.as_str().to_ascii_lowercase().parse() {
318                Some(FilterRule::Confidence(comparator, confidence))
319            } else {
320                err(ErrorKey::Config)
321                    .msg(format!(
322                        "Invalid Confidence value. Valid values are {}",
323                        stringify_list(
324                            &Confidence::iter().map(Confidence::into).collect::<Vec<_>>()
325                        )
326                    ))
327                    .loc(token)
328                    .push();
329                None
330            }
331        }
332    }
333}
334
335fn load_rule_key(value: &BV) -> Option<FilterRule> {
336    match value {
337        BV::Block(_) => {
338            err(ErrorKey::Config)
339                .msg("`key` can't open a block. Example usage: `key = missing-item`")
340                .loc(value)
341                .push();
342            None
343        }
344        BV::Value(token) => {
345            if let Ok(error_key) = token.as_str().parse() {
346                Some(FilterRule::Key(error_key))
347            } else {
348                err(ErrorKey::Config).msg(
349                    "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`.",
350                ).loc(token).push();
351                None
352            }
353        }
354    }
355}
356
357fn load_rule_file(value: &BV) -> Option<FilterRule> {
358    match value {
359        BV::Block(_) => {
360            err(
361                ErrorKey::Config).msg(
362                "`file` can't open a block. Example usage: `file = common/traits/00_traits.txt`",
363            ).loc(value).push();
364            None
365        }
366        BV::Value(token) => FilterRule::file_from_token(token),
367    }
368}
369
370fn load_rule_text(bv: &BV) -> Option<FilterRule> {
371    match bv {
372        BV::Block(_) => {
373            err(
374                ErrorKey::Config).msg(
375                "`text` can't open a block. Example usage: `text = \"coat of arms is redefined\"`",
376            ).loc(bv).push();
377            None
378        }
379        BV::Value(token) => Some(FilterRule::Text(token.to_string())),
380    }
381}
382
383/// Assert that the given key occurs at most once within the given block.
384/// If the assertion fails, an error report will be created. No other action will be taken.
385pub fn assert_one_key(assert_key: &str, block: &Block) {
386    let keys: Vec<_> = block
387        .iter_items()
388        .filter_map(|item| {
389            if let BlockItem::Field(Field(key, _, _)) = item {
390                (key.as_str() == assert_key).then_some(key)
391            } else {
392                None
393            }
394        })
395        .collect();
396    if keys.len() > 1 {
397        let pointers = keys
398            .iter()
399            .enumerate()
400            .map(|(index, key)| PointedMessage {
401                loc: key.into_loc(),
402                length: 1,
403                msg: Some((if index == 0 { "It occurs here" } else { "and here" }).into()),
404            })
405            .collect();
406        err(ErrorKey::Config)
407            .strong()
408            .msg(format!("Detected more than one `{assert_key}`: there can be only one here!"))
409            .pointers(pointers)
410            .push();
411    }
412}