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, Default, Serialize, Deserialize, PartialEq, Eq)]
167#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "name")]
168pub enum GeneratorParameters {
169 #[serde(alias = "retain-lines")]
171 #[default]
172 RetainLines,
173 Dense {
175 #[serde(default = "get_default_column_span")]
177 column_span: usize,
178 },
179 Readable {
181 #[serde(default = "get_default_column_span")]
183 column_span: usize,
184 },
185}
186
187impl GeneratorParameters {
188 pub fn default_dense() -> Self {
190 Self::Dense {
191 column_span: DEFAULT_COLUMN_SPAN,
192 }
193 }
194
195 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 "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#[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 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 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 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}