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