rails-schema 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/PROJECT.md +322 -0
- data/README.md +77 -0
- data/Rakefile +12 -0
- data/docs/index.html +1015 -0
- data/docs/screenshot.png +0 -0
- data/lib/rails/schema/assets/app.js +487 -0
- data/lib/rails/schema/assets/style.css +473 -0
- data/lib/rails/schema/assets/template.html.erb +53 -0
- data/lib/rails/schema/assets/vendor/d3.min.js +2 -0
- data/lib/rails/schema/configuration.rb +17 -0
- data/lib/rails/schema/extractor/association_reader.rb +44 -0
- data/lib/rails/schema/extractor/column_reader.rb +37 -0
- data/lib/rails/schema/extractor/model_scanner.rb +108 -0
- data/lib/rails/schema/extractor/schema_file_parser.rb +107 -0
- data/lib/rails/schema/railtie.rb +17 -0
- data/lib/rails/schema/renderer/html_generator.rb +71 -0
- data/lib/rails/schema/transformer/edge.rb +33 -0
- data/lib/rails/schema/transformer/graph_builder.rb +60 -0
- data/lib/rails/schema/transformer/node.rb +25 -0
- data/lib/rails/schema/version.rb +7 -0
- data/lib/rails/schema.rb +43 -0
- data/sig/rails/schema.rbs +6 -0
- metadata +96 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_accessor :output_path, :exclude_models, :title, :theme, :expand_columns
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@output_path = "docs/schema.html"
|
|
10
|
+
@exclude_models = []
|
|
11
|
+
@title = "Database Schema"
|
|
12
|
+
@theme = :auto
|
|
13
|
+
@expand_columns = false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
module Extractor
|
|
6
|
+
class AssociationReader
|
|
7
|
+
def read(model)
|
|
8
|
+
model.reflect_on_all_associations.filter_map do |ref|
|
|
9
|
+
next if skip_association?(ref)
|
|
10
|
+
|
|
11
|
+
build_association_data(model, ref)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def skip_association?(ref)
|
|
18
|
+
# Skip polymorphic belongs_to — no fixed target model
|
|
19
|
+
ref.macro == :belongs_to && ref.options[:polymorphic]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_association_data(model, ref)
|
|
23
|
+
{
|
|
24
|
+
from: model.name,
|
|
25
|
+
to: target_model_name(ref),
|
|
26
|
+
association_type: ref.macro.to_s,
|
|
27
|
+
label: ref.name.to_s,
|
|
28
|
+
foreign_key: ref.foreign_key.to_s,
|
|
29
|
+
through: ref.options[:through]&.to_s,
|
|
30
|
+
polymorphic: ref.options[:as] ? true : false
|
|
31
|
+
}
|
|
32
|
+
rescue StandardError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def target_model_name(ref)
|
|
37
|
+
ref.klass.name
|
|
38
|
+
rescue StandardError
|
|
39
|
+
ref.class_name
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
module Extractor
|
|
6
|
+
class ColumnReader
|
|
7
|
+
def initialize(schema_data: nil)
|
|
8
|
+
@schema_data = schema_data
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def read(model)
|
|
12
|
+
if @schema_data&.key?(model.table_name)
|
|
13
|
+
@schema_data[model.table_name]
|
|
14
|
+
else
|
|
15
|
+
read_from_model(model)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def read_from_model(model)
|
|
22
|
+
model.columns.map do |col|
|
|
23
|
+
{
|
|
24
|
+
name: col.name,
|
|
25
|
+
type: col.type.to_s,
|
|
26
|
+
nullable: col.null,
|
|
27
|
+
primary: col.name == model.primary_key,
|
|
28
|
+
default: col.default
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
rescue StandardError
|
|
32
|
+
[]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
module Extractor
|
|
6
|
+
class ModelScanner
|
|
7
|
+
def initialize(configuration: ::Rails::Schema.configuration, schema_data: nil)
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
@schema_data = schema_data
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def scan
|
|
13
|
+
eager_load_models!
|
|
14
|
+
|
|
15
|
+
all_descendants = ActiveRecord::Base.descendants
|
|
16
|
+
non_abstract = all_descendants.reject(&:abstract_class?)
|
|
17
|
+
named = non_abstract.reject { |m| m.name.nil? }
|
|
18
|
+
with_tables = named.select { |m| table_known?(m) }
|
|
19
|
+
included = with_tables.reject { |m| excluded?(m) }
|
|
20
|
+
log_empty_scan(all_descendants, non_abstract, named, with_tables) if included.empty?
|
|
21
|
+
|
|
22
|
+
included.sort_by(&:name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def eager_load_models!
|
|
28
|
+
return unless defined?(::Rails.application) && ::Rails.application
|
|
29
|
+
|
|
30
|
+
if defined?(::Rails.autoloaders) && ::Rails.autoloaders.respond_to?(:main)
|
|
31
|
+
eager_load_via_zeitwerk!
|
|
32
|
+
else
|
|
33
|
+
eager_load_via_application!
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def eager_load_via_zeitwerk!
|
|
38
|
+
loader = ::Rails.autoloaders.main
|
|
39
|
+
models_path = ::Rails.root&.join("app", "models")&.to_s
|
|
40
|
+
|
|
41
|
+
if models_path && File.directory?(models_path) && loader.respond_to?(:eager_load_dir)
|
|
42
|
+
loader.eager_load_dir(models_path)
|
|
43
|
+
else
|
|
44
|
+
loader.eager_load
|
|
45
|
+
end
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
warn "[rails-schema] Zeitwerk eager_load failed (#{e.class}: #{e.message}), " \
|
|
48
|
+
"trying Rails.application.eager_load!"
|
|
49
|
+
eager_load_via_application!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def eager_load_via_application!
|
|
53
|
+
::Rails.application.eager_load!
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
warn "[rails-schema] eager_load! failed (#{e.class}: #{e.message}), " \
|
|
56
|
+
"falling back to per-file model loading"
|
|
57
|
+
eager_load_model_files!
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def eager_load_model_files!
|
|
61
|
+
return unless defined?(::Rails.root) && ::Rails.root
|
|
62
|
+
|
|
63
|
+
models_path = ::Rails.root.join("app", "models")
|
|
64
|
+
return unless models_path.exist?
|
|
65
|
+
|
|
66
|
+
Dir.glob(models_path.join("**/*.rb")).each do |file|
|
|
67
|
+
require file
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
warn "[rails-schema] Could not load #{file}: #{e.class}: #{e.message}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def table_known?(model)
|
|
74
|
+
if @schema_data
|
|
75
|
+
@schema_data.key?(model.table_name)
|
|
76
|
+
else
|
|
77
|
+
safe_table_exists?(model)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def safe_table_exists?(model)
|
|
82
|
+
model.table_exists?
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
warn "[rails-schema] Could not check table for #{model.name}: #{e.class}: #{e.message}"
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def excluded?(model)
|
|
89
|
+
@configuration.exclude_models.any? do |pattern|
|
|
90
|
+
if pattern.end_with?("*")
|
|
91
|
+
model.name.start_with?(pattern.delete_suffix("*"))
|
|
92
|
+
else
|
|
93
|
+
model.name == pattern
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def log_empty_scan(all_descendants, non_abstract, named, with_tables)
|
|
99
|
+
return if all_descendants.empty?
|
|
100
|
+
|
|
101
|
+
warn "[rails-schema] No models found! Filtering: " \
|
|
102
|
+
"#{all_descendants.size} descendants → #{non_abstract.size} concrete → " \
|
|
103
|
+
"#{named.size} named → #{with_tables.size} with tables"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
module Extractor
|
|
6
|
+
class SchemaFileParser
|
|
7
|
+
def initialize(schema_path = nil)
|
|
8
|
+
@schema_path = schema_path
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def parse
|
|
12
|
+
path = resolve_path
|
|
13
|
+
return {} unless path && File.exist?(path)
|
|
14
|
+
|
|
15
|
+
parse_content(File.read(path))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse_content(content)
|
|
19
|
+
@tables = {}
|
|
20
|
+
@current_table = nil
|
|
21
|
+
@pk_type = nil
|
|
22
|
+
@has_pk = true
|
|
23
|
+
|
|
24
|
+
content.each_line { |line| process_line(line.strip) }
|
|
25
|
+
|
|
26
|
+
@tables
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def resolve_path
|
|
32
|
+
return @schema_path if @schema_path
|
|
33
|
+
|
|
34
|
+
if defined?(::Rails.root) && ::Rails.root
|
|
35
|
+
::Rails.root.join("db", "schema.rb").to_s
|
|
36
|
+
else
|
|
37
|
+
File.join(Dir.pwd, "db", "schema.rb")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_pk_type(line)
|
|
42
|
+
if (match = line.match(/id:\s*:(\w+)/))
|
|
43
|
+
match[1]
|
|
44
|
+
else
|
|
45
|
+
"integer"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_column(line)
|
|
50
|
+
return nil if line.start_with?("t.index")
|
|
51
|
+
|
|
52
|
+
match = line.match(/\At\.(\w+)\s+"(\w+)"(.*)/)
|
|
53
|
+
return nil unless match
|
|
54
|
+
|
|
55
|
+
type = match[1]
|
|
56
|
+
name = match[2]
|
|
57
|
+
options = match[3]
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
name: name,
|
|
61
|
+
type: type,
|
|
62
|
+
nullable: !options.match?(/null:\s*false/),
|
|
63
|
+
default: extract_default(options),
|
|
64
|
+
primary: false
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def process_line(stripped)
|
|
69
|
+
if (match = stripped.match(/\Acreate_table\s+"(\w+)"/))
|
|
70
|
+
start_table(match, stripped)
|
|
71
|
+
elsif @current_table && stripped == "end"
|
|
72
|
+
close_table
|
|
73
|
+
elsif @current_table && (col = parse_column(stripped))
|
|
74
|
+
@tables[@current_table] << col
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def start_table(match, stripped)
|
|
79
|
+
@current_table = match[1]
|
|
80
|
+
@tables[@current_table] = []
|
|
81
|
+
@has_pk = !stripped.match?(/id:\s*false/)
|
|
82
|
+
@pk_type = extract_pk_type(stripped)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def close_table
|
|
86
|
+
if @has_pk
|
|
87
|
+
pk_column = { name: "id", type: @pk_type, nullable: false, default: nil, primary: true }
|
|
88
|
+
@tables[@current_table].unshift(pk_column)
|
|
89
|
+
end
|
|
90
|
+
@current_table = nil
|
|
91
|
+
@pk_type = nil
|
|
92
|
+
@has_pk = true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extract_default(options)
|
|
96
|
+
if (match = options.match(/default:\s*(?:"([^"]*)"|(\d+(?:\.\d+)?))/))
|
|
97
|
+
match[1] || match[2]
|
|
98
|
+
elsif options.match?(/default:\s*true/)
|
|
99
|
+
"true"
|
|
100
|
+
elsif options.match?(/default:\s*false/)
|
|
101
|
+
"false"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
class Railtie < ::Rails::Railtie
|
|
6
|
+
rake_tasks do
|
|
7
|
+
namespace :rails_schema do
|
|
8
|
+
desc "Generate an interactive HTML schema diagram"
|
|
9
|
+
task generate: :environment do
|
|
10
|
+
path = ::Rails::Schema.generate
|
|
11
|
+
puts "Schema diagram generated: #{path}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "json"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Rails
|
|
8
|
+
module Schema
|
|
9
|
+
module Renderer
|
|
10
|
+
class HtmlGenerator
|
|
11
|
+
ASSETS_DIR = File.expand_path("../../schema/assets", __dir__)
|
|
12
|
+
|
|
13
|
+
def initialize(graph_data:, configuration: ::Rails::Schema.configuration)
|
|
14
|
+
@graph_data = graph_data
|
|
15
|
+
@configuration = configuration
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render
|
|
19
|
+
template = File.read(File.join(ASSETS_DIR, "template.html.erb"))
|
|
20
|
+
erb = ERB.new(template)
|
|
21
|
+
erb.result(binding)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def render_to_file(path = nil)
|
|
25
|
+
path ||= @configuration.output_path
|
|
26
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
27
|
+
File.write(path, render)
|
|
28
|
+
path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def title
|
|
34
|
+
@configuration.title
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def theme_class
|
|
38
|
+
case @configuration.theme
|
|
39
|
+
when :light then "light"
|
|
40
|
+
when :dark then "dark"
|
|
41
|
+
else ""
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def css_content
|
|
46
|
+
File.read(File.join(ASSETS_DIR, "style.css"))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def d3_js_content
|
|
50
|
+
File.read(File.join(ASSETS_DIR, "vendor", "d3.min.js"))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def app_js_content
|
|
54
|
+
File.read(File.join(ASSETS_DIR, "app.js"))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def graph_json
|
|
58
|
+
JSON.generate(@graph_data).gsub("</", '<\/')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def config_json
|
|
62
|
+
config = {
|
|
63
|
+
expand_columns: @configuration.expand_columns,
|
|
64
|
+
theme: @configuration.theme.to_s
|
|
65
|
+
}
|
|
66
|
+
JSON.generate(config).gsub("</", '<\/')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
module Transformer
|
|
6
|
+
class Edge
|
|
7
|
+
attr_reader :from, :to, :association_type, :label, :foreign_key, :through, :polymorphic
|
|
8
|
+
|
|
9
|
+
def initialize(from:, to:, association_type:, label:, foreign_key: nil, through: nil, polymorphic: false)
|
|
10
|
+
@from = from
|
|
11
|
+
@to = to
|
|
12
|
+
@association_type = association_type
|
|
13
|
+
@label = label
|
|
14
|
+
@foreign_key = foreign_key
|
|
15
|
+
@through = through
|
|
16
|
+
@polymorphic = polymorphic
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
from: @from,
|
|
22
|
+
to: @to,
|
|
23
|
+
association_type: @association_type,
|
|
24
|
+
label: @label,
|
|
25
|
+
foreign_key: @foreign_key,
|
|
26
|
+
through: @through,
|
|
27
|
+
polymorphic: @polymorphic
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
module Transformer
|
|
6
|
+
class GraphBuilder
|
|
7
|
+
def initialize(column_reader: Extractor::ColumnReader.new, association_reader: Extractor::AssociationReader.new)
|
|
8
|
+
@column_reader = column_reader
|
|
9
|
+
@association_reader = association_reader
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build(models)
|
|
13
|
+
node_names = models.to_set(&:name)
|
|
14
|
+
nodes = models.map { |m| build_node(m) }
|
|
15
|
+
edges = models.flat_map { |m| build_edges(m, node_names) }
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
nodes: nodes.map(&:to_h),
|
|
19
|
+
edges: edges.map(&:to_h),
|
|
20
|
+
metadata: build_metadata(models)
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def build_node(model)
|
|
27
|
+
Node.new(
|
|
28
|
+
id: model.name,
|
|
29
|
+
table_name: model.table_name,
|
|
30
|
+
columns: @column_reader.read(model)
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_edges(model, node_names)
|
|
35
|
+
@association_reader.read(model).filter_map do |assoc|
|
|
36
|
+
next unless node_names.include?(assoc[:to])
|
|
37
|
+
|
|
38
|
+
Edge.new(
|
|
39
|
+
from: assoc[:from],
|
|
40
|
+
to: assoc[:to],
|
|
41
|
+
association_type: assoc[:association_type],
|
|
42
|
+
label: assoc[:label],
|
|
43
|
+
foreign_key: assoc[:foreign_key],
|
|
44
|
+
through: assoc[:through],
|
|
45
|
+
polymorphic: assoc[:polymorphic]
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_metadata(models)
|
|
51
|
+
{
|
|
52
|
+
generated_at: Time.now.utc.iso8601,
|
|
53
|
+
model_count: models.size,
|
|
54
|
+
rails_version: defined?(::Rails.version) ? ::Rails.version : nil
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Schema
|
|
5
|
+
module Transformer
|
|
6
|
+
class Node
|
|
7
|
+
attr_reader :id, :table_name, :columns
|
|
8
|
+
|
|
9
|
+
def initialize(id:, table_name:, columns: [])
|
|
10
|
+
@id = id
|
|
11
|
+
@table_name = table_name
|
|
12
|
+
@columns = columns
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{
|
|
17
|
+
id: @id,
|
|
18
|
+
table_name: @table_name,
|
|
19
|
+
columns: @columns
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/rails/schema.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "schema/version"
|
|
4
|
+
require_relative "schema/configuration"
|
|
5
|
+
require_relative "schema/transformer/node"
|
|
6
|
+
require_relative "schema/transformer/edge"
|
|
7
|
+
require_relative "schema/extractor/schema_file_parser"
|
|
8
|
+
require_relative "schema/extractor/model_scanner"
|
|
9
|
+
require_relative "schema/extractor/column_reader"
|
|
10
|
+
require_relative "schema/extractor/association_reader"
|
|
11
|
+
require_relative "schema/transformer/graph_builder"
|
|
12
|
+
require_relative "schema/renderer/html_generator"
|
|
13
|
+
|
|
14
|
+
module Rails
|
|
15
|
+
module Schema
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def configuration
|
|
20
|
+
@configuration ||= Configuration.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def configure
|
|
24
|
+
yield(configuration)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset_configuration!
|
|
28
|
+
@configuration = Configuration.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def generate(output: nil)
|
|
32
|
+
schema_data = Extractor::SchemaFileParser.new.parse
|
|
33
|
+
models = Extractor::ModelScanner.new(schema_data: schema_data).scan
|
|
34
|
+
column_reader = Extractor::ColumnReader.new(schema_data: schema_data)
|
|
35
|
+
graph_data = Transformer::GraphBuilder.new(column_reader: column_reader).build(models)
|
|
36
|
+
generator = Renderer::HtmlGenerator.new(graph_data: graph_data)
|
|
37
|
+
generator.render_to_file(output)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
require_relative "schema/railtie" if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails-schema
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrei Kislichenko
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activerecord
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: railties
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '6.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '6.0'
|
|
40
|
+
description: Introspects a Rails app's models, associations, and columns, then generates
|
|
41
|
+
a single self-contained HTML file with an interactive entity-relationship diagram.
|
|
42
|
+
email:
|
|
43
|
+
- android.2net@gmail.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- LICENSE.txt
|
|
49
|
+
- PROJECT.md
|
|
50
|
+
- README.md
|
|
51
|
+
- Rakefile
|
|
52
|
+
- docs/index.html
|
|
53
|
+
- docs/screenshot.png
|
|
54
|
+
- lib/rails/schema.rb
|
|
55
|
+
- lib/rails/schema/assets/app.js
|
|
56
|
+
- lib/rails/schema/assets/style.css
|
|
57
|
+
- lib/rails/schema/assets/template.html.erb
|
|
58
|
+
- lib/rails/schema/assets/vendor/d3.min.js
|
|
59
|
+
- lib/rails/schema/configuration.rb
|
|
60
|
+
- lib/rails/schema/extractor/association_reader.rb
|
|
61
|
+
- lib/rails/schema/extractor/column_reader.rb
|
|
62
|
+
- lib/rails/schema/extractor/model_scanner.rb
|
|
63
|
+
- lib/rails/schema/extractor/schema_file_parser.rb
|
|
64
|
+
- lib/rails/schema/railtie.rb
|
|
65
|
+
- lib/rails/schema/renderer/html_generator.rb
|
|
66
|
+
- lib/rails/schema/transformer/edge.rb
|
|
67
|
+
- lib/rails/schema/transformer/graph_builder.rb
|
|
68
|
+
- lib/rails/schema/transformer/node.rb
|
|
69
|
+
- lib/rails/schema/version.rb
|
|
70
|
+
- sig/rails/schema.rbs
|
|
71
|
+
homepage: https://github.com/nicholaides/rails-schema
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata:
|
|
75
|
+
homepage_uri: https://github.com/nicholaides/rails-schema
|
|
76
|
+
source_code_uri: https://github.com/nicholaides/rails-schema
|
|
77
|
+
changelog_uri: https://github.com/nicholaides/rails-schema/blob/main/CHANGELOG.md
|
|
78
|
+
rubygems_mfa_required: 'true'
|
|
79
|
+
rdoc_options: []
|
|
80
|
+
require_paths:
|
|
81
|
+
- lib
|
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: 3.2.0
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '0'
|
|
92
|
+
requirements: []
|
|
93
|
+
rubygems_version: 3.6.9
|
|
94
|
+
specification_version: 4
|
|
95
|
+
summary: Interactive HTML visualization of your Rails database schema
|
|
96
|
+
test_files: []
|