object_tracer 1.0.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 +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/gempush.yml +28 -0
- data/.github/workflows/ruby.yml +59 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +296 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +21 -0
- data/Makefile +3 -0
- data/README.md +310 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/images/print_calls - single entry.png +0 -0
- data/images/print_calls.png +0 -0
- data/images/print_mutations.png +0 -0
- data/images/print_traces.png +0 -0
- data/lib/object_tracer.rb +258 -0
- data/lib/object_tracer/configuration.rb +34 -0
- data/lib/object_tracer/exceptions.rb +16 -0
- data/lib/object_tracer/manageable.rb +37 -0
- data/lib/object_tracer/method_hijacker.rb +55 -0
- data/lib/object_tracer/output.rb +41 -0
- data/lib/object_tracer/output/payload_wrapper.rb +186 -0
- data/lib/object_tracer/output/writer.rb +22 -0
- data/lib/object_tracer/payload.rb +40 -0
- data/lib/object_tracer/trackable.rb +133 -0
- data/lib/object_tracer/trackers/association_call_tracker.rb +17 -0
- data/lib/object_tracer/trackers/initialization_tracker.rb +53 -0
- data/lib/object_tracer/trackers/method_call_tracker.rb +9 -0
- data/lib/object_tracer/trackers/mutation_tracker.rb +111 -0
- data/lib/object_tracer/trackers/passed_tracker.rb +16 -0
- data/lib/object_tracer/version.rb +3 -0
- data/object_tracer.gemspec +29 -0
- metadata +113 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
class Configuration
|
3
|
+
DEFAULTS = {
|
4
|
+
filter_by_paths: [],
|
5
|
+
exclude_by_paths: [],
|
6
|
+
with_trace_to: 50,
|
7
|
+
event_type: :return,
|
8
|
+
hijack_attr_methods: false,
|
9
|
+
track_as_records: false,
|
10
|
+
ignore_private: false,
|
11
|
+
only_private: false
|
12
|
+
}.merge(ObjectTracer::Output::DEFAULT_OPTIONS)
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@options = {}
|
16
|
+
|
17
|
+
DEFAULTS.each do |key, value|
|
18
|
+
@options[key] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](key)
|
23
|
+
@options[key]
|
24
|
+
end
|
25
|
+
|
26
|
+
def []=(key, value)
|
27
|
+
@options[key] = value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.config
|
32
|
+
@config ||= Configuration.new
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
class Exception < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class NotAnActiveRecordInstanceError < Exception
|
6
|
+
def initialize(object)
|
7
|
+
super("target object should be an instance of ActiveRecord::Base, got #{object}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class NotAClassError < Exception
|
12
|
+
def initialize(object)
|
13
|
+
super("target object should be a class, got #{object}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
module Manageable
|
3
|
+
|
4
|
+
def suspend_new
|
5
|
+
@suspend_new
|
6
|
+
end
|
7
|
+
|
8
|
+
# list all registered devices
|
9
|
+
def devices
|
10
|
+
@devices
|
11
|
+
end
|
12
|
+
|
13
|
+
# disable given device and remove it from registered list
|
14
|
+
def delete_device(device)
|
15
|
+
device.trace_point&.disable
|
16
|
+
@devices -= [device]
|
17
|
+
end
|
18
|
+
|
19
|
+
# stops all registered devices and remove them from registered list
|
20
|
+
def stop_all!
|
21
|
+
@devices.each(&:stop!)
|
22
|
+
end
|
23
|
+
|
24
|
+
# suspend enabling new trace points
|
25
|
+
# user can still create new Device instances, but they won't be functional
|
26
|
+
def suspend_new!
|
27
|
+
@suspend_new = true
|
28
|
+
end
|
29
|
+
|
30
|
+
# reset everything to clean state and disable all devices
|
31
|
+
def reset!
|
32
|
+
@suspend_new = false
|
33
|
+
stop_all!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
class MethodHijacker
|
3
|
+
attr_reader :target
|
4
|
+
|
5
|
+
def initialize(target)
|
6
|
+
@target = target
|
7
|
+
end
|
8
|
+
|
9
|
+
def hijack_methods!
|
10
|
+
target.methods.each do |method_name|
|
11
|
+
if is_writer_method?(method_name)
|
12
|
+
redefine_writer_method!(method_name)
|
13
|
+
elsif is_reader_method?(method_name)
|
14
|
+
redefine_reader_method!(method_name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def is_writer_method?(method_name)
|
22
|
+
has_definition_source?(method_name) && method_name.match?(/\w+=/) && target.method(method_name).source.match?(/attr_writer|attr_accessor/)
|
23
|
+
rescue MethodSource::SourceNotFoundError
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def is_reader_method?(method_name)
|
28
|
+
has_definition_source?(method_name) && target.method(method_name).source.match?(/attr_reader|attr_accessor/)
|
29
|
+
rescue MethodSource::SourceNotFoundError
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_definition_source?(method_name)
|
34
|
+
target.method(method_name).source_location
|
35
|
+
end
|
36
|
+
|
37
|
+
def redefine_writer_method!(method_name)
|
38
|
+
ivar_name = "@#{method_name.to_s.sub("=", "")}"
|
39
|
+
|
40
|
+
target.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
41
|
+
def #{method_name}(val)
|
42
|
+
#{ivar_name} = val
|
43
|
+
end
|
44
|
+
RUBY
|
45
|
+
end
|
46
|
+
|
47
|
+
def redefine_reader_method!(method_name)
|
48
|
+
target.instance_eval <<-RUBY, __FILE__, __LINE__ + 1
|
49
|
+
def #{method_name}
|
50
|
+
@#{method_name}
|
51
|
+
end
|
52
|
+
RUBY
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "object_tracer/output/payload_wrapper"
|
3
|
+
require "object_tracer/output/writer"
|
4
|
+
|
5
|
+
class ObjectTracer
|
6
|
+
module Output
|
7
|
+
DEFAULT_OPTIONS = {
|
8
|
+
inspect: false,
|
9
|
+
colorize: true,
|
10
|
+
log_file: "/tmp/object_tracer.log"
|
11
|
+
}
|
12
|
+
|
13
|
+
module Helpers
|
14
|
+
def and_write(payload_method = nil, options: {}, &block)
|
15
|
+
and_output(payload_method, options: options, logger: Logger.new(options[:log_file]), &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def and_print(payload_method = nil, options: {}, &block)
|
19
|
+
and_output(payload_method, options: options, logger: Logger.new($stdout), &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def and_output(payload_method = nil, options: {}, logger:, &block)
|
23
|
+
output_block = generate_output_block(payload_method, block)
|
24
|
+
@output_writer = Writer.new(options: options, output_block: output_block, logger: logger)
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def generate_output_block(payload_method, block)
|
31
|
+
if block
|
32
|
+
block
|
33
|
+
elsif payload_method
|
34
|
+
-> (output_payload, output_options) { output_payload.send(payload_method, output_options) }
|
35
|
+
else
|
36
|
+
raise "need to provide either a payload method name or a block"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require "pastel"
|
2
|
+
|
3
|
+
class ObjectTracer
|
4
|
+
module Output
|
5
|
+
class PayloadWrapper
|
6
|
+
UNDEFINED = "[undefined]"
|
7
|
+
PRIVATE_MARK = " (private)"
|
8
|
+
|
9
|
+
PASTEL = Pastel.new
|
10
|
+
PASTEL.alias_color(:orange, :bright_red, :bright_yellow)
|
11
|
+
|
12
|
+
ObjectTracer::Payload::ATTRS.each do |attr|
|
13
|
+
define_method attr do |options = {}|
|
14
|
+
@payload.send(attr)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
alias :is_private_call? :is_private_call
|
19
|
+
|
20
|
+
def method_head
|
21
|
+
@payload.method_head
|
22
|
+
end
|
23
|
+
|
24
|
+
def location(options = {})
|
25
|
+
@payload.location(options)
|
26
|
+
end
|
27
|
+
|
28
|
+
alias :raw_arguments :arguments
|
29
|
+
alias :raw_return_value :return_value
|
30
|
+
|
31
|
+
def initialize(payload)
|
32
|
+
@payload = payload
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_name(options = {})
|
36
|
+
name = ":#{@payload.method_name}"
|
37
|
+
|
38
|
+
name += " [#{tag}]" if tag
|
39
|
+
name += PRIVATE_MARK if is_private_call?
|
40
|
+
|
41
|
+
name
|
42
|
+
end
|
43
|
+
|
44
|
+
def arguments(options = {})
|
45
|
+
generate_string_result(raw_arguments, options[:inspect])
|
46
|
+
end
|
47
|
+
|
48
|
+
def return_value(options = {})
|
49
|
+
generate_string_result(raw_return_value, options[:inspect])
|
50
|
+
end
|
51
|
+
|
52
|
+
PAYLOAD_ATTRIBUTES = {
|
53
|
+
method_name: {symbol: "", color: :bright_blue},
|
54
|
+
location: {symbol: "from:", color: :green},
|
55
|
+
return_value: {symbol: "=>", color: :magenta},
|
56
|
+
arguments: {symbol: "<=", color: :orange},
|
57
|
+
ivar_changes: {symbol: "changes:\n", color: :blue},
|
58
|
+
defined_class: {symbol: "#", color: :yellow}
|
59
|
+
}
|
60
|
+
|
61
|
+
PAYLOAD_ATTRIBUTES.each do |attribute, attribute_options|
|
62
|
+
color = attribute_options[:color]
|
63
|
+
|
64
|
+
alias_method "original_#{attribute}".to_sym, attribute
|
65
|
+
|
66
|
+
# regenerate attributes with `colorize: true` support
|
67
|
+
define_method attribute do |options = {}|
|
68
|
+
call_result = send("original_#{attribute}", options).to_s
|
69
|
+
|
70
|
+
if options[:colorize]
|
71
|
+
PASTEL.send(color, call_result)
|
72
|
+
else
|
73
|
+
call_result
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
define_method "#{attribute}_with_color" do |options = {}|
|
78
|
+
send(attribute, options.merge(colorize: true))
|
79
|
+
end
|
80
|
+
|
81
|
+
PAYLOAD_ATTRIBUTES.each do |and_attribute, and_attribute_options|
|
82
|
+
next if and_attribute == attribute
|
83
|
+
|
84
|
+
define_method "#{attribute}_and_#{and_attribute}" do |options = {}|
|
85
|
+
"#{send(attribute, options)} #{and_attribute_options[:symbol]} #{send(and_attribute, options)}"
|
86
|
+
end
|
87
|
+
|
88
|
+
define_method "#{attribute}_and_#{and_attribute}_with_color" do |options = {}|
|
89
|
+
send("#{attribute}_and_#{and_attribute}", options.merge(colorize: true))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def passed_at(options = {})
|
95
|
+
with_method_head = options.fetch(:with_method_head, false)
|
96
|
+
arg_name = raw_arguments.keys.detect { |k| raw_arguments[k] == target }
|
97
|
+
|
98
|
+
return unless arg_name
|
99
|
+
|
100
|
+
arg_name = ":#{arg_name}"
|
101
|
+
arg_name = PASTEL.orange(arg_name) if options[:colorize]
|
102
|
+
msg = "Passed as #{arg_name} in '#{defined_class(options)}##{method_name(options)}' at #{location(options)}\n"
|
103
|
+
msg += " > #{method_head}\n" if with_method_head
|
104
|
+
msg
|
105
|
+
end
|
106
|
+
|
107
|
+
def detail_call_info(options = {})
|
108
|
+
<<~MSG
|
109
|
+
#{method_name_and_defined_class(options)}
|
110
|
+
from: #{location(options)}
|
111
|
+
<= #{arguments(options)}
|
112
|
+
=> #{return_value(options)}
|
113
|
+
|
114
|
+
MSG
|
115
|
+
end
|
116
|
+
|
117
|
+
def ivar_changes(options = {})
|
118
|
+
@payload.ivar_changes.map do |ivar, value_changes|
|
119
|
+
before = generate_string_result(value_changes[:before], options[:inspect])
|
120
|
+
after = generate_string_result(value_changes[:after], options[:inspect])
|
121
|
+
|
122
|
+
if options[:colorize]
|
123
|
+
ivar = PASTEL.orange(ivar.to_s)
|
124
|
+
before = PASTEL.bright_blue(before.to_s)
|
125
|
+
after = PASTEL.bright_blue(after.to_s)
|
126
|
+
end
|
127
|
+
|
128
|
+
" #{ivar}: #{before} => #{after}"
|
129
|
+
end.join("\n")
|
130
|
+
end
|
131
|
+
|
132
|
+
def call_info_with_ivar_changes(options = {})
|
133
|
+
<<~MSG
|
134
|
+
#{method_name_and_defined_class(options)}
|
135
|
+
from: #{location(options)}
|
136
|
+
changes:
|
137
|
+
#{ivar_changes(options)}
|
138
|
+
|
139
|
+
MSG
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def generate_string_result(obj, inspect)
|
145
|
+
case obj
|
146
|
+
when Array
|
147
|
+
array_to_string(obj, inspect)
|
148
|
+
when Hash
|
149
|
+
hash_to_string(obj, inspect)
|
150
|
+
when UNDEFINED
|
151
|
+
UNDEFINED
|
152
|
+
when String
|
153
|
+
"\"#{obj}\""
|
154
|
+
when nil
|
155
|
+
"nil"
|
156
|
+
else
|
157
|
+
inspect ? obj.inspect : obj.to_s
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def array_to_string(array, inspect)
|
162
|
+
elements_string = array.map do |elem|
|
163
|
+
generate_string_result(elem, inspect)
|
164
|
+
end.join(", ")
|
165
|
+
"[#{elements_string}]"
|
166
|
+
end
|
167
|
+
|
168
|
+
def hash_to_string(hash, inspect)
|
169
|
+
elements_string = hash.map do |key, value|
|
170
|
+
"#{key.to_s}: #{generate_string_result(value, inspect)}"
|
171
|
+
end.join(", ")
|
172
|
+
"{#{elements_string}}"
|
173
|
+
end
|
174
|
+
|
175
|
+
def obj_to_string(element, inspect)
|
176
|
+
to_string_method = inspect ? :inspect : :to_s
|
177
|
+
|
178
|
+
if !inspect && element.is_a?(String)
|
179
|
+
"\"#{element}\""
|
180
|
+
else
|
181
|
+
element.send(to_string_method)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
module Output
|
3
|
+
class Writer
|
4
|
+
def initialize(options:, output_block:, logger:)
|
5
|
+
@options = options
|
6
|
+
@output_block = output_block
|
7
|
+
@logger = logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def write!(payload)
|
11
|
+
output = generate_output(payload)
|
12
|
+
@logger << output
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def generate_output(payload)
|
18
|
+
@output_block.call(PayloadWrapper.new(payload), @options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
class Payload
|
3
|
+
ATTRS = [
|
4
|
+
:target, :receiver, :method_name, :method_object, :arguments, :return_value, :filepath, :line_number,
|
5
|
+
:defined_class, :trace, :tag, :tp, :ivar_changes, :is_private_call
|
6
|
+
]
|
7
|
+
|
8
|
+
attr_accessor(*ATTRS)
|
9
|
+
|
10
|
+
alias :is_private_call? :is_private_call
|
11
|
+
|
12
|
+
def initialize(
|
13
|
+
target:, receiver:, method_name:, method_object:, arguments:, return_value:, filepath:, line_number:,
|
14
|
+
defined_class:, trace:, tag:, tp:, is_private_call:
|
15
|
+
)
|
16
|
+
@target = target
|
17
|
+
@receiver = receiver
|
18
|
+
@method_name = method_name
|
19
|
+
@method_object = method_object
|
20
|
+
@arguments = arguments
|
21
|
+
@return_value = return_value
|
22
|
+
@filepath = filepath
|
23
|
+
@line_number = line_number
|
24
|
+
@defined_class = defined_class
|
25
|
+
@trace = trace
|
26
|
+
@tag = tag
|
27
|
+
@tp = tp
|
28
|
+
@ivar_changes = {}
|
29
|
+
@is_private_call = is_private_call
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_head
|
33
|
+
method_object.source.strip if method_object.source_location
|
34
|
+
end
|
35
|
+
|
36
|
+
def location(options = {})
|
37
|
+
"#{filepath}:#{line_number}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
class ObjectTracer
|
2
|
+
module Trackable
|
3
|
+
def tap_init!(object, options = {}, &block)
|
4
|
+
ObjectTracer::Trackers::InitializationTracker.new(options, &block).track(object)
|
5
|
+
end
|
6
|
+
|
7
|
+
def tap_passed!(object, options = {}, &block)
|
8
|
+
ObjectTracer::Trackers::PassedTracker.new(options, &block).track(object)
|
9
|
+
end
|
10
|
+
|
11
|
+
def tap_assoc!(object, options = {}, &block)
|
12
|
+
ObjectTracer::Trackers::AssociactionCallTracker.new(options, &block).track(object)
|
13
|
+
end
|
14
|
+
|
15
|
+
def tap_on!(object, options = {}, &block)
|
16
|
+
ObjectTracer::Trackers::MethodCallTracker.new(options, &block).track(object)
|
17
|
+
end
|
18
|
+
|
19
|
+
def tap_mutation!(object, options = {}, &block)
|
20
|
+
ObjectTracer::Trackers::MutationTracker.new(options, &block).track(object)
|
21
|
+
end
|
22
|
+
|
23
|
+
[:calls, :traces, :mutations].each do |subject|
|
24
|
+
[:print, :write].each do |output_action|
|
25
|
+
helper_method_name = "#{output_action}_#{subject}"
|
26
|
+
|
27
|
+
define_method helper_method_name do |target, options = {}|
|
28
|
+
send("output_#{subject}", target, options, output_action: "and_#{output_action}")
|
29
|
+
end
|
30
|
+
|
31
|
+
define_method "with_#{helper_method_name}" do |options = {}|
|
32
|
+
send(helper_method_name, self, options)
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
define_method "#{output_action}_instance_#{subject}" do |target_klass, options = {}|
|
37
|
+
collection_proxy = AsyncCollectionProxy.new
|
38
|
+
|
39
|
+
tap_init!(target_klass, options.merge(force_recording: true)) do |payload|
|
40
|
+
collection_proxy << send(helper_method_name, payload.return_value, options)
|
41
|
+
end
|
42
|
+
|
43
|
+
collection_proxy
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def output_calls(target, options = {}, output_action:)
|
51
|
+
device_options, output_options = separate_options(options)
|
52
|
+
|
53
|
+
tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
54
|
+
output_payload.detail_call_info(output_options)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def output_traces(target, options = {}, output_action:)
|
59
|
+
device_options, output_options = separate_options(options)
|
60
|
+
device_options[:event_type] = :call
|
61
|
+
|
62
|
+
device_1 = tap_on!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
63
|
+
"Called #{output_payload.method_name_and_location(output_options)}\n"
|
64
|
+
end
|
65
|
+
device_2 = tap_passed!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
66
|
+
output_payload.passed_at(output_options)
|
67
|
+
end
|
68
|
+
CollectionProxy.new([device_1, device_2])
|
69
|
+
end
|
70
|
+
|
71
|
+
def output_mutations(target, options = {}, output_action:)
|
72
|
+
device_options, output_options = separate_options(options)
|
73
|
+
|
74
|
+
tap_mutation!(target, device_options).send(output_action, options: output_options) do |output_payload, output_options|
|
75
|
+
output_payload.call_info_with_ivar_changes(output_options)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def separate_options(options)
|
80
|
+
output_options = Output::DEFAULT_OPTIONS.keys.each_with_object({}) do |key, hash|
|
81
|
+
hash[key] = options.fetch(key, ObjectTracer.config[key])
|
82
|
+
options.delete(key)
|
83
|
+
end
|
84
|
+
|
85
|
+
[options, output_options]
|
86
|
+
end
|
87
|
+
|
88
|
+
# CollectionProxy delegates chained actions to multiple devices
|
89
|
+
class CollectionProxy
|
90
|
+
CHAINABLE_ACTIONS = [:stop!, :stop_when, :with]
|
91
|
+
|
92
|
+
def initialize(devices)
|
93
|
+
@devices = devices
|
94
|
+
end
|
95
|
+
|
96
|
+
CHAINABLE_ACTIONS.each do |method|
|
97
|
+
define_method method do |&block|
|
98
|
+
@devices.each do |device|
|
99
|
+
device.send(method, &block)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# AsyncCollectionProxy delegates chained actions to multiple device "asyncronously"
|
106
|
+
# when we use tapping methods like `tap_init!` to create sub-devices
|
107
|
+
# we need to find a way to pass the chained actions to every sub-device that's created
|
108
|
+
# and this can only happen asyncronously as we won't know when'll that happen
|
109
|
+
class AsyncCollectionProxy < CollectionProxy
|
110
|
+
def initialize(devices = [])
|
111
|
+
super
|
112
|
+
@blocks = {}
|
113
|
+
end
|
114
|
+
|
115
|
+
CHAINABLE_ACTIONS.each do |method|
|
116
|
+
define_method method do |&block|
|
117
|
+
super(&block)
|
118
|
+
@blocks[method] = block
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def <<(device)
|
123
|
+
@devices << device
|
124
|
+
|
125
|
+
@blocks.each do |method, block|
|
126
|
+
device.send(method, &block)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
include ObjectTracer::Trackable
|