rails-schema 0.1.0 → 0.1.2

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.
data/docs/screenshot.png CHANGED
Binary file
@@ -16,13 +16,66 @@
16
16
  var NODE_COLUMN_HEIGHT = 18;
17
17
  var NODE_PADDING = 8;
18
18
 
19
+ // Bezier curve tuning for edge routing
20
+ var MIN_LOOP_OFFSET = 60; // Minimum control-point distance for same-side (loop) edges
21
+ var LOOP_OFFSET_RATIO = 0.5; // Proportion of vertical gap used for loop curve spread
22
+ var MIN_CURVE_OFFSET = 50; // Minimum control-point distance for opposite-side edges
23
+ var CURVE_OFFSET_RATIO = 0.4; // Proportion of horizontal gap used for curve spread
24
+
19
25
  // Setup SVG
20
26
  var svgEl = document.getElementById("schema-svg");
21
27
  var svg = d3.select(svgEl);
28
+ var defs = svg.append("defs");
22
29
  var container = svg.append("g").attr("class", "zoom-container");
23
30
  var edgeGroup = container.append("g").attr("class", "edges");
24
31
  var nodeGroup = container.append("g").attr("class", "nodes");
25
32
 
33
+ // Marker definitions for crow's foot notation
34
+ var MARKER_TYPES = {
35
+ belongs_to: "--edge-belongs-to",
36
+ has_many: "--edge-has-many",
37
+ has_one: "--edge-has-one",
38
+ has_and_belongs_to_many: "--edge-habtm"
39
+ };
40
+
41
+ function buildMarkers() {
42
+ defs.selectAll("marker").remove();
43
+ var cs = getComputedStyle(document.documentElement);
44
+ Object.keys(MARKER_TYPES).forEach(function(type) {
45
+ var color = cs.getPropertyValue(MARKER_TYPES[type]).trim();
46
+
47
+ // "one" marker: perpendicular bar
48
+ var one = defs.append("marker")
49
+ .attr("id", "marker-one-" + type)
50
+ .attr("viewBox", "0 0 4 10")
51
+ .attr("refX", 4).attr("refY", 5)
52
+ .attr("markerWidth", 4).attr("markerHeight", 10)
53
+ .attr("markerUnits", "userSpaceOnUse")
54
+ .attr("orient", "auto-start-reverse");
55
+ one.append("path")
56
+ .attr("d", "M 4 0 L 4 10")
57
+ .attr("stroke", color)
58
+ .attr("stroke-width", 1.5)
59
+ .attr("fill", "none");
60
+
61
+ // "many" marker: crow's foot (three tines)
62
+ var many = defs.append("marker")
63
+ .attr("id", "marker-many-" + type)
64
+ .attr("viewBox", "0 0 10 10")
65
+ .attr("refX", 10).attr("refY", 5)
66
+ .attr("markerWidth", 10).attr("markerHeight", 10)
67
+ .attr("markerUnits", "userSpaceOnUse")
68
+ .attr("orient", "auto-start-reverse");
69
+ many.append("path")
70
+ .attr("d", "M 10 5 L 0 0 M 10 5 L 0 10 M 10 5 L 0 5")
71
+ .attr("stroke", color)
72
+ .attr("stroke-width", 1.5)
73
+ .attr("fill", "none");
74
+ });
75
+ }
76
+
77
+ buildMarkers();
78
+
26
79
  // Compute node heights
27
80
  nodes.forEach(function(n) {
28
81
  var cols = config.expand_columns ? n.columns : n.columns.slice(0, 4);
@@ -31,6 +84,20 @@
31
84
  if (!config.expand_columns && n.columns.length > 4) {
32
85
  n._height += NODE_COLUMN_HEIGHT; // "+N more" row
33
86
  }
87
+
88
+ // Build column y-offset map (from node center)
89
+ n._colYMap = {};
90
+ var startY = -n._height / 2 + NODE_HEADER_HEIGHT + NODE_PADDING;
91
+ cols.forEach(function(col, i) {
92
+ n._colYMap[col.name] = startY + i * NODE_COLUMN_HEIGHT + 10;
93
+ });
94
+
95
+ // Store "+N more" row y-offset for collapsed column fallback
96
+ if (!config.expand_columns && n.columns.length > 4) {
97
+ n._moreRowY = startY + cols.length * NODE_COLUMN_HEIGHT + 10;
98
+ } else {
99
+ n._moreRowY = null;
100
+ }
34
101
  });
