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 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 "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}