use std::{collections::HashMap, mem, rc::Rc, str::FromStr};
use log::{debug, info, trace};
use orrery_core::{color::Color, draw, identifier::Id, semantic};
use crate::{
builtin_types, elaborate_utils,
error::{Diagnostic, ErrorCode, Result},
parser_types,
span::{Span, Spanned},
};
#[derive(Debug, Clone, Default)]
pub struct ElaborateConfig {
pub component_layout: semantic::LayoutEngine,
pub sequence_layout: semantic::LayoutEngine,
}
impl ElaborateConfig {
pub fn new(
component_layout: semantic::LayoutEngine,
sequence_layout: semantic::LayoutEngine,
) -> Self {
Self {
component_layout,
sequence_layout,
}
}
}
pub struct Builder {
cfg: ElaborateConfig,
type_definitions: HashMap<Id, elaborate_utils::TypeDefinition>,
}
impl Builder {
pub fn new(cfg: ElaborateConfig) -> Self {
Self {
cfg,
type_definitions: HashMap::new(),
}
}
pub fn build(mut self, ast: &parser_types::FileAst) -> Result<semantic::Diagram> {
debug!("Building elaborated diagram");
self.build_diagram_from_file_ast(ast)
}
fn build_diagram_from_file_ast(
&mut self,
file_ast: &parser_types::FileAst,
) -> Result<semantic::Diagram> {
let (kind_spanned, attributes) = match &file_ast.header {
parser_types::FileHeader::Diagram { kind, attributes } => (kind, attributes),
parser_types::FileHeader::Library { span } => {
return Err(Diagnostic::error("expected diagram, found library")
.with_code(ErrorCode::E306)
.with_label(*span, "expected diagram"));
}
};
info!("Processing diagram of kind: {kind_spanned}");
trace!("Type definitions: {:?}", file_ast.type_definitions);
trace!("Elements count: {}", file_ast.elements.len());
let saved_type_defs = mem::replace(
&mut self.type_definitions,
Self::builtin_type_definitions_map(),
);
debug!("Updating type definitions");
self.update_type_direct_definitions(&file_ast.type_definitions)?;
let kind = **kind_spanned;
debug!("Building block from elements");
let block = self.build_block_from_elements(&file_ast.elements, kind)?;
let scope = match block {
semantic::Block::None => {
debug!("Empty block, using default scope");
semantic::Scope::default()
}
semantic::Block::Scope(scope) => {
debug!(
elements_len = scope.elements().len();
"Using scope from block",
);
scope
}
semantic::Block::Diagram(_) => {
return Err(Diagnostic::error("nested diagram not allowed")
.with_code(ErrorCode::E305)
.with_label(kind_spanned.span(), "nested diagram")
.with_help("diagrams cannot be nested inside other diagrams"));
}
};
let (layout_engine, background_color, lifeline_definition) =
self.extract_diagram_attributes(kind, attributes)?;
info!(kind:?; "Diagram elaboration completed successfully");
self.type_definitions = saved_type_defs;
Ok(semantic::Diagram::new(
kind,
scope,
layout_engine,
background_color,
lifeline_definition,
))
}
fn build_diagram_from_diagram_source(
&mut self,
source: &parser_types::DiagramSource,
) -> Result<semantic::Diagram> {
match source {
parser_types::DiagramSource::Inline(rc) => {
let file_ast = rc.borrow();
self.build_diagram_from_file_ast(&file_ast)
}
parser_types::DiagramSource::Ref(id) => Err(Diagnostic::error(format!(
"unresolved embed reference `{id}`",
))
.with_code(ErrorCode::E309)
.with_label(id.span(), "expected inlined embedded diagram")),
}
}
fn extract_type_spec<'b>(
attr: &'b parser_types::Attribute<'b>,
key: &str,
) -> Result<&'b parser_types::TypeSpec<'b>> {
attr.value.as_type_spec().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), format!("invalid {key} attribute value"))
.with_help(format!(
"{key} attribute must be a type reference or inline attributes"
))
})
}
fn extract_string<'b>(attr: &'b parser_types::Attribute<'b>, key: &str) -> Result<&'b str> {
attr.value.as_str().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), format!("invalid {key} value"))
.with_help(format!("{key} values must be strings"))
})
}
fn extract_color(attr: &parser_types::Attribute<'_>, key: &str) -> Result<Color> {
let color_str = attr.value.as_str().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid color value")
.with_help("color values must be strings")
})?;
Color::new(color_str).map_err(|err| {
Diagnostic::error(format!("invalid {key} `{color_str}`: {err}"))
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid color")
.with_help("use a valid CSS color")
})
}
fn extract_positive_float(attr: &parser_types::Attribute<'_>, key: &str) -> Result<f32> {
attr.value.as_float().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), format!("invalid {key} value"))
.with_help(format!("{key} must be a positive number"))
})
}
fn extract_usize(attr: &parser_types::Attribute<'_>, key: &str, hint: &str) -> Result<usize> {
attr.value.as_usize().map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E302)
.with_label(attr.span(), format!("invalid {key} value"))
.with_help(format!("{key} {hint}"))
})
}
fn builtin_type_definitions_map() -> HashMap<Id, elaborate_utils::TypeDefinition> {
builtin_types::defaults()
.into_iter()
.map(|def| (def.id(), def))
.collect()
}
fn insert_type_definition(
&mut self,
type_def: elaborate_utils::TypeDefinition,
span: Span,
) -> Result<elaborate_utils::TypeDefinition> {
let id = type_def.id();
if self.type_definitions.insert(id, type_def.clone()).is_none() {
Ok(type_def)
} else {
Err(Diagnostic::error(format!("cannot override type `{id}`"))
.with_code(ErrorCode::E301)
.with_label(span, "type override not supported")
.with_help("built-in types cannot be redefined"))
}
}
fn update_type_direct_definitions(
&mut self,
type_definitions: &Vec<parser_types::TypeDefinition>,
) -> Result<()> {
for type_def in type_definitions {
let base_type_name = type_def
.type_spec
.type_name
.as_ref()
.expect("TypeDefinition should always have a type_name in TypeSpec");
let base = self
.type_definitions
.get(base_type_name.inner())
.ok_or_else(|| {
self.create_undefined_type_error(
base_type_name,
&format!("base type `{}` not found", base_type_name.inner()),
)
})?;
let new_type_def = self.build_type_from_base(
*type_def.name.inner(),
base,
&type_def.type_spec.attributes,
)?;
self.insert_type_definition(new_type_def, type_def.span())?;
}
Ok(())
}
fn build_block_from_elements(
&mut self,
parser_elements: &[parser_types::Element],
diagram_kind: semantic::DiagramKind,
) -> Result<semantic::Block> {
if parser_elements.is_empty() {
Ok(semantic::Block::None)
} else {
Ok(semantic::Block::Scope(self.build_scope_from_elements(
parser_elements,
diagram_kind,
)?))
}
}
fn build_scope_from_elements(
&mut self,
parser_elements: &[parser_types::Element],
diagram_kind: semantic::DiagramKind,
) -> Result<semantic::Scope> {
let mut elements = Vec::new();
for parser_elm in parser_elements {
let element = match parser_elm {
parser_types::Element::Component {
name,
display_name,
type_spec,
content,
} => self.build_component_element(
name,
display_name,
type_spec,
content,
parser_elm,
diagram_kind,
)?,
parser_types::Element::Relation {
source,
target,
relation_type,
type_spec,
label,
} => {
self.build_relation_element(source, target, relation_type, type_spec, label)?
}
parser_types::Element::ActivateBlock { .. } => {
unreachable!(
"ActivateBlock should have been desugared into explicit activate/deactivate statements before elaboration"
);
}
parser_types::Element::Activate {
component,
type_spec,
} => self.build_activate_element(component, type_spec, diagram_kind)?,
parser_types::Element::Deactivate { component } => {
self.build_deactivate_element(component, diagram_kind)?
}
parser_types::Element::Fragment(fragment) => {
self.build_fragment_element(fragment, diagram_kind)?
}
parser_types::Element::AltElseBlock { .. }
| parser_types::Element::OptBlock { .. }
| parser_types::Element::LoopBlock { .. }
| parser_types::Element::ParBlock { .. }
| parser_types::Element::BreakBlock { .. }
| parser_types::Element::CriticalBlock { .. } => {
unreachable!(
"Fragment sugar syntax should have been desugared into Fragment elements before elaboration"
);
}
parser_types::Element::Note(note) => self.build_note_element(note, diagram_kind)?,
};
elements.push(element);
}
Ok(semantic::Scope::new(elements))
}
fn build_component_element(
&mut self,
name: &Spanned<Id>,
display_name: &Option<Spanned<String>>,
type_spec: &parser_types::TypeSpec,
content: &parser_types::ComponentContent,
parser_elm: &parser_types::Element,
diagram_kind: semantic::DiagramKind,
) -> Result<semantic::Element> {
let type_def = self.build_type_definition(type_spec)?;
let shape_def = type_def.shape_definition().map_err(|err| {
Diagnostic::error(err)
.with_code(ErrorCode::E307)
.with_label(type_spec.span(), "invalid shape type")
})?;
if !matches!(content, parser_types::ComponentContent::None) && !shape_def.supports_content()
{
let type_name = type_spec
.type_name
.as_ref()
.map_or(type_def.id(), |name| *name.inner());
return Err(Diagnostic::error(format!(
"shape type `{type_name}` does not support nested content"
))
.with_code(ErrorCode::E308)
.with_label(parser_elm.span(), "content not supported")
.with_help(format!(
"shape `{type_name}` is content-free and cannot contain nested elements or embedded diagrams"
)));
}
let block = match content {
parser_types::ComponentContent::None => semantic::Block::None,
parser_types::ComponentContent::Scope(elements) => {
self.build_block_from_elements(elements, diagram_kind)?
}
parser_types::ComponentContent::Diagram(source) => {
semantic::Block::Diagram(self.build_diagram_from_diagram_source(source)?)
}
};
let node = semantic::Node::new(
*name.inner(),
display_name.as_ref().map(|n| n.to_string()),
block,
Rc::clone(shape_def),
);
Ok(semantic::Element::Node(node))
}
fn build_relation_element(
&mut self,
source: &Spanned<Id>,
target: &Spanned<Id>,
relation_type: &Spanned<&str>,
type_spec: &parser_types::TypeSpec,
label: &Option<Spanned<String>>,
) -> Result<semantic::Element> {
let relation_type_def = self.build_type_definition(type_spec)?;
let arrow_def = relation_type_def.arrow_definition().map_err(|err| {
Diagnostic::error(err)
.with_code(ErrorCode::E307)
.with_label(type_spec.span(), "invalid arrow type")
})?;
let arrow_direction = draw::ArrowDirection::from_str(relation_type).map_err(|_| {
Diagnostic::error(format!("invalid arrow direction `{relation_type}`"))
.with_code(ErrorCode::E302)
.with_label(relation_type.span(), "invalid direction")
.with_help("arrow direction must be `->`, `<-`, `<->`, or `-`")
})?;
Ok(semantic::Element::Relation(semantic::Relation::new(
*source.inner(),
*target.inner(),
arrow_direction,
label.as_ref().map(|l| l.to_string()),
Rc::clone(arrow_def),
)))
}
fn build_activate_element(
&mut self,
component: &Spanned<Id>,
type_spec: &parser_types::TypeSpec,
diagram_kind: semantic::DiagramKind,
) -> Result<semantic::Element> {
if diagram_kind != semantic::DiagramKind::Sequence {
return Err(Diagnostic::error(
"activate statements are only supported in sequence diagrams",
)
.with_code(ErrorCode::E304)
.with_label(component.span(), "activate not allowed here")
.with_help("activate statements are used for temporal grouping in sequence diagrams"));
}
let activate_type_def = self.build_type_definition(type_spec)?;
let activation_box_def = activate_type_def
.activation_box_definition()
.map_err(|err| {
Diagnostic::error(err)
.with_code(ErrorCode::E307)
.with_label(type_spec.span(), "invalid activation box type")
})?;
Ok(semantic::Element::Activate(semantic::Activate::new(
*component.inner(),
Rc::clone(activation_box_def),
)))
}
fn build_deactivate_element(
&mut self,
component: &Spanned<Id>,
diagram_kind: semantic::DiagramKind,
) -> Result<semantic::Element> {
if diagram_kind != semantic::DiagramKind::Sequence {
return Err(Diagnostic::error(
"deactivate statements are only supported in sequence diagrams",
)
.with_code(ErrorCode::E304)
.with_label(component.span(), "deactivate not allowed here")
.with_help(
"deactivate statements are used for temporal grouping in sequence diagrams",
));
}
Ok(semantic::Element::Deactivate(*component.inner()))
}
fn build_fragment_element(
&mut self,
fragment: &parser_types::Fragment,
diagram_kind: semantic::DiagramKind,
) -> Result<semantic::Element> {
if diagram_kind != semantic::DiagramKind::Sequence {
return Err(Diagnostic::error(
"fragment blocks are only supported in sequence diagrams",
)
.with_code(ErrorCode::E304)
.with_label(fragment.span(), "fragment not allowed here")
.with_help("fragment blocks are used for grouping in sequence diagrams"));
}
let type_def = self
.build_type_definition(&fragment.type_spec)
.map_err(|_| {
Diagnostic::error(format!(
"invalid fragment type for operation `{}`",
fragment.operation.inner()
))
.with_code(ErrorCode::E300)
.with_label(fragment.operation.span(), "invalid fragment type")
.with_help("fragment types must be defined in the type system")
})?;
let fragment_def = type_def.fragment_definition().map_err(|err| {
Diagnostic::error(err)
.with_code(ErrorCode::E307)
.with_label(fragment.type_spec.span(), "invalid fragment type")
})?;
let mut sections = Vec::new();
for parser_section in &fragment.sections {
let scope = self.build_scope_from_elements(&parser_section.elements, diagram_kind)?;
let elements_vec = scope.elements().to_vec();
sections.push(semantic::FragmentSection::new(
parser_section.title.as_ref().map(|t| t.inner().to_string()),
elements_vec,
));
}
Ok(semantic::Element::Fragment(semantic::Fragment::new(
fragment.operation.inner().to_string(),
sections,
Rc::clone(fragment_def),
)))
}
fn build_type_definition(
&mut self,
type_spec: &parser_types::TypeSpec,
) -> Result<elaborate_utils::TypeDefinition> {
let type_name = type_spec.type_name.as_ref().ok_or_else(|| {
Diagnostic::error("base type `type_spec` must have a type name")
.with_code(ErrorCode::E306)
.with_label(type_spec.span(), "missing type name")
})?;
let Some(base) = self.type_definitions.get(type_name.inner()) else {
return Err(
self.create_undefined_type_error(type_name, &format!("unknown type `{type_name}`"))
);
};
let attributes = &type_spec.attributes;
if attributes.is_empty() {
return Ok(base.clone());
}
let id = Id::from_anonymous();
let new_type = self.build_type_from_base(id, base, attributes)?;
self.insert_type_definition(new_type, type_name.span())
}
fn resolve_text_type_reference(
&self,
type_spec: &parser_types::TypeSpec,
current_text_rc: &Rc<draw::TextDefinition>,
) -> Result<Rc<draw::TextDefinition>> {
let mut text_rc = if let Some(type_name) = &type_spec.type_name {
let base_type = self
.type_definitions
.get(type_name.inner())
.ok_or_else(|| {
Diagnostic::error(format!("undefined text type `{}`", type_name.inner()))
.with_code(ErrorCode::E300)
.with_label(type_spec.span(), "undefined type")
.with_help("type must be defined with `type` statement before use")
})?;
let base_text_rc = base_type.text_definition_from_draw().map_err(|err| {
Diagnostic::error(format!(
"type `{}` is not a text type: {}",
type_name.inner(),
err
))
.with_code(ErrorCode::E307)
.with_label(type_spec.span(), "invalid type reference")
.with_help("only `Text` types can be used for text attributes")
})?;
Rc::clone(base_text_rc)
} else {
Rc::clone(current_text_rc)
};
if !type_spec.attributes.is_empty() {
let text_def_mut = Rc::make_mut(&mut text_rc);
elaborate_utils::TextAttributeExtractor::extract_text_attributes(
text_def_mut,
&type_spec.attributes,
)?;
}
Ok(text_rc)
}
fn resolve_stroke_type_reference(
&self,
type_spec: &parser_types::TypeSpec,
current_stroke_rc: &Rc<draw::StrokeDefinition>,
) -> Result<Rc<draw::StrokeDefinition>> {
let mut stroke_rc = if let Some(type_name) = &type_spec.type_name {
let base_type = self
.type_definitions
.get(type_name.inner())
.ok_or_else(|| {
Diagnostic::error(format!("undefined stroke type `{}`", type_name.inner()))
.with_code(ErrorCode::E300)
.with_label(type_spec.span(), "undefined type")
.with_help("type must be defined with `type` statement before use")
})?;
let base_stroke_rc = base_type.stroke_definition().map_err(|err| {
Diagnostic::error(format!(
"type `{}` is not a stroke type: {}",
type_name.inner(),
err
))
.with_code(ErrorCode::E307)
.with_label(type_spec.span(), "invalid type reference")
.with_help("only `Stroke` types can be used for stroke attributes")
})?;
Rc::clone(base_stroke_rc)
} else {
Rc::clone(current_stroke_rc)
};
if !type_spec.attributes.is_empty() {
let stroke_def_mut = Rc::make_mut(&mut stroke_rc);
elaborate_utils::StrokeAttributeExtractor::extract_stroke_attributes(
stroke_def_mut,
&type_spec.attributes,
)?;
}
Ok(stroke_rc)
}
fn build_type_from_base(
&self,
id: Id,
base: &elaborate_utils::TypeDefinition,
attributes: &[parser_types::Attribute],
) -> Result<elaborate_utils::TypeDefinition> {
match base.draw_definition() {
elaborate_utils::DrawDefinition::Shape(shape_def) => {
let mut new_shape_def = Rc::clone(shape_def);
let shape_def_mut = Rc::make_mut(&mut new_shape_def);
for attr in attributes {
let name = attr.name.inner();
match *name {
"fill_color" => {
let color = Self::extract_color(attr, "fill_color")?;
shape_def_mut.set_fill_color(Some(color)).map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E304)
.with_label(attr.span(), "unsupported attribute")
})?;
}
"stroke" => {
let type_spec = Self::extract_type_spec(attr, "stroke")?;
let stroke_rc = self
.resolve_stroke_type_reference(type_spec, shape_def_mut.stroke())?;
shape_def_mut.set_stroke(stroke_rc);
}
"rounded" => {
let val =
Self::extract_usize(attr, "rounded", "must be a positive number")?;
shape_def_mut.set_rounded(val).map_err(|err| {
Diagnostic::error(err.to_string())
.with_code(ErrorCode::E304)
.with_label(attr.span(), "unsupported attribute")
})?;
}
"text" => {
let type_spec = Self::extract_type_spec(attr, "text")?;
let text_rc =
self.resolve_text_type_reference(type_spec, shape_def_mut.text())?;
shape_def_mut.set_text(text_rc);
}
name => {
return Err(Diagnostic::error(format!(
"unknown shape attribute `{name}`"
))
.with_code(ErrorCode::E303)
.with_label(attr.span(), "unknown attribute")
.with_help(
"valid shape attributes are: `fill_color`, `stroke`=[...], `rounded`, `text`=[...]",
));
}
}
}
Ok(elaborate_utils::TypeDefinition::new_shape(
id,
new_shape_def,
))
}
elaborate_utils::DrawDefinition::Arrow(arrow_def) => {
let mut new_arrow_def = Rc::clone(arrow_def);
let arrow_def_mut = Rc::make_mut(&mut new_arrow_def);
for attr in attributes {
let name = attr.name.inner();
match *name {
"stroke" => {
let type_spec = Self::extract_type_spec(attr, "stroke")?;
let stroke_rc = self
.resolve_stroke_type_reference(type_spec, arrow_def_mut.stroke())?;
arrow_def_mut.set_stroke(stroke_rc);
}
"style" => {
let style_str = Self::extract_string(attr, "style")?;
let val = draw::ArrowStyle::from_str(style_str).map_err(|_| {
Diagnostic::error("invalid arrow style")
.with_code(ErrorCode::E302)
.with_label(attr.span(), "invalid style")
.with_help(
"arrow style must be `straight`, `curved`, or `orthogonal`",
)
})?;
arrow_def_mut.set_style(val);
}
"text" => {
let type_spec = Self::extract_type_spec(attr, "text")?;
let text_rc =
self.resolve_text_type_reference(type_spec, arrow_def_mut.text())?;
arrow_def_mut.set_text(text_rc);
}
name => {
return Err(Diagnostic::error(format!(
"unknown arrow attribute `{name}`"
))
.with_code(ErrorCode::E303)
.with_label(attr.span(), "unknown attribute")
.with_help(
"valid arrow attributes are: `stroke`=[...], `style`, `text`=[...]",
));
}
}
}
Ok(elaborate_utils::TypeDefinition::new_arrow(
id,
new_arrow_def,
))
}
elaborate_utils::DrawDefinition::Fragment(fragment_def) => {
let mut new_fragment_def = Rc::clone(fragment_def);
let fragment_def_mut = Rc::make_mut(&mut new_fragment_def);
for attr in attributes {
let name = attr.name.inner();
match *name {
"border_stroke" => {
let type_spec = Self::extract_type_spec(attr, "border_stroke")?;
let stroke_rc = self.resolve_stroke_type_reference(
type_spec,
fragment_def_mut.border_stroke(),
)?;
fragment_def_mut.set_border_stroke(stroke_rc);
}
"background_color" => {
let color = Self::extract_color(attr, "background_color")?;
fragment_def_mut.set_background_color(Some(color));
}
"separator_stroke" => {
let type_spec = Self::extract_type_spec(attr, "separator_stroke")?;
let stroke_rc = self.resolve_stroke_type_reference(
type_spec,
fragment_def_mut.separator_stroke(),
)?;
fragment_def_mut.set_separator_stroke(stroke_rc);
}
"operation_label_text" => {
let type_spec = Self::extract_type_spec(attr, "operation_label_text")?;
let text_rc = self.resolve_text_type_reference(
type_spec,
fragment_def_mut.operation_label_text(),
)?;
fragment_def_mut.set_operation_label_text(text_rc);
}
"section_title_text" => {
let type_spec = Self::extract_type_spec(attr, "section_title_text")?;
let text_rc = self.resolve_text_type_reference(
type_spec,
fragment_def_mut.section_title_text(),
)?;
fragment_def_mut.set_section_title_text(text_rc);
}
name => {
return Err(Diagnostic::error(format!(
"unknown fragment attribute `{name}`"
))
.with_code(ErrorCode::E303)
.with_label(attr.span(), "unknown attribute")
.with_help("valid fragment attributes are: `border_stroke`=[...], `separator_stroke`=[...], `background_color`, `operation_label_text`=[...], `section_title_text`=[...]"));
}
}
}
Ok(elaborate_utils::TypeDefinition::new_fragment(
id,
new_fragment_def,
))
}
elaborate_utils::DrawDefinition::Note(note_def) => {
let mut new_note_def = Rc::clone(note_def);
let note_def_mut = Rc::make_mut(&mut new_note_def);
for attr in attributes {
let name = attr.name.inner();
match *name {
"background_color" => {
let color = Self::extract_color(attr, "background_color")?;
note_def_mut.set_background_color(Some(color));
}
"stroke" => {
let type_spec = Self::extract_type_spec(attr, "stroke")?;
let stroke_rc = self
.resolve_stroke_type_reference(type_spec, note_def_mut.stroke())?;
note_def_mut.set_stroke(stroke_rc);
}
"text" => {
let type_spec = Self::extract_type_spec(attr, "text")?;
let text_rc =
self.resolve_text_type_reference(type_spec, note_def_mut.text())?;
note_def_mut.set_text(text_rc);
}
"on" | "align" => {
}
name => {
return Err(Diagnostic::error(format!(
"unknown note attribute `{name}`"
))
.with_code(ErrorCode::E303)
.with_label(attr.span(), "unknown attribute")
.with_help(
"valid note attributes are: `background_color`, `stroke`=[...], `text`=[...]",
));
}
}
}
Ok(elaborate_utils::TypeDefinition::new_note(id, new_note_def))
}
elaborate_utils::DrawDefinition::ActivationBox(activation_box_def) => {
let mut new_activation_box_def = Rc::clone(activation_box_def);
let activation_box_def_mut = Rc::make_mut(&mut new_activation_box_def);
for attr in attributes {
let name = attr.name.inner();
match *name {
"width" => {
let val = Self::extract_positive_float(attr, "width")?;
activation_box_def_mut.set_width(val);
}
"nesting_offset" => {
let val = Self::extract_positive_float(attr, "nesting_offset")?;
activation_box_def_mut.set_nesting_offset(val);
}
"fill_color" => {
let color = Self::extract_color(attr, "fill_color")?;
activation_box_def_mut.set_fill_color(color);
}
"stroke" => {
let type_spec = Self::extract_type_spec(attr, "stroke")?;
let stroke_rc = self.resolve_stroke_type_reference(
type_spec,
activation_box_def_mut.stroke(),
)?;
activation_box_def_mut.set_stroke(stroke_rc);
}
name => {
return Err(Diagnostic::error(format!(
"unknown activation box attribute `{name}`"
))
.with_code(ErrorCode::E303)
.with_label(attr.span(), "unknown attribute")
.with_help("valid activation box attributes are: `width`, `nesting_offset`, `fill_color`, `stroke`=[...]"));
}
}
}
Ok(elaborate_utils::TypeDefinition::new_activation_box(
id,
new_activation_box_def,
))
}
elaborate_utils::DrawDefinition::Stroke(stroke_def) => {
let mut new_stroke = (**stroke_def).clone();
elaborate_utils::StrokeAttributeExtractor::extract_stroke_attributes(
&mut new_stroke,
attributes,
)?;
Ok(elaborate_utils::TypeDefinition::new_stroke(id, new_stroke))
}
elaborate_utils::DrawDefinition::Text(text_def) => {
let mut new_text_def = (**text_def).clone();
elaborate_utils::TextAttributeExtractor::extract_text_attributes(
&mut new_text_def,
attributes,
)?;
Ok(elaborate_utils::TypeDefinition::new_text(id, new_text_def))
}
}
}
fn create_undefined_type_error(&self, span: &Spanned<Id>, message: &str) -> Diagnostic {
Diagnostic::error(message)
.with_code(ErrorCode::E300)
.with_label(span.span(), "undefined type")
.with_help(format!(
"type `{}` must be a built-in type or defined with a `type` statement before it can be used as a base type",
span.inner()
))
}
fn extract_diagram_attributes(
&self,
kind: semantic::DiagramKind,
attrs: &Vec<parser_types::Attribute<'_>>,
) -> Result<(
semantic::LayoutEngine,
Option<Color>,
Option<Rc<draw::LifelineDefinition>>,
)> {
let mut layout_engine = match kind {
semantic::DiagramKind::Component => self.cfg.component_layout,
semantic::DiagramKind::Sequence => self.cfg.sequence_layout,
};
let mut background_color = None;
let mut lifeline_definition = None;
for attr in attrs {
match *attr.name {
"layout_engine" => {
layout_engine = Self::determine_layout_engine(attr)?;
}
"background_color" => {
let color = Self::extract_background_color(attr)?;
background_color = Some(color);
}
"lifeline" => {
if kind != semantic::DiagramKind::Sequence {
return Err(Diagnostic::error(
"`lifeline` attribute is only valid for sequence diagrams",
)
.with_code(ErrorCode::E304)
.with_label(attr.span(), "invalid attribute"));
}
let definition = self.extract_lifeline_definition(attr)?;
lifeline_definition = Some(Rc::new(definition));
}
_ => {
return Err(Diagnostic::error(format!(
"unsupported diagram attribute `{}`",
attr.name
))
.with_code(ErrorCode::E303)
.with_label(attr.span(), "unsupported attribute"));
}
}
}
Ok((layout_engine, background_color, lifeline_definition))
}
fn extract_background_color(color_attr: &parser_types::Attribute<'_>) -> Result<Color> {
Self::extract_color(color_attr, "background_color")
}
fn extract_lifeline_definition(
&self,
lifeline_attr: &parser_types::Attribute<'_>,
) -> Result<draw::LifelineDefinition> {
let type_spec = Self::extract_type_spec(lifeline_attr, "lifeline")?;
let default_stroke_rc = Rc::new(draw::StrokeDefinition::dashed(Color::default(), 1.0));
let stroke_rc =
if let Some(stroke_attr) = type_spec.attributes.iter().find(|a| *a.name == "stroke") {
let stroke_type_spec = Self::extract_type_spec(stroke_attr, "stroke")?;
self.resolve_stroke_type_reference(stroke_type_spec, &default_stroke_rc)?
} else if !type_spec.attributes.is_empty() {
return Err(Diagnostic::error(format!(
"unknown lifeline attribute `{}`",
type_spec.attributes[0].name
))
.with_code(ErrorCode::E303)
.with_label(type_spec.attributes[0].span(), "unknown attribute")
.with_help("valid lifeline attributes are: `stroke`=[...]"));
} else {
default_stroke_rc
};
Ok(draw::LifelineDefinition::new(stroke_rc))
}
fn determine_layout_engine(
engine_attr: &parser_types::Attribute<'_>,
) -> Result<semantic::LayoutEngine> {
let engine_str = Self::extract_string(engine_attr, "layout_engine")?;
semantic::LayoutEngine::from_str(engine_str).map_err(|_| {
Diagnostic::error(format!("invalid `layout_engine` value: `{engine_str}`"))
.with_code(ErrorCode::E302)
.with_label(engine_attr.value.span(), "unsupported layout engine")
.with_help("supported layout engines are: `basic`, `sugiyama`")
})
}
fn build_note_element(
&mut self,
note: &parser_types::Note,
diagram_kind: semantic::DiagramKind,
) -> Result<semantic::Element> {
let type_def = self.build_type_definition(¬e.type_spec)?;
let (on, align) = self.extract_note_attributes(¬e.type_spec.attributes, diagram_kind)?;
let content = note.content.inner().to_string();
let note_def_ref = type_def.note_definition().map_err(|err| {
Diagnostic::error(err)
.with_code(ErrorCode::E307)
.with_label(note.content.span(), "invalid note type")
})?;
let note_def = Rc::clone(note_def_ref);
Ok(semantic::Element::Note(semantic::Note::new(
on, align, content, note_def,
)))
}
fn extract_note_attributes(
&mut self,
attributes: &[parser_types::Attribute],
diagram_kind: semantic::DiagramKind,
) -> Result<(Vec<Id>, semantic::NoteAlign)> {
let mut on: Option<Vec<Id>> = None;
let mut align: Option<semantic::NoteAlign> = None;
for attr in attributes {
match *attr.name.inner() {
"on" => {
let ids = attr.value.as_identifiers().map_err(|_| {
Diagnostic::error("`on` attribute must be a list of element identifiers")
.with_code(ErrorCode::E302)
.with_label(attr.value.span(), "invalid on value")
.with_help("use syntax: `on=[element1, element2]`")
})?;
on = Some(ids.iter().map(|id| *id.inner()).collect());
}
"align" => {
let align_str = Self::extract_string(attr, "align")?;
let alignment = align_str.parse::<semantic::NoteAlign>().map_err(|_| {
Diagnostic::error(format!("invalid alignment value: `{}`", align_str))
.with_code(ErrorCode::E302)
.with_label(attr.value.span(), "invalid alignment")
.with_help("valid values: over, left, right, top, bottom")
})?;
align = Some(alignment);
}
_ => {} }
}
let on = on.unwrap_or_default();
let align = align.unwrap_or(match diagram_kind {
semantic::DiagramKind::Sequence => semantic::NoteAlign::Over,
semantic::DiagramKind::Component => semantic::NoteAlign::Bottom,
});
Ok((on, align))
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
fn builder_with_builtins() -> Builder {
let mut builder = Builder::new(ElaborateConfig::default());
builder.type_definitions = Builder::builtin_type_definitions_map();
builder
}
#[test]
#[should_panic(expected = "ActivateBlock should have been desugared")]
fn test_activate_block_panics_in_elaboration() {
let elements = vec![parser_types::Element::ActivateBlock {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
type_spec: parser_types::TypeSpec::default(),
elements: vec![],
}];
let diagram = parser_types::FileAst {
header: parser_types::FileHeader::Diagram {
kind: Spanned::new(semantic::DiagramKind::Component, Span::new(0..9)),
attributes: vec![],
},
import_decls: vec![],
type_definitions: vec![],
elements,
imports: vec![],
};
let config = ElaborateConfig::default();
let builder = Builder::new(config);
let _ = builder.build(&diagram);
}
#[test]
fn test_explicit_activation_scoping_behavior() {
let elements = vec![
parser_types::Element::Activate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Activate"), Span::new(0..8))),
attributes: vec![],
},
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("user"), Span::new(0..4)),
target: Spanned::new(Id::new("server"), Span::new(0..6)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new("Request".to_string(), Span::new(0..7))),
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("server"), Span::new(0..6)),
target: Spanned::new(Id::new("database"), Span::new(0..8)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new("Query".to_string(), Span::new(0..5))),
},
parser_types::Element::Deactivate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
},
];
let diagram = parser_types::FileAst {
header: parser_types::FileHeader::Diagram {
kind: Spanned::new(semantic::DiagramKind::Sequence, Span::new(0..8)),
attributes: vec![],
},
import_decls: vec![],
type_definitions: vec![],
elements,
imports: vec![],
};
let config = ElaborateConfig::default();
let builder = Builder::new(config);
let result = builder.build(&diagram);
assert!(
result.is_ok(),
"Sequence diagram with activate block should work"
);
let diagram = result.unwrap();
for element in diagram.scope().elements() {
if let semantic::Element::Relation(relation) = element {
let source_str = relation.source().to_string();
let target_str = relation.target().to_string();
assert!(
!source_str.starts_with("user::user::"),
"Source should not be double-scoped: {}",
source_str
);
assert!(
!target_str.starts_with("user::server::"),
"Target should not be double-scoped: {}",
target_str
);
}
}
}
#[test]
fn test_nested_explicit_activations_same_component() {
let elements = vec![
parser_types::Element::Activate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Activate"), Span::new(0..8))),
attributes: vec![],
},
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("user"), Span::new(0..4)),
target: Spanned::new(Id::new("server"), Span::new(0..6)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new(
"Initial request".to_string(),
Span::new(0..16),
)),
},
parser_types::Element::Activate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Activate"), Span::new(0..8))),
attributes: vec![],
},
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("user"), Span::new(0..4)),
target: Spanned::new(Id::new("database"), Span::new(0..8)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new("Direct query".to_string(), Span::new(0..12))),
},
parser_types::Element::Deactivate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
},
parser_types::Element::Activate {
component: Spanned::new(Id::new("server"), Span::new(0..6)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Activate"), Span::new(0..8))),
attributes: vec![],
},
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("server"), Span::new(0..6)),
target: Spanned::new(Id::new("cache"), Span::new(0..5)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new("Cache lookup".to_string(), Span::new(0..12))),
},
parser_types::Element::Deactivate {
component: Spanned::new(Id::new("server"), Span::new(0..6)),
},
parser_types::Element::Deactivate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
},
];
let diagram = parser_types::FileAst {
header: parser_types::FileHeader::Diagram {
kind: Spanned::new(semantic::DiagramKind::Sequence, Span::new(0..8)),
attributes: vec![],
},
import_decls: vec![],
type_definitions: vec![],
elements,
imports: vec![],
};
let config = ElaborateConfig::default();
let builder = Builder::new(config);
let result = builder.build(&diagram);
assert!(
result.is_ok(),
"Nested activate blocks should work: {:?}",
result.err()
);
let diagram = result.unwrap();
let elems = diagram.scope().elements();
let activations: Vec<_> = elems
.iter()
.filter_map(|e| {
if let semantic::Element::Activate(activate) = e {
Some(activate.component().to_string())
} else {
None
}
})
.collect();
let deactivations: Vec<_> = elems
.iter()
.filter_map(|e| {
if let semantic::Element::Deactivate(id) = e {
Some(id.to_string())
} else {
None
}
})
.collect();
let relations: Vec<_> = elems
.iter()
.filter_map(|e| {
if let semantic::Element::Relation(r) = e {
Some((r.source().to_string(), r.target().to_string()))
} else {
None
}
})
.collect();
assert_eq!(
relations.len(),
3,
"Should have 3 relations after desugaring"
);
assert_eq!(
activations.len(),
3,
"Should have 3 activation starts after desugaring"
);
assert_eq!(
deactivations.len(),
3,
"Should have 3 activation ends after desugaring"
);
assert_eq!(
activations[0], "user",
"First activation should be for 'user'"
);
assert_eq!(
deactivations.last().unwrap(),
"user",
"Last deactivation should be for 'user'"
);
}
#[test]
fn test_explicit_activate_in_sequence_diagram() {
let config = ElaborateConfig::default();
let builder = Builder::new(config);
let elements = vec![
parser_types::Element::Component {
name: Spanned::new(Id::new("user"), Span::new(0..4)),
display_name: None,
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Rectangle"), Span::new(5..14))),
attributes: vec![],
},
content: parser_types::ComponentContent::None,
},
parser_types::Element::Activate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Activate"), Span::new(0..8))),
attributes: vec![],
},
},
parser_types::Element::Deactivate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
},
];
let diagram = parser_types::FileAst {
header: parser_types::FileHeader::Diagram {
kind: Spanned::new(semantic::DiagramKind::Sequence, Span::new(0..8)),
attributes: vec![],
},
import_decls: vec![],
type_definitions: vec![],
elements,
imports: vec![],
};
let result = builder.build(&diagram);
assert!(
result.is_ok(),
"Should successfully build sequence diagram with explicit activate/deactivate"
);
let elaborate_diagram = result.unwrap();
let elements = elaborate_diagram.scope().elements();
assert_eq!(
elements.len(),
3,
"Should have 3 elements: component, activate, deactivate"
);
if let semantic::Element::Activate(activate) = &elements[1] {
assert_eq!(
activate.component().to_string(),
"user",
"Activate should reference 'user' component"
);
} else {
panic!("Second element should be Activate");
}
if let semantic::Element::Deactivate(id) = &elements[2] {
assert_eq!(
id.to_string(),
"user",
"Deactivate should reference 'user' component"
);
} else {
panic!("Third element should be Deactivate");
}
}
#[test]
fn test_explicit_activate_not_allowed_in_component_diagram() {
let config = ElaborateConfig::default();
let builder = Builder::new(config);
let elements = vec![
parser_types::Element::Component {
name: Spanned::new(Id::new("user"), Span::new(0..4)),
display_name: None,
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Rectangle"), Span::new(5..14))),
attributes: vec![],
},
content: parser_types::ComponentContent::None,
},
parser_types::Element::Activate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
type_spec: parser_types::TypeSpec::default(),
},
];
let diagram = parser_types::FileAst {
header: parser_types::FileHeader::Diagram {
kind: Spanned::new(semantic::DiagramKind::Component, Span::new(0..9)),
attributes: vec![],
},
import_decls: vec![],
type_definitions: vec![],
elements,
imports: vec![],
};
let result = builder.build(&diagram);
assert!(
result.is_err(),
"Should fail to build component diagram with explicit activate"
);
if let Err(err) = result {
let error_message = format!("{}", err);
assert!(
error_message
.contains("activate statements are only supported in sequence diagrams"),
"Error should mention that activate is not allowed in component diagrams"
);
}
}
#[test]
fn test_explicit_activation_timing_and_nesting() {
let elements = vec![
parser_types::Element::Component {
name: Spanned::new(Id::new("user"), Span::new(0..4)),
display_name: None,
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Rectangle"), Span::new(0..9))),
attributes: vec![],
},
content: parser_types::ComponentContent::None,
},
parser_types::Element::Component {
name: Spanned::new(Id::new("server"), Span::new(0..6)),
display_name: None,
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Rectangle"), Span::new(0..9))),
attributes: vec![],
},
content: parser_types::ComponentContent::None,
},
parser_types::Element::Component {
name: Spanned::new(Id::new("database"), Span::new(0..8)),
display_name: None,
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Rectangle"), Span::new(0..9))),
attributes: vec![],
},
content: parser_types::ComponentContent::None,
},
parser_types::Element::Activate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Activate"), Span::new(0..8))),
attributes: vec![],
},
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("user"), Span::new(0..4)),
target: Spanned::new(Id::new("server"), Span::new(0..6)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new("First request".to_string(), Span::new(0..13))),
},
parser_types::Element::Activate {
component: Spanned::new(Id::new("server"), Span::new(0..6)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Activate"), Span::new(0..8))),
attributes: vec![],
},
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("server"), Span::new(0..6)),
target: Spanned::new(Id::new("database"), Span::new(0..8)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new("Nested query".to_string(), Span::new(0..12))),
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("database"), Span::new(0..8)),
target: Spanned::new(Id::new("server"), Span::new(0..6)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new(
"Nested response".to_string(),
Span::new(0..15),
)),
},
parser_types::Element::Deactivate {
component: Spanned::new(Id::new("server"), Span::new(0..6)),
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("server"), Span::new(0..6)),
target: Spanned::new(Id::new("user"), Span::new(0..4)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new("First response".to_string(), Span::new(0..14))),
},
parser_types::Element::Activate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Activate"), Span::new(0..8))),
attributes: vec![],
},
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("user"), Span::new(0..4)),
target: Spanned::new(Id::new("server"), Span::new(0..6)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new("Second request".to_string(), Span::new(0..14))),
},
parser_types::Element::Deactivate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
},
parser_types::Element::Deactivate {
component: Spanned::new(Id::new("user"), Span::new(0..4)),
},
];
let diagram = parser_types::FileAst {
header: parser_types::FileHeader::Diagram {
kind: Spanned::new(semantic::DiagramKind::Sequence, Span::new(0..8)),
attributes: vec![],
},
import_decls: vec![],
type_definitions: vec![],
elements,
imports: vec![],
};
let config = ElaborateConfig::default();
let builder = Builder::new(config);
let result = builder.build(&diagram);
assert!(
result.is_ok(),
"Complex nested activate blocks should work: {:?}",
result.err()
);
let diagram = result.unwrap();
let elems = diagram.scope().elements();
let relations = elems
.iter()
.filter(|e| matches!(e, semantic::Element::Relation(_)))
.count();
let activates = elems
.iter()
.filter(|e| matches!(e, semantic::Element::Activate(_)))
.count();
let deactivates = elems
.iter()
.filter(|e| matches!(e, semantic::Element::Deactivate(_)))
.count();
assert!(
relations >= 5,
"Should have at least 5 relations after desugaring, found {}",
relations
);
assert!(
activates >= 3,
"Should have at least 3 activates after desugaring, found {}",
activates
);
assert!(
deactivates >= 3,
"Should have at least 3 deactivates after desugaring, found {}",
deactivates
);
}
#[test]
fn test_note_with_default_alignment_sequence() {
let mut builder = builder_with_builtins();
let note = parser_types::Note {
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Note"), Span::new(0..4))),
attributes: vec![],
},
content: Spanned::new("Test note".to_string(), Span::new(0..9)),
};
let diagram_kind = semantic::DiagramKind::Sequence;
let result = builder.build_note_element(¬e, diagram_kind);
assert!(result.is_ok());
let element = result.unwrap();
if let semantic::Element::Note(note_elem) = element {
assert_eq!(note_elem.on().len(), 0); assert_eq!(note_elem.align(), semantic::NoteAlign::Over); assert_eq!(note_elem.content(), "Test note");
} else {
panic!("Expected Note element");
}
}
#[test]
fn test_note_with_default_alignment_component() {
let mut builder = builder_with_builtins();
let note = parser_types::Note {
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Note"), Span::new(0..4))),
attributes: vec![],
},
content: Spanned::new("Test note".to_string(), Span::new(0..9)),
};
let diagram_kind = semantic::DiagramKind::Component;
let result = builder.build_note_element(¬e, diagram_kind);
assert!(result.is_ok());
let element = result.unwrap();
if let semantic::Element::Note(note_elem) = element {
assert_eq!(note_elem.on().len(), 0); assert_eq!(note_elem.align(), semantic::NoteAlign::Bottom); assert_eq!(note_elem.content(), "Test note");
} else {
panic!("Expected Note element");
}
}
#[test]
fn test_note_with_styling_attributes() {
let mut builder = builder_with_builtins();
let attributes = vec![
parser_types::Attribute {
name: Spanned::new("background_color", Span::new(0..16)),
value: parser_types::AttributeValue::String(Spanned::new(
"lightyellow".to_string(),
Span::new(0..11),
)),
},
parser_types::Attribute {
name: Spanned::new("stroke", Span::new(0..6)),
value: parser_types::AttributeValue::TypeSpec(parser_types::TypeSpec {
type_name: None,
attributes: vec![
parser_types::Attribute {
name: Spanned::new("color", Span::new(0..5)),
value: parser_types::AttributeValue::String(Spanned::new(
"blue".to_string(),
Span::new(0..4),
)),
},
parser_types::Attribute {
name: Spanned::new("width", Span::new(0..5)),
value: parser_types::AttributeValue::Float(Spanned::new(
2.0,
Span::new(0..3),
)),
},
],
}),
},
parser_types::Attribute {
name: Spanned::new("text", Span::new(0..4)),
value: parser_types::AttributeValue::TypeSpec(parser_types::TypeSpec {
type_name: None,
attributes: vec![parser_types::Attribute {
name: Spanned::new("font_size", Span::new(0..9)),
value: parser_types::AttributeValue::Float(Spanned::new(
14.0,
Span::new(0..2),
)),
}],
}),
},
];
let note = parser_types::Note {
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Note"), Span::new(0..4))),
attributes,
},
content: Spanned::new("Styled note".to_string(), Span::new(0..11)),
};
let diagram_kind = semantic::DiagramKind::Sequence;
let result = builder.build_note_element(¬e, diagram_kind);
assert!(result.is_ok());
let element = result.unwrap();
if let semantic::Element::Note(note_elem) = element {
assert_eq!(note_elem.content(), "Styled note");
assert_eq!(note_elem.align(), semantic::NoteAlign::Over); assert_eq!(note_elem.on().len(), 0); } else {
panic!("Expected Note element");
}
}
#[test]
fn test_extract_type_spec_success() {
use crate::parser_types::{Attribute, AttributeValue, TypeSpec};
let type_spec = TypeSpec {
type_name: Some(Spanned::new(Id::new("BoldText"), Span::new(0..8))),
attributes: vec![],
};
let attr = Attribute {
name: Spanned::new("text", Span::new(0..4)),
value: AttributeValue::TypeSpec(type_spec),
};
let result = Builder::extract_type_spec(&attr, "text");
assert!(result.is_ok());
}
#[test]
fn test_extract_type_spec_error() {
use crate::parser_types::{Attribute, AttributeValue};
let attr = Attribute {
name: Spanned::new("text", Span::new(0..4)),
value: AttributeValue::String(Spanned::new(
"not a type spec".to_string(),
Span::new(5..20),
)),
};
let result = Builder::extract_type_spec(&attr, "text");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("expected type spec"));
}
#[test]
fn test_extract_string_success() {
use crate::parser_types::{Attribute, AttributeValue};
let attr = Attribute {
name: Spanned::new("style", Span::new(0..5)),
value: AttributeValue::String(Spanned::new("curved".to_string(), Span::new(6..14))),
};
let result = Builder::extract_string(&attr, "style");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "curved");
}
#[test]
fn test_extract_string_error() {
use crate::parser_types::{Attribute, AttributeValue};
let attr = Attribute {
name: Spanned::new("style", Span::new(0..5)),
value: AttributeValue::Float(Spanned::new(42.0, Span::new(6..8))),
};
let result = Builder::extract_string(&attr, "style");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("expected string value"));
}
#[test]
fn test_extract_color_success() {
use crate::parser_types::{Attribute, AttributeValue};
let attr = Attribute {
name: Spanned::new("fill_color", Span::new(0..10)),
value: AttributeValue::String(Spanned::new("red".to_string(), Span::new(11..16))),
};
let result = Builder::extract_color(&attr, "fill_color");
assert!(result.is_ok());
}
#[test]
fn test_extract_color_invalid_string() {
use crate::parser_types::{Attribute, AttributeValue};
let attr = Attribute {
name: Spanned::new("fill_color", Span::new(0..10)),
value: AttributeValue::Float(Spanned::new(42.0, Span::new(11..13))),
};
let result = Builder::extract_color(&attr, "fill_color");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("expected string value"));
}
#[test]
fn test_extract_color_invalid_color() {
use crate::parser_types::{Attribute, AttributeValue};
let attr = Attribute {
name: Spanned::new("fill_color", Span::new(0..10)),
value: AttributeValue::String(Spanned::new(
"not-a-color-xyz".to_string(),
Span::new(11..28),
)),
};
let result = Builder::extract_color(&attr, "fill_color");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("invalid fill_color"));
}
#[test]
fn test_extract_positive_float_success() {
use crate::parser_types::{Attribute, AttributeValue};
let attr = Attribute {
name: Spanned::new("width", Span::new(0..5)),
value: AttributeValue::Float(Spanned::new(42.5, Span::new(6..10))),
};
let result = Builder::extract_positive_float(&attr, "width");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42.5);
}
#[test]
fn test_extract_positive_float_error() {
use crate::parser_types::{Attribute, AttributeValue};
let attr = Attribute {
name: Spanned::new("width", Span::new(0..5)),
value: AttributeValue::String(Spanned::new(
"not a number".to_string(),
Span::new(6..20),
)),
};
let result = Builder::extract_positive_float(&attr, "width");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("expected"));
}
#[test]
fn test_extract_usize_success() {
use crate::parser_types::{Attribute, AttributeValue};
let attr = Attribute {
name: Spanned::new("rounded", Span::new(0..7)),
value: AttributeValue::Float(Spanned::new(10.0, Span::new(8..10))),
};
let result = Builder::extract_usize(&attr, "rounded", "must be a positive number");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 10);
}
#[test]
fn test_extract_usize_error() {
use crate::parser_types::{Attribute, AttributeValue};
let attr = Attribute {
name: Spanned::new("rounded", Span::new(0..7)),
value: AttributeValue::String(Spanned::new(
"not a number".to_string(),
Span::new(8..22),
)),
};
let result = Builder::extract_usize(&attr, "rounded", "must be a positive number");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("expected"));
}
#[test]
fn test_fragment_with_both_text_attributes() {
use crate::parser_types::{Attribute, AttributeValue, TypeSpec};
let mut builder = builder_with_builtins();
let type_spec = TypeSpec {
type_name: Some(Spanned::new(Id::new("Fragment"), Span::new(0..8))),
attributes: vec![
Attribute {
name: Spanned::new("operation_label_text", Span::new(0..4)),
value: AttributeValue::TypeSpec(TypeSpec {
type_name: None,
attributes: vec![Attribute {
name: Spanned::new("font_size", Span::new(0..9)),
value: AttributeValue::Float(Spanned::new(14.0, Span::new(0..2))),
}],
}),
},
Attribute {
name: Spanned::new("section_title_text", Span::new(0..18)),
value: AttributeValue::TypeSpec(TypeSpec {
type_name: None,
attributes: vec![Attribute {
name: Spanned::new("font_size", Span::new(0..9)),
value: AttributeValue::Float(Spanned::new(12.0, Span::new(0..2))),
}],
}),
},
],
};
let result = builder.build_type_definition(&type_spec);
assert!(
result.is_ok(),
"Failed to build type definition with both operation_label_text and section_title_text: {:?}",
result.err()
);
let type_def = result.unwrap();
match type_def.draw_definition() {
elaborate_utils::DrawDefinition::Fragment(_) => {
}
_ => panic!("Expected Fragment draw definition"),
}
}
#[test]
fn test_embedded_diagram_type_definitions_are_isolated() {
let child_ast = parser_types::FileAst {
header: parser_types::FileHeader::Diagram {
kind: Spanned::new(semantic::DiagramKind::Sequence, Span::new(0..8)),
attributes: vec![],
},
import_decls: vec![],
type_definitions: vec![parser_types::TypeDefinition {
name: Spanned::new(Id::new("Database"), Span::new(0..8)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Oval"), Span::new(0..4))),
attributes: vec![parser_types::Attribute {
name: Spanned::new("fill_color", Span::new(0..10)),
value: parser_types::AttributeValue::String(Spanned::new(
"#e0f0e0".to_string(),
Span::new(0..7),
)),
}],
},
}],
elements: vec![parser_types::Element::Component {
name: Spanned::new(Id::new("database"), Span::new(0..8)),
display_name: None,
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Database"), Span::new(0..8))),
attributes: vec![],
},
content: parser_types::ComponentContent::None,
}],
imports: vec![],
};
let parent_ast = parser_types::FileAst {
header: parser_types::FileHeader::Diagram {
kind: Spanned::new(semantic::DiagramKind::Component, Span::new(0..9)),
attributes: vec![],
},
import_decls: vec![],
type_definitions: vec![parser_types::TypeDefinition {
name: Spanned::new(Id::new("Service"), Span::new(0..7)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Rectangle"), Span::new(0..9))),
attributes: vec![parser_types::Attribute {
name: Spanned::new("fill_color", Span::new(0..10)),
value: parser_types::AttributeValue::String(Spanned::new(
"#e6f3ff".to_string(),
Span::new(0..7),
)),
}],
},
}],
elements: vec![
parser_types::Element::Component {
name: Spanned::new(Id::new("gateway"), Span::new(0..7)),
display_name: Some(Spanned::new("API Gateway".to_string(), Span::new(0..11))),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Service"), Span::new(0..7))),
attributes: vec![],
},
content: parser_types::ComponentContent::None,
},
parser_types::Element::Component {
name: Spanned::new(Id::new("auth_overview"), Span::new(0..13)),
display_name: Some(Spanned::new("Auth Overview".to_string(), Span::new(0..13))),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Service"), Span::new(0..7))),
attributes: vec![],
},
content: parser_types::ComponentContent::Diagram(
parser_types::DiagramSource::Inline(Rc::new(RefCell::new(child_ast))),
),
},
parser_types::Element::Relation {
source: Spanned::new(Id::new("gateway"), Span::new(0..7)),
target: Spanned::new(Id::new("auth_overview"), Span::new(0..13)),
relation_type: Spanned::new("->", Span::new(0..2)),
type_spec: parser_types::TypeSpec {
type_name: Some(Spanned::new(Id::new("Arrow"), Span::new(0..5))),
attributes: vec![],
},
label: Some(Spanned::new("Auth detail".to_string(), Span::new(0..11))),
},
],
imports: vec![],
};
let config = ElaborateConfig::default();
let builder = Builder::new(config);
let result = builder.build(&parent_ast);
assert!(
result.is_ok(),
"Embedded diagram with its own type definitions should build successfully, got: {:?}",
result.err()
);
let diagram = result.unwrap();
assert_eq!(diagram.kind(), semantic::DiagramKind::Component);
let elements = diagram.scope().elements();
assert_eq!(
elements.len(),
3,
"Expected gateway, auth_overview, and a relation"
);
if let semantic::Element::Node(node) = &elements[1] {
assert!(
matches!(node.block(), semantic::Block::Diagram(_)),
"auth_overview should contain an embedded diagram"
);
} else {
panic!("Expected second element to be a Node (auth_overview)");
}
}
}