mermaid_text/er.rs
1//! Data model for Mermaid `erDiagram` (entity-relationship) charts.
2//!
3//! Mermaid's erDiagram describes entities (tables / record types)
4//! with attribute lists, joined by relationships that carry
5//! crow's-foot cardinality glyphs at each end.
6//!
7//! Example:
8//!
9//! ```text
10//! erDiagram
11//! CUSTOMER ||--o{ ORDER : places
12//! CUSTOMER {
13//! string name
14//! string email PK
15//! }
16//! ORDER ||--|{ LINE-ITEM : contains
17//! ```
18//!
19//! The cardinality halves `||`, `}|`, `}o`, `o|` map to
20//! [`Cardinality::ExactlyOne`], [`OneOrMany`], [`ZeroOrMany`],
21//! [`ZeroOrOne`]. The connector between them — `--` or `..` — picks
22//! [`LineStyle::Identifying`] or [`LineStyle::NonIdentifying`].
23//!
24//! [`OneOrMany`]: Cardinality::OneOrMany
25//! [`ZeroOrMany`]: Cardinality::ZeroOrMany
26//! [`ZeroOrOne`]: Cardinality::ZeroOrOne
27
28/// One column of an [`Entity`]'s attribute table.
29///
30/// `type_name` and `name` are required; `keys` is the list of
31/// recognised modifiers in source order; `comment` is the optional
32/// trailing quoted string.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Attribute {
35 pub type_name: String,
36 pub name: String,
37 pub keys: Vec<AttributeKey>,
38 pub comment: Option<String>,
39}
40
41/// Recognised key modifiers on an [`Attribute`]. Mermaid's grammar
42/// admits exactly these three; arbitrary other modifiers are rejected
43/// at parse time so typos surface instead of silently disappearing.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum AttributeKey {
46 /// Primary key (`PK`).
47 PrimaryKey,
48 /// Foreign key (`FK`).
49 ForeignKey,
50 /// Unique key (`UK`).
51 UniqueKey,
52}
53
54/// One row in an [`ErDiagram`] — a named entity with an attribute
55/// list. The list may be empty (entities mentioned only in
56/// relationships and never declared with a body).
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct Entity {
59 pub name: String,
60 pub attributes: Vec<Attribute>,
61}
62
63impl Entity {
64 /// Construct an entity with no attributes — used by the parser
65 /// when an entity name first appears as a relationship endpoint
66 /// before its `{ ... }` block (or when no block is ever supplied).
67 pub fn bare(name: impl Into<String>) -> Self {
68 Self {
69 name: name.into(),
70 attributes: Vec::new(),
71 }
72 }
73}
74
75/// One end of a [`Relationship`]'s cardinality. Each Mermaid
76/// crow's-foot half (`||`, `}|`, `}o`, `o|`) maps to one of these
77/// four discrete categories.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum Cardinality {
80 /// `||` — required, exactly one (mandatory single).
81 ExactlyOne,
82 /// `o|` (or `|o`) — optional, at most one.
83 ZeroOrOne,
84 /// `}|` (or `|{`) — required, one or more.
85 OneOrMany,
86 /// `}o` (or `o{`) — optional, zero or more.
87 ZeroOrMany,
88}
89
90/// Connector style between two cardinality halves of a relationship.
91/// Mermaid distinguishes `--` (identifying — solid line, child cannot
92/// exist without parent) from `..` (non-identifying — dashed line,
93/// looser association).
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum LineStyle {
96 /// `--` — solid line. Child entity's identity depends on parent's.
97 Identifying,
98 /// `..` — dashed line. Looser association.
99 NonIdentifying,
100}
101
102impl LineStyle {
103 /// True for `..` (dashed) — used by the renderer to pick `┄`
104 /// over `─` for the relationship line glyph.
105 pub fn is_dashed(self) -> bool {
106 matches!(self, LineStyle::NonIdentifying)
107 }
108}
109
110/// One labelled relationship between two entities.
111///
112/// `from` and `to` reference [`Entity::name`]s. The two cardinality
113/// fields describe each end's "how many" semantics; together they
114/// reconstruct Mermaid's `||--o{` style notation.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct Relationship {
117 pub from: String,
118 pub to: String,
119 pub from_cardinality: Cardinality,
120 pub to_cardinality: Cardinality,
121 pub line_style: LineStyle,
122 pub label: Option<String>,
123}
124
125/// A parsed `erDiagram` chart.
126///
127/// Constructed by [`crate::parser::er::parse`] and consumed by
128/// [`crate::render::er::render`]. Entities are listed in declaration
129/// order; relationships in the order they appear in the source.
130#[derive(Debug, Clone, PartialEq, Eq, Default)]
131pub struct ErDiagram {
132 pub entities: Vec<Entity>,
133 pub relationships: Vec<Relationship>,
134}
135
136impl ErDiagram {
137 /// Find an entity by name (case-sensitive — Mermaid treats entity
138 /// names as opaque identifiers). Returns the entity's index in
139 /// `entities` so callers can update it in place.
140 pub fn entity_index(&self, name: &str) -> Option<usize> {
141 self.entities.iter().position(|e| e.name == name)
142 }
143
144 /// Insert an entity if its name isn't already present, returning
145 /// its index either way. Used by the parser when a relationship
146 /// mentions an entity before its `{ … }` body has been declared.
147 pub fn ensure_entity(&mut self, name: &str) -> usize {
148 if let Some(idx) = self.entity_index(name) {
149 return idx;
150 }
151 self.entities.push(Entity::bare(name));
152 self.entities.len() - 1
153 }
154}
155
156// ---------------------------------------------------------------------------
157// Tests
158// ---------------------------------------------------------------------------
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn line_style_is_dashed_distinguishes_identifying_from_non() {
166 assert!(LineStyle::NonIdentifying.is_dashed());
167 assert!(!LineStyle::Identifying.is_dashed());
168 }
169
170 #[test]
171 fn entity_bare_starts_with_no_attributes() {
172 let e = Entity::bare("CUSTOMER");
173 assert_eq!(e.name, "CUSTOMER");
174 assert!(e.attributes.is_empty());
175 }
176
177 #[test]
178 fn ensure_entity_inserts_then_reuses() {
179 let mut diag = ErDiagram::default();
180 let first = diag.ensure_entity("A");
181 let second = diag.ensure_entity("A");
182 let third = diag.ensure_entity("B");
183 assert_eq!(first, 0);
184 assert_eq!(second, 0); // reuse existing
185 assert_eq!(third, 1);
186 assert_eq!(diag.entities.len(), 2);
187 }
188
189 #[test]
190 fn entity_index_returns_none_for_unknown() {
191 let diag = ErDiagram::default();
192 assert_eq!(diag.entity_index("X"), None);
193 }
194}