#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Port {
Left,
Right,
Top,
Bottom,
}
impl Port {
pub fn abbreviation(self) -> char {
match self {
Port::Left => 'L',
Port::Right => 'R',
Port::Top => 'T',
Port::Bottom => 'B',
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArchGroup {
pub id: String,
pub icon: Option<String>,
pub label: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArchService {
pub id: String,
pub icon: Option<String>,
pub label: Option<String>,
pub group: Option<String>,
}
impl ArchService {
pub fn display_label(&self) -> &str {
match &self.label {
Some(l) if !l.is_empty() => l.as_str(),
_ => &self.id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArchEdge {
pub source: String,
pub source_port: Option<Port>,
pub target: String,
pub target_port: Option<Port>,
pub label: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Architecture {
pub groups: Vec<ArchGroup>,
pub services: Vec<ArchService>,
pub edges: Vec<ArchEdge>,
}
impl Architecture {
pub fn group_count(&self) -> usize {
self.groups.len()
}
pub fn service_count(&self) -> usize {
self.services.len()
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
pub fn find_group(&self, id: &str) -> Option<&ArchGroup> {
self.groups.iter().find(|g| g.id == id)
}
pub fn find_service(&self, id: &str) -> Option<&ArchService> {
self.services.iter().find(|s| s.id == id)
}
pub fn services_in_group<'a>(&'a self, group_id: &str) -> Vec<&'a ArchService> {
self.services
.iter()
.filter(|s| s.group.as_deref() == Some(group_id))
.collect()
}
pub fn top_level_services(&self) -> Vec<&ArchService> {
self.services.iter().filter(|s| s.group.is_none()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_architecture_is_empty() {
let arch = Architecture::default();
assert_eq!(arch.group_count(), 0);
assert_eq!(arch.service_count(), 0);
assert_eq!(arch.edge_count(), 0);
}
#[test]
fn arch_service_display_label_falls_back_to_id() {
let with_label = ArchService {
id: "db".to_string(),
icon: None,
label: Some("Database".to_string()),
group: None,
};
assert_eq!(with_label.display_label(), "Database");
let no_label = ArchService {
id: "db".to_string(),
icon: None,
label: None,
group: None,
};
assert_eq!(no_label.display_label(), "db");
let empty_label = ArchService {
id: "srv".to_string(),
icon: None,
label: Some(String::new()),
group: None,
};
assert_eq!(empty_label.display_label(), "srv");
}
#[test]
fn equality_holds_for_identical_diagrams() {
let a = Architecture {
groups: vec![ArchGroup {
id: "api".to_string(),
icon: Some("cloud".to_string()),
label: Some("API".to_string()),
}],
services: vec![ArchService {
id: "db".to_string(),
icon: Some("database".to_string()),
label: Some("Database".to_string()),
group: Some("api".to_string()),
}],
edges: vec![ArchEdge {
source: "db".to_string(),
source_port: Some(Port::Left),
target: "server".to_string(),
target_port: Some(Port::Right),
label: None,
}],
};
let b = a.clone();
assert_eq!(a, b);
let c = Architecture::default();
assert_ne!(a, c);
}
#[test]
fn port_abbreviations_are_correct() {
assert_eq!(Port::Left.abbreviation(), 'L');
assert_eq!(Port::Right.abbreviation(), 'R');
assert_eq!(Port::Top.abbreviation(), 'T');
assert_eq!(Port::Bottom.abbreviation(), 'B');
}
#[test]
fn find_group_and_service_helpers() {
let arch = Architecture {
groups: vec![ArchGroup {
id: "g1".to_string(),
icon: None,
label: Some("G1".to_string()),
}],
services: vec![
ArchService {
id: "s1".to_string(),
icon: None,
label: None,
group: Some("g1".to_string()),
},
ArchService {
id: "s2".to_string(),
icon: None,
label: None,
group: None,
},
],
edges: vec![],
};
assert!(arch.find_group("g1").is_some());
assert!(arch.find_group("missing").is_none());
assert!(arch.find_service("s1").is_some());
assert!(arch.find_service("nope").is_none());
let in_g1 = arch.services_in_group("g1");
assert_eq!(in_g1.len(), 1);
assert_eq!(in_g1[0].id, "s1");
let top_level = arch.top_level_services();
assert_eq!(top_level.len(), 1);
assert_eq!(top_level[0].id, "s2");
}
#[test]
fn arch_edge_port_is_optional() {
let with_ports = ArchEdge {
source: "a".to_string(),
source_port: Some(Port::Left),
target: "b".to_string(),
target_port: Some(Port::Right),
label: None,
};
assert!(with_ports.source_port.is_some());
assert!(with_ports.target_port.is_some());
let without_ports = ArchEdge {
source: "a".to_string(),
source_port: None,
target: "b".to_string(),
target_port: None,
label: None,
};
assert!(without_ports.source_port.is_none());
assert!(without_ports.target_port.is_none());
assert_ne!(with_ports, without_ports);
}
}