activerecord_callback_lens 0.3.0 → 0.4.1

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: 93034e63e1e99dfe1e6ea64193460d4a206bdf88a01e1c6606385e48228e292c
4
- data.tar.gz: d5fae7891b074b7459d7127d9b34befbf8345bd6ee27cef938e0cf0215adddc5
3
+ metadata.gz: fdca61507b8d4d6d6a15a92622bbb38c90a42c1f95cde0d9660603e96556ce23
4
+ data.tar.gz: 642c585e532bd018e36ebab4471072f387a4423f0ce418153b48d9006ed5153b
5
5
  SHA512:
6
- metadata.gz: 9a29d788fe927d7c24b82e70965219b3dfdebdd04a6fa36d1b05d0b968f9265a4b280a0e5e8f67436681cd71a80cbddc0ee993f1fe76befcc3bd698aa7371daf
7
- data.tar.gz: 0dfa3cc6f1ebac81b1b8469ce70a1a35482ff948bab5462d3ed777d64843ee948dbeea89fd208d32b8efa24fe54f91a59ff50b7116b1d7fdc25141e9e3b1ba8e
6
+ metadata.gz: a3d3a39ed781d2f176f621a9d18d86e944da7e08b5da3cf54957f7574fcfe45f4f428156e7c69e3e5b6760e5e28ab2d822a9bddabddb5adcbb8a9cb7b4a2bcd3
7
+ data.tar.gz: 3d15531570d96609a40e6ae0e7f8f3ea02d9860326c01c3a643de542b3e5545fdc6ab77178221208dd195555754e140d0f68ce06f8564947dc279caf8e23d31f
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **X-ray your ActiveRecord callbacks.**
4
4
 
5
- Callbacks are easy to add and hard to reason about. `activerecord_callback_lens` statically analyzes the callbacks registered on your ActiveRecord models, parses their `if`/`unless` conditions into logical trees, and renders the result as a Mermaid diagram so you can see exactly what runs and why.
5
+ Callbacks are easy to add and hard to reason about. `activerecord_callback_lens` statically analyzes the callbacks registered on your ActiveRecord models, parses their `if`/`unless` conditions into logical trees, and renders the result as a Mermaid diagram, Graphviz DOT graph, or a self-contained HTML report so you can see exactly what runs and why.
6
6
 
7
7
  ## Requirements
8
8
 
@@ -99,6 +99,37 @@ rake callback_lens:graphviz MODEL=User EXPAND=true
99
99
 
