rails-flow-map 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/CHANGELOG.md +52 -0
- data/LICENSE +21 -0
- data/README.md +314 -0
- data/README_ja.md +314 -0
- data/README_zh.md +314 -0
- data/lib/rails_flow_map/analyzers/controller_analyzer.rb +124 -0
- data/lib/rails_flow_map/analyzers/model_analyzer.rb +139 -0
- data/lib/rails_flow_map/configuration.rb +22 -0
- data/lib/rails_flow_map/engine.rb +16 -0
- data/lib/rails_flow_map/errors.rb +240 -0
- data/lib/rails_flow_map/formatters/d3js_formatter.rb +488 -0
- data/lib/rails_flow_map/formatters/erd_formatter.rb +64 -0
- data/lib/rails_flow_map/formatters/git_diff_formatter.rb +589 -0
- data/lib/rails_flow_map/formatters/graphviz_formatter.rb +111 -0
- data/lib/rails_flow_map/formatters/mermaid_formatter.rb +91 -0
- data/lib/rails_flow_map/formatters/metrics_formatter.rb +196 -0
- data/lib/rails_flow_map/formatters/openapi_formatter.rb +557 -0
- data/lib/rails_flow_map/formatters/plantuml_formatter.rb +92 -0
- data/lib/rails_flow_map/formatters/sequence_formatter.rb +288 -0
- data/lib/rails_flow_map/generators/install/templates/rails_flow_map.rb +34 -0
- data/lib/rails_flow_map/generators/install_generator.rb +32 -0
- data/lib/rails_flow_map/logging.rb +215 -0
- data/lib/rails_flow_map/models/flow_edge.rb +31 -0
- data/lib/rails_flow_map/models/flow_graph.rb +58 -0
- data/lib/rails_flow_map/models/flow_node.rb +37 -0
- data/lib/rails_flow_map/version.rb +3 -0
- data/lib/rails_flow_map.rb +310 -0
- data/lib/tasks/rails_flow_map.rake +70 -0
- metadata +156 -0
@@ -0,0 +1,488 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module RailsFlowMap
|
4
|
+
# Generates an interactive HTML visualization using D3.js library
|
5
|
+
#
|
6
|
+
# This formatter creates a full HTML page with embedded JavaScript that renders
|
7
|
+
# an interactive force-directed graph. Users can drag nodes, zoom, search, and
|
8
|
+
# filter by node types.
|
9
|
+
#
|
10
|
+
# @example Basic usage
|
11
|
+
# formatter = D3jsFormatter.new(graph)
|
12
|
+
# html = formatter.format
|
13
|
+
# File.write('interactive.html', html)
|
14
|
+
#
|
15
|
+
# @example With custom options
|
16
|
+
# formatter = D3jsFormatter.new(graph, {
|
17
|
+
# width: 1200,
|
18
|
+
# height: 800,
|
19
|
+
# node_colors: { model: '#ff0000', controller: '#00ff00' }
|
20
|
+
# })
|
21
|
+
#
|
22
|
+
class D3jsFormatter
|
23
|
+
# Creates a new D3.js formatter instance
|
24
|
+
#
|
25
|
+
# @param graph [FlowGraph] The graph to visualize
|
26
|
+
# @param options [Hash] Optional configuration
|
27
|
+
# @option options [Integer] :width The width of the visualization canvas
|
28
|
+
# @option options [Integer] :height The height of the visualization canvas
|
29
|
+
# @option options [Hash] :node_colors Custom colors for node types
|
30
|
+
def initialize(graph, options = {})
|
31
|
+
@graph = graph
|
32
|
+
@options = options
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generates the HTML visualization
|
36
|
+
#
|
37
|
+
# @param graph [FlowGraph] Optional graph to format (uses instance graph by default)
|
38
|
+
# @return [String] Complete HTML document with embedded D3.js visualization
|
39
|
+
def format(graph = @graph)
|
40
|
+
html_content = <<~HTML
|
41
|
+
<!DOCTYPE html>
|
42
|
+
<html lang="ja">
|
43
|
+
<head>
|
44
|
+
<meta charset="UTF-8">
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
46
|
+
<title>Rails Flow Map - Interactive Visualization</title>
|
47
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
48
|
+
<style>
|
49
|
+
body {
|
50
|
+
font-family: Arial, sans-serif;
|
51
|
+
margin: 0;
|
52
|
+
overflow: hidden;
|
53
|
+
}
|
54
|
+
|
55
|
+
#container {
|
56
|
+
width: 100vw;
|
57
|
+
height: 100vh;
|
58
|
+
position: relative;
|
59
|
+
}
|
60
|
+
|
61
|
+
#controls {
|
62
|
+
position: absolute;
|
63
|
+
top: 10px;
|
64
|
+
left: 10px;
|
65
|
+
background: white;
|
66
|
+
padding: 15px;
|
67
|
+
border-radius: 5px;
|
68
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
69
|
+
z-index: 1000;
|
70
|
+
}
|
71
|
+
|
72
|
+
#search {
|
73
|
+
padding: 5px 10px;
|
74
|
+
border: 1px solid #ddd;
|
75
|
+
border-radius: 3px;
|
76
|
+
width: 200px;
|
77
|
+
margin-bottom: 10px;
|
78
|
+
}
|
79
|
+
|
80
|
+
.filter-group {
|
81
|
+
margin-bottom: 10px;
|
82
|
+
}
|
83
|
+
|
84
|
+
.filter-group label {
|
85
|
+
display: block;
|
86
|
+
margin: 5px 0;
|
87
|
+
cursor: pointer;
|
88
|
+
}
|
89
|
+
|
90
|
+
.node {
|
91
|
+
cursor: pointer;
|
92
|
+
}
|
93
|
+
|
94
|
+
.node circle {
|
95
|
+
stroke-width: 2px;
|
96
|
+
}
|
97
|
+
|
98
|
+
.node text {
|
99
|
+
font: 12px sans-serif;
|
100
|
+
pointer-events: none;
|
101
|
+
}
|
102
|
+
|
103
|
+
.link {
|
104
|
+
fill: none;
|
105
|
+
stroke: #999;
|
106
|
+
stroke-opacity: 0.6;
|
107
|
+
stroke-width: 1.5px;
|
108
|
+
}
|
109
|
+
|
110
|
+
.link-label {
|
111
|
+
font: 10px sans-serif;
|
112
|
+
fill: #666;
|
113
|
+
}
|
114
|
+
|
115
|
+
.highlighted {
|
116
|
+
opacity: 1 !important;
|
117
|
+
}
|
118
|
+
|
119
|
+
.dimmed {
|
120
|
+
opacity: 0.2;
|
121
|
+
}
|
122
|
+
|
123
|
+
.tooltip {
|
124
|
+
position: absolute;
|
125
|
+
padding: 10px;
|
126
|
+
background: rgba(0, 0, 0, 0.8);
|
127
|
+
color: white;
|
128
|
+
border-radius: 5px;
|
129
|
+
pointer-events: none;
|
130
|
+
opacity: 0;
|
131
|
+
transition: opacity 0.3s;
|
132
|
+
font-size: 12px;
|
133
|
+
}
|
134
|
+
|
135
|
+
#info {
|
136
|
+
position: absolute;
|
137
|
+
bottom: 10px;
|
138
|
+
right: 10px;
|
139
|
+
background: white;
|
140
|
+
padding: 10px;
|
141
|
+
border-radius: 5px;
|
142
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
143
|
+
font-size: 12px;
|
144
|
+
}
|
145
|
+
|
146
|
+
.legend {
|
147
|
+
position: absolute;
|
148
|
+
top: 10px;
|
149
|
+
right: 10px;
|
150
|
+
background: white;
|
151
|
+
padding: 15px;
|
152
|
+
border-radius: 5px;
|
153
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
154
|
+
}
|
155
|
+
|
156
|
+
.legend-item {
|
157
|
+
margin: 5px 0;
|
158
|
+
}
|
159
|
+
|
160
|
+
.legend-color {
|
161
|
+
display: inline-block;
|
162
|
+
width: 20px;
|
163
|
+
height: 20px;
|
164
|
+
margin-right: 5px;
|
165
|
+
vertical-align: middle;
|
166
|
+
border-radius: 3px;
|
167
|
+
}
|
168
|
+
</style>
|
169
|
+
</head>
|
170
|
+
<body>
|
171
|
+
<div id="container">
|
172
|
+
<div id="controls">
|
173
|
+
<h3>フィルター</h3>
|
174
|
+
<input type="text" id="search" placeholder="検索...">
|
175
|
+
|
176
|
+
<div class="filter-group">
|
177
|
+
<label><input type="checkbox" class="type-filter" value="model" checked> モデル</label>
|
178
|
+
<label><input type="checkbox" class="type-filter" value="controller" checked> コントローラー</label>
|
179
|
+
<label><input type="checkbox" class="type-filter" value="action" checked> アクション</label>
|
180
|
+
<label><input type="checkbox" class="type-filter" value="service" checked> サービス</label>
|
181
|
+
<label><input type="checkbox" class="type-filter" value="route" checked> ルート</label>
|
182
|
+
</div>
|
183
|
+
|
184
|
+
<button id="reset-zoom">ズームリセット</button>
|
185
|
+
<button id="center-graph">中央に配置</button>
|
186
|
+
</div>
|
187
|
+
|
188
|
+
<div class="legend">
|
189
|
+
<h4>凡例</h4>
|
190
|
+
<div class="legend-item">
|
191
|
+
<span class="legend-color" style="background: #ff9999"></span>モデル
|
192
|
+
</div>
|
193
|
+
<div class="legend-item">
|
194
|
+
<span class="legend-color" style="background: #9999ff"></span>コントローラー
|
195
|
+
</div>
|
196
|
+
<div class="legend-item">
|
197
|
+
<span class="legend-color" style="background: #99ff99"></span>アクション
|
198
|
+
</div>
|
199
|
+
<div class="legend-item">
|
200
|
+
<span class="legend-color" style="background: #ffcc99"></span>サービス
|
201
|
+
</div>
|
202
|
+
<div class="legend-item">
|
203
|
+
<span class="legend-color" style="background: #ffff99"></span>ルート
|
204
|
+
</div>
|
205
|
+
</div>
|
206
|
+
|
207
|
+
<div id="info">
|
208
|
+
ノード: <span id="node-count">0</span> |
|
209
|
+
エッジ: <span id="edge-count">0</span> |
|
210
|
+
ズーム: <span id="zoom-level">100%</span>
|
211
|
+
</div>
|
212
|
+
|
213
|
+
<div class="tooltip"></div>
|
214
|
+
</div>
|
215
|
+
|
216
|
+
<script>
|
217
|
+
// データの準備
|
218
|
+
const graphData = #{generate_graph_data.to_json};
|
219
|
+
|
220
|
+
// 色の定義
|
221
|
+
const colors = {
|
222
|
+
model: '#ff9999',
|
223
|
+
controller: '#9999ff',
|
224
|
+
action: '#99ff99',
|
225
|
+
service: '#ffcc99',
|
226
|
+
route: '#ffff99'
|
227
|
+
};
|
228
|
+
|
229
|
+
// SVGの設定
|
230
|
+
const width = window.innerWidth;
|
231
|
+
const height = window.innerHeight;
|
232
|
+
|
233
|
+
const svg = d3.select("#container")
|
234
|
+
.append("svg")
|
235
|
+
.attr("width", width)
|
236
|
+
.attr("height", height);
|
237
|
+
|
238
|
+
const g = svg.append("g");
|
239
|
+
|
240
|
+
// ズーム機能
|
241
|
+
const zoom = d3.zoom()
|
242
|
+
.scaleExtent([0.1, 10])
|
243
|
+
.on("zoom", (event) => {
|
244
|
+
g.attr("transform", event.transform);
|
245
|
+
d3.select("#zoom-level").text(Math.round(event.transform.k * 100) + "%");
|
246
|
+
});
|
247
|
+
|
248
|
+
svg.call(zoom);
|
249
|
+
|
250
|
+
// Force simulation
|
251
|
+
const simulation = d3.forceSimulation(graphData.nodes)
|
252
|
+
.force("link", d3.forceLink(graphData.links).id(d => d.id).distance(100))
|
253
|
+
.force("charge", d3.forceManyBody().strength(-300))
|
254
|
+
.force("center", d3.forceCenter(width / 2, height / 2))
|
255
|
+
.force("collision", d3.forceCollide().radius(50));
|
256
|
+
|
257
|
+
// リンクの描画
|
258
|
+
const link = g.append("g")
|
259
|
+
.attr("class", "links")
|
260
|
+
.selectAll("line")
|
261
|
+
.data(graphData.links)
|
262
|
+
.enter().append("line")
|
263
|
+
.attr("class", "link")
|
264
|
+
.attr("stroke", d => d.type === 'belongs_to' ? '#ff6666' : '#999')
|
265
|
+
.attr("stroke-dasharray", d => d.type === 'has_action' ? '5,5' : 'none');
|
266
|
+
|
267
|
+
// ノードグループ
|
268
|
+
const node = g.append("g")
|
269
|
+
.attr("class", "nodes")
|
270
|
+
.selectAll("g")
|
271
|
+
.data(graphData.nodes)
|
272
|
+
.enter().append("g")
|
273
|
+
.attr("class", "node")
|
274
|
+
.call(d3.drag()
|
275
|
+
.on("start", dragstarted)
|
276
|
+
.on("drag", dragged)
|
277
|
+
.on("end", dragended));
|
278
|
+
|
279
|
+
// ノードの円
|
280
|
+
node.append("circle")
|
281
|
+
.attr("r", d => d.type === 'model' ? 15 : 10)
|
282
|
+
.attr("fill", d => colors[d.type] || '#999')
|
283
|
+
.attr("stroke", "#fff");
|
284
|
+
|
285
|
+
// ノードのラベル
|
286
|
+
node.append("text")
|
287
|
+
.attr("dy", ".35em")
|
288
|
+
.attr("x", 20)
|
289
|
+
.text(d => d.name)
|
290
|
+
.style("font-size", "12px");
|
291
|
+
|
292
|
+
// ツールチップ
|
293
|
+
const tooltip = d3.select(".tooltip");
|
294
|
+
|
295
|
+
node.on("mouseover", function(event, d) {
|
296
|
+
tooltip.transition()
|
297
|
+
.duration(200)
|
298
|
+
.style("opacity", .9);
|
299
|
+
|
300
|
+
let content = `<strong>${d.name}</strong><br/>
|
301
|
+
タイプ: ${d.type}<br/>`;
|
302
|
+
|
303
|
+
if (d.attributes) {
|
304
|
+
if (d.attributes.associations) {
|
305
|
+
content += `関連: ${d.attributes.associations.join(', ')}<br/>`;
|
306
|
+
}
|
307
|
+
if (d.attributes.path) {
|
308
|
+
content += `パス: ${d.attributes.path}<br/>`;
|
309
|
+
}
|
310
|
+
if (d.attributes.verb) {
|
311
|
+
content += `メソッド: ${d.attributes.verb}<br/>`;
|
312
|
+
}
|
313
|
+
}
|
314
|
+
|
315
|
+
tooltip.html(content)
|
316
|
+
.style("left", (event.pageX + 10) + "px")
|
317
|
+
.style("top", (event.pageY - 28) + "px");
|
318
|
+
})
|
319
|
+
.on("mouseout", function(d) {
|
320
|
+
tooltip.transition()
|
321
|
+
.duration(500)
|
322
|
+
.style("opacity", 0);
|
323
|
+
});
|
324
|
+
|
325
|
+
// ダブルクリックで関連ノードのハイライト
|
326
|
+
node.on("dblclick", function(event, d) {
|
327
|
+
event.stopPropagation();
|
328
|
+
highlightConnected(d);
|
329
|
+
});
|
330
|
+
|
331
|
+
// シミュレーションの更新
|
332
|
+
simulation.on("tick", () => {
|
333
|
+
link
|
334
|
+
.attr("x1", d => d.source.x)
|
335
|
+
.attr("y1", d => d.source.y)
|
336
|
+
.attr("x2", d => d.target.x)
|
337
|
+
.attr("y2", d => d.target.y);
|
338
|
+
|
339
|
+
node
|
340
|
+
.attr("transform", d => `translate(${d.x},${d.y})`);
|
341
|
+
});
|
342
|
+
|
343
|
+
// ドラッグ機能
|
344
|
+
function dragstarted(event, d) {
|
345
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
346
|
+
d.fx = d.x;
|
347
|
+
d.fy = d.y;
|
348
|
+
}
|
349
|
+
|
350
|
+
function dragged(event, d) {
|
351
|
+
d.fx = event.x;
|
352
|
+
d.fy = event.y;
|
353
|
+
}
|
354
|
+
|
355
|
+
function dragended(event, d) {
|
356
|
+
if (!event.active) simulation.alphaTarget(0);
|
357
|
+
d.fx = null;
|
358
|
+
d.fy = null;
|
359
|
+
}
|
360
|
+
|
361
|
+
// 検索機能
|
362
|
+
d3.select("#search").on("input", function() {
|
363
|
+
const searchTerm = this.value.toLowerCase();
|
364
|
+
|
365
|
+
node.classed("dimmed", d => {
|
366
|
+
return searchTerm && !d.name.toLowerCase().includes(searchTerm);
|
367
|
+
});
|
368
|
+
|
369
|
+
link.classed("dimmed", d => {
|
370
|
+
return searchTerm &&
|
371
|
+
!d.source.name.toLowerCase().includes(searchTerm) &&
|
372
|
+
!d.target.name.toLowerCase().includes(searchTerm);
|
373
|
+
});
|
374
|
+
});
|
375
|
+
|
376
|
+
// フィルター機能
|
377
|
+
d3.selectAll(".type-filter").on("change", function() {
|
378
|
+
const visibleTypes = [];
|
379
|
+
d3.selectAll(".type-filter:checked").each(function() {
|
380
|
+
visibleTypes.push(this.value);
|
381
|
+
});
|
382
|
+
|
383
|
+
node.style("display", d => visibleTypes.includes(d.type) ? null : "none");
|
384
|
+
|
385
|
+
link.style("display", d => {
|
386
|
+
return visibleTypes.includes(d.source.type) &&
|
387
|
+
visibleTypes.includes(d.target.type) ? null : "none";
|
388
|
+
});
|
389
|
+
});
|
390
|
+
|
391
|
+
// 関連ノードのハイライト
|
392
|
+
function highlightConnected(selectedNode) {
|
393
|
+
const connectedNodeIds = new Set([selectedNode.id]);
|
394
|
+
|
395
|
+
graphData.links.forEach(link => {
|
396
|
+
if (link.source.id === selectedNode.id) {
|
397
|
+
connectedNodeIds.add(link.target.id);
|
398
|
+
}
|
399
|
+
if (link.target.id === selectedNode.id) {
|
400
|
+
connectedNodeIds.add(link.source.id);
|
401
|
+
}
|
402
|
+
});
|
403
|
+
|
404
|
+
node.classed("dimmed", d => !connectedNodeIds.has(d.id));
|
405
|
+
node.classed("highlighted", d => connectedNodeIds.has(d.id));
|
406
|
+
|
407
|
+
link.classed("dimmed", d => {
|
408
|
+
return !connectedNodeIds.has(d.source.id) || !connectedNodeIds.has(d.target.id);
|
409
|
+
});
|
410
|
+
link.classed("highlighted", d => {
|
411
|
+
return connectedNodeIds.has(d.source.id) && connectedNodeIds.has(d.target.id);
|
412
|
+
});
|
413
|
+
}
|
414
|
+
|
415
|
+
// リセット機能
|
416
|
+
svg.on("dblclick", function() {
|
417
|
+
node.classed("dimmed", false);
|
418
|
+
node.classed("highlighted", false);
|
419
|
+
link.classed("dimmed", false);
|
420
|
+
link.classed("highlighted", false);
|
421
|
+
});
|
422
|
+
|
423
|
+
// コントロールボタン
|
424
|
+
d3.select("#reset-zoom").on("click", () => {
|
425
|
+
svg.transition()
|
426
|
+
.duration(750)
|
427
|
+
.call(zoom.transform, d3.zoomIdentity);
|
428
|
+
});
|
429
|
+
|
430
|
+
d3.select("#center-graph").on("click", () => {
|
431
|
+
const bounds = g.node().getBBox();
|
432
|
+
const fullWidth = width;
|
433
|
+
const fullHeight = height;
|
434
|
+
const widthScale = fullWidth / bounds.width;
|
435
|
+
const heightScale = fullHeight / bounds.height;
|
436
|
+
const scale = 0.8 * Math.min(widthScale, heightScale);
|
437
|
+
const translate = [
|
438
|
+
fullWidth / 2 - scale * (bounds.x + bounds.width / 2),
|
439
|
+
fullHeight / 2 - scale * (bounds.y + bounds.height / 2)
|
440
|
+
];
|
441
|
+
|
442
|
+
svg.transition()
|
443
|
+
.duration(750)
|
444
|
+
.call(zoom.transform, d3.zoomIdentity
|
445
|
+
.translate(translate[0], translate[1])
|
446
|
+
.scale(scale));
|
447
|
+
});
|
448
|
+
|
449
|
+
// 情報更新
|
450
|
+
d3.select("#node-count").text(graphData.nodes.length);
|
451
|
+
d3.select("#edge-count").text(graphData.links.length);
|
452
|
+
</script>
|
453
|
+
</body>
|
454
|
+
</html>
|
455
|
+
HTML
|
456
|
+
|
457
|
+
html_content
|
458
|
+
end
|
459
|
+
|
460
|
+
private
|
461
|
+
|
462
|
+
# Generates the graph data structure for D3.js consumption
|
463
|
+
#
|
464
|
+
# @return [Hash] Graph data with nodes and links arrays
|
465
|
+
# @note Node IDs and names are NOT HTML-escaped as they will be used in JavaScript
|
466
|
+
def generate_graph_data
|
467
|
+
nodes = @graph.nodes.values.map do |node|
|
468
|
+
{
|
469
|
+
id: node.id,
|
470
|
+
name: node.name,
|
471
|
+
type: node.type.to_s,
|
472
|
+
attributes: node.attributes
|
473
|
+
}
|
474
|
+
end
|
475
|
+
|
476
|
+
links = @graph.edges.map do |edge|
|
477
|
+
{
|
478
|
+
source: edge.from,
|
479
|
+
target: edge.to,
|
480
|
+
type: edge.type.to_s,
|
481
|
+
label: edge.label
|
482
|
+
}
|
483
|
+
end
|
484
|
+
|
485
|
+
{ nodes: nodes, links: links }
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module RailsFlowMap
|
2
|
+
class ErdFormatter
|
3
|
+
def initialize(graph)
|
4
|
+
@graph = graph
|
5
|
+
end
|
6
|
+
|
7
|
+
def format(graph = @graph)
|
8
|
+
output = []
|
9
|
+
output << "# Entity Relationship Diagram\n"
|
10
|
+
output << "```"
|
11
|
+
|
12
|
+
# モデルのみを抽出
|
13
|
+
models = graph.nodes_by_type(:model)
|
14
|
+
|
15
|
+
models.each do |model|
|
16
|
+
output << format_model_box(model)
|
17
|
+
output << ""
|
18
|
+
end
|
19
|
+
|
20
|
+
# 関係性を表示
|
21
|
+
output << "## Relationships\n"
|
22
|
+
graph.edges_by_type(:belongs_to).each do |edge|
|
23
|
+
from_model = graph.find_node(edge.from)
|
24
|
+
to_model = graph.find_node(edge.to)
|
25
|
+
output << "#{from_model.name} belongs_to #{to_model.name}"
|
26
|
+
end
|
27
|
+
|
28
|
+
graph.edges_by_type(:has_many).each do |edge|
|
29
|
+
from_model = graph.find_node(edge.from)
|
30
|
+
to_model = graph.find_node(edge.to)
|
31
|
+
output << "#{from_model.name} has_many #{to_model.name}"
|
32
|
+
end
|
33
|
+
|
34
|
+
output << "```"
|
35
|
+
output.join("\n")
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def format_model_box(model)
|
41
|
+
lines = []
|
42
|
+
lines << "┌─────────────────────┐"
|
43
|
+
lines << "│ #{model.name.center(19)} │"
|
44
|
+
lines << "├─────────────────────┤"
|
45
|
+
|
46
|
+
# 仮のカラム情報(実際の実装では、モデルから取得)
|
47
|
+
lines << "│ id :integer │"
|
48
|
+
lines << "│ created_at :datetime│"
|
49
|
+
lines << "│ updated_at :datetime│"
|
50
|
+
|
51
|
+
if model.attributes[:associations]
|
52
|
+
model.attributes[:associations].each do |assoc|
|
53
|
+
if assoc.include?("belongs_to")
|
54
|
+
foreign_key = assoc.split(" ").last.downcase + "_id"
|
55
|
+
lines << "│ #{foreign_key.ljust(10)} :integer │"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
lines << "└─────────────────────┘"
|
61
|
+
lines.join("\n")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|