activerecord_callback_lens 0.1.0 → 0.2.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: a539b633bd3793bd684d7a93236783afbdb13b946400fd968c204281c3435e7f
4
- data.tar.gz: 806014fbee749570c24bac4df333d85f96157d16e42ba5b880380682c729102d
3
+ metadata.gz: 253d8c471770342d467836fd019cdf2712d7601b3bc3b03f71a23dffe2170fe2
4
+ data.tar.gz: 53bd60c299d8041698740cecbe133c798482bb7642d0f02a0c93843e815907ce
5
5
  SHA512:
6
- metadata.gz: 44ab27417d490c5ab1e39b8d3f900f92aa94ece3334be91dd65a04113b77ad2c93a1b95a09b5ec12e90344bd9911f7239e2b9f6864c11cd1f673d4cd23ec792a
7
- data.tar.gz: 384e76570141682a41e7fd5dd2402580999a52e24c8196271739017b83452d4ec5ccd94fcf1918ef8a04f1a8842ea1c49ed16a68653b665796d427fdff29be53
6
+ metadata.gz: 8deed0aa768a9ed5b06a423d8e88a84588737b88409e1bdfecb213ae48e1b33e751bc53e938edefff3d6c1567c0e5c0276f4e546c338c184cc4927aff164f293
7
+ data.tar.gz: b4edfa71ceb65df82d3e0807bb146bf5cbecaf78b1eae5a02dc288f0b7f1268d3c8a0e42970825b716c242f6aace8c08d163e69ca35bdc978b0101b88f21a7df
data/README.md CHANGED
@@ -57,6 +57,25 @@ rake callback_lens:analyze MODEL=User
57
57
  rake callback_lens:mermaid MODEL=User # alias
58
58
  ```
59
59
 
60
+ #### Recursive method expansion
61
+
62
+ Pass `--expand` (CLI) or `EXPAND=true` (Rake) to recursively resolve symbol
63
+ callback conditions into their `ConditionTree` representations (up to 5
64
+ levels deep). Cycles and unresolvable methods are left unexpanded with a
65
+ warning printed to stderr.
66
+
67
+ ```bash
68
+ # CLI
69
+ callback_lens analyze User --expand --mermaid
70
+
71
+ # Rake
72
+ rake callback_lens:analyze MODEL=User EXPAND=true
73
+ rake callback_lens:mermaid MODEL=User EXPAND=true
74
+ ```
75
+
76
+ Only the exact value `EXPAND=true` (case-insensitive) enables expansion; any
77
+ other value (`EXPAND=1`, `EXPAND=yes`, empty, or absent) leaves it off.
78
+
60
79
  ### Programmatic API
61
80
 
62
81
  ```ruby
@@ -98,8 +117,8 @@ puts ActiverecordCallbackLens::Renderer::MermaidRenderer.render(graph)
98
117
 
99
118
  | Version | Feature |
100
119
  |---|---|
101
- | **v0.1** | CallbackCollector, Prism condition parser, Mermaid renderer, CLI, Rake task |
102
- | v0.2 | MethodResolver — recursive expansion of Symbol conditions |
120
+ | v0.1 | CallbackCollector, Prism condition parser, Mermaid renderer, CLI, Rake task |
121
+ | **v0.2** | MethodResolver — recursive expansion of Symbol conditions (`--expand` / `EXPAND=true`) |
103
122
  | v0.3 | Graphviz / DOT renderer |
104
123
  | v0.4 | HTML report (callback list, execution flow, embedded diagram) |
105
124
  | v1.0 | Runtime tracer via `ActiveSupport::Notifications` |
@@ -4,6 +4,7 @@ require "thor"
4
4
 
5
5
  require_relative "../collector/callback_collector"
6
6
  require_relative "../parser/condition_parser"
7
+ require_relative "../resolver/method_resolver"
7
8
  require_relative "../graph/graph_builder"
8
9
  require_relative "../renderer/mermaid_renderer"
9
10
 
@@ -26,13 +27,15 @@ module ActiverecordCallbackLens
26
27
 
