larynx 0.1.5 → 0.1.6
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/bin/larynx +1 -0
- data/lib/larynx.rb +2 -91
- data/lib/larynx/field.rb +122 -0
- data/lib/larynx/form.rb +49 -3
- data/lib/larynx/menu.rb +4 -0
- data/lib/larynx/version.rb +1 -1
- data/spec/larynx/field_spec.rb +218 -0
- data/spec/larynx/form_spec.rb +44 -0
- data/spec/larynx/menu_spec.rb +11 -0
- metadata +8 -6
- data/lib/larynx/fields.rb +0 -184
- data/spec/larynx/fields_spec.rb +0 -271
data/bin/larynx
CHANGED
data/lib/larynx.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'eventmachine'
|
3
3
|
require 'active_support'
|
4
|
-
require 'logger'
|
5
|
-
require 'daemons/daemonize'
|
6
4
|
|
7
5
|
require 'larynx/version'
|
8
6
|
require 'larynx/logger'
|
7
|
+
require 'larynx/server'
|
9
8
|
require 'larynx/observable'
|
10
9
|
require 'larynx/callbacks'
|
11
10
|
require 'larynx/callbacks_with_async'
|
@@ -15,7 +14,7 @@ require 'larynx/command'
|
|
15
14
|
require 'larynx/commands'
|
16
15
|
require 'larynx/prompt'
|
17
16
|
require 'larynx/application'
|
18
|
-
require 'larynx/
|
17
|
+
require 'larynx/field'
|
19
18
|
require 'larynx/form'
|
20
19
|
require 'larynx/restartable_timer'
|
21
20
|
require 'larynx/call_handler'
|
@@ -23,97 +22,9 @@ require 'larynx/call_handler'
|
|
23
22
|
module Larynx
|
24
23
|
class << self
|
25
24
|
include Callbacks
|
26
|
-
|
27
25
|
define_callback :connect, :answer, :hungup
|
28
|
-
|
29
|
-
def parse_options(args=ARGV)
|
30
|
-
@options = {
|
31
|
-
:ip => "0.0.0.0",
|
32
|
-
:port => 8084,
|
33
|
-
:pid_file => "./larynx.pid",
|
34
|
-
:log_file => "./larynx.log"
|
35
|
-
}
|
36
|
-
opts = OptionParser.new
|
37
|
-
opts.banner = "Usage: larynx [options]"
|
38
|
-
opts.separator ''
|
39
|
-
opts.separator "Larynx is a framework to develop FreeSWITCH IVR applications in Ruby."
|
40
|
-
opts.on('-i', '--ip IP', 'Listen for connections on this IP') {|ip| @options[:ip] = ip }
|
41
|
-
opts.on('-p', '--port PORT', 'Listen on this port', Integer) {|port| @options[:port] = port }
|
42
|
-
opts.on('-d', '--daemonize', 'Run as daemon') { @options[:daemonize] = true }
|
43
|
-
opts.on('-l', '--log-file FILE', 'Defaults to /app/root/larynx.log') {|log| @options[:log_file] = log }
|
44
|
-
opts.on( '--pid-file FILE', 'Defaults to /app/root/larynx.pid') {|pid| @options[:pid_file] = pid }
|
45
|
-
opts.on('-h', '--help', 'This is it') { $stderr.puts opts; exit 0 }
|
46
|
-
opts.on('-v', '--version') { $stderr.puts "Larynx version #{Larynx::VERSION}"; exit 0 }
|
47
|
-
opts.parse!(args)
|
48
|
-
end
|
49
|
-
|
50
|
-
def setup_logger
|
51
|
-
logger = Larynx::Logger.new(@options[:log_file])
|
52
|
-
logger.level = Logger::INFO
|
53
|
-
Object.const_set "LARYNX_LOGGER", logger
|
54
|
-
end
|
55
|
-
|
56
|
-
def graceful_exit
|
57
|
-
msg = "Shutting down Larynx"
|
58
|
-
$stderr.puts msg unless @options[:daemon]
|
59
|
-
LARYNX_LOGGER.info msg
|
60
|
-
|
61
|
-
EM.stop_server @em_signature
|
62
|
-
@em_signature = nil
|
63
|
-
remove_pid_file if @options[:daemonize]
|
64
|
-
exit 130
|
65
|
-
end
|
66
|
-
|
67
|
-
def daemonize
|
68
|
-
Daemonize.daemonize
|
69
|
-
Dir.chdir LARYNX_ROOT
|
70
|
-
File.open(@options[:pid_file], 'w+') {|f| f.write("#{Process.pid}\n") }
|
71
|
-
end
|
72
|
-
|
73
|
-
def remove_pid_file
|
74
|
-
File.delete @options[:pid_file]
|
75
|
-
end
|
76
|
-
|
77
|
-
def trap_signals
|
78
|
-
trap('TERM') { graceful_exit }
|
79
|
-
trap('INT') { graceful_exit }
|
80
|
-
end
|
81
|
-
|
82
|
-
def setup_app
|
83
|
-
if ARGV[0].nil?
|
84
|
-
$stderr.puts "You must specify an application file"
|
85
|
-
exit -1
|
86
|
-
end
|
87
|
-
Object.const_set "LARYNX_ROOT", File.expand_path(File.dirname(ARGV[0]))
|
88
|
-
require File.expand_path(ARGV[0])
|
89
|
-
end
|
90
|
-
|
91
|
-
def start_server
|
92
|
-
msg = "Larynx starting up on #{@options[:ip]}:#{@options[:port]}"
|
93
|
-
$stderr.puts msg unless @options[:daemon]
|
94
|
-
LARYNX_LOGGER.info msg
|
95
|
-
|
96
|
-
EM::run {
|
97
|
-
@em_signature = EM::start_server @options[:ip], @options[:port], Larynx::CallHandler
|
98
|
-
}
|
99
|
-
end
|
100
|
-
|
101
|
-
def run
|
102
|
-
parse_options(ARGV)
|
103
|
-
setup_app
|
104
|
-
daemonize if @options[:daemonize]
|
105
|
-
setup_logger
|
106
|
-
trap_signals
|
107
|
-
start_server
|
108
|
-
end
|
109
|
-
|
110
|
-
def running?
|
111
|
-
!@em_signature.nil?
|
112
|
-
end
|
113
26
|
end
|
114
27
|
|
115
28
|
# Default connect callback is to answer call
|
116
29
|
connect {|call| call.answer }
|
117
30
|
end
|
118
|
-
|
119
|
-
Larynx.run unless defined?(TEST)
|
data/lib/larynx/field.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
module Larynx
|
2
|
+
class NoPromptDefined < StandardError; end
|
3
|
+
|
4
|
+
class Field
|
5
|
+
include CallbacksWithAsync
|
6
|
+
|
7
|
+
VALID_PROMPT_OPTIONS = [:play, :speak, :phrase, :bargein, :repeats, :interdigit_timeout, :timeout]
|
8
|
+
|
9
|
+
attr_reader :name, :app, :attempt
|
10
|
+
define_callback :setup, :validate, :invalid, :success, :failure, :scope => :app
|
11
|
+
|
12
|
+
def initialize(name, options, &block)
|
13
|
+
@name = name
|
14
|
+
@options = options.reverse_merge(:attempts => 3)
|
15
|
+
@prompt_queue = []
|
16
|
+
|
17
|
+
instance_eval(&block)
|
18
|
+
raise(Larynx::NoPromptDefined, 'A field requires a prompt to be defined') if @prompt_queue.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def prompt(options)
|
22
|
+
add_prompt(options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def reprompt(options)
|
26
|
+
raise 'A reprompt can only be used after a prompt' if @prompt_queue.empty?
|
27
|
+
add_prompt(options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_prompt(options)
|
31
|
+
options.assert_valid_keys(*VALID_PROMPT_OPTIONS)
|
32
|
+
repeats = options.delete(:repeats) || 1
|
33
|
+
options.merge!(@options.slice(:length, :min_length, :max_length, :interdigit_timeout, :timeout))
|
34
|
+
@prompt_queue += ([options] * repeats)
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_prompt
|
38
|
+
options = (@prompt_queue[@attempt-1] || @prompt_queue.last).dup
|
39
|
+
method = command_from_options(options)
|
40
|
+
message = options[method].is_a?(Symbol) ? @app.send(options[method]) : options[method]
|
41
|
+
options[method] = message
|
42
|
+
|
43
|
+
Prompt.new(call, options) {|input, result|
|
44
|
+
set_instance_variables(input, result)
|
45
|
+
evaluate_input
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute_prompt
|
50
|
+
call.execute current_prompt.command
|
51
|
+
send_next_command
|
52
|
+
end
|
53
|
+
|
54
|
+
def increment_attempts
|
55
|
+
@attempt += 1
|
56
|
+
end
|
57
|
+
|
58
|
+
# hook called when callback is complete
|
59
|
+
def callback_complete(callback, result=true)
|
60
|
+
case callback
|
61
|
+
when :validate
|
62
|
+
evaluate_validity(result)
|
63
|
+
when :invalid
|
64
|
+
invalid_input
|
65
|
+
when :success, :failure
|
66
|
+
finalize
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def evaluate_input
|
71
|
+
@valid_length ? fire_callback(:validate) : fire_callback(:invalid)
|
72
|
+
end
|
73
|
+
|
74
|
+
def evaluate_validity(result)
|
75
|
+
result ? fire_callback(:success) : fire_callback(:invalid)
|
76
|
+
end
|
77
|
+
|
78
|
+
def invalid_input
|
79
|
+
if last_attempt?
|
80
|
+
fire_callback(:failure)
|
81
|
+
else
|
82
|
+
increment_attempts
|
83
|
+
execute_prompt
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def send_next_command
|
88
|
+
call.send_next_command if call.state == :ready
|
89
|
+
end
|
90
|
+
|
91
|
+
def set_instance_variables(input, result)
|
92
|
+
@value, @valid_length = input, result
|
93
|
+
@app.send("#{@name}=", input)
|
94
|
+
end
|
95
|
+
|
96
|
+
def command_from_options(options)
|
97
|
+
(Prompt::COMMAND_OPTIONS & options.keys).first
|
98
|
+
end
|
99
|
+
|
100
|
+
def run(app)
|
101
|
+
@app = app
|
102
|
+
@attempt = 1
|
103
|
+
call.add_observer self
|
104
|
+
fire_callback(:setup)
|
105
|
+
execute_prompt
|
106
|
+
end
|
107
|
+
|
108
|
+
def call
|
109
|
+
@app.call
|
110
|
+
end
|
111
|
+
|
112
|
+
def finalize
|
113
|
+
call.remove_observer self
|
114
|
+
send_next_command
|
115
|
+
end
|
116
|
+
|
117
|
+
def last_attempt?
|
118
|
+
@attempt == @options[:attempts]
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
data/lib/larynx/form.rb
CHANGED
@@ -1,10 +1,27 @@
|
|
1
1
|
module Larynx
|
2
2
|
class Form < Application
|
3
|
-
include Fields
|
4
3
|
class_inheritable_accessor :setup_block
|
5
4
|
|
6
|
-
|
7
|
-
|
5
|
+
class_inheritable_accessor :field_definitions
|
6
|
+
self.field_definitions = []
|
7
|
+
|
8
|
+
attr_accessor :fields
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def setup(&block)
|
12
|
+
self.setup_block = block
|
13
|
+
end
|
14
|
+
|
15
|
+
def field(name, options={}, &block)
|
16
|
+
self.field_definitions << {:name => name, :options => options, :block => block}
|
17
|
+
attr_accessor name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(*args, &block)
|
22
|
+
@fields = self.class.field_definitions.map {|field| Field.new(field[:name], field[:options], &field[:block]) }
|
23
|
+
@field_index = -1
|
24
|
+
super
|
8
25
|
end
|
9
26
|
|
10
27
|
def run
|
@@ -16,5 +33,34 @@ module Larynx
|
|
16
33
|
@field_index = -1
|
17
34
|
run
|
18
35
|
end
|
36
|
+
|
37
|
+
def next_field(field_name=nil)
|
38
|
+
if field_name
|
39
|
+
@field_index = field_index(field_name)
|
40
|
+
else
|
41
|
+
@field_index += 1
|
42
|
+
end
|
43
|
+
if field = current_field
|
44
|
+
field.run(self)
|
45
|
+
field
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def current_field
|
50
|
+
@fields[@field_index]
|
51
|
+
end
|
52
|
+
|
53
|
+
def field_index(name)
|
54
|
+
field = @fields.find {|f| f.name == name }
|
55
|
+
@fields.index(field)
|
56
|
+
end
|
57
|
+
|
58
|
+
def attempt
|
59
|
+
current_field.attempt
|
60
|
+
end
|
61
|
+
|
62
|
+
def last_attempt?
|
63
|
+
current_field.last_attempt?
|
64
|
+
end
|
19
65
|
end
|
20
66
|
end
|
data/lib/larynx/menu.rb
ADDED
data/lib/larynx/version.rb
CHANGED
@@ -0,0 +1,218 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
class TestForm < Larynx::Form; end
|
4
|
+
|
5
|
+
describe Larynx::Field do
|
6
|
+
attr_reader :call, :form
|
7
|
+
|
8
|
+
before do
|
9
|
+
@call = TestCallHandler.new(1)
|
10
|
+
@form = define_form.new(@call)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should raise exception if field has no prompt' do
|
14
|
+
lambda { field(:guess) {} }.should raise_exception(Larynx::NoPromptDefined)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should run setup callback once' do
|
18
|
+
call_me = should_be_called
|
19
|
+
fld = field(:guess) do
|
20
|
+
prompt :speak => 'first'
|
21
|
+
setup &call_me
|
22
|
+
end
|
23
|
+
fld.run form
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should pass timeout and length options to the prompt object' do
|
27
|
+
fld = field(:guess, :length => 1, :min_length => 1, :max_length => 2, :interdigit_timeout => 1, :timeout => 2) do
|
28
|
+
prompt :speak => 'first'
|
29
|
+
end
|
30
|
+
fld.run(form)
|
31
|
+
prompt = fld.current_prompt
|
32
|
+
prompt.interdigit_timeout.should == 1
|
33
|
+
prompt.timeout.should == 2
|
34
|
+
prompt.minimum_length.should == 1
|
35
|
+
prompt.maximum_length.should == 2
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should return same prompt all attempts if single prompt' do
|
39
|
+
fld = field(:guess) do
|
40
|
+
prompt :speak => 'first'
|
41
|
+
end
|
42
|
+
fld.run(form)
|
43
|
+
fld.current_prompt.message.should == 'first'
|
44
|
+
fld.increment_attempts
|
45
|
+
fld.current_prompt.message.should == 'first'
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should return reprompt for subsequent prompts' do
|
49
|
+
fld = field(:guess) do
|
50
|
+
prompt :speak => 'first'
|
51
|
+
reprompt :speak => 'second'
|
52
|
+
end
|
53
|
+
fld.run(form)
|
54
|
+
fld.current_prompt.message.should == 'first'
|
55
|
+
fld.increment_attempts
|
56
|
+
fld.current_prompt.message.should == 'second'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should return prompt for given number of repeats before subsequent prompts' do
|
60
|
+
fld = field(:guess) do
|
61
|
+
prompt :speak => 'first', :repeats => 2
|
62
|
+
reprompt :speak => 'second'
|
63
|
+
end
|
64
|
+
fld.run(form)
|
65
|
+
fld.current_prompt.message.should == 'first'
|
66
|
+
fld.increment_attempts
|
67
|
+
fld.current_prompt.message.should == 'first'
|
68
|
+
fld.increment_attempts
|
69
|
+
fld.current_prompt.message.should == 'second'
|
70
|
+
end
|
71
|
+
|
72
|
+
context "#last_attempt?" do
|
73
|
+
it 'should return false when current attempt not equal to max attempts' do
|
74
|
+
fld = field(:guess, :attempts => 2) do
|
75
|
+
prompt :speak => 'first'
|
76
|
+
end
|
77
|
+
fld.run(form)
|
78
|
+
fld.attempt.should == 1
|
79
|
+
fld.last_attempt?.should be_false
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should return true when current attempt equals max attempts' do
|
83
|
+
fld = field(:guess, :attempts => 2) do
|
84
|
+
prompt :speak => 'first'
|
85
|
+
end
|
86
|
+
fld.run(form)
|
87
|
+
fld.increment_attempts
|
88
|
+
fld.attempt.should == 2
|
89
|
+
fld.last_attempt?.should be_true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'input evaluation' do
|
94
|
+
it 'should run validate callback if input minimum length' do
|
95
|
+
call_me = should_be_called
|
96
|
+
fld = field(:guess, :min_length => 1) do
|
97
|
+
prompt :speak => 'first'
|
98
|
+
validate &call_me
|
99
|
+
end
|
100
|
+
fld.run form
|
101
|
+
call.input << '1'
|
102
|
+
fld.current_prompt.finalise
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should run invalid callback if length not valid' do
|
106
|
+
call_me = should_be_called
|
107
|
+
fld = field(:guess) do
|
108
|
+
prompt :speak => 'first'
|
109
|
+
invalid &call_me
|
110
|
+
end
|
111
|
+
fld.run form
|
112
|
+
fld.current_prompt.finalise
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should run invalid callback if validate callback returns false' do
|
116
|
+
call_me = should_be_called
|
117
|
+
fld = field(:guess, :min_length => 1) do
|
118
|
+
prompt :speak => 'first'
|
119
|
+
validate { false }
|
120
|
+
invalid &call_me
|
121
|
+
end
|
122
|
+
fld.run form
|
123
|
+
call.input << '1'
|
124
|
+
fld.current_prompt.finalise
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should run invalid callback if validate callback returns nil' do
|
128
|
+
call_me = should_be_called
|
129
|
+
fld = field(:guess, :min_length => 1) do
|
130
|
+
prompt :speak => 'first'
|
131
|
+
validate { nil }
|
132
|
+
invalid &call_me
|
133
|
+
end
|
134
|
+
fld.run form
|
135
|
+
call.input << '1'
|
136
|
+
fld.current_prompt.finalise
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should run success callback if length valid and no validate callback' do
|
140
|
+
call_me = should_be_called
|
141
|
+
fld = field(:guess, :min_length => 1) do
|
142
|
+
prompt :speak => 'first'
|
143
|
+
success &call_me
|
144
|
+
end
|
145
|
+
fld.run form
|
146
|
+
call.input << '1'
|
147
|
+
fld.current_prompt.finalise
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should run success callback if validate callback returns true' do
|
151
|
+
call_me = should_be_called
|
152
|
+
fld = field(:guess, :min_length => 1) do
|
153
|
+
prompt :speak => 'first'
|
154
|
+
validate { true }
|
155
|
+
success &call_me
|
156
|
+
end
|
157
|
+
fld.run form
|
158
|
+
call.input << '1'
|
159
|
+
fld.current_prompt.finalise
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'should run failure callback if not valid and last attempt' do
|
163
|
+
call_me = should_be_called
|
164
|
+
fld = field(:guess, :min_length => 1, :attempts => 1) do
|
165
|
+
prompt :speak => 'first'
|
166
|
+
failure &call_me
|
167
|
+
end
|
168
|
+
fld.run form
|
169
|
+
fld.current_prompt.finalise
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'should increment attempts if not valid' do
|
173
|
+
fld = field(:guess) do
|
174
|
+
prompt :speak => 'first'
|
175
|
+
reprompt :speak => 'second'
|
176
|
+
end
|
177
|
+
fld.run form
|
178
|
+
fld.current_prompt.finalise
|
179
|
+
fld.current_prompt.message.should == 'second'
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'should execute next prompt if not valid' do
|
183
|
+
fld = field(:guess) do
|
184
|
+
prompt :speak => 'first'
|
185
|
+
reprompt :speak => 'second'
|
186
|
+
end
|
187
|
+
fld.run form
|
188
|
+
fld.should_receive(:execute_prompt)
|
189
|
+
fld.current_prompt.finalise
|
190
|
+
end
|
191
|
+
|
192
|
+
context "async callbacks" do
|
193
|
+
# it "should be run in thread" do
|
194
|
+
# em do
|
195
|
+
# fld = field(:guess) do
|
196
|
+
# prompt :speak => 'first'
|
197
|
+
# validate(:async) { sleep(0.25) }
|
198
|
+
# success { done }
|
199
|
+
# end.run(form)
|
200
|
+
# call.input << '1'
|
201
|
+
# end
|
202
|
+
# @callback.should be_nil
|
203
|
+
# end
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
def field(name, options={}, &block)
|
209
|
+
@form.class.class_eval { attr_accessor name }
|
210
|
+
Larynx::Field.new(name, options, &block)
|
211
|
+
end
|
212
|
+
|
213
|
+
def define_form(&block)
|
214
|
+
reset_class(TestForm) do
|
215
|
+
instance_eval &block if block_given?
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
data/spec/larynx/form_spec.rb
CHANGED
@@ -9,6 +9,16 @@ describe Larynx::Form do
|
|
9
9
|
@call = TestCallHandler.new(1)
|
10
10
|
end
|
11
11
|
|
12
|
+
it 'should add field class method' do
|
13
|
+
Larynx::Form.should respond_to(:field)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should add instance accessor for field name' do
|
17
|
+
form_class = define_form
|
18
|
+
form_class.field(:guess) { prompt :speak => 'hello' }
|
19
|
+
form_class.methods.include?(:guess)
|
20
|
+
end
|
21
|
+
|
12
22
|
context "#run" do
|
13
23
|
it 'should call setup block' do
|
14
24
|
this_should_be_called = should_be_called
|
@@ -40,6 +50,40 @@ describe Larynx::Form do
|
|
40
50
|
end
|
41
51
|
end
|
42
52
|
|
53
|
+
context '#next_field' do
|
54
|
+
let(:form) {
|
55
|
+
define_form do
|
56
|
+
field(:field1) { prompt :speak => 'hello' }
|
57
|
+
field(:field2) { prompt :speak => 'hello' }
|
58
|
+
field(:field3) { prompt :speak => 'hello' }
|
59
|
+
end.new(call)
|
60
|
+
}
|
61
|
+
|
62
|
+
it 'should iterate over defined fields' do
|
63
|
+
form.next_field.name.should == :field1
|
64
|
+
form.next_field.name.should == :field2
|
65
|
+
form.next_field.name.should == :field3
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should jump to field name if supplied' do
|
69
|
+
form.next_field(:field2).name.should == :field2
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "#current_field" do
|
74
|
+
it 'should return field of current position' do
|
75
|
+
form = define_form do
|
76
|
+
field(:field1) { prompt :speak => 'hello' }
|
77
|
+
field(:field2) { prompt :speak => 'hello' }
|
78
|
+
end.new(call)
|
79
|
+
form.run
|
80
|
+
|
81
|
+
form.current_field.should == form.fields[0]
|
82
|
+
form.next_field
|
83
|
+
form.current_field.should == form.fields[1]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
43
87
|
def define_form(&block)
|
44
88
|
reset_class(TestForm) do
|
45
89
|
instance_eval &block if block_given?
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: larynx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 6
|
10
|
+
version: 0.1.6
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Adam Meehan
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-09-
|
18
|
+
date: 2010-09-04 00:00:00 +10:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -100,9 +100,10 @@ files:
|
|
100
100
|
- lib/larynx/callbacks_with_async.rb
|
101
101
|
- lib/larynx/command.rb
|
102
102
|
- lib/larynx/commands.rb
|
103
|
-
- lib/larynx/
|
103
|
+
- lib/larynx/field.rb
|
104
104
|
- lib/larynx/form.rb
|
105
105
|
- lib/larynx/logger.rb
|
106
|
+
- lib/larynx/menu.rb
|
106
107
|
- lib/larynx/observable.rb
|
107
108
|
- lib/larynx/prompt.rb
|
108
109
|
- lib/larynx/response.rb
|
@@ -122,8 +123,9 @@ files:
|
|
122
123
|
- spec/larynx/call_handler_spec.rb
|
123
124
|
- spec/larynx/command_spec.rb
|
124
125
|
- spec/larynx/eventmachince_spec.rb
|
125
|
-
- spec/larynx/
|
126
|
+
- spec/larynx/field_spec.rb
|
126
127
|
- spec/larynx/form_spec.rb
|
128
|
+
- spec/larynx/menu_spec.rb
|
127
129
|
- spec/larynx/prompt_spec.rb
|
128
130
|
- spec/larynx_spec.rb
|
129
131
|
- spec/spec_helper.rb
|
data/lib/larynx/fields.rb
DELETED
@@ -1,184 +0,0 @@
|
|
1
|
-
module Larynx
|
2
|
-
class NoPromptDefined < StandardError; end
|
3
|
-
|
4
|
-
module Fields
|
5
|
-
|
6
|
-
def self.included(base)
|
7
|
-
base.extend ClassMethods
|
8
|
-
base.class_eval do
|
9
|
-
include InstanceMethods
|
10
|
-
class_inheritable_accessor :field_definitions
|
11
|
-
self.field_definitions = []
|
12
|
-
attr_accessor :fields
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
module ClassMethods
|
17
|
-
|
18
|
-
def field(name, options={}, &block)
|
19
|
-
self.field_definitions << {:name => name, :options => options, :block => block}
|
20
|
-
attr_accessor name
|
21
|
-
end
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
module InstanceMethods
|
26
|
-
|
27
|
-
def initialize(*args, &block)
|
28
|
-
@fields = self.class.field_definitions.map {|field| Field.new(field[:name], field[:options], &field[:block]) }
|
29
|
-
@field_index = -1
|
30
|
-
super
|
31
|
-
end
|
32
|
-
|
33
|
-
def next_field(field_name=nil)
|
34
|
-
if field_name
|
35
|
-
@field_index = field_index(field_name)
|
36
|
-
else
|
37
|
-
@field_index += 1
|
38
|
-
end
|
39
|
-
if field = current_field
|
40
|
-
field.run(self)
|
41
|
-
field
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def current_field
|
46
|
-
@fields[@field_index]
|
47
|
-
end
|
48
|
-
|
49
|
-
def field_index(name)
|
50
|
-
field = @fields.find {|f| f.name == name }
|
51
|
-
@fields.index(field)
|
52
|
-
end
|
53
|
-
|
54
|
-
def attempt
|
55
|
-
current_field.attempt
|
56
|
-
end
|
57
|
-
|
58
|
-
def last_attempt?
|
59
|
-
current_field.last_attempt?
|
60
|
-
end
|
61
|
-
|
62
|
-
end
|
63
|
-
|
64
|
-
class Field
|
65
|
-
include CallbacksWithAsync
|
66
|
-
|
67
|
-
VALID_PROMPT_OPTIONS = [:play, :speak, :phrase, :bargein, :repeats, :interdigit_timeout, :timeout]
|
68
|
-
|
69
|
-
attr_reader :name, :app, :attempt
|
70
|
-
define_callback :setup, :validate, :invalid, :success, :failure, :scope => :app
|
71
|
-
|
72
|
-
def initialize(name, options, &block)
|
73
|
-
@name = name
|
74
|
-
@options = options.reverse_merge(:attempts => 3)
|
75
|
-
@prompt_queue = []
|
76
|
-
|
77
|
-
instance_eval(&block)
|
78
|
-
raise(Larynx::NoPromptDefined, 'A field requires a prompt to be defined') if @prompt_queue.empty?
|
79
|
-
end
|
80
|
-
|
81
|
-
def prompt(options)
|
82
|
-
add_prompt(options)
|
83
|
-
end
|
84
|
-
|
85
|
-
def reprompt(options)
|
86
|
-
raise 'A reprompt can only be used after a prompt' if @prompt_queue.empty?
|
87
|
-
add_prompt(options)
|
88
|
-
end
|
89
|
-
|
90
|
-
def add_prompt(options)
|
91
|
-
options.assert_valid_keys(*VALID_PROMPT_OPTIONS)
|
92
|
-
repeats = options.delete(:repeats) || 1
|
93
|
-
options.merge!(@options.slice(:length, :min_length, :max_length, :interdigit_timeout, :timeout))
|
94
|
-
@prompt_queue += ([options] * repeats)
|
95
|
-
end
|
96
|
-
|
97
|
-
def current_prompt
|
98
|
-
options = (@prompt_queue[@attempt-1] || @prompt_queue.last).dup
|
99
|
-
method = command_from_options(options)
|
100
|
-
message = options[method].is_a?(Symbol) ? @app.send(options[method]) : options[method]
|
101
|
-
options[method] = message
|
102
|
-
|
103
|
-
Prompt.new(call, options) {|input, result|
|
104
|
-
set_instance_variables(input, result)
|
105
|
-
evaluate_input
|
106
|
-
}
|
107
|
-
end
|
108
|
-
|
109
|
-
def execute_prompt
|
110
|
-
call.execute current_prompt.command
|
111
|
-
send_next_command
|
112
|
-
end
|
113
|
-
|
114
|
-
def increment_attempts
|
115
|
-
@attempt += 1
|
116
|
-
end
|
117
|
-
|
118
|
-
# hook called when callback is complete
|
119
|
-
def callback_complete(callback, result=true)
|
120
|
-
case callback
|
121
|
-
when :validate
|
122
|
-
evaluate_validity(result)
|
123
|
-
when :invalid
|
124
|
-
invalid_input
|
125
|
-
when :success, :failure
|
126
|
-
finalize
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def evaluate_input
|
131
|
-
@valid_length ? fire_callback(:validate) : fire_callback(:invalid)
|
132
|
-
end
|
133
|
-
|
134
|
-
def evaluate_validity(result)
|
135
|
-
result ? fire_callback(:success) : fire_callback(:invalid)
|
136
|
-
end
|
137
|
-
|
138
|
-
def invalid_input
|
139
|
-
if last_attempt?
|
140
|
-
fire_callback(:failure)
|
141
|
-
else
|
142
|
-
increment_attempts
|
143
|
-
execute_prompt
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
def send_next_command
|
148
|
-
call.send_next_command if call.state == :ready
|
149
|
-
end
|
150
|
-
|
151
|
-
def set_instance_variables(input, result)
|
152
|
-
@value, @valid_length = input, result
|
153
|
-
@app.send("#{@name}=", input)
|
154
|
-
end
|
155
|
-
|
156
|
-
def command_from_options(options)
|
157
|
-
(Prompt::COMMAND_OPTIONS & options.keys).first
|
158
|
-
end
|
159
|
-
|
160
|
-
def run(app)
|
161
|
-
@app = app
|
162
|
-
@attempt = 1
|
163
|
-
call.add_observer self
|
164
|
-
fire_callback(:setup)
|
165
|
-
execute_prompt
|
166
|
-
end
|
167
|
-
|
168
|
-
def call
|
169
|
-
@app.call
|
170
|
-
end
|
171
|
-
|
172
|
-
def finalize
|
173
|
-
call.remove_observer self
|
174
|
-
send_next_command
|
175
|
-
end
|
176
|
-
|
177
|
-
def last_attempt?
|
178
|
-
@attempt == @options[:attempts]
|
179
|
-
end
|
180
|
-
|
181
|
-
end
|
182
|
-
|
183
|
-
end
|
184
|
-
end
|
data/spec/larynx/fields_spec.rb
DELETED
@@ -1,271 +0,0 @@
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
-
|
3
|
-
class TestApp < Larynx::Application; end
|
4
|
-
|
5
|
-
describe Larynx::Fields do
|
6
|
-
attr_reader :call, :app
|
7
|
-
|
8
|
-
before do
|
9
|
-
@call = TestCallHandler.new(1)
|
10
|
-
@app = define_app.new(@call)
|
11
|
-
end
|
12
|
-
|
13
|
-
context 'module' do
|
14
|
-
before do
|
15
|
-
@app_class = define_app
|
16
|
-
end
|
17
|
-
|
18
|
-
it 'should add field class method' do
|
19
|
-
@app_class.should respond_to(:field)
|
20
|
-
end
|
21
|
-
|
22
|
-
it 'should add instance accessor for field name' do
|
23
|
-
@app_class.field(:guess) { prompt :speak => 'hello' }
|
24
|
-
@app_class.methods.include?(:guess)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
context '#next_field' do
|
29
|
-
before do
|
30
|
-
@app = define_app do
|
31
|
-
field(:field1) { prompt :speak => 'hello' }
|
32
|
-
field(:field2) { prompt :speak => 'hello' }
|
33
|
-
field(:field3) { prompt :speak => 'hello' }
|
34
|
-
end.new(call)
|
35
|
-
end
|
36
|
-
|
37
|
-
it 'should iterate over defined fields' do
|
38
|
-
app.next_field.name.should == :field1
|
39
|
-
app.next_field.name.should == :field2
|
40
|
-
app.next_field.name.should == :field3
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'should jump to field name if supplied' do
|
44
|
-
app.next_field(:field2).name.should == :field2
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
context "#current_field" do
|
49
|
-
it 'should return field of current position' do
|
50
|
-
@app = define_app do
|
51
|
-
field(:field1) { prompt :speak => 'hello' }
|
52
|
-
field(:field2) { prompt :speak => 'hello' }
|
53
|
-
end.new(call)
|
54
|
-
app.run
|
55
|
-
|
56
|
-
app.next_field
|
57
|
-
app.current_field.should == app.fields[0]
|
58
|
-
app.next_field
|
59
|
-
app.current_field.should == app.fields[1]
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
context 'field object' do
|
64
|
-
it 'should raise exception if field has no prompt' do
|
65
|
-
lambda { field(:guess) {} }.should raise_exception(Larynx::NoPromptDefined)
|
66
|
-
end
|
67
|
-
|
68
|
-
it 'should run setup callback once' do
|
69
|
-
call_me = should_be_called
|
70
|
-
fld = field(:guess) do
|
71
|
-
prompt :speak => 'first'
|
72
|
-
setup &call_me
|
73
|
-
end
|
74
|
-
fld.run app
|
75
|
-
end
|
76
|
-
|
77
|
-
it 'should pass timeout and length options to the prompt object' do
|
78
|
-
fld = field(:guess, :length => 1, :min_length => 1, :max_length => 2, :interdigit_timeout => 1, :timeout => 2) do
|
79
|
-
prompt :speak => 'first'
|
80
|
-
end
|
81
|
-
fld.run(app)
|
82
|
-
prompt = fld.current_prompt
|
83
|
-
prompt.interdigit_timeout.should == 1
|
84
|
-
prompt.timeout.should == 2
|
85
|
-
prompt.minimum_length.should == 1
|
86
|
-
prompt.maximum_length.should == 2
|
87
|
-
end
|
88
|
-
|
89
|
-
it 'should return same prompt all attempts if single prompt' do
|
90
|
-
fld = field(:guess) do
|
91
|
-
prompt :speak => 'first'
|
92
|
-
end
|
93
|
-
fld.run(app)
|
94
|
-
fld.current_prompt.message.should == 'first'
|
95
|
-
fld.increment_attempts
|
96
|
-
fld.current_prompt.message.should == 'first'
|
97
|
-
end
|
98
|
-
|
99
|
-
it 'should return reprompt for subsequent prompts' do
|
100
|
-
fld = field(:guess) do
|
101
|
-
prompt :speak => 'first'
|
102
|
-
reprompt :speak => 'second'
|
103
|
-
end
|
104
|
-
fld.run(app)
|
105
|
-
fld.current_prompt.message.should == 'first'
|
106
|
-
fld.increment_attempts
|
107
|
-
fld.current_prompt.message.should == 'second'
|
108
|
-
end
|
109
|
-
|
110
|
-
it 'should return prompt for given number of repeats before subsequent prompts' do
|
111
|
-
fld = field(:guess) do
|
112
|
-
prompt :speak => 'first', :repeats => 2
|
113
|
-
reprompt :speak => 'second'
|
114
|
-
end
|
115
|
-
fld.run(app)
|
116
|
-
fld.current_prompt.message.should == 'first'
|
117
|
-
fld.increment_attempts
|
118
|
-
fld.current_prompt.message.should == 'first'
|
119
|
-
fld.increment_attempts
|
120
|
-
fld.current_prompt.message.should == 'second'
|
121
|
-
end
|
122
|
-
|
123
|
-
context "#last_attempt?" do
|
124
|
-
it 'should return false when current attempt not equal to max attempts' do
|
125
|
-
fld = field(:guess, :attempts => 2) do
|
126
|
-
prompt :speak => 'first'
|
127
|
-
end
|
128
|
-
fld.run(app)
|
129
|
-
fld.attempt.should == 1
|
130
|
-
fld.last_attempt?.should be_false
|
131
|
-
end
|
132
|
-
|
133
|
-
it 'should return true when current attempt equals max attempts' do
|
134
|
-
fld = field(:guess, :attempts => 2) do
|
135
|
-
prompt :speak => 'first'
|
136
|
-
end
|
137
|
-
fld.run(app)
|
138
|
-
fld.increment_attempts
|
139
|
-
fld.attempt.should == 2
|
140
|
-
fld.last_attempt?.should be_true
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
context 'input evaluation' do
|
145
|
-
it 'should run validate callback if input minimum length' do
|
146
|
-
call_me = should_be_called
|
147
|
-
fld = field(:guess, :min_length => 1) do
|
148
|
-
prompt :speak => 'first'
|
149
|
-
validate &call_me
|
150
|
-
end
|
151
|
-
fld.run app
|
152
|
-
call.input << '1'
|
153
|
-
fld.current_prompt.finalise
|
154
|
-
end
|
155
|
-
|
156
|
-
it 'should run invalid callback if length not valid' do
|
157
|
-
call_me = should_be_called
|
158
|
-
fld = field(:guess) do
|
159
|
-
prompt :speak => 'first'
|
160
|
-
invalid &call_me
|
161
|
-
end
|
162
|
-
fld.run app
|
163
|
-
fld.current_prompt.finalise
|
164
|
-
end
|
165
|
-
|
166
|
-
it 'should run invalid callback if validate callback returns false' do
|
167
|
-
call_me = should_be_called
|
168
|
-
fld = field(:guess, :min_length => 1) do
|
169
|
-
prompt :speak => 'first'
|
170
|
-
validate { false }
|
171
|
-
invalid &call_me
|
172
|
-
end
|
173
|
-
fld.run app
|
174
|
-
call.input << '1'
|
175
|
-
fld.current_prompt.finalise
|
176
|
-
end
|
177
|
-
|
178
|
-
it 'should run invalid callback if validate callback returns nil' do
|
179
|
-
call_me = should_be_called
|
180
|
-
fld = field(:guess, :min_length => 1) do
|
181
|
-
prompt :speak => 'first'
|
182
|
-
validate { nil }
|
183
|
-
invalid &call_me
|
184
|
-
end
|
185
|
-
fld.run app
|
186
|
-
call.input << '1'
|
187
|
-
fld.current_prompt.finalise
|
188
|
-
end
|
189
|
-
|
190
|
-
it 'should run success callback if length valid and no validate callback' do
|
191
|
-
call_me = should_be_called
|
192
|
-
fld = field(:guess, :min_length => 1) do
|
193
|
-
prompt :speak => 'first'
|
194
|
-
success &call_me
|
195
|
-
end
|
196
|
-
fld.run app
|
197
|
-
call.input << '1'
|
198
|
-
fld.current_prompt.finalise
|
199
|
-
end
|
200
|
-
|
201
|
-
it 'should run success callback if validate callback returns true' do
|
202
|
-
call_me = should_be_called
|
203
|
-
fld = field(:guess, :min_length => 1) do
|
204
|
-
prompt :speak => 'first'
|
205
|
-
validate { true }
|
206
|
-
success &call_me
|
207
|
-
end
|
208
|
-
fld.run app
|
209
|
-
call.input << '1'
|
210
|
-
fld.current_prompt.finalise
|
211
|
-
end
|
212
|
-
|
213
|
-
it 'should run failure callback if not valid and last attempt' do
|
214
|
-
call_me = should_be_called
|
215
|
-
fld = field(:guess, :min_length => 1, :attempts => 1) do
|
216
|
-
prompt :speak => 'first'
|
217
|
-
failure &call_me
|
218
|
-
end
|
219
|
-
fld.run app
|
220
|
-
fld.current_prompt.finalise
|
221
|
-
end
|
222
|
-
|
223
|
-
it 'should increment attempts if not valid' do
|
224
|
-
fld = field(:guess) do
|
225
|
-
prompt :speak => 'first'
|
226
|
-
reprompt :speak => 'second'
|
227
|
-
end
|
228
|
-
fld.run app
|
229
|
-
fld.current_prompt.finalise
|
230
|
-
fld.current_prompt.message.should == 'second'
|
231
|
-
end
|
232
|
-
|
233
|
-
it 'should execute next prompt if not valid' do
|
234
|
-
fld = field(:guess) do
|
235
|
-
prompt :speak => 'first'
|
236
|
-
reprompt :speak => 'second'
|
237
|
-
end
|
238
|
-
fld.run app
|
239
|
-
fld.should_receive(:execute_prompt)
|
240
|
-
fld.current_prompt.finalise
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
context "async callbacks" do
|
245
|
-
# it "should be run in thread" do
|
246
|
-
# em do
|
247
|
-
# fld = field(:guess) do
|
248
|
-
# prompt :speak => 'first'
|
249
|
-
# validate(:async) { sleep(0.25) }
|
250
|
-
# success { done }
|
251
|
-
# end.run(app)
|
252
|
-
# call.input << '1'
|
253
|
-
# end
|
254
|
-
# @callback.should be_nil
|
255
|
-
# end
|
256
|
-
end
|
257
|
-
|
258
|
-
end
|
259
|
-
|
260
|
-
def field(name, options={}, &block)
|
261
|
-
@app.class.class_eval { attr_accessor name }
|
262
|
-
Larynx::Fields::Field.new(name, options, &block)
|
263
|
-
end
|
264
|
-
|
265
|
-
def define_app(&block)
|
266
|
-
reset_class(TestApp) do
|
267
|
-
include Larynx::Fields
|
268
|
-
instance_eval &block if block_given?
|
269
|
-
end
|
270
|
-
end
|
271
|
-
end
|