1#[derive(Debug)]
3pub struct Map {
4 pub components: Vec<Component>,
5 pub dependencies: Vec<Dependency>,
6 pub notes: Vec<Note>,
7 pub stages: Vec<StageData>,
8 pub groups: Vec<Group>,
9 pub inertias: Vec<Inertia>,
10 pub evolutions: Vec<Evolution>,
11}
12
13#[derive(Debug, PartialEq)]
15pub enum Shape {
16 Circle,
17 X,
18 Square,
19 Triangle,
20}
21
22#[derive(Debug, PartialEq)]
24pub enum Stage {
25 I,
26 Ii,
27 Iii,
28 Iv,
29}
30
31#[derive(Debug, PartialEq)]
33pub struct Component {
34 pub label: String,
35 pub coordinates: (f64, f64),
36 pub shape: Shape,
37}
38
39#[derive(Debug, PartialEq)]
41pub struct Dependency {
42 pub from: String,
43 pub to: String,
44 pub is_directed: bool,
45}
46
47#[derive(Debug, PartialEq)]
49pub struct Note {
50 pub text: String,
51 pub coordinates: (f64, f64),
52}
53
54#[derive(Debug, PartialEq)]
56pub struct StageData {
57 pub stage: Stage,
58 pub value: f64,
59}
60
61#[derive(Debug, PartialEq)]
63pub struct Group {
64 pub components: Vec<String>,
65}
66
67#[derive(Debug, PartialEq)]
69pub struct Inertia {
70 pub component: String,
71}
72
73#[derive(Debug, PartialEq)]
75pub struct Evolution {
76 pub component: String,
77 pub value: f64,
78}
79
80#[must_use]
82pub fn parse(source: &str) -> Map {
83 let estimated_lines = source.len() / 30;
85
86 let mut components = Vec::with_capacity(estimated_lines);
87 let mut dependencies = Vec::with_capacity(estimated_lines);
88 let mut notes = Vec::with_capacity(estimated_lines / 10);
89 let mut stages = Vec::with_capacity(4);
90 let mut groups = Vec::with_capacity(estimated_lines / 20);
91 let mut inertias = Vec::with_capacity(estimated_lines / 20);
92 let mut evolutions = Vec::with_capacity(estimated_lines / 20);
93
94 let bytes = source.as_bytes();
95 let mut i = 0;
96 let len = bytes.len();
97
98 while i < len {
99 while i < len && (bytes[i] == b'\n' || bytes[i] == b'\r') {
101 i += 1;
102 }
103
104 let line_start = i;
105
106 while i < len && bytes[i] != b'\n' && bytes[i] != b'\r' {
108 i += 1;
109 }
110
111 let line = source[line_start..i].trim();
112
113 if line.is_empty() {
114 continue;
115 }
116
117 let bytes = line.as_bytes();
119 if bytes[0] == b'[' {
120 parse_keyword_line(
121 line,
122 &mut stages,
123 &mut notes,
124 &mut groups,
125 &mut inertias,
126 &mut evolutions,
127 );
128 } else if has_dependency(bytes) {
129 if let Some(dep) = parse_dependency(line) {
130 dependencies.push(dep);
131 }
132 } else if let Some(comp) = parse_component(line) {
133 components.push(comp);
134 }
135 }
136
137 Map {
138 components,
139 dependencies,
140 notes,
141 stages,
142 groups,
143 inertias,
144 evolutions,
145 }
146}
147
148#[inline]
150fn has_dependency(bytes: &[u8]) -> bool {
151 for i in 0..bytes.len() {
152 if bytes[i] == b'(' {
153 return false;
154 }
155 if bytes[i] == b'-' && i + 1 < bytes.len() {
156 let next = bytes[i + 1];
157 if next == b'>' || next == b'-' {
158 return true;
159 }
160 }
161 }
162 false
163}
164
165fn parse_keyword_line(
169 line: &str,
170 stages: &mut Vec<StageData>,
171 notes: &mut Vec<Note>,
172 groups: &mut Vec<Group>,
173 inertias: &mut Vec<Inertia>,
174 evolutions: &mut Vec<Evolution>,
175) {
176 let bytes = line.as_bytes();
177 let mut i = 0;
178
179 i += 1;
181
182 let keyword_start = i;
183 while i < bytes.len() && bytes[i] != b']' {
184 i += 1;
185 }
186 if i >= bytes.len() {
187 return;
188 }
189
190 let keyword = &line[keyword_start..i];
191 i += 1;
192
193 i = skip_whitespace(bytes, i);
194 let rest = &line[i..];
195
196 if keyword.eq_ignore_ascii_case("i")
197 || keyword.eq_ignore_ascii_case("ii")
198 || keyword.eq_ignore_ascii_case("iii")
199 || keyword.eq_ignore_ascii_case("iv")
200 {
201 stages.extend(parse_stage(keyword, rest));
202 } else if keyword.eq_ignore_ascii_case("note") {
203 notes.extend(parse_note(rest));
204 } else if keyword.eq_ignore_ascii_case("group") {
205 groups.extend(parse_group(rest));
206 } else if keyword.eq_ignore_ascii_case("inertia") {
207 let component = rest.trim();
208 if !component.is_empty() {
209 inertias.push(Inertia {
210 component: component.to_string(),
211 });
212 }
213 } else if keyword.eq_ignore_ascii_case("evolution") {
214 evolutions.extend(parse_evolution(rest));
215 }
216}
217
218fn parse_stage(stage_str: &str, rest: &str) -> Option<StageData> {
221 let value = rest.trim().parse::<f64>().ok()?;
222 let stage = if stage_str.eq_ignore_ascii_case("i") {
223 Stage::I
224 } else if stage_str.eq_ignore_ascii_case("ii") {
225 Stage::Ii
226 } else if stage_str.eq_ignore_ascii_case("iii") {
227 Stage::Iii
228 } else {
229 Stage::Iv
230 };
231 Some(StageData { stage, value })
232}
233
234fn parse_note(rest: &str) -> Option<Note> {
236 let bytes = rest.as_bytes();
237 let mut i = skip_whitespace(bytes, 0);
238
239 if i >= bytes.len() || bytes[i] != b'(' {
240 return None;
241 }
242 i += 1;
243
244 let (x, y, end_i) = parse_coordinates(rest, i)?;
245 i = end_i;
246
247 i = skip_whitespace(bytes, i);
248 let text = rest[i..].trim().to_string();
249
250 Some(Note {
251 text,
252 coordinates: (x, y),
253 })
254}
255
256fn parse_group(rest: &str) -> Option<Group> {
258 let components: Vec<String> = rest
259 .split(',')
260 .map(str::trim)
261 .filter(|s| !s.is_empty())
262 .map(std::string::ToString::to_string)
263 .collect();
264
265 if components.is_empty() {
266 None
267 } else {
268 Some(Group { components })
269 }
270}
271
272fn parse_evolution(rest: &str) -> Option<Evolution> {
274 let bytes = rest.as_bytes();
275 let mut position_of_sign = None;
276 let mut sign = 1.0;
277
278 for (i, byte) in bytes.iter().enumerate() {
279 if byte == &b'+' || byte == &b'-' {
280 position_of_sign = Some(i);
281 sign = if byte == &b'-' { -1.0 } else { 1.0 };
282 break;
283 }
284 }
285
286 let i = position_of_sign?;
287 let component = rest[..i].trim().to_string();
288 if component.is_empty() {
289 return None;
290 }
291
292 let value_str = rest[i + 1..].trim();
293 let value = value_str.parse::<f64>().ok()?;
294
295 Some(Evolution {
296 component,
297 value: sign * value,
298 })
299}
300
301fn parse_dependency(line: &str) -> Option<Dependency> {
304 let index_of_arrow = line.find("->");
305 let index_of_dash = line.find("--");
306
307 let index = index_of_arrow.or(index_of_dash)?;
308 let from = line[..index].trim();
309 let to = line[index + 2..].trim();
310
311 if from.is_empty() || to.is_empty() {
312 return None;
313 }
314
315 Some(Dependency {
316 from: from.to_string(),
317 to: to.to_string(),
318 is_directed: index_of_arrow.is_some(),
319 })
320}
321
322fn parse_component(line: &str) -> Option<Component> {
324 let bytes = line.as_bytes();
325 let index_of_parenthesis = line.find('(')?;
326 let label = line[..index_of_parenthesis].trim().to_string();
327 if label.is_empty() {
328 return None;
329 }
330
331 let (x, y, end_i) = parse_coordinates(line, index_of_parenthesis + 1)?;
332
333 let mut i = skip_whitespace(bytes, end_i);
334 let shape = if i < bytes.len() && bytes[i] == b'[' {
335 i += 1;
336 let shape_start = i;
337 while i < bytes.len() && bytes[i] != b']' {
338 i += 1;
339 }
340 if i > shape_start && i < bytes.len() {
341 let shape_label = line[shape_start..i].trim();
342 parse_shape(shape_label)
343 } else {
344 Shape::Circle
345 }
346 } else {
347 Shape::Circle
348 };
349
350 Some(Component {
351 label,
352 coordinates: (x, y),
353 shape,
354 })
355}
356
357#[inline]
359fn parse_shape(shape: &str) -> Shape {
360 if shape.eq_ignore_ascii_case("x") {
361 Shape::X
362 } else if shape.eq_ignore_ascii_case("square") {
363 Shape::Square
364 } else if shape.eq_ignore_ascii_case("triangle") {
365 Shape::Triangle
366 } else {
367 Shape::Circle
368 }
369}
370
371fn parse_coordinates(line: &str, start: usize) -> Option<(f64, f64, usize)> {
373 let bytes = line.as_bytes();
374 let mut i = skip_whitespace(bytes, start);
375
376 let x_start = i;
378 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
379 i += 1;
380 }
381 let x = line[x_start..i].parse::<f64>().ok()?;
382
383 i = skip_whitespace(bytes, i);
385 if i >= bytes.len() || bytes[i] != b',' {
386 return None;
387 }
388 i += 1;
389
390 i = skip_whitespace(bytes, i);
392 let y_start = i;
393 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
394 i += 1;
395 }
396 let y = line[y_start..i].parse::<f64>().ok()?;
397
398 i = skip_whitespace(bytes, i);
400 if i >= bytes.len() || bytes[i] != b')' {
401 return None;
402 }
403 i += 1;
404
405 Some((x, y, i))
406}
407
408#[inline]
412fn skip_whitespace(bytes: &[u8], mut i: usize) -> usize {
413 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
414 i += 1;
415 }
416 i
417}
418
419#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
428 fn should_parse_a_basic_component() {
429 let source = "Dominoes (0.9, 0.5)";
430 let result = parse(source);
431 assert_eq!(result.components.len(), 1);
432 assert_eq!(
433 result.components.first().unwrap(),
434 &Component {
435 label: "Dominoes".to_string(),
436 coordinates: (0.9, 0.5),
437 shape: Shape::Circle
438 }
439 );
440 }
441
442 #[test]
443 fn should_parse_a_component_with_a_shape() {
444 let source = "Database (0.33, 0.81) [Square]";
445 let result = parse(source);
446 assert_eq!(result.components.len(), 1);
447 assert_eq!(
448 result.components.first().unwrap(),
449 &Component {
450 label: "Database".to_string(),
451 coordinates: (0.33, 0.81),
452 shape: Shape::Square
453 }
454 );
455 }
456
457 #[test]
458 fn should_parse_a_component_with_x_shape() {
459 let source = "API (0.5, 0.6) [x]";
460 let result = parse(source);
461 assert_eq!(result.components.len(), 1);
462 assert_eq!(
463 result.components.first().unwrap(),
464 &Component {
465 label: "API".to_string(),
466 coordinates: (0.5, 0.6),
467 shape: Shape::X
468 }
469 );
470 }
471
472 #[test]
473 fn should_parse_a_component_with_triangle_shape() {
474 let source = "Service (0.4, 0.7) [Triangle]";
475 let result = parse(source);
476 assert_eq!(result.components.len(), 1);
477 assert_eq!(
478 result.components.first().unwrap(),
479 &Component {
480 label: "Service".to_string(),
481 coordinates: (0.4, 0.7),
482 shape: Shape::Triangle
483 }
484 );
485 }
486
487 #[test]
488 fn should_parse_a_component_with_circle_shape() {
489 let source = "Process (0.2, 0.9) [CIRCLE]";
490 let result = parse(source);
491 assert_eq!(result.components.len(), 1);
492 assert_eq!(
493 result.components.first().unwrap(),
494 &Component {
495 label: "Process".to_string(),
496 coordinates: (0.2, 0.9),
497 shape: Shape::Circle
498 }
499 );
500 }
501
502 #[test]
503 fn should_default_shape_to_circle_if_invalid() {
504 let source = "Figures (0.12, 0.711) [";
505 let result = parse(source);
506 assert_eq!(result.components.len(), 1);
507 assert_eq!(
508 result.components.first().unwrap(),
509 &Component {
510 label: "Figures".to_string(),
511 coordinates: (0.12, 0.711),
512 shape: Shape::Circle
513 }
514 );
515 }
516
517 #[test]
518 fn should_handle_decimal_numbers() {
519 let source = "Item (.5, .123)";
520 let result = parse(source);
521 assert_eq!(result.components.len(), 1);
522 assert_eq!(
523 result.components.first().unwrap(),
524 &Component {
525 label: "Item".to_string(),
526 coordinates: (0.5, 0.123),
527 shape: Shape::Circle
528 }
529 );
530 }
531
532 #[test]
533 fn should_handle_inteers_as_coordinates() {
534 let source = "Item (1, 0)";
535 let result = parse(source);
536 assert_eq!(result.components.len(), 1);
537 assert_eq!(
538 result.components.first().unwrap(),
539 &Component {
540 label: "Item".to_string(),
541 coordinates: (1.0, 0.0),
542 shape: Shape::Circle
543 }
544 );
545 }
546
547 #[test]
548 fn should_preserve_case_in_component_labels() {
549 let source = "RubberGaskets (0.5, 0.5)";
550 let result = parse(source);
551 assert_eq!(result.components.len(), 1);
552 assert_eq!(
553 result.components.first().unwrap(),
554 &Component {
555 label: "RubberGaskets".to_string(),
556 coordinates: (0.5, 0.5),
557 shape: Shape::Circle
558 }
559 );
560 }
561
562 #[test]
563 fn should_handle_component_labels_with_spaces() {
564 let source = "Big Lawnmowers (0.5, 0.5)";
565 let result = parse(source);
566 assert_eq!(result.components.len(), 1);
567 assert_eq!(
568 result.components.first().unwrap(),
569 &Component {
570 label: "Big Lawnmowers".to_string(),
571 coordinates: (0.5, 0.5),
572 shape: Shape::Circle
573 }
574 );
575 }
576
577 #[test]
578 fn should_handle_a_whitespace_around_tokens() {
579 let source = " Outer Space ( 0.9 , 0.5 ) [ Square ] ";
580 let result = parse(source);
581 assert_eq!(result.components.len(), 1);
582 assert_eq!(
583 result.components.first().unwrap(),
584 &Component {
585 label: "Outer Space".to_string(),
586 coordinates: (0.9, 0.5),
587 shape: Shape::Square
588 }
589 );
590 }
591
592 #[test]
595 fn should_parse_undirected_dependency() {
596 let source = "Smoke -- Fire";
597 let result = parse(source);
598 assert_eq!(result.dependencies.len(), 1);
599 assert_eq!(
600 result.dependencies.first().unwrap(),
601 &Dependency {
602 from: "Smoke".to_string(),
603 to: "Fire".to_string(),
604 is_directed: false
605 }
606 );
607 }
608
609 #[test]
610 fn should_parse_directed_dependency() {
611 let source = "Cool Thing -> Different Cool Thing";
612 let result = parse(source);
613 assert_eq!(result.dependencies.len(), 1);
614 assert_eq!(
615 result.dependencies.first().unwrap(),
616 &Dependency {
617 from: "Cool Thing".to_string(),
618 to: "Different Cool Thing".to_string(),
619 is_directed: true
620 }
621 );
622 }
623
624 #[test]
625 fn should_preserve_case_in_dependency_labels() {
626 let source = "SaRcAsM -> Seriousness.";
627 let result = parse(source);
628 assert_eq!(result.dependencies.len(), 1);
629 assert_eq!(
630 result.dependencies.first().unwrap(),
631 &Dependency {
632 from: "SaRcAsM".to_string(),
633 to: "Seriousness.".to_string(),
634 is_directed: true
635 }
636 );
637 }
638
639 #[test]
640 fn should_ignore_whitespace_in_dependencies() {
641 let source = " loose --TIGHT";
642 let result = parse(source);
643 assert_eq!(result.dependencies.len(), 1);
644 assert_eq!(
645 result.dependencies.first().unwrap(),
646 &Dependency {
647 from: "loose".to_string(),
648 to: "TIGHT".to_string(),
649 is_directed: false
650 }
651 );
652 }
653
654 #[test]
657 fn should_parse_a_note() {
658 let source = "[Note] (0.5, 0.5) This is a note";
659 let result = parse(source);
660 assert_eq!(result.notes.len(), 1);
661 assert_eq!(
662 result.notes.first().unwrap(),
663 &Note {
664 text: "This is a note".to_string(),
665 coordinates: (0.5, 0.5)
666 }
667 );
668 }
669
670 #[test]
671 fn should_handle_case_insensitive_note_keyword() {
672 let source = "[note] (0.1, 0.2) Text";
673 let result = parse(source);
674 assert_eq!(result.notes.len(), 1);
675 assert_eq!(
676 result.notes.first().unwrap(),
677 &Note {
678 text: "Text".to_string(),
679 coordinates: (0.1, 0.2)
680 }
681 );
682 }
683
684 #[test]
685 fn should_preserve_case_in_note_text() {
686 let source = "[NOTE] (0.77, 0.19) Important NOTE";
687 let result = parse(source);
688 assert_eq!(result.notes.len(), 1);
689 assert_eq!(
690 result.notes.first().unwrap(),
691 &Note {
692 text: "Important NOTE".to_string(),
693 coordinates: (0.77, 0.19)
694 }
695 );
696 }
697
698 #[test]
699 fn should_handle_empty_note_text() {
700 let source = "[Note] (0.5, 0.5) ";
701 let result = parse(source);
702 assert_eq!(result.notes.len(), 1);
703 assert_eq!(
704 result.notes.first().unwrap(),
705 &Note {
706 text: "".to_string(),
707 coordinates: (0.5, 0.5)
708 }
709 );
710 }
711
712 #[test]
715 fn should_parse_stage_i() {
716 let source = "[I] 0.25";
717 let result = parse(source);
718 assert_eq!(result.stages.len(), 1);
719 assert_eq!(
720 result.stages.first().unwrap(),
721 &StageData {
722 stage: Stage::I,
723 value: 0.25
724 }
725 );
726 }
727
728 #[test]
729 fn should_parse_stage_ii() {
730 let source = "[ii] 0.5";
731 let result = parse(source);
732 assert_eq!(result.stages.len(), 1);
733 assert_eq!(
734 result.stages.first().unwrap(),
735 &StageData {
736 stage: Stage::Ii,
737 value: 0.5
738 }
739 );
740 }
741
742 #[test]
743 fn should_parse_stage_iii() {
744 let source = "[III] 0.75";
745 let result = parse(source);
746 assert_eq!(result.stages.len(), 1);
747 assert_eq!(
748 result.stages.first().unwrap(),
749 &StageData {
750 stage: Stage::Iii,
751 value: 0.75
752 }
753 );
754 }
755
756 #[test]
757 fn should_parse_stage_iv() {
758 let source = "[Iv] 1.0";
759 let result = parse(source);
760 assert_eq!(result.stages.len(), 1);
761 assert_eq!(
762 result.stages.first().unwrap(),
763 &StageData {
764 stage: Stage::Iv,
765 value: 1.0
766 }
767 );
768 }
769
770 #[test]
771 fn should_handle_decimal_values() {
772 let source = "[i] .333";
773 let result = parse(source);
774 assert_eq!(result.stages.len(), 1);
775 assert_eq!(
776 result.stages.first().unwrap(),
777 &StageData {
778 stage: Stage::I,
779 value: 0.333
780 }
781 );
782 }
783
784 #[test]
787 fn should_parse_a_group_with_a_single_component() {
788 let source = "[Group] Lonely";
789 let result = parse(source);
790 assert_eq!(result.groups.len(), 1);
791 assert_eq!(
792 result.groups.first().unwrap().components,
793 vec!["Lonely".to_string()]
794 );
795 }
796
797 #[test]
798 fn should_parse_a_group_with_multiple_components() {
799 let source = "[GrouP] Huey, Dewey, Louie";
800 let result = parse(source);
801 assert_eq!(result.groups.len(), 1);
802 assert_eq!(
803 result.groups.first().unwrap().components,
804 vec!["Huey".to_string(), "Dewey".to_string(), "Louie".to_string()]
805 );
806 }
807
808 #[test]
809 fn should_handle_case_insensitive_group_keyword() {
810 let source = "[group] Salt, Pepper";
811 let result = parse(source);
812 assert_eq!(result.groups.len(), 1);
813 assert_eq!(
814 result.groups.first().unwrap().components,
815 vec!["Salt".to_string(), "Pepper".to_string()]
816 );
817 }
818
819 #[test]
820 fn should_preserve_case_in_group_component_label() {
821 let source = "[GROUP] XML, LaTeX";
822 let result = parse(source);
823 assert_eq!(result.groups.len(), 1);
824 assert_eq!(
825 result.groups.first().unwrap().components,
826 vec!["XML".to_string(), "LaTeX".to_string()]
827 );
828 }
829
830 #[test]
831 fn should_handle_whitespace_around_commas() {
832 let source = "[Group] sky, is large, emperor";
833 let result = parse(source);
834 assert_eq!(result.groups.len(), 1);
835 assert_eq!(
836 result.groups.first().unwrap().components,
837 vec![
838 "sky".to_string(),
839 "is large".to_string(),
840 "emperor".to_string()
841 ]
842 );
843 }
844
845 #[test]
847 fn should_parse_inertia() {
848 let source = "[Inertia] Print Media";
849 let result = parse(source);
850 assert_eq!(result.inertias.len(), 1);
851 assert_eq!(result.inertias.first().unwrap().component, "Print Media");
852 }
853
854 #[test]
855 fn should_handle_case_insensitive_inertia_keyword() {
856 let source = "[inertia] Yes";
857 let result = parse(source);
858 assert_eq!(result.inertias.len(), 1);
859 assert_eq!(result.inertias.first().unwrap().component, "Yes");
860 }
861
862 #[test]
863 fn should_preserve_case_in_inertia_component_label() {
864 let source = "[INERTIA] SpOnGeBoB";
865 let result = parse(source);
866 assert_eq!(result.inertias.len(), 1);
867 assert_eq!(result.inertias.first().unwrap().component, "SpOnGeBoB");
868 }
869
870 #[test]
872 fn should_parse_positive_evolution() {
873 let source = "[Evolution] Nanomachines + 0.5";
874 let result = parse(source);
875 assert_eq!(result.evolutions.len(), 1);
876 assert_eq!(
877 result.evolutions.first().unwrap(),
878 &Evolution {
879 component: "Nanomachines".to_string(),
880 value: 0.5
881 }
882 );
883 }
884
885 #[test]
886 fn should_parse_negative_evolution() {
887 let source = "[Evolution] FOXDIE - 0.3";
888 let result = parse(source);
889 assert_eq!(result.evolutions.len(), 1);
890 assert_eq!(
891 result.evolutions.first().unwrap(),
892 &Evolution {
893 component: "FOXDIE".to_string(),
894 value: -0.3
895 }
896 );
897 }
898
899 #[test]
900 fn should_handle_case_insensitive_evolution_keyword() {
901 let source = "[Evolution] tamales + 0.73";
902 let result = parse(source);
903 assert_eq!(result.evolutions.len(), 1);
904 assert_eq!(
905 result.evolutions.first().unwrap(),
906 &Evolution {
907 component: "tamales".to_string(),
908 value: 0.73
909 }
910 );
911 }
912
913 #[test]
914 fn should_preserve_case_in_evolution_component_label() {
915 let source = "[EVOLUTION] iMac - 0.2";
916 let result = parse(source);
917 assert_eq!(result.evolutions.len(), 1);
918 assert_eq!(
919 result.evolutions.first().unwrap(),
920 &Evolution {
921 component: "iMac".to_string(),
922 value: -0.2
923 }
924 );
925 }
926
927 #[test]
928 fn should_handle_decimal_evolution_values() {
929 let source = "[Evolution] Numerals + .75";
930 let result = parse(source);
931 assert_eq!(result.evolutions.len(), 1);
932 assert_eq!(
933 result.evolutions.first().unwrap(),
934 &Evolution {
935 component: "Numerals".to_string(),
936 value: 0.75
937 }
938 );
939 }
940
941 #[test]
943 fn should_parse_multiple_entities() {
944 let source = "Water (0.9, 0.5)
945Bucket (0.8, 0.4) [x]
946Water -> Bucket";
947 let result = parse(source);
948 assert_eq!(result.components.len(), 2);
949 assert_eq!(
950 result.components.first().unwrap(),
951 &Component {
952 label: "Water".to_string(),
953 coordinates: (0.9, 0.5),
954 shape: Shape::Circle
955 }
956 );
957 assert_eq!(
958 result.components.get(1).unwrap(),
959 &Component {
960 label: "Bucket".to_string(),
961 coordinates: (0.8, 0.4),
962 shape: Shape::X
963 }
964 );
965 assert_eq!(result.dependencies.len(), 1);
966 assert_eq!(
967 result.dependencies.first().unwrap(),
968 &Dependency {
969 from: "Water".to_string(),
970 to: "Bucket".to_string(),
971 is_directed: true
972 }
973 );
974 }
975
976 #[test]
977 fn should_handle_unix_line_endings() {
978 let source = "Penguins (0.9, 0.5)\nI Use Arch BTW (0.8, 0.4) [x]";
979 let result = parse(source);
980 assert_eq!(result.components.len(), 2);
981 assert_eq!(
982 result.components.first().unwrap(),
983 &Component {
984 label: "Penguins".to_string(),
985 coordinates: (0.9, 0.5),
986 shape: Shape::Circle
987 }
988 );
989 assert_eq!(
990 result.components.get(1).unwrap(),
991 &Component {
992 label: "I Use Arch BTW".to_string(),
993 coordinates: (0.8, 0.4),
994 shape: Shape::X
995 }
996 );
997 }
998
999 #[test]
1000 fn should_handle_windows_line_endings() {
1001 let source = "Clippy (0.9, 0.5)\r\nSpace Cadet (0.8, 0.4) [x]";
1002 let result = parse(source);
1003 assert_eq!(result.components.len(), 2);
1004 assert_eq!(
1005 result.components.first().unwrap(),
1006 &Component {
1007 label: "Clippy".to_string(),
1008 coordinates: (0.9, 0.5),
1009 shape: Shape::Circle
1010 }
1011 );
1012 assert_eq!(
1013 result.components.get(1).unwrap(),
1014 &Component {
1015 label: "Space Cadet".to_string(),
1016 coordinates: (0.8, 0.4),
1017 shape: Shape::X
1018 }
1019 );
1020 }
1021
1022 #[test]
1023 fn should_handle_mac_line_endings() {
1024 let source = "SE/30 (0.9, 0.5)\rPerforma (0.8, 0.4) [x]";
1025 let result = parse(source);
1026 assert_eq!(result.components.len(), 2);
1027 assert_eq!(
1028 result.components.first().unwrap(),
1029 &Component {
1030 label: "SE/30".to_string(),
1031 coordinates: (0.9, 0.5),
1032 shape: Shape::Circle
1033 }
1034 );
1035 assert_eq!(
1036 result.components.get(1).unwrap(),
1037 &Component {
1038 label: "Performa".to_string(),
1039 coordinates: (0.8, 0.4),
1040 shape: Shape::X
1041 }
1042 );
1043 }
1044
1045 #[test]
1046 fn should_ignore_empty_lines() {
1047 let source = "The Void (0.9, 0.5)
1048
1049The Abyss (0.8, 0.4)";
1050 let result = parse(source);
1051 assert_eq!(result.components.len(), 2);
1052 assert_eq!(
1053 result.components.first().unwrap(),
1054 &Component {
1055 label: "The Void".to_string(),
1056 coordinates: (0.9, 0.5),
1057 shape: Shape::Circle
1058 }
1059 );
1060 assert_eq!(
1061 result.components.get(1).unwrap(),
1062 &Component {
1063 label: "The Abyss".to_string(),
1064 coordinates: (0.8, 0.4),
1065 shape: Shape::Circle
1066 }
1067 );
1068 }
1069
1070 #[test]
1071 fn should_handle_incomplete_notes() {
1072 let source = "[Note]";
1073 let result = parse(source);
1074 assert_eq!(result.components.len(), 0);
1075 }
1076
1077 #[test]
1078 fn should_handle_incomplete_groups() {
1079 let source = "[Group]";
1080 let result = parse(source);
1081 assert_eq!(result.components.len(), 0);
1082 }
1083
1084 #[test]
1085 fn should_handle_incomplete_nodes_with_no_coordinates() {
1086 let source = "I am a node? (";
1087 let result = parse(source);
1088 assert_eq!(result.components.len(), 0);
1089 }
1090
1091 #[test]
1092 fn should_handle_incomplete_nodes_with_incomplete_coordinates() {
1093 let source = "I am a node? (12";
1094 let result = parse(source);
1095 assert_eq!(result.components.len(), 0);
1096 }
1097
1098 #[test]
1099 fn should_handle_incomplete_nodes_with_missing_y_coordinates() {
1100 let source = "I am a node? (12,";
1101 let result = parse(source);
1102 assert_eq!(result.components.len(), 0);
1103 }
1104
1105 #[test]
1106 fn should_handle_incomplete_nodes_with_unclosed_coordinates() {
1107 let source = "I am a node? (12,21";
1108 let result = parse(source);
1109 assert_eq!(result.components.len(), 0);
1110 }
1111
1112 #[test]
1113 fn should_handle_incomplete_evolution_without_value() {
1114 let source = "[Evolution] I accidentally a +";
1115 let result = parse(source);
1116 assert_eq!(result.components.len(), 0);
1117 }
1118
1119 #[test]
1120 fn should_handle_incomplete_evolution_without_component() {
1121 let source = "[Evolution]+";
1122 let result = parse(source);
1123 assert_eq!(result.components.len(), 0);
1124 }
1125 #[test]
1126 fn should_handle_just_a_poarenthesis() {
1127 let source = " (";
1128 let result = parse(source);
1129 assert_eq!(result.components.len(), 0);
1130 }
1131
1132 #[test]
1133 fn should_ignore_invalid_lines() {
1134 let source = "Hello (0.9, 0.5)
1135Oh No
1136It's OK (0.8, 0.4)
1137[ijole] NO IT ISN'T
1138A--
1139B->
1140(
1141Hello -> It's OK
1142[";
1143 let result = parse(source);
1144 assert_eq!(result.components.len(), 2);
1145 assert_eq!(
1146 result.components.first().unwrap(),
1147 &Component {
1148 label: "Hello".to_string(),
1149 coordinates: (0.9, 0.5),
1150 shape: Shape::Circle
1151 }
1152 );
1153 assert_eq!(
1154 result.components.get(1).unwrap(),
1155 &Component {
1156 label: "It's OK".to_string(),
1157 coordinates: (0.8, 0.4),
1158 shape: Shape::Circle
1159 }
1160 );
1161 assert_eq!(result.dependencies.len(), 1);
1162 assert_eq!(
1163 result.dependencies.first().unwrap(),
1164 &Dependency {
1165 from: "Hello".to_string(),
1166 to: "It's OK".to_string(),
1167 is_directed: true
1168 }
1169 );
1170 }
1171
1172 #[test]
1174 fn should_handle_empty_input() {
1175 let source = "";
1176 let result = parse(source);
1177 assert_eq!(result.components.len(), 0);
1178 }
1179
1180 #[test]
1181 fn should_handle_only_whitespace() {
1182 let source = " \n \n ";
1183 let result = parse(source);
1184 assert_eq!(result.components.len(), 0);
1185 }
1186
1187 #[test]
1188 fn only_line_breaks() {
1189 let source = "\n";
1190 let result = parse(source);
1191 assert_eq!(result.components.len(), 0);
1192 }
1193
1194 #[test]
1195 fn should_handle_only_invalid_lines() {
1196 let source = "invalid\nstill invalid";
1197 let result = parse(source);
1198 assert_eq!(result.components.len(), 0);
1199 }
1200
1201 #[test]
1202 fn should_handle_complex_component_labels() {
1203 let source = "My Sü'per Complex Label 123 (0.5, 0.5)";
1204 let result = parse(source);
1205 assert_eq!(result.components.len(), 1);
1206 assert_eq!(
1207 result.components.first().unwrap(),
1208 &Component {
1209 label: "My Sü'per Complex Label 123".to_string(),
1210 coordinates: (0.5, 0.5),
1211 shape: Shape::Circle
1212 }
1213 );
1214 }
1215
1216 #[test]
1218 fn should_parse_a_complete_wardley_map() {
1219 let source = "[I] 0.25
1220[II] 0.5
1221[III] 0.75
1222[IV] 1.0
1223
1224Tea (0.9, 0.5) [Circle]
1225Cup (0.8, 0.4)
1226Kettle (0.7, 0.3) [Square]
1227Hot Water (0.6, 0.2)
1228
1229Tea -> Cup
1230Cup -> Kettle
1231Kettle -> Hot Water
1232
1233[Note] (0.5, 0.5) This is our tea supply chain
1234
1235[Group] Tea, Cup, Kettle
1236[Inertia] Kettle
1237[Evolution] Tea + 0.1";
1238
1239 let result = parse(source);
1240
1241 assert_eq!(result.stages.len(), 4);
1243 assert_eq!(result.components.len(), 4);
1244 assert_eq!(result.dependencies.len(), 3);
1245 assert_eq!(result.notes.len(), 1);
1246 assert_eq!(result.groups.len(), 1);
1247 assert_eq!(result.inertias.len(), 1);
1248 assert_eq!(result.evolutions.len(), 1);
1249
1250 assert_eq!(
1251 result.stages.first().unwrap(),
1252 &StageData {
1253 stage: Stage::I,
1254 value: 0.25
1255 }
1256 );
1257 assert_eq!(
1258 result.stages.get(1).unwrap(),
1259 &StageData {
1260 stage: Stage::Ii,
1261 value: 0.5
1262 }
1263 );
1264 assert_eq!(
1265 result.stages.get(2).unwrap(),
1266 &StageData {
1267 stage: Stage::Iii,
1268 value: 0.75
1269 }
1270 );
1271 assert_eq!(
1272 result.stages.get(3).unwrap(),
1273 &StageData {
1274 stage: Stage::Iv,
1275 value: 1.0
1276 }
1277 );
1278 assert_eq!(
1279 result.components.first().unwrap(),
1280 &Component {
1281 label: "Tea".to_string(),
1282 coordinates: (0.9, 0.5),
1283 shape: Shape::Circle
1284 }
1285 );
1286 assert_eq!(
1287 result.components.get(1).unwrap(),
1288 &Component {
1289 label: "Cup".to_string(),
1290 coordinates: (0.8, 0.4),
1291 shape: Shape::Circle
1292 }
1293 );
1294 assert_eq!(
1295 result.components.get(2).unwrap(),
1296 &Component {
1297 label: "Kettle".to_string(),
1298 coordinates: (0.7, 0.3),
1299 shape: Shape::Square
1300 }
1301 );
1302 assert_eq!(
1303 result.components.get(3).unwrap(),
1304 &Component {
1305 label: "Hot Water".to_string(),
1306 coordinates: (0.6, 0.2),
1307 shape: Shape::Circle
1308 }
1309 );
1310 assert_eq!(
1311 result.dependencies.first().unwrap(),
1312 &Dependency {
1313 from: "Tea".to_string(),
1314 to: "Cup".to_string(),
1315 is_directed: true
1316 }
1317 );
1318 assert_eq!(
1319 result.dependencies.get(1).unwrap(),
1320 &Dependency {
1321 from: "Cup".to_string(),
1322 to: "Kettle".to_string(),
1323 is_directed: true
1324 }
1325 );
1326 assert_eq!(
1327 result.dependencies.get(2).unwrap(),
1328 &Dependency {
1329 from: "Kettle".to_string(),
1330 to: "Hot Water".to_string(),
1331 is_directed: true
1332 }
1333 );
1334 assert_eq!(
1335 result.notes.first().unwrap(),
1336 &Note {
1337 text: "This is our tea supply chain".to_string(),
1338 coordinates: (0.5, 0.5)
1339 }
1340 );
1341 assert_eq!(
1342 result.groups.first().unwrap().components,
1343 vec!["Tea".to_string(), "Cup".to_string(), "Kettle".to_string()]
1344 );
1345 assert_eq!(result.inertias.first().unwrap().component, "Kettle");
1346 assert_eq!(
1347 result.evolutions.first().unwrap(),
1348 &Evolution {
1349 component: "Tea".to_string(),
1350 value: 0.1
1351 }
1352 );
1353 }
1354}