use crate::diagrams::xychart::{XyChartAxisRenderModel, XyChartPlotType};
use crate::*;
use futures::executor::block_on;
use serde_json::json;
#[test]
fn parse_graph_defaults_to_flowchart_v2() {
let engine = Engine::new();
let res = block_on(engine.parse_metadata("graph TD;A-->B;", ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.diagram_type, "flowchart-v2");
assert_eq!(res.config.as_value(), &json!({}));
}
#[test]
fn parse_merges_frontmatter_and_directive_config() {
let engine = Engine::new();
let text = r#"---
config:
theme: forest
flowchart:
htmlLabels: true
---
%%{init: { 'theme': 'base' } }%%
graph TD;A-->B;"#;
let res = block_on(engine.parse_metadata(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(
res.config.as_value(),
&json!({
"theme": "base",
"flowchart": {
"htmlLabels": true
}
})
);
}
#[test]
fn parse_diagram_as_sync_matches_auto_detect_for_flowchart_v2() {
let engine = Engine::new();
let input = "flowchart TD; A[Start]-->B[End];";
let auto = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(auto.meta.diagram_type, "flowchart-v2");
let known = engine
.parse_diagram_as_sync("flowchart-v2", input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(known.meta.diagram_type, "flowchart-v2");
assert_eq!(known.model, auto.model);
}
#[test]
fn parse_metadata_as_sync_moves_init_config_without_detection() {
let engine = Engine::new();
let input = "%%{init: {\"config\": {\"htmlLabels\": true}}}%%\nflowchart TD; A-->B;";
let meta = engine
.parse_metadata_as_sync("flowchart-v2", input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(meta.config.get_bool("flowchart.htmlLabels"), Some(true));
}
#[test]
fn parse_metadata_as_sync_preserves_flowchart_elk_layout_side_effect() {
let engine = Engine::new();
let input = "flowchart-elk TD\nA-->B;";
let meta = engine
.parse_metadata_as_sync("flowchart-elk", input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(meta.effective_config.get_str("layout"), Some("elk"));
}
#[test]
fn parse_sanitizes_frontmatter_title_like_mermaid_common_db() {
let engine = Engine::new();
let text = r#"---
title: "Flowchart v2 arrows: graph direction \"<\""
---
graph <
A-->B
"#;
let meta = block_on(engine.parse_metadata(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(
meta.title,
Some(r#"Flowchart v2 arrows: graph direction "<""#.to_string())
);
}
#[test]
fn parse_merges_init_directive_numeric_values_like_upstream() {
let engine = Engine::new();
let text = r#"%%{init: { 'logLevel': 0 } }%%
sequenceDiagram
Alice->Bob: Hi
"#;
let res = block_on(engine.parse_metadata(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.diagram_type, "sequence");
assert_eq!(res.config.as_value(), &json!({ "logLevel": 0 }));
}
#[test]
fn parse_returns_malformed_frontmatter_error_for_unclosed_frontmatter() {
let engine = Engine::new();
let err = block_on(engine.parse_metadata(
"---\ntitle: a malformed YAML front-matter\n",
ParseOptions::default(),
))
.unwrap_err();
assert!(err.to_string().contains("Malformed YAML front-matter"));
}
#[test]
fn parse_can_suppress_unknown_diagram_errors() {
let engine = Engine::new();
let res = block_on(engine.parse_metadata(
"this is not a mermaid diagram definition",
ParseOptions {
suppress_errors: true,
},
))
.unwrap();
assert!(res.is_none());
}
#[test]
fn parse_lenient_failures_use_error_diagram_across_engine_entrypoints() {
let engine = Engine::new();
let input = "flowchart TD\nA -->";
let options = ParseOptions::lenient();
let parsed = engine.parse_diagram_sync(input, options).unwrap().unwrap();
assert_suppressed_error_diagram(&parsed);
let parsed = engine
.parse_diagram_as_sync("flowchart-v2", input, options)
.unwrap()
.unwrap();
assert_suppressed_error_diagram(&parsed);
let parsed = engine
.parse_diagram_for_render_model_sync(input, options)
.unwrap()
.unwrap();
assert_suppressed_error_render_diagram(&parsed);
let parsed = engine
.parse_diagram_for_render_model_as_sync("flowchart-v2", input, options)
.unwrap()
.unwrap();
assert_suppressed_error_render_diagram(&parsed);
}
fn assert_suppressed_error_diagram(parsed: &ParsedDiagram) {
assert_eq!(parsed.meta.diagram_type, "error");
assert_eq!(parsed.model["type"], json!("error"));
}
fn assert_suppressed_error_render_diagram(parsed: &ParsedDiagramRender) {
assert_eq!(parsed.meta.diagram_type, "error");
match &parsed.model {
RenderSemanticModel::Json(model) => assert_eq!(model["type"], json!("error")),
other => panic!("suppressed parse failures must render through JSON, got {other:?}"),
}
}
#[test]
fn render_semantic_model_kind_reports_canonical_names() {
let sequence = render_model_for("sequenceDiagram\nAlice->>Bob: Hi");
assert_eq!(sequence.kind(), "sequence");
let flowchart = render_model_for("flowchart TD\nA-->B");
assert_eq!(flowchart.kind(), "flowchart");
let er = render_model_for("erDiagram\nCUSTOMER ||--o{ ORDER : places");
assert_eq!(er.kind(), "er");
let json_model = RenderSemanticModel::Json(json!({ "type": "custom" }));
assert_eq!(json_model.kind(), "json");
}
#[test]
fn render_semantic_model_supports_diagram_type_aliases() {
let sequence = render_model_for("sequenceDiagram\nAlice->>Bob: Hi");
assert!(sequence.supports_diagram_type("sequence"));
assert!(sequence.supports_diagram_type("zenuml"));
assert!(!sequence.supports_diagram_type("flowchart-v2"));
let flowchart = render_model_for("flowchart TD\nA-->B");
assert!(flowchart.supports_diagram_type("flowchart-v2"));
assert!(flowchart.supports_diagram_type("flowchart"));
assert!(flowchart.supports_diagram_type("flowchart-elk"));
assert!(!flowchart.supports_diagram_type("sequence"));
let er = render_model_for("erDiagram\nCUSTOMER ||--o{ ORDER : places");
assert!(er.supports_diagram_type("er"));
assert!(er.supports_diagram_type("erDiagram"));
assert!(!er.supports_diagram_type("classDiagram"));
let json_model = RenderSemanticModel::Json(json!({ "type": "custom" }));
assert!(json_model.supports_diagram_type("unknown-plugin"));
}
#[test]
fn render_parser_registry_drives_typed_alias_parse() {
let engine = Engine::new();
assert!(
engine
.render_diagram_registry()
.get("flowchart-elk")
.is_some()
);
let parsed = engine
.parse_diagram_for_render_model_as_sync(
"flowchart-elk",
"flowchart-elk TD\nA-->B;",
ParseOptions::strict(),
)
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "flowchart-elk");
assert_eq!(parsed.model.kind(), "flowchart");
assert!(matches!(parsed.model, RenderSemanticModel::Flowchart(_)));
}
#[test]
fn render_parser_registry_falls_back_to_json_registry_for_custom_diagrams() {
let mut engine = Engine::new();
engine
.diagram_registry_mut()
.insert("customDiagram", custom_json_parser);
let parsed = engine
.parse_diagram_for_render_model_as_sync(
"customDiagram",
"customDiagram\npayload",
ParseOptions::strict(),
)
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "customDiagram");
match parsed.model {
RenderSemanticModel::Json(model) => assert_eq!(model["type"], json!("customDiagram")),
other => panic!("custom render fallback should use JSON model, got {other:?}"),
}
}
fn custom_json_parser(_code: &str, _meta: &ParseMetadata) -> Result<serde_json::Value> {
Ok(json!({ "type": "customDiagram" }))
}
fn render_model_for(input: &str) -> RenderSemanticModel {
Engine::new()
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap()
.model
}
#[test]
fn parse_sequence_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = "sequenceDiagram\nAlice->>Bob: Hi";
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "sequence");
match parsed.model {
RenderSemanticModel::Sequence(model) => {
assert_eq!(model.actor_order, ["Alice", "Bob"]);
assert_eq!(model.messages[0].message_text(), "Hi");
}
other => panic!("sequence render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("sequence"));
assert_eq!(parsed_json.model["messages"][0]["message"], json!("Hi"));
}
#[test]
fn parse_kanban_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = "kanban\n Todo\n item1\n Doing\n item2";
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "kanban");
match parsed.model {
RenderSemanticModel::Kanban(model) => {
assert_eq!(model.nodes.len(), 4);
assert!(model.nodes[0].is_group);
assert_eq!(model.nodes[0].label, "Todo");
assert_eq!(model.nodes[1].label, "item1");
assert_eq!(model.nodes[1].parent_id.as_deref(), Some("Todo"));
}
other => panic!("kanban render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("kanban"));
assert_eq!(parsed_json.model["nodes"][0]["label"], json!("Todo"));
assert_eq!(parsed_json.model["nodes"][1]["label"], json!("item1"));
}
#[test]
fn parse_gantt_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r#"
gantt
dateFormat YYYY-MM-DD
title Typed Gantt
section Alpha
Task 1: id1, 2024-01-01, 2d
"#;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "gantt");
match parsed.model {
RenderSemanticModel::Gantt(model) => {
assert_eq!(model.title.as_deref(), Some("Typed Gantt"));
assert_eq!(model.date_format, "YYYY-MM-DD");
assert_eq!(model.tasks.len(), 1);
assert_eq!(model.tasks[0].id, "id1");
assert_eq!(model.tasks[0].task, "Task 1");
assert_eq!(model.tasks[0].section, "Alpha");
}
other => panic!("gantt render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("gantt"));
assert_eq!(parsed_json.model["title"], json!("Typed Gantt"));
assert_eq!(parsed_json.model["tasks"][0]["id"], json!("id1"));
assert_eq!(parsed_json.model["tasks"][0]["task"], json!("Task 1"));
}
#[test]
fn parse_pie_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r#"
pie showData title Typed Pie
"Alpha" : 60
"Beta" : 40
"#;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "pie");
match parsed.model {
RenderSemanticModel::Pie(model) => {
assert!(model.show_data);
assert_eq!(model.title.as_deref(), Some("Typed Pie"));
assert_eq!(model.sections.len(), 2);
assert_eq!(model.sections[0].label, "Alpha");
assert_eq!(model.sections[0].value, 60.0);
}
other => panic!("pie render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("pie"));
assert_eq!(parsed_json.model["showData"], json!(true));
assert_eq!(parsed_json.model["title"], json!("Typed Pie"));
assert_eq!(parsed_json.model["sections"][0]["label"], json!("Alpha"));
assert_eq!(parsed_json.model["sections"][0]["value"], json!(60.0));
}
#[test]
fn parse_xychart_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r#"
xychart horizontal
title "Typed XYChart"
accTitle: XY accTitle
accDescr: XY accDescription
x-axis "X Axis" [Alpha, Beta]
y-axis "Y Axis" 1 --> 5
bar "Series 1" [1, 2]
line "Series 2" [2, 3]
"#;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "xychart");
match parsed.model {
RenderSemanticModel::XyChart(model) => {
assert_eq!(model.orientation, "horizontal");
assert_eq!(model.title.as_deref(), Some("Typed XYChart"));
assert_eq!(model.acc_title.as_deref(), Some("XY accTitle"));
assert_eq!(model.acc_descr.as_deref(), Some("XY accDescription"));
assert_eq!(
model.x_axis,
XyChartAxisRenderModel::Band {
title: "X Axis".to_string(),
categories: vec!["Alpha".to_string(), "Beta".to_string()],
}
);
assert_eq!(
model.y_axis,
XyChartAxisRenderModel::Linear {
title: "Y Axis".to_string(),
min: Some(1.0),
max: Some(5.0),
}
);
assert_eq!(model.plots.len(), 2);
assert_eq!(model.plots[0].plot_type, XyChartPlotType::Bar);
assert_eq!(model.plots[0].values, vec![1.0, 2.0]);
assert_eq!(
model.plots[0].data,
vec![
("Alpha".to_string(), Some(1.0)),
("Beta".to_string(), Some(2.0)),
]
);
assert_eq!(model.plots[1].plot_type, XyChartPlotType::Line);
}
other => panic!("xychart render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("xychart"));
assert_eq!(parsed_json.model["title"], json!("Typed XYChart"));
assert_eq!(parsed_json.model["xAxis"]["type"], json!("band"));
assert_eq!(
parsed_json.model["xAxis"]["categories"],
json!(["Alpha", "Beta"])
);
assert_eq!(parsed_json.model["yAxis"]["type"], json!("linear"));
assert_eq!(parsed_json.model["yAxis"]["min"], json!(1.0));
assert_eq!(parsed_json.model["yAxis"]["max"], json!(5.0));
assert_eq!(parsed_json.model["plots"][0]["type"], json!("bar"));
assert_eq!(parsed_json.model["plots"][1]["type"], json!("line"));
assert!(parsed_json.model.get("config").is_some());
}
#[test]
fn parse_packet_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r#"
packet
title Typed Packet
accTitle: Packet accTitle
accDescr: Packet accDescription
+8: "byte"
+16: "word"
"#;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "packet");
match parsed.model {
RenderSemanticModel::Packet(model) => {
assert_eq!(model.title.as_deref(), Some("Typed Packet"));
assert_eq!(model.acc_title.as_deref(), Some("Packet accTitle"));
assert_eq!(model.acc_descr.as_deref(), Some("Packet accDescription"));
assert_eq!(model.packet.len(), 1);
assert_eq!(model.packet[0].len(), 2);
assert_eq!(model.packet[0][0].start, 0);
assert_eq!(model.packet[0][0].end, 7);
assert_eq!(model.packet[0][0].bits, 8);
assert_eq!(model.packet[0][0].label, "byte");
}
other => panic!("packet render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("packet"));
assert_eq!(parsed_json.model["title"], json!("Typed Packet"));
assert_eq!(parsed_json.model["accTitle"], json!("Packet accTitle"));
assert_eq!(
parsed_json.model["accDescr"],
json!("Packet accDescription")
);
assert_eq!(parsed_json.model["packet"][0][0]["label"], json!("byte"));
assert_eq!(parsed_json.model["packet"][0][0]["bits"], json!(8));
}
#[test]
fn parse_timeline_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r#"
timeline
title Typed Timeline
accTitle: Timeline accTitle
accDescr: Timeline accDescription
section Alpha
Task 1: event 1: event 2
"#;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "timeline");
match parsed.model {
RenderSemanticModel::Timeline(model) => {
assert_eq!(model.title.as_deref(), Some("Typed Timeline"));
assert_eq!(model.acc_title.as_deref(), Some("Timeline accTitle"));
assert_eq!(model.acc_descr.as_deref(), Some("Timeline accDescription"));
assert_eq!(model.sections.as_slice(), ["Alpha"]);
assert_eq!(model.tasks.len(), 1);
assert_eq!(model.tasks[0].id, 0);
assert_eq!(model.tasks[0].section, "Alpha");
assert_eq!(model.tasks[0].task_type, "Alpha");
assert_eq!(model.tasks[0].task, "Task 1");
assert_eq!(model.tasks[0].events.as_slice(), ["event 1", "event 2"]);
}
other => panic!("timeline render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("timeline"));
assert_eq!(parsed_json.model["title"], json!("Typed Timeline"));
assert_eq!(parsed_json.model["sections"][0], json!("Alpha"));
assert_eq!(parsed_json.model["tasks"][0]["id"], json!(0));
assert_eq!(parsed_json.model["tasks"][0]["type"], json!("Alpha"));
assert_eq!(parsed_json.model["tasks"][0]["task"], json!("Task 1"));
assert_eq!(
parsed_json.model["tasks"][0]["events"],
json!(["event 1", "event 2"])
);
}
#[test]
fn parse_journey_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r#"
journey
title Typed Journey
accTitle: Journey accTitle
accDescr: Journey accDescription
section Shopping
Get keys: 5: Dad
Drive: bad-score: Dad, Mum
"#;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "journey");
match parsed.model {
RenderSemanticModel::Journey(model) => {
assert_eq!(model.title.as_deref(), Some("Typed Journey"));
assert_eq!(model.acc_title.as_deref(), Some("Journey accTitle"));
assert_eq!(model.acc_descr.as_deref(), Some("Journey accDescription"));
assert_eq!(model.sections.as_slice(), ["Shopping"]);
assert_eq!(model.actors.as_slice(), ["Dad", "Mum"]);
assert_eq!(model.tasks.len(), 2);
assert_eq!(model.tasks[0].score, 5);
assert!(!model.tasks[0].score_is_nan);
assert_eq!(model.tasks[0].people.as_slice(), ["Dad"]);
assert_eq!(model.tasks[1].task, "Drive");
assert!(model.tasks[1].score_is_nan);
assert_eq!(model.tasks[1].people.as_slice(), ["Dad", "Mum"]);
}
other => panic!("journey render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("journey"));
assert_eq!(parsed_json.model["title"], json!("Typed Journey"));
assert_eq!(parsed_json.model["actors"], json!(["Dad", "Mum"]));
assert_eq!(parsed_json.model["tasks"][0]["score"], json!(5));
assert!(parsed_json.model["tasks"][0].get("scoreIsNaN").is_none());
assert_eq!(parsed_json.model["tasks"][1]["scoreIsNaN"], json!(true));
assert_eq!(
parsed_json.model["tasks"][1]["people"],
json!(["Dad", "Mum"])
);
}
#[test]
fn parse_requirement_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r##"
requirementDiagram
accTitle: Requirement accTitle
accDescr: Requirement accDescription
direction LR
requirement req_login:::critical {
id: REQ-1
text: "Login must work"
risk: high
verifymethod: test
}
element api {
type: service
docRef: docs/api.md
}
class api external
classDef critical fill:#f9f,stroke:#333,color:#111
classDef external stroke:#0f0
req_login - verifies -> api
"##;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "requirement");
match parsed.model {
RenderSemanticModel::Requirement(model) => {
assert_eq!(model.acc_title.as_deref(), Some("Requirement accTitle"));
assert_eq!(
model.acc_descr.as_deref(),
Some("Requirement accDescription")
);
assert_eq!(model.direction, "LR");
assert_eq!(model.requirements.len(), 1);
assert_eq!(model.requirements[0].name, "req_login");
assert_eq!(model.requirements[0].node_type, "Requirement");
assert_eq!(model.requirements[0].requirement_id, "REQ-1");
assert_eq!(model.requirements[0].text, "Login must work");
assert_eq!(model.requirements[0].risk, "High");
assert_eq!(model.requirements[0].verify_method, "Test");
assert!(
model.requirements[0]
.classes
.iter()
.any(|c| c == "critical")
);
assert!(
model.requirements[0]
.css_styles
.iter()
.any(|s| s == "fill:#f9f")
);
assert_eq!(model.elements.len(), 1);
assert_eq!(model.elements[0].name, "api");
assert_eq!(model.elements[0].element_type, "service");
assert_eq!(model.elements[0].doc_ref, "docs/api.md");
assert!(model.elements[0].classes.iter().any(|c| c == "external"));
assert_eq!(model.relationships.len(), 1);
assert_eq!(model.relationships[0].rel_type, "verifies");
assert_eq!(model.relationships[0].src, "req_login");
assert_eq!(model.relationships[0].dst, "api");
assert!(model.classes.contains_key("critical"));
assert!(model.classes.contains_key("external"));
}
other => panic!("requirement render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("requirement"));
assert_eq!(parsed_json.model["accTitle"], json!("Requirement accTitle"));
assert_eq!(
parsed_json.model["accDescr"],
json!("Requirement accDescription")
);
assert_eq!(parsed_json.model["direction"], json!("LR"));
assert_eq!(
parsed_json.model["requirements"][0]["name"],
json!("req_login")
);
assert_eq!(
parsed_json.model["requirements"][0]["requirementId"],
json!("REQ-1")
);
assert_eq!(
parsed_json.model["requirements"][0]["verifyMethod"],
json!("Test")
);
assert_eq!(
parsed_json.model["elements"][0]["docRef"],
json!("docs/api.md")
);
assert_eq!(
parsed_json.model["relationships"][0]["type"],
json!("verifies")
);
assert!(parsed_json.model.get("config").is_some());
}
#[test]
fn parse_sankey_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r#"
sankey-beta
Source,Target,10
Target,Done,2.5
"#;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "sankey");
match parsed.model {
RenderSemanticModel::Sankey(model) => {
assert_eq!(model.graph.nodes.len(), 3);
assert_eq!(model.graph.nodes[0].id, "Source");
assert_eq!(model.graph.nodes[1].id, "Target");
assert_eq!(model.graph.nodes[2].id, "Done");
assert_eq!(model.graph.links.len(), 2);
assert_eq!(model.graph.links[0].source, "Source");
assert_eq!(model.graph.links[0].target, "Target");
assert_eq!(model.graph.links[0].value, json!(10));
assert_eq!(model.graph.links[1].source, "Target");
assert_eq!(model.graph.links[1].target, "Done");
assert_eq!(model.graph.links[1].value, json!(2.5));
}
other => panic!("sankey render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("sankey"));
assert_eq!(
parsed_json.model["graph"]["nodes"][0]["id"],
json!("Source")
);
assert_eq!(
parsed_json.model["graph"]["links"][0],
json!({
"source": "Source",
"target": "Target",
"value": 10,
})
);
assert_eq!(parsed_json.model["graph"]["links"][1]["value"], json!(2.5));
assert!(parsed_json.model.get("config").is_some());
}
#[test]
fn parse_radar_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r#"
radar-beta
title Typed Radar
accTitle: Radar accTitle
accDescr: Radar accDescription
axis A["Axis A"], B["Axis B"], C["Axis C"]
curve first["First Curve"]{1,2,3}
curve second{ C: 9, A: 7, B: 8 }
showLegend false
ticks 4
min 1
max 10
graticule polygon
"#;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "radar");
match parsed.model {
RenderSemanticModel::Radar(model) => {
assert_eq!(model.title.as_deref(), Some("Typed Radar"));
assert_eq!(model.acc_title.as_deref(), Some("Radar accTitle"));
assert_eq!(model.acc_descr.as_deref(), Some("Radar accDescription"));
assert_eq!(model.axes.len(), 3);
assert_eq!(model.axes[0].name, "A");
assert_eq!(model.axes[0].label, "Axis A");
assert_eq!(model.curves.len(), 2);
assert_eq!(model.curves[0].label, "First Curve");
assert_eq!(model.curves[0].entries, vec![json!(1), json!(2), json!(3)]);
assert_eq!(model.curves[1].entries, vec![json!(7), json!(8), json!(9)]);
assert!(!model.options.show_legend);
assert_eq!(model.options.ticks, json!(4));
assert_eq!(model.options.min, json!(1));
assert_eq!(model.options.max, Some(json!(10)));
assert_eq!(model.options.graticule, "polygon");
}
other => panic!("radar render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("radar"));
assert_eq!(parsed_json.model["title"], json!("Typed Radar"));
assert_eq!(parsed_json.model["accTitle"], json!("Radar accTitle"));
assert_eq!(
parsed_json.model["axes"][0],
json!({"name": "A", "label": "Axis A"})
);
assert_eq!(
parsed_json.model["curves"][1],
json!({"name": "second", "label": "second", "entries": [7, 8, 9]})
);
assert_eq!(
parsed_json.model["options"],
json!({
"showLegend": false,
"ticks": 4,
"max": 10,
"min": 1,
"graticule": "polygon",
})
);
assert!(parsed_json.model.get("config").is_some());
}
#[test]
fn parse_info_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r#"
info
showInfo
"#;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "info");
match parsed.model {
RenderSemanticModel::Info(model) => {
assert!(model.show_info);
}
other => panic!("info render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("info"));
assert_eq!(parsed_json.model["showInfo"], json!(true));
}
#[test]
fn parse_er_render_model_uses_typed_variant_without_changing_json_parse() {
let engine = Engine::new();
let input = r#"
erDiagram
accTitle: ER accTitle
accDescr: ER accDescription
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
string id
}
"#;
let parsed = engine
.parse_diagram_for_render_model_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed.meta.diagram_type, "er");
match parsed.model {
RenderSemanticModel::Er(model) => {
assert_eq!(model.acc_title.as_deref(), Some("ER accTitle"));
assert_eq!(model.acc_descr.as_deref(), Some("ER accDescription"));
assert_eq!(model.direction, "TB");
assert!(model.entities.contains_key("CUSTOMER"));
assert!(model.entities.contains_key("ORDER"));
assert_eq!(model.relationships.len(), 1);
assert_eq!(model.relationships[0].entity_a, "entity-CUSTOMER-0");
assert_eq!(model.relationships[0].entity_b, "entity-ORDER-1");
assert_eq!(model.relationships[0].role_a, "places");
assert_eq!(model.relationships[0].rel_spec.card_a, "ZERO_OR_MORE");
assert_eq!(model.relationships[0].rel_spec.card_b, "ONLY_ONE");
assert_eq!(model.relationships[0].rel_spec.rel_type, "IDENTIFYING");
}
other => panic!("er render parse should return typed model, got {other:?}"),
}
let parsed_json = engine
.parse_diagram_sync(input, ParseOptions::strict())
.unwrap()
.unwrap();
assert_eq!(parsed_json.model["type"], json!("er"));
assert_eq!(parsed_json.model["accTitle"], json!("ER accTitle"));
assert_eq!(
parsed_json.model["relationships"][0]["roleA"],
json!("places")
);
assert!(parsed_json.model.get("constants").is_some());
}
#[test]
fn parse_sanitizes_common_db_fields_in_strict_mode() {
let engine = Engine::new();
let text = r#"sequenceDiagram
title: <script>alert(1)</script><b>t</b>
accTitle: <script>alert(1)</script><b>a</b>
accDescr: <script>alert(1)</script><b>d</b>
Alice->Bob:Hello"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.model["title"], json!("<b>t</b>"));
assert_eq!(res.model["accTitle"], json!("<b>a</b>"));
assert_eq!(res.model["accDescr"], json!("<b>d</b>"));
}