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,91 @@
|
|
1
|
+
module RailsFlowMap
|
2
|
+
class MermaidFormatter
|
3
|
+
def format(graph)
|
4
|
+
lines = ["graph TD"]
|
5
|
+
|
6
|
+
# Add nodes
|
7
|
+
graph.nodes.each do |id, node|
|
8
|
+
lines << format_node(node)
|
9
|
+
end
|
10
|
+
|
11
|
+
lines << ""
|
12
|
+
|
13
|
+
# Add edges
|
14
|
+
graph.edges.each do |edge|
|
15
|
+
lines << format_edge(edge, graph)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Add styling
|
19
|
+
lines << ""
|
20
|
+
lines << "%% Styling"
|
21
|
+
lines << "classDef model fill:#f9f,stroke:#333,stroke-width:2px;"
|
22
|
+
lines << "classDef controller fill:#bbf,stroke:#333,stroke-width:2px;"
|
23
|
+
lines << "classDef action fill:#bfb,stroke:#333,stroke-width:2px;"
|
24
|
+
|
25
|
+
# Apply styles to nodes
|
26
|
+
model_nodes = graph.nodes_by_type(:model).map(&:id).join(",")
|
27
|
+
controller_nodes = graph.nodes_by_type(:controller).map(&:id).join(",")
|
28
|
+
action_nodes = graph.nodes_by_type(:action).map(&:id).join(",")
|
29
|
+
|
30
|
+
lines << "class #{model_nodes} model;" unless model_nodes.empty?
|
31
|
+
lines << "class #{controller_nodes} controller;" unless controller_nodes.empty?
|
32
|
+
lines << "class #{action_nodes} action;" unless action_nodes.empty?
|
33
|
+
|
34
|
+
lines.join("\n")
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def format_node(node)
|
40
|
+
label = escape_mermaid(node.name)
|
41
|
+
shape = case node.type
|
42
|
+
when :model
|
43
|
+
"#{node.id}[#{label}]"
|
44
|
+
when :controller
|
45
|
+
"#{node.id}[[#{label}]]"
|
46
|
+
when :action
|
47
|
+
"#{node.id}(#{label})"
|
48
|
+
else
|
49
|
+
"#{node.id}[#{label}]"
|
50
|
+
end
|
51
|
+
" #{shape}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def format_edge(edge, graph)
|
55
|
+
from_node = graph.find_node(edge.from)
|
56
|
+
to_node = graph.find_node(edge.to)
|
57
|
+
|
58
|
+
return nil unless from_node && to_node
|
59
|
+
|
60
|
+
arrow = case edge.type
|
61
|
+
when :belongs_to
|
62
|
+
"-->"
|
63
|
+
when :has_one
|
64
|
+
"-->"
|
65
|
+
when :has_many
|
66
|
+
"==>"
|
67
|
+
when :has_and_belongs_to_many
|
68
|
+
"<==>"
|
69
|
+
when :has_action
|
70
|
+
"-.->"
|
71
|
+
else
|
72
|
+
"-->"
|
73
|
+
end
|
74
|
+
|
75
|
+
label = edge.label ? "|#{escape_mermaid(edge.label.to_s)}|" : ""
|
76
|
+
|
77
|
+
" #{edge.from} #{arrow}#{label} #{edge.to}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def escape_mermaid(text)
|
81
|
+
text.gsub(/[<>{}"|]/, {
|
82
|
+
'<' => '<',
|
83
|
+
'>' => '>',
|
84
|
+
'{' => '{',
|
85
|
+
'}' => '}',
|
86
|
+
'"' => '"',
|
87
|
+
'|' => '|'
|
88
|
+
})
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module RailsFlowMap
|
2
|
+
class MetricsFormatter
|
3
|
+
def initialize(graph)
|
4
|
+
@graph = graph
|
5
|
+
end
|
6
|
+
|
7
|
+
def format(graph = @graph)
|
8
|
+
output = []
|
9
|
+
output << "# 📊 Rails Application Metrics Report"
|
10
|
+
output << "Generated at: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
11
|
+
output << "\n" + "=" * 60 + "\n"
|
12
|
+
|
13
|
+
# 概要統計
|
14
|
+
output << "## 📈 Overview Statistics"
|
15
|
+
output << "- Total Nodes: #{graph.node_count}"
|
16
|
+
output << "- Total Edges: #{graph.edge_count}"
|
17
|
+
output << "- Models: #{graph.nodes_by_type(:model).count}"
|
18
|
+
output << "- Controllers: #{graph.nodes_by_type(:controller).count}"
|
19
|
+
output << "- Actions: #{graph.nodes_by_type(:action).count}"
|
20
|
+
output << "- Services: #{graph.nodes_by_type(:service).count}"
|
21
|
+
output << "- Routes: #{graph.nodes_by_type(:route).count}"
|
22
|
+
|
23
|
+
# 複雑度分析
|
24
|
+
output << "\n## 🏆 Complexity Analysis"
|
25
|
+
output << "\n### Most Connected Models (by relationships)"
|
26
|
+
model_complexity = calculate_node_complexity(graph, :model)
|
27
|
+
model_complexity.first(5).each_with_index do |(node, score), index|
|
28
|
+
output << "#{index + 1}. #{node.name} (connections: #{score})"
|
29
|
+
end
|
30
|
+
|
31
|
+
output << "\n### Most Complex Controllers (by actions)"
|
32
|
+
controller_actions = {}
|
33
|
+
graph.nodes_by_type(:controller).each do |controller|
|
34
|
+
action_count = graph.edges.count { |e| e.from == controller.id && e.type == :has_action }
|
35
|
+
controller_actions[controller] = action_count
|
36
|
+
end
|
37
|
+
controller_actions.sort_by { |_, count| -count }.first(5).each_with_index do |(controller, count), index|
|
38
|
+
output << "#{index + 1}. #{controller.name} (actions: #{count})"
|
39
|
+
end
|
40
|
+
|
41
|
+
# 依存関係分析
|
42
|
+
output << "\n## 🔗 Dependency Analysis"
|
43
|
+
output << "\n### Models with Most Dependencies"
|
44
|
+
model_dependencies = calculate_dependencies(graph, :model)
|
45
|
+
model_dependencies.first(5).each_with_index do |(node, deps), index|
|
46
|
+
output << "#{index + 1}. #{node.name}"
|
47
|
+
output << " - Outgoing: #{deps[:outgoing]} (#{deps[:outgoing_types].join(', ')})"
|
48
|
+
output << " - Incoming: #{deps[:incoming]} (#{deps[:incoming_types].join(', ')})"
|
49
|
+
end
|
50
|
+
|
51
|
+
# サービス層分析
|
52
|
+
output << "\n## 🛠️ Service Layer Analysis"
|
53
|
+
services = graph.nodes_by_type(:service)
|
54
|
+
if services.any?
|
55
|
+
output << "- Total Services: #{services.count}"
|
56
|
+
output << "- Services per Controller: #{(services.count.to_f / graph.nodes_by_type(:controller).count).round(2)}"
|
57
|
+
|
58
|
+
output << "\n### Most Used Services"
|
59
|
+
service_usage = calculate_service_usage(graph)
|
60
|
+
service_usage.first(5).each_with_index do |(service, count), index|
|
61
|
+
output << "#{index + 1}. #{service.name} (used by #{count} actions)"
|
62
|
+
end
|
63
|
+
else
|
64
|
+
output << "- No service layer detected"
|
65
|
+
end
|
66
|
+
|
67
|
+
# 潜在的な問題
|
68
|
+
output << "\n## ⚠️ Potential Issues"
|
69
|
+
|
70
|
+
# 循環依存のチェック
|
71
|
+
circular = detect_circular_dependencies(graph)
|
72
|
+
if circular.any?
|
73
|
+
output << "\n### Circular Dependencies Detected: #{circular.count}"
|
74
|
+
circular.each do |cycle|
|
75
|
+
output << "- #{cycle.join(' → ')}"
|
76
|
+
end
|
77
|
+
else
|
78
|
+
output << "\n### ✅ No circular dependencies detected"
|
79
|
+
end
|
80
|
+
|
81
|
+
# God オブジェクトの検出
|
82
|
+
output << "\n### Potential God Objects (high connectivity)"
|
83
|
+
god_objects = model_complexity.select { |_, score| score > 10 }
|
84
|
+
if god_objects.any?
|
85
|
+
god_objects.each do |(node, score)|
|
86
|
+
output << "- ⚠️ #{node.name} has #{score} connections"
|
87
|
+
end
|
88
|
+
else
|
89
|
+
output << "- ✅ No god objects detected"
|
90
|
+
end
|
91
|
+
|
92
|
+
# 推奨事項
|
93
|
+
output << "\n## 💡 Recommendations"
|
94
|
+
|
95
|
+
if model_complexity.first[1] > 15
|
96
|
+
output << "- Consider breaking down #{model_complexity.first[0].name} - it has too many relationships"
|
97
|
+
end
|
98
|
+
|
99
|
+
if services.empty? && graph.nodes_by_type(:controller).count > 5
|
100
|
+
output << "- Consider implementing a service layer to separate business logic"
|
101
|
+
end
|
102
|
+
|
103
|
+
fat_controllers = controller_actions.select { |_, count| count > 7 }
|
104
|
+
if fat_controllers.any?
|
105
|
+
output << "- These controllers have too many actions: #{fat_controllers.keys.map(&:name).join(', ')}"
|
106
|
+
output << " Consider splitting into multiple controllers or using namespaces"
|
107
|
+
end
|
108
|
+
|
109
|
+
output.join("\n")
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def calculate_node_complexity(graph, type)
|
115
|
+
complexity = {}
|
116
|
+
graph.nodes_by_type(type).each do |node|
|
117
|
+
incoming = graph.edges.count { |e| e.to == node.id }
|
118
|
+
outgoing = graph.edges.count { |e| e.from == node.id }
|
119
|
+
complexity[node] = incoming + outgoing
|
120
|
+
end
|
121
|
+
complexity.sort_by { |_, score| -score }
|
122
|
+
end
|
123
|
+
|
124
|
+
def calculate_dependencies(graph, type)
|
125
|
+
dependencies = {}
|
126
|
+
graph.nodes_by_type(type).each do |node|
|
127
|
+
outgoing_edges = graph.edges.select { |e| e.from == node.id }
|
128
|
+
incoming_edges = graph.edges.select { |e| e.to == node.id }
|
129
|
+
|
130
|
+
dependencies[node] = {
|
131
|
+
outgoing: outgoing_edges.count,
|
132
|
+
outgoing_types: outgoing_edges.map(&:type).uniq,
|
133
|
+
incoming: incoming_edges.count,
|
134
|
+
incoming_types: incoming_edges.map(&:type).uniq
|
135
|
+
}
|
136
|
+
end
|
137
|
+
dependencies.sort_by { |_, deps| -(deps[:outgoing] + deps[:incoming]) }
|
138
|
+
end
|
139
|
+
|
140
|
+
def calculate_service_usage(graph)
|
141
|
+
usage = {}
|
142
|
+
graph.nodes_by_type(:service).each do |service|
|
143
|
+
calling_edges = graph.edges.select { |e| e.to == service.id && e.type == :calls_service }
|
144
|
+
usage[service] = calling_edges.count
|
145
|
+
end
|
146
|
+
usage.sort_by { |_, count| -count }
|
147
|
+
end
|
148
|
+
|
149
|
+
def detect_circular_dependencies(graph)
|
150
|
+
cycles = []
|
151
|
+
visited = Set.new
|
152
|
+
rec_stack = Set.new
|
153
|
+
|
154
|
+
graph.nodes.each do |id, node|
|
155
|
+
if !visited.include?(id)
|
156
|
+
path = []
|
157
|
+
if has_cycle?(graph, id, visited, rec_stack, path, cycles)
|
158
|
+
# cycle found and added to cycles
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
cycles
|
164
|
+
end
|
165
|
+
|
166
|
+
def has_cycle?(graph, node_id, visited, rec_stack, path, cycles)
|
167
|
+
visited.add(node_id)
|
168
|
+
rec_stack.add(node_id)
|
169
|
+
path.push(node_id)
|
170
|
+
|
171
|
+
edges = graph.edges.select { |e| e.from == node_id }
|
172
|
+
edges.each do |edge|
|
173
|
+
if !visited.include?(edge.to)
|
174
|
+
if has_cycle?(graph, edge.to, visited, rec_stack, path, cycles)
|
175
|
+
return true
|
176
|
+
end
|
177
|
+
elsif rec_stack.include?(edge.to)
|
178
|
+
# Found cycle
|
179
|
+
cycle_start = path.index(edge.to)
|
180
|
+
if cycle_start
|
181
|
+
cycle = path[cycle_start..-1].map { |id|
|
182
|
+
node = graph.find_node(id)
|
183
|
+
node ? node.name : id
|
184
|
+
}
|
185
|
+
cycles << cycle if cycle.any?
|
186
|
+
end
|
187
|
+
return true
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
path.pop
|
192
|
+
rec_stack.delete(node_id)
|
193
|
+
false
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|