activerecord_callback_lens 0.4.1 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fdca61507b8d4d6d6a15a92622bbb38c90a42c1f95cde0d9660603e96556ce23
4
- data.tar.gz: 642c585e532bd018e36ebab4471072f387a4423f0ce418153b48d9006ed5153b
3
+ metadata.gz: 4135903a860b81ecc58cade60bf40079716b57147a5ed5103385efb8888deabc
4
+ data.tar.gz: 76ef506c8897d02301f1fb6aa629c4364c5041f452e75f0547cb9085d3bc8883
5
5
  SHA512:
6
- metadata.gz: a3d3a39ed781d2f176f621a9d18d86e944da7e08b5da3cf54957f7574fcfe45f4f428156e7c69e3e5b6760e5e28ab2d822a9bddabddb5adcbb8a9cb7b4a2bcd3
7
- data.tar.gz: 3d15531570d96609a40e6ae0e7f8f3ea02d9860326c01c3a643de542b3e5545fdc6ab77178221208dd195555754e140d0f68ce06f8564947dc279caf8e23d31f
6
+ metadata.gz: bbd09f9d42c5a00a442cb3eb798eec0e07ecd6e68402f167310f171d1b1551a29704bc365fedc9d5f96598284c2703ae42c6a72151446d6342a6d62afc42dc71
7
+ data.tar.gz: a21cac7359e0e8939b642bca3f45630bbd66b586796051916902255b976f90336316268f231ec6f71560f3f6241b4a25078d8d7e50a80c3a320eb69c7d52bfaa
@@ -48,7 +48,7 @@ module ActiverecordCallbackLens
48
48
  def analyze(model_name)
49
49
  model_class = resolve_model(model_name)
50
50
  definitions, graph = build_pipeline(model_class, expand: options[:expand])
51
- render_outputs(graph, definitions)
51
+ render_outputs(graph, definitions, expand: options[:expand])
52
52
  end
53
53
 
54
54
  private
@@ -58,11 +58,12 @@ module ActiverecordCallbackLens
58
58
  #
59
59
  # @param graph [Graph::Graph]
60
60
  # @param definitions [Array<Collector::CallbackDefinition>]
61
+ # @param expand [Boolean]
61
62
  # @return [void]
62
- def render_outputs(graph, definitions)
63
- puts Renderer::MermaidRenderer.render(graph) if options[:mermaid]
64
- puts Renderer::GraphvizRenderer.render(graph) if options[:graphviz]
65
- write_html(graph, definitions, options[:html]) if options[:html]
63
+ def render_outputs(graph, definitions, expand:)
64
+ puts Renderer::MermaidRenderer.render(graph, expand: expand) if options[:mermaid]
65
+ puts Renderer::GraphvizRenderer.render(graph, expand: expand) if options[:graphviz]
66
+ write_html(graph, definitions, options[:html], expand: expand) if options[:html]
66
67
  end
67
68
 
68
69
  # Resolves a model class by name, printing a friendly error and exiting
@@ -105,9 +106,10 @@ module ActiverecordCallbackLens
105
106
  # @param graph [Graph::Graph]
106
107
  # @param definitions [Array<Collector::CallbackDefinition>]
107
108
  # @param path [String]
109
+ # @param expand [Boolean]
108
110
  # @return [void]
109
- def write_html(graph, definitions, path)
110
- html = Renderer::HtmlRenderer.render(graph, definitions: definitions)
111
+ def write_html(graph, definitions, path, expand: false)
112
+ html = Renderer::HtmlRenderer.render(graph, definitions: definitions, expand: expand)
111
113
  File.write(path, html)
112
114
  puts "HTML report written to #{path}"
113
115
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "prism"
4
+
3
5
  module ActiverecordCallbackLens
4
6
  module Collector
5
7
  # Represents a single callback registered on an ActiveRecord model.
@@ -17,5 +19,111 @@ module ActiverecordCallbackLens
17
19
  :condition_tree, # ConditionTree::Node | nil — parsed logical tree
18
20
  :source_location # [String, Integer] | nil — file and line of the filter
19
21
  )
