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 +4 -4
- data/README.md +58 -4
- data/lib/activerecord_callback_lens/cli/cli.rb +39 -6
- data/lib/activerecord_callback_lens/execution_order_analyzer.rb +56 -0
- data/lib/activerecord_callback_lens/renderer/html_renderer.rb +225 -0
- data/lib/activerecord_callback_lens/tasks/callback_lens.rake +10 -0
- data/lib/activerecord_callback_lens/tasks/callback_lens_helpers.rb +22 -0
- data/lib/activerecord_callback_lens/version.rb +1 -1
- data/lib/activerecord_callback_lens.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fdca61507b8d4d6d6a15a92622bbb38c90a42c1f95cde0d9660603e96556ce23
|
|
4
|
+
data.tar.gz: 642c585e532bd018e36ebab4471072f387a4423f0ce418153b48d9006ed5153b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
|
145
|
-
|
|
|
146
|
-
| v0.4 | HTML report
|
|
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 =
|
|
45
|
-
|
|
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
|
|
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.
|
|
@@ -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.
|
|
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-
|
|
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
|