ruby_ami 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cb7606f130bf3753537649990fbc188a62b31441
4
- data.tar.gz: eef9574ee77d8721fb9b480978595ec412a82857
3
+ metadata.gz: f5e6e035e1967a44a905b10ad704dbac564d4c90
4
+ data.tar.gz: 20f7fd7d4a8061c6e15dffa0b2f178645ef31b32
5
5
  SHA512:
6
- metadata.gz: 29b7868e1c9e4a99babf6a169b97741b53ea05dfa5b8a0d31de0caa6e919912b8aab09863ec3c874c6e2fabec9e674789e9524212117a64c198b4b9799eba34e
7
- data.tar.gz: 4213b880af9d91452b351b15ee5d55261e00954bacd79632f04467343e52371a1ee725d301216046d01ed83005b7f23e90eb8adce24ef63c94c9dce289be075e
6
+ metadata.gz: a5fafdd9ed56328ed906339a18d1ad8e3671985b6fd5a834ce9c6fe3d6e7ffccd7c44c2f0960d7f6f77358b3739ea6bb3e522f92191209e30b2ad12648cf0e7d
7
+ data.tar.gz: 0dc58ad3d2f2e3bf7e88fb66c43230d8c513bd61f7bdf7a196bd816e94fc78d7542b0eb55b26c154a7de6c26a56e4c881cd65ea52988192d5dd0d9e6d24be0f2
data/.gitignore CHANGED
@@ -3,7 +3,6 @@
3
3
  Gemfile.lock
4
4
  pkg/*
5
5
 
6
- lib/ruby_ami/lexer.rb
7
6
  .rvmrc
8
7
  .yardoc
9
8
  doc
data/.travis.yml CHANGED
@@ -5,6 +5,9 @@ rvm:
5
5
  - 2.0.0
6
6
  - jruby-19mode
7
7
  - rbx-19mode
8
- - ruby-head
8
+ - ruby-head
9
+ matrix:
10
+ allow_failures:
11
+ - rvm: jruby-19mode
9
12
  notifications:
10
13
  irc: "irc.freenode.org#adhearsion"
data/CHANGELOG.md CHANGED
@@ -1,11 +1,18 @@
1
1
  # [develop](https://github.com/adhearsion/ruby_ami)
2
2
 
3
+ # [2.1.0](https://github.com/adhearsion/ruby_ami/compare/v2.0.0...v2.1.0) - [2013-05-29](https://rubygems.org/gems/ruby_ami/versions/2.1.0)
4
+ * Enhancement: Replace Ragel parser with pure Ruby version, which is much more performant and simpler
5
+ * Bugfix: Handle AGI 5xx responses
6
+
3
7
  # [2.0.0](https://github.com/adhearsion/ruby_ami/compare/v1.3.3...v2.0.0) - [2013-04-15](https://rubygems.org/gems/ruby_ami/versions/2.0.0)
4
8
  * Major refactoring for simplification and performance
5
9
  * Actions are no longer synchronised on the wire since ActionID is now a reliable method of response/event association
6
10
  * Callbacks are no longer required. #send_action now simply blocks waiting for a response
7
11
  * Client still starts up two Streams, one for actions and one for events, but only for possible performance gains. It is possible to use Stream directly since it now does its own login and response association. Client is a very thin routing layer. It's encouraged that if you expect low traffic, you should use Stream directly. Client may be removed in v3.0.
8
12
 
13
+ # [1.3.4](https://github.com/adhearsion/ruby_ami/compare/v1.3.3...v1.3.4) - [2013-04-25](https://rubygems.org/gems/ruby_ami/versions/1.3.4)
14
+ * Bugfix: Handle AGI 5xx responses
15
+
9
16
  # [1.3.3](https://github.com/adhearsion/ruby_ami/compare/v1.3.2...v1.3.3) - [2013-04-09](https://rubygems.org/gems/ruby_ami/versions/1.3.3)
10
17
  * Bugfix: DBGet actions are now not terminated specially
11
18
 
data/Guardfile CHANGED
@@ -1,9 +1,17 @@
1
- guard 'shell', :all_on_start => true do
2
- watch("lib/ruby_ami/lexer_machine.rl") { `rake ragel` }
3
- end
4
-
5
- guard 'rspec', :version => 2, :cli => '--format documentation' do
1
+ guard 'rspec', :cli => '--format documentation' do
6
2
  watch(%r{^spec/.+_spec\.rb$})
7
3
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
8
4
  watch('spec/spec_helper.rb') { "spec/" }
9
5
  end
6
+
7
+ guard 'cucumber', cli: '--profile default --color --format progress' do
8
+ watch("lib/ruby_ami/lexer.rb") { 'features' }
9
+ watch(%r{^features/.+\.feature$})
10
+ watch(%r{^features/support/.+$}) { 'features' }
11
+ watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' }
12
+ end
13
+
14
+ guard 'rake', task: 'benchmark' do
15
+ watch("lib/ruby_ami/lexer.rb")
16
+ watch(/benchmarks\/*/)
17
+ end
data/README.md CHANGED
@@ -31,22 +31,6 @@ client.start
31
31
  Celluloid::Actor.join client
