activerecord_callback_lens 0.2.1 → 0.3.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: 32dbc737ca1362eab8b3a931bd7b2a6ccba9801be70b512d6e47e73d365634c1
4
- data.tar.gz: a8500d0321565cacec20b3145d315d804215ef616f8619407f5c0e72c4fb199d
3
+ metadata.gz: 93034e63e1e99dfe1e6ea64193460d4a206bdf88a01e1c6606385e48228e292c
4
+ data.tar.gz: d5fae7891b074b7459d7127d9b34befbf8345bd6ee27cef938e0cf0215adddc5
5
5
  SHA512:
6
- metadata.gz: f7b3ff8e64d67c4f875bc01dc9adbafe80786cabb5db56718805cb2503aa96acb06e38ac4df17041a9171c0a728a8c3acf7c4e3e766f38e66ac716b6bd29576d
7
- data.tar.gz: 59f152922559ae9811d339ba8e44bd7de7c3a9198a0c97cb397763e8ccb2bf442e58367d84d0bc61acd8250607d2b5f9fa60f6ca951f5ace8281a425a3fefc21
6
+ metadata.gz: 9a29d788fe927d7c24b82e70965219b3dfdebdd04a6fa36d1b05d0b968f9265a4b280a0e5e8f67436681cd71a80cbddc0ee993f1fe76befcc3bd698aa7371daf
7
+ data.tar.gz: 0dfa3cc6f1ebac81b1b8469ce70a1a35482ff948bab5462d3ed777d64843ee948dbeea89fd208d32b8efa24fe54f91a59ff50b7116b1d7fdc25141e9e3b1ba8e
data/README.md CHANGED
@@ -76,6 +76,29 @@ rake callback_lens:mermaid MODEL=User EXPAND=true
76
76
  Only the exact value `EXPAND=true` (case-insensitive) enables expansion; any
77
77
  other value (`EXPAND=1`, `EXPAND=yes`, empty, or absent) leaves it off.
78
78
 
