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, Default, 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    #[default]
172    RetainLines,
173    /// Generates dense, compact code with a specified column span.
174    Dense {
175        /// The maximum number of characters per line.
176        #[serde(default = "get_default_column_span")]
177        column_span: usize,
178    },
179    /// Attempts to generate readable code, with a specified column span.
180    Readable {
181        /// The maximum number of characters per line.
182        #[serde(default = "get_default_column_span")]
183        column_span: usize,
184    },
185}
186
187impl GeneratorParameters {
188    /// Creates a new dense generator with default column span.
189    pub fn default_dense() -> Self {
190        Self::Dense {
191            column_span: DEFAULT_COLUMN_SPAN,
192        }
193    }
194
195    /// Creates a new readable generator with default column span.
196    pub fn default_readable() -> Self {
197        Self::Readable {
198            column_span: DEFAULT_COLUMN_SPAN,
199        }
200    }
201
202    fn generate_lua(&self, block: &Block, code: &str) -> String {
203        match self {
204            Self::RetainLines => {
205                let mut generator = TokenBasedLuaGenerator::new(code);
206                generator.write_block(block);
207                generator.into_string()
208            }
209            Self::Dense { column_span } => {
210                let mut generator = DenseLuaGenerator::new(*column_span);
211                generator.write_block(block);
212                generator.into_string()
213            }
214            Self::Readable { column_span } => {
215                let mut generator = ReadableLuaGenerator::new(*column_span);
216                generator.write_block(block);
217                generator.into_string()
218            }
219        }
220    }
221
222    fn build_parser(&self) -> Parser {
223        match self {
224            Self::RetainLines => Parser::default().preserve_tokens(),
225            Self::Dense { .. } | Self::Readable { .. } => Parser::default(),
226        }
227    }
228}
229
230impl FromStr for GeneratorParameters {
231    type Err = String;
232
233    fn from_str(s: &str) -> Result<Self, Self::Err> {
234        Ok(match s {
235            // keep "retain-lines" for back-compatibility
236            "retain_lines" | "retain-lines" => Self::RetainLines,
237            "dense" => Self::Dense {
238                column_span: DEFAULT_COLUMN_SPAN,
239            },
240            "readable" => Self::Readable {
241                column_span: DEFAULT_COLUMN_SPAN,
242            },
243            _ => return Err(format!("invalid generator name `{}`", s)),
244        })
245    }
246}
247
248/// Configuration for bundling modules.
249///
250/// This struct defines how modules should be bundled together, including
251/// how requires are handled.
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
253#[serde(deny_unknown_fields, rename_all = "snake_case")]
254pub struct BundleConfiguration {
255    #[serde(deserialize_with = "crate::utils::string_or_struct")]
256    require_mode: BundleRequireMode,
257    #[serde(skip_serializing_if = "Option::is_none")]
258    modules_identifier: Option<String>,
259    #[serde(default, skip_serializing_if = "HashSet::is_empty")]
260    excludes: HashSet<String>,
261}
262
263impl BundleConfiguration {
264    /// Creates a new bundle configuration with the specified require mode.
265    pub fn new(require_mode: impl Into<BundleRequireMode>) -> Self {
266        Self {
267            require_mode: require_mode.into(),
268            modules_identifier: None,
269            excludes: Default::default(),
270        }
271    }
272
273    /// Sets the modules identifier for this bundle configuration.
274    pub fn with_modules_identifier(mut self, modules_identifier: impl Into<String>) -> Self {
275        self.modules_identifier = Some(modules_identifier.into());
276        self
277    }
278
279    /// Adds a module to exclude from bundling.
280    pub fn with_exclude(mut self, exclude: impl Into<String>) -> Self {
281        self.excludes.insert(exclude.into());
282        self
283    }
284
285    pub(crate) fn require_mode(&self) -> &BundleRequireMode {
286        &self.require_mode
287    }
288
289    pub(crate) fn modules_identifier(&self) -> &str {
290        self.modules_identifier
291            .as_ref()
292            .map(AsRef::as_ref)
293            .unwrap_or("__DARKLUA_BUNDLE_MODULES")
294    }
295
296    pub(crate) fn excludes(&self) -> impl Iterator<Item = &str> {
297        self.excludes.iter().map(AsRef::as_ref)
298    }
299}
300
301#[cfg(test)]
302mod test {
303    use super::*;
304
305    mod generator_parameters {
306        use super::*;
307
308        #[test]
309        fn deserialize_retain_lines_params() {
310            let config: Configuration =
311                json5::from_str("{ generator: { name: 'retain_lines' } }").unwrap();
312
313            pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
314        }
315
316        #[test]
317        fn deserialize_retain_lines_params_deprecated() {
318            let config: Configuration =
319                json5::from_str("{ generator: { name: 'retain-lines' } }").unwrap();
320
321            pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
322        }
323
324        #[test]
325        fn deserialize_dense_params() {
326            let config: Configuration = json5::from_str("{ generator: { name: 'dense' }}").unwrap();
327
328            pretty_assertions::assert_eq!(
329                config.generator,
330                GeneratorParameters::Dense {
331                    column_span: DEFAULT_COLUMN_SPAN
332                }
333            );
334        }
335
336        #[test]
337        fn deserialize_dense_params_with_column_span() {
338            let config: Configuration =
339                json5::from_str("{ generator: { name: 'dense', column_span: 110 } }").unwrap();
340
341            pretty_assertions::assert_eq!(
342                config.generator,
343                GeneratorParameters::Dense { column_span: 110 }
344            );
345        }
346
347        #[test]
348        fn deserialize_readable_params() {
349            let config: Configuration =
350                json5::from_str("{ generator: { name: 'readable' } }").unwrap();
351
352            pretty_assertions::assert_eq!(
353                config.generator,
354                GeneratorParameters::Readable {
355                    column_span: DEFAULT_COLUMN_SPAN
356                }
357            );
358        }
359
360        #[test]
361        fn deserialize_readable_params_with_column_span() {
362            let config: Configuration =
363                json5::from_str("{ generator: { name: 'readable', column_span: 110 }}").unwrap();
364
365            pretty_assertions::assert_eq!(
366                config.generator,
367                GeneratorParameters::Readable { column_span: 110 }
368            );
369        }
370
371        #[test]
372        fn deserialize_retain_lines_params_as_string() {
373            let config: Configuration = json5::from_str("{generator: 'retain_lines'}").unwrap();
374
375            pretty_assertions::assert_eq!(config.generator, GeneratorParameters::RetainLines);
376        }
377
378        #[test]
379        fn deserialize_dense_params_as_string() {
380            let config: Configuration = json5::from_str("{generator: 'dense'}").unwrap();
381
382            pretty_assertions::assert_eq!(
383                config.generator,
384                GeneratorParameters::Dense {
385                    column_span: DEFAULT_COLUMN_SPAN
386                }
387            );
388        }
389
390        #[test]
391        fn deserialize_readable_params_as_string() {
392            let config: Configuration = json5::from_str("{generator: 'readable'}").unwrap();
393
394            pretty_assertions::assert_eq!(
395                config.generator,
396                GeneratorParameters::Readable {
397                    column_span: DEFAULT_COLUMN_SPAN
398                }
399            );
400        }
401
402        #[test]
403        fn deserialize_unknown_generator_name() {
404            let result: Result<Configuration, _> = json5::from_str("{generator: 'oops'}");
405
406            pretty_assertions::assert_eq!(
407                result.expect_err("deserialization should fail").to_string(),
408                "invalid generator name `oops`"
409            );
410        }
411    }
412
413    mod bundle_configuration {
414        use crate::rules::require::PathRequireMode;
415
416        use super::*;
417
418        #[test]
419        fn deserialize_path_require_mode_as_string() {
420            let config: Configuration =
421                json5::from_str("{ bundle: { require_mode: 'path' } }").unwrap();
422
423            pretty_assertions::assert_eq!(
424                config.bundle.unwrap(),
425                BundleConfiguration::new(PathRequireMode::default())
426            );
427        }
428
429        #[test]
430        fn deserialize_path_require_mode_as_object() {
431            let config: Configuration =
432                json5::from_str("{bundle: { require_mode: { name: 'path' } } }").unwrap();
433
434            pretty_assertions::assert_eq!(
435                config.bundle.unwrap(),
436                BundleConfiguration::new(PathRequireMode::default())
437            );
438        }
439
440        #[test]
441        fn deserialize_path_require_mode_with_custom_module_folder_name() {
442            let config: Configuration = json5::from_str(
443                "{bundle: { require_mode: { name: 'path', module_folder_name: '__INIT__' } } }",
444            )
445            .unwrap();
446
447            pretty_assertions::assert_eq!(
448                config.bundle.unwrap(),
449                BundleConfiguration::new(PathRequireMode::new("__INIT__"))
450            );
451        }
452
453        #[test]
454        fn deserialize_path_require_mode_with_custom_module_identifier() {
455            let config: Configuration =
456                json5::from_str("{bundle: { require_mode: 'path', modules_identifier: '__M' } }")
457                    .unwrap();
458
459            pretty_assertions::assert_eq!(
460                config.bundle.unwrap(),
461                BundleConfiguration::new(PathRequireMode::default()).with_modules_identifier("__M")
462            );
463        }
464
465        #[test]
466        fn deserialize_path_require_mode_with_custom_module_identifier_and_module_folder_name() {
467            let config: Configuration = json5::from_str(
468                "{bundle: { require_mode: { name: 'path', module_folder_name: '__INIT__' }, modules_identifier: '__M' } }",
469            )
470            .unwrap();
471
472            pretty_assertions::assert_eq!(
473                config.bundle.unwrap(),
474                BundleConfiguration::new(PathRequireMode::new("__INIT__"))
475                    .with_modules_identifier("__M")
476            );
477        }
478
479        #[test]
480        fn deserialize_path_require_mode_with_excludes() {
481            let config: Configuration = json5::from_str(
482                "{bundle: { require_mode: { name: 'path' }, excludes: ['@lune', 'secrets'] } }",
483            )
484            .unwrap();
485
486            pretty_assertions::assert_eq!(
487                config.bundle.unwrap(),
488                BundleConfiguration::new(PathRequireMode::default())
489                    .with_exclude("@lune")
490                    .with_exclude("secrets")
491            );
492        }
493
494        #[test]
495        fn deserialize_unknown_require_mode_name() {
496            let result: Result<Configuration, _> =
497                json5::from_str("{bundle: { require_mode: 'oops' } }");
498
499            pretty_assertions::assert_eq!(
500                result.expect_err("deserialization should fail").to_string(),
501                "invalid require mode `oops`"
502            );
503        }
504    }
505}