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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f3cfe23f8fa1343b2bd8a3ac5d47c1967613ce7a832d65de858b363402db0b1
4
- data.tar.gz: 146d83e00452b8c56cbf8c4c6e0aebd62f99b3224e6b7e3062b4889b55bf0723
3
+ metadata.gz: 02a09db4c8fe0efe3647b171cd713eb46664a961c36eb820691b069f1dec82b2
4
+ data.tar.gz: 0bc7bec06228e7a2e03055b0b82a9ff63031ef2e6ce008e0e3c7edbf6044128b
5
5
  SHA512:
6
- metadata.gz: 2c68987537096806829a9709f477dd8cc8a0108291387ff670796d2c41c4a900fe0f1a55902381b12d582d4e66d77f2b0421d929dfc72060a10d5b9ed020c4a6
7
- data.tar.gz: 04cbecd36399c810f0846e1d4c4275922acfeb1ec2bcf426440f08902db6b4b051368f57ec57a0f8259108d9f727b77d0760ee0f817aee8122e4f3952dcfcc58
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
- 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,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.7.0"
3
3
  end
@@ -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
- 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
11
 
12
12
  class << self
13
13
  def configuration
14
- @configuration ||= RailsMermaidErd::Configuration.new
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
- 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)
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