79
+ #### Graphviz DOT output
80
+
81
+ Pass `--graphviz` (CLI) or use `callback_lens:graphviz` (Rake) to output a
82
+ [Graphviz DOT](https://graphviz.org/) diagram to stdout. Pipe it to `dot` to
83
+ generate an image:
84
+
85
+ ```bash
86
+ # CLI — print DOT to stdout
87
+ callback_lens analyze User --graphviz
88
+
89
+ # CLI — combine with Mermaid output
90
+ callback_lens analyze User --mermaid --graphviz
91
+
92
+ # CLI — pipe to dot for a PNG
93
+ callback_lens analyze User --graphviz | dot -Tpng -o callbacks.png
94
+
95
+ # Rake
96
+ rake callback_lens:graphviz MODEL=User
97
+ rake callback_lens:graphviz MODEL=User EXPAND=true
98
+ ```
99
+
100
+ Requires [Graphviz](https://graphviz.org/download/) only when piping to `dot`.
101
+
79
102
  ### Programmatic API
80
103
 
81
104
  ```ruby
@@ -119,7 +142,7 @@ puts ActiverecordCallbackLens::Renderer::MermaidRenderer.render(graph)
119
142
  |---|---|
120
143
  | v0.1 | CallbackCollector, Prism condition parser, Mermaid renderer, CLI, Rake task |
121
144
  | **v0.2** | MethodResolver — recursive expansion of Symbol conditions (`--expand` / `EXPAND=true`) |
122
- | v0.3 | Graphviz / DOT renderer |
145
+ | **v0.3** | Graphviz / DOT renderer (`--graphviz` / `callback_lens:graphviz`) |
123
146
  | v0.4 | HTML report (callback list, execution flow, embedded diagram) |
124
147
  | v1.0 | Runtime tracer via `ActiveSupport::Notifications` |
125
148
  | v2.0 | RBS analysis, cross-model dependency graph |
@@ -7,15 +7,19 @@ require_relative "../parser/condition_parser"
7
7
  require_relative "../resolver/method_resolver"
8
8
  require_relative "../graph/graph_builder"
9
9
  require_relative "../renderer/mermaid_renderer"
10
+ require_relative "../renderer/graphviz_renderer"
10
11
 
11
12
  module ActiverecordCallbackLens
12
13
  module CLI
13
14
  # Thor application exposing the callback_lens command-line interface.
14
15
  #
15
- # For v0.1 it provides a single command, +analyze+, which runs the full
16
- # pipeline (collect -> parse -> build graph -> render) and prints a Mermaid
17
- # diagram to stdout. An unknown model name is reported with a friendly
18
- # message and a non-zero exit status rather than a Ruby backtrace.
16
+ # It provides a single command, +analyze+, which runs the full pipeline
17
+ # (collect -> parse -> build graph -> render) and prints the result to
18
+ # stdout. Output format is selectable per invocation: a Mermaid diagram
19
+ # (+--mermaid+, on by default) and/or a Graphviz DOT graph (+--graphviz+);
20
+ # the two flags are independent and may be combined. An unknown model name
21
+ # is reported with a friendly message and a non-zero exit status rather than
22
+ # a Ruby backtrace.
19
23
  class App < Thor
20
24
  # Tells Thor to exit with a non-zero status when a command raises, so the
21
25
  # +exit 1+ paths below propagate a failure code to the shell.
@@ -25,8 +29,10 @@ module ActiverecordCallbackLens
25
29
  true
26
30
  end
27
31
 
28
- desc "analyze MODEL", "Analyze callbacks for a model class and print a Mermaid diagram"
32
+ desc "analyze MODEL", "Analyze callbacks for a model class and print a Mermaid and/or Graphviz diagram"
29
33
  option :mermaid, type: :boolean, default: true, desc: "Output a Mermaid diagram to stdout"
34
+ option :graphviz, type: :boolean, default: false,
35
+ desc: "Output DOT graph via Graphviz to stdout"
30
36
  option :expand, type: :boolean, default: false,
31
37
  desc: "Expand method conditions recursively (up to depth 5)"
32
38
  # Runs the analysis pipeline for +model_name+ and prints the result.
@@ -37,6 +43,7 @@ module ActiverecordCallbackLens
37
43
  model_class = resolve_model(model_name)
38
44
  graph = build_graph(model_class, expand: options[:expand])
39
45
  puts Renderer::MermaidRenderer.render(graph) if options[:mermaid]
46
+ puts Renderer::GraphvizRenderer.render(graph) if options[:graphviz]
40
47
  end
41
48
 
42
49
  private
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../graph/nodes"
4
+
5
+ module ActiverecordCallbackLens
6
+ module Renderer
7
+ # Renders a Graph::Graph as a Graphviz DOT language string.
8
+ #
9
+ # The output is a `digraph` block with a left-to-right rank direction, one
10
+ # declaration per node and one arrow per edge:
11
+ #
12
+ # digraph callback_lens {
13
+ # rankdir=LR;
14
+ # n0 [label="before_save"];
15
+ # n1 [label="active?"];
16
+ # n1 -> n0;
17
+ # }
18
+ #
19
+ # Node labels are derived from the node type (see #node_label) using the same
20
+ # four-case logic as MermaidRenderer. Double quotes in a label are escaped as
21
+ # `\"` so they cannot break the surrounding DOT label syntax.
22
+ class GraphvizRenderer
23
+ # @param graph [Graph::Graph]
24
+ # @return [String] DOT language string
25
+ def self.render(graph)
26
+ new(graph).render
27
+ end
28
+
29
+ # @param graph [Graph::Graph]
30
+ def initialize(graph)
31
+ @graph = graph
32
+ end
33
+
34
+ # @return [String] DOT language string
35
+ def render
36
+ lines = ["digraph callback_lens {", " rankdir=LR;"]
37
+ lines.concat(node_declarations)
38
+ lines.concat(edge_declarations)
39
+ lines << "}"
40
+ lines.join("\n")
41
+ end
42
+
43
+ # Shells out to the `dot` binary and returns the rendered SVG string.
44
+ # Returns nil when Graphviz is not installed (the `dot` binary is absent
45
+ # from PATH) rather than raising.
46
+ #
47
+ # @return [String, nil]
48
+ def to_svg
49
+ IO.popen(["dot", "-Tsvg"], "r+") do |io|
50
+ io.write(render)
51
+ io.close_write
52
+ io.read
53
+ end
54
+ rescue Errno::ENOENT
55
+ nil
56
+ end
57
+
58
+ private
59
+
60
+ # @return [Array<String>]
61
+ def node_declarations
62
+ @graph.nodes.map { |node| " #{node.id} [label=\"#{escape(node_label(node))}\"];" }
63
+ end
64
+
65
+ # @return [Array<String>]
66
+ def edge_declarations
67
+ @graph.edges.map { |edge| " #{edge.from_id} -> #{edge.to_id};" }
68
+ end
69
+
70
+ # Derives the human-readable label for a graph node from its type.
71
+ #
72
+ # @param node [Graph::CallbackNode, Graph::PredicateNode, Graph::MethodNode, Graph::ConditionNode]
73
+ # @return [String]
74
+ def node_label(node)
75
+ case node
76
+ when Graph::CallbackNode then "#{node.definition.phase}_#{node.definition.event}"
77
+ when Graph::PredicateNode then node.predicate_name
78
+ when Graph::MethodNode then node.method_name
79
+ when Graph::ConditionNode then node.tree_node.class.name.split("::").last
80
+ else node.id
81
+ end
82
+ end
83
+
84
+ # Escapes characters that would otherwise break a `label="..."` DOT label.
85
+ # Double quotes become a backslash-escaped quote so the label stays a
86
+ # single, valid DOT token. (DOT uses `\"`, unlike Mermaid's `&quot;`.)
87
+ #
88
+ # @param label [String]
89
+ # @return [String]
90
+ def escape(label)
91
+ label.to_s.gsub("\"", "\\\"")
92
+ end
93
+ end
94
+ end
95
+ end
@@ -17,4 +17,12 @@ namespace :callback_lens do
17
17
  expand = CallbackLensRakeHelpers.expand?(ENV.fetch("EXPAND", nil))
18
18
  puts CallbackLensRakeHelpers.render_mermaid(model_class, expand: expand)
19
19
  end
20
+
21
+ desc "Write Graphviz DOT to STDOUT for MODEL " \
22
+ "(e.g. rake callback_lens:graphviz MODEL=User EXPAND=true)"
23
+ task graphviz: :environment do
24
+ model_class = CallbackLensRakeHelpers.resolve_model!(ENV.fetch("MODEL", nil))
25
+ expand = CallbackLensRakeHelpers.expand?(ENV.fetch("EXPAND", nil))
26
+ puts CallbackLensRakeHelpers.render_graphviz(model_class, expand: expand)
27
+ end
20
28
  end
@@ -46,6 +46,26 @@ module CallbackLensRakeHelpers
46
46
  ActiverecordCallbackLens::Renderer::MermaidRenderer.render(graph)
47
47
  end
48
48
 
49
+ # Runs the full pipeline (collect -> parse -> [expand] -> build -> render) for
50
+ # a model and returns a Graphviz DOT language string. Mirrors +render_mermaid+
51
+ # but uses the GraphvizRenderer; +expand+ is threaded through identically so
52
+ # the +EXPAND=true+ rake convention applies to the graphviz task too.
53
+ #
54
+ # @param model_class [Class]
55
+ # @param expand [Boolean]
56
+ # @return [String] DOT language string
57
+ def render_graphviz(model_class, expand: false)
58
+ definitions = ActiverecordCallbackLens::Collector::CallbackCollector.collect(model_class)
59
+ definitions = definitions.map { |definition| ActiverecordCallbackLens::Parser::ConditionParser.parse(definition) }
60
+ if expand
61
+ definitions = definitions.map do |definition|
62
+ ActiverecordCallbackLens::Resolver::MethodResolver.expand(definition, model_class)
63
+ end
64
+ end
65
+ graph = ActiverecordCallbackLens::Graph::GraphBuilder.build(definitions)
66
+ ActiverecordCallbackLens::Renderer::GraphvizRenderer.render(graph)
67
+ end
68
+
49
69
  # Parses the EXPAND environment variable using the strict truthy rule: only the
50
70
  # exact string "true" (case-insensitive, surrounding whitespace stripped)
51
71
  # 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.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -10,6 +10,7 @@ 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
12
  require "activerecord_callback_lens/renderer/mermaid_renderer"
13
+ require "activerecord_callback_lens/renderer/graphviz_renderer"
13
14
  require "activerecord_callback_lens/cli/cli"
14
15
  require "activerecord_callback_lens/railtie" if defined?(Rails::Railtie)
15
16
 
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.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eraxel.Dev
@@ -81,6 +81,7 @@ files:
81
81
  - lib/activerecord_callback_lens/parser/condition_parser.rb
82
82
  - lib/activerecord_callback_lens/parser/condition_tree.rb
83
83
  - lib/activerecord_callback_lens/railtie.rb
84
+ - lib/activerecord_callback_lens/renderer/graphviz_renderer.rb
84
85
  - lib/activerecord_callback_lens/renderer/mermaid_renderer.rb
85
86
  - lib/activerecord_callback_lens/resolver/method_resolver.rb
86
87
  - lib/activerecord_callback_lens/tasks/callback_lens.rake