#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Visibility {
Public,
Private,
Protected,
Package,
}
impl Visibility {
pub fn as_char(self) -> char {
match self {
Self::Public => '+',
Self::Private => '-',
Self::Protected => '#',
Self::Package => '~',
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attribute {
pub visibility: Option<Visibility>,
pub name: String,
pub type_name: String,
pub is_static: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Method {
pub visibility: Option<Visibility>,
pub name: String,
pub params: String,
pub return_type: Option<String>,
pub is_static: bool,
pub is_abstract: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Member {
Attribute(Attribute),
Method(Method),
}
impl Member {
pub fn name(&self) -> &str {
match self {
Self::Attribute(a) => &a.name,
Self::Method(m) => &m.name,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stereotype {
Interface,
Enumeration,
Abstract,
Other(String),
}
impl Stereotype {
pub fn label(&self) -> &str {
match self {
Self::Interface => "interface",
Self::Enumeration => "enumeration",
Self::Abstract => "abstract",
Self::Other(s) => s,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Class {
pub name: String,
pub stereotype: Option<Stereotype>,
pub members: Vec<Member>,
}
impl Class {
pub fn bare(name: impl Into<String>) -> Self {
Self {
name: name.into(),
stereotype: None,
members: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelKind {
Inheritance,
Composition,
Aggregation,
AssociationDirected,
AssociationPlain,
Realization,
Dependency,
}
impl RelKind {
pub fn is_dashed(self) -> bool {
matches!(self, Self::Realization | Self::Dependency)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Relation {
pub from: String,
pub to: String,
pub kind: RelKind,
pub from_multiplicity: Option<String>,
pub to_multiplicity: Option<String>,
pub label: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ClassDiagram {
pub classes: Vec<Class>,
pub relations: Vec<Relation>,
}
impl ClassDiagram {
pub fn class_index(&self, name: &str) -> Option<usize> {
self.classes.iter().position(|c| c.name == name)
}
pub fn ensure_class(&mut self, name: &str) -> usize {
if let Some(idx) = self.class_index(name) {
return idx;
}
self.classes.push(Class::bare(name));
self.classes.len() - 1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn visibility_as_char_round_trips() {
assert_eq!(Visibility::Public.as_char(), '+');
assert_eq!(Visibility::Private.as_char(), '-');
assert_eq!(Visibility::Protected.as_char(), '#');
assert_eq!(Visibility::Package.as_char(), '~');
}
#[test]
fn class_bare_starts_empty() {
let c = Class::bare("Animal");
assert_eq!(c.name, "Animal");
assert!(c.members.is_empty());
assert!(c.stereotype.is_none());
}
#[test]
fn ensure_class_inserts_then_reuses() {
let mut diag = ClassDiagram::default();
let a0 = diag.ensure_class("A");
let a1 = diag.ensure_class("A");
let b0 = diag.ensure_class("B");
assert_eq!(a0, 0);
assert_eq!(a1, 0);
assert_eq!(b0, 1);
assert_eq!(diag.classes.len(), 2);
}
#[test]
fn class_index_returns_none_for_unknown() {
let diag = ClassDiagram::default();
assert_eq!(diag.class_index("X"), None);
}
#[test]
fn rel_kind_is_dashed_for_realization_and_dependency() {
assert!(RelKind::Realization.is_dashed());
assert!(RelKind::Dependency.is_dashed());
assert!(!RelKind::Inheritance.is_dashed());
assert!(!RelKind::Composition.is_dashed());
assert!(!RelKind::Aggregation.is_dashed());
assert!(!RelKind::AssociationDirected.is_dashed());
assert!(!RelKind::AssociationPlain.is_dashed());
}
#[test]
fn stereotype_label_returns_canonical_strings() {
assert_eq!(Stereotype::Interface.label(), "interface");
assert_eq!(Stereotype::Enumeration.label(), "enumeration");
assert_eq!(Stereotype::Abstract.label(), "abstract");
assert_eq!(Stereotype::Other("service".to_string()).label(), "service");
}
#[test]
fn member_name_delegates_correctly() {
let attr = Member::Attribute(Attribute {
visibility: None,
name: "count".to_string(),
type_name: "int".to_string(),
is_static: false,
});
let method = Member::Method(Method {
visibility: None,
name: "run".to_string(),
params: String::new(),
return_type: None,
is_static: false,
is_abstract: false,
});
assert_eq!(attr.name(), "count");
assert_eq!(method.name(), "run");
}
}