32
32
  ```
33
33
 
34
- ## Development Requirements
35
-
36
- ruby_ami uses [ragel](http://www.complang.org/ragel/) to generate some of it's files.
37
-
38
- On OS X (if you use homebrew):
39
-
40
- brew install ragel
41
-
42
- On Linux:
43
-
44
- apt-get install ragel OR yum install ragel
45
-
46
- Once you are inside the repository, before anything else, you will want to run:
47
-
48
- rake ragel
49
-
50
34
  ## Links:
51
35
  * [Source](https://github.com/adhearsion/ruby_ami)
52
36
  * [Documentation](http://rdoc.info/github/adhearsion/ruby_ami/master/frames)
data/Rakefile CHANGED
@@ -17,37 +17,22 @@ Cucumber::Rake::Task.new(:wip) do |t|
17
17
  t.cucumber_opts = %w{-p wip}
18
18
  end
19
19
 
20
- task :default => [:ragel, :spec, :features]
21
-
22
- require 'yard'
23
- YARD::Rake::YardocTask.new
24
-
25
- desc "Check Ragel version"
26
- task :check_ragel_version do
27
- ragel_version_match = `ragel --version`.match /(\d)\.(\d)+/
28
- abort "Could not get Ragel version! Is it installed? You must have at least version 6.7" unless ragel_version_match
29
- big, small = ragel_version_match.captures.map &:to_i
30
- puts "You're using Ragel v#{ragel_version_match[0]}"
31
- if big < 6 || big == 6 && small < 7
32
- abort "Please upgrade Ragel! v6.7 or later is required"
20
+ task :default => [:spec, :features]
21
+ require 'timeout'
22
+ desc "Run benchmarks"
23
+ task :benchmark do
24
+ begin
25
+ Timeout.timeout(120) do
26
+ glob = File.expand_path("../benchmarks/*.rb", __FILE__)
27
+ Dir[glob].each { |benchmark| load benchmark }
28
+ end
29
+ rescue Exception, Timeout::Error => ex
30
+ puts "ERROR: Couldn't complete benchmark: #{ex.class}: #{ex}"
31
+ puts " #{ex.backtrace.join("\n ")}"
32
+
33
+ exit 1 unless ENV['CI'] # Hax for running benchmarks on Travis
33
34
  end
34
35
  end
35
36
 
36
- desc "Used to regenerate the AMI source code files. Note: requires Ragel 6.3 or later be installed on your system"
37
- task :ragel => :check_ragel_version do
38
- run_ragel '-n -R'
39
- end
40
-
41
- desc "Generates a GraphVis document showing the Ragel state machine"
42
- task :visualize_ragel => :check_ragel_version do
43
- run_ragel '-V', 'dot'
44
- end
45
-
46
- def run_ragel(options = nil, extension = 'rb')
47
- ragel_file = 'lib/ruby_ami/lexer.rl.rb'
48
- base_file = ragel_file.sub ".rl.rb", ""
49
- command = ["ragel", options, "#{ragel_file} -o #{base_file}.#{extension} 2>&1"].compact.join ' '
50
- puts "Running command '#{command}'"
51
- puts `#{command}`
52
- raise "Failed generating code from Ragel file #{ragel_file}" if $?.to_i.nonzero?
53
- end
37
+ require 'yard'
38
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+ require 'ruby_ami'
6
+ require 'benchmark/ips'
7
+
8
+ class LexerHost
9
+ def initialize
10
+ @lexer = RubyAMI::Lexer.new self
11
+ end
12
+
13
+ def receive_data(data)
14
+ @lexer << data
15
+ end
16
+
17
+ def message_received(message)
18
+ end
19
+
20
+ def error_received(error)
21
+ end
22
+
23
+ def syntax_error_encountered(ignored_chunk)
24
+ end
25
+ end
26
+
27
+ lexer_host = LexerHost.new
28
+
29
+ event = <<-EVENT
30
+ Event: Dial
31
+ SubEvent: <value>
32
+ Channel: <value>
33
+ Destination: <value>
34
+ CallerIDNum: <value>
35
+ CallerIDName: <value>
36
+ ConnectedLineNum: <value>
37
+ ConnectedLineName: <value>
38
+ UniqueID: <value>
39
+ DestUniqueID: <value>
40
+ Dialstring: <value>
41
+
42
+ EVENT
43
+ event.gsub!("\n", "\r\n")
44
+
45
+ Benchmark.ips do |ips|
46
+ ips.report("event lexing") { lexer_host.receive_data event }
47
+ end
@@ -7,8 +7,6 @@ Feature: Lexing AMI
7
7
  Given a new lexer
8
8
  And a version header for AMI 1.0
9
9
 
10
- When the buffer is lexed
11
-
12
10
  Then the protocol should have lexed without syntax errors
13
11
  And the version should be set to 1.0
14
12
 
@@ -17,28 +15,22 @@ Feature: Lexing AMI
17
15
  And a version header for AMI 1.0
18
16
  And a normal login success with events
19
17
 
20
- When the buffer is lexed
21
-
22
18
  Then the protocol should have lexed without syntax errors
23
19
  And 1 message should have been received
24
20
 
25
21
  Scenario: Lexing the initial AMI header and then a Response:Follows section
26
22
  Given a new lexer
27
23
  And a version header for AMI 1.0
28
- And a multi-line Response:Follows body of ragel_description
29
-
30
- When the buffer is lexed
24
+ And a multi-line Response:Follows body of show_channels_from_wayne
31
25
 
32
26
  Then the protocol should have lexed without syntax errors
33
- And the 'follows' body of 1 message received should equal ragel_description
27
+ And the 'follows' body of 1 message received should equal show_channels_from_wayne
34
28
 
35
29
  Scenario: Lexing a Response:Follows section with no body
36
30
  Given a new lexer
37
31
  And a version header for AMI 1.0
38
32
  And a multi-line Response:Follows body of empty_String
39
33
 
40
- When the buffer is lexed
41
-
42
34
  Then the protocol should have lexed without syntax errors
43
35
  And the 'follows' body of 1 message received should equal empty_string
44
36
 
@@ -47,8 +39,6 @@ Feature: Lexing AMI
47
39
  And a version header for AMI 1.0
48
40
  Given a multi-line Response:Follows body of show_channels_from_wayne
49
41
 
50
- When the buffer is lexed
51
-
52
42
  Then the protocol should have lexed without syntax errors
53
43
  And the 'follows' body of 1 message received should equal show_channels_from_wayne
54
44
 
@@ -57,8 +47,6 @@ Feature: Lexing AMI
57
47
  And a version header for AMI 1.0
58
48
  Given a multi-line Response:Follows response simulating uptime
59
49
 
60
- When the buffer is lexed
61
-
62
50
  Then the protocol should have lexed without syntax errors