27
28
  desc "analyze MODEL", "Analyze callbacks for a model class and print a Mermaid diagram"
28
29
  option :mermaid, type: :boolean, default: true, desc: "Output a Mermaid diagram to stdout"
30
+ option :expand, type: :boolean, default: false,
31
+ desc: "Expand method conditions recursively (up to depth 5)"
29
32
  # Runs the analysis pipeline for +model_name+ and prints the result.
30
33
  #
31
34
  # @param model_name [String] the ActiveRecord model class name
32
35
  # @return [void]
33
36
  def analyze(model_name)
34
37
  model_class = resolve_model(model_name)
35
- graph = build_graph(model_class)
38
+ graph = build_graph(model_class, expand: options[:expand])
36
39
  puts Renderer::MermaidRenderer.render(graph) if options[:mermaid]
37
40
  end
38
41
 
@@ -50,13 +53,22 @@ module ActiverecordCallbackLens
50
53
  exit 1
51
54
  end
52
55
 
53
- # Collect -> Parse -> Build the dependency graph for a model class.
56
+ # Collect -> Parse -> (optionally Expand) -> Build the dependency graph for
57
+ # a model class.
58
+ #
59
+ # When +expand+ is true, every parsed definition's condition_tree has its
60
+ # MethodRefNodes resolved into ConditionTree sub-trees via MethodResolver.
61
+ # When false, the pipeline is identical to v0.1 output.
54
62
  #
55
63
  # @param model_class [Class]
64
+ # @param expand [Boolean]
56
65
  # @return [Graph::Graph]
57
- def build_graph(model_class)
66
+ def build_graph(model_class, expand: false)
58
67
  definitions = Collector::CallbackCollector.collect(model_class)
59
68
  definitions = definitions.map { |definition| Parser::ConditionParser.parse(definition) }
69
+ if expand
70
+ definitions = definitions.map { |definition| Resolver::MethodResolver.expand(definition, model_class) }
71
+ end
60
72
  Graph::GraphBuilder.build(definitions)
61
73
  end
62
74
  end
@@ -89,6 +89,12 @@ module ActiverecordCallbackLens
89
89
  tree_node.children.each { |child| add_tree(child, parent_id: condition_node.id) }
90
90
  end
91
91
 
92
+ # The +expanded_tree+ inspected here is populated upstream by
93
+ # Resolver::MethodResolver.expand; the GraphBuilder neither resolves nor
94
+ # mutates it. When the tree was not run through expand (the v0.1 path),
95
+ # +expanded_tree+ is nil and the MethodNode stays a leaf, so unexpanded
96
+ # output is byte-for-byte identical to v0.1.
97
+ #
92
98
  # @param tree_node [Parser::ConditionTree::MethodRefNode]
93
99
  # @param parent_id [String]
