streamly_ffi 0.1.5

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.
@@ -0,0 +1,122 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ # encoding: UTF-8
5
+ require 'streamly_ffi/version'
6
+
7
+ module StreamlyFFI
8
+ autoload :Request, "streamly_ffi/request"
9
+
10
+ class Error < StandardError; end
11
+ class UnsupportedProtocol < StandardError; end
12
+ class URLFormatError < StandardError; end
13
+ class HostResolutionError < StandardError; end
14
+ class ConnectionFailed < StandardError; end
15
+ class PartialFileError < StandardError; end
16
+ class TimeoutError < StandardError; end
17
+ class TooManyRedirects < StandardError; end
18
+
19
+ # A helper method to make HEAD requests a dead-simple one-liner
20
+ #
21
+ # Example:
22
+ # Streamly.head("www.somehost.com/some_resource/1")
23
+ #
24
+ # Streamly.head("www.somehost.com/some_resource/1") do |header_chunk|
25
+ # # do something with _header_chunk_
26
+ # end
27
+ #
28
+ # Parameters:
29
+ # +url+ should be a String, the url to request
30
+ # +headers+ should be a Hash and is optional
31
+ #
32
+ # This method also accepts a block, which will stream the response headers in chunks to the caller
33
+ def self.head(url, headers=nil, &block)
34
+ opts = {:method => :head, :url => url, :headers => headers}
35
+ opts.merge!({:response_header_handler => block}) if block_given?
36
+ Request.execute(opts)
37
+ end
38
+
39
+ # A helper method to make HEAD requests a dead-simple one-liner
40
+ #
41
+ # Example:
42
+ # Streamly.get("www.somehost.com/some_resource/1")
43
+ #
44
+ # Streamly.get("www.somehost.com/some_resource/1") do |chunk|
45
+ # # do something with _chunk_
46
+ # end
47
+ #
48
+ # Parameters:
49
+ # +url+ should be a String, the url to request
50
+ # +headers+ should be a Hash and is optional
51
+ #
52
+ # This method also accepts a block, which will stream the response body in chunks to the caller
53
+ def self.get(url, headers=nil, &block)
54
+ opts = {:method => :get, :url => url, :headers => headers}
55
+ opts.merge!({:response_body_handler => block}) if block_given?
56
+ Request.execute(opts)
57
+ end
58
+
59
+ # A helper method to make HEAD requests a dead-simple one-liner
60
+ #
61
+ # Example:
62
+ # Streamly.post("www.somehost.com/some_resource", "asset[id]=2&asset[name]=bar")
63
+ #
64
+ # Streamly.post("www.somehost.com/some_resource", "asset[id]=2&asset[name]=bar") do |chunk|
65
+ # # do something with _chunk_
66
+ # end
67
+ #
68
+ # Parameters:
69
+ # +url+ should be a String (the url to request) and is required
70
+ # +payload+ should be a String and is required
71
+ # +headers+ should be a Hash and is optional
72
+ #
73
+ # This method also accepts a block, which will stream the response body in chunks to the caller
74
+ def self.post(url, payload, headers=nil, &block)
75
+ opts = {:method => :post, :url => url, :payload => payload, :headers => headers}
76
+ opts.merge!({:response_body_handler => block}) if block_given?
77
+ Request.execute(opts)
78
+ end
79
+
80
+ # A helper method to make HEAD requests a dead-simple one-liner
81
+ #
82
+ # Example:
83
+ # Streamly.put("www.somehost.com/some_resource/1", "asset[name]=foo")
84
+ #
85
+ # Streamly.put("www.somehost.com/some_resource/1", "asset[name]=foo") do |chunk|
86
+ # # do something with _chunk_
87
+ # end
88
+ #
89
+ # Parameters:
90
+ # +url+ should be a String (the url to request) and is required
91
+ # +payload+ should be a String and is required
92
+ # +headers+ should be a Hash and is optional
93
+ #
94
+ # This method also accepts a block, which will stream the response body in chunks to the caller
95
+ def self.put(url, payload, headers=nil, &block)
96
+ opts = {:method => :put, :url => url, :payload => payload, :headers => headers}
97
+ opts.merge!({:response_body_handler => block}) if block_given?
98
+ Request.execute(opts)
99
+ end
100
+
101
+ # A helper method to make HEAD requests a dead-simple one-liner
102
+ #
103
+ # Example:
104
+ # Streamly.delete("www.somehost.com/some_resource/1")
105
+ #
106
+ # Streamly.delete("www.somehost.com/some_resource/1") do |chunk|
107
+ # # do something with _chunk_
108
+ # end
109
+ #
110
+ # Parameters:
111
+ # +url+ should be a String, the url to request
112
+ # +headers+ should be a Hash and is optional
113
+ #
114
+ # This method also accepts a block, which will stream the response body in chunks to the caller
115
+ def self.delete(url, headers={}, &block)
116
+ opts = {:method => :delete, :url => url, :headers => headers}
117
+ opts.merge!({:response_body_handler => block}) if block_given?
118
+ Request.execute(opts)
119
+ end
120
+ end
121
+
122
+ # require "streamly/request" # May need to do this? Not sure how autoload works with FFI yet
@@ -0,0 +1,163 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require "curl_ffi"
5
+ require "singleton"
6
+
7
+ module StreamlyFFI
8
+ class Request
9
+ include Singleton
10
+
11
+ alias __method__ method
12
+
13
+ attr_reader :url, :method
14
+
15
+ # @TODO: Argumenting Checking + Error Handling
16
+ def initialize(options={})
17
+ url = options[:url]
18
+ method = options[:method]
19
+
20
+ @response_body = nil
21
+ @response_header = nil
22
+ @custom_header_handler = nil
23
+ @custom_write_handler = nil
24
+
25
+ # url should be a string that doesn't suck
26
+ # method should be :post, :get, :put, :delete, :head
27
+ # options should contain any of the following keys:
28
+ # :headers, :response_header_handler, :response_body_handler, :payload (required if method = :post / :put)
29
+
30
+ case method
31
+ when :get then connection.setopt :HTTPGET, 1
32
+ when :head then connection.setopt :NOBODY, 1
33
+ when :post then connection.setopt :POST, 1
34
+ connection.setopt :POSTFIELDS, options[:payload]
35
+ connection.setopt :POSTFIELDSIZE, options[:payload].size
36
+ when :put then connection.setopt :CUSTOMREQUEST, "PUT"
37
+ connection.setopt :POSTFIELDS, options[:payload]
38
+ connection.setopt :POSTFIELDSIZE, options[:payload].size
39
+ when :delete then connection.setopt :CUSTOMREQUEST, "DELETE"
40
+ # else I WILL CUT YOU
41
+ end
42
+
43
+ if options[:headers].is_a? Hash and options[:headers].size > 0
44
+ options[:headers].each_pair do |key_and_value|
45
+ request_headers = CurlFFI.slist_append(request_headers, key_and_value.join(": "))
46
+ end
47
+ connection.setopt :HTTPHEADER, request_headers
48
+ end
49
+
50
+ @custom_header_handler = options[:response_header_handler] if options.has_key?(:response_header_handler)
51
+ @custom_write_handler = options[:response_body_handler] if options.has_key?(:response_body_handler)
52
+
53
+ default_header_handler
54
+ default_write_handler
55
+
56
+
57
+ connection.setopt :ENCODING, "identity, deflate, gzip" unless method == :head
58
+ connection.setopt :URL, url
59
+
60
+ # Other common options (blame streamly guy)
61
+ connection.setopt :FOLLOWLOCATION, 1
62
+ connection.setopt :MAXREDIRS, 3
63
+ # @TODO: This should be an option
64
+ connection.setopt :SSL_VERIFYPEER, 0
65
+ connection.setopt :SSL_VERIFYHOST, 0
66
+
67
+ connection.setopt :ERRORBUFFER, error_buffer
68
+
69
+ return self # I am a terrible hack. I should abandon the singleton
70
+ end
71
+
72
+ def connection
73
+ @connection ||= CurlFFI::Easy.new
74
+ end
75
+
76
+ def error_buffer
77
+ @error_buffer ||= FFI::MemoryPointer.new(:char, CurlFFI::ERROR_SIZE, :clear)
78
+ end
79
+
80
+ def request_headers
81
+ @request_headers ||= FFI::MemoryPointer.from_string("")
82
+ end
83
+
84
+ def response_body
85
+ @response_body ||= ""
86
+ end
87
+
88
+ def response_header
89
+ @response_header ||= ""
90
+ end
91
+
92
+ def execute
93
+ status = connection.perform
94
+
95
+ # @TODO: Intelligent error stuff
96
+ # raise Streamly::Error if status
97
+
98
+ CurlFFI.slist_free_all(@request_headers) if @request_headers
99
+
100
+ @connection.reset
101
+ end
102
+
103
+ def self.execute(options={})
104
+ request = new(options)
105
+
106
+ request.execute
107
+
108
+ return nil if(options.has_key?(:response_header_handler) or options.has_key?(:response_body_handler))
109
+
110
+ resp = if(options[:method] == :head && request.response_header.respond_to?(:to_str))
111
+ request.response_header
112
+ elsif(request.response_body.is_a?(String))
113
+ request.response_body
114
+ else
115
+ nil
116
+ end
117
+
118
+ return resp
119
+ end
120
+
121
+ def default_write_handler
122
+ connection.setopt(:WRITEFUNCTION, FFI::Function.new(:size_t, [:pointer, :size_t, :size_t,], &self.__method__(:write_callback)))
123
+ end
124
+
125
+ def default_header_handler
126
+ connection.setopt(:HEADERFUNCTION, FFI::Function.new(:size_t, [:pointer, :size_t, :size_t], &self.__method__(:header_callback)))
127
+ end
128
+
129
+ def write_callback(string_ptr, size, nmemb)
130
+ length = size * nmemb
131
+
132
+ if(@custom_write_handler)
133
+ @custom_write_handler.call(string_ptr.read_string(length))
134
+ else
135
+ response_body << string_ptr.read_string(length)
136
+ end
137
+
138
+ return length
139
+ end
140
+
141
+ def header_callback(string_ptr, size, nmemb)
142
+ length = size * nmemb
143
+
144
+ if(@custom_header_handler)
145
+ @custom_header_handler.call(string_ptr.read_string(length))
146
+ else
147
+ response_header << string_ptr.read_string(length)
148
+ end
149
+
150
+ return length
151
+ end
152
+
153
+ # streamly's .c internal methods:
154
+ # @TODO: header_handler
155
+ # @TODO: data_handler
156
+ # @TODO: each_http_header
157
+ # @TODO: select_error
158
+ # @TODO: rb_streamly_new
159
+ # @TODO: rb_streamly_init
160
+ # @TODO: nogvl_perform
161
+ # @TODO: rb_streamly_execute
162
+ end
163
+ end
@@ -0,0 +1,3 @@
1
+ module StreamlyFFI
2
+ VERSION = "0.1.5"
3
+ end
data/spec/server.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+
4
+ get '/' do
5
+ "Hello, #{params[:name]}".strip
6
+ end
7
+
8
+ post '/' do
9
+ "Hello, #{params[:name]}".strip
10
+ end
11
+
12
+ put '/' do
13
+ "Hello, #{params[:name]}".strip
14
+ end
15
+
16
+ delete '/' do
17
+ "Hello, #{params[:name]}".strip
18
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: UTF-8
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+
6
+ require 'streamly_ffi'
7
+ require 'rspec'
@@ -0,0 +1,293 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path("../spec_helper", File.dirname(__FILE__))
3
+
4
+ describe StreamlyFFI::Request do
5
+
6
+ before(:all) do
7
+ @response = "Hello, brian".strip
8
+ end
9
+
10
+ describe "HEAD" do
11
+ describe "basic" do
12
+ it "should perform a basic request" do
13
+ resp = Streamly.head('localhost:4567')
14
+ resp.should_not be_nil
15
+ end
16
+ =begin
17
+ if RUBY_VERSION =~ /^1.9/
18
+ it "should default to utf-8 if Encoding.default_internal is nil" do
19
+ Encoding.default_internal = nil
20
+ Streamly.head('localhost:4567').encoding.should eql(Encoding.find('utf-8'))
21
+ end
22
+
23
+ it "should use Encoding.default_internal" do
24
+ Encoding.default_internal = Encoding.find('utf-8')
25
+ Streamly.head('localhost:4567').encoding.should eql(Encoding.default_internal)
26
+ Encoding.default_internal = Encoding.find('us-ascii')
27
+ Streamly.head('localhost:4567').encoding.should eql(Encoding.default_internal)
28
+ end
29
+ end
30
+ =end
31
+ end
32
+
33
+ describe "streaming" do
34
+ it "should perform a basic request and stream header chunks to the caller" do
35
+ streamed_response = ""
36
+ resp = Streamly.head('localhost:4567') do |chunk|
37
+ chunk.should_not be_empty
38
+ streamed_response << chunk
39
+ end
40
+ resp.should be_nil
41
+ streamed_response.should_not be_nil
42
+ end
43
+ =begin
44
+ if RUBY_VERSION =~ /^1.9/
45
+ it "should default to utf-8 if Encoding.default_internal is nil" do
46
+ Encoding.default_internal = nil
47
+ Streamly.head('localhost:4567') do |chunk|
48
+ chunk.encoding.should eql(Encoding.find('utf-8'))
49
+ end
50
+ end
51
+
52
+ it "should use Encoding.default_internal" do
53
+ Encoding.default_internal = Encoding.find('utf-8')
54
+ Streamly.head('localhost:4567') do |chunk|
55
+ chunk.encoding.should eql(Encoding.default_internal)
56
+ end
57
+ Encoding.default_internal = Encoding.find('us-ascii')
58
+ Streamly.head('localhost:4567') do |chunk|
59
+ chunk.encoding.should eql(Encoding.default_internal)
60
+ end
61
+ end
62
+ end
63
+ =end
64
+ end
65
+ end
66
+
67
+ describe "GET" do
68
+ describe "basic" do
69
+ it "should perform a basic request" do
70
+ resp = Streamly.get('localhost:4567/?name=brian')
71
+ resp.should eql(@response)
72
+ end
73
+ =begin
74
+ if RUBY_VERSION =~ /^1.9/
75
+ it "should default to utf-8 if Encoding.default_internal is nil" do
76
+ Encoding.default_internal = nil
77
+ Streamly.get('localhost:4567').encoding.should eql(Encoding.find('utf-8'))
78
+ end
79
+
80
+ it "should use Encoding.default_internal" do
81
+ Encoding.default_internal = Encoding.find('utf-8')
82
+ Streamly.get('localhost:4567').encoding.should eql(Encoding.default_internal)
83
+ Encoding.default_internal = Encoding.find('us-ascii')
84
+ Streamly.get('localhost:4567').encoding.should eql(Encoding.default_internal)
85
+ end
86
+ end
87
+ =end
88
+ end
89
+
90
+ describe "streaming" do
91
+ it "should perform a basic request and stream the response to the caller" do
92
+ streamed_response = ''
93
+ resp = Streamly.get('localhost:4567/?name=brian') do |chunk|
94
+ chunk.should_not be_empty
95
+ streamed_response << chunk
96
+ end
97
+ resp.should be_nil
98
+ streamed_response.should eql(@response)
99
+ end
100
+ =begin
101
+ if RUBY_VERSION =~ /^1.9/
102
+ it "should default to utf-8 if Encoding.default_internal is nil" do
103
+ Encoding.default_internal = nil
104
+ Streamly.get('localhost:4567') do |chunk|
105
+ chunk.encoding.should eql(Encoding.find('utf-8'))
106
+ end
107
+ end
108
+
109
+ it "should use Encoding.default_internal" do
110
+ Encoding.default_internal = Encoding.find('utf-8')
111
+ Streamly.get('localhost:4567') do |chunk|
112
+ chunk.encoding.should eql(Encoding.default_internal)
113
+ end
114
+ Encoding.default_internal = Encoding.find('us-ascii')
115
+ Streamly.get('localhost:4567') do |chunk|
116
+ chunk.encoding.should eql(Encoding.default_internal)
117
+ end
118
+ end
119
+ end
120
+ =end
121
+ end
122
+ end
123
+
124
+ describe "POST" do
125
+ describe "basic" do
126
+ it "should perform a basic request" do
127
+ resp = Streamly.post('localhost:4567', 'name=brian')
128
+ resp.should eql(@response)
129
+ end
130
+ =begin
131
+ if RUBY_VERSION =~ /^1.9/
132
+ it "should default to utf-8 if Encoding.default_internal is nil" do
133
+ Encoding.default_internal = nil
134
+ Streamly.post('localhost:4567', 'name=brian').encoding.should eql(Encoding.find('utf-8'))
135
+ end
136
+
137
+ it "should use Encoding.default_internal" do
138
+ Encoding.default_internal = Encoding.find('utf-8')
139
+ Streamly.post('localhost:4567', 'name=brian').encoding.should eql(Encoding.default_internal)
140
+ Encoding.default_internal = Encoding.find('us-ascii')
141
+ Streamly.post('localhost:4567', 'name=brian').encoding.should eql(Encoding.default_internal)
142
+ end
143
+ end
144
+ =end
145
+ end
146
+
147
+ describe "streaming" do
148
+ it "should perform a basic request and stream the response to the caller" do
149
+ streamed_response = ''
150
+ resp = Streamly.post('localhost:4567', 'name=brian') do |chunk|
151
+ chunk.should_not be_empty
152
+ streamed_response << chunk
153
+ end
154
+ resp.should be_nil
155
+ streamed_response.should eql(@response)
156
+ end
157
+ =begin
158
+ if RUBY_VERSION =~ /^1.9/
159
+ it "should default to utf-8 if Encoding.default_internal is nil" do
160
+ Encoding.default_internal = nil
161
+ Streamly.post('localhost:4567', 'name=brian') do |chunk|
162
+ chunk.encoding.should eql(Encoding.find('utf-8'))
163
+ end
164
+ end
165
+
166
+ it "should use Encoding.default_internal" do
167
+ Encoding.default_internal = Encoding.find('utf-8')
168
+ Streamly.post('localhost:4567', 'name=brian') do |chunk|
169
+ chunk.encoding.should eql(Encoding.default_internal)
170
+ end
171
+ Encoding.default_internal = Encoding.find('us-ascii')
172
+ Streamly.post('localhost:4567', 'name=brian') do |chunk|
173
+ chunk.encoding.should eql(Encoding.default_internal)
174
+ end
175
+ end
176
+ end
177
+ =end
178
+ end
179
+ end
180
+
181
+ describe "PUT" do
182
+ describe "basic" do
183
+ it "should perform a basic request" do
184
+ resp = Streamly.put('localhost:4567', 'name=brian')
185
+ resp.should eql(@response)
186
+ end
187
+ =begin
188
+ if RUBY_VERSION =~ /^1.9/
189
+ it "should default to utf-8 if Encoding.default_internal is nil" do
190
+ Encoding.default_internal = nil
191
+ Streamly.put('localhost:4567', 'name=brian').encoding.should eql(Encoding.find('utf-8'))
192
+ end
193
+
194
+ it "should use Encoding.default_internal" do
195
+ Encoding.default_internal = Encoding.find('utf-8')
196
+ Streamly.put('localhost:4567', 'name=brian').encoding.should eql(Encoding.default_internal)
197
+ Encoding.default_internal = Encoding.find('us-ascii')
198
+ Streamly.put('localhost:4567', 'name=brian').encoding.should eql(Encoding.default_internal)
199
+ end
200
+ end
201
+ =end
202
+ end
203
+
204
+ describe "streaming" do
205
+ it "should perform a basic request and stream the response to the caller" do
206
+ streamed_response = ''
207
+ resp = Streamly.put('localhost:4567', 'name=brian') do |chunk|
208
+ chunk.should_not be_empty
209
+ streamed_response << chunk
210
+ end
211
+ resp.should be_nil
212
+ streamed_response.should eql(@response)
213
+ end
214
+ =begin
215
+ if RUBY_VERSION =~ /^1.9/
216
+ it "should default to utf-8 if Encoding.default_internal is nil" do
217
+ Encoding.default_internal = nil
218
+ Streamly.put('localhost:4567', 'name=brian') do |chunk|
219
+ chunk.encoding.should eql(Encoding.find('utf-8'))
220
+ end
221
+ end
222
+
223
+ it "should use Encoding.default_internal" do
224
+ Encoding.default_internal = Encoding.find('utf-8')
225
+ Streamly.put('localhost:4567', 'name=brian') do |chunk|
226
+ chunk.encoding.should eql(Encoding.default_internal)
227
+ end
228
+ Encoding.default_internal = Encoding.find('us-ascii')
229
+ Streamly.put('localhost:4567', 'name=brian') do |chunk|
230
+ chunk.encoding.should eql(Encoding.default_internal)
231
+ end
232
+ end
233
+ end
234
+ =end
235
+ end
236
+ end
237
+
238
+ describe "DELETE" do
239
+ describe "basic" do
240
+ it "should perform a basic request" do
241
+ resp = Streamly.delete('localhost:4567/?name=brian').should eql(@response)
242
+ end
243
+ =begin
244
+ if RUBY_VERSION =~ /^1.9/
245
+ it "should default to utf-8 if Encoding.default_internal is nil" do
246
+ Encoding.default_internal = nil
247
+ Streamly.delete('localhost:4567/?name=brian').encoding.should eql(Encoding.find('utf-8'))
248
+ end
249
+
250
+ it "should use Encoding.default_internal" do
251
+ Encoding.default_internal = Encoding.find('utf-8')
252
+ Streamly.delete('localhost:4567/?name=brian').encoding.should eql(Encoding.default_internal)
253
+ Encoding.default_internal = Encoding.find('us-ascii')
254
+ Streamly.delete('localhost:4567/?name=brian').encoding.should eql(Encoding.default_internal)
255
+ end
256
+ end
257
+ =end
258
+ end
259
+
260
+ describe "streaming" do
261
+ it "should perform a basic request and stream the response to the caller" do
262
+ streamed_response = ''
263
+ resp = Streamly.delete('localhost:4567/?name=brian') do |chunk|
264
+ chunk.should_not be_empty
265
+ streamed_response << chunk
266
+ end
267
+ resp.should be_nil
268
+ streamed_response.should eql(@response)
269
+ end
270
+ =begin
271
+ if RUBY_VERSION =~ /^1.9/
272
+ it "should default to utf-8 if Encoding.default_internal is nil" do
273
+ Encoding.default_internal = nil
274
+ Streamly.delete('localhost:4567/?name=brian') do |chunk|
275
+ chunk.encoding.should eql(Encoding.find('utf-8'))
276
+ end
277
+ end
278
+
279
+ it "should use Encoding.default_internal" do
280
+ Encoding.default_internal = Encoding.find('utf-8')
281
+ Streamly.delete('localhost:4567/?name=brian') do |chunk|
282
+ chunk.encoding.should eql(Encoding.default_internal)
283
+ end
284
+ Encoding.default_internal = Encoding.find('us-ascii')
285
+ Streamly.delete('localhost:4567/?name=brian') do |chunk|
286
+ chunk.encoding.should eql(Encoding.default_internal)
287
+ end
288
+ end
289
+ end
290
+ =end
291
+ end
292
+ end
293
+ end