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 pub include: Option<Vec<String>>,
34
35 pub exclude: Option<Vec<String>>,
45
46 #[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 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 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 pub schema: Option<SchemaOptions>,
237 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#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
272#[serde(deny_unknown_fields)]
273pub struct Rule {
274 pub name: Option<String>,
278
279 pub include: Option<Vec<String>>,
288
289 pub exclude: Option<Vec<String>>,
298
299 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#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
357#[serde(deny_unknown_fields)]
358pub struct SchemaOptions {
359 pub enabled: Option<bool>,
363
364 pub path: Option<String>,
370
371 pub url: Option<Url>,
375}
376
377#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
379pub struct Plugin {
380 #[serde(default)]
382 pub settings: Option<Value>,
383}