22
+
23
+ # Reopened to add the shared formatting behaviour every renderer needs: the
24
+ # callback's lifecycle name and a human-readable label for its filter. Keeping
25
+ # this on the domain object (rather than duplicating it across the Mermaid,
26
+ # Graphviz, and HTML renderers) guarantees identical output everywhere and
27
+ # isolates the one I/O-heavy path (proc source slicing) for focused testing.
28
+ class CallbackDefinition
29
+ # The callback's lifecycle name, e.g. "before_save". Centralises the string
30
+ # the renderers previously each rebuilt from phase + event.
31
+ #
32
+ # @return [String]
33
+ def callback_name
34
+ "#{phase}_#{event}"
35
+ end
36
+
37
+ # A human-readable label for the filter (Symbol, String, or Proc).
38
+ #
39
+ # Symbol/String filters render as their own text. A Proc renders as
40
+ # "(proc)" by default; when +expand+ is true it renders its actual source
41
+ # snippet (e.g. "-> { compute_reading_time }"), falling back to "(proc)" on
42
+ # any I/O or parse error.
43
+ #
44
+ # @param expand [Boolean]
45
+ # @return [String]
46
+ def filter_label(expand: false)
47
+ case filter
48
+ when Proc
49
+ return "(proc)" unless expand
50
+
51
+ proc_source || "(proc)"
52
+ else
53
+ filter.to_s
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # Extracts the Proc's raw source snippet by reading its source file with
60
+ # Prism and slicing the enclosing lambda/block node, reusing the same
61
+ # technique as ConditionParser#parse_proc. Returns nil on any error
62
+ # (nil source_location, missing/unreadable file, parse failure, slice
63
+ # error), so #filter_label falls back to "(proc)".
64
+ #
65
+ # @return [String, nil] the single-line source snippet, or nil on any error
66
+ def proc_source
67
+ file, line = filter.source_location
68
+ return nil unless file && File.exist?(file)
69
+
70
+ result = Prism.parse_file(file)
71
+ return nil unless result.success?
72
+
73
+ locator = ProcNodeLocator.new(target_line: line)
74
+ locator.visit(result.value)
75
+ node = locator.node
76
+ return nil if node.nil?
77
+
78
+ node.location.slice.strip.gsub(/\s*\n\s*/, " ")
79
+ rescue StandardError
80
+ nil
81
+ end
82
+
83
+ # A Prism visitor that captures the innermost LambdaNode or BlockNode whose
84
+ # source range encloses a target line — the *whole* node (so its
85
+ # location.slice yields the full "-> { ... }" / "{ ... }" text), unlike
86
+ # ConditionParser::LambdaLocator which captures only the node body.
87
+ class ProcNodeLocator < Prism::Visitor
88
+ # @return [Prism::Node, nil] the innermost matching lambda/block node
89
+ attr_reader :node
90
+
91
+ # @param target_line [Integer] the line the proc's source_location reports
92
+ def initialize(target_line:)
93
+ @target_line = target_line
94
+ @node = nil
95
+ super()
96
+ end
97
+
98
+ # @param lambda_node [Prism::LambdaNode]
99
+ # @return [void]
100
+ def visit_lambda_node(lambda_node)
101
+ capture(lambda_node)
102
+ super
103
+ end
104
+
105
+ # @param block_node [Prism::BlockNode]
106
+ # @return [void]
107
+ def visit_block_node(block_node)
108
+ capture(block_node)
109
+ super
110
+ end
111
+
112
+ private
113
+
114
+ # Records the candidate when it encloses the target line. Because the
115
+ # visitor descends depth-first, the last (innermost) enclosing match
116
+ # wins, isolating nested blocks correctly.
117
+ #
118
+ # @param candidate [Prism::LambdaNode, Prism::BlockNode]
119
+ # @return [void]
120
+ def capture(candidate)
121
+ location = candidate.location
122
+ return unless @target_line.between?(location.start_line, location.end_line)
123
+
124
+ @node = candidate
125
+ end
126
+ end
127
+ end
20
128
  end
21
129
  end
@@ -21,14 +21,17 @@ module ActiverecordCallbackLens
21
21
  # `\"` so they cannot break the surrounding DOT label syntax.
22
22
  class GraphvizRenderer
23
23
  # @param graph [Graph::Graph]
24
+ # @param expand [Boolean] expand proc filter labels to their source snippet
24
25
  # @return [String] DOT language string
