rails-mermaid_erd 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f3cfe23f8fa1343b2bd8a3ac5d47c1967613ce7a832d65de858b363402db0b1
4
- data.tar.gz: 146d83e00452b8c56cbf8c4c6e0aebd62f99b3224e6b7e3062b4889b55bf0723
3
+ metadata.gz: 5a6ae2943632ef191d3758420e84a0fc2e43f17a677359e21328bc67b83bd172
4
+ data.tar.gz: d1ecd0768360f3bdbca63e1569ac26f708af009bfc3b18d09c8001ff474ea77a
5
5
  SHA512:
6
- metadata.gz: 2c68987537096806829a9709f477dd8cc8a0108291387ff670796d2c41c4a900fe0f1a55902381b12d582d4e66d77f2b0421d929dfc72060a10d5b9ed020c4a6
7
- data.tar.gz: 04cbecd36399c810f0846e1d4c4275922acfeb1ec2bcf426440f08902db6b4b051368f57ec57a0f8259108d9f727b77d0760ee0f817aee8122e4f3952dcfcc58
6
+ metadata.gz: 4ef1270960ec9ce3f1a91d7c8c9d6182f0ef489f11e10be3653f905e72dbf344d39cee5da22ac35cc2f799836e49da799f0db2df82f2e9c3b59f120b6592fcdd
7
+ data.tar.gz: f3a1d2f73421f4f2cbec8c7d394ca55ced02835df12e2fb3bfb24ec6c97685609cdd30137be1b9943d853b80ff86d6debdd996aa19066dd173009635c0615464
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [English](./README.md) | [日本語](./README.ja.md)
1
+ [English](./README.md) | [日本語](./README.ja.md) | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) | [한국어](./README.ko.md) | [Español](./README.es.md) | [Français](./README.fr.md) | [Deutsch](./README.de.md) | [Italiano](./README.it.md) | [Português (Brasil)](./README.pt-BR.md) | [Русский](./README.ru.md) | [العربية](./README.ar.md)
2
2
 
3
3
  # Rails Mermaid ERD
4
4
 
@@ -58,11 +58,45 @@ This file is not required for Git management, so you can add it to `.gitignore`
58
58
  mermaid_erd
