larynx 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -212,6 +212,10 @@ together and define another form for unrelated fields.
212
212
  Lets look at a simple Form class with empty callbacks in place.
213
213
 
214
214
  class MyApp < Larynx::Form
215
+ setup do
216
+ # Run when the form is first run or restarted.
217
+ end
218
+
215
219
  field(:my_field, :attempts => 3, :length => 1) do
216
220
  prompt :speak => 'Please enter a value.'
217
221
 
data/Rakefile CHANGED
@@ -23,7 +23,6 @@ spec = Gem::Specification.new do |s|
23
23
  s.homepage = "http://github.com/adzap/larynx"
24
24
 
25
25
  s.require_path = 'lib'
26
- s.autorequire = GEM_NAME
27
26
  s.files = %w(MIT-LICENSE README.rdoc Rakefile) + Dir.glob("{lib,spec,examples}/**/*")
28
27
  s.add_dependency "eventmachine", "~> 0.12.10"
29
28
  s.add_dependency "activesupport", "~> 2.3.5"
@@ -1,7 +1,9 @@
1
1
  module Larynx
2
2
  class Application
3
3
  attr_reader :call
4
+
4
5
  delegate *Commands.instance_methods << {:to => :call}
6
+ delegate :session, :to => :call
5
7
 
6
8
  def self.run(call)
7
9
  app = self.new(call)
@@ -107,7 +107,7 @@ module Larynx
107
107
  finalize_command
108
108
  @state = :ready
109
109
  Larynx.fire_callback(:answer, self)
110
- send_next_command
110
+ send_next_command if @state == :ready
111
111
  when @response.executing?
112
112
  log "Executing: #{current_command.name}"
113
113
  current_command.setup
@@ -168,6 +168,7 @@ module Larynx
168
168
  break! if @state == :executing
169
169
  cancel_all_timers
170
170
  clear_observers!
171
+ @session = nil
171
172
  end
172
173
 
173
174
  def log(msg)
@@ -41,7 +41,7 @@ module Larynx
41
41
  callback_complete(callback, scope_callback(block, scope).call)
42
42
  end
43
43
  else
44
- callback_complete(callback, nil)
44
+ callback_complete(callback)
45
45
  end
46
46
  end
47
47
 
data/lib/larynx/fields.rb CHANGED
@@ -26,30 +26,47 @@ module Larynx
26
26
 
27
27
  def initialize(*args, &block)
28
28
  @fields = self.class.field_definitions.map {|field| Field.new(field[:name], field[:options], &field[:block]) }
29
- @current_field = 0
29
+ @field_index = -1
30
30
  super
31
31
  end
32
32
 
33
33
  def next_field(field_name=nil)
34
- @current_field = field_index(field_name) if field_name
35
- if field = @fields[@current_field]
34
+ if field_name
35
+ @field_index = field_index(field_name)
36
+ else
37
+ @field_index += 1
38
+ end
39
+ if field = current_field
36
40
  field.run(self)
37
- @current_field += 1
38
41
  field
39
42
  end
40
43
  end
41
44
 
45
+ def current_field
46
+ @fields[@field_index]
47
+ end
48
+
42
49
  def field_index(name)
43
50
  field = @fields.find {|f| f.name == name }
44
51
  @fields.index(field)
45
52
  end
46
53
 
54
+ def attempt
55
+ current_field.attempt
56
+ end
57
+
58
+ def last_attempt?
59
+ current_field.last_attempt?
60
+ end
61
+
47
62
  end
48
63
 
49
64
  class Field
50
65
  include CallbacksWithAsync
51
66
 
52
- attr_reader :name, :app
67
+ VALID_PROMPT_OPTIONS = [:play, :speak, :phrase, :bargein, :repeats, :interdigit_timeout, :timeout]
68
+
69
+ attr_reader :name, :app, :attempt
53
70
  define_callback :setup, :validate, :invalid, :success, :failure, :scope => :app
54
71
 
55
72
  def initialize(name, options, &block)
@@ -71,7 +88,7 @@ module Larynx
71
88
  end
72
89
 
73
90
  def add_prompt(options)
74
- options.assert_valid_keys(:play, :speak, :phrase, :bargein, :repeats, :interdigit_timeout, :timeout)
91
+ options.assert_valid_keys(*VALID_PROMPT_OPTIONS)
75
92
  repeats = options.delete(:repeats) || 1
76
93
  options.merge!(@options.slice(:length, :min_length, :max_length, :interdigit_timeout, :timeout))
77
94
  @prompt_queue += ([options] * repeats)
@@ -99,10 +116,9 @@ module Larynx
99
116
  end
100
117
 
101
118
  # hook called when callback is complete
102
- def callback_complete(callback, result)
119
+ def callback_complete(callback, result=true)
103
120
  case callback
104
121
  when :validate
