rfmt 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,403 @@
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ /// Complete configuration structure matching .rfmt.yml format
4
+ #[derive(Debug, Clone, Serialize, Deserialize)]
5
+ pub struct Config {
6
+ #[serde(default)]
7
+ pub version: String,
8
+
9
+ #[serde(default)]
10
+ pub parser: ParserConfig,
11
+
12
+ #[serde(default)]
13
+ pub formatting: FormattingConfig,
14
+
15
+ #[serde(default)]
16
+ pub include: Vec<String>,
17
+
18
+ #[serde(default)]
19
+ pub exclude: Vec<String>,
20
+ }
21
+
22
+ #[derive(Debug, Clone, Serialize, Deserialize)]
23
+ pub struct ParserConfig {
24
+ pub version: String,
25
+ pub error_tolerance: bool,
26
+ pub encoding: String,
27
+ }
28
+
29
+ #[derive(Debug, Clone, Serialize, Deserialize)]
30
+ pub struct FormattingConfig {
31
+ #[serde(default = "default_line_length")]
32
+ pub line_length: usize,
33
+
34
+ #[serde(rename = "indent_style", default)]
35
+ pub indent_style: IndentStyle,
36
+
37
+ #[serde(rename = "indent_width", default = "default_indent_width")]
38
+ pub indent_width: usize,
39
+
40
+ #[serde(rename = "quote_style", default)]
41
+ pub quote_style: QuoteStyle,
42
+
43
+ #[serde(default)]
44
+ pub style: StyleConfig,
45
+ }
46
+
47
+ fn default_line_length() -> usize {
48
+ 100
49
+ }
50
+
51
+ fn default_indent_width() -> usize {
52
+ 2
53
+ }
54
+
55
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
56
+ #[serde(rename_all = "lowercase")]
57
+ pub enum IndentStyle {
58
+ #[default]
59
+ Spaces,
60
+ Tabs,
61
+ }
62
+
63
+ #[derive(Debug, Clone, Serialize, Deserialize)]
64
+ pub struct StyleConfig {
65
+ #[serde(default)]
66
+ pub quotes: QuoteStyle,
67
+
68
+ #[serde(default)]
69
+ pub hash_syntax: HashSyntax,
70
+
71
+ #[serde(default)]
72
+ pub trailing_comma: TrailingComma,
73
+ }
74
+
75
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
76
+ #[serde(rename_all = "lowercase")]
77
+ pub enum QuoteStyle {
78
+ #[default]
79
+ Double,
80
+ Single,
81
+ Consistent,
82
+ }
83
+
84
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
85
+ pub enum HashSyntax {
86
+ #[default]
87
+ Ruby19,
88
+ HashRockets,
89
+ Consistent,
90
+ }
91
+
92
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
93
+ #[serde(rename_all = "lowercase")]
94
+ pub enum TrailingComma {
95
+ Always,
96
+ Never,
97
+ #[default]
98
+ Multiline,
99
+ }
100
+
101
+ impl Config {
102
+ /// Load configuration from a YAML file
103
+ #[cfg(test)]
104
+ pub fn load_file(path: &std::path::Path) -> crate::error::Result<Self> {
105
+ use crate::error::RfmtError;
106
+
107
+ let contents = std::fs::read_to_string(path).map_err(|e| RfmtError::ConfigError {
108
+ message: format!("Failed to read config file: {}", e),
109
+ })?;
110
+
111
+ let config: Config =
112
+ serde_yaml::from_str(&contents).map_err(|e| RfmtError::ConfigError {
113
+ message: format!("Failed to parse config file: {}", e),
114
+ })?;
115
+
116
+ config.validate()?;
117
+
118
+ Ok(config)
119
+ }
120
+
121
+ /// Validate configuration values
122
+ #[cfg(test)]
123
+ fn validate(&self) -> crate::error::Result<()> {
124
+ use crate::error::RfmtError;
125
+
126
+ if self.formatting.line_length < 40 || self.formatting.line_length > 500 {
127
+ return Err(RfmtError::ConfigError {
128
+ message: format!(
129
+ "line_length must be between 40 and 500, got {}",
130
+ self.formatting.line_length
131
+ ),
132
+ });
133
+ }
134
+
135
+ if self.formatting.indent_width < 1 || self.formatting.indent_width > 8 {
136
+ return Err(RfmtError::ConfigError {
137
+ message: format!(
138
+ "indent_width must be between 1 and 8, got {}",
139
+ self.formatting.indent_width
140
+ ),
141
+ });
142
+ }
143
+
144
+ Ok(())
145
+ }
146
+
147
+ /// Get the indent string based on configuration
148
+ #[cfg(test)]
149
+ pub fn indent_string(&self) -> String {
150
+ match self.formatting.indent_style {
151
+ IndentStyle::Spaces => " ".repeat(self.formatting.indent_width),
152
+ IndentStyle::Tabs => "\t".to_string(),
153
+ }
154
+ }
155
+
156
+ /// Check if a file path should be included based on include/exclude patterns
157
+ #[cfg(test)]
158
+ pub fn should_include(&self, path: &std::path::Path) -> bool {
159
+ use globset::{Glob, GlobSetBuilder};
160
+
161
+ let path_str = path.to_string_lossy();
162
+
163
+ // Check exclude patterns first
164
+ let mut exclude_builder = GlobSetBuilder::new();
165
+ for pattern in &self.exclude {
166
+ if let Ok(glob) = Glob::new(pattern) {
167
+ exclude_builder.add(glob);
168
+ }
169
+ }
170
+
171
+ if let Ok(exclude_set) = exclude_builder.build() {
172
+ if exclude_set.is_match(&*path_str) {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ // Check include patterns
178
+ let mut include_builder = GlobSetBuilder::new();
179
+ for pattern in &self.include {
180
+ if let Ok(glob) = Glob::new(pattern) {
181
+ include_builder.add(glob);
182
+ }
183
+ }
184
+
185
+ if let Ok(include_set) = include_builder.build() {
186
+ return include_set.is_match(&*path_str);
187
+ }
188
+
189
+ false
190
+ }
191
+ }
192
+
193
+ impl Default for Config {
194
+ fn default() -> Self {
195
+ Self {
196
+ version: "1.0".to_string(),
197
+ parser: ParserConfig::default(),
198
+ formatting: FormattingConfig::default(),
199
+ include: vec!["**/*.rb".to_string(), "**/*.rake".to_string()],
200
+ exclude: vec![
201
+ "vendor/**/*".to_string(),
202
+ "tmp/**/*".to_string(),
203
+ "node_modules/**/*".to_string(),
204
+ ],
205
+ }
206
+ }
207
+ }
208
+
209
+ impl Default for ParserConfig {
210
+ fn default() -> Self {
211
+ Self {
212
+ version: "latest".to_string(),
213
+ error_tolerance: true,
214
+ encoding: "UTF-8".to_string(),
215
+ }
216
+ }
217
+ }
218
+
219
+ impl Default for FormattingConfig {
220
+ fn default() -> Self {
221
+ Self {
222
+ line_length: 100,
223
+ indent_style: IndentStyle::Spaces,
224
+ indent_width: 2,
225
+ quote_style: QuoteStyle::Double,
226
+ style: StyleConfig::default(),
227
+ }
228
+ }
229
+ }
230
+
231
+ impl Default for StyleConfig {
232
+ fn default() -> Self {
233
+ Self {
234
+ quotes: QuoteStyle::Double,
235
+ hash_syntax: HashSyntax::Ruby19,
236
+ trailing_comma: TrailingComma::Multiline,
237
+ }
238
+ }
239
+ }
240
+
241
+ #[cfg(test)]
242
+ mod tests {
243
+ use super::*;
244
+ use crate::error::RfmtError;
245
+ use std::io::Write;
246
+ use std::path::Path;
247
+ use tempfile::NamedTempFile;
248
+
249
+ #[test]
250
+ fn test_default_config() {
251
+ let config = Config::default();
252
+ assert_eq!(config.version, "1.0");
253
+ assert_eq!(config.formatting.line_length, 100);
254
+ assert_eq!(config.formatting.indent_width, 2);
255
+ assert!(matches!(
256
+ config.formatting.indent_style,
257
+ IndentStyle::Spaces
258
+ ));
259
+ }
260
+
261
+ #[test]
262
+ fn test_load_valid_config() {
263
+ let yaml = r#"
264
+ version: "1.0"
265
+ formatting:
266
+ line_length: 120
267
+ indent_width: 4
268
+ indent_style: tabs
269
+ quote_style: single
270
+ include:
271
+ - "**/*.rb"
272
+ exclude:
273
+ - "vendor/**/*"
274
+ "#;
275
+
276
+ let mut file = NamedTempFile::new().unwrap();
277
+ file.write_all(yaml.as_bytes()).unwrap();
278
+ file.flush().unwrap();
279
+
280
+ let config = Config::load_file(file.path()).unwrap();
281
+ assert_eq!(config.formatting.line_length, 120);
282
+ assert_eq!(config.formatting.indent_width, 4);
283
+ assert!(matches!(config.formatting.indent_style, IndentStyle::Tabs));
284
+ assert!(matches!(config.formatting.quote_style, QuoteStyle::Single));
285
+ }
286
+
287
+ #[test]
288
+ fn test_validate_line_length_too_small() {
289
+ let yaml = r#"
290
+ formatting:
291
+ line_length: 30
292
+ "#;
293
+
294
+ let mut file = NamedTempFile::new().unwrap();
295
+ file.write_all(yaml.as_bytes()).unwrap();
296
+ file.flush().unwrap();
297
+
298
+ let result = Config::load_file(file.path());
299
+ assert!(result.is_err());
300
+ if let Err(RfmtError::ConfigError { message, .. }) = result {
301
+ assert!(message.contains("line_length"));
302
+ assert!(message.contains("40 and 500"));
303
+ }
304
+ }
305
+
306
+ #[test]
307
+ fn test_validate_line_length_too_large() {
308
+ let yaml = r#"
309
+ formatting:
310
+ line_length: 600
311
+ "#;
312
+
313
+ let mut file = NamedTempFile::new().unwrap();
314
+ file.write_all(yaml.as_bytes()).unwrap();
315
+ file.flush().unwrap();
316
+
317
+ let result = Config::load_file(file.path());
318
+ assert!(result.is_err());
319
+ }
320
+
321
+ #[test]
322
+ fn test_validate_indent_width() {
323
+ let yaml = r#"
324
+ formatting:
325
+ indent_width: 10
326
+ "#;
327
+
328
+ let mut file = NamedTempFile::new().unwrap();
329
+ file.write_all(yaml.as_bytes()).unwrap();
330
+ file.flush().unwrap();
331
+
332
+ let result = Config::load_file(file.path());
333
+ assert!(result.is_err());
334
+ if let Err(RfmtError::ConfigError { message, .. }) = result {
335
+ assert!(message.contains("indent_width"));
336
+ }
337
+ }
338
+
339
+ #[test]
340
+ fn test_indent_string_spaces() {
341
+ let config = Config::default();
342
+ assert_eq!(config.indent_string(), " "); // 2 spaces
343
+ }
344
+
345
+ #[test]
346
+ fn test_indent_string_tabs() {
347
+ let mut config = Config::default();
348
+ config.formatting.indent_style = IndentStyle::Tabs;
349
+ assert_eq!(config.indent_string(), "\t");
350
+ }
351
+
352
+ #[test]
353
+ fn test_should_include_basic() {
354
+ let config = Config::default();
355
+ assert!(config.should_include(Path::new("lib/foo.rb")));
356
+ assert!(!config.should_include(Path::new("vendor/gem/foo.rb")));
357
+ }
358
+
359
+ #[test]
360
+ fn test_should_include_with_exclude() {
361
+ let mut config = Config::default();
362
+ config.exclude.push("test/**/*".to_string());
363
+ assert!(!config.should_include(Path::new("test/foo.rb")));
364
+ }
365
+
366
+ #[test]
367
+ fn test_invalid_yaml_syntax() {
368
+ let yaml = r#"
369
+ formatting:
370
+ line_length: not_a_number
371
+ "#;
372
+
373
+ let mut file = NamedTempFile::new().unwrap();
374
+ file.write_all(yaml.as_bytes()).unwrap();
375
+ file.flush().unwrap();
376
+
377
+ let result = Config::load_file(file.path());
378
+ assert!(result.is_err());
379
+ if let Err(RfmtError::ConfigError { message, .. }) = result {
380
+ assert!(message.contains("parse"));
381
+ }
382
+ }
383
+
384
+ #[test]
385
+ fn test_partial_config_uses_defaults() {
386
+ let yaml = r#"
387
+ formatting:
388
+ line_length: 80
389
+ "#;
390
+
391
+ let mut file = NamedTempFile::new().unwrap();
392
+ file.write_all(yaml.as_bytes()).unwrap();
393
+ file.flush().unwrap();
394
+
395
+ let config = Config::load_file(file.path()).unwrap();
396
+ assert_eq!(config.formatting.line_length, 80);
397
+ assert_eq!(config.formatting.indent_width, 2); // default
398
+ assert!(matches!(
399
+ config.formatting.indent_style,
400
+ IndentStyle::Spaces
401
+ )); // default
402
+ }
403
+ }