35
102
 
36
103
  // Build adjacency for focus
@@ -41,6 +108,76 @@
41
108
  if (adjacency[e.to]) adjacency[e.to].add(e.from);
42
109
  });
43
110
 
111
+ // Marker assignment per association type
112
+ var MARKER_MAP = {
113
+ belongs_to: { start: "many", end: "one" },
114
+ has_many: { start: "one", end: "many" },
115
+ has_one: { start: "one", end: "one" },
116
+ has_and_belongs_to_many: { start: "many", end: "many" }
117
+ };
118
+
119
+ function getColumnY(node, colName) {
120
+ if (colName && node._colYMap[colName] !== undefined) {
121
+ return node._colYMap[colName];
122
+ }
123
+ if (node._moreRowY !== null) return node._moreRowY;
124
+ return 0;
125
+ }
126
+
127
+ function getPkColumnY(node) {
128
+ for (var i = 0; i < node._displayCols.length; i++) {
129
+ if (node._displayCols[i].primary) {
130
+ return node._colYMap[node._displayCols[i].name];
131
+ }
132
+ }
133
+ if (node._moreRowY !== null) return node._moreRowY;
134
+ return 0;
135
+ }
136
+
137
+ function getConnectionPoints(d) {
138
+ var src = d.source;
139
+ var tgt = d.target;
140
+ var assocType = d.data.association_type;
141
+ var fk = d.data.foreign_key;
142
+ var srcColY = 0;
143
+ var tgtColY = 0;
144
+
145
+ if (assocType === "belongs_to") {
146
+ srcColY = getColumnY(src, fk);
147
+ tgtColY = getPkColumnY(tgt);
148
+ } else if (assocType === "has_many" || assocType === "has_one") {
149
+ srcColY = getPkColumnY(src);
150
+ tgtColY = getColumnY(tgt, fk);
151
+ } else {
152
+ srcColY = getPkColumnY(src);
153
+ tgtColY = getPkColumnY(tgt);
154
+ }
155
+
156
+ var dx = tgt.x - src.x;
157
+ var srcX, tgtX, sameDirection;
158
+
159
+ if (Math.abs(dx) > NODE_WIDTH) {
160
+ sameDirection = false;
161
+ if (dx > 0) {
162
+ srcX = src.x + NODE_WIDTH / 2;
163
+ tgtX = tgt.x - NODE_WIDTH / 2;
164
+ } else {
165
+ srcX = src.x - NODE_WIDTH / 2;
166
+ tgtX = tgt.x + NODE_WIDTH / 2;
167
+ }
168
+ } else {
169
+ sameDirection = true;
170
+ srcX = src.x + NODE_WIDTH / 2;
171
+ tgtX = tgt.x + NODE_WIDTH / 2;
172
+ }
173
+
174
+ return {
175
+ x1: srcX, y1: src.y + srcColY,
176
+ x2: tgtX, y2: tgt.y + tgtColY,
177
+ sameDirection: sameDirection
178
+ };
179
+ }
180
+
44
181
  // d3-force simulation
