why_chain 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: 57709eb19a0190d1c1cde75dc2cb9b4a657edfd5b4a57cda237a7665b9b027f4
4
- data.tar.gz: 76a6b127db4b8acaa90f4e7047e2d64d4d12ff5e8553c3faf48d8b20a6b23141
3
+ metadata.gz: 2fa488f47789be0531b412a2c9407deda76c1cf5760f1a13510f313d852dbe5a
4
+ data.tar.gz: d464be25491fb7111b91869d340bb3f8ab8c003878be50d80ff8f2deaf765719
5
5
  SHA512:
6
- metadata.gz: b1b2fd4c412353c75e0fc7b2645fae16bd76587e91b312785125e48487b5a7304c12ff72f4569955471fb9c657c3db474ac3190163860c1d9d166ad70bbcc78a
7
- data.tar.gz: fce81cd5ebf55df0fe92a8ddcb6fb1667f836ca806cb323625447fe77e22c2ab4dd1ce3a260981f1cb7b0787b1a4f74e7b92680f84d42fe41da0800dd891de51
6
+ metadata.gz: bc4a485648f1dc97335cb4bec48909903156f7c464f3ac637c1945ea6ec33ad0c2ea4b668e15e5054f5651075e5ce51d17b2844c1ad9f0e9df121f03a8faac95
7
+ data.tar.gz: 9f2b823a8e304dfc90ca9d704e320d9b98b090ac706841c9300cff024805ef568569c861526e94e872f5a89f0c0a131c9548e05ad24be27696f1e04f59f495c1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-05-15
4
+
5
+ - Add style/color options to `WhyChain.explain` with `:teaching` and compact debug format.
6
+ - Introduce compact step labels for dispatch kind: `singleton`, `prepend`, `include`, and `class`.
7
+ - Refactor explainer internals into dedicated formatter and colorizer classes for maintainability.
8
+
3
9
  ## [0.2.1] - 2026-05-11
4
10
 
5
11
  - Refactor internals by extracting shared method-definition predicate into `MethodDefinition`.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- why_chain (0.2.1)
