Skip to main content

stern4rust/settings/
config_file.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use crate::settings::package_config::PackageConfig;
6use anyhow::Context;
7use anyhow::Result;
8use serde::Deserialize;
9use std::collections::BTreeMap;
10use std::fs::read_to_string;
11use std::path::Path;
12use std::path::PathBuf;
13use toml::from_str;
14
15// `stern4rust.toml`, beside the manifest it configures.
16//
17// Every switch this tool has had to be repeated at every invocation, which is
18// tolerable for a person running it once and useless for a repository that
19// wants the same run in a gate script, a pre-commit hook and a developer's
20// terminal. The excludes are the sharpest case: a pattern naming a vendored
21// tree is a fact about the repository, not about one command line.
22//
23// Unknown keys are rejected. A misspelled `exclude` that silently did nothing
24// would look exactly like an exclude that worked, which is the failure this
25// tool exists to refuse -- and it is the same reason an unknown `--rule` name
26// is an error rather than a switch matching nothing.
27#[derive(Debug, Default, Deserialize, PartialEq, Eq)]
28#[serde(deny_unknown_fields, rename_all = "kebab-case")]
29pub struct ConfigFile {
30    #[serde(default)]
31    pub baseline: Option<PathBuf>,
32    #[serde(default)]
33    pub header_file: Option<PathBuf>,
34    #[serde(default)]
35    pub max_files_per_directory: Option<usize>,
36    #[serde(default)]
37    pub max_subfolders_per_directory: Option<usize>,
38    #[serde(default)]
39    pub offence_threshold: Option<usize>,
40    #[serde(default)]
41    pub rules: Vec<String>,
42    #[serde(default)]
43    pub skip: Vec<String>,
44    #[serde(default)]
45    pub exclude: Vec<String>,
46    // A section per package, keyed by the name its manifest declares. Absent
47    // for the packages that apply everything, which is most of them.
48    #[serde(default, rename = "package")]
49    pub packages: BTreeMap<String, PackageConfig>,
50}
51
52impl ConfigFile {
53    pub const NAME: &'static str = "stern4rust.toml";
54
55    // Ok(None) when there is no file, which is the ordinary case and not a
56    // failure. A file that exists and cannot be read or parsed is an error: it
57    // was written on purpose, and running as though it were absent would apply
58    // a configuration nobody chose.
59    pub fn load(directory: &Path) -> Result<Option<Self>> {
60        let path = directory.join(Self::NAME);
61        if !path.exists() {
62            return Ok(None);
63        }
64        let text = read_to_string(&path)
65            .with_context(|| format!("{} could not be read", path.display()))?;
66        let parsed = from_str(&text)
67            .with_context(|| format!("{} is not valid stern4rust configuration", path.display()))?;
68        Ok(Some(parsed))
69    }
70
71    // Paths in the file are relative to the file, so a repository can be checked
72    // out anywhere and cloned into any directory name.
73    pub fn baseline_from(&self, directory: &Path) -> Option<PathBuf> {
74        self.baseline
75            .as_ref()
76            .map(|relative| directory.join(relative))
77    }
78
79    pub fn header_file_from(&self, directory: &Path) -> Option<PathBuf> {
80        self.header_file
81            .as_ref()
82            .map(|relative| directory.join(relative))
83    }
84}