105
- result = result.nil? ? true : result
106
122
  evaluate_validity(result)
107
123
  when :invalid
108
124
  invalid_input
@@ -120,11 +136,11 @@ module Larynx
120
136
  end
121
137
 
122
138
  def invalid_input
123
- if @attempt < @options[:attempts]
139
+ if last_attempt?
140
+ fire_callback(:failure)
141
+ else
124
142
  increment_attempts
125
143
  execute_prompt
126
- else
127
- fire_callback(:failure)
128
144
  end
129
145
  end
130
146
 
@@ -138,7 +154,7 @@ module Larynx
138
154
  end
139
155
 
140
156
  def command_from_options(options)
141
- ([:play, :speak, :phrase] & options.keys).first
157
+ (Prompt::COMMAND_OPTIONS & options.keys).first
142
158
  end
143
159
 
144
160
  def run(app)
@@ -158,6 +174,10 @@ module Larynx
158
174
  send_next_command
159
175
  end
160
176
 
177
+ def last_attempt?
178
+ @attempt == @options[:attempts]
179
+ end
180
+
161
181
  end
162
182
 
163
183
  end
data/lib/larynx/form.rb CHANGED
@@ -1,19 +1,19 @@
1
1
  module Larynx
2
2
  class Form < Application
3
3
  include Fields
4
- @@setup = nil
4
+ class_inheritable_accessor :setup_block
5
5
 
6
6
  def self.setup(&block)
7
- @@setup = block
7
+ self.setup_block = block
8
8
  end
9
9
 
10
10
  def run
11
- instance_eval &@@setup if @@setup
11
+ instance_eval &self.class.setup_block if self.class.setup_block
12
12
  next_field
13
13
  end
14
14
 
15
15
  def restart_form
16
- @current_field = 0
16
+ @field_index = -1
17
17
  run
18
18
  end
19
19
  end
data/lib/larynx/prompt.rb CHANGED
@@ -13,6 +13,8 @@ module Larynx
13
13
  class Prompt
14
14
  attr_reader :call
15
15
 
16
+ COMMAND_OPTIONS = [:play, :speak, :phrase]
17
+
16
18
  def initialize(call, options, &block)
17
19
  @call, @options, @block = call, options, block
18
20
  @options.reverse_merge!(:bargein => true, :timeout => 10, :interdigit_timeout => 3, :termchar => '#')
@@ -21,8 +23,9 @@ module Larynx
21
23
 
22
24
  def command
23
25
  @command ||= AppCommand.new(command_name, message, :bargein => @options[:bargein]).
