Module: CalledUncalled

Defined in:
lib/called_uncalled.rb,
lib/called_uncalled/version.rb

Constant Summary collapse

VERSION =
"1.0.0"

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/called_uncalled.rb', line 7

def self.included(base)

  base.define_singleton_method(:called_methods) do |*sources|
    # Default to the class itself if no sources are given
    sources.push(base) if sources.empty?
    # If an instance is given, change it to be both the class and the singleton class
    sources = sources.flatten.map { |source| source.is_a?(base) ? [source.class, source.singleton_class] : source }.flatten
    # Select all methods that come from the given sources
    base.class_variable_get(:@@called_methods).select { |m| sources.include?(:all) || sources.include?(m.owner) }.map(&:name)
  end

  base.define_singleton_method(:uncalled_methods) do |*sources|
    # Default to the class itself if no sources are given
    sources.push(base) if sources.empty?
    # If an instance is given, change it to be both the class and the singleton class
    sources = sources.flatten.map { |source| source.is_a?(base) ? [source.class, source.singleton_class] : source }.flatten
    # Select all methods that come from the given sources
    base.class_variable_get(:@@uncalled_methods).select { |m| sources.include?(:all) || sources.include?(m.owner) }.map(&:name)
  end

  observe = ->(base, klass,  mthd) do
    method_name = mthd.name
    # Add the method name to uncalled methods
    klass.class_variable_get(:@@uncalled_methods) << mthd
    # Define a wrapping method
    base.define_method(method_name) do |*args, **kwargs, &block|
      # Remove ourselves from the uncalled list and add to the called list
      newly_called = klass.class_variable_get(:@@uncalled_methods).find { |x| x.name == method_name }
      if newly_called
        klass.class_variable_get(:@@uncalled_methods).delete(newly_called)
        # Add ourselves to the called list if this is the first time we were called
        klass.class_variable_get(:@@called_methods).push(newly_called)
      end

      # Make the original call
      super(*args, **kwargs, &block)
    end
  end

  singleton_method_wrapper = Module.new

  method_wrapper = Module.new do
    # Track called and uncalled methods
    base.class_variable_set(:@@called_methods, [])
    base.class_variable_set(:@@uncalled_methods, [])

    # Add the existing methods to the method wrapper
    method_names = base.instance_methods
    method_names.each { |method_name|
      mthd = base.instance_method(method_name)
      observe.call(self, base, mthd)
    }

    define_method(:singleton_method_added) do |method_name|
      mthd = singleton_method(method_name)
      # Add the new method to the existing singleton method wrapper
      singleton_method_wrapper.module_eval { observe.call(self, base, mthd) }
      singleton_class.prepend(singleton_method_wrapper) unless singleton_class < singleton_method_wrapper
      # Special case deletion as we may be overriding the observer we added on the existing methods
      newly_called = base.class_variable_get(:@@uncalled_methods).find { |x| x.name == :singleton_method_added }
      if newly_called
        base.class_variable_get(:@@uncalled_methods).delete(newly_called)
        base.class_variable_get(:@@called_methods).push(newly_called)
      end
      # Call the original singleton_method_added
      super(method_name)
    end
  end

  method_added_wrapper = Module.new do
    define_method(:method_added) do |method_name|
      mthd = base.instance_method(method_name)
      # Add the new method to the existing method wrapper
      method_wrapper.module_eval { observe.call(self, base, mthd) }
      # Call the original method_added
      super(method_name)
    end
  end
  # Start watching for new methods
  base.singleton_class.prepend(method_added_wrapper)
  # Watch all existing new singleton methods
  base.prepend(method_wrapper)
end