fallow_config/config/
mod.rs1mod duplicates_config;
2mod format;
3mod health;
4mod parsing;
5mod resolution;
6mod rules;
7
8pub use duplicates_config::{
9 DetectionMode, DuplicatesConfig, NormalizationConfig, ResolvedNormalization,
10};
11pub use format::OutputFormat;
12pub use health::HealthConfig;
13pub use resolution::{ConfigOverride, IgnoreExportRule, ResolvedConfig, ResolvedOverride};
14pub use rules::{PartialRulesConfig, RulesConfig, Severity};
15
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18
19use crate::external_plugin::ExternalPluginDef;
20use crate::workspace::WorkspaceConfig;
21
22#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)]
43#[serde(deny_unknown_fields, rename_all = "camelCase")]
44pub struct FallowConfig {
45 #[serde(rename = "$schema", default, skip_serializing)]
47 #[schemars(skip)]
48 pub schema: Option<String>,
49
50 #[serde(default, skip_serializing)]
55 pub extends: Vec<String>,
56
57 #[serde(default)]
59 pub entry: Vec<String>,
60
61 #[serde(default)]
63 pub ignore_patterns: Vec<String>,
64
65 #[serde(default)]
67 pub framework: Vec<ExternalPluginDef>,
68
69 #[serde(default)]
71 pub workspaces: Option<WorkspaceConfig>,
72
73 #[serde(default)]
75 pub ignore_dependencies: Vec<String>,
76
77 #[serde(default)]
79 pub ignore_exports: Vec<IgnoreExportRule>,
80
81 #[serde(default)]
83 pub duplicates: DuplicatesConfig,
84
85 #[serde(default)]
87 pub health: HealthConfig,
88
89 #[serde(default)]
91 pub rules: RulesConfig,
92
93 #[serde(default)]
95 pub production: bool,
96
97 #[serde(default)]
105 pub plugins: Vec<String>,
106
107 #[serde(default)]
109 pub overrides: Vec<ConfigOverride>,
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
119 fn default_config_has_empty_collections() {
120 let config = FallowConfig::default();
121 assert!(config.schema.is_none());
122 assert!(config.extends.is_empty());
123 assert!(config.entry.is_empty());
124 assert!(config.ignore_patterns.is_empty());
125 assert!(config.framework.is_empty());
126 assert!(config.workspaces.is_none());
127 assert!(config.ignore_dependencies.is_empty());
128 assert!(config.ignore_exports.is_empty());
129 assert!(config.plugins.is_empty());
130 assert!(config.overrides.is_empty());
131 assert!(!config.production);
132 }
133
134 #[test]
135 fn default_config_rules_are_error() {
136 let config = FallowConfig::default();
137 assert_eq!(config.rules.unused_files, Severity::Error);
138 assert_eq!(config.rules.unused_exports, Severity::Error);
139 assert_eq!(config.rules.unused_dependencies, Severity::Error);
140 }
141
142 #[test]
143 fn default_config_duplicates_enabled() {
144 let config = FallowConfig::default();
145 assert!(config.duplicates.enabled);
146 assert_eq!(config.duplicates.min_tokens, 50);
147 assert_eq!(config.duplicates.min_lines, 5);
148 }
149
150 #[test]
151 fn default_config_health_thresholds() {
152 let config = FallowConfig::default();
153 assert_eq!(config.health.max_cyclomatic, 20);
154 assert_eq!(config.health.max_cognitive, 15);
155 }
156
157 #[test]
160 fn deserialize_empty_json_object() {
161 let config: FallowConfig = serde_json::from_str("{}").unwrap();
162 assert!(config.entry.is_empty());
163 assert!(!config.production);
164 }
165
166 #[test]
167 fn deserialize_json_with_all_top_level_fields() {
168 let json = r#"{
169 "$schema": "https://fallow.dev/schema.json",
170 "entry": ["src/main.ts"],
171 "ignorePatterns": ["generated/**"],
172 "ignoreDependencies": ["postcss"],
173 "production": true,
174 "plugins": ["custom-plugin.toml"],
175 "rules": {"unused-files": "warn"},
176 "duplicates": {"enabled": false},
177 "health": {"maxCyclomatic": 30}
178 }"#;
179 let config: FallowConfig = serde_json::from_str(json).unwrap();
180 assert_eq!(
181 config.schema.as_deref(),
182 Some("https://fallow.dev/schema.json")
183 );
184 assert_eq!(config.entry, vec!["src/main.ts"]);
185 assert_eq!(config.ignore_patterns, vec!["generated/**"]);
186 assert_eq!(config.ignore_dependencies, vec!["postcss"]);
187 assert!(config.production);
188 assert_eq!(config.plugins, vec!["custom-plugin.toml"]);
189 assert_eq!(config.rules.unused_files, Severity::Warn);
190 assert!(!config.duplicates.enabled);
191 assert_eq!(config.health.max_cyclomatic, 30);
192 }
193
194 #[test]
195 fn deserialize_json_deny_unknown_fields() {
196 let json = r#"{"unknownField": true}"#;
197 let result: Result<FallowConfig, _> = serde_json::from_str(json);
198 assert!(result.is_err(), "unknown fields should be rejected");
199 }
200
201 #[test]
202 fn deserialize_json_production_mode_default_false() {
203 let config: FallowConfig = serde_json::from_str("{}").unwrap();
204 assert!(!config.production);
205 }
206
207 #[test]
208 fn deserialize_json_production_mode_true() {
209 let config: FallowConfig = serde_json::from_str(r#"{"production": true}"#).unwrap();
210 assert!(config.production);
211 }
212
213 #[test]
216 fn deserialize_toml_minimal() {
217 let toml_str = r#"
218entry = ["src/index.ts"]
219production = true
220"#;
221 let config: FallowConfig = toml::from_str(toml_str).unwrap();
222 assert_eq!(config.entry, vec!["src/index.ts"]);
223 assert!(config.production);
224 }
225
226 #[test]
227 fn deserialize_toml_with_inline_framework() {
228 let toml_str = r#"
229[[framework]]
230name = "my-framework"
231enablers = ["my-framework-pkg"]
232entryPoints = ["src/routes/**/*.tsx"]
233"#;
234 let config: FallowConfig = toml::from_str(toml_str).unwrap();
235 assert_eq!(config.framework.len(), 1);
236 assert_eq!(config.framework[0].name, "my-framework");
237 assert_eq!(config.framework[0].enablers, vec!["my-framework-pkg"]);
238 assert_eq!(
239 config.framework[0].entry_points,
240 vec!["src/routes/**/*.tsx"]
241 );
242 }
243
244 #[test]
245 fn deserialize_toml_with_workspace_config() {
246 let toml_str = r#"
247[workspaces]
248patterns = ["packages/*", "apps/*"]
249"#;
250 let config: FallowConfig = toml::from_str(toml_str).unwrap();
251 assert!(config.workspaces.is_some());
252 let ws = config.workspaces.unwrap();
253 assert_eq!(ws.patterns, vec!["packages/*", "apps/*"]);
254 }
255
256 #[test]
257 fn deserialize_toml_with_ignore_exports() {
258 let toml_str = r#"
259[[ignoreExports]]
260file = "src/types/**/*.ts"
261exports = ["*"]
262"#;
263 let config: FallowConfig = toml::from_str(toml_str).unwrap();
264 assert_eq!(config.ignore_exports.len(), 1);
265 assert_eq!(config.ignore_exports[0].file, "src/types/**/*.ts");
266 assert_eq!(config.ignore_exports[0].exports, vec!["*"]);
267 }
268
269 #[test]
270 fn deserialize_toml_deny_unknown_fields() {
271 let toml_str = r"bogus_field = true";
272 let result: Result<FallowConfig, _> = toml::from_str(toml_str);
273 assert!(result.is_err(), "unknown fields should be rejected");
274 }
275
276 #[test]
279 fn json_serialize_roundtrip() {
280 let config = FallowConfig {
281 entry: vec!["src/main.ts".to_string()],
282 production: true,
283 ..FallowConfig::default()
284 };
285 let json = serde_json::to_string(&config).unwrap();
286 let restored: FallowConfig = serde_json::from_str(&json).unwrap();
287 assert_eq!(restored.entry, vec!["src/main.ts"]);
288 assert!(restored.production);
289 }
290
291 #[test]
292 fn schema_field_not_serialized() {
293 let config = FallowConfig {
294 schema: Some("https://example.com/schema.json".to_string()),
295 ..FallowConfig::default()
296 };
297 let json = serde_json::to_string(&config).unwrap();
298 assert!(
300 !json.contains("$schema"),
301 "schema field should be skipped in serialization"
302 );
303 }
304
305 #[test]
306 fn extends_field_not_serialized() {
307 let config = FallowConfig {
308 extends: vec!["base.json".to_string()],
309 ..FallowConfig::default()
310 };
311 let json = serde_json::to_string(&config).unwrap();
312 assert!(
313 !json.contains("extends"),
314 "extends field should be skipped in serialization"
315 );
316 }
317}