94
100
  # @return [void]
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "condition_tree"
6
+
7
+ module ActiverecordCallbackLens
8
+ module Parser
9
+ # Maps a Prism boolean AST (`&&`, `||`, `!`, predicate calls) onto the
10
+ # ConditionTree node types. Shared by ConditionParser (lambda bodies) and
11
+ # MethodResolver (method bodies) so the two stay in lockstep.
12
+ #
13
+ # The walker recognises the same boolean vocabulary the v0.1 ConditionParser
14
+ # established:
15
+ # - StatementsNode -> the last statement is the effective return value
16
+ # - ParenthesesNode -> transparent; walk the inner body
17
+ # - AndNode/OrNode -> AndNode/OrNode combinators (nil operands dropped)
18
+ # - CallNode `!` -> NotNode wrapping the negated receiver
19
+ # - CallNode -> PredicateNode for the called method name
20
+ # - anything else -> nil (unresolvable)
21
+ module AstWalker
22
+ module_function
23
+
24
+ # Recursively maps a Prism boolean AST onto ConditionTree nodes.
25
+ #
26
+ # @param node [Prism::Node, nil]
27
+ # @return [ConditionTree::Node, nil]
28
+ def walk(node)
29
+ case node
30
+ in Prism::StatementsNode then walk(node.body.last)
31
+ in Prism::ParenthesesNode then walk(node.body)
32
+ in Prism::AndNode then combinator(ConditionTree::AndNode, node)
33
+ in Prism::OrNode then combinator(ConditionTree::OrNode, node)
34
+ in Prism::CallNode if node.name == :! then negate(walk(node.receiver))
35
+ in Prism::CallNode then ConditionTree::PredicateNode.new(name: node.name.to_s)
36
+ else nil
37
+ end
38
+ end
39
+
40
+ # Builds an And/Or combinator from a binary Prism node, walking both sides
41
+ # and dropping any unresolved (nil) operand.
42
+ #
43
+ # @param klass [Class] ConditionTree::AndNode or ConditionTree::OrNode
44
+ # @param node [Prism::AndNode, Prism::OrNode]
45
+ # @return [ConditionTree::Node]
46
+ def combinator(klass, node)
47
+ klass.new(children: [walk(node.left), walk(node.right)].compact)
48
+ end
49
+
50
+ # @param node [ConditionTree::Node, nil]
51
+ # @return [ConditionTree::NotNode, nil]
52
+ def negate(node)
53
+ return nil if node.nil?
54
+
55
+ ConditionTree::NotNode.new(child: node)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "condition_tree"
6
+ require_relative "ast_walker"
6
7
 
7
8
  module ActiverecordCallbackLens
8
9
  module Parser
@@ -64,9 +65,7 @@ module ActiverecordCallbackLens
64
65
  # @param node [ConditionTree::Node, nil]
65
66
  # @return [ConditionTree::NotNode, nil]
66
67
  def negate(node)
67
- return nil if node.nil?
68
-
69
- ConditionTree::NotNode.new(child: node)
68
+ AstWalker.negate(node)
70
69
  end
71
70
 
72
71
  # Dispatches a single raw condition entry to the right handler.
@@ -96,33 +95,7 @@ module ActiverecordCallbackLens
96
95
 
97
96
  locator = LambdaLocator.new(target_line: line)
98
97
  locator.visit(result.value)
99
- walk(locator.node)
100
- end
101
-
102
- # Recursively maps a Prism boolean AST onto ConditionTree nodes.
103
- #
104
- # @param node [Prism::Node, nil]
105
- # @return [ConditionTree::Node, nil]
106
- def walk(node)
107
- case node
108
- in Prism::StatementsNode then walk(node.body.last)
109
- in Prism::ParenthesesNode then walk(node.body)
110
- in Prism::AndNode then combinator(ConditionTree::AndNode, node)
111
- in Prism::OrNode then combinator(ConditionTree::OrNode, node)
112
- in Prism::CallNode if node.name == :! then negate(walk(node.receiver))
113
- in Prism::CallNode then ConditionTree::PredicateNode.new(name: node.name.to_s)
114
- else nil
115
- end
116
- end
117
-
118
- # Builds an And/Or combinator from a binary Prism node, walking both sides
119
- # and dropping any unresolved (nil) operand.
120
- #
121
- # @param klass [Class] ConditionTree::AndNode or ConditionTree::OrNode
122
- # @param node [Prism::AndNode, Prism::OrNode]
123
- # @return [ConditionTree::Node]
124
- def combinator(klass, node)
125
- klass.new(children: [walk(node.left), walk(node.right)].compact)
98
+ AstWalker.walk(locator.node)
126
99
  end
127
100
 
