1use std::{
2 collections::HashSet,
3 path::{Path, PathBuf},
4 str::FromStr,
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 generator::{DenseLuaGenerator, LuaGenerator, ReadableLuaGenerator, TokenBasedLuaGenerator},
11 nodes::Block,
12 rules::{
13 bundle::{BundleRequireMode, Bundler},
14 get_default_rules, Rule,
15 },
16 utils::{deserialize_one_or_many, FilterPattern},
17 DarkluaError, Parser,
18};
19
20const DEFAULT_COLUMN_SPAN: usize = 80;
21
22fn get_default_column_span() -> usize {
23 DEFAULT_COLUMN_SPAN
24}
25
26#[derive(Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct Configuration {
30 #[serde(alias = "process", default = "get_default_rules")]
31 rules: Vec<Box<dyn Rule>>,
32 #[serde(default, deserialize_with = "crate::utils::string_or_struct")]
33 generator: GeneratorParameters,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 bundle: Option<BundleConfiguration>,
36 #[serde(default, skip)]
37 location: Option<PathBuf>,
38 #[serde(
39 default,
40 skip_serializing_if = "Vec::is_empty",
41 deserialize_with = "deserialize_one_or_many"
42 )]
43 apply_to_files: Vec<FilterPattern>,
44 #[serde(
45 default,
46 skip_serializing_if = "Vec::is_empty",
47 deserialize_with = "deserialize_one_or_many"
48 )]
49 skip_files: Vec<FilterPattern>,
50}
51
52impl Configuration {
53 pub fn empty() -> Self {
55 Self {
56 rules: Vec::new(),
57 generator: GeneratorParameters::default(),
58 bundle: None,
59 location: None,
60 apply_to_files: Vec::new(),
61 skip_files: Vec::new(),
62 }
63 }
64
65 #[inline]
67 pub fn with_generator(mut self, generator: GeneratorParameters) -> Self {
68 self.generator = generator;
69 self
70 }
71
72 #[inline]
74 pub fn set_generator(&mut self, generator: GeneratorParameters) {
75 self.generator = generator;
76 }
77
78 #[inline]
80 pub fn with_rule(mut self, rule: impl Into<Box<dyn Rule>>) -> Self {
81 self.push_rule(rule);
82 self
83 }
84
85 #[inline]
87 pub fn with_bundle_configuration(mut self, configuration: BundleConfiguration) -> Self {
88 self.bundle = Some(configuration);
89 self
90 }
91
92 #[inline]
94 pub fn with_location(mut self, location: impl Into<PathBuf>) -> Self {
95 self.location = Some(location.into());
96 self
97 }
98
99 #[inline]
101 pub fn push_rule(&mut self, rule: impl Into<Box<dyn Rule>>) {
102 self.rules.push(rule.into());
103 }
104
105 pub fn with_apply_to_filter(mut self, apply_to_files: &str) -> Result<Self, DarkluaError> {
108 self.push_apply_to_filter(apply_to_files)?;
109 Ok(self)
110 }
111
112 pub fn push_apply_to_filter(&mut self, apply_to_files: &str) -> Result<(), DarkluaError> {
115 let pattern = FilterPattern::new(apply_to_files.to_owned())?;
116 self.apply_to_files.push(pattern);
117 Ok(())
118 }
119
120 pub fn with_skip_filter(mut self, skip_files: &str) -> Result<Self, DarkluaError> {
123 self.push_skip_filter(skip_files)?;
124 Ok(self)
125 }
126
127 pub fn push_skip_filter(&mut self, skip_files: &str) -> Result<(), DarkluaError> {
130 let pattern = FilterPattern::new(skip_files.to_owned())?;
131 self.skip_files.push(pattern);
132 Ok(())
133 }
134
135 #[inline]
136 pub(crate) fn rules<'a, 'b: 'a>(&'b self) -> impl Iterator<Item = &'a dyn Rule> {
137 self.rules.iter().map(AsRef::as_ref)
138 }
139
140 #[inline]
141 pub(crate) fn build_parser(&self) -> Parser {
142 self.generator.build_parser()
143 }
144
145 #[inline]
146 pub(crate) fn generate_lua(&self, block: &Block, code: &str) -> String {
147 self.generator.generate_lua(block, code)
148 }
149
150 pub(crate) fn bundle(&self) -> Option<Bundler> {
151 if let Some(bundle_config) = self.bundle.as_ref() {
152 let bundler = Bundler::new(
153 self.build_parser(),
154 bundle_config.require_mode().clone(),
155 bundle_config.excludes(),
156 )
157 .with_modules_identifier(bundle_config.modules_identifier());
158 Some(bundler)
159 } else {
160 None
161 }
162 }
163
164 #[inline]
165 pub(crate) fn rules_len(&self) -> usize {
166 self.rules.len()
167 }
168
169 #[inline]
170 pub(crate) fn location(&self) -> Option<&Path> {
171 self.location.as_deref()
172 }
173
174 pub(crate) fn should_apply_rule(&self, path: &Path) -> bool {
175 if !self.apply_to_files.is_empty() && self.apply_to_files.iter().all(|f| !f.matches(path)) {
176 return false;
177 }
178
179 if !self.skip_files.is_empty() && self.skip_files.iter().any(|f| f.matches(path)) {
180 return false;
181 }
182
183 true
184 }
185}
186
187impl Default for Configuration {
188 fn default() -> Self {
189 Self {
190 rules: get_default_rules(),
191 generator: Default::default(),
192 bundle: None,
193 location: None,
194 apply_to_files: Vec::new(),
195 skip_files: Vec::new(),
196 }
197 }
198}
199
200impl std::fmt::Debug for Configuration {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.debug_struct("Config")
203 .field("generator", &self.generator)
204 .field(
205 "rules",
206 &self
207 .rules
208 .iter()
209 .map(|rule| {
210 json5::to_string(rule)
211 .ok()
212 .unwrap_or_else(|| rule.get_name().to_owned())
213 })
214 .collect::<Vec<_>>()
215 .join(", "),
216 )
217 .finish()
218 }
219}
220
221#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
226#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "name")]
227pub enum GeneratorParameters {
228 #[serde(alias = "retain-lines")]
230 #[default]
231 RetainLines,
232 Dense {
234 #[serde(default = "get_default_column_span")]
236 column_span: usize,
237 },
238 Readable {
240 #[serde(default = "get_default_column_span")]
242 column_span: usize,
243 },
244}
245
246impl GeneratorParameters {
247 pub fn default_dense() -> Self {
249 Self::Dense {
250 column_span: DEFAULT_COLUMN_SPAN,
251 }
252 }
253
254 pub fn default_readable() -> Self {
256 Self::Readable {
257 column_span: DEFAULT_COLUMN_SPAN,
258 }
259 }
260
261 fn generate_lua(&self, block: &Block, code: &str) -> String {
262 match self {
263 Self::RetainLines => {
264 let mut generator = TokenBasedLuaGenerator::new(code);
265 generator.write_block(block);
266 generator.into_string()
267 }
268 Self::Dense { column_span } => {
269 let mut generator = DenseLuaGenerator::new(*column_span);
270 generator.write_block(block);
271 generator.into_string()
272 }
273 Self::Readable { column_span } => {
274 let mut generator = ReadableLuaGenerator::new(*column_span);
275 generator.write_block(block);
276 generator.into_string()
277 }
278 }
279 }
280
281 fn build_parser(&self) -> Parser {
282 match self {
283 Self::RetainLines => Parser::default().preserve_tokens(),
284 Self::Dense { .. } | Self::Readable { .. } => Parser::default(),
285 }
286 }
287}
288
289impl FromStr for GeneratorParameters {
290 type Err = String;
291
292 fn from_str(s: &str) -> Result<Self, Self::Err> {
293 Ok(match s {
294 "retain_lines" | "retain-lines" => Self::RetainLines,
296 "dense" => Self::Dense {
297 column_span: DEFAULT_COLUMN_SPAN,
298 },
299 "readable" => Self::Readable {
300 column_span: DEFAULT_COLUMN_SPAN,
301 },
302 _ => return Err(format!("invalid generator name `{}`", s)),
303 })
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
312#[serde(deny_unknown_fields, rename_all = "snake_case")]
313pub struct BundleConfiguration {
314 #[serde(deserialize_with = "crate::utils::string_or_struct")]
315 require_mode: BundleRequireMode,
316 #[serde(skip_serializing_if = "Option::is_none")]
317 modules_identifier: Option<String>,
318 #[serde(default, skip_serializing_if = "HashSet::is_empty")]
319 excludes: HashSet<String>,
320}
321
322impl BundleConfiguration {
323 pub fn new(require_mode: impl Into<BundleRequireMode>) -> Self {
325 Self {
326 require_mode: require_mode.into(),
327 modules_identifier: None,
328 excludes: Default::default(),
329 }
330 }
331
332 pub fn with_modules_identifier(mut self, modules_identifier: impl Into<String>) -> Self {
334 self.modules_identifier = Some(modules_identifier.into());
335 self
336 }
337
338 pub fn with_exclude(mut self, exclude: impl Into<String>) -> Self {
340 self.excludes.insert(exclude.into());
341 self
342 }
343
344 pub(crate) fn require_mode(&self) -> &BundleRequireMode {
345 &self.require_mode
346 }
347
348 pub(crate) fn modules_identifier(&self) -> &str {
349 self.modules_identifier
350 .as_ref()
351 .map(AsRef::as_ref)
352 .unwrap_or("__DARKLUA_BUNDLE_MODULES")
353 }
354
355 pub(crate) fn excludes(&self) -> impl Iterator<Item = &str> {
356 self.excludes.iter().map(AsRef::as_ref)
357 }
358}
359
360#[cfg(test)]
361mod test {
362 use super::*;
363
364 mod generator_parameters {
365 use super::*;
366
367 #[test]
368 fn deserialize_retain_lines_params() {
369 let config: Configuration =
370 json5::from_str("{ generator: { name: 'retain_lines' } }").unwrap();
371
372 pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
373 }
374
375 #[test]
376 fn deserialize_retain_lines_params_deprecated() {
377 let config: Configuration =
378 json5::from_str("{ generator: { name: 'retain-lines' } }").unwrap();
379
380 pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
381 }
382
383 #[test]
384 fn deserialize_dense_params() {
385 let config: Configuration = json5::from_str("{ generator: { name: 'dense' }}").unwrap();
386
387 pretty_assertions::assert_eq!(
388 config.generator,
389 GeneratorParameters::Dense {
390 column_span: DEFAULT_COLUMN_SPAN
391 }
392 );
393 }
394
395 #[test]
396 fn deserialize_dense_params_with_column_span() {
397 let config: Configuration =
398 json5::from_str("{ generator: { name: 'dense', column_span: 110 } }").unwrap();
399
400 pretty_assertions::assert_eq!(
401 config.generator,
402 GeneratorParameters::Dense { column_span: 110 }
403 );
404 }
405
406 #[test]
407 fn deserialize_readable_params() {
408 let config: Configuration =
409 json5::from_str("{ generator: { name: 'readable' } }").unwrap();
410
411 pretty_assertions::assert_eq!(
412 config.generator,
413 GeneratorParameters::Readable {
414 column_span: DEFAULT_COLUMN_SPAN
415 }
416 );
417 }
418
419 #[test]
420 fn deserialize_readable_params_with_column_span() {
421 let config: Configuration =
422 json5::from_str("{ generator: { name: 'readable', column_span: 110 }}").unwrap();
423
424 pretty_assertions::assert_eq!(
425 config.generator,
426 GeneratorParameters::Readable { column_span: 110 }
427 );
428 }
429
430 #[test]
431 fn deserialize_retain_lines_params_as_string() {
432 let config: Configuration = json5::from_str("{generator: 'retain_lines'}").unwrap();
433
434 pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
435 }
436
437 #[test]
438 fn deserialize_dense_params_as_string() {
439 let config: Configuration = json5::from_str("{generator: 'dense'}").unwrap();
440
441 pretty_assertions::assert_eq!(
442 config.generator,
443 GeneratorParameters::Dense {
444 column_span: DEFAULT_COLUMN_SPAN
445 }
446 );
447 }
448
449 #[test]
450 fn deserialize_readable_params_as_string() {
451 let config: Configuration = json5::from_str("{generator: 'readable'}").unwrap();
452
453 pretty_assertions::assert_eq!(
454 config.generator,
455 GeneratorParameters::Readable {
456 column_span: DEFAULT_COLUMN_SPAN
457 }
458 );
459 }
460
461 #[test]
462 fn deserialize_unknown_generator_name() {
463 let result: Result<Configuration, _> = json5::from_str("{generator: 'oops'}");
464
465 insta::assert_snapshot!(
466 result.expect_err("deserialization should fail").to_string(),
467 @"invalid generator name `oops` at line 1 column 13"
468 );
469 }
470 }
471
472 mod bundle_configuration {
473 use crate::rules::require::PathRequireMode;
474
475 use super::*;
476
477 #[test]
478 fn deserialize_path_require_mode_as_string() {
479 let config: Configuration =
480 json5::from_str("{ bundle: { require_mode: 'path' } }").unwrap();
481
482 pretty_assertions::assert_eq!(
483 config.bundle.unwrap(),
484 BundleConfiguration::new(PathRequireMode::default())
485 );
486 }
487
488 #[test]
489 fn deserialize_path_require_mode_as_object() {
490 let config: Configuration =
491 json5::from_str("{bundle: { require_mode: { name: 'path' } } }").unwrap();
492
493 pretty_assertions::assert_eq!(
494 config.bundle.unwrap(),
495 BundleConfiguration::new(PathRequireMode::default())
496 );
497 }
498
499 #[test]
500 fn deserialize_path_require_mode_with_custom_module_folder_name() {
501 let config: Configuration = json5::from_str(
502 "{bundle: { require_mode: { name: 'path', module_folder_name: '__INIT__' } } }",
503 )
504 .unwrap();
505
506 pretty_assertions::assert_eq!(
507 config.bundle.unwrap(),
508 BundleConfiguration::new(PathRequireMode::new("__INIT__"))
509 );
510 }
511
512 #[test]
513 fn deserialize_path_require_mode_with_custom_module_identifier() {
514 let config: Configuration =
515 json5::from_str("{bundle: { require_mode: 'path', modules_identifier: '__M' } }")
516 .unwrap();
517
518 pretty_assertions::assert_eq!(
519 config.bundle.unwrap(),
520 BundleConfiguration::new(PathRequireMode::default()).with_modules_identifier("__M")
521 );
522 }
523
524 #[test]
525 fn deserialize_path_require_mode_with_custom_module_identifier_and_module_folder_name() {
526 let config: Configuration = json5::from_str(
527 "{bundle: { require_mode: { name: 'path', module_folder_name: '__INIT__' }, modules_identifier: '__M' } }",
528 )
529 .unwrap();
530
531 pretty_assertions::assert_eq!(
532 config.bundle.unwrap(),
533 BundleConfiguration::new(PathRequireMode::new("__INIT__"))
534 .with_modules_identifier("__M")
535 );
536 }
537
538 #[test]
539 fn deserialize_path_require_mode_with_excludes() {
540 let config: Configuration = json5::from_str(
541 "{bundle: { require_mode: { name: 'path' }, excludes: ['@lune', 'secrets'] } }",
542 )
543 .unwrap();
544
545 pretty_assertions::assert_eq!(
546 config.bundle.unwrap(),
547 BundleConfiguration::new(PathRequireMode::default())
548 .with_exclude("@lune")
549 .with_exclude("secrets")
550 );
551 }
552
553 #[test]
554 fn deserialize_unknown_require_mode_name() {
555 let result: Result<Configuration, _> =
556 json5::from_str("{bundle: { require_mode: 'oops' } }");
557
558 insta::assert_snapshot!(
559 result.expect_err("deserialization should fail").to_string(),
560 @"invalid require mode `oops` at line 1 column 26"
561 );
562 }
563 }
564}