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