128
101
  # A Prism visitor that captures the innermost LambdaNode or BlockNode whose
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../parser/condition_tree"
6
+ require_relative "../parser/ast_walker"
7
+
8
+ module ActiverecordCallbackLens
9
+ module Resolver
10
+ # Resolves a callback's MethodRefNode symbol (e.g. :sync_required?) into an
11
+ # expanded ConditionTree.
12
+ #
13
+ # Given a model class and a method name, MethodResolver locates the method's
14
+ # source via `instance_method(name).source_location`, parses the file with
15
+ # Prism, isolates the matching DefNode, and maps its body onto a
16
+ # ConditionTree with the shared Parser::AstWalker. Any MethodRefNode found in
17
+ # that tree is then resolved recursively, so a predicate that delegates to
18
+ # other predicates expands into a full boolean tree.
19
+ #
20
+ # Two guards prevent runaway recursion:
21
+ # - a `visited` Set closes direct (A -> A) and indirect (A -> B -> A) cycles
22
+ # - a MAX_DEPTH cap bounds otherwise-acyclic but deep chains
23
+ #
24
+ # Known limitation: a DefNode body is a StatementsNode and the resolver treats
25
+ # the *last* statement as the effective return value (the same assumption
26
+ # ConditionParser makes for lambda bodies). Methods with early returns or
27
+ # guard clauses are therefore only partially understood and are left
28
+ # unexpanded where the heuristic does not apply.
29
+ class MethodResolver
30
+ # Hard cap on recursion depth. A chain of method refs deeper than this is
31
+ # left unexpanded rather than followed further.
32
+ MAX_DEPTH = 5
33
+
34
+ # @param model_class [Class] the ActiveRecord model the method is defined on
35
+ # @param method_name [Symbol] the predicate/method to resolve
36
+ # @return [Parser::ConditionTree::Node, nil] the expanded tree, or nil when
37
+ # the method cannot be located/parsed
38
+ def self.resolve(model_class, method_name)
39
+ new(model_class).resolve(method_name)
40
+ end
41
+
42
+ # Per-definition expansion entry point. Walks a CallbackDefinition's
43
+ # condition_tree, resolves every MethodRefNode against the model, and
44
+ # returns a new definition whose tree carries populated expanded_tree
45
+ # fields. Bridges the resolver core and the CLI/Rake integration.
46
+ #
47
+ # @param definition [Collector::CallbackDefinition]
48
+ # @param model_class [Class]
49
+ # @return [Collector::CallbackDefinition] with condition_tree fully expanded
50
+ def self.expand(definition, model_class)
51
+ new(model_class).expand(definition)
52
+ end
53
+
54
+ # @param model_class [Class]
55
+ def initialize(model_class)
56
+ @model_class = model_class
57
+ end
58
+
59
+ # Expands every MethodRefNode in the definition's condition_tree.
60
+ #
61
+ # A definition with no condition_tree (nil) is returned unchanged; so is a
62
+ # tree that contains no MethodRefNodes (expand_tree rebuilds it into a
63
+ # value-equal copy, leaving non-expansion output identical to v0.1).
64
+ #
65
+ # @param definition [Collector::CallbackDefinition]
66
+ # @return [Collector::CallbackDefinition]
67
+ def expand(definition)
68
+ return definition unless definition.condition_tree
69
+
70
+ expanded = expand_tree(definition.condition_tree)
71
+ definition.with(condition_tree: expanded)
72
+ end
73
+
74
+ # Resolves a method name into an expanded ConditionTree.
75
+ #
76
+ # @param method_name [Symbol]
77
+ # @param depth [Integer] current recursion depth (0 at the entry point)
78
+ # @param visited [Set<Symbol>] method names already on the current path
79
+ # @return [Parser::ConditionTree::Node, nil]
80
+ def resolve(method_name, depth: 0, visited: Set.new)
81
+ if depth >= MAX_DEPTH
82
+ warn("[ActiverecordCallbackLens] Max resolution depth (#{MAX_DEPTH}) reached at #{method_name}")
83
+ return nil
84
+ end
85
+ return nil if visited.include?(method_name)
86
+
87
+ visited = visited.dup
88
+ visited.add(method_name)
89
+
90
+ node = locate_and_parse(method_name)
91
+ return nil if node.nil?
92
+
93
+ expand_refs(node, depth: depth + 1, visited: visited)
94
+ end
95
+
96
+ private
97
+
98
+ # Recursively rebuilds a condition_tree, populating expanded_tree on every
99
+ # MethodRefNode. And/Or/Not structure is preserved by rebuilding children
100
+ # through Data#with so callers' references are never mutated; nodes with no
101
+ # MethodRefNode beneath them rebuild into value-equal copies.
102
+ #
103
+ # Each MethodRefNode is resolved from a fresh depth-0 walk (resolve applies
104
+ # its own MAX_DEPTH/visited guards), so a ref's name (a String) is converted
105
+ # to a Symbol before being handed to resolve.
106
+ #
107
+ # @param node [Parser::ConditionTree::Node, nil]
108
+ # @return [Parser::ConditionTree::Node, nil]
109
+ def expand_tree(node)
110
+ case node
111
+ in Parser::ConditionTree::AndNode | Parser::ConditionTree::OrNode
112
+ node.with(children: node.children.map { |child| expand_tree(child) })
113
+ in Parser::ConditionTree::NotNode
114
+ node.with(child: expand_tree(node.child))
115
+ in Parser::ConditionTree::MethodRefNode
116
+ node.with(expanded_tree: resolve(node.name.to_sym))
117
+ else
118
+ node
119
+ end
120
+ end
121
+
122
+ # Locates the method's source and parses its body into a ConditionTree.
123
+ #
124
+ # @param method_name [Symbol]
125
+ # @return [Parser::ConditionTree::Node, nil] nil when the method has no Ruby
126
+ # source, the file is missing/unparseable, or no DefNode is found
127
+ def locate_and_parse(method_name)
128
+ location = source_location_for(method_name)
129
+ return nil if location.nil?
130
+
131
+ file, line = location
132
+ return nil unless file && File.exist?(file)
133
+
134
+ result = Prism.parse_file(file)
135
+ unless result.success?
136
+ warn("[ActiverecordCallbackLens] Failed to parse #{file}: #{result.errors.map(&:message).join(', ')}")
137
+ return nil
138
+ end
139
+
140
+ locator = DefLocator.new(target_line: line, method_name: method_name)
141
+ locator.visit(result.value)
142
+ reclassify_refs(Parser::AstWalker.walk(locator.node))
143
+ end
144
+
145
+ # The shared AstWalker maps every bare predicate call to a PredicateNode
146
+ # leaf. Inside a *method* body, though, a call to another method that this
147
+ # model defines in Ruby is a MethodRefNode the resolver can expand. This
148
+ # pass rewrites those leaves: a PredicateNode whose name resolves to a
149
+ # method with a Ruby source_location becomes a (yet-unexpanded)
150
+ # MethodRefNode; everything else (AR-generated dirty-tracking predicates,
151
+ # C-level methods) stays a PredicateNode leaf.
152
+ #
153
+ # @param node [Parser::ConditionTree::Node, nil]
154
+ # @return [Parser::ConditionTree::Node, nil]
155
+ def reclassify_refs(node)
156
+ tree = Parser::ConditionTree
157
+ case node
158
+ when tree::PredicateNode
159
+ resolvable?(node.name) ? tree::MethodRefNode.new(name: node.name, expanded_tree: nil) : node
160
+ when tree::AndNode, tree::OrNode
161
+ node.with(children: node.children.map { |child| reclassify_refs(child) })
162
+ when tree::NotNode
163
+ node.with(child: reclassify_refs(node.child))
164
+ else
165
+ node
166
+ end
167
+ end
168
+
169
+ # @param name [String] a predicate/method name
170
+ # @return [Boolean] true when the model defines a method by this name that
171
+ # has a Ruby source_location (i.e. can be followed by the resolver)
172
+ def resolvable?(name)
173
+ location = source_location_for(name.to_sym)
174
+ !location.nil?
175
+ end
176
+
177
+ # @param method_name [Symbol]
178
+ # @return [Array(String, Integer), nil] the [file, line] pair, or nil when
179
+ # the method is undefined or C-level/eval'd (nil source_location)
180
+ def source_location_for(method_name)
181
+ @model_class.instance_method(method_name).source_location
182
+ rescue NameError
183
+ nil
184
+ end
185
+
186
+ # Walks a parsed ConditionTree and replaces every MethodRefNode with a copy
187
+ # whose `expanded_tree` is the recursive resolution of that ref. And/Or/Not
188
+ # structure is preserved by rebuilding children through Data#with.
189
+ #
190
+ # @param node [Parser::ConditionTree::Node, nil]
191
+ # @param depth [Integer]
192
+ # @param visited [Set<Symbol>]
193
+ # @return [Parser::ConditionTree::Node, nil]
194
+ def expand_refs(node, depth:, visited:)
195
+ tree = Parser::ConditionTree
196
+ case node
197
+ when tree::MethodRefNode
198
+ sub_tree = resolve(node.name.to_sym, depth: depth, visited: visited)
199
+ node.with(expanded_tree: sub_tree)
200
+ when tree::AndNode, tree::OrNode
201
+ node.with(children: node.children.map { |child| expand_refs(child, depth: depth, visited: visited) })
202
+ when tree::NotNode
203
+ node.with(child: expand_refs(node.child, depth: depth, visited: visited))
204
+ else
205
+ node
206
+ end
207
+ end
208
+
209
+ # A Prism visitor that captures the body of the DefNode matching a target
210
+ # line (and, when available, a target method name). Mirrors ConditionParser's
211
+ # LambdaLocator but targets `def` definitions rather than lambdas/blocks.
212
+ class DefLocator < Prism::Visitor
213
+ # @return [Prism::Node, nil] the body of the matched DefNode
214
+ attr_reader :node
215
+
216
+ # @param target_line [Integer] the line source_location reports for the def
217
+ # @param method_name [Symbol, nil] the method name to disambiguate overloads
218
+ def initialize(target_line:, method_name: nil)
219
+ @target_line = target_line
220
+ @method_name = method_name
221
+ @node = nil
222
+ super()
223
+ end
224
+
225
+ # @param def_node [Prism::DefNode]
226
+ # @return [void]
227
+ def visit_def_node(def_node)
228
+ capture(def_node)
229
+ super
230
+ end
231
+
232
+ private
233
+
234
+ # Records the def's body when it both starts on the target line and (if a
235
+ # name was supplied) carries the expected method name. Matching on the
236
+ # start line keeps a method's own `def` from being shadowed by an enclosing
237
+ # definition, while the name check guards against same-line edge cases.
238
+ #
239
+ # @param candidate [Prism::DefNode]
240
+ # @return [void]
241
+ def capture(candidate)
242
+ return unless candidate.location.start_line == @target_line
243
+ return if @method_name && candidate.name != @method_name
244
+
245
+ @node = candidate.body
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -3,15 +3,18 @@
3
3
  require_relative "callback_lens_helpers"
