#[derive(Debug)]
pub struct Map {
pub components: Vec<Component>,
pub dependencies: Vec<Dependency>,
pub notes: Vec<Note>,
pub stages: Vec<StageData>,
pub groups: Vec<Group>,
pub inertias: Vec<Inertia>,
pub evolutions: Vec<Evolution>,
}
#[derive(Debug, PartialEq)]
pub enum Shape {
Circle,
X,
Square,
Triangle,
}
#[derive(Debug, PartialEq)]
pub enum Stage {
I,
Ii,
Iii,
Iv,
}
#[derive(Debug, PartialEq)]
pub struct Component {
pub label: String,
pub coordinates: (f64, f64),
pub shape: Shape,
}
#[derive(Debug, PartialEq)]
pub struct Dependency {
pub from: String,
pub to: String,
pub is_directed: bool,
}
#[derive(Debug, PartialEq)]
pub struct Note {
pub text: String,
pub coordinates: (f64, f64),
}
#[derive(Debug, PartialEq)]
pub struct StageData {
pub stage: Stage,
pub value: f64,
}
#[derive(Debug, PartialEq)]
pub struct Group {
pub components: Vec<String>,
}
#[derive(Debug, PartialEq)]
pub struct Inertia {
pub component: String,
}
#[derive(Debug, PartialEq)]
pub struct Evolution {
pub component: String,
pub value: f64,
}
#[must_use]
pub fn parse(source: &str) -> Map {
let estimated_lines = source.len() / 30;
let mut components = Vec::with_capacity(estimated_lines);
let mut dependencies = Vec::with_capacity(estimated_lines);
let mut notes = Vec::with_capacity(estimated_lines / 10);
let mut stages = Vec::with_capacity(4);
let mut groups = Vec::with_capacity(estimated_lines / 20);
let mut inertias = Vec::with_capacity(estimated_lines / 20);
let mut evolutions = Vec::with_capacity(estimated_lines / 20);
let bytes = source.as_bytes();
let mut i = 0;
let len = bytes.len();
while i < len {
while i < len && (bytes[i] == b'\n' || bytes[i] == b'\r') {
i += 1;
}
let line_start = i;
while i < len && bytes[i] != b'\n' && bytes[i] != b'\r' {
i += 1;
}
let line = source[line_start..i].trim();
if line.is_empty() {
continue;
}
let bytes = line.as_bytes();
if bytes[0] == b'[' {
parse_keyword_line(
line,
&mut stages,
&mut notes,
&mut groups,
&mut inertias,
&mut evolutions,
);
} else if has_dependency(bytes) {
if let Some(dep) = parse_dependency(line) {
dependencies.push(dep);
}
} else if let Some(comp) = parse_component(line) {
components.push(comp);
}
}
Map {
components,
dependencies,
notes,
stages,
groups,
inertias,
evolutions,
}
}
#[inline]
fn has_dependency(bytes: &[u8]) -> bool {
for i in 0..bytes.len() {
if bytes[i] == b'(' {
return false;
}
if bytes[i] == b'-' && i + 1 < bytes.len() {
let next = bytes[i + 1];
if next == b'>' || next == b'-' {
return true;
}
}
}
false
}
fn parse_keyword_line(
line: &str,
stages: &mut Vec<StageData>,
notes: &mut Vec<Note>,
groups: &mut Vec<Group>,
inertias: &mut Vec<Inertia>,
evolutions: &mut Vec<Evolution>,
) {
let bytes = line.as_bytes();
let mut i = 0;
i += 1;
let keyword_start = i;
while i < bytes.len() && bytes[i] != b']' {
i += 1;
}
if i >= bytes.len() {
return;
}
let keyword = &line[keyword_start..i];
i += 1;
i = skip_whitespace(bytes, i);
let rest = &line[i..];
if keyword.eq_ignore_ascii_case("i")
|| keyword.eq_ignore_ascii_case("ii")
|| keyword.eq_ignore_ascii_case("iii")
|| keyword.eq_ignore_ascii_case("iv")
{
stages.extend(parse_stage(keyword, rest));
} else if keyword.eq_ignore_ascii_case("note") {
notes.extend(parse_note(rest));
} else if keyword.eq_ignore_ascii_case("group") {
groups.extend(parse_group(rest));
} else if keyword.eq_ignore_ascii_case("inertia") {
let component = rest.trim();
if !component.is_empty() {
inertias.push(Inertia {
component: component.to_string(),
});
}
} else if keyword.eq_ignore_ascii_case("evolution") {
evolutions.extend(parse_evolution(rest));
}
}
fn parse_stage(stage_str: &str, rest: &str) -> Option<StageData> {
let value = rest.trim().parse::<f64>().ok()?;
let stage = if stage_str.eq_ignore_ascii_case("i") {
Stage::I
} else if stage_str.eq_ignore_ascii_case("ii") {
Stage::Ii
} else if stage_str.eq_ignore_ascii_case("iii") {
Stage::Iii
} else {
Stage::Iv
};
Some(StageData { stage, value })
}
fn parse_note(rest: &str) -> Option<Note> {
let bytes = rest.as_bytes();
let mut i = skip_whitespace(bytes, 0);
if i >= bytes.len() || bytes[i] != b'(' {
return None;
}
i += 1;
let (x, y, end_i) = parse_coordinates(rest, i)?;
i = end_i;
i = skip_whitespace(bytes, i);
let text = rest[i..].trim().to_string();
Some(Note {
text,
coordinates: (x, y),
})
}
fn parse_group(rest: &str) -> Option<Group> {
let components: Vec<String> = rest
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(std::string::ToString::to_string)
.collect();
if components.is_empty() {
None
} else {
Some(Group { components })
}
}
fn parse_evolution(rest: &str) -> Option<Evolution> {
let bytes = rest.as_bytes();
let mut position_of_sign = None;
let mut sign = 1.0;
for (i, byte) in bytes.iter().enumerate() {
if byte == &b'+' || byte == &b'-' {
position_of_sign = Some(i);
sign = if byte == &b'-' { -1.0 } else { 1.0 };
break;
}
}
let i = position_of_sign?;
let component = rest[..i].trim().to_string();
if component.is_empty() {
return None;
}
let value_str = rest[i + 1..].trim();
let value = value_str.parse::<f64>().ok()?;
Some(Evolution {
component,
value: sign * value,
})
}
fn parse_dependency(line: &str) -> Option<Dependency> {
let index_of_arrow = line.find("->");
let index_of_dash = line.find("--");
let index = index_of_arrow.or(index_of_dash)?;
let from = line[..index].trim();
let to = line[index + 2..].trim();
if from.is_empty() || to.is_empty() {
return None;
}
Some(Dependency {
from: from.to_string(),
to: to.to_string(),
is_directed: index_of_arrow.is_some(),
})
}
fn parse_component(line: &str) -> Option<Component> {
let bytes = line.as_bytes();
let index_of_parenthesis = line.find('(')?;
let label = line[..index_of_parenthesis].trim().to_string();
if label.is_empty() {
return None;
}
let (x, y, end_i) = parse_coordinates(line, index_of_parenthesis + 1)?;
let mut i = skip_whitespace(bytes, end_i);
let shape = if i < bytes.len() && bytes[i] == b'[' {
i += 1;
let shape_start = i;
while i < bytes.len() && bytes[i] != b']' {
i += 1;
}
if i > shape_start && i < bytes.len() {
let shape_label = line[shape_start..i].trim();
parse_shape(shape_label)
} else {
Shape::Circle
}
} else {
Shape::Circle
};
Some(Component {
label,
coordinates: (x, y),
shape,
})
}
#[inline]
fn parse_shape(shape: &str) -> Shape {
if shape.eq_ignore_ascii_case("x") {
Shape::X
} else if shape.eq_ignore_ascii_case("square") {
Shape::Square
} else if shape.eq_ignore_ascii_case("triangle") {
Shape::Triangle
} else {
Shape::Circle
}
}
fn parse_coordinates(line: &str, start: usize) -> Option<(f64, f64, usize)> {
let bytes = line.as_bytes();
let mut i = skip_whitespace(bytes, start);
let x_start = i;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
i += 1;
}
let x = line[x_start..i].parse::<f64>().ok()?;
i = skip_whitespace(bytes, i);
if i >= bytes.len() || bytes[i] != b',' {
return None;
}
i += 1;
i = skip_whitespace(bytes, i);
let y_start = i;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
i += 1;
}
let y = line[y_start..i].parse::<f64>().ok()?;
i = skip_whitespace(bytes, i);
if i >= bytes.len() || bytes[i] != b')' {
return None;
}
i += 1;
Some((x, y, i))
}
#[inline]
fn skip_whitespace(bytes: &[u8], mut i: usize) -> usize {
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
i
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_parse_a_basic_component() {
let source = "Dominoes (0.9, 0.5)";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Dominoes".to_string(),
coordinates: (0.9, 0.5),
shape: Shape::Circle
}
);
}
#[test]
fn should_parse_a_component_with_a_shape() {
let source = "Database (0.33, 0.81) [Square]";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Database".to_string(),
coordinates: (0.33, 0.81),
shape: Shape::Square
}
);
}
#[test]
fn should_parse_a_component_with_x_shape() {
let source = "API (0.5, 0.6) [x]";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "API".to_string(),
coordinates: (0.5, 0.6),
shape: Shape::X
}
);
}
#[test]
fn should_parse_a_component_with_triangle_shape() {
let source = "Service (0.4, 0.7) [Triangle]";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Service".to_string(),
coordinates: (0.4, 0.7),
shape: Shape::Triangle
}
);
}
#[test]
fn should_parse_a_component_with_circle_shape() {
let source = "Process (0.2, 0.9) [CIRCLE]";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Process".to_string(),
coordinates: (0.2, 0.9),
shape: Shape::Circle
}
);
}
#[test]
fn should_default_shape_to_circle_if_invalid() {
let source = "Figures (0.12, 0.711) [";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Figures".to_string(),
coordinates: (0.12, 0.711),
shape: Shape::Circle
}
);
}
#[test]
fn should_handle_decimal_numbers() {
let source = "Item (.5, .123)";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Item".to_string(),
coordinates: (0.5, 0.123),
shape: Shape::Circle
}
);
}
#[test]
fn should_handle_inteers_as_coordinates() {
let source = "Item (1, 0)";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Item".to_string(),
coordinates: (1.0, 0.0),
shape: Shape::Circle
}
);
}
#[test]
fn should_preserve_case_in_component_labels() {
let source = "RubberGaskets (0.5, 0.5)";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "RubberGaskets".to_string(),
coordinates: (0.5, 0.5),
shape: Shape::Circle
}
);
}
#[test]
fn should_handle_component_labels_with_spaces() {
let source = "Big Lawnmowers (0.5, 0.5)";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Big Lawnmowers".to_string(),
coordinates: (0.5, 0.5),
shape: Shape::Circle
}
);
}
#[test]
fn should_handle_a_whitespace_around_tokens() {
let source = " Outer Space ( 0.9 , 0.5 ) [ Square ] ";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Outer Space".to_string(),
coordinates: (0.9, 0.5),
shape: Shape::Square
}
);
}
#[test]
fn should_parse_undirected_dependency() {
let source = "Smoke -- Fire";
let result = parse(source);
assert_eq!(result.dependencies.len(), 1);
assert_eq!(
result.dependencies.first().unwrap(),
&Dependency {
from: "Smoke".to_string(),
to: "Fire".to_string(),
is_directed: false
}
);
}
#[test]
fn should_parse_directed_dependency() {
let source = "Cool Thing -> Different Cool Thing";
let result = parse(source);
assert_eq!(result.dependencies.len(), 1);
assert_eq!(
result.dependencies.first().unwrap(),
&Dependency {
from: "Cool Thing".to_string(),
to: "Different Cool Thing".to_string(),
is_directed: true
}
);
}
#[test]
fn should_preserve_case_in_dependency_labels() {
let source = "SaRcAsM -> Seriousness.";
let result = parse(source);
assert_eq!(result.dependencies.len(), 1);
assert_eq!(
result.dependencies.first().unwrap(),
&Dependency {
from: "SaRcAsM".to_string(),
to: "Seriousness.".to_string(),
is_directed: true
}
);
}
#[test]
fn should_ignore_whitespace_in_dependencies() {
let source = " loose --TIGHT";
let result = parse(source);
assert_eq!(result.dependencies.len(), 1);
assert_eq!(
result.dependencies.first().unwrap(),
&Dependency {
from: "loose".to_string(),
to: "TIGHT".to_string(),
is_directed: false
}
);
}
#[test]
fn should_parse_a_note() {
let source = "[Note] (0.5, 0.5) This is a note";
let result = parse(source);
assert_eq!(result.notes.len(), 1);
assert_eq!(
result.notes.first().unwrap(),
&Note {
text: "This is a note".to_string(),
coordinates: (0.5, 0.5)
}
);
}
#[test]
fn should_handle_case_insensitive_note_keyword() {
let source = "[note] (0.1, 0.2) Text";
let result = parse(source);
assert_eq!(result.notes.len(), 1);
assert_eq!(
result.notes.first().unwrap(),
&Note {
text: "Text".to_string(),
coordinates: (0.1, 0.2)
}
);
}
#[test]
fn should_preserve_case_in_note_text() {
let source = "[NOTE] (0.77, 0.19) Important NOTE";
let result = parse(source);
assert_eq!(result.notes.len(), 1);
assert_eq!(
result.notes.first().unwrap(),
&Note {
text: "Important NOTE".to_string(),
coordinates: (0.77, 0.19)
}
);
}
#[test]
fn should_handle_empty_note_text() {
let source = "[Note] (0.5, 0.5) ";
let result = parse(source);
assert_eq!(result.notes.len(), 1);
assert_eq!(
result.notes.first().unwrap(),
&Note {
text: "".to_string(),
coordinates: (0.5, 0.5)
}
);
}
#[test]
fn should_parse_stage_i() {
let source = "[I] 0.25";
let result = parse(source);
assert_eq!(result.stages.len(), 1);
assert_eq!(
result.stages.first().unwrap(),
&StageData {
stage: Stage::I,
value: 0.25
}
);
}
#[test]
fn should_parse_stage_ii() {
let source = "[ii] 0.5";
let result = parse(source);
assert_eq!(result.stages.len(), 1);
assert_eq!(
result.stages.first().unwrap(),
&StageData {
stage: Stage::Ii,
value: 0.5
}
);
}
#[test]
fn should_parse_stage_iii() {
let source = "[III] 0.75";
let result = parse(source);
assert_eq!(result.stages.len(), 1);
assert_eq!(
result.stages.first().unwrap(),
&StageData {
stage: Stage::Iii,
value: 0.75
}
);
}
#[test]
fn should_parse_stage_iv() {
let source = "[Iv] 1.0";
let result = parse(source);
assert_eq!(result.stages.len(), 1);
assert_eq!(
result.stages.first().unwrap(),
&StageData {
stage: Stage::Iv,
value: 1.0
}
);
}
#[test]
fn should_handle_decimal_values() {
let source = "[i] .333";
let result = parse(source);
assert_eq!(result.stages.len(), 1);
assert_eq!(
result.stages.first().unwrap(),
&StageData {
stage: Stage::I,
value: 0.333
}
);
}
#[test]
fn should_parse_a_group_with_a_single_component() {
let source = "[Group] Lonely";
let result = parse(source);
assert_eq!(result.groups.len(), 1);
assert_eq!(
result.groups.first().unwrap().components,
vec!["Lonely".to_string()]
);
}
#[test]
fn should_parse_a_group_with_multiple_components() {
let source = "[GrouP] Huey, Dewey, Louie";
let result = parse(source);
assert_eq!(result.groups.len(), 1);
assert_eq!(
result.groups.first().unwrap().components,
vec!["Huey".to_string(), "Dewey".to_string(), "Louie".to_string()]
);
}
#[test]
fn should_handle_case_insensitive_group_keyword() {
let source = "[group] Salt, Pepper";
let result = parse(source);
assert_eq!(result.groups.len(), 1);
assert_eq!(
result.groups.first().unwrap().components,
vec!["Salt".to_string(), "Pepper".to_string()]
);
}
#[test]
fn should_preserve_case_in_group_component_label() {
let source = "[GROUP] XML, LaTeX";
let result = parse(source);
assert_eq!(result.groups.len(), 1);
assert_eq!(
result.groups.first().unwrap().components,
vec!["XML".to_string(), "LaTeX".to_string()]
);
}
#[test]
fn should_handle_whitespace_around_commas() {
let source = "[Group] sky, is large, emperor";
let result = parse(source);
assert_eq!(result.groups.len(), 1);
assert_eq!(
result.groups.first().unwrap().components,
vec![
"sky".to_string(),
"is large".to_string(),
"emperor".to_string()
]
);
}
#[test]
fn should_parse_inertia() {
let source = "[Inertia] Print Media";
let result = parse(source);
assert_eq!(result.inertias.len(), 1);
assert_eq!(result.inertias.first().unwrap().component, "Print Media");
}
#[test]
fn should_handle_case_insensitive_inertia_keyword() {
let source = "[inertia] Yes";
let result = parse(source);
assert_eq!(result.inertias.len(), 1);
assert_eq!(result.inertias.first().unwrap().component, "Yes");
}
#[test]
fn should_preserve_case_in_inertia_component_label() {
let source = "[INERTIA] SpOnGeBoB";
let result = parse(source);
assert_eq!(result.inertias.len(), 1);
assert_eq!(result.inertias.first().unwrap().component, "SpOnGeBoB");
}
#[test]
fn should_parse_positive_evolution() {
let source = "[Evolution] Nanomachines + 0.5";
let result = parse(source);
assert_eq!(result.evolutions.len(), 1);
assert_eq!(
result.evolutions.first().unwrap(),
&Evolution {
component: "Nanomachines".to_string(),
value: 0.5
}
);
}
#[test]
fn should_parse_negative_evolution() {
let source = "[Evolution] FOXDIE - 0.3";
let result = parse(source);
assert_eq!(result.evolutions.len(), 1);
assert_eq!(
result.evolutions.first().unwrap(),
&Evolution {
component: "FOXDIE".to_string(),
value: -0.3
}
);
}
#[test]
fn should_handle_case_insensitive_evolution_keyword() {
let source = "[Evolution] tamales + 0.73";
let result = parse(source);
assert_eq!(result.evolutions.len(), 1);
assert_eq!(
result.evolutions.first().unwrap(),
&Evolution {
component: "tamales".to_string(),
value: 0.73
}
);
}
#[test]
fn should_preserve_case_in_evolution_component_label() {
let source = "[EVOLUTION] iMac - 0.2";
let result = parse(source);
assert_eq!(result.evolutions.len(), 1);
assert_eq!(
result.evolutions.first().unwrap(),
&Evolution {
component: "iMac".to_string(),
value: -0.2
}
);
}
#[test]
fn should_handle_decimal_evolution_values() {
let source = "[Evolution] Numerals + .75";
let result = parse(source);
assert_eq!(result.evolutions.len(), 1);
assert_eq!(
result.evolutions.first().unwrap(),
&Evolution {
component: "Numerals".to_string(),
value: 0.75
}
);
}
#[test]
fn should_parse_multiple_entities() {
let source = "Water (0.9, 0.5)
Bucket (0.8, 0.4) [x]
Water -> Bucket";
let result = parse(source);
assert_eq!(result.components.len(), 2);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Water".to_string(),
coordinates: (0.9, 0.5),
shape: Shape::Circle
}
);
assert_eq!(
result.components.get(1).unwrap(),
&Component {
label: "Bucket".to_string(),
coordinates: (0.8, 0.4),
shape: Shape::X
}
);
assert_eq!(result.dependencies.len(), 1);
assert_eq!(
result.dependencies.first().unwrap(),
&Dependency {
from: "Water".to_string(),
to: "Bucket".to_string(),
is_directed: true
}
);
}
#[test]
fn should_handle_unix_line_endings() {
let source = "Penguins (0.9, 0.5)\nI Use Arch BTW (0.8, 0.4) [x]";
let result = parse(source);
assert_eq!(result.components.len(), 2);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Penguins".to_string(),
coordinates: (0.9, 0.5),
shape: Shape::Circle
}
);
assert_eq!(
result.components.get(1).unwrap(),
&Component {
label: "I Use Arch BTW".to_string(),
coordinates: (0.8, 0.4),
shape: Shape::X
}
);
}
#[test]
fn should_handle_windows_line_endings() {
let source = "Clippy (0.9, 0.5)\r\nSpace Cadet (0.8, 0.4) [x]";
let result = parse(source);
assert_eq!(result.components.len(), 2);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Clippy".to_string(),
coordinates: (0.9, 0.5),
shape: Shape::Circle
}
);
assert_eq!(
result.components.get(1).unwrap(),
&Component {
label: "Space Cadet".to_string(),
coordinates: (0.8, 0.4),
shape: Shape::X
}
);
}
#[test]
fn should_handle_mac_line_endings() {
let source = "SE/30 (0.9, 0.5)\rPerforma (0.8, 0.4) [x]";
let result = parse(source);
assert_eq!(result.components.len(), 2);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "SE/30".to_string(),
coordinates: (0.9, 0.5),
shape: Shape::Circle
}
);
assert_eq!(
result.components.get(1).unwrap(),
&Component {
label: "Performa".to_string(),
coordinates: (0.8, 0.4),
shape: Shape::X
}
);
}
#[test]
fn should_ignore_empty_lines() {
let source = "The Void (0.9, 0.5)
The Abyss (0.8, 0.4)";
let result = parse(source);
assert_eq!(result.components.len(), 2);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "The Void".to_string(),
coordinates: (0.9, 0.5),
shape: Shape::Circle
}
);
assert_eq!(
result.components.get(1).unwrap(),
&Component {
label: "The Abyss".to_string(),
coordinates: (0.8, 0.4),
shape: Shape::Circle
}
);
}
#[test]
fn should_handle_incomplete_notes() {
let source = "[Note]";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_incomplete_groups() {
let source = "[Group]";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_incomplete_nodes_with_no_coordinates() {
let source = "I am a node? (";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_incomplete_nodes_with_incomplete_coordinates() {
let source = "I am a node? (12";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_incomplete_nodes_with_missing_y_coordinates() {
let source = "I am a node? (12,";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_incomplete_nodes_with_unclosed_coordinates() {
let source = "I am a node? (12,21";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_incomplete_evolution_without_value() {
let source = "[Evolution] I accidentally a +";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_incomplete_evolution_without_component() {
let source = "[Evolution]+";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_just_a_poarenthesis() {
let source = " (";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_ignore_invalid_lines() {
let source = "Hello (0.9, 0.5)
Oh No
It's OK (0.8, 0.4)
[ijole] NO IT ISN'T
A--
B->
(
Hello -> It's OK
[";
let result = parse(source);
assert_eq!(result.components.len(), 2);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Hello".to_string(),
coordinates: (0.9, 0.5),
shape: Shape::Circle
}
);
assert_eq!(
result.components.get(1).unwrap(),
&Component {
label: "It's OK".to_string(),
coordinates: (0.8, 0.4),
shape: Shape::Circle
}
);
assert_eq!(result.dependencies.len(), 1);
assert_eq!(
result.dependencies.first().unwrap(),
&Dependency {
from: "Hello".to_string(),
to: "It's OK".to_string(),
is_directed: true
}
);
}
#[test]
fn should_handle_empty_input() {
let source = "";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_only_whitespace() {
let source = " \n \n ";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn only_line_breaks() {
let source = "\n";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_only_invalid_lines() {
let source = "invalid\nstill invalid";
let result = parse(source);
assert_eq!(result.components.len(), 0);
}
#[test]
fn should_handle_complex_component_labels() {
let source = "My Sü'per Complex Label 123 (0.5, 0.5)";
let result = parse(source);
assert_eq!(result.components.len(), 1);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "My Sü'per Complex Label 123".to_string(),
coordinates: (0.5, 0.5),
shape: Shape::Circle
}
);
}
#[test]
fn should_parse_a_complete_wardley_map() {
let source = "[I] 0.25
[II] 0.5
[III] 0.75
[IV] 1.0
Tea (0.9, 0.5) [Circle]
Cup (0.8, 0.4)
Kettle (0.7, 0.3) [Square]
Hot Water (0.6, 0.2)
Tea -> Cup
Cup -> Kettle
Kettle -> Hot Water
[Note] (0.5, 0.5) This is our tea supply chain
[Group] Tea, Cup, Kettle
[Inertia] Kettle
[Evolution] Tea + 0.1";
let result = parse(source);
assert_eq!(result.stages.len(), 4);
assert_eq!(result.components.len(), 4);
assert_eq!(result.dependencies.len(), 3);
assert_eq!(result.notes.len(), 1);
assert_eq!(result.groups.len(), 1);
assert_eq!(result.inertias.len(), 1);
assert_eq!(result.evolutions.len(), 1);
assert_eq!(
result.stages.first().unwrap(),
&StageData {
stage: Stage::I,
value: 0.25
}
);
assert_eq!(
result.stages.get(1).unwrap(),
&StageData {
stage: Stage::Ii,
value: 0.5
}
);
assert_eq!(
result.stages.get(2).unwrap(),
&StageData {
stage: Stage::Iii,
value: 0.75
}
);
assert_eq!(
result.stages.get(3).unwrap(),
&StageData {
stage: Stage::Iv,
value: 1.0
}
);
assert_eq!(
result.components.first().unwrap(),
&Component {
label: "Tea".to_string(),
coordinates: (0.9, 0.5),
shape: Shape::Circle
}
);
assert_eq!(
result.components.get(1).unwrap(),
&Component {
label: "Cup".to_string(),
coordinates: (0.8, 0.4),
shape: Shape::Circle
}
);
assert_eq!(
result.components.get(2).unwrap(),
&Component {
label: "Kettle".to_string(),
coordinates: (0.7, 0.3),
shape: Shape::Square
}
);
assert_eq!(
result.components.get(3).unwrap(),
&Component {
label: "Hot Water".to_string(),
coordinates: (0.6, 0.2),
shape: Shape::Circle
}
);
assert_eq!(
result.dependencies.first().unwrap(),
&Dependency {
from: "Tea".to_string(),
to: "Cup".to_string(),
is_directed: true
}
);
assert_eq!(
result.dependencies.get(1).unwrap(),
&Dependency {
from: "Cup".to_string(),
to: "Kettle".to_string(),
is_directed: true
}
);
assert_eq!(
result.dependencies.get(2).unwrap(),
&Dependency {
from: "Kettle".to_string(),
to: "Hot Water".to_string(),
is_directed: true
}
);
assert_eq!(
result.notes.first().unwrap(),
&Note {
text: "This is our tea supply chain".to_string(),
coordinates: (0.5, 0.5)
}
);
assert_eq!(
result.groups.first().unwrap().components,
vec!["Tea".to_string(), "Cup".to_string(), "Kettle".to_string()]
);
assert_eq!(result.inertias.first().unwrap().component, "Kettle");
assert_eq!(
result.evolutions.first().unwrap(),
&Evolution {
component: "Tea".to_string(),
value: 0.1
}
);
}
}