aontu

Back to the Rails application overview

Generate the entity relationship diagram

Generate Mermaid text from entity definitions, understand cardinality rules, and check diagram drift.

Rendered from test/system/rb-solar/doc/erd.md in the engine repository. The model, the generators, the committed application and the check.sh that renders and validates all of it are in test/system/rb-solar/.

The Rails example generates doc/erd.mmd from model.aon using the handwritten template gen/doc/erd.mmd. The diagram and the Rails models therefore read the same entity definitions. Follow the model guide to set up the CLI.

Build the Mermaid source

Run from test/system/rb-solar:

aontu template --marker '%%-' gen/doc/erd.mmd > work/erd.aon
aontu model get out work/erd.aon

The result is the component tree carrying the Mermaid source. aontu render reads the template directly and writes the tree to the committed diagram location; with --check it holds that location to the tree instead:

aontu render --marker '%%-' gen/doc/erd.mmd doc
aontu render --check --marker '%%-' gen/doc/erd.mmd doc

The output directory and the template’s file("erd.mmd", …) determine the resulting filename. The Generated by comment is a literal line in the template, emitted along with the diagram.

Read the marker lines

The diagram template starts by importing the model:

%%- @"../model.aon"

--marker '%%-' tells aontu which lines contain instructions. Mermaid uses %% for comments, so it can ignore those instructions when reading the template. The other lines are target text with replacement names.

As in the Ruby templates, file(...) names the output file and its children supply the lines. The first child writes the comment and the erDiagram header.

Generate the relationship

The relationship rule selects entities that declare a parent:

%%-   emit(filter($.entity, { parent:string }), {
%%-     match: parent: string
%%-     replace: { OWNER:upper(.parent_class) OWNED:upper(.class) HAS:.parent_has }
%%-     body: [
  OWNER ||--o{ OWNED : "HAS"
%%-     ]
%%-   })

filter keeps entities whose parent is a string. For Moon, the rule reads parent_class: "Planet", class: "Moon", and parent_has: "moons". upper supplies the uppercase entity identifiers. The resulting line is:

  PLANET ||--o{ MOON : "moons"

The template fixes the cardinality as ||--o{: each moon has exactly one planet, and a planet has zero or more moons. The model supplies the names and the relationship’s existence. It does not currently supply arbitrary cardinality values. Supporting a different association would require a model property and a corresponding generator rule.

Generate entities and fields

The next declaration visits $.sequence, so Planet appears before Moon. Within each entity, this expression prepares the fields for the rules:

emit(each(.field, _ & { mark:"" }), [

This is the opening expression from the template; the rule list follows it. .field is the current entity’s field map. each creates a list with one item per field. Inside its template, _ is the current field, and & { mark:"" } combines that field with an empty mark property through unification.

For example, an ID field retains its name, kind, and pk: true, and gains mark: "". The outer emit then selects a rule for that prepared field. Its first matching rule supplies the output line:

MatchOutputExample
pk: trueType, column name, and PKstring id PK
fk: trueType, column name, and FKstring planet_id FK
name: stringType and column namefloat diameter

The mark property is unused by these rules. It does not supply PK or FK; those labels are literal text in the selected rule bodies. For the current model and rules, emit(.field, [...]) produces the same bytes. The wrapper is unnecessary here. Both forms visit this field map in sorted-key order; each does not recover the order of written keys.

Each field explicitly declares both key flags, including false, so ordinary fields reach the final rule. The type comes from kind; the column name comes from name.

The generated Mermaid file draws this diagram:

moonsPLANETfloat diameterstring forbid_reasonstring forbid_statestring id PKstring kindstring namestring terraform_stateMOONfloat diameterstring id PKstring kindstring namestring planet_id FK
Planet and Moon fields, with string primary keys and the parent relationship
Mermaid source
%% Generated by aontu from model.aon. Do not edit.

erDiagram
  PLANET ||--o{ MOON : "moons"
  PLANET {
    float diameter
    string forbid_reason
    string forbid_state
    string id PK
    string kind
    string name
    string terraform_state
  }
  MOON {
    float diameter
    string id PK
    string kind
    string name
    string planet_id FK
  }

The diagram describes modelled fields and associations. It is not a live inspection of Active Record or SQLite. For example, the migration template adds Rails timestamps that this ERD does not list. The FK marker identifies the model’s foreign-key field; the migration creates an index on it, without adding a database foreign-key constraint.

Draw the picture and detect drift

The website imports the generated Mermaid text and converts it to SVG at build time. Mermaid source remains available below the picture. aontu generates the diagram’s text; the website renderer draws it.

The example’s check.sh runs the same comparison shown above. A model or template change that leaves doc/erd.mmd stale fails that check. The model-tree figures use a separate command:

aontu view doc --depth 2 --as svg model.aon

view draws the model directly. The application architecture and layer diagrams are maintained documentation of the Rails source; they are not outputs of the ERD template.

Try adding a field and reviewing all generated changes to see how the application and diagram stay aligned.