63
51
  And the first message received should have a key "System uptime" with value "46 minutes, 30 seconds"
64
52
 
@@ -66,8 +54,6 @@ Feature: Lexing AMI
66
54
  Given a new lexer
67
55
  And a multi-line Response:Follows body of with_colon_after_first_line
68
56
 
69
- When the buffer is lexed
70
-
71
57
  Then the protocol should have lexed without syntax errors
72
58
  And 1 message should have been received
73
59
  And the 'follows' body of 1 message received should equal with_colon_after_first_line
@@ -77,8 +63,6 @@ Feature: Lexing AMI
77
63
  Given a new lexer
78
64
  And an immediate response with text "markq has 0 calls (max unlimited) in 'ringall' strategy (0s holdtime), W:0, C:0, A:0, SL:0.0% within 0s\r\n No Members\r\n No Callers\r\n\r\n\r\n\r\n"
79
65
 
80
- When the buffer is lexed
81
-
82
66
  Then the protocol should have lexed without syntax errors
83
67
  And 1 message should have been received
84
68
  And 1 message should be an immediate response with text "markq has 0 calls (max unlimited) in 'ringall' strategy (0s holdtime), W:0, C:0, A:0, SL:0.0% within 0s\r\n No Members\r\n No Callers"
@@ -88,27 +72,21 @@ Feature: Lexing AMI
88
72
  And a version header for AMI 1.0
89
73
  And an Authentication Required error
90
74
 
91
- When the buffer is lexed
92
-
93
75
  Then the protocol should have lexed without syntax errors
94
76
 
95
77
  Scenario: Lexing the initial AMI header and then a Response:Follows section
96
78
  Given a new lexer
97
79
  And a version header for AMI 1.0
98
- And a multi-line Response:Follows body of ragel_description
99
- And a multi-line Response:Follows body of ragel_description
100
-
101
- When the buffer is lexed
80
+ And a multi-line Response:Follows body of show_channels_from_wayne
81
+ And a multi-line Response:Follows body of show_channels_from_wayne
102
82
 
103
83
  Then the protocol should have lexed without syntax errors
104
- And the 'follows' body of 2 messages received should equal ragel_description
84
+ And the 'follows' body of 2 messages received should equal show_channels_from_wayne
105
85
 
106
86
  Scenario: Lexing a stanza without receiving an AMI header
107
87
  Given a new lexer
108
88
  And a normal login success with events
109
89
 
110
- When the buffer is lexed
111
-
112
90
  Then the protocol should have lexed without syntax errors
113
91
  And 1 message should have been received
114
92
 
@@ -116,8 +94,6 @@ Feature: Lexing AMI
116
94
  Given a new lexer
117
95
  And an immediate response with text "Immediate responses are so ridiculous"
118
96
 
119
- When the buffer is lexed
120
-
121
97
  Then the protocol should have lexed without syntax errors
122
98
  And 1 message should have been received
123
99
  And 1 message should be an immediate response with text "Immediate responses are so ridiculous"
@@ -128,8 +104,6 @@ Feature: Lexing AMI
128
104
  And an immediate response with text "No queues have been created."
129
105
  And a normal login success with events
130
106
 
131
- When the buffer is lexed
132
-
133
107
  Then the protocol should have lexed without syntax errors
134
108
  And 3 messages should have been received
135
109
  And 1 message should be an immediate response with text "No queues have been created."
@@ -140,8 +114,6 @@ Feature: Lexing AMI
140
114
  And a normal login success with events
141
115
  And a Pong response with an ActionID of randomness
142
116
 
143
- When the buffer is lexed
144
-
145
117
  Then the protocol should have lexed without syntax errors
146
118
  And 2 messages should have been received
147
119
 
@@ -150,8 +122,6 @@ Feature: Lexing AMI
150
122
  And 5 Pong responses without an ActionID
151
123
  And 5 Pong responses with an ActionID of randomness
152
124
 
153
- When the buffer is lexed
154
-
155
125
  Then the protocol should have lexed without syntax errors
156
126
  And 10 messages should have been received
157
127
 
@@ -159,8 +129,6 @@ Feature: Lexing AMI
159
129
  Given a new lexer
160
130
  And a Pong response with an ActionID of 1224469850.61673
161
131
 
162
- When the buffer is lexed
163
-
164
132
  Then the first message received should have a key "ActionID" with value "1224469850.61673"
165
133
 
166
134
  Scenario: A response containing a floating point value
@@ -170,7 +138,6 @@ Feature: Lexing AMI
170
138
  And the custom stanza named "call" has key "Uniqueid" with value "1173223225.10309"
171
139
 
172
140
  When the custom stanza named "call" is added to the buffer
173
- And the buffer is lexed
174
141
 
175
142
  Then the 1st message received should have a key "Uniqueid" with value "1173223225.10309"
176
143
 
@@ -186,7 +153,6 @@ Feature: Lexing AMI
186
153
  And the custom stanza named "person" has key "I have spaces" with value "i have trailing padding "
187
154
 
188
155
  When the custom stanza named "person" is added to the buffer
189
- And the buffer is lexed
190
156
 
191
157
  Then the protocol should have lexed without syntax errors
192
158
  And the first message received should have a key "Name" with value "Jay Phillips"
@@ -202,8 +168,6 @@ Feature: Lexing AMI
202
168
  Given a new lexer
203
169
  And a normal login success with events split into two pieces
204
170
 
205
- When the buffer is lexed
206
-
207
171
  Then the protocol should have lexed without syntax errors
208
172
  And 1 message should have been received
209
173
 
@@ -212,8 +176,6 @@ Feature: Lexing AMI
212
176
  And an AMI error whose message is "Missing action in request"
213
177
  And a normal login success with events
214
178
 
215
- When the buffer is lexed
216
-
217
179
  Then the protocol should have lexed without syntax errors
218
180
  And 1 AMI error should have been received
219
181
  And the 1st AMI error should have the message "Missing action in request"
@@ -225,8 +187,6 @@ Feature: Lexing AMI
225
187
  And an immediate response with text "Yes, plain English is sent sometimes over AMI."
