Skip to main content

mermaid_text/
class.rs

1//! Data model for Mermaid `classDiagram` charts.
2//!
3//! Mermaid's `classDiagram` describes object-oriented classes with attributes,
4//! methods, visibility modifiers, and inter-class relationships (inheritance,
5//! composition, aggregation, association, dependency, and realization).
6//!
7//! # v1 supported features
8//!
9//! - Class declarations with optional `{ … }` body
10//! - Member visibility: `+` public, `-` private, `#` protected, `~` package
11//! - Attributes: `+name Type` or `+Type name` (typed-before and typed-after)
12//! - Methods: `+method(args) ReturnType`; `$` static suffix, `*` abstract suffix
13//! - Stereotypes: `<<interface>>`, `<<enumeration>>`, `<<abstract>>`
14//! - All seven relationship types (see [`RelKind`])
15//! - Edge labels and multiplicity (quoted strings)
16//! - `%%` line comments
17//!
18//! # v1 explicitly unsupported
19//!
20//! The following Mermaid features return a [`crate::Error::ParseError`] when
21//! encountered:
22//! - Generics (`Class~T~`)
23//! - Namespace blocks
24//! - `note for X` annotations
25//! - `link` / `click` directives
26//! - Colon-shorthand member form (`Animal : +name String`)
27//! - `direction` header inside the diagram body
28
29/// Visibility modifier on a class member.
30///
31/// Mermaid's four visibility symbols map directly to OOP conventions.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Visibility {
34    /// `+` — public
35    Public,
36    /// `-` — private
37    Private,
38    /// `#` — protected
39    Protected,
40    /// `~` — package (internal to the package/module)
41    Package,
42}
43
44impl Visibility {
45    /// The single-character source symbol for this visibility level.
46    pub fn as_char(self) -> char {
47        match self {
48            Self::Public => '+',
49            Self::Private => '-',
50            Self::Protected => '#',
51            Self::Package => '~',
52        }
53    }
54}
55
56/// An attribute (field) member inside a class body.
57///
58/// Mermaid allows the type before or after the name:
59/// - `+String name` — typed-before (Mermaid's primary form)
60/// - `+name String` — typed-after (also accepted)
61///
62/// Both forms are normalised to `(visibility, name, type)` here.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct Attribute {
65    /// Visibility modifier (may be `None` when the `+/-/#/~` prefix is absent).
66    pub visibility: Option<Visibility>,
67    /// Attribute name.
68    pub name: String,
69    /// Declared type (may be empty if the source omits it).
70    pub type_name: String,
71    /// `true` when the `$` suffix marks this attribute as static.
72    pub is_static: bool,
73}
74
75/// A method member inside a class body.
76///
77/// `+method(args) ReturnType$*` decomposes as:
78/// - visibility = `Public`
79/// - name = `"method"`
80/// - params = `"args"`
81/// - return_type = `Some("ReturnType")`
82/// - is_static = `false`
83/// - is_abstract = `false`
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct Method {
86    /// Visibility modifier.
87    pub visibility: Option<Visibility>,
88    /// Method name.
89    pub name: String,
90    /// Raw parameter list (content between parentheses), may be empty.
91    pub params: String,
92    /// Declared return type, if present.
93    pub return_type: Option<String>,
94    /// `true` when the `$` suffix marks this method as static.
95    pub is_static: bool,
96    /// `true` when the `*` suffix marks this method as abstract.
97    pub is_abstract: bool,
98}
99
100/// A member of a class body — either an attribute or a method.
101///
102/// Methods are distinguished from attributes by the presence of
103/// parentheses in the source (`method()`).
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum Member {
106    /// A typed field / property.
107    Attribute(Attribute),
108    /// A callable method.
109    Method(Method),
110}
111
112impl Member {
113    /// The display name of this member (used for box width calculations).
114    pub fn name(&self) -> &str {
115        match self {
116            Self::Attribute(a) => &a.name,
117            Self::Method(m) => &m.name,
118        }
119    }
120}
121
122/// UML stereotype associated with a class.
123///
124/// Stereotypes appear as `<<name>>` inside or below the class declaration.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum Stereotype {
127    /// `<<interface>>` — class is a pure interface.
128    Interface,
129    /// `<<enumeration>>` — class is an enumeration.
130    Enumeration,
131    /// `<<abstract>>` — class is abstract.
132    Abstract,
133    /// Any other `<<name>>` (stored verbatim for future use).
134    Other(String),
135}
136
137impl Stereotype {
138    /// Returns the raw label string shown in `<<…>>`.
139    pub fn label(&self) -> &str {
140        match self {
141            Self::Interface => "interface",
142            Self::Enumeration => "enumeration",
143            Self::Abstract => "abstract",
144            Self::Other(s) => s,
145        }
146    }
147}
148
149/// One class in a [`ClassDiagram`].
150///
151/// Classes are identified by name. They may have zero or more members and an
152/// optional stereotype. Classes can be forward-declared (mentioned only in a
153/// relationship) with an empty `members` list.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct Class {
156    /// Unique class name (case-sensitive).
157    pub name: String,
158    /// Optional UML stereotype (`<<interface>>`, `<<enumeration>>`, etc.).
159    pub stereotype: Option<Stereotype>,
160    /// Members declared in the `{ … }` body, in source order.
161    pub members: Vec<Member>,
162}
163
164impl Class {
165    /// Construct a bare class with no members or stereotype.
166    ///
167    /// Used by the parser when a class is first mentioned in a relationship
168    /// before any `{ … }` declaration has been seen.
169    pub fn bare(name: impl Into<String>) -> Self {
170        Self {
171            name: name.into(),
172            stereotype: None,
173            members: Vec::new(),
174        }
175    }
176}
177
178/// The kind of relationship between two classes.
179///
180/// Mermaid's class diagram supports seven relationship arrows:
181///
182/// ```text
183/// <|--   Inheritance  (solid line, hollow triangle at parent)
184/// --|>   Inheritance  (solid line, hollow triangle at child — reversed)
185/// *--    Composition  (solid line, filled diamond at owner)
186/// o--    Aggregation  (solid line, hollow diamond at owner)
187/// -->    Association  (solid directed arrow)
188/// --     Association  (plain, no arrow)
189/// <|..   Realization  (dashed line, hollow triangle at interface)
190/// ..|>   Realization  (dashed line, hollow triangle — reversed)
191/// <..    Dependency   (dashed arrow, reversed)
192/// ..>    Dependency   (dashed arrow)
193/// ```
194///
195/// The `from`/`to` fields of [`Relation`] define which class the arrow
196/// originates from. The displayed endpoint glyph is at the `to` end unless
197/// `rel_kind` is `Inheritance` or `Realization` (where the triangle points
198/// at the parent/interface, i.e. the `to` end).
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum RelKind {
201    /// Inheritance — solid line with hollow triangle at parent (`--|>` / `<|--`).
202    Inheritance,
203    /// Composition — solid line with filled diamond at owner (`*--` / `--*`).
204    Composition,
205    /// Aggregation — solid line with hollow diamond at owner (`o--` / `--o`).
206    Aggregation,
207    /// Directed association — solid arrow (`-->`).
208    AssociationDirected,
209    /// Plain association — plain line with no arrow endpoints (`--`).
210    AssociationPlain,
211    /// Realization — dashed line with hollow triangle (`..|>` / `<|..`).
212    Realization,
213    /// Dependency — dashed arrow (`..>`).
214    Dependency,
215}
216
217impl RelKind {
218    /// Returns `true` if this relationship uses a dashed (non-identifying) line.
219    pub fn is_dashed(self) -> bool {
220        matches!(self, Self::Realization | Self::Dependency)
221    }
222}
223
224/// One directed relationship between two classes.
225///
226/// `from` and `to` are class names. The arrow's visual meaning depends on
227/// [`RelKind`]:
228/// - For [`RelKind::Inheritance`] and [`RelKind::Realization`] the hollow
229///   triangle points at `to` (the parent / interface).
230/// - For [`RelKind::Composition`] and [`RelKind::Aggregation`] the diamond is
231///   at the `from` end (the owner).
232/// - For [`RelKind::AssociationDirected`] and [`RelKind::Dependency`] the
233///   arrowhead points at `to`.
234/// - For [`RelKind::AssociationPlain`] there are no endpoint glyphs.
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct Relation {
237    /// Source class name.
238    pub from: String,
239    /// Target class name.
240    pub to: String,
241    /// Kind of relationship (determines the endpoint glyph style).
242    pub kind: RelKind,
243    /// Optional multiplicity at the `from` end (e.g. `"1"`, `"0..*"`).
244    pub from_multiplicity: Option<String>,
245    /// Optional multiplicity at the `to` end.
246    pub to_multiplicity: Option<String>,
247    /// Optional label text placed along the line.
248    pub label: Option<String>,
249}
250
251/// A fully-parsed `classDiagram`.
252///
253/// Constructed by [`crate::parser::class::parse`] and consumed by
254/// [`crate::render::class::render`]. Classes are listed in declaration order
255/// (forward references from relationships are inserted when first encountered);
256/// relations in source order.
257#[derive(Debug, Clone, PartialEq, Eq, Default)]
258pub struct ClassDiagram {
259    /// All classes, in declaration/first-mention order.
260    pub classes: Vec<Class>,
261    /// All relationships, in source order.
262    pub relations: Vec<Relation>,
263}
264
265impl ClassDiagram {
266    /// Find a class by name. Returns its index in `classes`, or `None` if not
267    /// found.
268    pub fn class_index(&self, name: &str) -> Option<usize> {
269        self.classes.iter().position(|c| c.name == name)
270    }
271
272    /// Insert a bare class if its name isn't already present, returning its
273    /// index either way. Used by the parser when a relationship mentions a class
274    /// before its `{ … }` body is declared.
275    pub fn ensure_class(&mut self, name: &str) -> usize {
276        if let Some(idx) = self.class_index(name) {
277            return idx;
278        }
279        self.classes.push(Class::bare(name));
280        self.classes.len() - 1
281    }
282}
283
284// ---------------------------------------------------------------------------
285// Tests
286// ---------------------------------------------------------------------------
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn visibility_as_char_round_trips() {
294        assert_eq!(Visibility::Public.as_char(), '+');
295        assert_eq!(Visibility::Private.as_char(), '-');
296        assert_eq!(Visibility::Protected.as_char(), '#');
297        assert_eq!(Visibility::Package.as_char(), '~');
298    }
299
300    #[test]
301    fn class_bare_starts_empty() {
302        let c = Class::bare("Animal");
303        assert_eq!(c.name, "Animal");
304        assert!(c.members.is_empty());
305        assert!(c.stereotype.is_none());
306    }
307
308    #[test]
309    fn ensure_class_inserts_then_reuses() {
310        let mut diag = ClassDiagram::default();
311        let a0 = diag.ensure_class("A");
312        let a1 = diag.ensure_class("A");
313        let b0 = diag.ensure_class("B");
314        assert_eq!(a0, 0);
315        assert_eq!(a1, 0);
316        assert_eq!(b0, 1);
317        assert_eq!(diag.classes.len(), 2);
318    }
319
320    #[test]
321    fn class_index_returns_none_for_unknown() {
322        let diag = ClassDiagram::default();
323        assert_eq!(diag.class_index("X"), None);
324    }
325
326    #[test]
327    fn rel_kind_is_dashed_for_realization_and_dependency() {
328        assert!(RelKind::Realization.is_dashed());
329        assert!(RelKind::Dependency.is_dashed());
330        assert!(!RelKind::Inheritance.is_dashed());
331        assert!(!RelKind::Composition.is_dashed());
332        assert!(!RelKind::Aggregation.is_dashed());
333        assert!(!RelKind::AssociationDirected.is_dashed());
334        assert!(!RelKind::AssociationPlain.is_dashed());
335    }
336
337    #[test]
338    fn stereotype_label_returns_canonical_strings() {
339        assert_eq!(Stereotype::Interface.label(), "interface");
340        assert_eq!(Stereotype::Enumeration.label(), "enumeration");
341        assert_eq!(Stereotype::Abstract.label(), "abstract");
342        assert_eq!(Stereotype::Other("service".to_string()).label(), "service");
343    }
344
345    #[test]
346    fn member_name_delegates_correctly() {
347        let attr = Member::Attribute(Attribute {
348            visibility: None,
349            name: "count".to_string(),
350            type_name: "int".to_string(),
351            is_static: false,
352        });
353        let method = Member::Method(Method {
354            visibility: None,
355            name: "run".to_string(),
356            params: String::new(),
357            return_type: None,
358            is_static: false,
359            is_abstract: false,
360        });
361        assert_eq!(attr.name(), "count");
362        assert_eq!(method.name(), "run");
363    }
364}