4
4
 
5
5
  namespace :callback_lens do
6
- desc "Print callback Mermaid diagram for MODEL (e.g. rake callback_lens:analyze MODEL=User)"
6
+ desc "Print callback Mermaid diagram for MODEL " \
7
+ "(e.g. rake callback_lens:analyze MODEL=User EXPAND=true)"
7
8
  task analyze: :environment do
8
9
  model_class = CallbackLensRakeHelpers.resolve_model!(ENV.fetch("MODEL", nil))
9
- puts CallbackLensRakeHelpers.render_mermaid(model_class)
10
+ expand = CallbackLensRakeHelpers.expand?(ENV.fetch("EXPAND", nil))
11
+ puts CallbackLensRakeHelpers.render_mermaid(model_class, expand: expand)
10
12
  end
11
13
 
12
14
  desc "Alias for callback_lens:analyze — print Mermaid diagram for MODEL"
13
15
  task mermaid: :environment do
14
16
  model_class = CallbackLensRakeHelpers.resolve_model!(ENV.fetch("MODEL", nil))
15
- puts CallbackLensRakeHelpers.render_mermaid(model_class)
17
+ expand = CallbackLensRakeHelpers.expand?(ENV.fetch("EXPAND", nil))
18
+ puts CallbackLensRakeHelpers.render_mermaid(model_class, expand: expand)
16
19
  end
