punchblock 0.7.1 → 0.7.2
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.
- data/CHANGELOG.md +12 -0
- data/lib/punchblock.rb +1 -0
- data/lib/punchblock/command/join.rb +6 -6
- data/lib/punchblock/command/unjoin.rb +6 -6
- data/lib/punchblock/command_node.rb +1 -0
- data/lib/punchblock/component/input.rb +24 -4
- data/lib/punchblock/component/output.rb +5 -1
- data/lib/punchblock/component/tropo/ask.rb +3 -1
- data/lib/punchblock/connection/xmpp.rb +28 -10
- data/lib/punchblock/event/joined.rb +6 -6
- data/lib/punchblock/event/unjoined.rb +6 -6
- data/lib/punchblock/media_container.rb +6 -5
- data/lib/punchblock/protocol_error.rb +5 -0
- data/lib/punchblock/rayo_node.rb +1 -1
- data/lib/punchblock/translator/asterisk.rb +3 -3
- data/lib/punchblock/translator/asterisk/call.rb +9 -3
- data/lib/punchblock/translator/asterisk/component.rb +35 -0
- data/lib/punchblock/translator/asterisk/component/asterisk.rb +1 -0
- data/lib/punchblock/translator/asterisk/component/asterisk/agi_command.rb +14 -23
- data/lib/punchblock/translator/asterisk/component/asterisk/ami_action.rb +5 -18
- data/lib/punchblock/translator/asterisk/component/asterisk/output.rb +94 -0
- data/lib/punchblock/version.rb +1 -1
- data/punchblock.gemspec +1 -0
- data/spec/punchblock/command/join_spec.rb +4 -4
- data/spec/punchblock/command/unjoin_spec.rb +4 -4
- data/spec/punchblock/component/input_spec.rb +28 -31
- data/spec/punchblock/component/output_spec.rb +23 -5
- data/spec/punchblock/component/tropo/ask_spec.rb +31 -34
- data/spec/punchblock/connection/xmpp_spec.rb +105 -3
- data/spec/punchblock/event/joined_spec.rb +4 -4
- data/spec/punchblock/event/unjoined_spec.rb +4 -4
- data/spec/punchblock/protocol_error_spec.rb +32 -1
- data/spec/punchblock/translator/asterisk/call_spec.rb +17 -3
- data/spec/punchblock/translator/asterisk/component/asterisk/agi_command_spec.rb +17 -0
- data/spec/punchblock/translator/asterisk/component/asterisk/output_spec.rb +489 -0
- data/spec/punchblock/translator/asterisk_spec.rb +14 -3
- metadata +53 -44
- data/assets/ozone/ask-1.0.xsd +0 -56
- data/assets/ozone/conference-1.0.xsd +0 -17
- data/assets/ozone/ozone-1.0.xsd +0 -127
- data/assets/ozone/say-1.0.xsd +0 -24
- data/assets/ozone/transfer-1.0.xsd +0 -32
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# develop
|
|
2
2
|
|
|
3
|
+
# v0.7.2 - 2011-12-28
|
|
4
|
+
* Feature: Allow sending commands to mixers easily
|
|
5
|
+
* Feature: Allow configuration of Rayo XMPP domains (root, call and mixer)
|
|
6
|
+
* Feature: Log Blather messages to the trace log level
|
|
7
|
+
* Feature: Return an error when trying to execute an Output on Asterisk with unsupported options set
|
|
8
|
+
* Feature: Add basic support for media output via MRCPSynth on Asterisk
|
|
9
|
+
* API change: Rename mixer_id to mixer_name to align with change to Rayo
|
|
10
|
+
* Bugfix: Handle and expose RubySpeech GRXML documents on Input/Ask properly
|
|
11
|
+
* Bugfix: Compare ProtocolErrors correctly
|
|
12
|
+
* Bugfix: Asterisk media output should default to Asterisk native output (STREAM FILE)
|
|
13
|
+
* Bugfix: An Output node's default max_time value should be nil rather than zero
|
|
14
|
+
|
|
3
15
|
# v0.7.1 - 2011-11-24
|
|
4
16
|
* [FEATURE] Add `Connection#not_ready!`, to instruct the server not to send any more offers.
|
|
5
17
|
* [BUGFIX] Translate all exceptions raised by the XMPP connection into a ProtocolError
|
data/lib/punchblock.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Punchblock
|
|
|
8
8
|
#
|
|
9
9
|
# @param [Hash] options
|
|
10
10
|
# @option options [String, Optional] :other_call_id the call ID to join
|
|
11
|
-
# @option options [String, Optional] :
|
|
11
|
+
# @option options [String, Optional] :mixer_name the mixer name to join
|
|
12
12
|
# @option options [Symbol, Optional] :direction the direction in which media should flow
|
|
13
13
|
# @option options [Symbol, Optional] :media the method by which to negotiate media
|
|
14
14
|
#
|
|
@@ -39,14 +39,14 @@ module Punchblock
|
|
|
39
39
|
|
|
40
40
|
##
|
|
41
41
|
# @return [String] the mixer name to join
|
|
42
|
-
def
|
|
43
|
-
read_attr :'mixer-
|
|
42
|
+
def mixer_name
|
|
43
|
+
read_attr :'mixer-name'
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
##
|
|
47
47
|
# @param [String] other the mixer name to join
|
|
48
|
-
def
|
|
49
|
-
write_attr :'mixer-
|
|
48
|
+
def mixer_name=(other)
|
|
49
|
+
write_attr :'mixer-name', other
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
##
|
|
@@ -74,7 +74,7 @@ module Punchblock
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def inspect_attributes # :nodoc:
|
|
77
|
-
[:other_call_id, :
|
|
77
|
+
[:other_call_id, :mixer_name, :direction, :media] + super
|
|
78
78
|
end
|
|
79
79
|
end # Join
|
|
80
80
|
end # Command
|
|
@@ -8,7 +8,7 @@ module Punchblock
|
|
|
8
8
|
#
|
|
9
9
|
# @param [Hash] options
|
|
10
10
|
# @option options [String, Optional] :other_call_id the call ID to unjoin
|
|
11
|
-
# @option options [String, Optional] :
|
|
11
|
+
# @option options [String, Optional] :mixer_name the mixer name to unjoin
|
|
12
12
|
#
|
|
13
13
|
# @return [Command::Unjoin] a formatted Rayo unjoin command
|
|
14
14
|
#
|
|
@@ -32,18 +32,18 @@ module Punchblock
|
|
|
32
32
|
|
|
33
33
|
##
|
|
34
34
|
# @return [String] the mixer name to unjoin
|
|
35
|
-
def
|
|
36
|
-
read_attr :'mixer-
|
|
35
|
+
def mixer_name
|
|
36
|
+
read_attr :'mixer-name'
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
##
|
|
40
40
|
# @param [String] other the mixer name to unjoin
|
|
41
|
-
def
|
|
42
|
-
write_attr :'mixer-
|
|
41
|
+
def mixer_name=(other)
|
|
42
|
+
write_attr :'mixer-name', other
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def inspect_attributes # :nodoc:
|
|
46
|
-
[:other_call_id, :
|
|
46
|
+
[:other_call_id, :mixer_name] + super
|
|
47
47
|
end
|
|
48
48
|
end # Unjoin
|
|
49
49
|
end # Command
|
|
@@ -208,7 +208,8 @@ module Punchblock
|
|
|
208
208
|
# @return [Choices] the choices available
|
|
209
209
|
#
|
|
210
210
|
def grammar
|
|
211
|
-
|
|
211
|
+
node = find_first 'ns:grammar', :ns => self.class.registered_ns
|
|
212
|
+
Grammar.new node if node
|
|
212
213
|
end
|
|
213
214
|
|
|
214
215
|
##
|
|
@@ -217,6 +218,7 @@ module Punchblock
|
|
|
217
218
|
# @option choices [String] :value the choices available
|
|
218
219
|
#
|
|
219
220
|
def grammar=(other)
|
|
221
|
+
return unless other
|
|
220
222
|
remove_children :grammar
|
|
221
223
|
grammar = Grammar.new(other) unless other.is_a?(Grammar)
|
|
222
224
|
self << grammar
|
|
@@ -252,21 +254,29 @@ module Punchblock
|
|
|
252
254
|
end
|
|
253
255
|
|
|
254
256
|
##
|
|
255
|
-
# @param [String] content_type Defaults to
|
|
257
|
+
# @param [String] content_type Defaults to GRXML
|
|
256
258
|
#
|
|
257
259
|
def content_type=(content_type)
|
|
258
|
-
write_attr 'content-type', content_type ||
|
|
260
|
+
write_attr 'content-type', content_type || grxml_content_type
|
|
259
261
|
end
|
|
260
262
|
|
|
261
263
|
##
|
|
262
264
|
# @return [String] the choices available
|
|
263
265
|
def value
|
|
264
|
-
|
|
266
|
+
if grxml?
|
|
267
|
+
RubySpeech::GRXML.import content
|
|
268
|
+
else
|
|
269
|
+
content
|
|
270
|
+
end
|
|
265
271
|
end
|
|
266
272
|
|
|
267
273
|
##
|
|
268
274
|
# @param [String] value the choices available
|
|
269
275
|
def value=(value)
|
|
276
|
+
return unless value
|
|
277
|
+
if grxml? && !value.is_a?(RubySpeech::GRXML::Element)
|
|
278
|
+
value = RubySpeech::GRXML.import value
|
|
279
|
+
end
|
|
270
280
|
Nokogiri::XML::Builder.with(self) do |xml|
|
|
271
281
|
xml.cdata " #{value} "
|
|
272
282
|
end
|
|
@@ -282,6 +292,16 @@ module Punchblock
|
|
|
282
292
|
def inspect_attributes # :nodoc:
|
|
283
293
|
[:content_type, :value] + super
|
|
284
294
|
end
|
|
295
|
+
|
|
296
|
+
private
|
|
297
|
+
|
|
298
|
+
def grxml_content_type
|
|
299
|
+
'application/grammar+grxml'
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def grxml?
|
|
303
|
+
content_type == grxml_content_type
|
|
304
|
+
end
|
|
285
305
|
end # Choices
|
|
286
306
|
|
|
287
307
|
class Complete
|
|
@@ -109,7 +109,7 @@ module Punchblock
|
|
|
109
109
|
# @return [String] the TTS voice to use
|
|
110
110
|
#
|
|
111
111
|
def max_time
|
|
112
|
-
read_attr
|
|
112
|
+
read_attr :'max-time', :to_i
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
##
|
|
@@ -119,6 +119,10 @@ module Punchblock
|
|
|
119
119
|
write_attr :'max-time', other
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
def inspect_attributes
|
|
123
|
+
super + [:interrupt_on, :start_offset, :start_paused, :repeat_interval, :repeat_times, :max_time]
|
|
124
|
+
end
|
|
125
|
+
|
|
122
126
|
state_machine :state do
|
|
123
127
|
event :paused do
|
|
124
128
|
transition :executing => :paused
|
|
@@ -147,7 +147,8 @@ module Punchblock
|
|
|
147
147
|
# @return [Choices] the choices available
|
|
148
148
|
#
|
|
149
149
|
def choices
|
|
150
|
-
|
|
150
|
+
node = find_first 'ns:choices', :ns => self.class.registered_ns
|
|
151
|
+
Choices.new node if node
|
|
151
152
|
end
|
|
152
153
|
|
|
153
154
|
##
|
|
@@ -156,6 +157,7 @@ module Punchblock
|
|
|
156
157
|
# @option choices [String] :value the choices available
|
|
157
158
|
#
|
|
158
159
|
def choices=(choices)
|
|
160
|
+
return unless choices
|
|
159
161
|
remove_children :choices
|
|
160
162
|
choices = Choices.new(choices) unless choices.is_a?(Choices)
|
|
161
163
|
self << choices
|
|
@@ -9,7 +9,7 @@ module Punchblock
|
|
|
9
9
|
module Connection
|
|
10
10
|
class XMPP < GenericConnection
|
|
11
11
|
include Blather::DSL
|
|
12
|
-
attr_accessor :event_handler
|
|
12
|
+
attr_accessor :event_handler, :root_domain, :calls_domain, :mixers_domain
|
|
13
13
|
|
|
14
14
|
##
|
|
15
15
|
# Initialize the required connection attributes
|
|
@@ -27,7 +27,9 @@ module Punchblock
|
|
|
27
27
|
|
|
28
28
|
setup *[:username, :password, :host, :port, :certs].map { |key| options.delete key }
|
|
29
29
|
|
|
30
|
-
@
|
|
30
|
+
@root_domain = Blather::JID.new(options[:root_domain] || options[:rayo_domain] || @username).domain
|
|
31
|
+
@calls_domain = options[:calls_domain] || "calls.#{@root_domain}"
|
|
32
|
+
@mixers_domain = options[:mixers_domain] || "mixers.#{@root_domain}"
|
|
31
33
|
|
|
32
34
|
@callmap = {} # This hash maps call IDs to their XMPP domain.
|
|
33
35
|
|
|
@@ -37,6 +39,7 @@ module Punchblock
|
|
|
37
39
|
@ping_period = options.has_key?(:ping_period) ? options[:ping_period] : 60
|
|
38
40
|
|
|
39
41
|
Blather.logger = pb_logger
|
|
42
|
+
Blather.default_log_level = :trace if Blather.respond_to? :default_log_level
|
|
40
43
|
|
|
41
44
|
super()
|
|
42
45
|
end
|
|
@@ -54,12 +57,11 @@ module Punchblock
|
|
|
54
57
|
end
|
|
55
58
|
|
|
56
59
|
def prep_command_for_execution(command, options = {})
|
|
57
|
-
|
|
58
|
-
command.
|
|
59
|
-
command.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
create_iq(jid).tap do |iq|
|
|
60
|
+
command.connection = self
|
|
61
|
+
command.call_id ||= options[:call_id]
|
|
62
|
+
command.mixer_name ||= options[:mixer_name]
|
|
63
|
+
command.component_id ||= options[:component_id]
|
|
64
|
+
create_iq(jid_for_command(command)).tap do |iq|
|
|
63
65
|
pb_logger.debug "Sending IQ ID #{iq.id} #{command.inspect} to #{jid}"
|
|
64
66
|
iq << command
|
|
65
67
|
end
|
|
@@ -102,9 +104,25 @@ module Punchblock
|
|
|
102
104
|
|
|
103
105
|
private
|
|
104
106
|
|
|
107
|
+
def jid_for_command(command)
|
|
108
|
+
return root_domain if command.is_a?(Command::Dial)
|
|
109
|
+
|
|
110
|
+
if command.call_id
|
|
111
|
+
node = command.call_id
|
|
112
|
+
domain = @callmap[command.call_id] || calls_domain
|
|
113
|
+
elsif command.mixer_name
|
|
114
|
+
node = command.mixer_name
|
|
115
|
+
domain = @callmap[command.mixer_name] || mixers_domain
|
|
116
|
+
else
|
|
117
|
+
domain = calls_domain
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
Blather::JID.new(node, domain, command.component_id).to_s
|
|
121
|
+
end
|
|
122
|
+
|
|
105
123
|
def send_presence(presence)
|
|
106
124
|
status = Blather::Stanza::Presence::Status.new presence
|
|
107
|
-
status.to =
|
|
125
|
+
status.to = root_domain
|
|
108
126
|
client.write status
|
|
109
127
|
end
|
|
110
128
|
|
|
@@ -160,7 +178,7 @@ module Punchblock
|
|
|
160
178
|
end
|
|
161
179
|
|
|
162
180
|
def ping_rayo
|
|
163
|
-
client.write_with_handler Blather::Stanza::Iq::Ping.new(:set,
|
|
181
|
+
client.write_with_handler Blather::Stanza::Iq::Ping.new(:set, root_domain) do |response|
|
|
164
182
|
begin
|
|
165
183
|
handle_error response if response.is_a? Blather::BlatherError
|
|
166
184
|
rescue ProtocolError => e
|
|
@@ -8,7 +8,7 @@ module Punchblock
|
|
|
8
8
|
#
|
|
9
9
|
# @param [Hash] options
|
|
10
10
|
# @option options [String, Optional] :other_call_id the call ID that was joined
|
|
11
|
-
# @option options [String, Optional] :
|
|
11
|
+
# @option options [String, Optional] :mixer_name the mixer name that was joined
|
|
12
12
|
#
|
|
13
13
|
# @return [Event::Joined] a formatted Rayo joined event
|
|
14
14
|
#
|
|
@@ -32,18 +32,18 @@ module Punchblock
|
|
|
32
32
|
|
|
33
33
|
##
|
|
34
34
|
# @return [String] the mixer name that was joined
|
|
35
|
-
def
|
|
36
|
-
read_attr :'mixer-
|
|
35
|
+
def mixer_name
|
|
36
|
+
read_attr :'mixer-name'
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
##
|
|
40
40
|
# @param [String] other the mixer name that was joined
|
|
41
|
-
def
|
|
42
|
-
write_attr :'mixer-
|
|
41
|
+
def mixer_name=(other)
|
|
42
|
+
write_attr :'mixer-name', other
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def inspect_attributes # :nodoc:
|
|
46
|
-
[:other_call_id, :
|
|
46
|
+
[:other_call_id, :mixer_name] + super
|
|
47
47
|
end
|
|
48
48
|
end # Joined
|
|
49
49
|
end
|
|
@@ -8,7 +8,7 @@ module Punchblock
|
|
|
8
8
|
#
|
|
9
9
|
# @param [Hash] options
|
|
10
10
|
# @option options [String, Optional] :other_call_id the call ID that was unjoined
|
|
11
|
-
# @option options [String, Optional] :
|
|
11
|
+
# @option options [String, Optional] :mixer_name the mixer name that was unjoined
|
|
12
12
|
#
|
|
13
13
|
# @return [Event::Unjoined] a formatted Rayo unjoined event
|
|
14
14
|
#
|
|
@@ -32,18 +32,18 @@ module Punchblock
|
|
|
32
32
|
|
|
33
33
|
##
|
|
34
34
|
# @return [String] the mixer name that was unjoined
|
|
35
|
-
def
|
|
36
|
-
read_attr :'mixer-
|
|
35
|
+
def mixer_name
|
|
36
|
+
read_attr :'mixer-name'
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
##
|
|
40
40
|
# @param [String] other the mixer name that was unjoined
|
|
41
|
-
def
|
|
42
|
-
write_attr :'mixer-
|
|
41
|
+
def mixer_name=(other)
|
|
42
|
+
write_attr :'mixer-name', other
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def inspect_attributes # :nodoc:
|
|
46
|
-
[:other_call_id, :
|
|
46
|
+
[:other_call_id, :mixer_name] + super
|
|
47
47
|
end
|
|
48
48
|
end # Unjoined
|
|
49
49
|
end
|
|
@@ -18,18 +18,19 @@ module Punchblock
|
|
|
18
18
|
# @return [String] the SSML document to render TTS
|
|
19
19
|
#
|
|
20
20
|
def ssml
|
|
21
|
-
children.
|
|
21
|
+
node = children.first
|
|
22
|
+
RubySpeech::SSML.import node if node
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
##
|
|
25
26
|
# @param [String] ssml the SSML document to render TTS
|
|
26
27
|
#
|
|
27
28
|
def ssml=(ssml)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
end
|
|
29
|
+
return unless ssml
|
|
30
|
+
unless ssml.is_a?(RubySpeech::SSML::Element)
|
|
31
|
+
ssml = RubySpeech::SSML.import ssml
|
|
32
32
|
end
|
|
33
|
+
self << ssml
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def inspect_attributes # :nodoc:
|
|
@@ -12,5 +12,10 @@ module Punchblock
|
|
|
12
12
|
"#<#{self.class}: name=#{name.inspect} text=#{text.inspect} call_id=#{call_id.inspect} component_id=#{component_id.inspect}>"
|
|
13
13
|
end
|
|
14
14
|
alias :inspect :to_s
|
|
15
|
+
|
|
16
|
+
def eql?(other)
|
|
17
|
+
other.is_a?(self.class) && [:name, :text, :call_id, :component_id].all? { |f| self.__send__(f) == other.__send__(f) }
|
|
18
|
+
end
|
|
19
|
+
alias :== :eql?
|
|
15
20
|
end
|
|
16
21
|
end
|
data/lib/punchblock/rayo_node.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Punchblock
|
|
|
7
7
|
|
|
8
8
|
class_attribute :registered_ns, :registered_name
|
|
9
9
|
|
|
10
|
-
attr_accessor :call_id, :component_id, :domain, :connection, :client, :original_component
|
|
10
|
+
attr_accessor :call_id, :mixer_name, :component_id, :domain, :connection, :client, :original_component
|
|
11
11
|
|
|
12
12
|
# Register a new stanza class to a name and/or namespace
|
|
13
13
|
#
|
|
@@ -11,11 +11,11 @@ module Punchblock
|
|
|
11
11
|
autoload :Call
|
|
12
12
|
autoload :Component
|
|
13
13
|
|
|
14
|
-
attr_reader :ami_client, :connection, :calls
|
|
14
|
+
attr_reader :ami_client, :connection, :media_engine, :calls
|
|
15
15
|
|
|
16
|
-
def initialize(ami_client, connection)
|
|
16
|
+
def initialize(ami_client, connection, media_engine = nil)
|
|
17
17
|
pb_logger.debug "Starting up..."
|
|
18
|
-
@ami_client, @connection = ami_client, connection
|
|
18
|
+
@ami_client, @connection, @media_engine = ami_client, connection, media_engine
|
|
19
19
|
@calls, @components, @channel_to_call_id = {}, {}, {}
|
|
20
20
|
@fully_booted_count = 0
|
|
21
21
|
end
|
|
@@ -64,12 +64,14 @@ module Punchblock
|
|
|
64
64
|
end
|
|
65
65
|
when Punchblock::Component::Asterisk::AGI::Command
|
|
66
66
|
execute_agi_command command
|
|
67
|
+
when Punchblock::Component::Output
|
|
68
|
+
execute_component Component::Asterisk::Output, command
|
|
67
69
|
end
|
|
68
70
|
end
|
|
69
71
|
|
|
70
|
-
def send_agi_action(command, &block)
|
|
72
|
+
def send_agi_action(command, *params, &block)
|
|
71
73
|
pb_logger.debug "Sending AGI action #{command}"
|
|
72
|
-
@current_agi_command = Punchblock::Component::Asterisk::AGI::Command.new :name => command, :call_id => id
|
|
74
|
+
@current_agi_command = Punchblock::Component::Asterisk::AGI::Command.new :name => command, :params => params, :call_id => id
|
|
73
75
|
@current_agi_command.request!
|
|
74
76
|
@current_agi_command.register_handler :internal, Punchblock::Event::Complete do |e|
|
|
75
77
|
pb_logger.debug "AGI action received complete event #{e.inspect}"
|
|
@@ -89,7 +91,11 @@ module Punchblock
|
|
|
89
91
|
private
|
|
90
92
|
|
|
91
93
|
def execute_agi_command(command)
|
|
92
|
-
Component::Asterisk::AGICommand
|
|
94
|
+
execute_component Component::Asterisk::AGICommand, command
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def execute_component(type, command)
|
|
98
|
+
type.new(command, current_actor).tap do |component|
|
|
93
99
|
register_component component
|
|
94
100
|
component.execute!
|
|
95
101
|
end
|