45
182
  function setupSimulation() {
46
183
  var nodeMap = {};
@@ -57,10 +194,10 @@
57
194
  if (simulation) simulation.stop();
58
195
 
59
196
  simulation = d3.forceSimulation(simNodes)
60
- .force("link", d3.forceLink(simEdges).id(function(d) { return d.id; }).distance(200))
61
- .force("charge", d3.forceManyBody().strength(-400))
197
+ .force("link", d3.forceLink(simEdges).id(function(d) { return d.id; }).distance(320))
198
+ .force("charge", d3.forceManyBody().strength(-600))
62
199
  .force("center", d3.forceCenter(svgEl.clientWidth / 2, svgEl.clientHeight / 2))
63
- .force("collide", d3.forceCollide().radius(function(d) { return Math.max(NODE_WIDTH, d._height) / 2 + 20; }))
200
+ .force("collide", d3.forceCollide().radius(function(d) { return Math.max(NODE_WIDTH, d._height) / 2 + 50; }))
64
201
  .on("tick", ticked);
65
202
 
66
203
  return { simNodes: simNodes, simEdges: simEdges };
@@ -78,17 +215,40 @@
78
215
  function renderEdges(simEdges) {
79
216
  edgeGroup.selectAll(".edge-group").remove();
80
217
 
218
+ // Compute parallel edge offsets for edges between same node pair
219
+ var pairCounts = {};
220
+ simEdges.forEach(function(d) {
221
+ var key = [d.data.from, d.data.to].sort().join("||");
222
+ pairCounts[key] = (pairCounts[key] || 0) + 1;
223
+ });
224
+ var pairSeen = {};
225
+ simEdges.forEach(function(d) {
226
+ var key = [d.data.from, d.data.to].sort().join("||");
227
+ pairSeen[key] = (pairSeen[key] || 0);
228
+ d._pairCount = pairCounts[key];
229
+ d._pairIndex = pairSeen[key];
230
+ pairSeen[key]++;
231
+ });
232
+
81
233
  var eGroups = edgeGroup.selectAll(".edge-group")
82
234
  .data(simEdges, function(d) { return d.data.from + "-" + d.data.to + "-" + d.data.label; })
83
235
  .enter().append("g")
84
236
  .attr("class", "edge-group");
85
237
 
86
- eGroups.append("line")
238
+ eGroups.append("path")
87
239
  .attr("class", function(d) {
88
240
  var cls = "edge-line " + d.data.association_type;
89
241
  if (d.data.through) cls += " through";
90
242
  if (d.data.polymorphic) cls += " polymorphic-edge";
91
243
  return cls;
244
+ })
245
+ .attr("marker-start", function(d) {
246
+ var m = MARKER_MAP[d.data.association_type];
247
+ return m ? "url(#marker-" + m.start + "-" + d.data.association_type + ")" : null;
248
+ })
249
+ .attr("marker-end", function(d) {
250
+ var m = MARKER_MAP[d.data.association_type];
251
+ return m ? "url(#marker-" + m.end + "-" + d.data.association_type + ")" : null;
92
252
  });
93
253
 
94
254
  eGroups.append("text")
@@ -192,14 +352,66 @@
192
352
  function ticked() {
193
353
  edgeGroup.selectAll(".edge-group").each(function(d) {
194
354
  var g = d3.select(this);
195
- g.select("line")
196
- .attr("x1", d.source.x)
197
- .attr("y1", d.source.y)
198
- .attr("x2", d.target.x)
199
- .attr("y2", d.target.y);
355
+ var src = d.source;
356
+ var tgt = d.target;
357
+
358
+ // Self-referential association (markers are applied at path creation time via marker-start/marker-end)
359
+ if (src.id === tgt.id) {
360
+ var selfX = src.x + NODE_WIDTH / 2;
361
+ var selfY1 = src.y - 20;
362
+ var selfY2 = src.y + 20;
363
+ var selfOffset = 60;
364
+ g.select("path").attr("d",
365
+ "M " + selfX + " " + selfY1 +
366
+ " C " + (selfX + selfOffset) + " " + selfY1 +
367
+ ", " + (selfX + selfOffset) + " " + selfY2 +
368
+ ", " + selfX + " " + selfY2);
369
+ g.select("text")
370
+ .attr("x", selfX + selfOffset + 5)
371
+ .attr("y", src.y);
372
+ return;
373
+ }
374
+
375
+ var connPts = getConnectionPoints(d);
376
+
377
+ // Parallel edge offset
378
+ var parallelOffset = 0;
379
+ if (d._pairCount > 1) {
380
+ parallelOffset = (d._pairIndex - (d._pairCount - 1) / 2) * 8;
381
+ }
382
+
383
+ var y1 = connPts.y1 + parallelOffset;
384
+ var y2 = connPts.y2 + parallelOffset;
385
+
386
+ // Compute cubic bezier control points
387
+ var cp1x, cp1y, cp2x, cp2y;
388
+ if (connPts.sameDirection) {
389
+ var loopOffset = Math.max(MIN_LOOP_OFFSET, Math.abs(y2 - y1) * LOOP_OFFSET_RATIO);
390
+ cp1x = connPts.x1 + loopOffset;
391
+ cp1y = y1;
392
+ cp2x = connPts.x2 + loopOffset;
393
+ cp2y = y2;
394
+ } else {
395
+ var dx = connPts.x2 - connPts.x1;
396
+ var cpOffset = Math.max(MIN_CURVE_OFFSET, Math.abs(dx) * CURVE_OFFSET_RATIO);
397
+ var dir = Math.sign(dx) || 1;
398
+ cp1x = connPts.x1 + cpOffset * dir;
399
+ cp1y = y1;
400
+ cp2x = connPts.x2 - cpOffset * dir;
401
+ cp2y = y2;
402
+ }
403
+
404
+ g.select("path").attr("d",
405
+ "M " + connPts.x1 + " " + y1 +
406
+ " C " + cp1x + " " + cp1y +
407
+ ", " + cp2x + " " + cp2y +
408
+ ", " + connPts.x2 + " " + y2);
409
+
410
+ var labelX = 0.125 * connPts.x1 + 0.375 * cp1x + 0.375 * cp2x + 0.125 * connPts.x2;
411
+ var labelY = (y1 + y2) / 2;
200
412
  g.select("text")
201
- .attr("x", (d.source.x + d.target.x) / 2)
202
- .attr("y", (d.source.y + d.target.y) / 2);
413
+ .attr("x", labelX)
414
+ .attr("y", labelY);
203
415
  });
204
416
 
205
417
  nodeGroup.selectAll(".node-group")
@@ -417,6 +629,12 @@
417
629
  var isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
418
630
  html.classList.add(isDark ? "light" : "dark");
419
631
  }
632
+ buildMarkers();
633
+ });
634
+
635
+ // Rebuild markers when system theme changes
636
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function() {
637
+ buildMarkers();
420
638
  });
