rails-mermaid_erd 0.6.0 → 0.7.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 +4 -4
- data/README.md +20 -1
- data/lib/rails-mermaid_erd/builder.rb +55 -3
- data/lib/rails-mermaid_erd/configuration.rb +31 -5
- data/lib/rails-mermaid_erd/railtie.rb +12 -0
- data/lib/rails-mermaid_erd/version.rb +1 -1
- data/lib/rails-mermaid_erd.rb +22 -24
- data/lib/tasks/mermaid_erd.rake +37 -0
- data/lib/templates/index.html.erb +591 -148
- data/lib/templates/vendor/CHECKSUMS.txt +5 -0
- data/lib/templates/vendor/LICENSES.md +103 -0
- data/lib/templates/vendor/README.md +42 -0
- data/lib/templates/vendor/mermaid.min.js +3405 -0
- data/lib/templates/vendor/tailwindcss.js +65 -0
- data/lib/templates/vendor/vue.global.prod.min.js +3 -0
- metadata +11 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02a09db4c8fe0efe3647b171cd713eb46664a961c36eb820691b069f1dec82b2
|
|
4
|
+
data.tar.gz: 0bc7bec06228e7a2e03055b0b82a9ff63031ef2e6ce008e0e3c7edbf6044128b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5228d44560de7ae4bc244b7854a926144eee9ef1b1d16c77ddd71b999e1b260ccc3ae6de468968de1a208b2257d31121445366c72e0e9df932a9c61a751019b0
|
|
7
|
+
data.tar.gz: 856f64ddfa060ce33f859fe914c4be5ce93bd9898e3c75ec5ab2fa8fe2657ab2063b3bd56129e869af52723844c9e4b22e36085842813b5ceb672fd6e44ed180
|
data/README.md
CHANGED
|
@@ -58,11 +58,29 @@ 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
|
+
## Supported versions
|
|
68
|
+
|
|
69
|
+
The Ruby × Rails combinations exercised by CI on every push and pull request:
|
|
70
|
+
|
|
71
|
+
| Rails | Ruby |
|
|
72
|
+
| ----- | -------------------------------- |
|
|
73
|
+
| 5.2 | 2.7 |
|
|
74
|
+
| 6.0 | 2.7, 3.0 |
|
|
75
|
+
| 6.1 | 2.7, 3.0, 3.1, 3.2 |
|
|
76
|
+
| 7.0 | 2.7, 3.0, 3.1, 3.2, 3.3 |
|
|
77
|
+
| 7.1 | 3.1, 3.2, 3.3, 3.4 |
|
|
78
|
+
| 7.2 | 3.1, 3.2, 3.3, 3.4 |
|
|
79
|
+
| 8.0 | 3.2, 3.3, 3.4, 4.0 |
|
|
80
|
+
| 8.1 | 3.2, 3.3, 3.4, 4.0 |
|
|
81
|
+
|
|
82
|
+
Other combinations may work but are not verified.
|
|
83
|
+
|
|
66
84
|
## Configuration
|
|
67
85
|
|
|
68
86
|
`./config/mermaid_erd.yml` to customize the configuration.
|
|
@@ -73,6 +91,7 @@ The setting items are as follows.
|
|
|
73
91
|
| key | description | default |
|
|
74
92
|
| --- | --- | --- |
|
|
75
93
|
| `result_path` | Destination of generated files. | `mermaid_erd/index.html` |
|
|
94
|
+
| `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
95
|
|
|
77
96
|
<!--
|
|
78
97
|
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
|
-
|
|
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
|
-
|
|
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,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
|
data/lib/rails-mermaid_erd.rb
CHANGED
|
@@ -1,34 +1,32 @@
|
|
|
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
|
-
|
|
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
11
|
|
|
12
12
|
class << self
|
|
13
13
|
def configuration
|
|
14
|
-
@configuration ||=
|
|
14
|
+
@configuration ||= Configuration.new
|
|
15
15
|
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
16
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
# File.read with a domain-specific error when the bundled asset is missing,
|
|
18
|
+
# which usually means a broken gem install (e.g. lib/templates/vendor/ got
|
|
19
|
+
# pruned). The default Errno::ENOENT just points at this file and is hard
|
|
20
|
+
# to act on.
|
|
21
|
+
def read_gem_asset(relative_path)
|
|
22
|
+
path = File.expand_path(relative_path, __dir__)
|
|
23
|
+
File.read(path)
|
|
24
|
+
rescue Errno::ENOENT
|
|
25
|
+
raise "rails-mermaid_erd: bundled asset missing at #{path}. " \
|
|
26
|
+
"The gem appears to be incompletely installed; " \
|
|
27
|
+
"try `gem pristine rails-mermaid_erd` or reinstall the gem."
|
|
28
|
+
end
|
|
33
29
|
end
|
|
34
30
|
end
|
|
31
|
+
|
|
32
|
+
require_relative "rails-mermaid_erd/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
|
|
10
|
+
desc "Generate Mermaid ERD."
|
|
11
|
+
task mermaid_erd: :environment do
|
|
12
|
+
result = RailsMermaidErd::Builder.model_data
|
|
13
|
+
|
|
14
|
+
version = RailsMermaidErd::VERSION
|
|
15
|
+
app_name = ::Rails.application.class.try(:parent_name) || ::Rails.application.class.try(:module_parent_name)
|
|
16
|
+
logo = RailsMermaidErd.read_gem_asset("./assets/logo.svg")
|
|
17
|
+
tailwindcss_js = RailsMermaidErd.read_gem_asset("./templates/vendor/tailwindcss.js")
|
|
18
|
+
mermaid_js = RailsMermaidErd.read_gem_asset("./templates/vendor/mermaid.min.js")
|
|
19
|
+
vue_js = RailsMermaidErd.read_gem_asset("./templates/vendor/vue.global.prod.min.js")
|
|
20
|
+
erb = ERB.new(RailsMermaidErd.read_gem_asset("./templates/index.html.erb"))
|
|
21
|
+
result_html = erb.result(binding)
|
|
22
|
+
|
|
23
|
+
result_file = ::Rails.root.join(RailsMermaidErd.configuration.result_path)
|
|
24
|
+
result_dir = result_file.dirname
|
|
25
|
+
|
|
26
|
+
# Re-raise filesystem failures with an actionable hint pointing at the
|
|
27
|
+
# `result_path` config key, mirroring the friendly error in
|
|
28
|
+
# `RailsMermaidErd.read_gem_asset`. Without this the user just sees a raw
|
|
29
|
+
# `Errno::EACCES` / `Errno::ENOSPC` and has to guess which knob controls it.
|
|
30
|
+
begin
|
|
31
|
+
FileUtils.mkdir_p(result_dir)
|
|
32
|
+
File.write(result_file, result_html)
|
|
33
|
+
rescue SystemCallError => e
|
|
34
|
+
raise "rails-mermaid_erd: could not write ERD to #{result_file} (#{e.class}: #{e.message}). " \
|
|
35
|
+
"Check the `result_path` key in config/mermaid_erd.yml and that the directory is writable."
|
|
36
|
+
end
|
|
37
|
+
end
|