59
59
  ```
60
60
 
61
- `<app_root>/mermaid_erd/index.html` is a single HTML file.
61
+ `<app_root>/mermaid_erd/index.html` is a single self-contained HTML file. All front-end dependencies (Tailwind, Mermaid, Vue) are inlined, so it works offline and behind strict corporate proxies — no CDN access is needed at view time. The file is roughly 4 MB because the bundles ship inside it.
62
+
62
63
  If you share this file, it can be used by those who do not have a Ruby on Rails environment. Or, you can upload the file to a web server and share it with the same URL.
63
64
 
64
65
  It would be very smart to generate it automatically using CI.
65
66
 
67
+ ### Print the Mermaid source to stdout
68
+
69
+ Run rake task `mermaid_erd:print` to print the raw `erDiagram` source to stdout instead of writing the HTML viewer. This pipes cleanly into other tools:
70
+
71
+ ```bash
72
+ $ bundle exec rails mermaid_erd:print
73
+ $ bundle exec rails mermaid_erd:print > er.mmd
74
+ $ bundle exec rails mermaid_erd:print | mmdc -i - -o er.svg
75
+ ```
76
+
77
+ The output is the full diagram — every table, column, key, comment, and relation — equivalent to the HTML viewer with all of its detail toggles enabled.
78
+
79
+ ## Languages
80
+
81
+ The viewer UI ships in 12 languages: English, 日本語, 简体中文, 繁體中文, 한국어, Español, Français, Deutsch, Italiano, Português (Brasil), Русский, and العربية (right-to-left). It auto-detects the browser language (`navigator.language`) on load, falls back to English for unsupported locales, and can be switched manually from the selector in the top-right corner.
82
+
83
+ ## Supported versions
84
+
85
+ The Ruby × Rails combinations exercised by CI on every push and pull request:
86
+
87
+ | Rails | Ruby |
88
+ | ----- | -------------------------------- |
89
+ | 5.2 | 2.7 |
90
+ | 6.0 | 2.7, 3.0 |
91
+ | 6.1 | 2.7, 3.0, 3.1, 3.2 |
92
+ | 7.0 | 2.7, 3.0, 3.1, 3.2, 3.3 |
93
+ | 7.1 | 3.1, 3.2, 3.3, 3.4 |
94
+ | 7.2 | 3.1, 3.2, 3.3, 3.4 |
95
+ | 8.0 | 3.2, 3.3, 3.4, 4.0 |
96
+ | 8.1 | 3.2, 3.3, 3.4, 4.0 |
97
+
98
+ Other combinations may work but are not verified.
99
+
66
100
  ## Configuration
67
101
 
68
102
  `./config/mermaid_erd.yml` to customize the configuration.
@@ -73,6 +107,7 @@ The setting items are as follows.
73
107
  | key | description | default |
74
108
  | --- | --- | --- |
75
109
  | `result_path` | Destination of generated files. | `mermaid_erd/index.html` |
110
+ | `ignore_tables` | Array of regular-expression strings. Tables whose `table_name` matches any pattern are dropped from the generated ERD, along with any relations that point at them. Useful for excluding audit-log models, soft-deleted/legacy tables, or other large noise you don't want to render. Patterns are compiled with `Regexp.new`, so escape backslashes inside YAML strings (e.g. `"\\Aaudit_"`). | `[]` |
76
111
 
77
112
  <!--
78
113
  TODO:
@@ -1,5 +1,3 @@
1
- require "yaml"
2
-
3
1
  class RailsMermaidErd::Builder
4
2
  class << self
5
3
  def model_data
@@ -9,10 +7,28 @@ class RailsMermaidErd::Builder
9
7
  }
10
8
 
11
9
  ::Rails.application.eager_load!
10
+
11
+ # Compile each `ignore_tables` entry once. Wrap `RegexpError` so the
12
+ # rake-task output names the offending YAML entry rather than just the
13
+ # underlying parser message.
14
+ ignore_patterns = RailsMermaidErd.configuration.ignore_tables.map do |pattern|
15
+ Regexp.new(pattern)
16
+ rescue RegexpError => e
17
+ raise ArgumentError, "config/mermaid_erd.yml: invalid `ignore_tables` pattern #{pattern.inspect}: #{e.message}"
18
+ end
19
+
20
+ # Class names whose `table_name` matches an ignore pattern. Resolved
21
+ # *before* the main loop so we can also drop outgoing reflections that
22
+ # point at an ignored model — otherwise the diagram would render orphan
23
+ # nodes for the ignored tables. A Hash gives O(1) `key?` lookups without
24
+ # pulling in `set` (which is autoloaded on Ruby 3.2+ but not earlier).
25
+ ignored_model_names = compute_ignored_model_names(ignore_patterns)
26
+
12
27
  ::ActiveRecord::Base.descendants.sort_by(&:name).each do |defined_model|
13
28
  next unless defined_model.table_exists?
14
29
  next if defined_model.name.include?("HABTM_")
15
30
  next if defined_model.table_name.blank?
31
+ next if ignored_model_names.key?(defined_model.name)
16
32
 
17
33
  table_name = defined_model.table_name
18
34
  model = {
@@ -44,6 +60,7 @@ class RailsMermaidErd::Builder
44
60
 
45
61
  defined_model.reflect_on_all_associations(:has_many).each do |reflection|
46
62
  reflection_model_name = get_reflection_model_name(reflection)
63
+ next if ignored_model_names.key?(reflection_model_name)
47
64
 
48
65
  reverse_relation = result[:Relations].find { |r|
49
66
  if reflection.options[:through]
@@ -72,6 +89,7 @@ class RailsMermaidErd::Builder
72
89
 
73
90
  defined_model.reflect_on_all_associations(:has_and_belongs_to_many).each do |reflection|
74
91
  reflection_model_name = get_reflection_model_name(reflection)
92
+ next if ignored_model_names.key?(reflection_model_name)
75
93
 
76
94
  reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name }
77
95
  if reverse_relation
@@ -89,7 +107,15 @@ class RailsMermaidErd::Builder
89
107
  end
90
108
 
91
109
  defined_model.reflect_on_all_associations(:belongs_to).each do |reflection|
110
+ # Polymorphic `belongs_to` has no concrete target class — the target is
111
+ # decided at row level by the `*_type` column. Emitting an edge to the
112
+ # macro name (e.g. `"Imageable"`) would render an orphan node with no
113
+ # columns; the polymorphic parents express the relationship via their
114
+ # `has_many ..., as: :foo` reflections instead.
115
+ next if reflection.polymorphic?
116
+
92
117
  reflection_model_name = get_reflection_model_name(reflection)
118
+ next if ignored_model_names.key?(reflection_model_name)
93
119
 
94
120
  reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == reflection_model_name }
95
121
  if reverse_relation
@@ -116,6 +142,7 @@ class RailsMermaidErd::Builder
116
142
 
117
143
  defined_model.reflect_on_all_associations(:has_one).each do |reflection|
118
144
  reflection_model_name = get_reflection_model_name(reflection)
145
+ next if ignored_model_names.key?(reflection_model_name)
119
146
 
120
147
  reverse_relation = result[:Relations].find { |r|
121
148
  if reflection.options[:through]
@@ -147,13 +174,38 @@ class RailsMermaidErd::Builder
147
174
  result
148
175
  end
149
176
 
177
+ # Returns a Hash keyed by class names whose underlying table matches an
178
+ # entry from `ignore_tables`. Mirrors the guard rails on the main loop
179
+ # (`table_exists?`, HABTM scaffolding, blank `table_name`) so that adding
180
+ # an `ignore_tables` entry can't crash the rake task on hosts with
181
+ # abstract STI bases or descendants whose backing table isn't created yet.
182
+ def compute_ignored_model_names(ignore_patterns)
183
+ return {} if ignore_patterns.empty?
184
+
185
+ ::ActiveRecord::Base.descendants.each_with_object({}) do |defined_model, acc|
186
+ next unless defined_model.table_exists?
187
+ next if defined_model.name.include?("HABTM_")
188
+ table_name = defined_model.table_name
189
+ next if table_name.blank?
190
+ acc[defined_model.name] = true if ignore_patterns.any? { |pattern| pattern.match?(table_name) }
191
+ end
192
+ end
193
+
150
194
  # Doc: https://guides.rubyonrails.org/association_basics.html
151
195
  def get_reflection_model_name(reflection)
152
196
  if reflection.options[:class_name]
153
197
  reflection.options[:class_name].to_s.classify
154
198
  elsif reflection.options[:through]
155
- if reflection.options[:source]
199
+ # `:source_type` is the authoritative class hint for a polymorphic
200
+ # `:source`, so it takes precedence over `:source` when both are set.
201
+ if reflection.options[:source_type]
202
+ reflection.options[:source_type].to_s.classify
203
+ elsif reflection.options[:source]
156
204
  reflection.options[:source].to_s.classify
205
+ elsif reflection.source_reflection.nil?
206
+ # `:through` targets a polymorphic `belongs_to` without `:source_type`;
207
+ # Rails can't resolve a single class. Fall back to its `:source` default.
208
+ reflection.name.to_s.classify
157
209
  else
158
210
  reflection.class_name
159
211
  end
@@ -1,19 +1,45 @@
1
1
  require "yaml"
2
2
 
3
3
  class RailsMermaidErd::Configuration
4
- attr_accessor :result_path
4
+ attr_accessor :result_path, :ignore_tables
5
+
6
+ DEFAULTS = {
7
+ result_path: "mermaid_erd/index.html",
8
+ ignore_tables: []
9
+ }.freeze
5
10
 
6
11
  def initialize
7
- config = {
8
- result_path: "mermaid_erd/index.html"
9
- }
12
+ config = DEFAULTS.dup
10
13
 
11
14
  config_file = Rails.root.join("config/mermaid_erd.yml")
12
15
  if File.exist?(config_file)
13
- custom_config = YAML.load(config_file.read).symbolize_keys
16
+ # `safe_load` over `load` because this file is checked into the host
17
+ # repo and may eventually be templated from CI inputs. Disallow custom
18
+ # classes and aliases — the only legitimate value shapes here are
19
+ # strings and arrays of strings.
20
+ custom_config = YAML.safe_load(config_file.read, permitted_classes: [], aliases: false).symbolize_keys
14
21
  config = config.merge(custom_config)
15
22
  end
16
23
 
17
24
  @result_path = config[:result_path]
25
+ @ignore_tables = normalize_ignore_tables(config[:ignore_tables])
26
+ end
27
+
28
+ private
29
+
30
+ # Accept `nil` as "no patterns", reject anything that isn't an Array of
31
+ # non-empty Strings. We validate eagerly so a malformed config surfaces a
32
+ # clear ArgumentError at boot rather than a confusing `NoMethodError` deep
33
+ # in `Builder.model_data`. The empty-string rejection in particular blocks
34
+ # the silent footgun of `Regexp.new("")` matching every table.
35
+ def normalize_ignore_tables(value)
36
+ return [] if value.nil?
37
+
38
+ unless value.is_a?(Array) && value.all? { |entry| entry.is_a?(String) && !entry.empty? }
39
+ raise ArgumentError,
40
+ "config/mermaid_erd.yml: `ignore_tables` must be an array of non-empty regex strings, got #{value.inspect}"
41
+ end
42
+
43
+ value
18
44
  end
19
45
  end
@@ -0,0 +1,74 @@
1
+ class RailsMermaidErd::MermaidText
2
+ # Renders the schema dump from RailsMermaidErd::Builder.model_data into
3
+ # Mermaid `erDiagram` source text.
4
+ #
5
+ # This is the *maximal* projection of the bundled front-end viewer
6
+ # (lib/templates/index.html.erb): the dump always shows every table, every
7
+ # column with its key marker and comment, and every relation label — i.e.
8
+ # the diagram the viewer produces with all of its detail toggles ("Show
9
+ # key", "Show comment", "Show relation comment") enabled and with no
10
+ # model-selection filtering. The viewer defaults those toggles off, so the
11
+ # dump is deliberately more detailed than the viewer's initial view. The only
12
+ # thing it drops is the viewer's "Restore Hash" comment line, which encodes
13
+ # browser-only UI state and is meaningless on the command line. Apart from
14
+ # that line and a single trailing newline (added by the rake task's `puts`),
15
+ # it is byte-identical to the viewer's "all toggles on" output.
16
+ #
17
+ # Comment values come from arbitrary DB metadata, so they are sanitised the
18
+ # same way the viewer's renderer must (and now does): newlines collapse to a
19
+ # space (a newline would split a label or terminate a `%%` line mid-diagram)
20
+ # and a literal `"` becomes Mermaid's `#quot;` entity (an unescaped quote
21
+ # closes the quoted label early). Keep this byte-aligned with the viewer at
22
+ # index.html.erb:809-845.
23
+ HEADER = [
24
+ "erDiagram",
25
+ " %% --------------------------------------------------------",
26
+ ' %% Generated by "Rails Mermaid ERD"',
27
+ " %% https://github.com/koedame/rails-mermaid_erd",
28
+ " %% --------------------------------------------------------",
29
+ ""
30
+ ].freeze
31
+
32
+ class << self
33
+ # `result` is the hash returned by RailsMermaidErd::Builder.model_data.
34
+ def build(result)
35
+ lines = HEADER.dup
36
+
37
+ result[:Models].each do |model|
38
+ lines << " %% table name: #{one_line(model[:TableName])}"
39
+ lines << " %% table comment: #{one_line(model[:TableComment])}"
40
+ # Mermaid entity names can't contain ":", so namespaced models like
41
+ # `Admin::User` are written as `Admin-User` (matches the front-end).
42
+ lines << " #{model[:ModelName].tr(":", "-")} {"
43
+ model[:Columns].each do |column|
44
+ lines << " #{column[:type]} #{column[:name]} #{column[:key]} #{quoted(column[:comment])}"
45
+ end
46
+ lines << " }"
47
+ lines << ""
48
+ end
49
+
50
+ result[:Relations].each do |relation|
51
+ left = relation[:LeftModelName].tr(":", "-")
52
+ right = relation[:RightModelName].tr(":", "-")
53
+ lines << " #{left} #{relation[:LeftValue]}#{relation[:Line]}#{relation[:RightValue]} #{right} : #{quoted(relation[:Comment])}"
54
+ end
55
+
56
+ lines.join("\n")
57
+ end
58
+
59
+ private
60
+
61
+ # Collapse newlines so a value can't split a label or terminate a `%%`
62
+ # comment line mid-diagram.
63
+ def one_line(value)
64
+ value.to_s.gsub(/[\r\n]+/, " ")
65
+ end
66
+
67
+ # A Mermaid quoted label: newline-free, with `"` escaped to the `#quot;`
68
+ # entity so a comment can't close the string early.
69
+ def quoted(value)
70
+ escaped = one_line(value).gsub('"', "#quot;")
71
+ "\"#{escaped}\""
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,12 @@
1
+ module RailsMermaidErd
2
+ # Registers the `mermaid_erd` rake task only when Rails is actually loading
3
+ # tasks (i.e. under `rake`/`rails` CLIs, never on web/console/job boots).
4
+ # Pairs with the `autoload` declarations in lib/rails-mermaid_erd.rb so the
5
+ # heavy `Builder` / `Configuration` constants — and the `erb`/`fileutils`
6
+ # requires their task body needs — stay off the boot path entirely.
7
+ class Railtie < ::Rails::Railtie
8
+ rake_tasks do
9
+ load File.expand_path("../tasks/mermaid_erd.rake", __dir__)
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsMermaidErd
2
- VERSION = "0.6.0"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -1,34 +1,33 @@
1
- require "erb"
2
- require "fileutils"
3
- require "rake"
4
- require "rake/dsl_definition"
5
1
  require_relative "rails-mermaid_erd/version"