100
100
  Requires [Graphviz](https://graphviz.org/download/) only when piping to `dot`.
101
101
 
102
+ #### HTML report
103
+
104
+ Pass `--html FILE` (CLI) or use `callback_lens:html` (Rake) to generate a
105
+ self-contained HTML report. The file embeds a Mermaid diagram (via CDN), an
106
+ inline Graphviz SVG (when `dot` is installed), a callback list table, an
107
+ execution-order flow, and a nested dependency tree for every condition — no
108
+ external dependencies at view time.
109
+
110
+ ```bash
111
+ # CLI — write report to a file
112
+ callback_lens analyze User --html report.html
113
+
114
+ # CLI — combine with stdout output
115
+ callback_lens analyze User --mermaid --html report.html
116
+
117
+ # Rake — default output filename: callback_lens_report.html
118
+ rake callback_lens:html MODEL=User
119
+
120
+ # Rake — custom output path
121
+ rake callback_lens:html MODEL=User OUT=tmp/user_callbacks.html
122
+
123
+ # Rake — with recursive method expansion
124
+ rake callback_lens:html MODEL=User OUT=tmp/user_callbacks.html EXPAND=true
125
+ ```
126
+
127
+ | ENV variable | Required | Default | Description |
128
+ |---|---|---|---|
129
+ | `MODEL` | Yes | — | ActiveRecord model class name |
130
+ | `OUT` | No | `callback_lens_report.html` | Output path for the HTML file |
131
+ | `EXPAND` | No | `false` | Set to `true` to recursively expand method conditions |
132
+
102
133
  ### Programmatic API
103
134
 
104
135
  ```ruby
@@ -117,14 +148,37 @@ graph = ActiverecordCallbackLens::Graph::GraphBuilder.build(definitions)
117
148
  puts ActiverecordCallbackLens::Renderer::MermaidRenderer.render(graph)
118
149
  ```
119
150
 
151
+ ## Example app
152
+
153
+ A runnable example Rails app lives in [`examples/blog_app/`](examples/blog_app/).
154
+ It defines a small blog domain (`Article`, `User`, `Comment`) with callbacks
155
+ covering every lifecycle event and condition style, and points its `Gemfile` at
156
+ this repo via `path: "../.."` so it always exercises the current gem code:
157
+
158
+ ```bash
159
+ cd examples/blog_app
160
+ bundle install
161
+ bin/rails callback_lens:analyze MODEL=Article # Mermaid diagram
162
+ bin/rails callback_lens:analyze MODEL=Article EXPAND=true # expanded predicates
163
+ bin/rails callback_lens:html MODEL=Article OUT=reports/article.html
164
+ bin/callback_lens_demo # programmatic API demo
165
+ ```
166
+
167
+ See [`examples/blog_app/README.md`](examples/blog_app/README.md) for the full
168
+ command matrix and expected output.
169
+
120
170
  ## How it works
121
171
 
122
172
  | Layer | Class | Responsibility |
123
173
  |---|---|---|
124
174
  | Collector | `CallbackCollector` | Reads ActiveRecord's internal `_save_callbacks`, `_create_callbacks`, `_update_callbacks`, `_destroy_callbacks`, and `_validation_callbacks` chains |
125
175
  | Parser | `ConditionParser` | Uses [Prism](https://github.com/ruby/prism) to parse `Proc`/`Lambda` conditions into `AndNode`/`OrNode`/`NotNode`/`PredicateNode` trees; `Symbol` conditions become `MethodRefNode` stubs |
176
+ | Resolver | `MethodResolver` | Recursively expands `MethodRefNode` symbols into full `ConditionTree` sub-trees (up to 5 levels deep) with cycle detection |
126
177
  | Graph | `GraphBuilder` | Assembles a DAG of `CallbackNode`, `ConditionNode`, `PredicateNode`, and `MethodNode` values |
178
+ | Analyzer | `ExecutionOrderAnalyzer` | Sorts definitions into canonical Rails execution order (create or update path) |
127
179
  | Renderer | `MermaidRenderer` | Serializes the graph to a Mermaid `graph TD` string |
180
+ | Renderer | `GraphvizRenderer` | Serializes the graph to a Graphviz DOT string; `#to_svg` shells out to `dot` |
181
+ | Renderer | `HtmlRenderer` | Produces a self-contained HTML report embedding Mermaid, SVG, callback table, execution flow, and dependency tree |
128
182
 
129
183
  ## Condition tree nodes
130
184
 
@@ -141,9 +195,9 @@ puts ActiverecordCallbackLens::Renderer::MermaidRenderer.render(graph)
141
195
  | Version | Feature |
142
196
  |---|---|
143
197
  | v0.1 | CallbackCollector, Prism condition parser, Mermaid renderer, CLI, Rake task |
144
- | **v0.2** | MethodResolver — recursive expansion of Symbol conditions (`--expand` / `EXPAND=true`) |
145
- | **v0.3** | Graphviz / DOT renderer (`--graphviz` / `callback_lens:graphviz`) |
146
- | v0.4 | HTML report (callback list, execution flow, embedded diagram) |
198
+ | v0.2 | MethodResolver — recursive expansion of Symbol conditions (`--expand` / `EXPAND=true`) |
199
+ | v0.3 | Graphviz / DOT renderer (`--graphviz` / `callback_lens:graphviz`) |
200
+ | **v0.4** | HTML report callback list table, execution-order flow, dependency tree, embedded Mermaid and Graphviz SVG (`--html` / `callback_lens:html`) |
147
201
  | v1.0 | Runtime tracer via `ActiveSupport::Notifications` |
148
202
  | v2.0 | RBS analysis, cross-model dependency graph |
149
203
 
@@ -8,6 +8,7 @@ require_relative "../resolver/method_resolver"
8
8
  require_relative "../graph/graph_builder"
9
9
  require_relative "../renderer/mermaid_renderer"
10
10
  require_relative "../renderer/graphviz_renderer"
11
+ require_relative "../renderer/html_renderer"
11
12
 
12
13
  module ActiverecordCallbackLens
13
14
  module CLI
@@ -35,19 +36,35 @@ module ActiverecordCallbackLens
35
36
  desc: "Output DOT graph via Graphviz to stdout"
36
37
  option :expand, type: :boolean, default: false,
37
38
  desc: "Expand method conditions recursively (up to depth 5)"
39
+ option :html, type: :string, desc: "Write HTML report to FILE"
38
40
  # Runs the analysis pipeline for +model_name+ and prints the result.
39
41
  #
42
+ # When +--html FILE+ is given, a self-contained HTML report is written to
43
+ # FILE (in addition to any stdout output selected by the other flags) and a
44
+ # confirmation line is printed.
45
+ #
40
46
  # @param model_name [String] the ActiveRecord model class name
41
47
  # @return [void]
42
48
  def analyze(model_name)
43
49
  model_class = resolve_model(model_name)
44
- graph = build_graph(model_class, expand: options[:expand])
45
- puts Renderer::MermaidRenderer.render(graph) if options[:mermaid]
46
- puts Renderer::GraphvizRenderer.render(graph) if options[:graphviz]
50
+ definitions, graph = build_pipeline(model_class, expand: options[:expand])
51
+ render_outputs(graph, definitions)
47
52
  end
48
53
 
49
54
  private
50
55
 
56
+ # Emits each output selected by the analyze options: Mermaid and/or Graphviz
57
+ # to stdout, and an HTML report to a file when +--html+ is given.
58
+ #
59
+ # @param graph [Graph::Graph]
60
+ # @param definitions [Array<Collector::CallbackDefinition>]
61
+ # @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]
66
+ end
67
+
51
68
  # Resolves a model class by name, printing a friendly error and exiting
52
69
  # non-zero when the constant cannot be found.
53
70
  #
@@ -67,16 +84,32 @@ module ActiverecordCallbackLens
67
84
  # MethodRefNodes resolved into ConditionTree sub-trees via MethodResolver.
68
85
  # When false, the pipeline is identical to v0.1 output.
69
86
  #
87
+ # Returns both the parsed definitions and the assembled graph so renderers
88
+ # that need the definitions directly (e.g. the HTML report) can access them
89
+ # without re-running the pipeline.
90
+ #
70
91
  # @param model_class [Class]
71
92
  # @param expand [Boolean]
72
- # @return [Graph::Graph]
73
- def build_graph(model_class, expand: false)
93
+ # @return [Array(Array<Collector::CallbackDefinition>, Graph::Graph)]
94
+ def build_pipeline(model_class, expand: false)
74
95
  definitions = Collector::CallbackCollector.collect(model_class)
75
96
  definitions = definitions.map { |definition| Parser::ConditionParser.parse(definition) }
76
97
  if expand
77
98
  definitions = definitions.map { |definition| Resolver::MethodResolver.expand(definition, model_class) }
78
99
  end
79
- Graph::GraphBuilder.build(definitions)
100
+ [definitions, Graph::GraphBuilder.build(definitions)]
101
+ end
102
+
103
+ # Writes the HTML report to +path+ and prints a confirmation line.
104
+ #
105
+ # @param graph [Graph::Graph]
106
+ # @param definitions [Array<Collector::CallbackDefinition>]
107
+ # @param path [String]
108
+ # @return [void]
109
+ def write_html(graph, definitions, path)
110
+ html = Renderer::HtmlRenderer.render(graph, definitions: definitions)
111
+ File.write(path, html)
112
+ puts "HTML report written to #{path}"
80
113
  end
81
114
  end
82
115
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiverecordCallbackLens
4
+ # Sorts a list of Collector::CallbackDefinition objects into the canonical
5
+ # order in which Rails executes callbacks on a record save.
6
+ #
7
+ # Two orderings are supported, selected via the +operation+ keyword:
8
+ #
9
+ # * +:create+ (the default) follows the create path
10
+ # (before_create / after_create around the INSERT).
11
+ # * +:update+ follows the update path
12
+ # (before_update / after_update around the UPDATE).
13
+ #
14
+ # A definition's position is derived from its +phase_event+ pair (for example
15
+ # +before_save+). Callbacks whose pair is not part of the canonical order are
16
+ # placed at the end (sort index 999) while preserving their relative order,
17
+ # because Ruby's Array#sort_by is stable for equal keys.
18
+ class ExecutionOrderAnalyzer
19
+ # Canonical create-path order. Mirrors spec section 7.1 (create path).
20
+ SAVE_ORDER = %i[
21
+ before_validation after_validation
22
+ before_save
23
+ before_create
24
+ after_create
25
+ after_save
26
+ after_commit
27
+ ].freeze
28
+
29
+ # Canonical update-path order. Mirrors spec section 7.1 (update path).
30
+ UPDATE_ORDER = %i[
31
+ before_validation after_validation
32
+ before_save
33
+ before_update
34
+ after_update
35
+ after_save
36
+ after_commit
37
+ ].freeze
38
+
39
+ # Sort index assigned to callbacks whose phase_event pair is not part of the
40
+ # canonical order, so they sort after every recognised callback.
41
+ UNRECOGNISED_INDEX = 999
42
+
43
+ # Sorts +definitions+ into canonical execution order.
44
+ #
45
+ # @param definitions [Array<Collector::CallbackDefinition>]
46
+ # @param operation [Symbol] :create (default) or :update
47
+ # @return [Array<Collector::CallbackDefinition>] a new sorted array
48
+ def self.sort(definitions, operation: :create)
49
+ order = operation == :update ? UPDATE_ORDER : SAVE_ORDER
50
+ definitions.sort_by do |definition|
51
+ key = :"#{definition.phase}_#{definition.event}"
52
+ order.index(key) || UNRECOGNISED_INDEX
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ require_relative "mermaid_renderer"
6
+ require_relative "graphviz_renderer"
7
+ require_relative "../execution_order_analyzer"
8
+ require_relative "../parser/condition_tree"
9
+
10
+ module ActiverecordCallbackLens
11
+ module Renderer
12
+ # Renders a single ConditionTree node (and its descendants) as nested +<ul>+
13
+ # markup, and exposes the flat list of leaf condition names. Extracted from
14
+ # HtmlRenderer so the recursive tree handling is a single, testable
15
+ # responsibility separate from page assembly.
16
+ #
17
+ # All names are HTML escaped via CGI.escapeHTML so values containing +<+,
18
+ # +>+ or +&+ cannot break the surrounding markup.
19
+ class ConditionTreeHtml
20
+ Tree = Parser::ConditionTree
21
+
22
+ # @param node [Parser::ConditionTree::Node]
23
+ def initialize(node)
24
+ @node = node
25
+ end
26
+
27
+ # @return [String] nested <ul> markup for the node
28
+ def to_html
29
+ render(@node)
30
+ end
31
+
32
+ # @return [Array<String>] leaf names (predicate + method refs), depth-first
33
+ def names
34
+ leaf_names(@node)
35
+ end
36
+
37
+ private
38
+
39
+ def render(node)
40
+ case node
41
+ when Tree::AndNode then combinator("AND", node.children)
42
+ when Tree::OrNode then combinator("OR", node.children)
43
+ when Tree::NotNode then combinator("NOT", [node.child])
44
+ when Tree::PredicateNode then "<ul><li>#{escape(node.name)}</li></ul>"
45
+ when Tree::MethodRefNode then method_ref(node)
46
+ else ""
47
+ end
48
+ end
49
+
50
+ def combinator(label, children)
51
+ items = children.map { |child| "<li>#{render(child)}</li>" }
52
+ "<ul><li>#{escape(label)}</li>#{items.join}</ul>"
53
+ end
54
+
55
+ def method_ref(node)
56
+ inner = node.expanded_tree.nil? ? "" : render(node.expanded_tree)
57
+ "<ul><li>#{escape(node.name)}#{inner}</li></ul>"
58
+ end
59
+
60
+ def leaf_names(node)
61
+ case node
62
+ when Tree::AndNode, Tree::OrNode
63
+ node.children.flat_map { |child| leaf_names(child) }
64
+ when Tree::NotNode
65
+ leaf_names(node.child)
66
+ when Tree::PredicateNode, Tree::MethodRefNode
67
+ [node.name]
68
+ else
69
+ []
70
+ end
71
+ end
72
+
73
+ def escape(value)
74
+ CGI.escapeHTML(value.to_s)
75
+ end
76
+ end
77
+
78
+ # Renders a self-contained HTML report for a model's callbacks.
79
+ #
80
+ # The document embeds five sections (spec section 8.3):
81
+ #
82
+ # 1. Callback List — a table with columns Phase, Event, Filter, Conditions.
83
+ # 2. Execution Flow — an ordered list sorted by ExecutionOrderAnalyzer
84
+ # (the +:create+ path).
85
+ # 3. Dependency Tree — a nested +<ul>+ built from each definition's
86
+ # ConditionTree (via ConditionTreeHtml).
87
+ # 4. Mermaid Diagram — a +<pre class="mermaid">+ block populated by
88
+ # MermaidRenderer and rendered client-side via the Mermaid CDN script.
89
+ # 5. Graphviz SVG — the inline +<svg>+ produced by GraphvizRenderer#to_svg
90
+ # when the +dot+ binary is available; the section is omitted otherwise.
91
+ #
92
+ # All user-derived text is HTML escaped with CGI.escapeHTML.
93
+ class HtmlRenderer
94
+ # The Mermaid.js bundle loaded from a CDN so the report stays a single,
95
+ # dependency-free HTML file.
96
+ MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"
97
+
98
+ # @param graph [Graph::Graph] the assembled dependency graph
99
+ # @param definitions [Array<Collector::CallbackDefinition>] parsed definitions
100
+ # @return [String] a complete HTML document
101
+ def self.render(graph, definitions:)
102
+ new(graph, definitions).render
103
+ end
104
+
105
+ # @param graph [Graph::Graph]
106
+ # @param definitions [Array<Collector::CallbackDefinition>]
107
+ def initialize(graph, definitions)
108
+ @graph = graph
109
+ @definitions = definitions
110
+ end
111
+
112
+ # Assembles the full HTML document.
113
+ #
114
+ # @return [String]
115
+ def render
116
+ <<~HTML
117
+ <!DOCTYPE html>
118
+ <html>
119
+ <head><meta charset="utf-8"><title>Callback Lens</title></head>
120
+ <body>
121
+ #{callback_table}
122
+ #{execution_flow}
123
+ #{dependency_trees}
124
+ #{mermaid_section}
125
+ #{svg_section}
126
+ <script src="#{MERMAID_CDN}"></script>
127
+ <script>mermaid.initialize({startOnLoad:true});</script>
128
+ </body>
129
+ </html>
130
+ HTML
131
+ end
132
+
133
+ private
134
+
135
+ # Section 1: a table with one row per definition.
136
+ def callback_table
137
+ rows = @definitions.map do |definition|
138
+ "<tr>" \
139
+ "<td>#{escape(definition.phase)}</td>" \
140
+ "<td>#{escape(definition.event)}</td>" \
141
+ "<td>#{escape(filter_label(definition))}</td>" \
142
+ "<td>#{escape(conditions_label(definition))}</td>" \
143
+ "</tr>"
144
+ end
145
+ <<~HTML.chomp
146
+ <h2>Callback List</h2>
147
+ <table>
148
+ <thead><tr><th>Phase</th><th>Event</th><th>Filter</th><th>Conditions</th></tr></thead>
149
+ <tbody>
150
+ #{rows.join("\n")}
151
+ </tbody>
152
+ </table>
153
+ HTML
154
+ end
155
+
156
+ # Section 2: callbacks in canonical create-path execution order.
157
+ def execution_flow
158
+ ordered = ExecutionOrderAnalyzer.sort(@definitions, operation: :create)
159
+ items = ordered.map { |d| "<li>#{escape("#{d.phase}_#{d.event}")}</li>" }
160
+ <<~HTML.chomp
161
+ <h2>Execution Flow</h2>
162
+ <ol>
163
+ #{items.join("\n")}
164
+ </ol>
165
+ HTML
166
+ end
167
+
168
+ # Section 3: a nested dependency tree per definition that carries a
169
+ # condition_tree. Definitions without conditions are skipped.
170
+ def dependency_trees
171
+ trees = @definitions.filter_map do |definition|
172
+ next if definition.condition_tree.nil?
173
+
174
+ "<li>#{escape("#{definition.phase}_#{definition.event}")}" \
175
+ "#{ConditionTreeHtml.new(definition.condition_tree).to_html}</li>"
176
+ end
177
+ <<~HTML.chomp
178
+ <h2>Dependency Tree</h2>
179
+ <ul>
180
+ #{trees.join("\n")}
181
+ </ul>
182
+ HTML
183
+ end
184
+
185
+ # Section 4: the Mermaid source wrapped in a client-rendered block.
186
+ def mermaid_section
187
+ <<~HTML.chomp
188
+ <h2>Mermaid Diagram</h2>
189
+ <pre class="mermaid">#{escape(MermaidRenderer.render(@graph))}</pre>
190
+ HTML
191
+ end
192
+
193
+ # Section 5: the inline Graphviz SVG, or an empty string when +dot+ is not
194
+ # installed (GraphvizRenderer#to_svg returns nil).
195
+ def svg_section
196
+ svg = GraphvizRenderer.new(@graph).to_svg
197
+ return "" if svg.nil?
198
+
199
+ <<~HTML.chomp
200
+ <h2>Graphviz SVG</h2>
201
+ <div class="graphviz">#{svg}</div>
202
+ HTML
203
+ end
204
+
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
+ # A comma-joined label of a definition's condition leaf names, or "".
212
+ def conditions_label(definition)
213
+ tree = definition.condition_tree
214
+ return "" if tree.nil?
215
+
216
+ ConditionTreeHtml.new(tree).names.join(", ")
217
+ end
218
+
219
+ # HTML-escapes a value so it is safe to embed as element text.
220
+ def escape(value)
221
+ CGI.escapeHTML(value.to_s)
222
+ end
223
+ end
224
+ end
225
+ end
@@ -25,4 +25,14 @@ namespace :callback_lens do
25
25
  expand = CallbackLensRakeHelpers.expand?(ENV.fetch("EXPAND", nil))
26
26
  puts CallbackLensRakeHelpers.render_graphviz(model_class, expand: expand)
27
27
  end
28
+
29
+ desc "Write a self-contained HTML report to OUT for MODEL " \
30
+ "(e.g. rake callback_lens:html MODEL=User OUT=report.html EXPAND=true)"
31
+ task html: :environment do
32
+ model_class = CallbackLensRakeHelpers.resolve_model!(ENV.fetch("MODEL", nil))
33
+ expand = CallbackLensRakeHelpers.expand?(ENV.fetch("EXPAND", nil))
34
+ out = ENV.fetch("OUT", "callback_lens_report.html")
35
+ File.write(out, CallbackLensRakeHelpers.render_html(model_class, expand: expand))
36
+ puts "Report written to #{out}"
37
+ end
28
38
  end
@@ -66,6 +66,28 @@ module CallbackLensRakeHelpers
66
66
  ActiverecordCallbackLens::Renderer::GraphvizRenderer.render(graph)
67
67
  end
68
68
 
69
+ # Runs the full pipeline (collect -> parse -> [expand] -> build) for a model and
70
+ # returns a self-contained HTML report via HtmlRenderer. Mirrors
71
+ # +render_mermaid+ / +render_graphviz+ but passes both the parsed definitions
72
+ # and the assembled graph to the HtmlRenderer, which needs the definitions to
73
+ # build the callback table, execution flow, and dependency tree. +expand+ is
74
+ # threaded through identically so the +EXPAND=true+ rake convention applies.
75
+ #
76
+ # @param model_class [Class]
77
+ # @param expand [Boolean]
78
+ # @return [String] a complete HTML document
79
+ def render_html(model_class, expand: false)
80
+ definitions = ActiverecordCallbackLens::Collector::CallbackCollector.collect(model_class)
81
+ definitions = definitions.map { |definition| ActiverecordCallbackLens::Parser::ConditionParser.parse(definition) }
82
+ if expand
83
+ definitions = definitions.map do |definition|
84
+ ActiverecordCallbackLens::Resolver::MethodResolver.expand(definition, model_class)
85
+ end
86
+ end
87
+ graph = ActiverecordCallbackLens::Graph::GraphBuilder.build(definitions)
88
+ ActiverecordCallbackLens::Renderer::HtmlRenderer.render(graph, definitions: definitions)
89
+ end
90
+
69
91
  # Parses the EXPAND environment variable using the strict truthy rule: only the
70
92
  # exact string "true" (case-insensitive, surrounding whitespace stripped)
71
93
  # enables expansion. Any other value ("1", "yes", "", nil) leaves it off.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiverecordCallbackLens
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -9,8 +9,10 @@ require "activerecord_callback_lens/parser/condition_parser"
9
9
  require "activerecord_callback_lens/resolver/method_resolver"
10
10
  require "activerecord_callback_lens/graph/nodes"
11
11
  require "activerecord_callback_lens/graph/graph_builder"
12
+ require "activerecord_callback_lens/execution_order_analyzer"
12
13
  require "activerecord_callback_lens/renderer/mermaid_renderer"
13
14
  require "activerecord_callback_lens/renderer/graphviz_renderer"
15
+ require "activerecord_callback_lens/renderer/html_renderer"
14
16
  require "activerecord_callback_lens/cli/cli"
15
17
  require "activerecord_callback_lens/railtie" if defined?(Rails::Railtie)
16
18
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord_callback_lens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eraxel.Dev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-05 00:00:00.000000000 Z
11
+ date: 2026-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -75,6 +75,7 @@ files:
75
75
  - lib/activerecord_callback_lens/cli/cli.rb
76
76
  - lib/activerecord_callback_lens/collector/callback_collector.rb
77
77
  - lib/activerecord_callback_lens/collector/callback_definition.rb
78
+ - lib/activerecord_callback_lens/execution_order_analyzer.rb
78
79
  - lib/activerecord_callback_lens/graph/graph_builder.rb
79
80
  - lib/activerecord_callback_lens/graph/nodes.rb
80
81
  - lib/activerecord_callback_lens/parser/ast_walker.rb
@@ -82,6 +83,7 @@ files:
82
83
  - lib/activerecord_callback_lens/parser/condition_tree.rb
83
84
  - lib/activerecord_callback_lens/railtie.rb
84
85
  - lib/activerecord_callback_lens/renderer/graphviz_renderer.rb
86
+ - lib/activerecord_callback_lens/renderer/html_renderer.rb
85
87
  - lib/activerecord_callback_lens/renderer/mermaid_renderer.rb
86
88
  - lib/activerecord_callback_lens/resolver/method_resolver.rb
87
89
  - lib/activerecord_callback_lens/tasks/callback_lens.rake