4
+ why_chain (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -3,17 +3,19 @@
3
3
  module WhyChain
4
4
  # Immutable value object for a single dispatch step.
5
5
  class DispatchStep
6
- attr_reader :owner, :source_location
6
+ attr_reader :owner, :source_location, :kind
7
7
 
8
- def initialize(owner:, source_location:)
8
+ def initialize(owner:, source_location:, kind: nil)
9
9
  @owner = owner
10
10
  @source_location = source_location
11
+ @kind = kind
11
12
  end
12
13
 
13
14
  def to_h
14
15
  {
15
16
  owner: owner,
16
- source_location: source_location
17
+ source_location: source_location,
18
+ kind: kind
17
19
  }
18
20
  end
19
21
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WhyChain
4
+ class Explainer
5
+ # Handles ANSI color output and source location formatting.
6
+ class Colorizer
7
+ ANSI_CODES = {
8
+ title: "1;36",
9
+ owner: "32",
10
+ kind: "35",
11
+ label: "2;37",
12
+ location: "2;37",
13
+ native: "33",
14
+ arrow: "36"
15
+ }.freeze
16
+
17
+ def initialize(color_mode)
18
+ @use_color = resolve_color(color_mode)
19
+ end
20
+
21
+ def colorize(text, tone)
22
+ return text unless @use_color
23
+
24
+ code = ANSI_CODES.fetch(tone, "0")
25
+ "\e[#{code}m#{text}\e[0m"
26
+ end
27
+
28
+ def format_source_location(source_location, color: true)
29
+ formatted = source_location ? source_location.join(":") : "<native>"
30
+ return formatted unless color
31
+
32
+ source_location ? colorize(formatted, :location) : colorize(formatted, :native)
33
+ end
34
+
35
+ private
36
+
37
+ def resolve_color(color)
38
+ case color
39
+ when true
40
+ true
41
+ when false
42
+ false
43
+ when :auto
44
+ $stdout.tty? && ENV["CI"] != "true"
45
+ else
46
+ raise ArgumentError, "Unknown color mode: #{color.inspect}"
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WhyChain
4
+ class Explainer
5
+ # Compact debug-friendly explanation format.
6
+ class CompactFormatter
7
+ def initialize(method_name, steps, graph, colorizer)
8
+ @method_name = method_name
9
+ @steps = steps
10
+ @graph = graph
11
+ @colorizer = colorizer
12
+ end
13
+
14
+ def render
15
+ lines = [@colorizer.colorize("WhyChain :#{@method_name}", :title)]
16
+
17
+ @steps.each_with_index do |step, index|
18
+ lines << step_line(step, index)
19
+ lines << @colorizer.colorize(" -> super", :arrow) if @graph && index < @steps.length - 1
20
+ end
21
+
22
+ lines.join("\n")
23
+ end
24
+
25
+ private
26
+
27
+ def step_line(step, index)
28
+ kind = @colorizer.colorize("[#{step.kind || :unknown}]", :kind)
29
+ label = @colorizer.colorize("#{index + 1}) #{kind} #{step.owner}##{@method_name}", :owner)
30
+ location = @colorizer.format_source_location(step.source_location, color: false)
31
+ "#{label} #{@colorizer.colorize(location, :location)}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WhyChain
4
+ class Explainer
5
+ # Verbose didactic explanation format.
6
+ class TeachingFormatter
7
+ def initialize(method_name, steps, graph, colorizer)
8
+ @method_name = method_name
9
+ @steps = steps
10
+ @graph = graph
11
+ @colorizer = colorizer
12
+ end
13
+
14
+ def render
15
+ lines = [@colorizer.colorize("Ruby dispatch explanation for :#{@method_name}", :title), ""]
16
+
17
+ @steps.each_with_index do |step, index|
18
+ lines.concat(step_lines(step, index))
19
+ end
20
+
21
+ lines.join("\n")
22
+ end
23
+
24
+ private
25
+
26
+ def step_lines(step, index)
27
+ lines = [
28
+ @colorizer.colorize("#{index + 1}. #{step.owner}##{@method_name}", :owner),
29
+ @colorizer.colorize(" defined at:", :label),
30
+ " #{@colorizer.format_source_location(step.source_location)}"
31
+ ]
32
+
33
+ return lines unless @graph && index < @steps.length - 1
34
+
35
+ lines + ["", @colorizer.colorize(" calls super ->", :arrow), ""]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,21 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "explainer/colorizer"
4
+ require_relative "explainer/teaching_formatter"
5
+ require_relative "explainer/compact_formatter"
6
+
3
7
  module WhyChain
4
8
  # Formats a human-friendly runtime dispatch explanation.
5
9
  class Explainer
6
- def initialize(trace, method_name)
10
+ def initialize(trace, method_name, style: :teaching, color: :auto, graph: true)
7
11
  @trace = trace
8
12
  @method_name = method_name
13
+ @style = style
14
+ @graph = graph
15
+ @colorizer = Colorizer.new(color)
9
16
  end
10
17
 
11
18
  def to_s
12
- lines = ["Ruby dispatch explanation for :#{@method_name}", ""]
13
-
14
- explanation_steps.each_with_index do |step, index|
15
- lines.concat(step_lines(step, index))
19
+ case @style
20
+ when :teaching
21
+ TeachingFormatter.new(@method_name, explanation_steps, @graph, @colorizer).render
22
+ when :compact
23
+ CompactFormatter.new(@method_name, explanation_steps, @graph, @colorizer).render
24
+ else
25
+ raise ArgumentError, "Unknown style: #{@style.inspect}"
16
26
  end
17
-
18
- lines.join("\n")
19
27
  end
20
28
 
21
29
  private
@@ -25,21 +33,5 @@ module WhyChain
25
33
 
26
34
  [DispatchStep.new(owner: @trace.owner, source_location: @trace.source_location)]
27
35
  end
28
-
29
- def format_source_location(source_location)
30
- source_location ? source_location.join(":") : "<native>"
31
- end
32
-
33
- def step_lines(step, index)
34
- lines = [
35
- "#{index + 1}. #{step.owner}##{@method_name}",
36
- " defined at:",
37
- " #{format_source_location(step.source_location)}"
38
- ]
39
-
40
- return lines unless index < explanation_steps.length - 1
41
-
42
- lines + ["", " calls super ->", ""]
43
- end
44
36
  end
45
37
  end
@@ -37,14 +37,27 @@ module WhyChain
37
37
  end
38
38
 
39
39
  def steps
40
+ receiver_class = @object.is_a?(Module) ? @object : @object.class
41
+ receiver_class_index = lookup_chain.index(receiver_class)
42
+
40
43
  lookup_chain.filter_map do |mod|
41
44
  next unless MethodDefinition.defined_directly?(mod, @method_name)
42
45
 
43
46
  DispatchStep.new(
44
47
  owner: mod,
45
- source_location: mod.instance_method(@method_name).source_location
48
+ source_location: mod.instance_method(@method_name).source_location,
49
+ kind: step_kind_for(mod, receiver_class_index)
46
50
  )
47
51
  end
48
52
  end
53
+
54
+ def step_kind_for(mod, receiver_class_index)
55
+ return :singleton if mod == @object.singleton_class
56
+ return :class if mod.is_a?(Class)
57
+
58
+ return :include unless receiver_class_index
59
+
60
+ lookup_chain.index(mod) < receiver_class_index ? :prepend : :include
61
+ end
49
62
  end
50
63
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WhyChain
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/why_chain.rb CHANGED
@@ -16,7 +16,13 @@ module WhyChain
16
16
  Tracer.new(object, method_name).trace
17
17
  end
18
18
 
19
- def self.explain(object, method_name)
20
- Explainer.new(trace(object, method_name), method_name).to_s
19
+ def self.explain(object, method_name, style: :teaching, color: :auto, graph: true)
20
+ Explainer.new(
21
+ trace(object, method_name),
22
+ method_name,
23
+ style: style,
24
+ color: color,
25
+ graph: graph
26
+ ).to_s
21
27
  end
22
28
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: why_chain
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
  - alessio salati
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-11 00:00:00.000000000 Z
11
+ date: 2026-05-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: WhyChain is a tiny educational gem to inspect lookup chain, method owner,
14
14
  and next super target in Ruby.
@@ -30,6 +30,9 @@ files:
30
30
  - lib/why_chain/dispatch_step.rb
31
31
  - lib/why_chain/dispatch_trace.rb
32
32
  - lib/why_chain/explainer.rb
33
+ - lib/why_chain/explainer/colorizer.rb
34
+ - lib/why_chain/explainer/compact_formatter.rb
35
+ - lib/why_chain/explainer/teaching_formatter.rb
33
36
  - lib/why_chain/method_definition.rb
34
37
  - lib/why_chain/method_locator.rb
35
38
  - lib/why_chain/tracer.rb