taplo_common/
config.rs

1use std::{
2    fmt::Debug,
3    path::{Path, PathBuf},
4};
5
6use anyhow::Context;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use taplo::formatter;
11use url::Url;
12
13use crate::{
14    environment::Environment,
15    util::{GlobRule, Normalize},
16    HashMap,
17};
18
19pub const CONFIG_FILE_NAMES: &[&str] = &[".taplo.toml", "taplo.toml"];
20
21#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
22#[serde(deny_unknown_fields)]
23pub struct Config {
24    /// Files to include.
25    ///
26    /// A list of Unix-like [glob](https://en.wikipedia.org/wiki/Glob_(programming)) path patterns.
27    /// Globstars (`**`) are supported.
28    ///
29    /// Relative paths are **not** relative to the configuration file, but rather
30    /// depends on the tool using the configuration.
31    ///
32    /// Omitting this property includes all files, **however an empty array will include none**.
33    pub include: Option<Vec<String>>,
34
35    /// Files to exclude (ignore).
36    ///
37    /// A list of Unix-like [glob](https://en.wikipedia.org/wiki/Glob_(programming)) path patterns.
38    /// Globstars (`**`) are supported.
39    ///
40    /// Relative paths are **not** relative to the configuration file, but rather
41    /// depends on the tool using the configuration.
42    ///
43    /// This has priority over `include`.
44    pub exclude: Option<Vec<String>>,
45
46    /// Rules are used to override configurations by path and keys.
47    #[serde(default)]
48    #[serde(skip_serializing_if = "Vec::is_empty")]
49    pub rule: Vec<Rule>,
50
51    #[serde(flatten)]
52    pub global_options: Options,
53
54    #[serde(skip)]
55    pub file_rule: Option<GlobRule>,
56
57    #[serde(default)]
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub plugins: Option<HashMap<String, Plugin>>,
60}
61
62impl Debug for Config {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("Config")
65            .field("include", &self.include)
66            .field("exclude", &self.exclude)
67            .field("rule", &self.rule)
68            .field("global_options", &self.global_options)
69            .finish()
70    }
71}
72
73impl Config {
74    /// Prepare the configuration for further use.
75    pub fn prepare(&mut self, e: &impl Environment, base: &Path) -> Result<(), anyhow::Error> {
76        self.make_absolute(e, base);
77
78        let default_include = String::from("**/*.toml");
79
80        self.file_rule = Some(GlobRule::new(
81            self.include
82                .as_deref()
83                .unwrap_or(&[default_include] as &[String]),
84            self.exclude.as_deref().unwrap_or(&[] as &[String]),
85        )?);
86
87        for rule in &mut self.rule {
88            rule.prepare(e, base).context("invalid rule")?;
89        }
90
91        self.global_options.prepare(e, base)?;
92
93        Ok(())
94    }
95
96    #[must_use]
97    pub fn is_included(&self, path: &Path) -> bool {
98        match &self.file_rule {
99            Some(r) => r.is_match(path),
100            None => {
101                tracing::debug!("no file matches were set up");
102                false
103            }
104        }
105    }
106
107    #[must_use]
108    pub fn rules_for<'r>(
109        &'r self,
110        path: &'r Path,
111    ) -> impl DoubleEndedIterator<Item = &'r Rule> + Clone + 'r {
112        self.rule.iter().filter(|r| r.is_included(path))
113    }
114
115    pub fn update_format_options(&self, path: &Path, options: &mut formatter::Options) {
116        if let Some(opts) = &self.global_options.formatting {
117            options.update(opts.clone());
118        }
119
120        for rule in self.rules_for(path) {
121            if rule.keys.is_none() {
122                if let Some(rule_opts) = rule.options.formatting.clone() {
123                    options.update(rule_opts);
124                }
125            }
126        }
127    }
128
129    pub fn format_scopes<'s>(
130        &'s self,
131        path: &'s Path,
132    ) -> impl Iterator<Item = (&'s String, taplo::formatter::OptionsIncomplete)> + Clone + 's {
133        self.rules_for(path)
134            .filter_map(|rule| match (&rule.keys, &rule.options.formatting) {
135                (Some(keys), Some(opts)) => Some(keys.iter().map(move |k| (k, opts.clone()))),
136                _ => None,
137            })
138            .flatten()
139    }
140
141    #[must_use]
142    pub fn is_schema_enabled(&self, path: &Path) -> bool {
143        let enabled = self
144            .global_options
145            .schema
146            .as_ref()
147            .and_then(|s| s.enabled)
148            .unwrap_or(true);
149
150        for rule in &self.rule {
151            let rule_matched = match &self.file_rule {
152                Some(r) => r.is_match(path),
153                None => {
154                    tracing::debug!("no file matches were set up");
155                    false
156                }
157            };
158
159            if !rule_matched {
160                continue;
161            }
162
163            let rule_schema_enabled = rule
164                .options
165                .schema
166                .as_ref()
167                .and_then(|s| s.enabled)
168                .unwrap_or(true);
169
170            if !rule_schema_enabled {
171                return false;
172            }
173        }
174
175        enabled
176    }
177
178    /// Transform all relative glob patterns to have the given base path.
179    fn make_absolute(&mut self, e: &impl Environment, base: &Path) {
180        if let Some(included) = &mut self.include {
181            for pat in included {
182                if !e.is_absolute(Path::new(pat)) {
183                    *pat = base
184                        .join(pat.as_str())
185                        .normalize()
186                        .to_string_lossy()
187                        .into_owned();
188                }
189            }
190        }
191
192        if let Some(excluded) = &mut self.exclude {
193            for pat in excluded {
194                if !e.is_absolute(Path::new(pat)) {
195                    *pat = base
196                        .join(pat.as_str())
197                        .normalize()
198                        .to_string_lossy()
199                        .into_owned();
200                }
201            }
202        }
203
204        for rule in &mut self.rule {
205            if let Some(included) = &mut rule.include {
206                for pat in included {
207                    if !e.is_absolute(Path::new(pat)) {
208                        *pat = base
209                            .join(pat.as_str())
210                            .normalize()
211                            .to_string_lossy()
212                            .into_owned();
213                    }
214                }
215            }
216
217            if let Some(excluded) = &mut rule.exclude {
218                for pat in excluded {
219                    if !e.is_absolute(Path::new(pat)) {
220                        *pat = base
221                            .join(pat.as_str())
222                            .normalize()
223                            .to_string_lossy()
224                            .into_owned();
225                    }
226                }
227            }
228        }
229    }
230}
231
232#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
233#[serde(deny_unknown_fields)]
234pub struct Options {
235    /// Schema validation options.
236    pub schema: Option<SchemaOptions>,
237    /// Formatting options.
238    pub formatting: Option<formatter::OptionsIncomplete>,
239}
240
241impl Options {
242    fn prepare(&mut self, e: &impl Environment, base: &Path) -> Result<(), anyhow::Error> {
243        if let Some(schema_opts) = &mut self.schema {
244            let url = match schema_opts.path.take() {
245                Some(p) => {
246                    if let Ok(url) = p.parse() {
247                        Some(url)
248                    } else {
249                        let p = if e.is_absolute(Path::new(&p)) {
250                            PathBuf::from(p)
251                        } else {
252                            base.join(p).normalize()
253                        };
254
255                        let s = p.to_string_lossy();
256
257                        Some(Url::parse(&format!("file://{s}")).context("invalid schema path")?)
258                    }
259                }
260                None => schema_opts.url.take(),
261            };
262
263            schema_opts.url = url;
264        }
265
266        Ok(())
267    }
268}
269
270/// A rule to override options by either name or file.
271#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
272#[serde(deny_unknown_fields)]
273pub struct Rule {
274    /// The name of the rule.
275    ///
276    /// Used in `taplo::<name>` comments.
277    pub name: Option<String>,
278
279    /// Files this rule is valid for.
280    ///
281    /// A list of Unix-like [glob](https://en.wikipedia.org/wiki/Glob_(programming)) path patterns.
282    ///
283    /// Relative paths are **not** relative to the configuration file, but rather
284    /// depends on the tool using the configuration.
285    ///
286    /// Omitting this property includes all files, **however an empty array will include none**.
287    pub include: Option<Vec<String>>,
288
289    /// Files that are excluded from this rule.
290    ///
291    /// A list of Unix-like [glob](https://en.wikipedia.org/wiki/Glob_(programming)) path patterns.
292    ///
293    /// Relative paths are **not** relative to the configuration file, but rather
294    /// depends on the tool using the configuration.
295    ///
296    /// This has priority over `include`.
297    pub exclude: Option<Vec<String>>,
298
299    /// Keys the rule is valid for in a document.
300    ///
301    /// A list of Unix-like [glob](https://en.wikipedia.org/wiki/Glob_(programming)) dotted key patterns.
302    ///
303    /// This allows enabling the rule for specific paths in the document.
304    ///
305    /// For example:
306    ///
307    /// - `package.metadata` will enable the rule for everything inside the `package.metadata` table, including itself.
308    ///
309    /// If omitted, the rule will always be valid for all keys.
310    pub keys: Option<Vec<String>>,
311
312    #[serde(flatten)]
313    pub options: Options,
314
315    #[serde(skip)]
316    pub file_rule: Option<GlobRule>,
317}
318
319impl Debug for Rule {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        f.debug_struct("Rule")
322            .field("name", &self.name)
323            .field("include", &self.include)
324            .field("exclude", &self.exclude)
325            .field("keys", &self.keys)
326            .field("options", &self.options)
327            .finish()
328    }
329}
330
331impl Rule {
332    pub fn prepare(&mut self, e: &impl Environment, base: &Path) -> Result<(), anyhow::Error> {
333        let default_include = String::from("**");
334        self.file_rule = Some(GlobRule::new(
335            self.include
336                .as_deref()
337                .unwrap_or(&[default_include] as &[String]),
338            self.exclude.as_deref().unwrap_or(&[] as &[String]),
339        )?);
340        self.options.prepare(e, base)?;
341        Ok(())
342    }
343
344    #[must_use]
345    pub fn is_included(&self, path: &Path) -> bool {
346        match &self.file_rule {
347            Some(r) => r.is_match(path),
348            None => true,
349        }
350    }
351}
352
353/// Options for schema validation and completion.
354///
355/// Schemas in rules with defined keys are ignored.
356#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
357#[serde(deny_unknown_fields)]
358pub struct SchemaOptions {
359    /// Whether the schema should be enabled or not.
360    ///
361    /// Defaults to true if omitted.
362    pub enabled: Option<bool>,
363
364    /// A local file path to the schema, overrides `url` if set.
365    ///
366    /// URLs are also accepted here, but it's not a guarantee and might
367    /// change in newer releases.
368    /// Please use the `url` field instead whenever possible.
369    pub path: Option<String>,
370
371    /// A full absolute URL to the schema.
372    ///
373    /// The url of the schema, supported schemes are `http`, `https`, `file` and `taplo`.
374    pub url: Option<Url>,
375}
376
377/// A plugin to extend Taplo's capabilities.
378#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
379pub struct Plugin {
380    /// Optional settings for the plugin.
381    #[serde(default)]
382    pub settings: Option<Value>,
383}