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