6
- require_relative "rails-mermaid_erd/configuration"
7
- require_relative "rails-mermaid_erd/builder"
8
2
 
9
3
  module RailsMermaidErd
10
- extend Rake::DSL
4
+ # Resolve `Builder` and `Configuration` lazily so requiring this file from
5
+ # Bundler's auto-require on every Rails boot (web, console, jobs) only pays
6
+ # for the Railtie declaration below. The actual classes — plus their
7
+ # transitive `yaml` require — load on first reference, which in practice
8
+ # means "when the rake task runs."
9
+ autoload :Builder, "rails-mermaid_erd/builder"
10
+ autoload :Configuration, "rails-mermaid_erd/configuration"
11
+ autoload :MermaidText, "rails-mermaid_erd/mermaid_text"
11
12
 
12
13
  class << self
13
14
  def configuration
14
- @configuration ||= RailsMermaidErd::Configuration.new
15
+ @configuration ||= Configuration.new
15
16
  end
16
- end
17
-
18
- desc "Generate Mermaid ERD."
19
- task mermaid_erd: :environment do
20
- result = RailsMermaidErd::Builder.model_data
21
-
22
- version = VERSION
23
- app_name = ::Rails.application.class.try(:parent_name) || ::Rails.application.class.try(:module_parent_name)
24
- logo = File.read(File.expand_path("./assets/logo.svg", __dir__))
25
- erb = ERB.new(File.read(File.expand_path("./templates/index.html.erb", __dir__)))
26
- result_html = erb.result(binding)
27
17
 
