Skip to main content

declint_core/
config_set.rs

1//! Loading a whole configuration *site*: a hidden file (`.declint.yaml`)
2//! or a hidden directory of per-language configs (`.declint/*.yaml`),
3//! found by walking up from a starting directory.
4
5use std::path::{Path, PathBuf};
6
7use crate::config::{Config, ConfigError};
8use crate::config::Scope;
9
10/// The hidden config file: `.declint.yaml`.
11pub const CONFIG_FILE: &str = ".declint.yaml";
12
13/// The hidden config directory: `.declint/` (every `*.yaml` inside is a
14/// config).
15pub const CONFIG_DIR: &str = ".declint";
16
17/// The legacy, non-hidden config file, still accepted last.
18pub const LEGACY_CONFIG_FILE: &str = "declint.yaml";
19
20/// One config file loaded as part of a [`ConfigSet`].
21#[derive(Debug, Clone)]
22pub struct NamedConfig {
23    /// The file the config came from (used in error messages).
24    pub path: PathBuf,
25    /// The validated config.
26    pub config: Config,
27}
28
29/// A set of config files loaded together — the whole `.declint.yaml`, or
30/// everything in a `.declint/` directory.
31///
32/// Ids must be unique *within* one file, but different files may reuse
33/// them: only the configs whose `languages` match a document ever apply
34/// to it, so diagnostic codes stay unambiguous per document.
35#[derive(Debug, Clone, Default)]
36pub struct ConfigSet {
37    configs: Vec<NamedConfig>,
38}
39
40/// One scope from a [`ConfigSet`], with its position in the set: which
41/// config it belongs to and its index within that config.
42#[derive(Debug, Clone)]
43pub struct ScopeEntry {
44    /// The scope itself.
45    pub scope: Scope,
46    /// Index into [`ConfigSet::configs`].
47    pub config: usize,
48    /// The scope's index within its own config.
49    pub local: usize,
50}
51
52impl ConfigSet {
53    /// A set holding exactly one config (no file path attached).
54    pub fn single(config: Config) -> Self {
55        Self {
56            configs: vec![NamedConfig {
57                path: PathBuf::new(),
58                config,
59            }],
60        }
61    }
62
63    /// Loads `path` as a config site: a file is one config; a directory
64    /// is every `*.yaml`/`*.yml` inside it (in file-name order).
65    pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
66        let path = path.as_ref();
67        let shown = path.display().to_string();
68        if path.is_dir() {
69            let mut files: Vec<PathBuf> = std::fs::read_dir(path)
70                .map_err(|e| {
71                    ConfigError::new(format!("cannot read config directory: {e}"))
72                        .with_path(&shown)
73                })?
74                .filter_map(|entry| entry.ok().map(|e| e.path()))
75                .filter(|p| {
76                    matches!(p.extension().and_then(|e| e.to_str()), Some("yaml") | Some("yml"))
77                })
78                .collect();
79            files.sort();
80            if files.is_empty() {
81                return Err(ConfigError::new(format!(
82                    "no `.yaml` or `.yml` config files in `{shown}`"
83                )));
84            }
85            let mut configs = Vec::with_capacity(files.len());
86            for file in files {
87                let config = Config::load(&file)?;
88                configs.push(NamedConfig { path: file, config });
89            }
90            return Ok(Self { configs });
91        }
92        let config = Config::load(path)?;
93        Ok(Self {
94            configs: vec![NamedConfig { path: path.to_path_buf(), config }],
95        })
96    }
97
98    /// Finds and loads the config site for `start`, walking up through
99    /// parent directories. At each directory the search order is
100    /// [`CONFIG_FILE`], [`CONFIG_DIR`], [`LEGACY_CONFIG_FILE`]; the first
101    /// hit is loaded.
102    pub fn discover(start: &Path) -> Result<Self, ConfigError> {
103        let start = if start.is_dir() {
104            start
105        } else {
106            start.parent().unwrap_or(Path::new("."))
107        };
108        for dir in start.ancestors() {
109            for candidate in [dir.join(CONFIG_FILE), dir.join(CONFIG_DIR)] {
110                if candidate.is_file() || candidate.is_dir() {
111                    return Self::load(&candidate);
112                }
113            }
114            let legacy = dir.join(LEGACY_CONFIG_FILE);
115            if legacy.is_file() {
116                return Self::load(&legacy);
117            }
118        }
119        Err(ConfigError::new(format!(
120            "no declint config found (looked for {CONFIG_FILE}, {CONFIG_DIR}/, \
121             {LEGACY_CONFIG_FILE} in `{}` and its parents)",
122            start.display()
123        )))
124    }
125
126    /// The loaded configs, in load order.
127    pub fn configs(&self) -> &[NamedConfig] {
128        &self.configs
129    }
130
131    /// Every scope across every config, with its global position: `entry.config`
132    /// indexes [`ConfigSet::configs`] and `entry.local` indexes that
133    /// config's `scopes`.
134    pub fn scope_table(&self) -> Vec<ScopeEntry> {
135        let mut out = Vec::new();
136        for (config_index, named) in self.configs.iter().enumerate() {
137            for (local, scope) in named.config.scopes.iter().enumerate() {
138                out.push(ScopeEntry {
139                    scope: scope.clone(),
140                    config: config_index,
141                    local,
142                });
143            }
144        }
145        out
146    }
147}