use std::collections::BTreeMap;
use std::iter;
use std::ops::Bound;
use svugy_model::builder::{ElementRef, ModelBuilder};
use svugy_model::legend::{DashKey, Legend};
use svugy_model::model::{ByteRange, Edge, Element, PlacementHint, Shape};
use svugy_model::path::ElementPath;
use svugy_model::validate::ModelAdmission;
use super::D2Import;
use super::D2Parse;
use super::ast::{
EdgeDeclaration, EdgeLink, EdgeOperator, Field, FieldAssignment, KeyPath, KeySegment, Scalar,
ShapeDeclaration, Span, Statement,
};
use super::carrier::{CarrierReader, ElementAttribute, LegendBadge, RootAttribute};
use super::diagnostic::{Construct, Diagnostics};
const LAYERED_DIRECTION: &str = "right";
const MAPPED_D2_SHAPES: [(&str, Option<Shape>); 5] = [
("rectangle", None),
("square", None),
("cylinder", Some(Shape::Cylinder)),
("circle", Some(Shape::Disc)),
("oval", Some(Shape::Disc)),
];
const SPECIAL_D2_SHAPES: [&str; 4] = ["sql_table", "sequence_diagram", "class", "hierarchy"];
struct Lowering {
title: Option<String>,
subtitle: Option<String>,
legend: Option<LegendDraft>,
elements: ElementDrafts,
edges: Vec<EdgeDraft>,
diagnostics: Diagnostics,
}
#[derive(Default)]
struct ElementDrafts {
drafts: BTreeMap<ElementPath, ElementDraft>,
}
struct ElementDraft {
name: String,
element: Element,
d2_shape: Option<Shape>,
svugy_shape: Option<Shape>,
badge_mark_span: Option<Span>,
}
#[derive(Default)]
struct LegendDraft {
title: Option<String>,
entries: Vec<String>,
dashes: Vec<(DashKey, String)>,
badges: Vec<LegendBadge>,
}
struct EdgeDraft {
from_key: ElementPath,
to_key: ElementPath,
edge: Edge,
}
impl D2Parse {
pub fn lower(self) -> D2Import {
let D2Parse {
document,
errors,
warnings,
} = self;
let diagnostics = Diagnostics::from_parts(errors, warnings);
let mut lowering = Lowering::new(diagnostics);
let root_key = ElementPath::root();
lowering.lower_statements(&root_key, document.statements);
lowering.into_import()
}
}
impl Lowering {
fn new(diagnostics: Diagnostics) -> Self {
let elements = ElementDrafts::default();
let edges = Vec::new();
Self {
title: None,
subtitle: None,
legend: None,
elements,
edges,
diagnostics,
}
}
fn lower_statements(&mut self, scope_key: &ElementPath, statements: Vec<Statement>) {
for statement in statements {
match statement {
Statement::Shape(shape) => self.lower_shape(scope_key, shape),
Statement::Edge(edge) => self.lower_edge(scope_key, edge),
Statement::Field(assignment) => self.lower_field_assignment(scope_key, assignment),
}
}
}
fn lower_shape(&mut self, scope_key: &ElementPath, shape: ShapeDeclaration) {
let ShapeDeclaration {
path,
label,
body,
span: _declaration_span,
} = shape;
let Some(element_key) = self
.elements
.declare_path(scope_key, &path, &mut self.diagnostics)
else {
return;
};
if let Some(label) = label {
self.elements
.edit_element(&element_key, |element| element.relabeled(label.text));
}
self.lower_statements(&element_key, body);
}
fn lower_edge(&mut self, scope_key: &ElementPath, edge: EdgeDeclaration) {
let EdgeDeclaration {
source,
links,
label,
fields,
span: _declaration_span,
} = edge;
let shared_edge = self.build_shared_edge_or_warn(label, fields);
let Some(mut tail_key) =
self.elements
.declare_path(scope_key, &source, &mut self.diagnostics)
else {
return;
};
let mut tail_start = source.span.start;
for link in links {
let EdgeLink {
operator,
operator_span,
target,
} = link;
let Some(head_key) =
self.elements
.declare_path(scope_key, &target, &mut self.diagnostics)
else {
return;
};
let link_head_key = head_key.clone();
let (from_key, to_key) =
self.orient_or_warn(operator, operator_span, tail_key, link_head_key);
let link_span = Span::new(tail_start, target.span.end);
let link_range = ByteRange::from(link_span);
let edge = shared_edge.clone().with_span(link_range);
let edge_draft = EdgeDraft {
from_key,
to_key,
edge,
};
self.edges.push(edge_draft);
tail_key = head_key;
tail_start = target.span.start;
}
}
fn build_shared_edge_or_warn(&mut self, label: Option<Scalar>, fields: Vec<Field>) -> Edge {
let mut edge = Edge::new();
if let Some(label) = label {
edge = edge.with_label(label.text);
}
for field in fields {
edge = self.apply_edge_field_or_warn(edge, field);
}
edge
}
fn apply_edge_field_or_warn(&mut self, edge: Edge, field: Field) -> Edge {
let field_keyword = field.keyword();
let keyword_name: &str = field_keyword.into();
match field {
Field::Label(label) => edge.with_label(label.text),
Field::Tooltip(tooltip) => edge.with_description(tooltip.text),
Field::Style(properties) => properties.into_iter().fold(edge, |edge, property| {
edge.with_style(property.name.name, property.value.text)
}),
Field::Shape(value) | Field::Direction(value) | Field::Link(value) => {
let construct = Construct::FieldOnEdge(keyword_name);
self.diagnostics.record_warning(construct, value.span);
edge
}
Field::Svugy(_rejected_carrier) => edge,
}
}
fn orient_or_warn(
&mut self,
operator: EdgeOperator,
operator_span: Span,
tail_key: ElementPath,
head_key: ElementPath,
) -> (ElementPath, ElementPath) {
match operator {
EdgeOperator::Forward => (tail_key, head_key),
EdgeOperator::Backward => (head_key, tail_key),
EdgeOperator::Bidirectional | EdgeOperator::Undirected => {
let construct = Construct::ArrowheadForm(operator);
self.diagnostics.record_warning(construct, operator_span);
(tail_key, head_key)
}
}
}
fn lower_field_assignment(&mut self, scope_key: &ElementPath, assignment: FieldAssignment) {
let FieldAssignment {
target,
field,
span,
} = assignment;
let Some(target_key) =
self.elements
.declare_path(scope_key, &target, &mut self.diagnostics)
else {
return;
};
let is_root = target_key.is_root();
if is_root {
self.lower_root_field(field, span);
return;
}
match field {
Field::Direction(direction) => self.lower_direction_field(&target_key, direction),
Field::Shape(shape) => self.lower_shape_field(&target_key, shape),
Field::Label(label) => {
self.elements
.edit_element(&target_key, |element| element.relabeled(label.text));
}
Field::Tooltip(tooltip) => {
self.elements.edit_element(&target_key, |element| {
element.with_description(tooltip.text)
});
}
Field::Link(link) => {
let link_range = ByteRange::from(link.span);
self.elements.edit_element(&target_key, |element| {
element.with_link(link.text).with_link_span(link_range)
});
}
Field::Style(properties) => self.elements.edit_element(&target_key, |element| {
properties.into_iter().fold(element, |element, property| {
element.with_style(property.name.name, property.value.text)
})
}),
Field::Svugy(properties) => {
let mut carrier_reader = CarrierReader::new(&mut self.diagnostics);
let element_attributes = carrier_reader.read_element(properties, span);
self.merge_element_carrier(&target_key, element_attributes);
}
}
}
fn lower_root_field(&mut self, field: Field, span: Span) {
let root_key = ElementPath::root();
let field_keyword = field.keyword();
let keyword_name: &str = field_keyword.into();
match field {
Field::Label(label) => self.title = Some(label.text),
Field::Svugy(properties) => {
let mut carrier_reader = CarrierReader::new(&mut self.diagnostics);
let root_attributes = carrier_reader.read_root(properties);
self.merge_root_carrier(root_attributes);
}
Field::Direction(direction) => self.lower_direction_field(&root_key, direction),
_element_only_field => {
let construct = Construct::FieldOnRoot(keyword_name);
self.diagnostics.record_warning(construct, span);
}
}
}
fn merge_root_carrier(&mut self, attributes: Vec<RootAttribute>) {
for attribute in attributes {
match attribute {
RootAttribute::Subtitle(subtitle) => self.subtitle = Some(subtitle),
RootAttribute::LegendTitle(title) => self.legend_mut().title = Some(title),
RootAttribute::LegendEntries(entries) => self.legend_mut().entries = entries,
RootAttribute::LegendDash(dash_key, description) => {
let dash_description = (dash_key, description);
self.legend_mut().dashes.push(dash_description);
}
RootAttribute::LegendBadge(legend_badge) => {
self.legend_mut().badges.push(legend_badge);
}
}
}
}
fn lower_direction_field(&mut self, element_key: &ElementPath, direction: Scalar) {
let is_diff_layered_direction = direction.text != LAYERED_DIRECTION;
if is_diff_layered_direction {
let construct = Construct::UnmappedDirection(direction.text);
self.diagnostics.record_warning(construct, direction.span);
return;
}
let is_root = element_key.is_root();
if is_root {
return;
}
self.elements.edit_element(element_key, |element| {
element.with_child_placement(PlacementHint::Layered)
});
}
fn lower_shape_field(&mut self, element_key: &ElementPath, shape: Scalar) {
let lowercase_shape = shape.text.to_ascii_lowercase();
let lowercase_shape_name = lowercase_shape.as_str();
let is_special_shape = SPECIAL_D2_SHAPES.contains(&lowercase_shape_name);
if is_special_shape {
let construct = Construct::SpecialShape(shape.text);
self.diagnostics.record_warning(construct, shape.span);
return;
}
let mut mapped_shapes = MAPPED_D2_SHAPES.iter();
let mapped_shape =
mapped_shapes.find(|(d2_shape, _primitive)| *d2_shape == lowercase_shape);
let Some((_d2_shape, primitive)) = mapped_shape else {
let construct = Construct::UnmappedShape(shape.text);
self.diagnostics.record_warning(construct, shape.span);
return;
};
self.elements.draft_mut(element_key).d2_shape = *primitive;
}
fn merge_element_carrier(
&mut self,
element_key: &ElementPath,
attributes: Vec<ElementAttribute>,
) {
for attribute in attributes {
match attribute {
ElementAttribute::Caption(caption) => {
self.elements
.edit_element(element_key, |element| element.with_caption(caption));
}
ElementAttribute::Primitive(shape) => {
self.elements.draft_mut(element_key).svugy_shape = Some(shape);
}
ElementAttribute::Height(height) => {
self.elements
.edit_element(element_key, |element| element.with_height(height));
}
ElementAttribute::BadgeMark { is_marked, span } => {
let badge_mark_span = is_marked.then_some(span);
self.elements.draft_mut(element_key).badge_mark_span = badge_mark_span;
}
ElementAttribute::Position(position) => {
self.elements
.edit_element(element_key, |element| element.with_position(position));
}
}
}
}
fn into_import(self) -> D2Import {
let Self {
title,
subtitle,
legend,
elements,
edges,
mut diagnostics,
} = self;
let mut builder = ModelBuilder::new();
if let Some(title) = title {
builder = builder.with_title(title);
}
if let Some(subtitle) = subtitle {
builder = builder.with_subtitle(subtitle);
}
if let Some(legend_draft) = legend {
let legend = legend_draft.resolve(&elements, &mut diagnostics);
builder = builder.with_legend(legend);
}
elements.warn_container_badge_marks(&mut diagnostics);
let admission = Self::build_model(builder, elements, edges);
let (errors, warnings) = diagnostics.into_sorted_parts();
D2Import {
admission,
errors,
warnings,
}
}
fn build_model(
mut builder: ModelBuilder,
elements: ElementDrafts,
edges: Vec<EdgeDraft>,
) -> ModelAdmission {
let badged_edges: Vec<EdgeDraft> = edges
.into_iter()
.map(|edge_draft| edge_draft.badged_if_from_leaf_into_badge_leaf(&elements))
.collect();
let element_refs = elements.add_to(&mut builder);
for edge_draft in badged_edges {
let EdgeDraft {
from_key,
to_key,
edge,
} = edge_draft;
let from_ref = &element_refs[&from_key];
let to_ref = &element_refs[&to_key];
builder.add_edge(from_ref, to_ref, edge);
}
builder.build()
}
fn legend_mut(&mut self) -> &mut LegendDraft {
self.legend.get_or_insert_with(LegendDraft::default)
}
}
impl ElementDrafts {
fn declare_path(
&mut self,
scope_key: &ElementPath,
path: &KeyPath,
diagnostics: &mut Diagnostics,
) -> Option<ElementPath> {
let mut element_key = scope_key.clone();
for _parent_reference in 0..path.parent_references {
let Some(parent_key) = element_key.parent() else {
let message = "`_` climbs above the root, which has no parent scope";
diagnostics.record_error(message, path.span);
return None;
};
element_key = parent_key;
}
for segment in &path.segments {
let prefix_span = Span::new(path.span.start, segment.span.end);
element_key = self.declare_child(&element_key, segment, prefix_span);
}
Some(element_key)
}
fn declare_child(
&mut self,
parent_key: &ElementPath,
segment: &KeySegment,
span: Span,
) -> ElementPath {
let element_key = Self::child_key(parent_key, &segment.name);
let drafted_key = element_key.clone();
self.drafts.entry(drafted_key).or_insert_with(|| {
let name = segment.name.clone();
ElementDraft::new(name, span)
});
element_key
}
fn child_key(parent_key: &ElementPath, name: &str) -> ElementPath {
let folded_name = name.to_lowercase();
parent_key.child(folded_name)
}
fn key_of(path: &[String]) -> ElementPath {
let root_key = ElementPath::root();
path.iter().fold(root_key, |parent_key, name| {
Self::child_key(&parent_key, name)
})
}
fn edit_element(&mut self, element_key: &ElementPath, edit: impl FnOnce(Element) -> Element) {
let draft = self.draft_mut(element_key);
let element = draft.element.clone();
draft.element = edit(element);
}
fn declared_path_of(&self, element_key: &ElementPath) -> Option<ElementPath> {
let ancestor_keys = element_key.ancestors();
let own_key = element_key.clone();
let own_key_once = iter::once(own_key);
let element_and_ancestor_keys = ancestor_keys.chain(own_key_once);
let declared_names: Option<Vec<String>> = element_and_ancestor_keys
.map(|key| {
let draft = self.drafts.get(&key)?;
let declared_name = draft.name.clone();
Some(declared_name)
})
.collect();
let declared_names = declared_names?;
let declared_path = ElementPath::from_segments(declared_names);
Some(declared_path)
}
fn add_to(self, builder: &mut ModelBuilder) -> BTreeMap<ElementPath, ElementRef> {
let mut element_refs: BTreeMap<ElementPath, ElementRef> = BTreeMap::new();
for (element_key, draft) in self.drafts {
let parent_key = element_key.parent();
let parent_ref = parent_key.and_then(|parent_key| element_refs.get(&parent_key));
let (name, element) = draft.into_named_element();
let element_ref = match parent_ref {
Some(parent_ref) => builder.add_child(parent_ref, &name, element),
None => builder.add_top_level(&name, element),
};
element_refs.insert(element_key, element_ref);
}
element_refs
}
fn warn_container_badge_marks(&self, diagnostics: &mut Diagnostics) {
for (element_key, draft) in &self.drafts {
let Some(badge_mark_span) = draft.badge_mark_span else {
continue;
};
let is_leaf = !self.is_container(element_key);
if is_leaf {
continue;
}
diagnostics.record_warning(Construct::ContainerBadge, badge_mark_span);
}
}
fn is_badge_target(&self, element_key: &ElementPath) -> bool {
let draft = self.drafts.get(element_key);
draft.is_some_and(|draft| draft.badge_mark_span.is_some())
}
fn is_container(&self, element_key: &ElementPath) -> bool {
let after_element_key = (Bound::Excluded(element_key), Bound::Unbounded);
let mut later_drafts = self.drafts.range::<ElementPath, _>(after_element_key);
let next_draft = later_drafts.next();
next_draft.is_some_and(|(next_key, _draft)| element_key.is_ancestor_of(next_key))
}
fn draft_mut(&mut self, element_key: &ElementPath) -> &mut ElementDraft {
self.drafts
.get_mut(element_key)
.expect("declare_path declares every element it returns")
}
}
impl LegendDraft {
fn resolve(self, elements: &ElementDrafts, diagnostics: &mut Diagnostics) -> Legend {
let Self {
title,
entries,
dashes,
badges,
} = self;
let mut legend = Legend::new();
if let Some(title) = title {
legend = legend.with_title(title);
}
legend = entries.into_iter().fold(legend, Legend::with_entry);
for (dash_key, description) in dashes {
legend = legend.with_dash_description(dash_key, description);
}
for legend_badge in badges {
let LegendBadge {
target_path,
description,
span,
} = legend_badge;
let target_key = ElementDrafts::key_of(&target_path);
let Some(target) = elements.declared_path_of(&target_key) else {
let message = format!(
"`{target_key}` names no shape: a `svugy.legend.badges` key is the path of a badge target, e.g. `pipeline.inter`"
);
diagnostics.record_error(message, span);
continue;
};
legend = legend.with_badge_description(target, description);
}
legend
}
}
impl ElementDraft {
fn new(name: String, span: Span) -> Self {
let source_range = ByteRange::from(span);
let label = name.clone();
let element = Element::new(label).with_span(source_range);
Self {
name,
element,
d2_shape: None,
svugy_shape: None,
badge_mark_span: None,
}
}
fn into_named_element(self) -> (String, Element) {
let Self {
name,
element,
d2_shape,
svugy_shape,
badge_mark_span: _badge_mark_span,
} = self;
let shape = svugy_shape.or(d2_shape);
let Some(shape) = shape else {
return (name, element);
};
let shaped_element = element.with_shape(shape);
(name, shaped_element)
}
}
impl EdgeDraft {
fn badged_if_from_leaf_into_badge_leaf(self, elements: &ElementDrafts) -> Self {
let is_from_container = elements.is_container(&self.from_key);
if is_from_container {
return self;
}
let is_into_container = elements.is_container(&self.to_key);
if is_into_container {
return self;
}
let is_into_unmarked_element = !elements.is_badge_target(&self.to_key);
if is_into_unmarked_element {
return self;
}
let badged_edge = self.edge.with_badge();
Self {
edge: badged_edge,
..self
}
}
}