28
- result_dir = Rails.root.join(File.dirname(RailsMermaidErd.configuration.result_path))
29
- FileUtils.mkdir_p(result_dir)
30
-
31
- result_file = Rails.root.join(RailsMermaidErd.configuration.result_path)
32
- File.write(result_file, result_html)
18
+ # File.read with a domain-specific error when the bundled asset is missing,
19
+ # which usually means a broken gem install (e.g. lib/templates/vendor/ got
20
+ # pruned). The default Errno::ENOENT just points at this file and is hard
21
+ # to act on.
22
+ def read_gem_asset(relative_path)
23
+ path = File.expand_path(relative_path, __dir__)
24
+ File.read(path)
25
+ rescue Errno::ENOENT
26
+ raise "rails-mermaid_erd: bundled asset missing at #{path}. " \
27
+ "The gem appears to be incompletely installed; " \
28
+ "try `gem pristine rails-mermaid_erd` or reinstall the gem."
29
+ end
33
30
  end
34
31
  end
32
+
33
+ require_relative "rails-mermaid_erd/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,61 @@
1
+ require "erb"
2
+ require "fileutils"
3
+ # Explicit `require_relative` rather than leaning on the module's `autoload`:
4
+ # if the gem install is broken (e.g. lib/rails-mermaid_erd/builder.rb pruned),
5
+ # we want a LoadError with the missing path here — not a NameError raised
6
+ # deep inside the task body where the cause is harder to diagnose.
7
+ require_relative "../rails-mermaid_erd/builder"
8
+ require_relative "../rails-mermaid_erd/configuration"
9
+ require_relative "../rails-mermaid_erd/mermaid_text"
10
+
11
+ desc "Generate Mermaid ERD."
12
+ task mermaid_erd: :environment do
13
+ result = RailsMermaidErd::Builder.model_data
14
+
15
+ version = RailsMermaidErd::VERSION
16
+ app_name = ::Rails.application.class.try(:parent_name) || ::Rails.application.class.try(:module_parent_name)
17
+ logo = RailsMermaidErd.read_gem_asset("./assets/logo.svg")
18
+ tailwindcss_js = RailsMermaidErd.read_gem_asset("./templates/vendor/tailwindcss.js")
19
+ mermaid_js = RailsMermaidErd.read_gem_asset("./templates/vendor/mermaid.min.js")
20
+ vue_js = RailsMermaidErd.read_gem_asset("./templates/vendor/vue.global.prod.min.js")
21
+ erb = ERB.new(RailsMermaidErd.read_gem_asset("./templates/index.html.erb"))
22
+ result_html = erb.result(binding)
23
+
24
+ result_file = ::Rails.root.join(RailsMermaidErd.configuration.result_path)
25
+ result_dir = result_file.dirname
26
+
27
+ # Re-raise filesystem failures with an actionable hint pointing at the
28
+ # `result_path` config key, mirroring the friendly error in
29
+ # `RailsMermaidErd.read_gem_asset`. Without this the user just sees a raw
30
+ # `Errno::EACCES` / `Errno::ENOSPC` and has to guess which knob controls it.
31
+ begin
32
+ FileUtils.mkdir_p(result_dir)
33
+ File.write(result_file, result_html)
34
+ rescue SystemCallError => e
35
+ raise "rails-mermaid_erd: could not write ERD to #{result_file} (#{e.class}: #{e.message}). " \
36
+ "Check the `result_path` key in config/mermaid_erd.yml and that the directory is writable."
37
+ end
38
+ end
39
+
40
+ namespace :mermaid_erd do
41
+ desc "Print Mermaid ERD source to stdout."
42
+ task print: :environment do
43
+ # Builder calls `ActiveRecord::Schema.foreign_keys`, whose migration-style
44
+ # `-- foreign_keys(...)` / `-> 0.001s` logging would otherwise land on
45
+ # stdout and corrupt the piped diagram. Mute it just for the build, and
46
+ # restore it in `ensure` so an error mid-build can't leak the muted global
47
+ # into later tasks running in the same process.
48
+ was_verbose = ActiveRecord::Migration.verbose
49
+ ActiveRecord::Migration.verbose = false
50
+ result =
51
+ begin
52
+ RailsMermaidErd::Builder.model_data
53
+ ensure
54
+ ActiveRecord::Migration.verbose = was_verbose
55
+ end
56
+
57
+ # Stream the raw `erDiagram` text to stdout so it pipes into other tools
58
+ # (`> er.mmd`, `| mmdc -i - -o er.svg`) without writing the HTML viewer.
59
+ $stdout.puts RailsMermaidErd::MermaidText.build(result)
60
+ end
61
+ end