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)]
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 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]
52 pub fn with_generator(mut self, generator: GeneratorParameters) -> Self {
53 self.generator = generator;
54 self
55 }
56
57 #[inline]
59 pub fn set_generator(&mut self, generator: GeneratorParameters) {
60 self.generator = generator;
61 }
62
63 #[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 #[inline]
72 pub fn with_bundle_configuration(mut self, configuration: BundleConfiguration) -> Self {
73 self.bundle = Some(configuration);
74 self
75 }
76
77 #[inline]
79 pub fn with_location(mut self, location: impl Into<PathBuf>) -> Self {
80 self.location = Some(location.into());
81 self
82 }
83
84 #[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
167#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "name")]
168pub enum GeneratorParameters {
169 #[serde(alias = "retain-lines")]
171 RetainLines,
172 Dense {
174 #[serde(default = "get_default_column_span")]
176 column_span: usize,
177 },
178 Readable {
180 #[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 pub fn default_dense() -> Self {
195 Self::Dense {
196 column_span: DEFAULT_COLUMN_SPAN,
197 }
198 }
199
200 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 "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#[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 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 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 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}