darklua_core/frontend/
configuration.rs

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    Parser,
17};
18
19const DEFAULT_COLUMN_SPAN: usize = 80;
20
21fn get_default_column_span() -> usize {
22    DEFAULT_COLUMN_SPAN
23}
24
25/// Configuration for processing files (rules, generator, bundling).
26#[derive(Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct Configuration {
29    #[serde(alias = "process", default = "get_default_rules")]
30    rules: Vec<Box<dyn Rule>>,
31    #[serde(default, deserialize_with = "crate::utils::string_or_struct")]
32    generator: GeneratorParameters,
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    bundle: Option<BundleConfiguration>,
35    #[serde(default, skip)]
36    location: Option<PathBuf>,
37}
38
39impl Configuration {
40    /// Creates a configuration object without any rules and with the default generator.
41    pub fn empty() -> Self {
42        Self {
43            rules: Vec::new(),
44            generator: GeneratorParameters::default(),
45            bundle: None,
46            location: None,
47        }
48    }
49
50    /// Sets the generator parameters for this configuration.
51    #[inline]
52    pub fn with_generator(mut self, generator: GeneratorParameters) -> Self {
53        self.generator = generator;
54        self
55    }
56
57    /// Sets the generator parameters for this configuration.
58    #[inline]
59    pub fn set_generator(&mut self, generator: GeneratorParameters) {
60        self.generator = generator;
61    }
62
63    /// Adds a rule to this configuration.
64    #[inline]
65    pub fn with_rule(mut self, rule: impl Into<Box<dyn Rule>>) -> Self {
66        self.push_rule(rule);
67        self
68    }
69
70    /// Sets the bundle configuration for this configuration.
71    #[inline]
72    pub fn with_bundle_configuration(mut self, configuration: BundleConfiguration) -> Self {
73        self.bundle = Some(configuration);
74        self
75    }
76
77    /// Sets the location of this configuration.
78    #[inline]
79    pub fn with_location(mut self, location: impl Into<PathBuf>) -> Self {
80        self.location = Some(location.into());
81        self
82    }
83
84    /// Adds a rule to this configuration.
85    #[inline]
86    pub fn push_rule(&mut self, rule: impl Into<Box<dyn Rule>>) {
87        self.rules.push(rule.into());
88    }
89
90    #[inline]
91    pub(crate) fn rules<'a, 'b: 'a>(&'b self) -> impl Iterator<Item = &'a dyn Rule> {
92        self.rules.iter().map(AsRef::as_ref)
93    }
94
95    #[inline]
96    pub(crate) fn build_parser(&self) -> Parser {
97        self.generator.build_parser()
98    }
99
100    #[inline]
101    pub(crate) fn generate_lua(&self, block: &Block, code: &str) -> String {
102        self.generator.generate_lua(block, code)
103    }
104
105    pub(crate) fn bundle(&self) -> Option<Bundler> {
106        if let Some(bundle_config) = self.bundle.as_ref() {
107            let bundler = Bundler::new(
108                self.build_parser(),
109                bundle_config.require_mode().clone(),
110                bundle_config.excludes(),
111            )
112            .with_modules_identifier(bundle_config.modules_identifier());
113            Some(bundler)
114        } else {
115            None
116        }
117    }
118
119    #[inline]
120    pub(crate) fn rules_len(&self) -> usize {
121        self.rules.len()
122    }
123
124    #[inline]
125    pub(crate) fn location(&self) -> Option<&Path> {
126        self.location.as_deref()
127    }
128}
129
130impl Default for Configuration {
131    fn default() -> Self {
132        Self {
133            rules: get_default_rules(),
134            generator: Default::default(),
135            bundle: None,
136            location: None,
137        }
138    }
139}
140
141impl std::fmt::Debug for Configuration {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        f.debug_struct("Config")
144            .field("generator", &self.generator)
145            .field(
146                "rules",
147                &self
148                    .rules
149                    .iter()
150                    .map(|rule| {
151                        json5::to_string(rule)
152                            .ok()
153                            .unwrap_or_else(|| rule.get_name().to_owned())
154                    })
155                    .collect::<Vec<_>>()
156                    .join(", "),
157            )
158            .finish()
159    }
160}
161
162/// Parameters for configuring the Lua code generator.
163///
164/// This enum defines different modes for generating Lua code, each with its own
165/// formatting characteristics.
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
167#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "name")]
168pub enum GeneratorParameters {
169    /// Retains the original line structure of the input code.
170    #[serde(alias = "retain-lines")]
171    RetainLines,
172    /// Generates dense, compact code with a specified column span.
173    Dense {
174        /// The maximum number of characters per line.
175        #[serde(default = "get_default_column_span")]
176        column_span: usize,
177    },
178    /// Attempts to generate readable code, with a specified column span.
179    Readable {
180        /// The maximum number of characters per line.
181        #[serde(default = "get_default_column_span")]
182        column_span: usize,
183    },
184}
185
186impl Default for GeneratorParameters {
187    fn default() -> Self {
188        Self::RetainLines
189    }
190}
191
192impl GeneratorParameters {
193    /// Creates a new dense generator with default column span.
194    pub fn default_dense() -> Self {
195        Self::Dense {
196            column_span: DEFAULT_COLUMN_SPAN,
197        }
198    }
199
200    /// Creates a new readable generator with default column span.
201    pub fn default_readable() -> Self {
202        Self::Readable {
203            column_span: DEFAULT_COLUMN_SPAN,
204        }
205    }
206
207    fn generate_lua(&self, block: &Block, code: &str) -> String {
208        match self {
209            Self::RetainLines => {
210                let mut generator = TokenBasedLuaGenerator::new(code);
211                generator.write_block(block);
212                generator.into_string()
213            }
214            Self::Dense { column_span } => {
215                let mut generator = DenseLuaGenerator::new(*column_span);
216                generator.write_block(block);
217                generator.into_string()
218            }
219            Self::Readable { column_span } => {
220                let mut generator = ReadableLuaGenerator::new(*column_span);
221                generator.write_block(block);
222                generator.into_string()
223            }
224        }
225    }
226
227    fn build_parser(&self) -> Parser {
228        match self {
229            Self::RetainLines => Parser::default().preserve_tokens(),
230            Self::Dense { .. } | Self::Readable { .. } => Parser::default(),
231        }
232    }
233}
234
235impl FromStr for GeneratorParameters {
236    type Err = String;
237
238    fn from_str(s: &str) -> Result<Self, Self::Err> {
239        Ok(match s {
240            // keep "retain-lines" for back-compatibility
241            "retain_lines" | "retain-lines" => Self::RetainLines,
242            "dense" => Self::Dense {
243                column_span: DEFAULT_COLUMN_SPAN,
244            },
245            "readable" => Self::Readable {
246                column_span: DEFAULT_COLUMN_SPAN,
247            },
248            _ => return Err(format!("invalid generator name `{}`", s)),
249        })
250    }
251}
252
253/// Configuration for bundling modules.
254///
255/// This struct defines how modules should be bundled together, including
256/// how requires are handled.
257#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
258#[serde(deny_unknown_fields, rename_all = "snake_case")]
259pub struct BundleConfiguration {
260    #[serde(deserialize_with = "crate::utils::string_or_struct")]
261    require_mode: BundleRequireMode,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    modules_identifier: Option<String>,
264    #[serde(default, skip_serializing_if = "HashSet::is_empty")]
265    excludes: HashSet<String>,
266}
267
268impl BundleConfiguration {
269    /// Creates a new bundle configuration with the specified require mode.
270    pub fn new(require_mode: impl Into<BundleRequireMode>) -> Self {
271        Self {
272            require_mode: require_mode.into(),
273            modules_identifier: None,
274            excludes: Default::default(),
275        }
276    }
277
278    /// Sets the modules identifier for this bundle configuration.
279    pub fn with_modules_identifier(mut self, modules_identifier: impl Into<String>) -> Self {
280        self.modules_identifier = Some(modules_identifier.into());
281        self
282    }
283
284    /// Adds a module to exclude from bundling.
285    pub fn with_exclude(mut self, exclude: impl Into<String>) -> Self {
286        self.excludes.insert(exclude.into());
287        self
288    }
289
290    pub(crate) fn require_mode(&self) -> &BundleRequireMode {
291        &self.require_mode
292    }
293
294    pub(crate) fn modules_identifier(&self) -> &str {
295        self.modules_identifier
296            .as_ref()
297            .map(AsRef::as_ref)
298            .unwrap_or("__DARKLUA_BUNDLE_MODULES")
299    }
300
301    pub(crate) fn excludes(&self) -> impl Iterator<Item = &str> {
302        self.excludes.iter().map(AsRef::as_ref)
303    }
304}
305
306#[cfg(test)]
307mod test {
308    use super::*;
309
310    mod generator_parameters {
311        use super::*;
312
313        #[test]
314        fn deserialize_retain_lines_params() {
315            let config: Configuration =
316                json5::from_str("{ generator: { name: 'retain_lines' } }").unwrap();
317
318            pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
319        }
320
321        #[test]
322        fn deserialize_retain_lines_params_deprecated() {
323            let config: Configuration =
324                json5::from_str("{ generator: { name: 'retain-lines' } }").unwrap();
325
326            pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
327        }
328
329        #[test]
330        fn deserialize_dense_params() {
331            let config: Configuration = json5::from_str("{ generator: { name: 'dense' }}").unwrap();
332
333            pretty_assertions::assert_eq!(
334                config.generator,
335                GeneratorParameters::Dense {
336                    column_span: DEFAULT_COLUMN_SPAN
337                }
338            );
339        }
340
341        #[test]
342        fn deserialize_dense_params_with_column_span() {
343            let config: Configuration =
344                json5::from_str("{ generator: { name: 'dense', column_span: 110 } }").unwrap();
345
346            pretty_assertions::assert_eq!(
347                config.generator,
348                GeneratorParameters::Dense { column_span: 110 }
349            );
350        }
351
352        #[test]
353        fn deserialize_readable_params() {
354            let config: Configuration =
355                json5::from_str("{ generator: { name: 'readable' } }").unwrap();
356
357            pretty_assertions::assert_eq!(
358                config.generator,
359                GeneratorParameters::Readable {
360                    column_span: DEFAULT_COLUMN_SPAN
361                }
362            );
363        }
364
365        #[test]
366        fn deserialize_readable_params_with_column_span() {
367            let config: Configuration =
368                json5::from_str("{ generator: { name: 'readable', column_span: 110 }}").unwrap();
369
370            pretty_assertions::assert_eq!(
371                config.generator,
372                GeneratorParameters::Readable { column_span: 110 }
373            );
374        }
375
376        #[test]
377        fn deserialize_retain_lines_params_as_string() {
378            let config: Configuration = json5::from_str("{generator: 'retain_lines'}").unwrap();
379
380            pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
381        }
382
383        #[test]
384        fn deserialize_dense_params_as_string() {
385            let config: Configuration = json5::from_str("{generator: '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_readable_params_as_string() {
397            let config: Configuration = json5::from_str("{generator: 'readable'}").unwrap();
398
399            pretty_assertions::assert_eq!(
400                config.generator,
401                GeneratorParameters::Readable {
402                    column_span: DEFAULT_COLUMN_SPAN
403                }
404            );
405        }
406
407        #[test]
408        fn deserialize_unknown_generator_name() {
409            let result: Result<Configuration, _> = json5::from_str("{generator: 'oops'}");
410
411            pretty_assertions::assert_eq!(
412                result.expect_err("deserialization should fail").to_string(),
413                "invalid generator name `oops`"
414            );
415        }
416    }
417
418    mod bundle_configuration {
419        use crate::rules::require::PathRequireMode;
420
421        use super::*;
422
423        #[test]
424        fn deserialize_path_require_mode_as_string() {
425            let config: Configuration =
426                json5::from_str("{ bundle: { require_mode: 'path' } }").unwrap();
427
428            pretty_assertions::assert_eq!(
429                config.bundle.unwrap(),
430                BundleConfiguration::new(PathRequireMode::default())
431            );
432        }
433
434        #[test]
435        fn deserialize_path_require_mode_as_object() {
436            let config: Configuration =
437                json5::from_str("{bundle: { require_mode: { name: 'path' } } }").unwrap();
438
439            pretty_assertions::assert_eq!(
440                config.bundle.unwrap(),
441                BundleConfiguration::new(PathRequireMode::default())
442            );
443        }
444
445        #[test]
446        fn deserialize_path_require_mode_with_custom_module_folder_name() {
447            let config: Configuration = json5::from_str(
448                "{bundle: { require_mode: { name: 'path', module_folder_name: '__INIT__' } } }",
449            )
450            .unwrap();
451
452            pretty_assertions::assert_eq!(
453                config.bundle.unwrap(),
454                BundleConfiguration::new(PathRequireMode::new("__INIT__"))
455            );
456        }
457
458        #[test]
459        fn deserialize_path_require_mode_with_custom_module_identifier() {
460            let config: Configuration =
461                json5::from_str("{bundle: { require_mode: 'path', modules_identifier: '__M' } }")
462                    .unwrap();
463
464            pretty_assertions::assert_eq!(
465                config.bundle.unwrap(),
466                BundleConfiguration::new(PathRequireMode::default()).with_modules_identifier("__M")
467            );
468        }
469
470        #[test]
471        fn deserialize_path_require_mode_with_custom_module_identifier_and_module_folder_name() {
472            let config: Configuration = json5::from_str(
473                "{bundle: { require_mode: { name: 'path', module_folder_name: '__INIT__' }, modules_identifier: '__M' } }",
474            )
475            .unwrap();
476
477            pretty_assertions::assert_eq!(
478                config.bundle.unwrap(),
479                BundleConfiguration::new(PathRequireMode::new("__INIT__"))
480                    .with_modules_identifier("__M")
481            );
482        }
483
484        #[test]
485        fn deserialize_path_require_mode_with_excludes() {
486            let config: Configuration = json5::from_str(
487                "{bundle: { require_mode: { name: 'path' }, excludes: ['@lune', 'secrets'] } }",
488            )
489            .unwrap();
490
491            pretty_assertions::assert_eq!(
492                config.bundle.unwrap(),
493                BundleConfiguration::new(PathRequireMode::default())
494                    .with_exclude("@lune")
495                    .with_exclude("secrets")
496            );
497        }
498
499        #[test]
500        fn deserialize_unknown_require_mode_name() {
501            let result: Result<Configuration, _> =
502                json5::from_str("{bundle: { require_mode: 'oops' } }");
503
504            pretty_assertions::assert_eq!(
505                result.expect_err("deserialization should fail").to_string(),
506                "invalid require mode `oops`"
507            );
508        }
509    }
510}