Skip to main content

stern4rust/
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 offence_threshold: Option<usize>,
34    #[serde(default)]
35    pub rules: Vec<String>,
36    #[serde(default)]
37    pub skip: Vec<String>,
38    #[serde(default)]
39    pub exclude: Vec<String>,
40}
41
42impl ConfigFile {
43    pub const NAME: &'static str = "stern4rust.toml";
44
45    // Ok(None) when there is no file, which is the ordinary case and not a
46    // failure. A file that exists and cannot be read or parsed is an error: it
47    // was written on purpose, and running as though it were absent would apply
48    // a configuration nobody chose.
49    pub fn load(directory: &Path) -> Result<Option<Self>> {
50        let path = directory.join(Self::NAME);
51        if !path.exists() {
52            return Ok(None);
53        }
54        let text = read_to_string(&path)
55            .with_context(|| format!("{} could not be read", path.display()))?;
56        let parsed = from_str(&text)
57            .with_context(|| format!("{} is not valid stern4rust configuration", path.display()))?;
58        Ok(Some(parsed))
59    }
60
61    // Paths in the file are relative to the file, so a repository can be checked
62    // out anywhere and cloned into any directory name.
63    pub fn baseline_from(&self, directory: &Path) -> Option<PathBuf> {
64        self.baseline
65            .as_ref()
66            .map(|relative| directory.join(relative))
67    }
68
69    pub fn header_file_from(&self, directory: &Path) -> Option<PathBuf> {
70        self.header_file
71            .as_ref()
72            .map(|relative| directory.join(relative))
73    }
74}