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