226
188
  And a normal login success with events
227
189
 
228
- When the buffer is lexed
229
-
230
190
  Then the protocol should have lexed without syntax errors
231
191
  And 3 messages should have been received
232
192
  And 1 message should be an immediate response with text "Yes, plain English is sent sometimes over AMI."
@@ -239,7 +199,6 @@ Feature: Lexing AMI
239
199
  And a custom header for event identified by "this_event" whose key is "AppData" and value is "agi://localhost"
240
200
 
241
201
  When the custom event identified by "this_event" is added to the buffer
242
- And the buffer is lexed
243
202
 
244
203
  Then the protocol should have lexed without syntax errors
245
204
  And 1 event should have been received
@@ -253,8 +212,6 @@ Feature: Lexing AMI
253
212
  And syntactically invalid immediate_packet_with_colon
254
213
  And a stanza break
255
214
 
256
- When the buffer is lexed
257
-
258
215
  Then 0 messages should have been received
259
216
  And the protocol should have lexed with 1 syntax error
260
217
  And the syntax error fixture named immediate_packet_with_colon should have been encountered
@@ -123,10 +123,6 @@ When 'the custom event identified by "$identifier" is added to the buffer' do |i
123
123
  @lexer << stringified_event
124
124
  end
125
125
 
126
- When "the buffer is lexed" do
127
- @lexer.resume!
128
- end
129
-
130
126
  ########################################
131
127
  #### THEN
132
128
  ########################################
@@ -160,7 +156,7 @@ Then /^the 'follows' body of (\d+) messages? received should equal (\w+)$/ do |n
160
156
  end
161
157
 
162
158
  Then "the version should be set to $version" do |version|
163
- @lexer.ami_version.should eql(version.to_f)
159
+ @lexer.ami_version.should eql(version)
164
160
  end
165
161
 
166
162
  Then /^the ([\w\d]*) message received should have a key "([^\"]*)" with value "([^\"]*)"$/ do |ordered,key,value|
@@ -93,11 +93,6 @@ end
93
93
 
94
94
  def follows_body_text(name)
95
95
  case name
96
- when "ragel_description"
97
- "Ragel is a software development tool that allows user actions to
98
- be embedded into the transitions of a regular expression's corresponding state machine,
99
- eliminating the need to switch from the regular expression engine and user code execution
100
- environment and back again."
101
96
  when "with_colon_after_first_line"
102
97
  "Host Username Refresh State Reg.Time \r\nlax.teliax.net:5060 jicksta 105 Registered Tue, 11 Nov 2008 02:29:55"
103
98
  when "show_channels_from_wayne"
@@ -4,7 +4,7 @@ module RubyAMI
4
4
  class AGIResultParser
5
5
  attr_reader :code, :result, :data
6
6
 
7
- FORMAT = /^(?<code>\d{3}) result=(?<result>-?\d*) ?(?<data>\(?.*\)?)?$/.freeze
7
+ FORMAT = /^(?<code>\d{3})( result=(?<result>-?\d*))? ?(?<data>\(?.*\)?)?$/.freeze
8
8
  DATA_KV_FORMAT = /(?<key>[\w\d]+)=(?<value>[\w\d]*)/.freeze
9
9
  DATA_CLEANER = /(^\()|(\)$)/.freeze
10
10
 
@@ -31,7 +31,7 @@ module RubyAMI
31
31
 
32
32
  def parse
33
33
  @code = match[:code].to_i
34
- @result = match[:result].to_i
34
+ @result = match[:result] ? match[:result].to_i : nil
35
35
  @data = match[:data] ? match[:data].gsub(DATA_CLEANER, '').freeze : nil
36
36
  end
37
37
 
@@ -12,6 +12,7 @@ module RubyAMI
12
12
  end
13
13
 
14
14
  def []=(key,value)
15
+ self.message = value if key == 'Message'
15
16
  @headers[key] = value
16
17
  end
17
18
 