421
639
 
422
640
  function fitToScreen() {
@@ -310,6 +310,8 @@ body {
310
310
  .edge-line {
311
311
  fill: none;
312
312
  stroke-width: 1.5;
313
+ stroke-linecap: round;
314
+ stroke-linejoin: round;
313
315
  }
314
316
 
315
317
  .edge-line.belongs_to { stroke: var(--edge-belongs-to); }
@@ -471,3 +473,23 @@ body {
471
473
  .legend-line.has_many { background: var(--edge-has-many); }
472
474
  .legend-line.has_one { background: var(--edge-has-one); }
473
475
  .legend-line.habtm { background: var(--edge-habtm); }
476
+
477
+ .legend-line.through {
478
+ background: var(--edge-color);
479
+ background-image: repeating-linear-gradient(
480
+ to right,
481
+ var(--edge-color) 0, var(--edge-color) 6px,
482
+ transparent 6px, transparent 9px
483
+ );
484
+ background-color: transparent;
485
+ }
486
+
487
+ .legend-line.polymorphic {
488
+ background: var(--edge-color);
489
+ background-image: repeating-linear-gradient(
490
+ to right,
491
+ var(--edge-color) 0, var(--edge-color) 3px,
492
+ transparent 3px, transparent 6px
493
+ );
494
+ background-color: transparent;
495
+ }
@@ -22,10 +22,12 @@
22
22
 
23
23
  <div id="toolbar">
24
24
  <div class="legend">
25
- <span class="legend-item"><span class="legend-line belongs_to"></span> belongs_to</span>
26
- <span class="legend-item"><span class="legend-line has_many"></span> has_many</span>
27
- <span class="legend-item"><span class="legend-line has_one"></span> has_one</span>
28
- <span class="legend-item"><span class="legend-line habtm"></span> habtm</span>
25
+ <span class="legend-item"><span class="legend-line belongs_to"></span> belongs_to (M:1)</span>
26
+ <span class="legend-item"><span class="legend-line has_many"></span> has_many (1:M)</span>
27
+ <span class="legend-item"><span class="legend-line has_one"></span> has_one (1:1)</span>
28
+ <span class="legend-item"><span class="legend-line habtm"></span> habtm (M:M)</span>
29
+ <span class="legend-item"><span class="legend-line through"></span> :through</span>
30
+ <span class="legend-item"><span class="legend-line polymorphic"></span> polymorphic</span>
29
31
  </div>
30
32
  <div class="spacer"></div>
31
33
  <span class="zoom-info" id="zoom-info">100%</span>
@@ -3,7 +3,7 @@
3
3
  module Rails
4
4
  module Schema
5
5
  class Configuration
6
- attr_accessor :output_path, :exclude_models, :title, :theme, :expand_columns
6
+ attr_accessor :output_path, :exclude_models, :title, :theme, :expand_columns, :schema_format
7
7
 
8
8
  def initialize
9
9
  @output_path = "docs/schema.html"
@@ -11,6 +11,7 @@ module Rails
11
11
  @title = "Database Schema"
12
12
  @theme = :auto
13
13
  @expand_columns = false
14
+ @schema_format = :auto
14
15
  end
15
16
  end
16
17
  end
@@ -26,16 +26,27 @@ module Rails
26
26
  association_type: ref.macro.to_s,
27
27
  label: ref.name.to_s,
28
28
  foreign_key: ref.foreign_key.to_s,
29
- through: ref.options[:through]&.to_s,
30
- polymorphic: ref.options[:as] ? true : false
29
+ through: through_name(ref),
30
+ polymorphic: polymorphic?(ref)
31
31
  }
32
- rescue StandardError
32
+ rescue StandardError => e
33
+ warn "[rails-schema] Could not read association #{ref.name} on #{model.name}: #{e.class}: #{e.message}"
33
34
  nil
34
35
  end
35
36
 
37
+ def through_name(ref)
38
+ ref.options[:through]&.to_s
39
+ end
40
+
41
+ def polymorphic?(ref)
42
+ ref.options[:as] ? true : false
43
+ end
44
+
36
45
  def target_model_name(ref)
37
46
  ref.klass.name
38
- rescue StandardError
47
+ rescue StandardError => e
48
+ warn "[rails-schema] Could not resolve target for #{ref.name}, " \
49
+ "falling back to #{ref.class_name}: #{e.class}: #{e.message}"
39
50
  ref.class_name
40
51
  end
41
52
  end
@@ -28,7 +28,8 @@ module Rails
28
28
  default: col.default
29
29
  }
30
30
  end
31
- rescue StandardError
31
+ rescue StandardError => e
32
+ warn "[rails-schema] Could not read columns for #{model.name}: #{e.class}: #{e.message}"
32
33
  []
33
34
  end
34
35
  end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails
4
+ module Schema
5
+ module Extractor
6
+ class StructureSqlParser
7
+ SQL_TYPE_MAP = {
8
+ "character varying" => "string", "varchar" => "string",
9
+ "integer" => "integer", "smallint" => "integer", "serial" => "integer",
10
+ "bigint" => "bigint", "bigserial" => "bigint",
11
+ "boolean" => "boolean", "text" => "text",
12
+ "timestamp without time zone" => "datetime", "timestamp with time zone" => "datetime",
13
+ "timestamp" => "datetime",
14
+ "json" => "json", "jsonb" => "jsonb", "uuid" => "uuid",
15
+ "numeric" => "decimal", "decimal" => "decimal", "money" => "decimal",
16
+ "date" => "date",
17
+ "float" => "float", "double precision" => "float", "real" => "float",
18
+ "bytea" => "binary"
19
+ }.freeze
20
+
21
+ COMPOUND_TYPE_RE = /\A(character\s+varying|bit\s+varying|double\s+precision|
22
+ timestamp(?:\(\d+\))?\s+with(?:out)?\s+time\s+zone)/ix
23
+ CONSTRAINT_RE = /\A(CONSTRAINT|UNIQUE|CHECK|EXCLUDE|FOREIGN\s+KEY)\b/i
24
+ PK_CONSTRAINT_RE = /PRIMARY\s+KEY\s*\(([^)]+)\)/i
25
+
26
+ def initialize(structure_path = nil)
27
+ @structure_path = structure_path
28
+ end
29
+
30
+ def parse
31
+ path = resolve_path
32
+ return {} unless path && File.exist?(path)
33
+
34
+ parse_content(File.read(path))
35
+ end
36
+
37
+ def parse_content(content)
38
+ tables = {}
39
+
40
+ content.scan(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w."]+)\s*\((.*?)\)\s*;/mi) do |table_name, body|
41
+ name = extract_table_name(table_name)
42
+ columns, pk_columns = parse_table_body(body)
43
+ pk_columns.each { |pk| columns.find { |c| c[:name] == pk }&.[]= :primary, true }
44
+ tables[name] = columns
45
+ end
46
+
47
+ tables
48
+ end
49
+
50
+ private
51
+
52
+ def resolve_path
53
+ return @structure_path if @structure_path
54
+ return ::Rails.root.join("db", "structure.sql").to_s if defined?(::Rails.root) && ::Rails.root
55
+
56
+ File.join(Dir.pwd, "db", "structure.sql")
57
+ end
58
+
59
+ def unquote(identifier) = identifier.delete('"')
60
+
61
+ def extract_table_name(raw)
62
+ unquote(raw).split(".").last
63
+ end
64
+
65
+ def parse_table_body(body)
66
+ columns = []
67
+ pk_columns = []
68
+ body.each_line do |raw|
69
+ line = raw.strip.chomp(",")
70
+ next if line.empty?
71
+
72
+ if (pk = extract_pk_constraint(line))
73
+ pk_columns.concat(pk)
74
+ elsif !line.match?(CONSTRAINT_RE) && (col = parse_column_line(line))
75
+ pk_columns << col[:name] if col.delete(:inline_pk)
76
+ columns << col
77
+ end
78
+ end
79
+ [columns, pk_columns]
80
+ end
81
+
82
+ def extract_pk_constraint(line)
83
+ return unless (match = line.match(PK_CONSTRAINT_RE))
84
+
85
+ match[1].split(",").map { |c| unquote(c.strip) }
86
+ end
87
+
88
+ def parse_column_line(line)
89
+ match = line.match(/\A("?\w+"?)\s+(.+)/i)
90
+ return nil unless match
91
+
92
+ rest = match[2]
93
+ type = extract_type(rest)
94
+ return nil unless type
95
+
96
+ build_column(unquote(match[1]), rest, type)
97
+ end
98
+
99
+ def build_column(col_name, rest, type)
100
+ {
101
+ name: col_name,
102
+ type: SQL_TYPE_MAP.fetch(type, type),
103
+ nullable: !rest.match?(/\bNOT\s+NULL\b/i),
104
+ default: extract_default(rest),
105
+ primary: false,
106
+ inline_pk: rest.match?(/\bPRIMARY\s+KEY\b/i)
107
+ }
108
+ end
109
+
110
+ def extract_type(rest)
111
+ if (m = rest.match(COMPOUND_TYPE_RE))
112
+ m[1].downcase.gsub(/\(\d+\)/, "")
113
+ elsif rest.match?(/\A(FOREIGN\s+KEY)\b/i)
114
+ nil
115
+ else
116
+ rest[/\A(\w+)/i, 1]&.downcase
117
+ end
118
+ end
119
+
120
+ def extract_default(rest)
121
+ case rest
122
+ when /\bDEFAULT\s+'([^']*)'(?:::\w+)?/i, /\bDEFAULT\s+(\d+(?:\.\d+)?)\b/i
123
+ Regexp.last_match(1)
124
+ when /\bDEFAULT\s+true\b/i then "true"
125
+ when /\bDEFAULT\s+false\b/i then "false"
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rails
4
4
  module Schema
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
data/lib/rails/schema.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "schema/configuration"
5
5
  require_relative "schema/transformer/node"
