1use serde::Deserialize;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5#[derive(Deserialize, Default, Debug)]
17#[serde(deny_unknown_fields)]
18pub struct Config {
19 #[serde(default)]
20 pub sqrust: SqrustConfig,
21 #[serde(default)]
22 pub rules: RulesConfig,
23}
24
25#[derive(Deserialize, Default, Debug)]
27#[serde(deny_unknown_fields)]
28pub struct SqrustConfig {
29 pub dialect: Option<String>,
32
33 #[serde(default)]
36 pub include: Vec<String>,
37
38 #[serde(default)]
41 pub exclude: Vec<String>,
42}
43
44#[derive(Deserialize, Default, Debug)]
46#[serde(deny_unknown_fields)]
47pub struct RulesConfig {
48 #[serde(default)]
60 pub disable: Vec<String>,
61
62 }
75
76impl Config {
77 pub fn load(start: &Path) -> Result<Self, String> {
81 if let Some(path) = find_config(start) {
82 let content = fs::read_to_string(&path)
83 .map_err(|e| format!("Cannot read {}: {}", path.display(), e))?;
84 toml::from_str(&content)
85 .map_err(|e| format!("Invalid sqrust.toml: {}", e))
86 } else {
87 Ok(Config::default())
88 }
89 }
90
91 pub fn rule_enabled(&self, name: &str) -> bool {
93 !self.rules.disable.iter().any(|d| d == name)
94 }
95}
96
97fn find_config(start: &Path) -> Option<PathBuf> {
99 let mut dir = if start.is_file() {
100 start.parent()?.to_path_buf()
101 } else {
102 start.to_path_buf()
103 };
104
105 loop {
106 let candidate = dir.join("sqrust.toml");
107 if candidate.exists() {
108 return Some(candidate);
109 }
110 match dir.parent() {
111 Some(parent) => dir = parent.to_path_buf(),
112 None => return None,
113 }
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn parse(toml: &str) -> Config {
122 toml::from_str(toml).expect("valid toml")
123 }
124
125 #[test]
126 fn empty_config_is_default() {
127 let cfg = parse("");
128 assert!(cfg.rules.disable.is_empty());
129 assert!(cfg.sqrust.exclude.is_empty());
130 }
131
132 #[test]
133 fn disable_list_parsed() {
134 let cfg = parse(r#"
135[rules]
136disable = ["Convention/SelectStar", "Layout/LongLines"]
137"#);
138 assert_eq!(cfg.rules.disable.len(), 2);
139 assert!(cfg.rules.disable.contains(&"Convention/SelectStar".to_string()));
140 }
141
142 #[test]
143 fn rule_enabled_respects_disable() {
144 let cfg = parse(r#"
145[rules]
146disable = ["Convention/SelectStar"]
147"#);
148 assert!(!cfg.rule_enabled("Convention/SelectStar"));
149 assert!(cfg.rule_enabled("Layout/LongLines"));
150 }
151
152 #[test]
153 fn exclude_patterns_parsed() {
154 let cfg = parse(r#"
155[sqrust]
156exclude = ["dbt_packages/**", "target/**"]
157"#);
158 assert_eq!(cfg.sqrust.exclude.len(), 2);
159 }
160
161 #[test]
162 fn dialect_parsed() {
163 let cfg = parse(r#"
164[sqrust]
165dialect = "bigquery"
166"#);
167 assert_eq!(cfg.sqrust.dialect.as_deref(), Some("bigquery"));
168 }
169
170 #[test]
171 fn unknown_field_rejected() {
172 let result: Result<Config, _> = toml::from_str(r#"
173[rules]
174select = ["Convention"]
175"#);
176 assert!(result.is_err(), "select is not yet supported and should be rejected");
177 }
178}