use std::fmt::Write as _;
use crate::{
DataTable,
error::ChartWarning,
flow::{Diagram, Edge, End, Ends, Node, Shape, layout_diagram},
metrics::TextMetrics,
scene::Scene,
spec::{ChartSpec, ClassesSpec, RelationKind, RelationSpec},
text,
};
pub(crate) fn layout(
spec: &ChartSpec,
warnings: &mut Vec<ChartWarning>,
metrics: &impl TextMetrics,
) -> Scene {
layout_diagram(spec, &diagram(spec), warnings, metrics)
}
fn classes(spec: &ChartSpec) -> &ClassesSpec {
spec.classes
.as_ref()
.expect("validated class diagrams carry a classes block")
}
const fn reversed(kind: RelationKind) -> bool {
matches!(kind, RelationKind::Inheritance | RelationKind::Realization)
}
fn ends(relation: &RelationSpec) -> Ends {
let (from_text, to_text) = (
relation.from_cardinality.clone(),
relation.to_cardinality.clone(),
);
let (tail, head) = match relation.kind {
RelationKind::Association => (End::None, End::None),
RelationKind::Inheritance | RelationKind::Realization => (End::Triangle, End::None),
RelationKind::Composition => (End::DiamondFilled, End::None),
RelationKind::Aggregation => (End::DiamondHollow, End::None),
RelationKind::Dependency => (End::None, End::Open),
};
if reversed(relation.kind) {
Ends {
tail,
head,
tail_text: to_text,
head_text: from_text,
}
} else {
Ends {
tail,
head,
tail_text: from_text,
head_text: to_text,
}
}
}
fn diagram(spec: &ChartSpec) -> Diagram {
let classes = classes(spec);
let nodes = classes
.classes
.iter()
.enumerate()
.map(|(index, class)| Node {
label: class.label.clone(),
sublabel: class.kind.stereotype().map(str::to_owned),
shape: Shape::Class,
lane: None,
group: None,
path: format!("/classes/classes/{index}"),
compartments: vec![class.attributes.clone(), class.methods.clone()],
})
.collect();
let edges = classes
.relations
.iter()
.enumerate()
.map(|(index, relation)| {
let (from, to) = (
classes.class(&relation.from).expect("validated"),
classes.class(&relation.to).expect("validated"),
);
let (from, to) = if reversed(relation.kind) {
(to, from)
} else {
(from, to)
};
Edge {
from,
to,
label: relation.label.clone(),
technology: None,
dash: relation.kind.dash(),
main: false,
label_path: format!("/classes/relations/{index}/label"),
ends: ends(relation),
}
})
.collect();
Diagram {
nodes,
edges,
lanes: Vec::new(),
groups: Vec::new(),
orientation: classes.orientation,
}
}
fn relation_sentence(spec: &ChartSpec, relation: &RelationSpec, from_side: bool) -> String {
let classes = classes(spec);
let label = |id: &str| {
classes.classes[classes.class(id).expect("validated")]
.label
.as_str()
};
let other = if from_side {
&relation.to
} else {
&relation.from
};
let cardinality = match (&relation.from_cardinality, &relation.to_cardinality) {
(Some(from), Some(to)) => Some(format!("{from} : {to}")),
(Some(one), None) | (None, Some(one)) => Some(one.clone()),
(None, None) => None,
};
text::relation_phrase(
spec.locale,
(relation.kind, from_side),
label(other),
cardinality.as_deref(),
relation.label.as_deref(),
)
}
pub(crate) fn description(spec: &ChartSpec) -> String {
let classes = classes(spec);
let mut description =
text::classes_opening(spec.locale, classes.classes.len(), classes.relations.len());
for class in &classes.classes {
let relations: Vec<String> = classes
.relations
.iter()
.filter(|relation| relation.from == class.id)
.map(|relation| relation_sentence(spec, relation, true))
.collect();
write!(
description,
" {}",
text::class_sentence(
spec.locale,
&class.label,
(class.attributes.len(), class.methods.len()),
&relations,
)
)
.expect("writing to String cannot fail");
}
description
}
pub(crate) fn data_table(spec: &ChartSpec) -> DataTable {
let classes = classes(spec);
let words = spec.locale.words();
let rows = classes
.classes
.iter()
.map(|class| {
let relations: Vec<String> = classes
.relations
.iter()
.filter(|relation| relation.from == class.id)
.map(|relation| relation_sentence(spec, relation, true))
.collect();
vec![
class.label.clone(),
text::class_kind(spec.locale, class.kind).to_owned(),
class.attributes.join("; "),
class.methods.join("; "),
relations.join("; "),
]
})
.collect();
DataTable {
caption: format!("{} {}", words.data_for, spec.title),
columns: vec![
words.item.to_owned(),
words.message_kind.to_owned(),
words.attributes.to_owned(),
words.methods.to_owned(),
words.relations.to_owned(),
],
rows,
}
}