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.
@@ -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
+ '<' => '&lt;',
83
+ '>' => '&gt;',
84
+ '{' => '&#123;',
85
+ '}' => '&#125;',
86
+ '"' => '&quot;',
87
+ '|' => '&#124;'
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