17
20
  end
@@ -22,14 +22,37 @@ module CallbackLensRakeHelpers
22
22
  raise "Cannot find model class '#{name}'. Make sure it is loaded."
23
23
  end
24
24
 
25
- # Runs the full pipeline (collect -> parse -> build -> render) for a model.
25
+ # Runs the full pipeline (collect -> parse -> [expand] -> build -> render) for
26
+ # a model.
27
+ #
28
+ # When +expand+ is true, every parsed definition's condition_tree has its
29
+ # MethodRefNodes resolved into ConditionTree sub-trees via MethodResolver,
30
+ # matching the CLI's +--expand+ behaviour. When false (the default), the
31
+ # pipeline is identical to v0.1 output. Threading +expand+ through this single
32
+ # helper keeps every rake task that delegates here uniform.
26
33
  #
27
34
  # @param model_class [Class]
35
+ # @param expand [Boolean]
28
36
  # @return [String] the Mermaid diagram
29
- def render_mermaid(model_class)
37
+ def render_mermaid(model_class, expand: false)
30
38
  definitions = ActiverecordCallbackLens::Collector::CallbackCollector.collect(model_class)
31
39
  definitions = definitions.map { |definition| ActiverecordCallbackLens::Parser::ConditionParser.parse(definition) }
40
+ if expand
41
+ definitions = definitions.map do |definition|
42
+ ActiverecordCallbackLens::Resolver::MethodResolver.expand(definition, model_class)
43
+ end
44
+ end
32
45
  graph = ActiverecordCallbackLens::Graph::GraphBuilder.build(definitions)
