1use crate::xml::{
7 DecomposeRule, DisassembleXmlFileHandler, MultiLevelRule, ReassembleXmlFileHandler, SidecarSpec,
8};
9
10pub struct DisassembleOpts<'a> {
12 pub path: Option<&'a str>,
13 pub unique_id_elements: Option<&'a str>,
14 pub pre_purge: bool,
15 pub post_purge: bool,
16 pub ignore_path: Option<&'a str>,
22 pub format: &'a str,
23 pub strategy: Option<&'a str>,
24 pub multi_level: Option<String>,
25 pub split_tags: Option<String>,
26 pub sidecar_elements: Option<String>,
28}
29
30pub fn parse_decompose_spec(spec: &str) -> Vec<DecomposeRule> {
34 let mut rules = Vec::new();
35 for part in spec.split(',') {
36 let part = part.trim();
37 let segments: Vec<&str> = part.splitn(4, ':').collect();
38 if segments.len() >= 3 {
39 let tag = segments[0].to_string();
40 let (path_segment, mode, field) = if segments.len() == 3 {
41 (
42 tag.clone(),
43 segments[1].to_string(),
44 segments[2].to_string(),
45 )
46 } else {
47 (
48 segments[1].to_string(),
49 segments[2].to_string(),
50 segments[3].to_string(),
51 )
52 };
53 if !tag.is_empty() && !mode.is_empty() && !field.is_empty() {
54 rules.push(DecomposeRule {
55 tag,
56 path_segment,
57 mode,
58 field,
59 });
60 }
61 }
62 }
63 rules
64}
65
66pub fn parse_multi_level_spec(spec: &str) -> Option<MultiLevelRule> {
68 let parts: Vec<&str> = spec.splitn(3, ':').collect();
69 if parts.len() != 3 {
70 return None;
71 }
72 let (file_pattern, root_to_strip, unique_id_elements) = (parts[0], parts[1], parts[2]);
73 if file_pattern.is_empty() || root_to_strip.is_empty() || unique_id_elements.is_empty() {
74 return None;
75 }
76 let path_segment = crate::xml::path_segment_from_file_pattern(file_pattern);
77 Some(MultiLevelRule {
78 file_pattern: file_pattern.to_string(),
79 root_to_strip: root_to_strip.to_string(),
80 unique_id_elements: unique_id_elements.to_string(),
81 path_segment: path_segment.clone(),
82 wrap_root_element: root_to_strip.to_string(),
83 wrap_xmlns: String::new(),
84 })
85}
86
87pub fn parse_multi_level_specs(spec: &str) -> Vec<MultiLevelRule> {
94 spec.split(';')
95 .map(str::trim)
96 .filter(|s| !s.is_empty())
97 .filter_map(parse_multi_level_spec)
98 .collect()
99}
100
101pub fn parse_disassemble_args(args: &[String]) -> DisassembleOpts<'_> {
115 let mut path = None;
116 let mut unique_id_elements = None;
117 let mut pre_purge = false;
118 let mut post_purge = false;
119 let mut ignore_path: Option<&str> = None;
120 let mut format = "xml";
121 let mut strategy = None;
122 let mut multi_level = None;
123 let mut split_tags = None;
124 let mut sidecar_elements = None;
125
126 let mut iter = args.iter();
127 while let Some(arg) = iter.next() {
128 if arg == "--postpurge" {
129 post_purge = true;
130 } else if arg == "--prepurge" {
131 pre_purge = true;
132 } else if let Some(rest) = arg.strip_prefix("--unique-id-elements=") {
133 unique_id_elements = Some(rest);
134 } else if arg == "--unique-id-elements" {
135 if let Some(value) = iter.next() {
136 unique_id_elements = Some(value.as_str());
137 }
138 } else if let Some(rest) = arg.strip_prefix("--ignore-path=") {
139 ignore_path = Some(rest);
140 } else if arg == "--ignore-path" {
141 if let Some(value) = iter.next() {
142 ignore_path = Some(value.as_str());
143 }
144 } else if let Some(rest) = arg.strip_prefix("--format=") {
145 format = rest;
146 } else if arg == "--format" {
147 if let Some(value) = iter.next() {
148 format = value.as_str();
149 }
150 } else if let Some(rest) = arg.strip_prefix("--strategy=") {
151 strategy = Some(rest);
152 } else if arg == "--strategy" {
153 if let Some(value) = iter.next() {
154 strategy = Some(value.as_str());
155 }
156 } else if let Some(rest) = arg.strip_prefix("--multi-level=") {
157 multi_level = Some(rest.to_string());
158 } else if arg == "--multi-level" {
159 if let Some(value) = iter.next() {
160 multi_level = Some(value.clone());
161 }
162 } else if let Some(rest) = arg.strip_prefix("--split-tags=") {
163 split_tags = Some(rest.to_string());
164 } else if arg == "--split-tags" || arg == "-p" {
165 if let Some(value) = iter.next() {
166 split_tags = Some(value.clone());
167 }
168 } else if let Some(rest) = arg.strip_prefix("--sidecar-elements=") {
169 sidecar_elements = Some(rest.to_string());
170 } else if arg == "--sidecar-elements" {
171 if let Some(value) = iter.next() {
172 sidecar_elements = Some(value.clone());
173 }
174 } else if arg.starts_with("--") {
175 } else if path.is_none() {
179 path = Some(arg.as_str());
180 }
181 }
185
186 DisassembleOpts {
187 path,
188 unique_id_elements,
189 pre_purge,
190 post_purge,
191 ignore_path,
192 format,
193 strategy,
194 multi_level,
195 split_tags,
196 sidecar_elements,
197 }
198}
199
200pub fn parse_reassemble_args(args: &[String]) -> (Option<&str>, Option<&str>, bool) {
202 let mut path = None;
203 let mut extension = None;
204 let mut post_purge = false;
205 for arg in args.iter() {
206 if arg == "--postpurge" {
207 post_purge = true;
208 } else if path.is_none() {
209 path = Some(arg.as_str());
210 } else if extension.is_none() {
211 extension = Some(arg.as_str());
212 }
213 }
214 (path, extension, post_purge)
215}
216
217pub fn parse_sidecar_specs(spec: &str) -> Vec<SidecarSpec> {
223 spec.split(',')
224 .map(str::trim)
225 .filter_map(|pair| {
226 let (element, extension) = pair.split_once(':')?;
227 let element = element.trim().to_string();
228 let extension = extension.trim().to_string();
229 if element.is_empty() || extension.is_empty() {
230 return None;
231 }
232 Some(SidecarSpec {
233 element,
234 extension,
235 original_format: None,
236 })
237 })
238 .collect()
239}
240
241pub fn print_usage() {
243 eprintln!("Usage: xml-disassembler <command> [options]");
244 eprintln!(" disassemble <path> [options] - Disassemble XML file or directory");
245 eprintln!(" --postpurge - Delete original file/dir after disassembling (default: false)");
246 eprintln!(" --prepurge - Remove existing disassembly output before running (default: false)");
247 eprintln!(
248 " --unique-id-elements <list> - Comma-separated element names for nested filenames"
249 );
250 eprintln!(" --ignore-path <path> - Path to ignore file (default: .cdignore; falls back to .xmldisassemblerignore for backward compatibility)");
251 eprintln!(
252 " --format <fmt> - Output format: xml, json, json5, yaml (default: xml)"
253 );
254 eprintln!(
255 " --strategy <name> - unique-id or grouped-by-tag (default: unique-id)"
256 );
257 eprintln!(" --multi-level <spec> - Further disassemble matching files: file_pattern:root_to_strip:unique_id_elements (multiple rules separated by ';')");
258 eprintln!(" -p, --split-tags <spec> - With grouped-by-tag: split/group nested tags (e.g. objectPermissions:split:object,fieldPermissions:group:field)");
259 eprintln!(" --sidecar-elements <spec> - Extract element text to companion files: element:extension (comma-separated, e.g. schema:yaml)");
260 eprintln!(" reassemble <path> [extension] [--postpurge] - Reassemble directory (default extension: xml); sidecar specs are auto-detected from .sidecars.json");
261}
262
263fn should_print_usage(args_len: usize) -> bool {
267 args_len < 2
268}
269
270fn multi_level_spec_failed_to_parse(spec_present: bool, parsed_empty: bool) -> bool {
274 spec_present && parsed_empty
275}
276
277fn should_parse_decompose_rules(strategy: &str) -> bool {
282 strategy == "grouped-by-tag"
283}
284
285pub async fn run(args: Vec<String>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
287 if should_print_usage(args.len()) {
288 print_usage();
289 return Ok(());
290 }
291
292 let command = &args[1];
293 match command.as_str() {
294 "disassemble" => run_disassemble(&args[2..]).await?,
295 "reassemble" => run_reassemble(&args[2..]).await?,
296 _ => {
297 eprintln!("Unknown command: {}", command);
298 }
299 }
300
301 Ok(())
302}
303
304async fn run_disassemble(args: &[String]) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
305 let opts = parse_disassemble_args(args);
306 let path = opts.path.unwrap_or(".");
307 let strategy = opts.strategy.unwrap_or("unique-id");
308 let multi_level_rules: Vec<MultiLevelRule> = opts
309 .multi_level
310 .as_deref()
311 .map(parse_multi_level_specs)
312 .unwrap_or_default();
313 if multi_level_spec_failed_to_parse(opts.multi_level.is_some(), multi_level_rules.is_empty()) {
314 eprintln!("Invalid --multi-level spec; use file_pattern:root_to_strip:unique_id_elements (multiple rules separated by ';')");
315 }
316 let decompose_rules: Vec<DecomposeRule> = if should_parse_decompose_rules(strategy) {
317 opts.split_tags
318 .as_ref()
319 .map(|s| parse_decompose_spec(s))
320 .unwrap_or_default()
321 } else {
322 Vec::new()
323 };
324 let decompose_rules_ref = if decompose_rules.is_empty() {
325 None
326 } else {
327 Some(decompose_rules.as_slice())
328 };
329 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::Path::new(".").to_path_buf());
330 let resolved_ignore = crate::ignore_file::resolve_xml_ignore_path(opts.ignore_path, &cwd);
331 let multi_level_rules_ref = if multi_level_rules.is_empty() {
332 None
333 } else {
334 Some(multi_level_rules.as_slice())
335 };
336 let sidecar_specs: Vec<SidecarSpec> = opts
337 .sidecar_elements
338 .as_deref()
339 .map(parse_sidecar_specs)
340 .unwrap_or_default();
341 let sidecar_specs_ref = if sidecar_specs.is_empty() {
342 None
343 } else {
344 Some(sidecar_specs.as_slice())
345 };
346 let mut handler = DisassembleXmlFileHandler::new();
347 handler
348 .disassemble(
349 path,
350 opts.unique_id_elements,
351 Some(strategy),
352 opts.pre_purge,
353 opts.post_purge,
354 &resolved_ignore,
355 opts.format,
356 multi_level_rules_ref,
357 decompose_rules_ref,
358 sidecar_specs_ref,
359 None,
360 )
361 .await?;
362 Ok(())
363}
364
365async fn run_reassemble(args: &[String]) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
366 let (path, extension, post_purge) = parse_reassemble_args(args);
367 let path = path.unwrap_or(".");
368 let handler = ReassembleXmlFileHandler::new();
369 handler
370 .reassemble(path, extension.or(Some("xml")), post_purge, None)
371 .await?;
372 Ok(())
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 fn sv(s: &str) -> String {
380 s.to_string()
381 }
382
383 #[test]
384 fn parse_decompose_spec_three_segments_defaults_path_segment_to_tag() {
385 let rules = parse_decompose_spec("objectPermissions:split:object");
386 assert_eq!(rules.len(), 1);
387 let r = &rules[0];
388 assert_eq!(r.tag, "objectPermissions");
389 assert_eq!(r.path_segment, "objectPermissions");
390 assert_eq!(r.mode, "split");
391 assert_eq!(r.field, "object");
392 }
393
394 #[test]
395 fn parse_decompose_spec_four_segments_uses_explicit_path_segment() {
396 let rules = parse_decompose_spec("fieldPermissions:fieldPerms:group:field");
397 assert_eq!(rules.len(), 1);
398 let r = &rules[0];
399 assert_eq!(r.tag, "fieldPermissions");
400 assert_eq!(r.path_segment, "fieldPerms");
401 assert_eq!(r.mode, "group");
402 assert_eq!(r.field, "field");
403 }
404
405 #[test]
406 fn parse_decompose_spec_comma_separated_rules_trims_whitespace() {
407 let rules = parse_decompose_spec("a:split:f, b:group:g , c:x:split:y");
408 assert_eq!(rules.len(), 3);
409 assert_eq!(rules[0].tag, "a");
410 assert_eq!(rules[1].tag, "b");
411 assert_eq!(rules[2].tag, "c");
412 assert_eq!(rules[2].path_segment, "x");
413 }
414
415 #[test]
416 fn parse_decompose_spec_rejects_empty_segments() {
417 assert!(parse_decompose_spec("only:two").is_empty());
419 assert!(parse_decompose_spec(":split:field").is_empty());
421 assert!(parse_decompose_spec("tag::field").is_empty());
422 assert!(parse_decompose_spec("tag:split:").is_empty());
423 }
424
425 #[test]
426 fn parse_multi_level_spec_valid_returns_rule() {
427 let rule = parse_multi_level_spec(
428 "programProcesses-meta:LoyaltyProgramSetup:parameterName,ruleName",
429 )
430 .unwrap();
431 assert_eq!(rule.file_pattern, "programProcesses-meta");
432 assert_eq!(rule.root_to_strip, "LoyaltyProgramSetup");
433 assert_eq!(rule.unique_id_elements, "parameterName,ruleName");
434 assert_eq!(rule.path_segment, "programProcesses");
435 assert_eq!(rule.wrap_root_element, "LoyaltyProgramSetup");
436 assert!(rule.wrap_xmlns.is_empty());
437 }
438
439 #[test]
440 fn parse_multi_level_spec_rejects_wrong_parts() {
441 assert!(parse_multi_level_spec("only:two").is_none());
442 assert!(parse_multi_level_spec(":Root:ids").is_none());
443 assert!(parse_multi_level_spec("file::ids").is_none());
444 assert!(parse_multi_level_spec("file:Root:").is_none());
445 }
446
447 #[test]
448 fn parse_multi_level_specs_single_rule_returns_one() {
449 let rules = parse_multi_level_specs("a-meta:Root:id");
450 assert_eq!(rules.len(), 1);
451 assert_eq!(rules[0].file_pattern, "a-meta");
452 }
453
454 #[test]
455 fn parse_multi_level_specs_semicolon_separates_rules() {
456 let rules =
457 parse_multi_level_specs("a-meta:RootA:id1,id2; b-meta:RootB:other ; c-meta:RootC:k");
458 assert_eq!(rules.len(), 3);
459 assert_eq!(rules[0].file_pattern, "a-meta");
460 assert_eq!(rules[0].unique_id_elements, "id1,id2");
461 assert_eq!(rules[1].file_pattern, "b-meta");
462 assert_eq!(rules[1].root_to_strip, "RootB");
463 assert_eq!(rules[2].file_pattern, "c-meta");
464 }
465
466 #[test]
467 fn parse_multi_level_specs_skips_empty_and_malformed() {
468 let rules = parse_multi_level_specs("a:R:id; ; bad ; b:R:id;");
470 assert_eq!(rules.len(), 2);
471 assert_eq!(rules[0].file_pattern, "a");
472 assert_eq!(rules[1].file_pattern, "b");
473 }
474
475 #[test]
476 fn parse_multi_level_specs_empty_string_returns_empty() {
477 assert!(parse_multi_level_specs("").is_empty());
478 assert!(parse_multi_level_specs(" ; ;").is_empty());
479 }
480
481 #[test]
482 fn parse_disassemble_args_handles_flags_and_eq_forms() {
483 let args = [
484 "path/to/file.xml",
485 "--postpurge",
486 "--prepurge",
487 "--unique-id-elements=name,id",
488 "--ignore-path=.foo",
489 "--format=json",
490 "--strategy=grouped-by-tag",
491 "--multi-level=pattern:Root:ids",
492 "--split-tags=a:split:b",
493 ]
494 .iter()
495 .map(|s| sv(s))
496 .collect::<Vec<_>>();
497 let opts = parse_disassemble_args(&args);
498 assert_eq!(opts.path, Some("path/to/file.xml"));
499 assert!(opts.pre_purge);
500 assert!(opts.post_purge);
501 assert_eq!(opts.unique_id_elements, Some("name,id"));
502 assert_eq!(opts.ignore_path, Some(".foo"));
503 assert_eq!(opts.format, "json");
504 assert_eq!(opts.strategy, Some("grouped-by-tag"));
505 assert_eq!(opts.multi_level.as_deref(), Some("pattern:Root:ids"));
506 assert_eq!(opts.split_tags.as_deref(), Some("a:split:b"));
507 }
508
509 #[test]
510 fn parse_disassemble_args_handles_space_separated_forms() {
511 let args = [
512 "file.xml",
513 "--unique-id-elements",
514 "name",
515 "--ignore-path",
516 ".gitignore",
517 "--format",
518 "yaml",
519 "--strategy",
520 "unique-id",
521 "--multi-level",
522 "p:R:ids",
523 "--split-tags",
524 "t:split:f",
525 ]
526 .iter()
527 .map(|s| sv(s))
528 .collect::<Vec<_>>();
529 let opts = parse_disassemble_args(&args);
530 assert_eq!(opts.path, Some("file.xml"));
531 assert_eq!(opts.unique_id_elements, Some("name"));
532 assert_eq!(opts.ignore_path, Some(".gitignore"));
533 assert_eq!(opts.format, "yaml");
534 assert_eq!(opts.strategy, Some("unique-id"));
535 assert_eq!(opts.multi_level.as_deref(), Some("p:R:ids"));
536 assert_eq!(opts.split_tags.as_deref(), Some("t:split:f"));
537 }
538
539 #[test]
540 fn parse_disassemble_args_space_form_value_is_not_misread_as_positional_path() {
541 let cases: &[(&[&str], &str)] = &[
548 (&["--unique-id-elements", "name"], "name"),
549 (&["--ignore-path", ".foo"], ".foo"),
550 (&["--format", "yaml"], "yaml"),
551 (&["--strategy", "grouped-by-tag"], "grouped-by-tag"),
552 (&["--multi-level", "p:R:ids"], "p:R:ids"),
553 (&["--split-tags", "t:split:f"], "t:split:f"),
554 (&["-p", "t:split:f"], "t:split:f"),
555 ];
556 for (args, expected_value) in cases {
557 let owned: Vec<String> = args.iter().map(|s| sv(s)).collect();
558 let opts = parse_disassemble_args(&owned);
559 assert!(
560 opts.path.is_none(),
561 "args {args:?}: value `{expected_value}` was incorrectly captured as path"
562 );
563 }
564 }
565
566 #[test]
567 fn parse_disassemble_args_space_form_value_missing_does_not_panic() {
568 let options = [
573 "--unique-id-elements",
574 "--ignore-path",
575 "--format",
576 "--strategy",
577 "--multi-level",
578 "--split-tags",
579 "-p",
580 ];
581 for opt in options {
582 let args = [opt].iter().map(|s| sv(s)).collect::<Vec<_>>();
583 let opts = parse_disassemble_args(&args);
584 assert!(opts.path.is_none(), "bare option `{opt}` set a path");
587 }
588 }
589
590 #[test]
591 fn parse_disassemble_args_p_alias_for_split_tags() {
592 let args = ["file.xml", "-p", "a:split:b"]
593 .iter()
594 .map(|s| sv(s))
595 .collect::<Vec<_>>();
596 let opts = parse_disassemble_args(&args);
597 assert_eq!(opts.split_tags.as_deref(), Some("a:split:b"));
598 }
599
600 #[test]
601 fn parse_disassemble_args_unknown_long_flag_is_skipped() {
602 let args = ["file.xml", "--unknown"]
603 .iter()
604 .map(|s| sv(s))
605 .collect::<Vec<_>>();
606 let opts = parse_disassemble_args(&args);
607 assert_eq!(opts.path, Some("file.xml"));
608 }
609
610 #[test]
611 fn parse_disassemble_args_defaults_when_empty() {
612 let opts = parse_disassemble_args(&[]);
613 assert!(opts.path.is_none());
614 assert!(opts.strategy.is_none());
615 assert!(opts.unique_id_elements.is_none());
616 assert!(!opts.pre_purge);
617 assert!(!opts.post_purge);
618 assert!(
619 opts.ignore_path.is_none(),
620 "default is `None` so the runner can pick the right filename"
621 );
622 assert_eq!(opts.format, "xml");
623 }
624
625 #[test]
626 fn parse_disassemble_args_space_forms_without_value_leave_default() {
627 let args = ["--unique-id-elements"]
628 .iter()
629 .map(|s| sv(s))
630 .collect::<Vec<_>>();
631 let opts = parse_disassemble_args(&args);
632 assert!(opts.unique_id_elements.is_none());
633 }
634
635 #[test]
636 fn parse_disassemble_args_trailing_extra_positional_ignored() {
637 let args = ["first.xml", "second.xml"]
638 .iter()
639 .map(|s| sv(s))
640 .collect::<Vec<_>>();
641 let opts = parse_disassemble_args(&args);
642 assert_eq!(opts.path, Some("first.xml"));
643 }
644
645 #[test]
646 fn parse_reassemble_args_picks_path_extension_and_flag() {
647 let args = ["some/dir", "json", "--postpurge"]
648 .iter()
649 .map(|s| sv(s))
650 .collect::<Vec<_>>();
651 let (path, ext, purge) = parse_reassemble_args(&args);
652 assert_eq!(path, Some("some/dir"));
653 assert_eq!(ext, Some("json"));
654 assert!(purge);
655 }
656
657 #[test]
658 fn parse_reassemble_args_defaults_and_extra_args_ignored() {
659 let (p, e, purge) = parse_reassemble_args(&[]);
660 assert!(p.is_none());
661 assert!(e.is_none());
662 assert!(!purge);
663
664 let args = ["dir", "xml", "extra"]
665 .iter()
666 .map(|s| sv(s))
667 .collect::<Vec<_>>();
668 let (p, e, _) = parse_reassemble_args(&args);
669 assert_eq!(p, Some("dir"));
670 assert_eq!(e, Some("xml"));
671 }
672
673 #[tokio::test]
674 async fn run_no_args_prints_usage_and_succeeds() {
675 run(vec![sv("xml-disassembler")]).await.unwrap();
676 }
677
678 #[tokio::test]
679 async fn run_unknown_command_is_not_an_error() {
680 run(vec![sv("xml-disassembler"), sv("unknown")])
681 .await
682 .unwrap();
683 }
684
685 #[tokio::test]
686 async fn run_reassemble_missing_path_returns_err() {
687 let err = run(vec![
689 sv("xml-disassembler"),
690 sv("reassemble"),
691 sv("/definitely/not/here/xyz"),
692 ])
693 .await;
694 assert!(err.is_err());
695 }
696
697 #[tokio::test]
698 async fn run_disassemble_writes_expected_output() {
699 let dir = tempfile::tempdir().unwrap();
700 let xml_path = dir.path().join("sample.xml");
701 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
702<Root xmlns="http://example.com">
703 <child><name>one</name></child>
704 <child><name>two</name></child>
705</Root>"#;
706 std::fs::write(&xml_path, xml).unwrap();
707 run(vec![
708 sv("xml-disassembler"),
709 sv("disassemble"),
710 xml_path.to_string_lossy().to_string(),
711 ])
712 .await
713 .unwrap();
714 assert!(dir.path().join("sample").exists());
715 }
716
717 #[tokio::test]
718 async fn run_disassemble_with_invalid_multi_level_spec_warns_and_continues() {
719 let dir = tempfile::tempdir().unwrap();
720 let xml_path = dir.path().join("sample.xml");
721 let xml =
722 r#"<?xml version="1.0" encoding="UTF-8"?><Root><child><name>a</name></child></Root>"#;
723 std::fs::write(&xml_path, xml).unwrap();
724 run(vec![
725 sv("xml-disassembler"),
726 sv("disassemble"),
727 xml_path.to_string_lossy().to_string(),
728 sv("--multi-level=bad-spec"),
729 ])
730 .await
731 .unwrap();
732 }
733
734 #[tokio::test]
735 async fn run_reassemble_on_existing_directory_succeeds() {
736 let dir = tempfile::tempdir().unwrap();
738 let xml_path = dir.path().join("reasm.xml");
739 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
740<Root><child><name>one</name></child><child><name>two</name></child></Root>"#;
741 std::fs::write(&xml_path, xml).unwrap();
742 run(vec![
743 sv("xml-disassembler"),
744 sv("disassemble"),
745 xml_path.to_string_lossy().to_string(),
746 ])
747 .await
748 .unwrap();
749 let disassembled_dir = dir.path().join("reasm");
750 assert!(disassembled_dir.exists());
751 run(vec![
752 sv("xml-disassembler"),
753 sv("reassemble"),
754 disassembled_dir.to_string_lossy().to_string(),
755 ])
756 .await
757 .unwrap();
758 }
759
760 #[tokio::test]
761 async fn run_disassemble_with_grouped_by_tag_no_split_tags_uses_empty_decompose_rules() {
762 let dir = tempfile::tempdir().unwrap();
765 let xml_path = dir.path().join("sample.xml");
766 let xml =
767 r#"<?xml version="1.0" encoding="UTF-8"?><Root><a><n>1</n></a><b><n>2</n></b></Root>"#;
768 std::fs::write(&xml_path, xml).unwrap();
769 run(vec![
770 sv("xml-disassembler"),
771 sv("disassemble"),
772 xml_path.to_string_lossy().to_string(),
773 sv("--strategy=grouped-by-tag"),
774 ])
775 .await
776 .unwrap();
777 }
778
779 #[tokio::test]
780 async fn run_disassemble_with_grouped_by_tag_split_tags_runs() {
781 let dir = tempfile::tempdir().unwrap();
782 let xml_path = dir.path().join("perms.xml");
783 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
784<Root>
785 <objectPermissions><object>A</object><allowRead>true</allowRead></objectPermissions>
786 <objectPermissions><object>B</object><allowRead>false</allowRead></objectPermissions>
787</Root>"#;
788 std::fs::write(&xml_path, xml).unwrap();
789 run(vec![
790 sv("xml-disassembler"),
791 sv("disassemble"),
792 xml_path.to_string_lossy().to_string(),
793 sv("--strategy=grouped-by-tag"),
794 sv("-p"),
795 sv("objectPermissions:split:object"),
796 ])
797 .await
798 .unwrap();
799 }
800
801 #[test]
802 fn should_print_usage_only_for_fewer_than_two_args() {
803 assert!(should_print_usage(0));
806 assert!(should_print_usage(1));
807 assert!(!should_print_usage(2));
808 assert!(!should_print_usage(3));
809 }
810
811 #[test]
812 fn multi_level_spec_failed_to_parse_requires_both_conditions() {
813 assert!(multi_level_spec_failed_to_parse(true, true));
816 assert!(!multi_level_spec_failed_to_parse(true, false));
817 assert!(!multi_level_spec_failed_to_parse(false, true));
818 assert!(!multi_level_spec_failed_to_parse(false, false));
819 }
820
821 #[test]
822 fn should_parse_decompose_rules_only_for_grouped_by_tag() {
823 assert!(should_parse_decompose_rules("grouped-by-tag"));
829 assert!(!should_parse_decompose_rules("unique-id"));
830 assert!(!should_parse_decompose_rules(""));
831 assert!(!should_parse_decompose_rules("Grouped-By-Tag"));
832 }
833
834 #[tokio::test]
835 async fn run_disassemble_with_valid_multi_level_spec_passes_rules_slice() {
836 let dir = tempfile::tempdir().unwrap();
839 let xml_path = dir.path().join("sample.xml");
840 let xml =
841 r#"<?xml version="1.0" encoding="UTF-8"?><Root><child><name>a</name></child></Root>"#;
842 std::fs::write(&xml_path, xml).unwrap();
843 run(vec![
844 sv("xml-disassembler"),
845 sv("disassemble"),
846 xml_path.to_string_lossy().to_string(),
847 sv("--multi-level=child:Root:name"),
848 ])
849 .await
850 .unwrap();
851 }
852
853 #[test]
854 fn parse_sidecar_specs_empty_element_is_dropped() {
855 let specs = parse_sidecar_specs(":yaml");
857 assert!(specs.is_empty(), "expected no specs, got: {specs:?}");
858 }
859
860 #[test]
861 fn parse_sidecar_specs_empty_extension_is_dropped() {
862 let specs = parse_sidecar_specs("schema:");
864 assert!(specs.is_empty(), "expected no specs, got: {specs:?}");
865 }
866}