25
- def self.render(graph)
26
- new(graph).render
26
+ def self.render(graph, expand: false)
27
+ new(graph, expand: expand).render
27
28
  end
28
29
 
29
30
  # @param graph [Graph::Graph]
30
- def initialize(graph)
31
+ # @param expand [Boolean]
32
+ def initialize(graph, expand: false)
31
33
  @graph = graph
34
+ @expand = expand
32
35
  end
33
36
 
34
37
  # @return [String] DOT language string
@@ -73,7 +76,8 @@ module ActiverecordCallbackLens
73
76
  # @return [String]
74
77
  def node_label(node)
75
78
  case node
76
- when Graph::CallbackNode then "#{node.definition.phase}_#{node.definition.event}"
79
+ when Graph::CallbackNode
80
+ "#{node.definition.callback_name}: #{node.definition.filter_label(expand: @expand)}"
77
81
  when Graph::PredicateNode then node.predicate_name
78
82
  when Graph::MethodNode then node.method_name
79
83
  when Graph::ConditionNode then node.tree_node.class.name.split("::").last
@@ -97,16 +97,19 @@ module ActiverecordCallbackLens
97
97
 
98
98
  # @param graph [Graph::Graph] the assembled dependency graph
99
99
  # @param definitions [Array<Collector::CallbackDefinition>] parsed definitions
100
+ # @param expand [Boolean] expand proc filter labels to their source snippet
100
101
  # @return [String] a complete HTML document
101
- def self.render(graph, definitions:)
102
- new(graph, definitions).render
102
+ def self.render(graph, definitions:, expand: false)
103
+ new(graph, definitions, expand: expand).render
103
104
  end
104
105
 
105
106
  # @param graph [Graph::Graph]
106
107
  # @param definitions [Array<Collector::CallbackDefinition>]
107
- def initialize(graph, definitions)
108
+ # @param expand [Boolean]
109
+ def initialize(graph, definitions, expand: false)
108
110
  @graph = graph
109
111
  @definitions = definitions
112
+ @expand = expand
110
113
  end
111
114
 
112
115
  # Assembles the full HTML document.
@@ -138,7 +141,7 @@ module ActiverecordCallbackLens
138
141
  "<tr>" \
139
142
  "<td>#{escape(definition.phase)}</td>" \
140
143
  "<td>#{escape(definition.event)}</td>" \
141
- "<td>#{escape(filter_label(definition))}</td>" \
144
+ "<td>#{escape(definition.filter_label(expand: @expand))}</td>" \
142
145
  "<td>#{escape(conditions_label(definition))}</td>" \
143
146
  "</tr>"
144
147
  end
@@ -156,7 +159,9 @@ module ActiverecordCallbackLens
156
159
  # Section 2: callbacks in canonical create-path execution order.
157
160
  def execution_flow
158
161
  ordered = ExecutionOrderAnalyzer.sort(@definitions, operation: :create)
