Skip to main content

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}