1use serde::{Deserialize, Serialize};
2
3use crate::condition::{CompositeDependency, EvalContext};
4use crate::error;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(rename = "config")]
9pub struct ModuleConfig {
10 #[serde(rename = "moduleName")]
11 pub module_name: ModuleName,
12
13 #[serde(rename = "moduleImage")]
14 pub module_image: Option<ModuleImage>,
15
16 #[serde(rename = "moduleDependencies")]
17 pub module_dependencies: Option<CompositeDependency>,
18
19 #[serde(rename = "requiredInstallFiles")]
20 pub required_install_files: Option<FileList>,
21
22 #[serde(rename = "installSteps")]
23 pub install_steps: Option<InstallSteps>,
24
25 #[serde(rename = "conditionalFileInstalls")]
26 pub conditional_file_installs: Option<ConditionalFileInstalls>,
27}
28
29impl ModuleConfig {
30 pub fn parse(xml: &str) -> error::Result<Self> {
31 quick_xml::de::from_str(xml).map_err(Into::into)
32 }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ModuleName {
38 #[serde(rename = "@position")]
39 pub position: Option<NamePosition>,
40
41 #[serde(rename = "$text")]
42 pub value: String,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46pub enum NamePosition {
47 Left,
48 Right,
49 RightOfImage,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ModuleImage {
55 #[serde(rename = "@path")]
56 pub path: String,
57
58 #[serde(rename = "@showImage", default = "default_true")]
59 pub show_image: bool,
60
61 #[serde(rename = "@showFade", default = "default_true")]
62 pub show_fade: bool,
63
64 #[serde(rename = "@height", default = "default_neg_one")]
65 pub height: i32,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct InstallSteps {
71 #[serde(rename = "@order")]
72 pub order: Option<SortOrder>,
73
74 #[serde(rename = "installStep", default)]
75 pub steps: Vec<InstallStep>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct InstallStep {
81 #[serde(rename = "@name")]
82 pub name: String,
83
84 pub visible: Option<CompositeDependency>,
85
86 #[serde(rename = "optionalFileGroups")]
87 pub optional_file_groups: Option<GroupList>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct GroupList {
93 #[serde(rename = "@order")]
94 pub order: Option<SortOrder>,
95
96 #[serde(rename = "group", default)]
97 pub groups: Vec<Group>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct Group {
103 #[serde(rename = "@name")]
104 pub name: String,
105
106 #[serde(rename = "@type")]
107 pub group_type: GroupType,
108
109 #[serde(rename = "plugins")]
110 pub plugins: PluginList,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct PluginList {
116 #[serde(rename = "@order")]
117 pub order: Option<SortOrder>,
118
119 #[serde(rename = "plugin", default)]
120 pub plugins: Vec<Plugin>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct Plugin {
126 #[serde(rename = "@name")]
127 pub name: String,
128
129 pub description: Option<String>,
130
131 pub image: Option<PluginImage>,
132
133 #[serde(rename = "typeDescriptor")]
134 pub type_descriptor: Option<TypeDescriptor>,
135
136 #[serde(rename = "conditionFlags")]
137 pub condition_flags: Option<ConditionFlagList>,
138
139 pub files: Option<FileList>,
140}
141
142impl Plugin {
143 pub fn plugin_type(&self) -> PluginType {
149 self.type_descriptor
150 .as_ref()
151 .map(|td| td.resolved_type())
152 .unwrap_or(PluginType::Optional)
153 }
154
155 pub fn plugin_type_in_context(&self, ctx: &EvalContext) -> PluginType {
160 self.type_descriptor
161 .as_ref()
162 .map(|td| td.resolved_type_in_context(ctx))
163 .unwrap_or(PluginType::Optional)
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct PluginImage {
169 #[serde(rename = "@path")]
170 pub path: String,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct TypeDescriptor {
177 #[serde(rename = "type")]
178 pub simple_type: Option<SimpleType>,
179
180 #[serde(rename = "dependencyType")]
181 pub dependency_type: Option<DependencyType>,
182}
183
184impl TypeDescriptor {
185 pub fn resolved_type(&self) -> PluginType {
187 self.resolve_with(|_| false)
188 }
189
190 pub fn resolved_type_in_context(&self, ctx: &EvalContext) -> PluginType {
192 use crate::condition::Evaluate;
193 self.resolve_with(|dep| dep.evaluate(ctx))
194 }
195
196 fn resolve_with(&self, eval: impl Fn(&CompositeDependency) -> bool) -> PluginType {
197 if let Some(ref st) = self.simple_type {
198 return st.name;
199 }
200 if let Some(ref dt) = self.dependency_type {
201 if let Some(ref patterns) = dt.patterns {
202 for pattern in &patterns.patterns {
203 if eval(&pattern.dependencies) {
204 return pattern.plugin_type.name;
205 }
206 }
207 }
208 return dt.default_type.name;
209 }
210 PluginType::Optional
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct SimpleType {
216 #[serde(rename = "@name")]
217 pub name: PluginType,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct DependencyType {
223 #[serde(rename = "defaultType")]
224 pub default_type: SimpleType,
225
226 #[serde(rename = "patterns")]
227 pub patterns: Option<TypePatterns>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct TypePatterns {
232 #[serde(rename = "pattern", default)]
233 pub patterns: Vec<TypePattern>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct TypePattern {
238 pub dependencies: CompositeDependency,
239
240 #[serde(rename = "type")]
241 pub plugin_type: SimpleType,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct ConditionFlagList {
247 #[serde(rename = "flag", default)]
248 pub flags: Vec<ConditionFlag>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ConditionFlag {
253 #[serde(rename = "@name")]
254 pub name: String,
255
256 #[serde(rename = "$text")]
257 pub value: String,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct ConditionalFileInstalls {
263 pub patterns: ConditionalPatterns,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct ConditionalPatterns {
268 #[serde(rename = "pattern", default)]
269 pub patterns: Vec<ConditionalPattern>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct ConditionalPattern {
274 pub dependencies: CompositeDependency,
275 pub files: FileList,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct FileList {
282 #[serde(rename = "$value", default)]
283 pub items: Vec<FileItem>,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
287#[serde(rename_all = "lowercase")]
288pub enum FileItem {
289 File(FileRef),
290 Folder(FileRef),
291}
292
293impl FileItem {
294 pub fn file_ref(&self) -> &FileRef {
295 match self {
296 FileItem::File(r) | FileItem::Folder(r) => r,
297 }
298 }
299
300 pub fn is_folder(&self) -> bool {
301 matches!(self, FileItem::Folder(_))
302 }
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct FileRef {
307 #[serde(rename = "@source")]
308 pub source: String,
309
310 #[serde(rename = "@destination", default)]
311 pub destination: String,
312
313 #[serde(rename = "@priority", default)]
314 pub priority: i32,
315
316 #[serde(rename = "@alwaysInstall", default)]
317 pub always_install: bool,
318
319 #[serde(rename = "@installIfUsable", default)]
320 pub install_if_usable: bool,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
326pub enum GroupType {
327 SelectExactlyOne,
328 SelectAtMostOne,
329 SelectAtLeastOne,
330 SelectAll,
331 SelectAny,
332}
333
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
335pub enum PluginType {
336 Required,
337 Recommended,
338 Optional,
339 CouldBeUsable,
340 NotUsable,
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
344pub enum SortOrder {
345 Explicit,
346 Ascending,
347 Descending,
348}
349
350fn default_true() -> bool {
353 true
354}
355
356fn default_neg_one() -> i32 {
357 -1
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
367 fn parse_minimal_config() {
368 let xml = r#"<config><moduleName>Test</moduleName></config>"#;
369 let config = ModuleConfig::parse(xml).unwrap();
370 assert_eq!(config.module_name.value, "Test");
371 assert!(config.module_image.is_none());
372 assert!(config.module_dependencies.is_none());
373 assert!(config.required_install_files.is_none());
374 assert!(config.install_steps.is_none());
375 assert!(config.conditional_file_installs.is_none());
376 }
377
378 #[test]
379 fn parse_empty_module_name_fails() {
380 let xml = r#"<config><moduleName></moduleName></config>"#;
382 assert!(ModuleConfig::parse(xml).is_err());
383 }
384
385 #[test]
386 fn parse_module_name_with_position() {
387 let xml = r#"<config><moduleName position="Left">Test</moduleName></config>"#;
388 let config = ModuleConfig::parse(xml).unwrap();
389 assert_eq!(config.module_name.position, Some(NamePosition::Left));
390
391 let xml = r#"<config><moduleName position="RightOfImage">Test</moduleName></config>"#;
392 let config = ModuleConfig::parse(xml).unwrap();
393 assert_eq!(
394 config.module_name.position,
395 Some(NamePosition::RightOfImage)
396 );
397 }
398
399 #[test]
400 fn parse_invalid_xml_fails() {
401 assert!(ModuleConfig::parse("not xml").is_err());
402 assert!(ModuleConfig::parse("").is_err());
403 assert!(ModuleConfig::parse("<config></config>").is_err());
404 }
405
406 #[test]
407 fn parse_module_image_defaults() {
408 let xml = r#"
409 <config>
410 <moduleName>Test</moduleName>
411 <moduleImage path="img.png"/>
412 </config>
413 "#;
414 let config = ModuleConfig::parse(xml).unwrap();
415 let img = config.module_image.unwrap();
416 assert_eq!(img.path, "img.png");
417 assert!(img.show_image);
418 assert!(img.show_fade);
419 assert_eq!(img.height, -1);
420 }
421
422 #[test]
423 fn parse_module_image_custom() {
424 let xml = r#"
425 <config>
426 <moduleName>Test</moduleName>
427 <moduleImage path="img.png" showImage="false" showFade="false" height="200"/>
428 </config>
429 "#;
430 let config = ModuleConfig::parse(xml).unwrap();
431 let img = config.module_image.unwrap();
432 assert!(!img.show_image);
433 assert!(!img.show_fade);
434 assert_eq!(img.height, 200);
435 }
436
437 #[test]
440 fn parse_all_group_types() {
441 for (type_str, expected) in [
442 ("SelectExactlyOne", GroupType::SelectExactlyOne),
443 ("SelectAtMostOne", GroupType::SelectAtMostOne),
444 ("SelectAtLeastOne", GroupType::SelectAtLeastOne),
445 ("SelectAll", GroupType::SelectAll),
446 ("SelectAny", GroupType::SelectAny),
447 ] {
448 let xml = format!(
449 r#"<config><moduleName>T</moduleName>
450 <installSteps><installStep name="S">
451 <optionalFileGroups><group name="G" type="{type_str}">
452 <plugins><plugin name="P"><typeDescriptor><type name="Optional"/></typeDescriptor></plugin></plugins>
453 </group></optionalFileGroups>
454 </installStep></installSteps></config>"#
455 );
456 let config = ModuleConfig::parse(&xml).unwrap();
457 let group = &config.install_steps.as_ref().unwrap().steps[0]
458 .optional_file_groups
459 .as_ref()
460 .unwrap()
461 .groups[0];
462 assert_eq!(group.group_type, expected, "Failed for {type_str}");
463 }
464 }
465
466 #[test]
469 fn parse_all_plugin_types() {
470 for (type_str, expected) in [
471 ("Required", PluginType::Required),
472 ("Recommended", PluginType::Recommended),
473 ("Optional", PluginType::Optional),
474 ("CouldBeUsable", PluginType::CouldBeUsable),
475 ("NotUsable", PluginType::NotUsable),
476 ] {
477 let xml = format!(
478 r#"<config><moduleName>T</moduleName>
479 <installSteps><installStep name="S">
480 <optionalFileGroups><group name="G" type="SelectAny">
481 <plugins><plugin name="P"><typeDescriptor><type name="{type_str}"/></typeDescriptor></plugin></plugins>
482 </group></optionalFileGroups>
483 </installStep></installSteps></config>"#
484 );
485 let config = ModuleConfig::parse(&xml).unwrap();
486 let plugin = &config.install_steps.as_ref().unwrap().steps[0]
487 .optional_file_groups
488 .as_ref()
489 .unwrap()
490 .groups[0]
491 .plugins
492 .plugins[0];
493 assert_eq!(plugin.plugin_type(), expected, "Failed for {type_str}");
494 }
495 }
496
497 #[test]
498 fn plugin_type_defaults_to_optional() {
499 let xml = r#"
500 <config><moduleName>T</moduleName>
501 <installSteps><installStep name="S">
502 <optionalFileGroups><group name="G" type="SelectAny">
503 <plugins><plugin name="P"></plugin></plugins>
504 </group></optionalFileGroups>
505 </installStep></installSteps></config>
506 "#;
507 let config = ModuleConfig::parse(xml).unwrap();
508 let plugin = &config.install_steps.as_ref().unwrap().steps[0]
509 .optional_file_groups
510 .as_ref()
511 .unwrap()
512 .groups[0]
513 .plugins
514 .plugins[0];
515 assert_eq!(plugin.plugin_type(), PluginType::Optional);
516 }
517
518 #[test]
521 fn type_descriptor_simple_takes_precedence() {
522 let td = TypeDescriptor {
524 simple_type: Some(SimpleType {
525 name: PluginType::Required,
526 }),
527 dependency_type: Some(DependencyType {
528 default_type: SimpleType {
529 name: PluginType::NotUsable,
530 },
531 patterns: None,
532 }),
533 };
534 assert_eq!(td.resolved_type(), PluginType::Required);
535
536 let ctx = EvalContext::default();
537 assert_eq!(td.resolved_type_in_context(&ctx), PluginType::Required);
538 }
539
540 #[test]
541 fn type_descriptor_dependency_default() {
542 let td = TypeDescriptor {
543 simple_type: None,
544 dependency_type: Some(DependencyType {
545 default_type: SimpleType {
546 name: PluginType::NotUsable,
547 },
548 patterns: None,
549 }),
550 };
551 assert_eq!(td.resolved_type(), PluginType::NotUsable);
552 }
553
554 #[test]
555 fn type_descriptor_none_defaults_optional() {
556 let td = TypeDescriptor {
557 simple_type: None,
558 dependency_type: None,
559 };
560 assert_eq!(td.resolved_type(), PluginType::Optional);
561 }
562
563 #[test]
566 fn file_item_is_folder() {
567 let file = FileItem::File(FileRef {
568 source: "a.esp".into(),
569 destination: "".into(),
570 priority: 0,
571 always_install: false,
572 install_if_usable: false,
573 });
574 assert!(!file.is_folder());
575
576 let folder = FileItem::Folder(FileRef {
577 source: "dir".into(),
578 destination: "".into(),
579 priority: 0,
580 always_install: false,
581 install_if_usable: false,
582 });
583 assert!(folder.is_folder());
584 }
585
586 #[test]
587 fn file_item_file_ref() {
588 let file = FileItem::File(FileRef {
589 source: "a.esp".into(),
590 destination: "Data".into(),
591 priority: 5,
592 always_install: true,
593 install_if_usable: false,
594 });
595 let r = file.file_ref();
596 assert_eq!(r.source, "a.esp");
597 assert_eq!(r.destination, "Data");
598 assert_eq!(r.priority, 5);
599 assert!(r.always_install);
600 }
601
602 #[test]
605 fn parse_file_ref_defaults() {
606 let xml = r#"
607 <config><moduleName>T</moduleName>
608 <requiredInstallFiles>
609 <file source="test.esp"/>
610 </requiredInstallFiles></config>
611 "#;
612 let config = ModuleConfig::parse(xml).unwrap();
613 let item = &config.required_install_files.as_ref().unwrap().items[0];
614 let r = item.file_ref();
615 assert_eq!(r.source, "test.esp");
616 assert_eq!(r.destination, "");
617 assert_eq!(r.priority, 0);
618 assert!(!r.always_install);
619 assert!(!r.install_if_usable);
620 }
621
622 #[test]
623 fn parse_file_and_folder_mix() {
624 let xml = r#"
625 <config><moduleName>T</moduleName>
626 <requiredInstallFiles>
627 <file source="a.esp" destination="Data"/>
628 <folder source="meshes" destination="Data/meshes"/>
629 <file source="b.esp" destination="Data" priority="10"/>
630 </requiredInstallFiles></config>
631 "#;
632 let config = ModuleConfig::parse(xml).unwrap();
633 let items = &config.required_install_files.as_ref().unwrap().items;
634 assert_eq!(items.len(), 3);
635 assert!(!items[0].is_folder());
636 assert!(items[1].is_folder());
637 assert!(!items[2].is_folder());
638 assert_eq!(items[2].file_ref().priority, 10);
639 }
640
641 #[test]
644 fn parse_sort_orders() {
645 for (order_str, expected) in [
646 ("Explicit", SortOrder::Explicit),
647 ("Ascending", SortOrder::Ascending),
648 ("Descending", SortOrder::Descending),
649 ] {
650 let xml = format!(
651 r#"<config><moduleName>T</moduleName>
652 <installSteps order="{order_str}">
653 <installStep name="S">
654 <optionalFileGroups><group name="G" type="SelectAny">
655 <plugins><plugin name="P"><typeDescriptor><type name="Optional"/></typeDescriptor></plugin></plugins>
656 </group></optionalFileGroups>
657 </installStep></installSteps></config>"#
658 );
659 let config = ModuleConfig::parse(&xml).unwrap();
660 assert_eq!(
661 config.install_steps.as_ref().unwrap().order,
662 Some(expected),
663 "Failed for {order_str}"
664 );
665 }
666 }
667
668 #[test]
671 fn parse_empty_required_files() {
672 let xml = r#"
673 <config><moduleName>T</moduleName>
674 <requiredInstallFiles></requiredInstallFiles></config>
675 "#;
676 let config = ModuleConfig::parse(xml).unwrap();
677 assert!(config.required_install_files.as_ref().unwrap().items.is_empty());
678 }
679
680 #[test]
681 fn parse_empty_install_steps() {
682 let xml = r#"
683 <config><moduleName>T</moduleName>
684 <installSteps></installSteps></config>
685 "#;
686 let config = ModuleConfig::parse(xml).unwrap();
687 assert!(config.install_steps.as_ref().unwrap().steps.is_empty());
688 }
689
690 #[test]
691 fn parse_unicode_names() {
692 let xml = r#"
693 <config><moduleName>日本語MOD</moduleName>
694 <installSteps><installStep name="ステップ1">
695 <optionalFileGroups><group name="グループ" type="SelectAny">
696 <plugins><plugin name="プラグイン"><typeDescriptor><type name="Optional"/></typeDescriptor></plugin></plugins>
697 </group></optionalFileGroups>
698 </installStep></installSteps></config>
699 "#;
700 let config = ModuleConfig::parse(xml).unwrap();
701 assert_eq!(config.module_name.value, "日本語MOD");
702 let step = &config.install_steps.as_ref().unwrap().steps[0];
703 assert_eq!(step.name, "ステップ1");
704 }
705
706 #[test]
709 fn parse_conditional_file_installs() {
710 let xml = r#"
711 <config><moduleName>T</moduleName>
712 <conditionalFileInstalls><patterns>
713 <pattern>
714 <dependencies operator="And">
715 <flagDependency flag="f" value="v"/>
716 </dependencies>
717 <files><file source="a.esp" destination="Data"/></files>
718 </pattern>
719 <pattern>
720 <dependencies operator="Or">
721 <flagDependency flag="x" value="1"/>
722 <flagDependency flag="y" value="2"/>
723 </dependencies>
724 <files>
725 <folder source="dir" destination="Data/dir"/>
726 </files>
727 </pattern>
728 </patterns></conditionalFileInstalls></config>
729 "#;
730 let config = ModuleConfig::parse(xml).unwrap();
731 let cfi = config.conditional_file_installs.as_ref().unwrap();
732 assert_eq!(cfi.patterns.patterns.len(), 2);
733 assert_eq!(cfi.patterns.patterns[0].files.items.len(), 1);
734 assert_eq!(cfi.patterns.patterns[1].files.items.len(), 1);
735 }
736
737 #[test]
740 fn parse_condition_flags() {
741 let xml = r#"
742 <config><moduleName>T</moduleName>
743 <installSteps><installStep name="S">
744 <optionalFileGroups><group name="G" type="SelectAny">
745 <plugins><plugin name="P">
746 <conditionFlags>
747 <flag name="flag_a">value_a</flag>
748 <flag name="flag_b">value_b</flag>
749 </conditionFlags>
750 <typeDescriptor><type name="Optional"/></typeDescriptor>
751 </plugin></plugins>
752 </group></optionalFileGroups>
753 </installStep></installSteps></config>
754 "#;
755 let config = ModuleConfig::parse(xml).unwrap();
756 let plugin = &config.install_steps.as_ref().unwrap().steps[0]
757 .optional_file_groups
758 .as_ref()
759 .unwrap()
760 .groups[0]
761 .plugins
762 .plugins[0];
763 let flags = plugin.condition_flags.as_ref().unwrap();
764 assert_eq!(flags.flags.len(), 2);
765 assert_eq!(flags.flags[0].name, "flag_a");
766 assert_eq!(flags.flags[0].value, "value_a");
767 assert_eq!(flags.flags[1].name, "flag_b");
768 assert_eq!(flags.flags[1].value, "value_b");
769 }
770}