@@ -0,0 +1,148 @@
1
+ # encoding: utf-8
2
+
3
+ module RubyAMI
4
+ class Lexer
5
+ STANZA_BREAK = "\r\n\r\n"
6
+ PROMPT = /Asterisk Call Manager\/(\d+\.\d+)\r\n/
7
+ KEYVALUEPAIR = /^([[[:alnum:]]-_ ]+): *(.*)\r\n/
8
+ FOLLOWSDELIMITER = /\r?\n?--END COMMAND--\r\n\r\n/
9
+ SUCCESS = /response: *success/i
10
+ PONG = /response: *pong/i
11
+ EVENT = /event: *(?<event_name>.*)?/i
12
+ ERROR = /response: *error/i
13
+ FOLLOWS = /response: *follows/i
14
+ SCANNER = /.*?#{STANZA_BREAK}/m
15
+ HEADER_SLICE = /.*\r\n/
16
+ IMMEDIATE_RESP = /.*/
17
+ CLASSIFIER = /((?<event>#{EVENT})|(?<success>#{SUCCESS})|(?<pong>#{PONG})|(?<follows>#{FOLLOWS})|(?<error>#{ERROR})|(?<immediate>#{IMMEDIATE_RESP})\r\n)\r\n/i
18
+
19
+ attr_accessor :ami_version
20
+
21
+ def initialize(delegate = nil)
22
+ @delegate = delegate
23
+ @buffer = ""
24
+ @ami_version = nil
25
+ end
26
+
27
+ def <<(new_data)
28
+ @buffer << new_data
29
+ parse_buffer
30
+ end
31
+
32
+ private
33
+
34
+ def parse_buffer
35
+ # Special case for the protocol header
36
+ if @buffer =~ PROMPT
37
+ @ami_version = $1
38
+ @buffer.slice! HEADER_SLICE
39
+ end
40
+
41
+ # We need at least one complete message before parsing
42
+ return unless @buffer.include?(STANZA_BREAK)
43
+
44
+ @processed = 0
45
+
46
+ response_follows_message = false
47
+ current_message = nil
48
+ @buffer.scan(SCANNER).each do |raw|
49
+ if response_follows_message
50
+ if handle_response_follows(response_follows_message, raw)
51
+ @processed += raw.length
52
+ message_received response_follows_message
53
+ response_follows_message = nil
54
+ end
55
+ else
56
+ response_follows_message = parse_message raw
57
+ end
58
+ end
59
+ @buffer.slice! 0, @processed
60
+ end
61
+
62
+ def parse_message(raw)
63
+ return if raw.length == 0
64
+
65
+ # Mark this message as processed, including the 4 stripped cr/lf bytes
66
+ @processed += raw.length
67
+
68
+ match = raw.match CLASSIFIER
69
+
70
+ msg = if match[:event]
71
+ Event.new match[:event_name]
72
+ elsif match[:success] || match[:pong]
73
+ Response.new
74
+ elsif match[:follows]
75
+ response_follows = true
76
+ Response.new
77
+ elsif match[:error]
78
+ Error.new
79
+ elsif match[:immediate]
80
+ if raw.include?(':')
81
+ syntax_error_encountered raw.chomp(STANZA_BREAK)
82
+ return
83
+ end
84
+ immediate_response = true
85
+ Response.from_immediate_response match[:immediate]
86
+ end
87
+
88
+ # Strip off the header line
89
+ raw.slice! HEADER_SLICE
90
+ populate_message_body msg, raw
91
+
92
+ return msg if response_follows && !handle_response_follows(msg, raw)
93
+
94
+ case msg
95
+ when Error
96
+ error_received msg
97
+ else
98
+ message_received msg
99
+ end
100
+
101
+ nil
102
+ end
103
+
104
+ ##
105
+ # Called after a response or event has been successfully parsed.
106
+ #
107
+ # @param [Response, Event] message The message just received
108
+ #
109
+ def message_received(message)
110
+ @delegate.message_received message
111
+ end
112
+
113
+ ##
114
+ # Called after an AMI error has been successfully parsed.
115
+ #
116
+ # @param [Response, Event] message The message just received
117
+ #
118
+ def error_received(message)
119
+ @delegate.error_received message
120
+ end
121
+
122
+ ##
123
+ # Called when there's a syntax error on the socket. This doesn't happen as often as it should because, in many cases,
124
+ # it's impossible to distinguish between a syntax error and an immediate packet.
125
+ #
126
+ # @param [String] ignored_chunk The offending text which caused the syntax error.
127
+ def syntax_error_encountered(ignored_chunk)
128
+ @delegate.syntax_error_encountered ignored_chunk
129
+ end
130
+
131
+ def populate_message_body(obj, raw)
132
+ while raw.slice! KEYVALUEPAIR
133
+ obj[$1] = $2
134
+ end
135
+ obj
136
+ end
137
+
138
+ def handle_response_follows(obj, raw)
139
+ obj.text_body ||= ''
140
+ obj.text_body << raw
141
+ return false unless raw =~ FOLLOWSDELIMITER
142
+ obj.text_body.sub! FOLLOWSDELIMITER, ''
143
+ obj.text_body.chomp!
144
+ true
145
+ end
146
+ end
147
+ end
148
+
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module RubyAMI
3
- VERSION = "2.0.0"
3
+ VERSION = "2.1.0"
4
4
  end
data/ruby_ami.gemspec CHANGED
@@ -5,11 +5,11 @@ require "ruby_ami/version"
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "ruby_ami"
7
7
  s.version = RubyAMI::VERSION
8
- s.authors = ["Ben Langfeld"]
9
- s.email = ["ben@langfeld.me"]
8
+ s.authors = ["Ben Langfeld", "Ben Klang"]
9
+ s.email = ["ben@langfeld.me", "bklang@mojolingo.com"]
10
10
  s.homepage = ""
11
11
  s.summary = %q{Futzing with AMI so you don't have to}
12
- s.description = %q{A Ruby client library for the Asterisk Management Interface build on eventmachine.}
12
+ s.description = %q{A Ruby client library for the Asterisk Management Interface built on Celluloid IO.}
13
13
 
14
14
  s.rubyforge_project = "ruby_ami"
15
15
 
@@ -27,5 +27,7 @@ Gem::Specification.new do |s|
27
27
  s.add_development_dependency %q<rake>, [">= 0"]
28
28
  s.add_development_dependency %q<guard-rspec>
29
29
  s.add_development_dependency %q<guard-shell>
30
- s.add_development_dependency %q<ruby_gntp>
30
+ s.add_development_dependency %q<guard-cucumber>
31
+ s.add_development_dependency %q<guard-rake>
32
+ s.add_development_dependency %q<benchmark_suite>
31
33
  end
@@ -48,5 +48,14 @@ module RubyAMI
48
48
  its(:data) { should == 'foo=bar' }
49
49
  its(:data_hash) { should == {'foo' => 'bar'} }
50
50
  end
51
+
52
+ context 'with a 5xx error' do
53
+ let(:result_string) { "510%20Invalid%20or%20unknown%20command%0A" }
54
+
55
+ its(:code) { should == 510 }
56
+ its(:result) { should be_nil }
57
+ its(:data) { should == 'Invalid or unknown command' }
58
+ its(:data_hash) { should be_nil }
59
+ end
51
60
  end
52
61
  end
@@ -98,6 +98,32 @@ Message: Recording started
98
98
  end
99
99
  end
100
100
 
101
+ it "can process an action with a Response: Follows result" do
102
+ action_id = RubyAMI.new_uuid
103
+ response = nil
104
+ mocked_server(1, lambda { response = @stream.send_action('Command', 'Command' => 'dialplan add extension 1,1,AGI,agi:async into adhearsion-redirect') }) do |val, server|
105
+ val.should == <<-ACTION
106
+ Action: command\r
107
+ ActionID: #{action_id}\r
108
+ Command: dialplan add extension 1,1,AGI,agi:async into adhearsion-redirect\r
109
+ \r
110
+ ACTION
111
+
112
+ server.send_data <<-EVENT
113
+ Response: Follows
114
+ Privilege: Command
115
+ ActionID: #{action_id}
116
+ Extension '1,1,AGI(agi:async)' added into 'adhearsion-redirect' context
117
+ --END COMMAND--
118
+
119
+ EVENT
120
+ end
121
+
122
+ expected_response = Response.new 'Privilege' => 'Command', 'ActionID' => action_id
123
+ expected_response.text_body = %q{Extension '1,1,AGI(agi:async)' added into 'adhearsion-redirect' context}
124
+ response.should == expected_response
125
+ end
126
+
101
127
  context "with a username and password set" do
102
128
  let(:username) { 'fred' }
103
129
  let(:password) { 'jones' }
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_ami
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Langfeld
8
+ - Ben Klang
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2013-04-15 00:00:00.000000000 Z
12
+ date: 2013-05-29 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: celluloid-io
@@ -123,7 +124,7 @@ dependencies:
123
124
  - !ruby/object:Gem::Version
124
125
  version: '0'
125
126
  - !ruby/object:Gem::Dependency
126
- name: ruby_gntp
127
+ name: guard-cucumber
127
128
  requirement: !ruby/object:Gem::Requirement
128
129
  requirements:
129
130
  - - '>='
@@ -136,10 +137,39 @@ dependencies:
136
137
  - - '>='
137
138
  - !ruby/object:Gem::Version
138
139
  version: '0'
139
- description: A Ruby client library for the Asterisk Management Interface build on
140
- eventmachine.
140
+ - !ruby/object:Gem::Dependency
141
+ name: guard-rake
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ - !ruby/object:Gem::Dependency
155
+ name: benchmark_suite
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - '>='
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - '>='
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ description: A Ruby client library for the Asterisk Management Interface built on
169
+ Celluloid IO.
141
170
  email:
142
171
  - ben@langfeld.me
172
+ - bklang@mojolingo.com
143
173
  executables: []
144
174
  extensions: []
145
175
  extra_rdoc_files: []
@@ -153,6 +183,7 @@ files:
153
183
  - LICENSE.txt
154
184
  - README.md
155
185
  - Rakefile
186
+ - benchmarks/lexer.rb
156
187
  - cucumber.yml
157
188
  - features/lexer.feature
158
189
  - features/step_definitions/lexer_steps.rb
@@ -168,8 +199,7 @@ files:
168
199
  - lib/ruby_ami/core_ext/celluloid.rb
169
200
  - lib/ruby_ami/error.rb
170
201
  - lib/ruby_ami/event.rb
171
- - lib/ruby_ami/lexer.rl.rb
172
- - lib/ruby_ami/lexer_machine.rl
202
+ - lib/ruby_ami/lexer.rb
173
203
  - lib/ruby_ami/response.rb
174
204
  - lib/ruby_ami/stream.rb
175
205
  - lib/ruby_ami/version.rb
@@ -184,7 +214,6 @@ files:
184
214
  - spec/ruby_ami/stream_spec.rb
185
215
  - spec/spec_helper.rb
186
216
  - spec/support/mock_server.rb
187
- - lib/ruby_ami/lexer.rb
188
217
  homepage: ''
189
218
  licenses: []
190
219
  metadata: {}
@@ -204,7 +233,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
204
233
  version: '0'
205
234
  requirements: []
206
235
  rubyforge_project: ruby_ami
207
- rubygems_version: 2.0.0
236
+ rubygems_version: 2.0.3
208
237
  signing_key:
209
238
  specification_version: 4
210
239
  summary: Futzing with AMI so you don't have to
@@ -1,304 +0,0 @@
1
- # encoding: utf-8
2
- module RubyAMI
3
- class Lexer
4
-
5
- KILOBYTE = 1024
6
- BUFFER_SIZE = 128 * KILOBYTE unless defined? BUFFER_SIZE
7
-
8
- ##
9
- # IMPORTANT! See method documentation for adjust_pointers!
10
- #
11
- # @see adjust_pointers
12
- #
13
- POINTERS = [
14
- :@current_pointer,
15
- :@token_start,
16
- :@token_end,
17
- :@version_start,
18
- :@event_name_start,
19
- :@current_key_position,
20
- :@current_value_position,
21
- :@last_seen_value_end,
22
- :@error_reason_start,
23
- :@follows_text_start,
24
- :@current_syntax_error_start,
25
- :@immediate_response_start
26
- ]
27
-
28
- %%{
29
- machine ami_protocol_parser;
30
-
31
- # All required Ragel actions are implemented as Ruby methods.
32
-
33
- # Executed after a "Response: Success" or "Response: Pong"
34
- action init_success { init_success }
35
-
36
- action init_response_follows { init_response_follows }
37
-
38
- action init_error { init_error }
39
-
40
- action message_received { message_received @current_message }
41
- action error_received { error_received @current_message }
42
-
43
- action version_starts { version_starts }
44
- action version_stops { version_stops }
45
-
46
- action key_starts { key_starts }
47
- action key_stops { key_stops }
48
-
49
- action value_starts { value_starts }
50
- action value_stops { value_stops }
51
-
52
- action error_reason_starts { error_reason_starts }
53
- action error_reason_stops { error_reason_stops }
54
-
55
- action syntax_error_starts { syntax_error_starts }
56
- action syntax_error_stops { syntax_error_stops }
57
-
58
- action immediate_response_starts { immediate_response_starts }
59
- action immediate_response_stops { immediate_response_stops }
60
-
61
- action follows_text_starts { follows_text_starts }
62
- action follows_text_stops { follows_text_stops }
63
-
64
- action event_name_starts { event_name_starts }
65
- action event_name_stops { event_name_stops }
66
-
67
- include ami_protocol_parser_machine "lexer_machine.rl";
68
-
69
- }%%##
70
-
71
- attr_accessor :ami_version
72
-
73
- def initialize(delegate = nil)
74
- @delegate = delegate
75
- @data = ''.force_encoding('ISO-8859-1')
76
- @current_pointer = 0
77
- @ragel_stack = []
78
- @ami_version = 0.0
79
-
80
- %%{
81
- # All other variables become local, letting Ruby garbage collect them. This
82
- # prevents us from having to manually reset them.
83
-
84
- variable data @data;
85
- variable p @current_pointer;
86
- variable pe @data_ending_pointer;
87
- variable cs @current_state;
88
- variable ts @token_start;
89
- variable te @token_end;
90
- variable act @ragel_act;
91
- variable eof @eof;
92
- variable stack @ragel_stack;
93
- variable top @ragel_stack_top;
94
-
95
- write data;
96
- write init;
97
- }%%##
98
- end
99
-
100
- def <<(new_data)
101
- extend_buffer_with new_data
102
- resume!
103
- end
104
-
105
- def resume!
106
- %%{ write exec; }%%##
107
- end
108
-
109
- def extend_buffer_with(new_data)
110
- length = new_data.size
111
-
112
- if length > BUFFER_SIZE
113
- raise Exception, "ERROR: Buffer overrun! Input size (#{new_data.size}) larger than buffer (#{BUFFER_SIZE})"
114
- end
115
-
116
- if length + @data.size > BUFFER_SIZE
117
- if @data.size != @current_pointer
118
- if @current_pointer < length
119
- # We are about to shift more bytes off the array than we have
120
- # parsed. This will cause the parser to lose state so
121
- # integrity cannot be guaranteed.
122
- raise Exception, "ERROR: Buffer overrun! AMI parser cannot guarantee sanity. New data size: #{new_data.size}; Current pointer at #{@current_pointer}; Data size: #{@data.size}"
123
- end
124
- end
125
- @data.slice! 0...length
126
- adjust_pointers -length
127
- end
128
- @data << new_data.force_encoding('ISO-8859-1')
129
- @data_ending_pointer = @data.size
130
- end
131
-
132
- protected
133
-
134
- ##
135
- # This method will adjust all pointers into the buffer according
136
- # to the supplied offset. This is necessary any time the buffer
137
- # changes, for example when the sliding window is incremented forward
138
- # after new data is received.
139
- #
140
- # It is VERY IMPORTANT that when any additional pointers are defined
141
- # that they are added to this method. Unpredictable results may
142
- # otherwise occur!
143
- #
144
- # @see https://adhearsion.lighthouseapp.com/projects/5871-adhearsion/tickets/72-ami-lexer-buffer-offset#ticket-72-26
145
- #
146
- # @param offset Adjust pointers by offset. May be negative.
147
- #
148
- def adjust_pointers(offset)
149
- POINTERS.each do |ptr|
150
- value = instance_variable_get(ptr)
151
- instance_variable_set(ptr, value + offset) if !value.nil?
152
- end
153
- end
154
-
155
- ##
156
- # Called after a response or event has been successfully parsed.
157
- #
158
- # @param [Response, Event] message The message just received
159
- #
160
- def message_received(message)
161
- @delegate.message_received message
162
- end
163
-
164
- ##
165
- # Called when there is an Error: stanza on the socket. Could be caused by executing an unrecognized command, trying
166
- # to originate into an invalid priority, etc. Note: many errors' responses are actually tightly coupled to a
167
- # Event which comes directly after it. Often the message will say something like "Channel status
168
- # will follow".
169
- #
170
- # @param [String] reason The reason given in the Message: header for the error stanza.
171
- #
172
- def error_received(message)
173
- @delegate.error_received message
174
- end
175
-
176
- ##
177
- # Called when there's a syntax error on the socket. This doesn't happen as often as it should because, in many cases,
178
- # it's impossible to distinguish between a syntax error and an immediate packet.
179
- #
180
- # @param [String] ignored_chunk The offending text which caused the syntax error.
181
- def syntax_error_encountered(ignored_chunk)
182
- @delegate.syntax_error_encountered ignored_chunk
183
- end
184
-
185
- def init_success
186
- @current_message = Response.new
187
- end
188
-
189
- def init_response_follows
190
- @current_message = Response.new
191
- end
192
-
193
- def init_error
194
- @current_message = Error.new
195
- end
196
-
197
- def version_starts
198
- @version_start = @current_pointer
199
- end
200
-
201
- def version_stops
202
- self.ami_version = @data[@version_start...@current_pointer].to_f
203
- @version_start = nil
204
- end
205
-
206
- def event_name_starts
207
- @event_name_start = @current_pointer
208
- end
209
-
210
- def event_name_stops
211
- event_name = @data[@event_name_start...@current_pointer]
212
- @event_name_start = nil
213
- @current_message = Event.new(event_name)
214
- end
215
-
216
- def key_starts
217
- @current_key_position = @current_pointer
218
- end
219
-
220
- def key_stops
221
- @current_key = @data[@current_key_position...@current_pointer]
222
- end
223
-
224
- def value_starts
225
- @current_value_position = @current_pointer
226
- end
227
-
228
- def value_stops
229
- @current_value = @data[@current_value_position...@current_pointer]
230
- @last_seen_value_end = @current_pointer + 2 # 2 for \r\n
231
- add_pair_to_current_message
232
- end
233
-
234
- def error_reason_starts
235
- @error_reason_start = @current_pointer
236
- end
237
-
238
- def error_reason_stops
239
- @current_message.message = @data[@error_reason_start...@current_pointer]
240
- end
241
-
242
- def follows_text_starts
243
- @follows_text_start = @current_pointer
244
- end
245
-
246
- def follows_text_stops
247
- text = @data[@last_seen_value_end..@current_pointer]
248
- text.sub! /\r?\n--END COMMAND--/, ""
249
- @current_message.text_body = text
250
- @follows_text_start = nil
251
- end
252
-
253
- def add_pair_to_current_message
254
- @current_message[@current_key] = @current_value
255
- reset_key_and_value_positions
256
- end
257
-
258
- def reset_key_and_value_positions
259
- @current_key, @current_value, @current_key_position, @current_value_position = nil
260
- end
261
-
262
- def syntax_error_starts
263
- @current_syntax_error_start = @current_pointer # Adding 1 since the pointer is still set to the last successful match
264
- end
265
-
266
- def syntax_error_stops
267
- # Subtracting 3 from @current_pointer below for "\r\n" which separates a stanza
268
- offending_data = @data[@current_syntax_error_start...@current_pointer - 1]
269
- syntax_error_encountered offending_data
270
- @current_syntax_error_start = nil
271
- end
272
-
273
- def immediate_response_starts
274
- @immediate_response_start = @current_pointer
275
- end
276
-
277
- def immediate_response_stops
278
- message = @data[@immediate_response_start...(@current_pointer -1)]
279
- message_received Response.from_immediate_response(message)
280
- end
281
-
282
- ##
283
- # This method is used primarily in debugging.
284
- #
285
- def view_buffer(message = nil)
286
- message ||= "Viewing the buffer"
287
-
288
- buffer = @data.clone
289
- buffer.insert(@current_pointer, "\033[0;31m\033[1;31m^\033[0m")
290
-
291
- buffer.gsub!("\r", "\\\\r")
292
- buffer.gsub!("\n", "\\n\n")
293
-
294
- puts <<-INSPECTION
295
- VVVVVVVVVVVVVVVVVVVVVVVVVVVVV
296
- #### #{message}
297
- #############################
298
- #{buffer}
299
- #############################
300
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
301
- INSPECTION
302
- end
303
- end
304
- end
@@ -1,87 +0,0 @@
1
- %%{ #%
2
-
3
- #########
4
- ## This file is written with the Ragel programming language and parses the Asterisk Manager Interface protocol. It depends
5
- ## upon Ragel actions which should be implemented in another Ragel-parsed file which includes this file.
6
- ##
7
- ## Ragel was used because the AMI protocol is extremely non-deterministic and, in the edge cases, requires something both
8
- ## very robust and something which can recover from syntax errors.
9
- ##
10
- ## Note: This file is language agnostic. From this AMI parsers in many other languages can be generated.
11
- #########
12
-
13
- machine ami_protocol_parser_machine;
14
-
15
- cr = "\r"; # A carriage return. Used before (almost) every newline character.
16
- lf = "\n"; # Newline. Used (with cr) to separate key/value pairs and stanzas.
17
- crlf = cr lf; # Means "carriage return and line feed". Used to separate key/value pairs and stanzas
18
- loose_newline = cr? lf; # Used sometimes when the AMI protocol is nondeterministic about the delimiter
19
-
20
- white = [\t ]; # Single whitespace character, either a tab or a space
21
- colon = ":" [ ]**; # Separates keys from values. "A colon followed by any number of spaces"
22
- stanza_break = crlf crlf; # The seperator between two stanzas.
23
- rest_of_line = (any* -- crlf); # Match all characters until the next line seperator.
24
-
25
- Prompt = "Asterisk Call Manager/" digit+ >version_starts "." digit+ %version_stops crlf;
26
-
27
- Key = ((alnum | print) -- (cr | lf | ":"))+;
28
- KeyValuePair = Key >key_starts %key_stops colon rest_of_line >value_starts %value_stops crlf;
29
-
30
- FollowsDelimiter = loose_newline "--END COMMAND--";
31
-
32
- Response = "Response"i colon;
33
-
34
- Success = Response "Success"i %init_success crlf @{ fgoto success; };
35
- Pong = Response "Pong"i %init_success crlf @{ fgoto success; };
36
- Event = "Event"i colon %event_name_starts rest_of_line %event_name_stops crlf @{ fgoto success; };
37
- Error = Response "Error"i %init_error crlf (("Message"i colon rest_of_line >error_reason_starts crlf >error_reason_stops) | KeyValuePair)+ crlf @error_received;
38
- Follows = Response "Follows"i crlf @init_response_follows @{ fgoto response_follows; };
39
-
40
- # For "Response: Follows"
41
- FollowsBody = (any* -- FollowsDelimiter) >follows_text_starts FollowsDelimiter @follows_text_stops crlf;
42
-
43
- ImmediateResponse = (any+ -- (loose_newline | ":")) >immediate_response_starts loose_newline @immediate_response_stops @{fret;};
44
- SyntaxError = (any+ -- crlf) >syntax_error_starts crlf @syntax_error_stops;
45
-
46
- irregularity := |*
47
- ImmediateResponse; # Performs the fret in the ImmediateResponse FSM
48
- SyntaxError => { fret; };
49
- *|;
50
-
51
- # When a new socket is established, Asterisk will send the version of the protocol per the Prompt machine. Because it's
52
- # tedious for unit tests to always send this, we'll put some intelligence into this parser to support going straight into
53
- # the protocol-parsing machine. It's also conceivable that a variant of AMI would not send this initial information.
54
- main := |*
55
- Prompt => { fgoto protocol; };
56
- any => {
57
- # If this scanner's look-ahead capability didn't match the prompt, let's ignore the need for a prompt
58
- fhold;
59
- fgoto protocol;
60
- };
61
- *|;
62
-
63
- protocol := |*
64
- Prompt;
65
- Success;
66
- Pong;
67
- Event;
68
- Error;
69
- Follows crlf;
70
- crlf => { fgoto protocol; }; # If we get a crlf out of place, let's just ignore it.
71
- any => {
72
- # If NONE of the above patterns match, we consider this a syntax error. The irregularity machine can recover gracefully.
73
- fhold;
74
- fcall irregularity;
75
- };
76
- *|;
77
-
78
- success := KeyValuePair* crlf @message_received @{fgoto protocol;};
79
-
80
- # For the "Response: Follows" protocol abnormality. What happens if there's a protocol irregularity in this state???
81
- response_follows := |*
82
- KeyValuePair+;
83
- FollowsBody;
84
- crlf @{ message_received @current_message; fgoto protocol; };
85
- *|;
86
-
87
- }%%