159
- items = ordered.map { |d| "<li>#{escape("#{d.phase}_#{d.event}")}</li>" }
162
+ items = ordered.map do |d|
163
+ "<li>#{escape("#{d.callback_name}: #{d.filter_label(expand: @expand)}")}</li>"
164
+ end
160
165
  <<~HTML.chomp
161
166
  <h2>Execution Flow</h2>
162
167
  <ol>
@@ -171,7 +176,7 @@ module ActiverecordCallbackLens
171
176
  trees = @definitions.filter_map do |definition|
172
177
  next if definition.condition_tree.nil?
173
178
 
174
- "<li>#{escape("#{definition.phase}_#{definition.event}")}" \
179
+ "<li>#{escape("#{definition.callback_name}: #{definition.filter_label(expand: @expand)}")}" \
175
180
  "#{ConditionTreeHtml.new(definition.condition_tree).to_html}</li>"
176
181
  end
177
182
  <<~HTML.chomp
@@ -186,14 +191,14 @@ module ActiverecordCallbackLens
186
191
  def mermaid_section
187
192
  <<~HTML.chomp
188
193
  <h2>Mermaid Diagram</h2>
189
- <pre class="mermaid">#{escape(MermaidRenderer.render(@graph))}</pre>
194
+ <pre class="mermaid">#{escape(MermaidRenderer.render(@graph, expand: @expand))}</pre>
190
195
  HTML
191
196
  end
192
197
 
193
198
  # Section 5: the inline Graphviz SVG, or an empty string when +dot+ is not
194
199
  # installed (GraphvizRenderer#to_svg returns nil).
195
200
  def svg_section
196
- svg = GraphvizRenderer.new(@graph).to_svg
201
+ svg = GraphvizRenderer.new(@graph, expand: @expand).to_svg
197
202
  return "" if svg.nil?
198
203
 
199
204
  <<~HTML.chomp
@@ -202,12 +207,6 @@ module ActiverecordCallbackLens
202
207
  HTML
203
208
  end
204
209
 
205
- # A human-readable label for a definition's filter (Symbol, String, or Proc).
206
- def filter_label(definition)
207
- filter = definition.filter
208
- filter.is_a?(Proc) ? "(proc)" : filter.to_s
209
- end
210
-
211
210
  # A comma-joined label of a definition's condition leaf names, or "".
212
211
  def conditions_label(definition)
213
212
  tree = definition.condition_tree
@@ -19,14 +19,17 @@ module ActiverecordCallbackLens
19
19
  # surrounding Mermaid label syntax.
20
20
  class MermaidRenderer
21
21
  # @param graph [Graph::Graph]
22
+ # @param expand [Boolean] expand proc filter labels to their source snippet
22
23
  # @return [String]
23
- def self.render(graph)
24
- new(graph).render
24
+ def self.render(graph, expand: false)
25
+ new(graph, expand: expand).render
25
26
  end
26
27
 
27
28
  # @param graph [Graph::Graph]
28
- def initialize(graph)
29
+ # @param expand [Boolean]
30
+ def initialize(graph, expand: false)
29
31
  @graph = graph
32
+ @expand = expand
30
33
  end
31
34
 
32
35
  # @return [String]
@@ -55,7 +58,8 @@ module ActiverecordCallbackLens
55
58
  # @return [String]
56
59
  def node_label(node)
57
60
  case node
58
- when Graph::CallbackNode then "#{node.definition.phase}_#{node.definition.event}"
61
+ when Graph::CallbackNode
62
+ "#{node.definition.callback_name}: #{node.definition.filter_label(expand: @expand)}"
59
63
  when Graph::PredicateNode then node.predicate_name
60
64
  when Graph::MethodNode then node.method_name
61
65
  when Graph::ConditionNode then node.tree_node.class.name.split("::").last
@@ -43,7 +43,7 @@ module CallbackLensRakeHelpers
43
43
  end
44
44
  end
45
45
  graph = ActiverecordCallbackLens::Graph::GraphBuilder.build(definitions)
46
- ActiverecordCallbackLens::Renderer::MermaidRenderer.render(graph)
46
+ ActiverecordCallbackLens::Renderer::MermaidRenderer.render(graph, expand: expand)
47
47
  end
48
48
 
49
49
  # Runs the full pipeline (collect -> parse -> [expand] -> build -> render) for
@@ -63,7 +63,7 @@ module CallbackLensRakeHelpers
63
63
  end
64
64
  end
65
65
  graph = ActiverecordCallbackLens::Graph::GraphBuilder.build(definitions)
66
- ActiverecordCallbackLens::Renderer::GraphvizRenderer.render(graph)
66
+ ActiverecordCallbackLens::Renderer::GraphvizRenderer.render(graph, expand: expand)
67
67
  end
68
68
 
69
69
  # Runs the full pipeline (collect -> parse -> [expand] -> build) for a model and
@@ -85,7 +85,7 @@ module CallbackLensRakeHelpers
85
85
  end
86
86
  end
87
87
  graph = ActiverecordCallbackLens::Graph::GraphBuilder.build(definitions)
88
- ActiverecordCallbackLens::Renderer::HtmlRenderer.render(graph, definitions: definitions)
88
+ ActiverecordCallbackLens::Renderer::HtmlRenderer.render(graph, definitions: definitions, expand: expand)
89
89
  end
90
90
 
91
91
  # Parses the EXPAND environment variable using the strict truthy rule: only the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiverecordCallbackLens
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord_callback_lens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eraxel.Dev