streamly_ffi 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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