use crate::*;
use futures::executor::block_on;
use serde_json::json;
#[test]
fn parse_diagram_er_allows_standalone_entities() {
let engine = Engine::new();
let text = "erDiagram\nISLAND\nMAINLAND\n";
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.meta.diagram_type, "er");
assert_eq!(res.model["relationships"].as_array().unwrap().len(), 0);
assert_eq!(res.model["entities"].as_object().unwrap().len(), 2);
assert!(res.model["entities"].get("ISLAND").is_some());
assert!(res.model["entities"].get("MAINLAND").is_some());
}
#[test]
fn parse_diagram_er_parses_alias_and_attributes() {
let engine = Engine::new();
let text = r#"erDiagram
foo["bar"] {
string title PK, FK "comment"
}
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
let e = &res.model["entities"]["foo"];
assert_eq!(e["alias"], json!("bar"));
assert_eq!(e["attributes"][0]["type"], json!("string"));
assert_eq!(e["attributes"][0]["name"], json!("title"));
assert_eq!(e["attributes"][0]["keys"], json!(["PK", "FK"]));
assert_eq!(e["attributes"][0]["comment"], json!("comment"));
}
#[test]
fn parse_diagram_er_empty_quoted_entity_name_is_error() {
let engine = Engine::new();
let err = block_on(engine.parse_diagram("erDiagram\n\"\"\n", ParseOptions::default()))
.unwrap_err()
.to_string();
assert!(err.contains("DiagramParse") || err.contains("Unsupported") || !err.is_empty());
}
#[test]
fn parse_diagram_er_rejects_percent_and_backslash_in_quoted_entity_name() {
let engine = Engine::new();
assert!(
block_on(engine.parse_diagram("erDiagram\n\"Blo%rf\"\n", ParseOptions::default())).is_err()
);
assert!(
block_on(engine.parse_diagram("erDiagram\n\"Blo\\\\rf\"\n", ParseOptions::default()))
.is_err()
);
}
#[test]
fn parse_diagram_er_supports_empty_attribute_blocks_and_multiple_blocks() {
let engine = Engine::new();
let text = r#"erDiagram
BOOK {}
BOOK {
string title
}
BOOK{
string author
}
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
let e = &res.model["entities"]["BOOK"];
assert_eq!(e["attributes"].as_array().unwrap().len(), 2);
assert_eq!(e["attributes"][0]["name"], json!("title"));
assert_eq!(e["attributes"][1]["name"], json!("author"));
}
#[test]
fn parse_diagram_er_alias_applies_even_when_relationship_is_defined_first() {
let engine = Engine::new();
let text = r#"erDiagram
foo ||--o{ bar : rel
foo["batman"]
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.model["entities"]["foo"]["alias"], json!("batman"));
assert_eq!(res.model["entities"]["bar"]["alias"], json!(""));
}
#[test]
fn parse_diagram_er_allows_multiple_statements_without_newlines_like_upstream_jison() {
let engine = Engine::new();
let text = r#"erDiagram
foo ||--o{ bar : rel
buzz foo["batman"]
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.model["entities"]["foo"]["alias"], json!("batman"));
assert_eq!(res.model["entities"]["bar"]["alias"], json!(""));
assert!(res.model["entities"].get("buzz").is_some());
}
#[test]
fn parse_diagram_er_self_relationship_does_not_duplicate_entity() {
let engine = Engine::new();
let text = r#"erDiagram
NODE ||--o{ NODE : "leads to"
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.model["entities"].as_object().unwrap().len(), 1);
let rels = res.model["relationships"].as_array().unwrap();
assert_eq!(rels.len(), 1);
assert_eq!(rels[0]["entityA"], rels[0]["entityB"]);
}
#[test]
fn parse_diagram_er_inline_class_assignment_applies_css_classes() {
let engine = Engine::new();
let text = r#"erDiagram
FOO:::pink
classDef pink fill:#f9f
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
let e = &res.model["entities"]["FOO"];
assert_eq!(e["cssClasses"], json!("default pink"));
assert_eq!(res.model["classes"]["pink"]["styles"], json!(["fill:#f9f"]));
}
#[test]
fn parse_diagram_er_direction_statement_sets_direction() {
let engine = Engine::new();
let text = r#"erDiagram
direction LR
A ||--o{ B : has
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.model["direction"], json!("LR"));
}
#[test]
fn parse_diagram_er_allows_hyphen_and_underscore_in_unquoted_entity_name() {
let engine = Engine::new();
let text = "erDiagram\nDUCK-BILLED_PLATYPUS\n";
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert!(res.model["entities"].get("DUCK-BILLED_PLATYPUS").is_some());
}
#[test]
fn parse_diagram_er_rejects_unquoted_entity_names_with_non_identifier_punctuation() {
let engine = Engine::new();
assert!(
block_on(engine.parse_diagram("erDiagram\nBlo@rf\n", ParseOptions::default())).is_err()
);
assert!(
block_on(engine.parse_diagram("erDiagram\nBlo?rf\n", ParseOptions::default())).is_err()
);
}
#[test]
fn parse_diagram_er_supports_attribute_name_brackets_and_parens() {
let engine = Engine::new();
let text = r#"erDiagram
BOOK {
string author-ref[name](1)
}
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
let attrs = res.model["entities"]["BOOK"]["attributes"]
.as_array()
.unwrap();
assert_eq!(attrs.len(), 1);
assert_eq!(attrs[0]["name"], json!("author-ref[name](1)"));
}
#[test]
fn parse_diagram_er_allows_asterisk_at_start_of_attribute_name() {
let engine = Engine::new();
let text = r#"erDiagram
BOOK {
string *title
id *the_Primary_Key
}
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
let attrs = res.model["entities"]["BOOK"]["attributes"]
.as_array()
.unwrap();
assert_eq!(attrs.len(), 2);
}
#[test]
fn parse_diagram_er_rejects_attribute_names_with_leading_numbers_dashes_or_brackets() {
let engine = Engine::new();
for bad in ["0author", "-author", "[author", "(author"] {
let text = format!("erDiagram\nBOOK {{\n string {bad}\n}}\n");
assert!(block_on(engine.parse_diagram(&text, ParseOptions::default())).is_err());
}
}
#[test]
fn parse_diagram_er_supports_generic_and_array_and_limited_length_types() {
let engine = Engine::new();
let text = r#"erDiagram
BOOK {
type~T~ type
option~T~ readable "comment"
string[] readers FK
character(10) isbn FK
varchar(5) postal_code "Five digits"
}
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
let attrs = res.model["entities"]["BOOK"]["attributes"]
.as_array()
.unwrap();
assert_eq!(attrs.len(), 5);
assert_eq!(attrs[3]["type"], json!("character(10)"));
assert_eq!(attrs[4]["type"], json!("varchar(5)"));
assert_eq!(attrs[4]["comment"], json!("Five digits"));
}
#[test]
fn parse_diagram_er_parses_many_constraints_and_comments() {
let engine = Engine::new();
let text = r#"erDiagram
CUSTOMER {
int customer_number PK, FK "comment1"
datetime customer_status_start_datetime PK,UK, FK
datetime customer_status_end_datetime PK , UK "comment3"
string customer_firstname
string customer_lastname "comment5"
}
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
let attrs = res.model["entities"]["CUSTOMER"]["attributes"]
.as_array()
.unwrap();
assert_eq!(attrs[0]["keys"], json!(["PK", "FK"]));
assert_eq!(attrs[0]["comment"], json!("comment1"));
assert_eq!(attrs[1]["keys"], json!(["PK", "UK", "FK"]));
assert_eq!(attrs[2]["keys"], json!(["PK", "UK"]));
assert_eq!(attrs[2]["comment"], json!("comment3"));
assert_eq!(attrs[3]["keys"], json!([]));
assert_eq!(attrs[4]["keys"], json!([]));
assert_eq!(attrs[4]["comment"], json!("comment5"));
}
#[test]
fn parse_diagram_er_allows_multiple_relationships_between_same_two_entities() {
let engine = Engine::new();
let text = r#"erDiagram
CAR ||--o{ PERSON : "insured for"
CAR }o--|| PERSON : "owned by"
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.model["entities"].as_object().unwrap().len(), 2);
assert_eq!(res.model["relationships"].as_array().unwrap().len(), 2);
}
#[test]
fn parse_diagram_er_supports_one_or_more_cardinality_markers() {
let engine = Engine::new();
let text = r#"erDiagram
A ||--|{ B : has
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
let rels = res.model["relationships"].as_array().unwrap();
assert_eq!(rels.len(), 1);
assert_eq!(rels[0]["relSpec"]["cardA"], json!("ONE_OR_MORE"));
assert_eq!(rels[0]["relSpec"]["cardB"], json!("ONLY_ONE"));
}
#[test]
fn parse_diagram_er_relationship_matrix_matches_upstream_spec_minimally() {
let engine = Engine::new();
let cases: &[(&str, &str, &str, &str)] = &[
("A ||--|{ B : has", "ONE_OR_MORE", "ONLY_ONE", "IDENTIFYING"),
(
"A ||..o{ B : has",
"ZERO_OR_MORE",
"ONLY_ONE",
"NON_IDENTIFYING",
),
(
"A |o..o{ B : has",
"ZERO_OR_MORE",
"ZERO_OR_ONE",
"NON_IDENTIFYING",
),
(
"A |o--|{ B : has",
"ONE_OR_MORE",
"ZERO_OR_ONE",
"IDENTIFYING",
),
("A }|--|| B : has", "ONLY_ONE", "ONE_OR_MORE", "IDENTIFYING"),
(
"A }o--|| B : has",
"ONLY_ONE",
"ZERO_OR_MORE",
"IDENTIFYING",
),
(
"A }o..o| B : has",
"ZERO_OR_ONE",
"ZERO_OR_MORE",
"NON_IDENTIFYING",
),
(
"A }|..o| B : has",
"ZERO_OR_ONE",
"ONE_OR_MORE",
"NON_IDENTIFYING",
),
(
"A |o..|| B : has",
"ONLY_ONE",
"ZERO_OR_ONE",
"NON_IDENTIFYING",
),
(
"A ||..|| B : has",
"ONLY_ONE",
"ONLY_ONE",
"NON_IDENTIFYING",
),
("A ||--o| B : has", "ZERO_OR_ONE", "ONLY_ONE", "IDENTIFYING"),
(
"A |o..o| B : has",
"ZERO_OR_ONE",
"ZERO_OR_ONE",
"NON_IDENTIFYING",
),
(
"A }o--o{ B : has",
"ZERO_OR_MORE",
"ZERO_OR_MORE",
"IDENTIFYING",
),
(
"A }|..|{ B : has",
"ONE_OR_MORE",
"ONE_OR_MORE",
"NON_IDENTIFYING",
),
(
"A }o--|{ B : has",
"ONE_OR_MORE",
"ZERO_OR_MORE",
"IDENTIFYING",
),
(
"A }|..o{ B : has",
"ZERO_OR_MORE",
"ONE_OR_MORE",
"NON_IDENTIFYING",
),
(
"A ||.-o{ B : has",
"ZERO_OR_MORE",
"ONLY_ONE",
"NON_IDENTIFYING",
),
(
"A ||-.o{ B : has",
"ZERO_OR_MORE",
"ONLY_ONE",
"NON_IDENTIFYING",
),
];
for (line, card_a, card_b, rel_type) in cases {
let text = format!("erDiagram\n{line}\n");
let res = block_on(engine.parse_diagram(&text, ParseOptions::default()))
.unwrap()
.unwrap();
let rels = res.model["relationships"].as_array().unwrap();
assert_eq!(rels.len(), 1, "{line}");
assert_eq!(rels[0]["relSpec"]["cardA"], json!(*card_a), "{line}");
assert_eq!(rels[0]["relSpec"]["cardB"], json!(*card_b), "{line}");
assert_eq!(rels[0]["relSpec"]["relType"], json!(*rel_type), "{line}");
}
}
#[test]
fn parse_diagram_er_relationship_word_aliases_match_upstream_spec_minimally() {
let engine = Engine::new();
let cases: &[(&str, &str, &str, &str)] = &[
(
"A one or zero to many B : has",
"ZERO_OR_MORE",
"ZERO_OR_ONE",
"IDENTIFYING",
),
(
"A one or many optionally to zero or one B : has",
"ZERO_OR_ONE",
"ONE_OR_MORE",
"NON_IDENTIFYING",
),
(
"A zero or more to zero or many B : has",
"ZERO_OR_MORE",
"ZERO_OR_MORE",
"IDENTIFYING",
),
(
"A many(0) to many(1) B : has",
"ONE_OR_MORE",
"ZERO_OR_MORE",
"IDENTIFYING",
),
(
"A many optionally to one B : has",
"ONLY_ONE",
"ZERO_OR_MORE",
"NON_IDENTIFYING",
),
(
"A only one optionally to 1+ B : has",
"ONE_OR_MORE",
"ONLY_ONE",
"NON_IDENTIFYING",
),
(
"A 0+ optionally to 1 B : has",
"ONLY_ONE",
"ZERO_OR_MORE",
"NON_IDENTIFYING",
),
(
"HOUSE one to one ROOM : contains",
"ONLY_ONE",
"ONLY_ONE",
"IDENTIFYING",
),
];
for (line, card_a, card_b, rel_type) in cases {
let text = format!("erDiagram\n{line}\n");
let res = block_on(engine.parse_diagram(&text, ParseOptions::default()))
.unwrap()
.unwrap();
let rels = res.model["relationships"].as_array().unwrap();
assert_eq!(rels.len(), 1, "{line}");
assert_eq!(rels[0]["relSpec"]["cardA"], json!(*card_a), "{line}");
assert_eq!(rels[0]["relSpec"]["cardB"], json!(*card_b), "{line}");
assert_eq!(rels[0]["relSpec"]["relType"], json!(*rel_type), "{line}");
}
}
#[test]
fn parse_diagram_er_rejects_invalid_relationship_syntax() {
let engine = Engine::new();
assert!(
block_on(engine.parse_diagram("erDiagram\nA xxx B : has\n", ParseOptions::default()))
.is_err()
);
}
#[test]
fn parse_diagram_er_style_statement_applies_styles() {
let engine = Engine::new();
let text = r#"erDiagram
CUSTOMER
style CUSTOMER color:red,stroke:blue,fill:#f9f
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(
res.model["entities"]["CUSTOMER"]["cssStyles"],
json!(["color:red", "stroke:blue", "fill:#f9f"])
);
}
#[test]
fn parse_diagram_er_style_statements_append_across_multiple_lines() {
let engine = Engine::new();
let text = r#"erDiagram
CUSTOMER
style CUSTOMER color:red
style CUSTOMER fill:#f9f
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(
res.model["entities"]["CUSTOMER"]["cssStyles"],
json!(["color:red", "fill:#f9f"])
);
}
#[test]
fn parse_diagram_er_class_statement_assigns_classes() {
let engine = Engine::new();
let text = r#"erDiagram
CUSTOMER
class CUSTOMER firstClass, secondClass, thirdClass
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(
res.model["entities"]["CUSTOMER"]["cssClasses"],
json!("default firstClass secondClass thirdClass")
);
}
#[test]
fn parse_diagram_er_class_statement_appends_across_multiple_lines() {
let engine = Engine::new();
let text = r#"erDiagram
CUSTOMER
class CUSTOMER firstClass
class CUSTOMER secondClass
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(
res.model["entities"]["CUSTOMER"]["cssClasses"],
json!("default firstClass secondClass")
);
}
#[test]
fn parse_diagram_er_classdef_defines_styles_and_text_styles() {
let engine = Engine::new();
let text = r#"erDiagram
classDef myClass fill:#f9f, stroke: red, color: pink
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(
res.model["classes"]["myClass"],
json!({
"id": "myClass",
"styles": ["fill:#f9f", "stroke:red", "color:pink"],
"textStyles": ["color:pink"]
})
);
}
#[test]
fn parse_diagram_er_classdef_supports_multiple_classes_in_one_statement() {
let engine = Engine::new();
let text = r#"erDiagram
classDef firstClass,secondClass fill:#f9f, stroke: red, color: pink
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(
res.model["classes"]["firstClass"]["styles"][1],
json!("stroke:red")
);
assert_eq!(
res.model["classes"]["secondClass"]["styles"][2],
json!("color:pink")
);
}
#[test]
fn parse_diagram_er_shorthand_class_assignment_variants_work() {
let engine = Engine::new();
let text = r#"erDiagram
CUSTOMER:::myClass
CUSTOMER2:::myClass {}
CUSTOMER3:::myClass {
string name
}
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(
res.model["entities"]["CUSTOMER"]["cssClasses"],
json!("default myClass")
);
assert_eq!(
res.model["entities"]["CUSTOMER2"]["cssClasses"],
json!("default myClass")
);
assert_eq!(
res.model["entities"]["CUSTOMER3"]["cssClasses"],
json!("default myClass")
);
}
#[test]
fn parse_diagram_er_shorthand_assignment_supports_multiple_classes_and_alias() {
let engine = Engine::new();
let text = r#"erDiagram
c[CUSTOMER]:::firstClass,secondClass
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.model["entities"]["c"]["alias"], json!("CUSTOMER"));
assert_eq!(
res.model["entities"]["c"]["cssClasses"],
json!("default firstClass secondClass")
);
}
#[test]
fn parse_diagram_er_shorthand_assignment_works_in_relationships() {
let engine = Engine::new();
let text = r#"erDiagram
CUSTOMER:::myClass ||--o{ PERSON:::myClass : allows
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(
res.model["entities"]["CUSTOMER"]["cssClasses"],
json!("default myClass")
);
assert_eq!(
res.model["entities"]["PERSON"]["cssClasses"],
json!("default myClass")
);
}
#[test]
fn parse_diagram_er_relationship_labels_allow_empty_quoted_and_unquoted() {
let engine = Engine::new();
let res_empty = block_on(engine.parse_diagram(
"erDiagram\nCUSTOMER ||--|{ ORDER : \"\"\n",
ParseOptions::default(),
))
.unwrap()
.unwrap();
assert_eq!(res_empty.model["relationships"][0]["roleA"], json!(""));
let res_unquoted = block_on(engine.parse_diagram(
"erDiagram\nCUSTOMER ||--|{ ORDER : places\n",
ParseOptions::default(),
))
.unwrap()
.unwrap();
assert_eq!(
res_unquoted.model["relationships"][0]["roleA"],
json!("places")
);
}
#[test]
fn parse_diagram_er_parent_child_relationship_sets_md_parent_cardinality() {
let engine = Engine::new();
let text = r#"erDiagram
PROJECT u--o{ TEAM_MEMBER : "parent"
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
let rel = &res.model["relationships"][0];
assert_eq!(rel["relSpec"]["cardB"], json!("MD_PARENT"));
assert_eq!(rel["relSpec"]["cardA"], json!("ZERO_OR_MORE"));
}
#[test]
fn parse_diagram_er_allows_prototype_like_entity_names() {
let engine = Engine::new();
for name in ["__proto__", "constructor", "prototype"] {
let text = format!("erDiagram\n{name} ||--|{{ ORDER : place\n");
assert!(block_on(engine.parse_diagram(&text, ParseOptions::default())).is_ok());
}
}
#[test]
fn parse_diagram_er_parses_relationship_cardinality_and_type() {
let engine = Engine::new();
let text = r#"erDiagram
CAR ||--o{ DRIVER : "insured for"
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.model["entities"].as_object().unwrap().len(), 2);
let rels = res.model["relationships"].as_array().unwrap();
assert_eq!(rels.len(), 1);
assert_eq!(rels[0]["roleA"], json!("insured for"));
assert_eq!(rels[0]["relSpec"]["cardA"], json!("ZERO_OR_MORE"));
assert_eq!(rels[0]["relSpec"]["cardB"], json!("ONLY_ONE"));
assert_eq!(rels[0]["relSpec"]["relType"], json!("IDENTIFYING"));
}
#[test]
fn parse_diagram_er_supports_numeric_cardinality_shorthands() {
let engine = Engine::new();
let text = r#"erDiagram
A 1+--0+ B : has
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
let rels = res.model["relationships"].as_array().unwrap();
assert_eq!(rels.len(), 1);
assert_eq!(rels[0]["relSpec"]["cardA"], json!("ZERO_OR_MORE"));
assert_eq!(rels[0]["relSpec"]["cardB"], json!("ONE_OR_MORE"));
}
#[test]
fn parse_diagram_er_acc_title_and_multiline_description() {
let engine = Engine::new();
let text = r#"erDiagram
accTitle: graph title
accDescr { this graph is
about
stuff
}
A ||--o{ B : has
"#;
let res = block_on(engine.parse_diagram(text, ParseOptions::default()))
.unwrap()
.unwrap();
assert_eq!(res.model["accTitle"], json!("graph title"));
assert_eq!(res.model["accDescr"], json!("this graph is\nabout\nstuff"));
}