6
6
  require_relative "schema/transformer/edge"
7
7
  require_relative "schema/extractor/schema_file_parser"
8
+ require_relative "schema/extractor/structure_sql_parser"
8
9
  require_relative "schema/extractor/model_scanner"
9
10
  require_relative "schema/extractor/column_reader"
10
11
  require_relative "schema/extractor/association_reader"
@@ -29,13 +30,27 @@ module Rails
29
30
  end
30
31
 
31
32
  def generate(output: nil)
32
- schema_data = Extractor::SchemaFileParser.new.parse
33
+ schema_data = parse_schema
33
34
  models = Extractor::ModelScanner.new(schema_data: schema_data).scan
34
35
  column_reader = Extractor::ColumnReader.new(schema_data: schema_data)
35
36
  graph_data = Transformer::GraphBuilder.new(column_reader: column_reader).build(models)
36
37
  generator = Renderer::HtmlGenerator.new(graph_data: graph_data)
37
38
  generator.render_to_file(output)
38
39
  end
40
+
41
+ private
42
+
43
+ def parse_schema
44
+ case configuration.schema_format
45
+ when :ruby
46
+ Extractor::SchemaFileParser.new.parse
47
+ when :sql
48
+ Extractor::StructureSqlParser.new.parse
49
+ when :auto
50
+ data = Extractor::SchemaFileParser.new.parse
51
+ data.empty? ? Extractor::StructureSqlParser.new.parse : data
52
+ end
53
+ end
39
54
  end
40
55
  end
41
56
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Kislichenko
@@ -45,6 +45,7 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - CHANGELOG.md
48
49
  - LICENSE.txt
49
50
  - PROJECT.md
50
51
  - README.md
@@ -61,6 +62,7 @@ files:
61
62
  - lib/rails/schema/extractor/column_reader.rb
62
63
  - lib/rails/schema/extractor/model_scanner.rb
63
64
  - lib/rails/schema/extractor/schema_file_parser.rb
65
+ - lib/rails/schema/extractor/structure_sql_parser.rb
64
66
  - lib/rails/schema/railtie.rb
65
67
  - lib/rails/schema/renderer/html_generator.rb
66
68
  - lib/rails/schema/transformer/edge.rb