33
46
  ActiverecordCallbackLens::Renderer::MermaidRenderer.render(graph)
34
47
  end
48
+
49
+ # Parses the EXPAND environment variable using the strict truthy rule: only the
50
+ # exact string "true" (case-insensitive, surrounding whitespace stripped)
51
+ # enables expansion. Any other value ("1", "yes", "", nil) leaves it off.
52
+ #
53
+ # @param value [String, nil] the raw ENV["EXPAND"] value
54
+ # @return [Boolean]
55
+ def expand?(value)
56
+ value.to_s.strip.downcase == "true"
57
+ end
35
58
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiverecordCallbackLens
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -4,7 +4,9 @@ require "activerecord_callback_lens/version"
4
4
  require "activerecord_callback_lens/collector/callback_definition"
5
5
  require "activerecord_callback_lens/collector/callback_collector"
6
6
  require "activerecord_callback_lens/parser/condition_tree"
7
+ require "activerecord_callback_lens/parser/ast_walker"
7
8
  require "activerecord_callback_lens/parser/condition_parser"
9
+ require "activerecord_callback_lens/resolver/method_resolver"
8
10
  require "activerecord_callback_lens/graph/nodes"
9
11
  require "activerecord_callback_lens/graph/graph_builder"
10
12
  require "activerecord_callback_lens/renderer/mermaid_renderer"
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eraxel.Dev
@@ -77,10 +77,12 @@ files:
77
77
  - lib/activerecord_callback_lens/collector/callback_definition.rb
78
78
  - lib/activerecord_callback_lens/graph/graph_builder.rb
79
79
  - lib/activerecord_callback_lens/graph/nodes.rb
80
+ - lib/activerecord_callback_lens/parser/ast_walker.rb
80
81
  - lib/activerecord_callback_lens/parser/condition_parser.rb
81
82
  - lib/activerecord_callback_lens/parser/condition_tree.rb
82
83
  - lib/activerecord_callback_lens/railtie.rb
83
84
  - lib/activerecord_callback_lens/renderer/mermaid_renderer.rb
85
+ - lib/activerecord_callback_lens/resolver/method_resolver.rb
84
86
  - lib/activerecord_callback_lens/tasks/callback_lens.rake
85
87
  - lib/activerecord_callback_lens/tasks/callback_lens_helpers.rb
86
88
  - lib/activerecord_callback_lens/version.rb