1use std::{
2 collections::HashSet,
3 path::{Path, PathBuf},
4 str::FromStr,
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::{
10 frontend::Loader,
11 generator::{DenseLuaGenerator, LuaGenerator, ReadableLuaGenerator, TokenBasedLuaGenerator},
12 nodes::Block,
13 rules::{
14 bundle::{BundleRequireMode, Bundler},
15 get_default_rules, Rule,
16 },
17 utils::{deserialize_one_or_many, deserialize_vec_of_pairs, FilterPattern},
18 DarkluaError, Parser,
19};
20
21const DEFAULT_COLUMN_SPAN: usize = 80;
22
23fn get_default_column_span() -> usize {
24 DEFAULT_COLUMN_SPAN
25}
26
27#[derive(Serialize, Deserialize)]
29#[serde(deny_unknown_fields)]
30pub struct Configuration {
31 #[serde(alias = "process", default = "get_default_rules")]
32 rules: Vec<Box<dyn Rule>>,
33 #[serde(default, deserialize_with = "crate::utils::string_or_struct")]
34 generator: GeneratorParameters,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 bundle: Option<BundleConfiguration>,
37 #[serde(default, skip)]
38 location: Option<PathBuf>,
39 #[serde(
40 default,
41 skip_serializing_if = "Vec::is_empty",
42 deserialize_with = "deserialize_one_or_many"
43 )]
44 apply_to_files: Vec<FilterPattern>,
45 #[serde(
46 default,
47 skip_serializing_if = "Vec::is_empty",
48 deserialize_with = "deserialize_one_or_many"
49 )]
50 skip_files: Vec<FilterPattern>,
51 #[serde(flatten)]
52 loaders: LoaderConfiguration,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 lua_extension: Option<LuaExtension>,
55}
56
57impl Configuration {
58 pub fn empty() -> Self {
60 Self {
61 rules: Vec::new(),
62 generator: GeneratorParameters::default(),
63 bundle: None,
64 location: None,
65 apply_to_files: Vec::new(),
66 skip_files: Vec::new(),
67 loaders: Default::default(),
68 lua_extension: None,
69 }
70 }
71
72 #[inline]
74 pub fn with_generator(mut self, generator: GeneratorParameters) -> Self {
75 self.generator = generator;
76 self
77 }
78
79 #[inline]
81 pub fn set_generator(&mut self, generator: GeneratorParameters) {
82 self.generator = generator;
83 }
84
85 #[inline]
87 pub fn with_rule(mut self, rule: impl Into<Box<dyn Rule>>) -> Self {
88 self.push_rule(rule);
89 self
90 }
91
92 #[inline]
94 pub fn with_bundle_configuration(mut self, configuration: BundleConfiguration) -> Self {
95 self.bundle = Some(configuration);
96 self
97 }
98
99 #[inline]
101 pub fn with_location(mut self, location: impl Into<PathBuf>) -> Self {
102 self.location = Some(location.into());
103 self
104 }
105
106 #[inline]
108 pub fn push_rule(&mut self, rule: impl Into<Box<dyn Rule>>) {
109 self.rules.push(rule.into());
110 }
111
112 pub fn with_apply_to_filter(mut self, apply_to_files: &str) -> Result<Self, DarkluaError> {
115 self.push_apply_to_filter(apply_to_files)?;
116 Ok(self)
117 }
118
119 pub fn push_apply_to_filter(&mut self, apply_to_files: &str) -> Result<(), DarkluaError> {
122 let pattern = FilterPattern::new(apply_to_files.to_owned())?;
123 self.apply_to_files.push(pattern);
124 Ok(())
125 }
126
127 pub fn with_skip_filter(mut self, skip_files: &str) -> Result<Self, DarkluaError> {
130 self.push_skip_filter(skip_files)?;
131 Ok(self)
132 }
133
134 pub fn push_skip_filter(&mut self, skip_files: &str) -> Result<(), DarkluaError> {
137 let pattern = FilterPattern::new(skip_files.to_owned())?;
138 self.skip_files.push(pattern);
139 Ok(())
140 }
141
142 pub fn with_loader(mut self, loader: Loader, pattern: &str) -> Result<Self, DarkluaError> {
144 self.add_loader(loader, pattern)?;
145 Ok(self)
146 }
147
148 pub fn add_loader(&mut self, loader: Loader, pattern: &str) -> Result<(), DarkluaError> {
150 self.loaders
151 .loaders
152 .push((FilterPattern::new(pattern.to_owned())?, loader));
153 Ok(())
154 }
155
156 pub fn clear_loaders(&mut self) {
158 self.loaders.loaders.clear();
159 }
160
161 #[inline]
162 pub(crate) fn rules<'a, 'b: 'a>(&'b self) -> impl Iterator<Item = &'a dyn Rule> {
163 self.rules.iter().map(AsRef::as_ref)
164 }
165
166 #[inline]
167 pub(crate) fn build_parser(&self) -> Parser {
168 self.generator.build_parser()
169 }
170
171 #[inline]
172 pub(crate) fn generate_lua(&self, block: &Block, code: &str) -> String {
173 self.generator.generate_lua(block, code)
174 }
175
176 pub(crate) fn bundle(&self) -> Option<Bundler> {
177 if let Some(bundle_config) = self.bundle.as_ref() {
178 let bundler = Bundler::new(
179 self.build_parser(),
180 bundle_config.require_mode().clone(),
181 bundle_config.excludes(),
182 self.loaders.clone(),
183 )
184 .with_modules_identifier(bundle_config.modules_identifier());
185 Some(bundler)
186 } else {
187 None
188 }
189 }
190
191 #[inline]
192 pub(crate) fn rules_len(&self) -> usize {
193 self.rules.len()
194 }
195
196 #[inline]
197 pub(crate) fn location(&self) -> Option<&Path> {
198 self.location.as_deref()
199 }
200
201 pub(crate) fn should_apply_rule(&self, path: &Path) -> bool {
202 if !self.apply_to_files.is_empty() && self.apply_to_files.iter().all(|f| !f.matches(path)) {
203 return false;
204 }
205
206 if !self.skip_files.is_empty() && self.skip_files.iter().any(|f| f.matches(path)) {
207 return false;
208 }
209
210 true
211 }
212
213 pub(crate) fn loaders(&self) -> &LoaderConfiguration {
214 &self.loaders
215 }
216
217 pub(crate) fn preferred_lua_extension(&self) -> &'static str {
218 self.lua_extension
219 .as_ref()
220 .unwrap_or(&Default::default())
221 .as_str()
222 }
223}
224
225impl Default for Configuration {
226 fn default() -> Self {
227 Self {
228 rules: get_default_rules(),
229 generator: Default::default(),
230 bundle: None,
231 location: None,
232 apply_to_files: Vec::new(),
233 skip_files: Vec::new(),
234 loaders: Default::default(),
235 lua_extension: None,
236 }
237 }
238}
239
240impl std::fmt::Debug for Configuration {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 f.debug_struct("Config")
243 .field("location", &self.location)
244 .field("generator", &self.generator)
245 .field("apply_to_files", &self.apply_to_files)
246 .field("skip_files", &self.skip_files)
247 .field("loaders", &self.loaders)
248 .field("bundle", &self.bundle)
249 .field(
250 "rules",
251 &self
252 .rules
253 .iter()
254 .map(|rule| {
255 json5::to_string(rule)
256 .ok()
257 .unwrap_or_else(|| rule.get_name().to_owned())
258 })
259 .collect::<Vec<_>>()
260 .join(", "),
261 )
262 .finish()
263 }
264}
265
266#[derive(Debug, Clone, Default, Serialize, Deserialize)]
267#[serde(rename_all = "snake_case")]
268pub(crate) struct LoaderConfiguration {
269 #[serde(
270 default,
271 skip_serializing_if = "Vec::is_empty",
272 deserialize_with = "deserialize_vec_of_pairs"
273 )]
274 loaders: Vec<(FilterPattern, Loader)>,
275}
276
277impl LoaderConfiguration {
278 pub(crate) fn get_loader(&self, path: &Path) -> Loader {
279 self.loaders
280 .iter()
281 .find(|(pattern, _)| pattern.matches(path))
282 .map(|(_, loader)| *loader)
283 .or_else(|| Loader::from_path(path))
284 .unwrap_or(Loader::Skip)
285 }
286}
287
288#[derive(Debug, Clone, Default, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub(crate) enum LuaExtension {
291 #[default]
292 Lua,
293 Luau,
294}
295
296impl LuaExtension {
297 const fn as_str(&self) -> &'static str {
298 match self {
299 Self::Lua => "lua",
300 Self::Luau => "luau",
301 }
302 }
303}
304
305#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
310#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "name")]
311pub enum GeneratorParameters {
312 #[serde(alias = "retain-lines")]
314 #[default]
315 RetainLines,
316 Dense {
318 #[serde(
320 default = "get_default_column_span",
321 deserialize_with = "crate::utils::deserialize_usize_from_float"
322 )]
323 column_span: usize,
324 },
325 Readable {
327 #[serde(
329 default = "get_default_column_span",
330 deserialize_with = "crate::utils::deserialize_usize_from_float"
331 )]
332 column_span: usize,
333 },
334}
335
336impl GeneratorParameters {
337 pub fn default_dense() -> Self {
339 Self::Dense {
340 column_span: DEFAULT_COLUMN_SPAN,
341 }
342 }
343
344 pub fn default_readable() -> Self {
346 Self::Readable {
347 column_span: DEFAULT_COLUMN_SPAN,
348 }
349 }
350
351 fn generate_lua(&self, block: &Block, code: &str) -> String {
352 match self {
353 Self::RetainLines => {
354 let mut generator = TokenBasedLuaGenerator::new(code);
355 generator.write_block(block);
356 generator.into_string()
357 }
358 Self::Dense { column_span } => {
359 let mut generator = DenseLuaGenerator::new(*column_span);
360 generator.write_block(block);
361 generator.into_string()
362 }
363 Self::Readable { column_span } => {
364 let mut generator = ReadableLuaGenerator::new(*column_span);
365 generator.write_block(block);
366 generator.into_string()
367 }
368 }
369 }
370
371 fn build_parser(&self) -> Parser {
372 match self {
373 Self::RetainLines => Parser::default().preserve_tokens(),
374 Self::Dense { .. } | Self::Readable { .. } => Parser::default(),
375 }
376 }
377}
378
379impl FromStr for GeneratorParameters {
380 type Err = String;
381
382 fn from_str(s: &str) -> Result<Self, Self::Err> {
383 Ok(match s {
384 "retain_lines" | "retain-lines" => Self::RetainLines,
386 "dense" => Self::Dense {
387 column_span: DEFAULT_COLUMN_SPAN,
388 },
389 "readable" => Self::Readable {
390 column_span: DEFAULT_COLUMN_SPAN,
391 },
392 _ => return Err(format!("invalid generator name `{}`", s)),
393 })
394 }
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
402#[serde(deny_unknown_fields, rename_all = "snake_case")]
403pub struct BundleConfiguration {
404 #[serde(deserialize_with = "crate::utils::string_or_struct")]
405 require_mode: BundleRequireMode,
406 #[serde(skip_serializing_if = "Option::is_none")]
407 modules_identifier: Option<String>,
408 #[serde(default, skip_serializing_if = "HashSet::is_empty")]
409 excludes: HashSet<String>,
410}
411
412impl BundleConfiguration {
413 pub fn new(require_mode: impl Into<BundleRequireMode>) -> Self {
415 Self {
416 require_mode: require_mode.into(),
417 modules_identifier: None,
418 excludes: Default::default(),
419 }
420 }
421
422 pub fn with_modules_identifier(mut self, modules_identifier: impl Into<String>) -> Self {
424 self.modules_identifier = Some(modules_identifier.into());
425 self
426 }
427
428 pub fn with_exclude(mut self, exclude: impl Into<String>) -> Self {
430 self.excludes.insert(exclude.into());
431 self
432 }
433
434 pub(crate) fn require_mode(&self) -> &BundleRequireMode {
435 &self.require_mode
436 }
437
438 pub(crate) fn modules_identifier(&self) -> &str {
439 self.modules_identifier
440 .as_ref()
441 .map(AsRef::as_ref)
442 .unwrap_or("__DARKLUA_BUNDLE_MODULES")
443 }
444
445 pub(crate) fn excludes(&self) -> impl Iterator<Item = &str> {
446 self.excludes.iter().map(AsRef::as_ref)
447 }
448}
449
450#[cfg(test)]
451mod test {
452 use super::*;
453
454 mod generator_parameters {
455 use super::*;
456
457 #[test]
458 fn deserialize_retain_lines_params() {
459 let config: Configuration =
460 json5::from_str("{ generator: { name: 'retain_lines' } }").unwrap();
461
462 pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
463 }
464
465 #[test]
466 fn deserialize_retain_lines_params_deprecated() {
467 let config: Configuration =
468 json5::from_str("{ generator: { name: 'retain-lines' } }").unwrap();
469
470 pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
471 }
472
473 #[test]
474 fn deserialize_dense_params() {
475 let config: Configuration = json5::from_str("{ generator: { name: 'dense' }}").unwrap();
476
477 pretty_assertions::assert_eq!(
478 config.generator,
479 GeneratorParameters::Dense {
480 column_span: DEFAULT_COLUMN_SPAN
481 }
482 );
483 }
484
485 #[test]
486 fn deserialize_dense_params_with_column_span() {
487 let config: Configuration =
488 json5::from_str("{ generator: { name: 'dense', column_span: 110 } }").unwrap();
489
490 pretty_assertions::assert_eq!(
491 config.generator,
492 GeneratorParameters::Dense { column_span: 110 }
493 );
494 }
495
496 #[test]
497 fn deserialize_dense_params_with_float_column_span() {
498 let config: Configuration =
499 json5::from_str("{ generator: { name: 'dense', column_span: 120.0 } }").unwrap();
500
501 pretty_assertions::assert_eq!(
502 config.generator,
503 GeneratorParameters::Dense { column_span: 120 }
504 );
505 }
506
507 #[test]
508 fn deserialize_dense_params_with_infinite_column_span() {
509 let config: Configuration =
510 json5::from_str("{ generator: { name: 'dense', column_span: Infinity } }").unwrap();
511
512 pretty_assertions::assert_eq!(
513 config.generator,
514 GeneratorParameters::Dense {
515 column_span: usize::MAX
516 }
517 );
518 }
519
520 #[test]
521 fn deserialize_readable_params() {
522 let config: Configuration =
523 json5::from_str("{ generator: { name: 'readable' } }").unwrap();
524
525 pretty_assertions::assert_eq!(
526 config.generator,
527 GeneratorParameters::Readable {
528 column_span: DEFAULT_COLUMN_SPAN
529 }
530 );
531 }
532
533 #[test]
534 fn deserialize_readable_params_with_column_span() {
535 let config: Configuration =
536 json5::from_str("{ generator: { name: 'readable', column_span: 110 }}").unwrap();
537
538 pretty_assertions::assert_eq!(
539 config.generator,
540 GeneratorParameters::Readable { column_span: 110 }
541 );
542 }
543
544 #[test]
545 fn deserialize_readable_params_with_float_column_span() {
546 let config: Configuration =
547 json5::from_str("{ generator: { name: 'readable', column_span: 120.6 } }").unwrap();
548
549 pretty_assertions::assert_eq!(
550 config.generator,
551 GeneratorParameters::Readable { column_span: 120 }
552 );
553 }
554
555 #[test]
556 fn deserialize_readable_params_with_infinite_column_span() {
557 let config: Configuration =
558 json5::from_str("{ generator: { name: 'readable', column_span: Infinity } }")
559 .unwrap();
560
561 pretty_assertions::assert_eq!(
562 config.generator,
563 GeneratorParameters::Readable {
564 column_span: usize::MAX
565 }
566 );
567 }
568 #[test]
569 fn deserialize_retain_lines_params_as_string() {
570 let config: Configuration = json5::from_str("{generator: 'retain_lines'}").unwrap();
571
572 pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
573 }
574
575 #[test]
576 fn deserialize_dense_params_as_string() {
577 let config: Configuration = json5::from_str("{generator: 'dense'}").unwrap();
578
579 pretty_assertions::assert_eq!(
580 config.generator,
581 GeneratorParameters::Dense {
582 column_span: DEFAULT_COLUMN_SPAN
583 }
584 );
585 }
586
587 #[test]
588 fn deserialize_readable_params_as_string() {
589 let config: Configuration = json5::from_str("{generator: 'readable'}").unwrap();
590
591 pretty_assertions::assert_eq!(
592 config.generator,
593 GeneratorParameters::Readable {
594 column_span: DEFAULT_COLUMN_SPAN
595 }
596 );
597 }
598
599 #[test]
600 fn deserialize_unknown_generator_name() {
601 let result: Result<Configuration, _> = json5::from_str("{generator: 'oops'}");
602
603 insta::assert_snapshot!(
604 result.expect_err("deserialization should fail").to_string(),
605 @"invalid generator name `oops` at line 1 column 13"
606 );
607 }
608 }
609
610 mod bundle_configuration {
611 use crate::rules::require::PathRequireMode;
612
613 use super::*;
614
615 #[test]
616 fn deserialize_path_require_mode_as_string() {
617 let config: Configuration =
618 json5::from_str("{ bundle: { require_mode: 'path' } }").unwrap();
619
620 pretty_assertions::assert_eq!(
621 config.bundle.unwrap(),
622 BundleConfiguration::new(PathRequireMode::default())
623 );
624 }
625
626 #[test]
627 fn deserialize_path_require_mode_as_object() {
628 let config: Configuration =
629 json5::from_str("{bundle: { require_mode: { name: 'path' } } }").unwrap();
630
631 pretty_assertions::assert_eq!(
632 config.bundle.unwrap(),
633 BundleConfiguration::new(PathRequireMode::default())
634 );
635 }
636
637 #[test]
638 fn deserialize_path_require_mode_with_custom_module_folder_name() {
639 let config: Configuration = json5::from_str(
640 "{bundle: { require_mode: { name: 'path', module_folder_name: '__INIT__' } } }",
641 )
642 .unwrap();
643
644 pretty_assertions::assert_eq!(
645 config.bundle.unwrap(),
646 BundleConfiguration::new(PathRequireMode::new("__INIT__"))
647 );
648 }
649
650 #[test]
651 fn deserialize_path_require_mode_with_custom_module_identifier() {
652 let config: Configuration =
653 json5::from_str("{bundle: { require_mode: 'path', modules_identifier: '__M' } }")
654 .unwrap();
655
656 pretty_assertions::assert_eq!(
657 config.bundle.unwrap(),
658 BundleConfiguration::new(PathRequireMode::default()).with_modules_identifier("__M")
659 );
660 }
661
662 #[test]
663 fn deserialize_path_require_mode_with_custom_module_identifier_and_module_folder_name() {
664 let config: Configuration = json5::from_str(
665 "{bundle: { require_mode: { name: 'path', module_folder_name: '__INIT__' }, modules_identifier: '__M' } }",
666 )
667 .unwrap();
668
669 pretty_assertions::assert_eq!(
670 config.bundle.unwrap(),
671 BundleConfiguration::new(PathRequireMode::new("__INIT__"))
672 .with_modules_identifier("__M")
673 );
674 }
675
676 #[test]
677 fn deserialize_path_require_mode_with_excludes() {
678 let config: Configuration = json5::from_str(
679 "{bundle: { require_mode: { name: 'path' }, excludes: ['@lune', 'secrets'] } }",
680 )
681 .unwrap();
682
683 pretty_assertions::assert_eq!(
684 config.bundle.unwrap(),
685 BundleConfiguration::new(PathRequireMode::default())
686 .with_exclude("@lune")
687 .with_exclude("secrets")
688 );
689 }
690
691 #[test]
692 fn deserialize_unknown_require_mode_name() {
693 let result: Result<Configuration, _> =
694 json5::from_str("{bundle: { require_mode: 'oops' } }");
695
696 insta::assert_snapshot!(
697 result.expect_err("deserialization should fail").to_string(),
698 @"invalid require mode `oops` at line 1 column 26"
699 );
700 }
701 }
702
703 mod loaders {
704 use super::*;
705
706 #[test]
707 fn deserialize_custom_loaders() {
708 let config: Configuration =
709 json5::from_str("{ loaders: { '**/*.luau': 'luau', '**/*.json': 'json', '**/*.jsonl': 'json_lines' } }").unwrap();
710
711 insta::assert_debug_snapshot!(config.loaders, @r###"
712 LoaderConfiguration {
713 loaders: [
714 (
715 FilterPattern {
716 pattern: "**/*.luau",
717 },
718 Luau,
719 ),
720 (
721 FilterPattern {
722 pattern: "**/*.json",
723 },
724 Json,
725 ),
726 (
727 FilterPattern {
728 pattern: "**/*.jsonl",
729 },
730 JsonLines,
731 ),
732 ],
733 }
734 "###);
735 }
736 }
737}