object_tracer 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|