24
- before { call.clear_input }.
26
+ before { call.clear_input unless @options[:bargein] }.
25
27
  after {
28
+ call.clear_input unless @options[:bargein]
26
29
  if prompt_finished?
27
30
  finalise
28
31
  else
@@ -62,7 +65,7 @@ module Larynx
62
65
  end
63
66
 
64
67
  def command_name
65
- ([:play, :speak, :phrase] & @options.keys).first.to_s
68
+ (COMMAND_OPTIONS & @options.keys).first.to_s
66
69
  end
67
70
 
68
71
  def message
@@ -0,0 +1,95 @@
1
+ require 'logger'
2
+ require 'daemons/daemonize'
3
+
4
+ module Larynx
5
+ module Server
6
+ class << self
7
+
8
+ def boot
9
+ parse_options(ARGV)
10
+ setup_app
11
+ daemonize if @options[:daemonize]
12
+ setup_logger
13
+ trap_signals
14
+ start_server
15
+ end
16
+
17
+ def parse_options(args=ARGV)
18
+ @options = {
19
+ :ip => "0.0.0.0",
20
+ :port => 8084,
21
+ :pid_file => "./larynx.pid",
22
+ :log_file => "./larynx.log"
23
+ }
24
+ opts = OptionParser.new
25
+ opts.banner = "Usage: larynx [options] app_file"
26
+ opts.separator ''
27
+ opts.separator "Larynx is a framework to develop FreeSWITCH IVR applications in Ruby."
28
+ opts.on('-i', '--ip IP', 'Listen for connections on this IP') {|ip| @options[:ip] = ip }
29
+ opts.on('-p', '--port PORT', 'Listen on this port', Integer) {|port| @options[:port] = port }
30
+ opts.on('-d', '--daemonize', 'Run as daemon') { @options[:daemonize] = true }
31
+ opts.on('-l', '--log-file FILE', 'Defaults to /app/root/larynx.log') {|log| @options[:log_file] = log }
32
+ opts.on( '--pid-file FILE', 'Defaults to /app/root/larynx.pid') {|pid| @options[:pid_file] = pid }
33
+ opts.on('-h', '--help', 'This is it') { $stderr.puts opts; exit 0 }
34
+ opts.on('-v', '--version') { $stderr.puts "Larynx version #{Larynx::VERSION}"; exit 0 }
35
+ opts.parse!(args)
36
+ end
37
+
38
+ def setup_logger
39
+ logger = Larynx::Logger.new(@options[:log_file])
40
+ logger.level = Logger::INFO
41
+ Object.const_set "LARYNX_LOGGER", logger
42
+ end
43
+
44
+ def graceful_exit
45
+ msg = "Shutting down Larynx"
46
+ $stderr.puts msg unless @options[:daemon]
47
+ LARYNX_LOGGER.info msg
48
+
49
+ EM.stop_server @em_signature
50
+ @em_signature = nil
51
+ remove_pid_file if @options[:daemonize]
52
+ exit 130
53
+ end
54
+
55
+ def daemonize
56
+ Daemonize.daemonize
57
+ Dir.chdir LARYNX_ROOT
58
+ File.open(@options[:pid_file], 'w+') {|f| f.write("#{Process.pid}\n") }
59
+ end
60
+
61
+ def remove_pid_file
62
+ File.delete @options[:pid_file]
63
+ end
64
+
65
+ def trap_signals
66
+ trap('TERM') { graceful_exit }
67
+ trap('INT') { graceful_exit }
68
+ end
69
+
70
+ def setup_app
71
+ if ARGV[0].nil?
72
+ $stderr.puts "You must specify an application file"
73
+ exit -1
74
+ end
75
+ Object.const_set "LARYNX_ROOT", File.expand_path(File.dirname(ARGV[0]))
76
+ require File.expand_path(ARGV[0])
77
+ end
78
+
79
+ def start_server
80
+ msg = "Larynx starting up on #{@options[:ip]}:#{@options[:port]}"
81
+ $stderr.puts msg unless @options[:daemon]
82
+ LARYNX_LOGGER.info msg
83
+
84
+ EM::run {
85
+ @em_signature = EM::start_server @options[:ip], @options[:port], Larynx::CallHandler
86
+ }
87
+ end
88
+
89
+ def running?
90
+ !@em_signature.nil?
91
+ end
92
+
93
+ end
94
+ end
95
+ end
@@ -12,6 +12,10 @@ module Larynx
12
12
  end
13
13
  end
14
14
 
15
+ def []=(key, value)
16
+ @variables[key] = value
17
+ end
18
+
15
19
  def [](key)
16
20
  @variables[key]
17
21
  end
@@ -1,3 +1,3 @@
1
1
  module Larynx
2
- VERSION = '0.1.4'
2
+ VERSION = '0.1.5'
3
3
  end
@@ -198,6 +198,18 @@ describe Larynx::CallHandler do
198
198
  end
199
199
  end
200
200
 
201
+ it "should send next command if state is ready" do
202
+ call.should_receive(:send_next_command)
203
+ answer_call
204
+ end
205
+
206
+ it "should not send next command if answer callback changed state" do
207
+ call.should_not_receive(:send_next_command)
208
+ with_global_callback(:answer, lambda { call.state = :sending }) do
209
+ answer_call
210
+ end
211
+ end
212
+
201
213
  def answer_call
202
214
  call.send_response :answered
203
215
  end
@@ -25,7 +25,7 @@ describe Larynx::Fields do
25
25
  end
26
26
  end
27
27
 
28
- context 'next_field' do
28
+ context '#next_field' do
29
29
  before do
30
30
  @app = define_app do
31
31
  field(:field1) { prompt :speak => 'hello' }
@@ -45,6 +45,21 @@ describe Larynx::Fields do
45
45
  end
46
46
  end
47
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
+
48
63
  context 'field object' do
49
64
  it 'should raise exception if field has no prompt' do
50
65
  lambda { field(:guess) {} }.should raise_exception(Larynx::NoPromptDefined)
@@ -105,6 +120,27 @@ describe Larynx::Fields do
105
120
  fld.current_prompt.message.should == 'second'
106
121
  end
107
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
+
108
144
  context 'input evaluation' do
109
145
  it 'should run validate callback if input minimum length' do
110
146
  call_me = should_be_called
@@ -139,6 +175,18 @@ describe Larynx::Fields do
139
175
  fld.current_prompt.finalise
140
176
  end
141
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
+
142
190
  it 'should run success callback if length valid and no validate callback' do
143
191
  call_me = should_be_called
144
192
  fld = field(:guess, :min_length => 1) do
@@ -0,0 +1,49 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ class TestForm < Larynx::Form; end
4
+
5
+ describe Larynx::Form do
6
+ attr_reader :call
7
+
8
+ before do
9
+ @call = TestCallHandler.new(1)
10
+ end
11
+
12
+ context "#run" do
13
+ it 'should call setup block' do
14
+ this_should_be_called = should_be_called
15
+ define_form do
16
+ setup &this_should_be_called
17
+ end.run(call)
18
+ end
19
+ end
20
+
21
+ context "#restart_form" do
22
+ it 'should call form setup block' do
23
+ this_should_be_called = should_be_called
24
+ form = define_form do
25
+ setup &this_should_be_called
26
+ end.new(call)
27
+ form.restart_form
28
+ end
29
+
30
+ it 'should run the first field again' do
31
+ form = define_form do
32
+ field(:test1) { prompt :speak => '' }
33
+ field(:test2) { prompt :speak => '' }
34
+ end.new(call)
35
+
36
+ form.fields[0].should_receive(:run).twice
37
+ form.run
38
+ form.next_field
39
+ form.restart_form
40
+ end
41
+ end
42
+
43
+ def define_form(&block)
44
+ reset_class(TestForm) do
45
+ instance_eval &block if block_given?
46
+ end
47
+ end
48
+
49
+ end
@@ -39,6 +39,27 @@ describe Larynx::Prompt do
39
39
  @prompt.input.should == '1'
40
40
  end
41
41
  end
42
+ context "command" do
43
+ it "should return command object for command name" do
44
+ cmd = new_prompt.command
45
+ cmd.should be_kind_of(Larynx::AppCommand)
46
+ cmd.command.should == 'speak'
47
+ end
48
+ end
49
+
50
+ context "before callback" do
51
+ it "should clear input on execution if no bargein allowed" do
52
+ call.input << '1'
53
+ before_callback new_prompt(:speak => 'hello', :bargein => false)
54
+ call.input.should be_empty
55
+ end
56
+
57
+ it "should clear input on execution if bargein allowed" do
58
+ call.input << '1'
59
+ before_callback new_prompt(:speak => 'hello', :bargein => true)
60
+ call.input.should_not be_empty
61
+ end
62
+ end
42
63
 
43
64
  context "prompt_finished?" do
44
65
  it "should return true if input length reached" do
@@ -66,23 +87,28 @@ describe Larynx::Prompt do
66
87
  end
67
88
  end
68
89
 
69
- context "command" do
70
- it "should return command object for command name" do
71
- cmd = new_prompt.command
72
- cmd.should be_kind_of(Larynx::AppCommand)
73
- cmd.command.should == 'speak'
74
- end
75
- end
76
90
 
77
- context "before callback" do
78
- it "should clear input on execution" do
79
- call.input << '1'
80
- before_callback new_prompt
81
- call.input.should be_empty
91
+ context "after callback" do
92
+ context "bargein" do
93
+ it 'should clear input before prompt status evaluated if false' do
94
+ prompt = new_prompt(:speak => 'hello', :length => 1, :bargein => false)
95
+ call.input << '1'
96
+ em do
97
+ after_callback prompt
98
+ prompt.prompt_finished?.should be_false
99
+ done
100
+ end
101
+ end
102
+
103
+ it 'should not clear input before prompt status evaluated if true' do
104
+ prompt = new_prompt(:speak => 'hello', :length => 1, :bargein => true) { done }
105
+ call.input << '1'
106
+ em do
107
+ after_callback prompt
108
+ end
109
+ end
82
110
  end
83
- end
84
111
 
85
- context "after callback" do
86
112
  context "input completed" do
87
113
  it "should not add timers if reached length" do
88
114
  prompt = new_prompt
@@ -146,9 +172,10 @@ describe Larynx::Prompt do
146
172
  end
147
173
 
148
174
  it "should clear input" do
175
+ call.input << '1'
149
176
  prompt = new_prompt
150
- call.should_receive(:clear_input)
151
177
  prompt.finalise
178
+ call.input.should be_empty
152
179
  end
153
180
  end
154
181
 
metadata CHANGED
@@ -1,21 +1,21 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: larynx
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 17
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 4
10
- version: 0.1.4
9
+ - 5
10
+ version: 0.1.5
11
11
  platform: ruby
12
12
  authors:
13
13
  - Adam Meehan
14
- autorequire: larynx
14
+ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-08-26 00:00:00 +10:00
18
+ date: 2010-09-03 00:00:00 +10:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -107,6 +107,7 @@ files:
107
107
  - lib/larynx/prompt.rb
108
108
  - lib/larynx/response.rb
109
109
  - lib/larynx/restartable_timer.rb
110
+ - lib/larynx/server.rb
110
111
  - lib/larynx/session.rb
111
112
  - lib/larynx/version.rb
112
113
  - lib/larynx.rb
@@ -122,6 +123,7 @@ files:
122
123
  - spec/larynx/command_spec.rb
123
124
  - spec/larynx/eventmachince_spec.rb
124
125
  - spec/larynx/fields_spec.rb
126
+ - spec/larynx/form_spec.rb
125
127
  - spec/larynx/prompt_spec.rb
126
128
  - spec/larynx_spec.rb
127
129
  - spec/spec_helper.rb