brianmario-streamly 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.bundle
2
+ *.o
3
+ Makefile
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 (July 27th, 2009)
4
+ * Version bump for Github's gem builder
5
+
6
+ ## 0.1.0 (July 27th, 2009)
7
+ * Initial release
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2009 Brian Lopez - http://github.com/brianmario
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,62 @@
1
+ = A streaming REST client for Ruby that uses libcurl
2
+
3
+ == Features
4
+ * rest-client like API
5
+ * Streaming API allows the caller to be handed chunks of the response while it's being received
6
+
7
+ == How to install
8
+
9
+ Install it like any other gem hosted at the Githubs like so:
10
+
11
+ (more instructions here: http://gems.github.com)
12
+
13
+ sudo gem install brianmario-streamly
14
+
15
+ == Example of use
16
+
17
+ === A basic GET request
18
+
19
+ Streamly.get('www.somehost.com')
20
+
21
+ === A streaming GET request
22
+
23
+ Streamly.get('www.somehost.com') do |chunk|
24
+ # do something with _chunk_
25
+ end
26
+
27
+ === A basic POST request
28
+
29
+ Streamly.post('www.somehost.com', 'blah=foo')
30
+
31
+ === A streaming POST request
32
+
33
+ Streamly.post('www.somehost.com', 'blah=foo') do |chunk|
34
+ # do something with _chunk_
35
+ end
36
+
37
+ == Benchmarks
38
+
39
+ Fetching 2,405,005 bytes of JSON from a local lighttpd server
40
+
41
+ * Streamly: 0.011s
42
+ * Shell out to curl: 0.046s
43
+ * rest-client: 0.205s
44
+
45
+ Streaming, and parsing 2,405,005 bytes of JSON from a local lighttpd server
46
+
47
+ * Streamly: 0.231s
48
+ * Shell out to curl: 0.341s
49
+ * rest-client: 0.447s
50
+
51
+ == Special Thanks
52
+
53
+ There are quite a few *extremely* nice REST client libraries out there for Ruby today. I especially owe thanks
54
+ to the following projects. Without them I probably would have never had the inspiration to even take the time
55
+ to write this library. In Streamly, you'll find snippets of code, API patterns and examples from all 3 of these projects.
56
+ I'll do my best to make sure I give credit where it's due *in* the source. Please let me know if I've missed something!
57
+
58
+ * rest-client - https://github.com/adamwiggins/rest-client
59
+ * curb - https://github.com/taf2/curb
60
+ * patron - https://github.com/toland/patron
61
+
62
+ And again, the Github crew for this amazing service!
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ # encoding: UTF-8
2
+ begin
3
+ require 'jeweler'
4
+ Jeweler::Tasks.new do |gem|
5
+ gem.name = "streamly"
6
+ gem.summary = "A streaming REST client for Ruby, in C."
7
+ gem.email = "seniorlopez@gmail.com"
8
+ gem.homepage = "http://github.com/brianmario/streamly"
9
+ gem.authors = ["Brian Lopez"]
10
+ gem.require_paths = ["lib", "ext"]
11
+ gem.extra_rdoc_files = `git ls-files *.rdoc`.split("\n")
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.extensions = ["ext/extconf.rb"]
14
+ gem.files.include %w(lib/jeweler/templates/.document lib/jeweler/templates/.gitignore)
15
+ gem.rubyforge_project = "streamly"
16
+ end
17
+ rescue LoadError
18
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
19
+ end
20
+
21
+ require 'rake'
22
+ require 'spec/rake/spectask'
23
+
24
+ desc "Run all examples with RCov"
25
+ Spec::Rake::SpecTask.new('spec:rcov') do |t|
26
+ t.spec_files = FileList['spec/']
27
+ t.rcov = true
28
+ t.rcov_opts = lambda do
29
+ IO.readlines("spec/rcov.opts").map {|l| l.chomp.split " "}.flatten
30
+ end
31
+ end
32
+ Spec::Rake::SpecTask.new('spec') do |t|
33
+ t.spec_files = FileList['spec/']
34
+ t.spec_opts << '--options' << 'spec/spec.opts'
35
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 1
4
+ :major: 0
@@ -0,0 +1,33 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'rubygems'
5
+ require 'net/http'
6
+ require 'rest_client'
7
+ require 'streamly'
8
+ require 'benchmark'
9
+
10
+ url = ARGV[0]
11
+
12
+ Benchmark.bm do |x|
13
+ puts "Shell out to curl"
14
+ x.report do
15
+ (ARGV[1] || 1).to_i.times do
16
+ `curl -s --compressed #{url}`
17
+ end
18
+ end
19
+
20
+ puts "Streamly"
21
+ x.report do
22
+ (ARGV[1] || 1).to_i.times do
23
+ Streamly.get(url)
24
+ end
25
+ end
26
+
27
+ puts "rest-client"
28
+ x.report do
29
+ (ARGV[1] || 1).to_i.times do
30
+ RestClient.get(url, {"Accept-Encoding" => "identity, deflate, gzip"})
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'rubygems'
5
+ require 'net/http'
6
+ require 'rest_client'
7
+ require 'streamly'
8
+ require 'yajl'
9
+ require 'benchmark'
10
+
11
+ url = ARGV[0]
12
+
13
+ Benchmark.bm do |x|
14
+ puts "Shell out to curl"
15
+ parser = Yajl::Parser.new
16
+ x.report do
17
+ (ARGV[1] || 1).to_i.times do
18
+ parser.parse `curl -s --compressed #{url}`
19
+ end
20
+ end
21
+
22
+ puts "Streamly"
23
+ parser = Yajl::Parser.new
24
+ parser.on_parse_complete = lambda {|obj| }
25
+ x.report do
26
+ (ARGV[1] || 1).to_i.times do
27
+ Streamly.get(url) do |chunk|
28
+ parser << chunk
29
+ end
30
+ end
31
+ end
32
+
33
+ puts "rest-client"
34
+ parser = Yajl::Parser.new
35
+ x.report do
36
+ (ARGV[1] || 1).to_i.times do
37
+ parser.parse RestClient.get(url, {"Accept-Encoding" => "identity, deflate, gzip"})
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
3
+
4
+ require 'streamly'
5
+
6
+ raise Exception, "You must pass a URL to request" if ARGV[0].nil?
7
+
8
+ url = ARGV[0]
9
+ puts Streamly.delete(url)
@@ -0,0 +1,9 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
3
+
4
+ require 'streamly'
5
+
6
+ raise Exception, "You must pass a URL to request" if ARGV[0].nil?
7
+
8
+ url = ARGV[0]
9
+ puts Streamly.get(url)
@@ -0,0 +1,9 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
3
+
4
+ require 'streamly'
5
+
6
+ raise Exception, "You must pass a URL to request" if ARGV[0].nil?
7
+
8
+ url = ARGV[0]
9
+ puts Streamly.head(url)
@@ -0,0 +1,11 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
3
+
4
+ require 'streamly'
5
+
6
+ raise Exception, "You must pass a URL to request" if ARGV[0].nil?
7
+ raise Exception, "You must pass a payload to POST" if ARGV[1].nil?
8
+
9
+ url = ARGV[0]
10
+ payload = ARGV[1]
11
+ puts Streamly.post(url, payload)
@@ -0,0 +1,11 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
3
+
4
+ require 'streamly'
5
+
6
+ raise Exception, "You must pass a URL to request" if ARGV[0].nil?
7
+ raise Exception, "You must pass a payload to PUT" if ARGV[1].nil?
8
+
9
+ url = ARGV[0]
10
+ payload = ARGV[1]
11
+ puts Streamly.put(url, payload)
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
3
+
4
+ require 'streamly'
5
+
6
+ raise Exception, "You must pass a URL to request" if ARGV[0].nil?
7
+
8
+ url = ARGV[0]
9
+ Streamly.delete(url) do |chunk|
10
+ STDOUT.write chunk
11
+ STDOUT.flush
12
+ end
13
+ puts
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
3
+
4
+ require 'streamly'
5
+
6
+ raise Exception, "You must pass a URL to request" if ARGV[0].nil?
7
+
8
+ url = ARGV[0]
9
+ Streamly.get(url) do |chunk|
10
+ STDOUT.write chunk
11
+ STDOUT.flush
12
+ end
13
+ puts
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
3
+
4
+ require 'streamly'
5
+
6
+ raise Exception, "You must pass a URL to request" if ARGV[0].nil?
7
+
8
+ url = ARGV[0]
9
+ Streamly.head(url) do |chunk|
10
+ STDOUT.write chunk
11
+ STDOUT.flush
12
+ end
13
+ puts
@@ -0,0 +1,15 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
3
+
4
+ require 'streamly'
5
+
6
+ raise Exception, "You must pass a URL to request" if ARGV[0].nil?
7
+ raise Exception, "You must pass a payload to POST" if ARGV[1].nil?
8
+
9
+ url = ARGV[0]
10
+ payload = ARGV[1]
11
+ Streamly.post(url, payload) do |chunk|
12
+ STDOUT.write chunk
13
+ STDOUT.flush
14
+ end
15
+ puts
@@ -0,0 +1,16 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../../lib')
3
+
4
+ require 'streamly'
5
+
6
+ raise Exception, "You must pass a URL to request" if ARGV[0].nil?
7
+ raise Exception, "You must pass a payload to PUT" if ARGV[1].nil?
8
+
9
+ url = ARGV[0]
10
+ payload = ARGV[1]
11
+ resp = ''
12
+ Streamly.put(url, payload) do |chunk|
13
+ STDOUT.write chunk
14
+ STDOUT.flush
15
+ end
16
+ puts
data/ext/extconf.rb ADDED
@@ -0,0 +1,25 @@
1
+ # encoding: UTF-8
2
+ require 'mkmf'
3
+ require 'rbconfig'
4
+
5
+ $CFLAGS << ' -DHAVE_RBTRAP' if have_var('rb_trap_immediate', ['ruby.h', 'rubysig.h'])
6
+ # add_define 'HAVE_RBTRAP' if have_var('rb_trap_immediate', ['ruby.h', 'rubysig.h'])
7
+
8
+ # Borrowed from taf2-curb
9
+ dir_config('curl')
10
+ if find_executable('curl-config')
11
+ $CFLAGS << " #{`curl-config --cflags`.strip}"
12
+ $LIBS << " #{`curl-config --libs`.strip}"
13
+ elsif !have_library('curl') or !have_header('curl/curl.h')
14
+ fail <<-EOM
15
+ Can't find libcurl or curl/curl.h
16
+
17
+ Try passing --with-curl-dir or --with-curl-lib and --with-curl-include
18
+ options to extconf.
19
+ EOM
20
+ end
21
+
22
+ $CFLAGS << ' -Wall'
23
+ # $CFLAGS << ' -O0 -ggdb'
24
+
25
+ create_makefile("streamly_ext")
data/ext/streamly.c ADDED
@@ -0,0 +1,349 @@
1
+ // static size_t put_data_handler(char * stream, size_t size, size_t nmemb, VALUE upload_stream) {
2
+ // size_t result = 0;
3
+ //
4
+ // // TODO
5
+ // // Change upload_stream back to a VALUE
6
+ // // if TYPE(upload_stream) == T_STRING - read at most "len" continuously
7
+ // // if upload_stream is IO-like, read chunks of it
8
+ // // OR
9
+ // // if upload_stream responds to "each", use that?
10
+ //
11
+ // TRAP_BEG;
12
+ // // if (upload_stream != NULL && *upload_stream != NULL) {
13
+ // // int len = size * nmemb;
14
+ // // char *s1 = strncpy(stream, *upload_stream, len);
15
+ // // result = strlen(s1);
16
+ // // *upload_stream += result;
17
+ // // }
18
+ // TRAP_END;
19
+ //
20
+ // return result;
21
+ // }
22
+
23
+ #include "streamly.h"
24
+
25
+ static size_t header_handler(char * stream, size_t size, size_t nmemb, VALUE handler) {
26
+ TRAP_BEG;
27
+ if(TYPE(handler) == T_STRING) {
28
+ rb_str_buf_cat(handler, stream, size * nmemb);
29
+ } else {
30
+ rb_funcall(handler, rb_intern("call"), 1, rb_str_new(stream, size * nmemb));
31
+ }
32
+ TRAP_END;
33
+ return size * nmemb;
34
+ }
35
+
36
+ static size_t data_handler(char * stream, size_t size, size_t nmemb, VALUE handler) {
37
+ TRAP_BEG;
38
+ if(TYPE(handler) == T_STRING) {
39
+ rb_str_buf_cat(handler, stream, size * nmemb);
40
+ } else {
41
+ rb_funcall(handler, rb_intern("call"), 1, rb_str_new(stream, size * nmemb));
42
+ }
43
+ TRAP_END;
44
+ return size * nmemb;
45
+ }
46
+
47
+ void streamly_instance_mark(struct curl_instance * instance) {
48
+ rb_gc_mark(instance->request_method);
49
+ rb_gc_mark(instance->request_payload_handler);
50
+ rb_gc_mark(instance->response_header_handler);
51
+ rb_gc_mark(instance->response_body_handler);
52
+ rb_gc_mark(instance->options);
53
+ }
54
+
55
+ void streamly_instance_free(struct curl_instance * instance) {
56
+ curl_easy_cleanup(instance->handle);
57
+ free(instance);
58
+ }
59
+
60
+ // Initially borrowed from Patron - http://github.com/toland/patron
61
+ // slightly modified for Streamly
62
+ static VALUE each_http_header(VALUE header, VALUE self) {
63
+ struct curl_instance * instance;
64
+ GetInstance(self, instance);
65
+ VALUE name, value, header_str = Qnil;
66
+
67
+ name = rb_obj_as_string(rb_ary_entry(header, 0));
68
+ value = rb_obj_as_string(rb_ary_entry(header, 1));
69
+
70
+ header_str = rb_str_plus(name, rb_str_new2(": "));
71
+ header_str = rb_str_plus(header_str, value);
72
+
73
+ instance->request_headers = curl_slist_append(instance->request_headers, RSTRING_PTR(header_str));
74
+ rb_gc_mark(name);
75
+ rb_gc_mark(value);
76
+ rb_gc_mark(header_str);
77
+ return Qnil;
78
+ }
79
+
80
+ // Initially borrowed from Patron - http://github.com/toland/patron
81
+ // slightly modified for Streamly
82
+ static VALUE select_error(CURLcode code) {
83
+ VALUE error = Qnil;
84
+
85
+ switch (code) {
86
+ case CURLE_UNSUPPORTED_PROTOCOL:
87
+ error = eUnsupportedProtocol;
88
+ break;
89
+ case CURLE_URL_MALFORMAT:
90
+ error = eURLFormatError;
91
+ break;
92
+ case CURLE_COULDNT_RESOLVE_HOST:
93
+ error = eHostResolutionError;
94
+ break;
95
+ case CURLE_COULDNT_CONNECT:
96
+ error = eConnectionFailed;
97
+ break;
98
+ case CURLE_PARTIAL_FILE:
99
+ error = ePartialFileError;
100
+ break;
101
+ case CURLE_OPERATION_TIMEDOUT:
102
+ error = eTimeoutError;
103
+ break;
104
+ case CURLE_TOO_MANY_REDIRECTS:
105
+ error = eTooManyRedirects;
106
+ break;
107
+ default:
108
+ error = eStreamlyError;
109
+ }
110
+
111
+ return error;
112
+ }
113
+
114
+ /*
115
+ * Document-class: Streamly::Request
116
+ *
117
+ * A streaming REST client for Ruby that uses libcurl to do the heavy lifting.
118
+ * The API is almost exactly like rest-client, so users of that library should find it very familiar.
119
+ */
120
+ /*
121
+ * Document-method: new
122
+ *
123
+ * call-seq: new(args)
124
+ *
125
+ * +args+ should be a Hash and is required
126
+ * This Hash should at least contain +:url+ and +:method+ keys.
127
+ * You may also provide the following optional keys:
128
+ * +:headers+ - should be a Hash of name/value pairs
129
+ * +:response_header_handler+ - can be a string or object that responds to #call
130
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
131
+ * +:response_body_handler+ - can be a string or object that responds to #call
132
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
133
+ * +:payload+ - If +:method+ is either +:post+ or +:put+ this will be used as the request body
134
+ *
135
+ */
136
+ VALUE rb_streamly_new(int argc, VALUE * argv, VALUE klass) {
137
+ struct curl_instance * instance;
138
+ VALUE obj = Data_Make_Struct(klass, struct curl_instance, streamly_instance_mark, streamly_instance_free, instance);
139
+ rb_obj_call_init(obj, argc, argv);
140
+ return obj;
141
+ }
142
+
143
+ /*
144
+ * Document-method: initialize
145
+ *
146
+ * call-seq: initialize(args)
147
+ *
148
+ * +args+ should be a Hash and is required
149
+ * This Hash should at least contain +:url+ and +:method+ keys.
150
+ * You may also provide the following optional keys:
151
+ * +:headers+ - should be a Hash of name/value pairs
152
+ * +:response_header_handler+ - can be a string or object that responds to #call
153
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
154
+ * +:response_body_handler+ - can be a string or object that responds to #call
155
+ * If an object was passed, it's #call method will be called and passed the current chunk of data
156
+ * +:payload+ - If +:method+ is either +:post+ or +:put+ this will be used as the request body
157
+ *
158
+ */
159
+ VALUE rb_streamly_init(int argc, VALUE * argv, VALUE self) {
160
+ struct curl_instance * instance;
161
+ char * credential_sep = ":";
162
+ VALUE args, url, payload, headers, username, password, credentials;
163
+
164
+ GetInstance(self, instance);
165
+ instance->handle = curl_easy_init();
166
+ instance->request_headers = NULL;
167
+ instance->request_method = Qnil;
168
+ instance->request_payload_handler = Qnil;
169
+ instance->response_header_handler = Qnil;
170
+ instance->response_body_handler = Qnil;
171
+ instance->options = Qnil;
172
+
173
+ rb_scan_args(argc, argv, "10", &args);
174
+
175
+ // Ensure our args parameter is a hash
176
+ Check_Type(args, T_HASH);
177
+
178
+ instance->request_method = rb_hash_aref(args, sym_method);
179
+ url = rb_hash_aref(args, sym_url);
180
+ payload = rb_hash_aref(args, sym_payload);
181
+ headers = rb_hash_aref(args, sym_headers);
182
+ username = rb_hash_aref(args, sym_username);
183
+ password = rb_hash_aref(args, sym_password);
184
+ instance->response_header_handler = rb_hash_aref(args, sym_response_header_handler);
185
+ instance->response_body_handler = rb_hash_aref(args, sym_response_body_handler);
186
+
187
+ // First lets verify we have a :method key
188
+ if (NIL_P(instance->request_method)) {
189
+ rb_raise(eStreamlyError, "You must specify a :method");
190
+ } else {
191
+ // OK, a :method was specified, but if it's POST or PUT we require a :payload
192
+ if (instance->request_method == sym_post || instance->request_method == sym_put) {
193
+ if (NIL_P(payload)) {
194
+ rb_raise(eStreamlyError, "You must specify a :payload for POST and PUT requests");
195
+ }
196
+ }
197
+ }
198
+
199
+ // Now verify a :url was provided
200
+ if (NIL_P(url)) {
201
+ rb_raise(eStreamlyError, "You must specify a :url to request");
202
+ }
203
+
204
+ if (NIL_P(instance->response_header_handler)) {
205
+ instance->response_header_handler = rb_str_new2("");
206
+ }
207
+ if (instance->request_method != sym_head && NIL_P(instance->response_body_handler)) {
208
+ instance->response_body_handler = rb_str_new2("");
209
+ }
210
+
211
+ if (!NIL_P(headers)) {
212
+ Check_Type(headers, T_HASH);
213
+ rb_iterate(rb_each, headers, each_http_header, self);
214
+ curl_easy_setopt(instance->handle, CURLOPT_HTTPHEADER, instance->request_headers);
215
+ }
216
+
217
+ // So far so good, lets start setting up our request
218
+
219
+ // Set the type of request
220
+ if (instance->request_method == sym_head) {
221
+ curl_easy_setopt(instance->handle, CURLOPT_NOBODY, 1);
222
+ } else if (instance->request_method == sym_get) {
223
+ curl_easy_setopt(instance->handle, CURLOPT_HTTPGET, 1);
224
+ } else if (instance->request_method == sym_post) {
225
+ curl_easy_setopt(instance->handle, CURLOPT_POST, 1);
226
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDS, RSTRING_PTR(payload));
227
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDSIZE, RSTRING_LEN(payload));
228
+
229
+ // (multipart)
230
+ // curl_easy_setopt(instance->handle, CURLOPT_HTTPPOST, 1);
231
+
232
+ // TODO: get streaming upload working
233
+ // curl_easy_setopt(instance->handle, CURLOPT_READFUNCTION, &put_data_handler);
234
+ // curl_easy_setopt(instance->handle, CURLOPT_READDATA, &instance->upload_stream);
235
+ // curl_easy_setopt(instance->handle, CURLOPT_INFILESIZE, len);
236
+ } else if (instance->request_method == sym_put) {
237
+ curl_easy_setopt(instance->handle, CURLOPT_CUSTOMREQUEST, "PUT");
238
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDS, RSTRING_PTR(payload));
239
+ curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDSIZE, RSTRING_LEN(payload));
240
+
241
+ // TODO: get streaming upload working
242
+ // curl_easy_setopt(instance->handle, CURLOPT_UPLOAD, 1);
243
+ // curl_easy_setopt(instance->handle, CURLOPT_READFUNCTION, &put_data_handler);
244
+ // curl_easy_setopt(instance->handle, CURLOPT_READDATA, &instance->upload_stream);
245
+ // curl_easy_setopt(instance->handle, CURLOPT_INFILESIZE, len);
246
+ } else if (instance->request_method == sym_delete) {
247
+ curl_easy_setopt(instance->handle, CURLOPT_CUSTOMREQUEST, "DELETE");
248
+ }
249
+
250
+ // Other common options
251
+ curl_easy_setopt(instance->handle, CURLOPT_URL, RSTRING_PTR(url));
252
+ curl_easy_setopt(instance->handle, CURLOPT_FOLLOWLOCATION, 1);
253
+ curl_easy_setopt(instance->handle, CURLOPT_MAXREDIRS, 3);
254
+
255
+ // Response header handling
256
+ curl_easy_setopt(instance->handle, CURLOPT_HEADERFUNCTION, &header_handler);
257
+ curl_easy_setopt(instance->handle, CURLOPT_HEADERDATA, instance->response_header_handler);
258
+
259
+ // Response body handling
260
+ if (instance->request_method != sym_head) {
261
+ curl_easy_setopt(instance->handle, CURLOPT_ENCODING, "identity, deflate, gzip");
262
+ curl_easy_setopt(instance->handle, CURLOPT_WRITEFUNCTION, &data_handler);
263
+ curl_easy_setopt(instance->handle, CURLOPT_WRITEDATA, instance->response_body_handler);
264
+ }
265
+
266
+ curl_easy_setopt(instance, CURLOPT_USERPWD, NULL);
267
+ if (!NIL_P(username) || !NIL_P(password)) {
268
+ credentials = rb_str_new2("");
269
+ rb_str_buf_cat(credentials, RSTRING_PTR(username), RSTRING_LEN(username));
270
+ rb_str_buf_cat(credentials, credential_sep, 1);
271
+ rb_str_buf_cat(credentials, RSTRING_PTR(password), RSTRING_LEN(password));
272
+ curl_easy_setopt(instance->handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC | CURLAUTH_DIGEST);
273
+ curl_easy_setopt(instance->handle, CURLOPT_USERPWD, RSTRING_PTR(credentials));
274
+ rb_gc_mark(credentials);
275
+ }
276
+
277
+ curl_easy_setopt(instance->handle, CURLOPT_SSL_VERIFYPEER, 0);
278
+ curl_easy_setopt(instance->handle, CURLOPT_SSL_VERIFYHOST, 0);
279
+
280
+ curl_easy_setopt(instance->handle, CURLOPT_ERRORBUFFER, instance->error_buffer);
281
+
282
+ return self;
283
+ }
284
+
285
+ /*
286
+ * Document-method: rb_streamly_execute
287
+ *
288
+ * call-seq: rb_streamly_execute
289
+ */
290
+ VALUE rb_streamly_execute(int argc, VALUE * argv, VALUE self) {
291
+ CURLcode res;
292
+ struct curl_instance * instance;
293
+ GetInstance(self, instance);
294
+
295
+ // Done setting up, lets do this!
296
+ res = curl_easy_perform(instance->handle);
297
+ if (CURLE_OK != res) {
298
+ rb_raise(select_error(res), instance->error_buffer);
299
+ }
300
+
301
+ // Cleanup
302
+ if (instance->request_headers != NULL) {
303
+ curl_slist_free_all(instance->request_headers);
304
+ instance->request_headers = NULL;
305
+ }
306
+ curl_easy_reset(instance->handle);
307
+ instance->request_payload_handler = Qnil;
308
+
309
+ if (instance->request_method == sym_head && TYPE(instance->response_header_handler) == T_STRING) {
310
+ return instance->response_header_handler;
311
+ } else if (TYPE(instance->response_body_handler) == T_STRING) {
312
+ return instance->response_body_handler;
313
+ } else {
314
+ return Qnil;
315
+ }
316
+ }
317
+
318
+ // Ruby Extension initializer
319
+ void Init_streamly_ext() {
320
+ mStreamly = rb_define_module("Streamly");
321
+
322
+ cRequest = rb_define_class_under(mStreamly, "Request", rb_cObject);
323
+ rb_define_singleton_method(cRequest, "new", rb_streamly_new, -1);
324
+ rb_define_method(cRequest, "initialize", rb_streamly_init, -1);
325
+ rb_define_method(cRequest, "execute", rb_streamly_execute, -1);
326
+
327
+ eStreamlyError = rb_define_class_under(mStreamly, "Error", rb_eStandardError);
328
+ eUnsupportedProtocol = rb_define_class_under(mStreamly, "UnsupportedProtocol", rb_eStandardError);
329
+ eURLFormatError = rb_define_class_under(mStreamly, "URLFormatError", rb_eStandardError);
330
+ eHostResolutionError = rb_define_class_under(mStreamly, "HostResolutionError", rb_eStandardError);
331
+ eConnectionFailed = rb_define_class_under(mStreamly, "ConnectionFailed", rb_eStandardError);
332
+ ePartialFileError = rb_define_class_under(mStreamly, "PartialFileError", rb_eStandardError);
333
+ eTimeoutError = rb_define_class_under(mStreamly, "TimeoutError", rb_eStandardError);
334
+ eTooManyRedirects = rb_define_class_under(mStreamly, "TooManyRedirects", rb_eStandardError);
335
+
336
+ sym_method = ID2SYM(rb_intern("method"));
337
+ sym_url = ID2SYM(rb_intern("url"));
338
+ sym_payload = ID2SYM(rb_intern("payload"));
339
+ sym_headers = ID2SYM(rb_intern("headers"));
340
+ sym_head = ID2SYM(rb_intern("head"));
341
+ sym_get = ID2SYM(rb_intern("get"));
342
+ sym_post = ID2SYM(rb_intern("post"));
343
+ sym_put = ID2SYM(rb_intern("put"));
344
+ sym_delete = ID2SYM(rb_intern("delete"));
345
+ sym_username = ID2SYM(rb_intern("username"));
346
+ sym_password = ID2SYM(rb_intern("password"));
347
+ sym_response_header_handler = ID2SYM(rb_intern("response_header_handler"));
348
+ sym_response_body_handler = ID2SYM(rb_intern("response_body_handler"));
349
+ }
data/ext/streamly.h ADDED
@@ -0,0 +1,43 @@
1
+ #include <curl/curl.h>
2
+ #include <ruby.h>
3
+
4
+ VALUE mStreamly, cRequest, eStreamlyError, eUnsupportedProtocol, eURLFormatError, eHostResolutionError;
5
+ VALUE eConnectionFailed, ePartialFileError, eTimeoutError, eTooManyRedirects;
6
+ VALUE sym_method, sym_url, sym_payload, sym_headers, sym_head, sym_get, sym_post, sym_put, sym_delete;
7
+ VALUE sym_response_header_handler, sym_response_body_handler, sym_username, sym_password;
8
+
9
+ #define GetInstance(obj, sval) (sval = (struct curl_instance*)DATA_PTR(obj));
10
+
11
+ #ifdef HAVE_RBTRAP
12
+ #include <rubysig.h>
13
+ #else
14
+ void rb_enable_interrupt(void);
15
+ void rb_disable_interrupt(void);
16
+
17
+ #define TRAP_BEG rb_enable_interrupt();
18
+ #define TRAP_END do { rb_disable_interrupt(); rb_thread_check_ints(); } while(0);
19
+ #endif
20
+
21
+ struct curl_instance {
22
+ CURL* handle;
23
+ char error_buffer[CURL_ERROR_SIZE];
24
+ struct curl_slist* request_headers;
25
+ VALUE request_payload_handler;
26
+ VALUE response_header_handler;
27
+ VALUE response_body_handler;
28
+ VALUE request_method;
29
+ VALUE options;
30
+ };
31
+
32
+ // libcurl callbacks
33
+ static size_t header_handler(char * stream, size_t size, size_t nmemb, VALUE handler);
34
+ static size_t data_handler(char * stream, size_t size, size_t nmemb, VALUE handler);
35
+ // static size_t put_data_handler(char * stream, size_t size, size_t nmemb, VALUE upload_stream);
36
+ //
37
+ static VALUE select_error(CURLcode code);
38
+ static VALUE each_http_header(VALUE header, VALUE header_array);
39
+ void streamly_instance_mark(struct curl_instance * instance);
40
+ void streamly_instance_free(struct curl_instance * instance);
41
+
42
+ VALUE rb_streamly_new(int argc, VALUE * argv, VALUE klass);
43
+ VALUE rb_streamly_new(int argc, VALUE * argv, VALUE self);
data/lib/streamly.rb ADDED
@@ -0,0 +1,127 @@
1
+ # encoding: UTF-8
2
+ require 'streamly_ext'
3
+
4
+ module Streamly
5
+ VERSION = "0.1.1"
6
+
7
+ class Request
8
+ # A helper method to make your fire-and-forget requests easier
9
+ #
10
+ # Parameters:
11
+ # +args+ should be a Hash and is required
12
+ # This Hash should at least contain +:url+ and +:method+ keys.
13
+ # You may also provide the following optional keys:
14
+ # +:headers+ - should be a Hash of name/value pairs
15
+ # +:response_header_handler+ - can be a string or object that responds to #call
16
+ # If an object was passed, it's #call method will be called and passed the current chunk of data
17
+ # +:response_body_handler+ - can be a string or object that responds to #call
18
+ # If an object was passed, it's #call method will be called and passed the current chunk of data
19
+ # +:payload+ - If +:method+ is either +:post+ or +:put+ this will be used as the request body
20
+ #
21
+ def self.execute(args)
22
+ new(args).execute
23
+ end
24
+ end
25
+
26
+ # A helper method to make HEAD requests a dead-simple one-liner
27
+ #
28
+ # Example:
29
+ # Streamly.head("www.somehost.com/some_resource/1")
30
+ #
31
+ # Streamly.head("www.somehost.com/some_resource/1") do |header_chunk|
32
+ # # do something with _header_chunk_
33
+ # end
34
+ #
35
+ # Parameters:
36
+ # +url+ should be a String, the url to request
37
+ # +headers+ should be a Hash and is optional
38
+ #
39
+ # This method also accepts a block, which will stream the response headers in chunks to the caller
40
+ def self.head(url, headers=nil, &block)
41
+ opts = {:method => :head, :url => url, :headers => headers}
42
+ opts.merge!({:response_header_handler => block}) if block_given?
43
+ Request.execute(opts)
44
+ end
45
+
46
+ # A helper method to make HEAD requests a dead-simple one-liner
47
+ #
48
+ # Example:
49
+ # Streamly.get("www.somehost.com/some_resource/1")
50
+ #
51
+ # Streamly.get("www.somehost.com/some_resource/1") do |chunk|
52
+ # # do something with _chunk_
53
+ # end
54
+ #
55
+ # Parameters:
56
+ # +url+ should be a String, the url to request
57
+ # +headers+ should be a Hash and is optional
58
+ #
59
+ # This method also accepts a block, which will stream the response body in chunks to the caller
60
+ def self.get(url, headers=nil, &block)
61
+ opts = {:method => :get, :url => url, :headers => headers}
62
+ opts.merge!({:response_body_handler => block}) if block_given?
63
+ Request.execute(opts)
64
+ end
65
+
66
+ # A helper method to make HEAD requests a dead-simple one-liner
67
+ #
68
+ # Example:
69
+ # Streamly.post("www.somehost.com/some_resource", "asset[id]=2&asset[name]=bar")
70
+ #
71
+ # Streamly.post("www.somehost.com/some_resource", "asset[id]=2&asset[name]=bar") do |chunk|
72
+ # # do something with _chunk_
73
+ # end
74
+ #
75
+ # Parameters:
76
+ # +url+ should be a String (the url to request) and is required
77
+ # +payload+ should be a String and is required
78
+ # +headers+ should be a Hash and is optional
79
+ #
80
+ # This method also accepts a block, which will stream the response body in chunks to the caller
81
+ def self.post(url, payload, headers=nil, &block)
82
+ opts = {:method => :post, :url => url, :payload => payload, :headers => headers}
83
+ opts.merge!({:response_body_handler => block}) if block_given?
84
+ Request.execute(opts)
85
+ end
86
+
87
+ # A helper method to make HEAD requests a dead-simple one-liner
88
+ #
89
+ # Example:
90
+ # Streamly.put("www.somehost.com/some_resource/1", "asset[name]=foo")
91
+ #
92
+ # Streamly.put("www.somehost.com/some_resource/1", "asset[name]=foo") do |chunk|
93
+ # # do something with _chunk_
94
+ # end
95
+ #
96
+ # Parameters:
97
+ # +url+ should be a String (the url to request) and is required
98
+ # +payload+ should be a String and is required
99
+ # +headers+ should be a Hash and is optional
100
+ #
101
+ # This method also accepts a block, which will stream the response body in chunks to the caller
102
+ def self.put(url, payload, headers=nil, &block)
103
+ opts = {:method => :put, :url => url, :payload => payload, :headers => headers}
104
+ opts.merge!({:response_body_handler => block}) if block_given?
105
+ Request.execute(opts)
106
+ end
107
+
108
+ # A helper method to make HEAD requests a dead-simple one-liner
109
+ #
110
+ # Example:
111
+ # Streamly.delete("www.somehost.com/some_resource/1")
112
+ #
113
+ # Streamly.delete("www.somehost.com/some_resource/1") do |chunk|
114
+ # # do something with _chunk_
115
+ # end
116
+ #
117
+ # Parameters:
118
+ # +url+ should be a String, the url to request
119
+ # +headers+ should be a Hash and is optional
120
+ #
121
+ # This method also accepts a block, which will stream the response body in chunks to the caller
122
+ def self.delete(url, headers={}, &block)
123
+ opts = {:method => :delete, :url => url, :headers => headers}
124
+ opts.merge!({:response_body_handler => block}) if block_given?
125
+ Request.execute(opts)
126
+ end
127
+ end
data/spec/rcov.opts ADDED
@@ -0,0 +1,4 @@
1
+ --exclude spec,gem
2
+ --text-summary
3
+ --spec-only
4
+ --sort coverage --sort-reverse
@@ -0,0 +1,93 @@
1
+ # encoding: UTF-8
2
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
3
+
4
+ describe "Streamly's REST API" do
5
+
6
+ before(:all) do
7
+ @response = "Hello, brian".strip
8
+ end
9
+
10
+ context "HEAD" do
11
+ it "should perform a basic request" do
12
+ resp = Streamly.head('localhost:4567')
13
+ resp.should_not be_empty
14
+ end
15
+
16
+ it "should perform a basic request and stream the response to the caller" do
17
+ streamed_response = ''
18
+ resp = Streamly.head('localhost:4567') do |chunk|
19
+ chunk.should_not be_empty
20
+ streamed_response << chunk
21
+ end
22
+ resp.should be_nil
23
+ streamed_response.should_not be_nil
24
+ end
25
+ end
26
+
27
+ context "GET" do
28
+ it "should perform a basic request" do
29
+ resp = Streamly.get('localhost:4567/?name=brian')
30
+ resp.should eql(@response)
31
+ end
32
+
33
+ it "should perform a basic request and stream the response to the caller" do
34
+ streamed_response = ''
35
+ resp = Streamly.get('localhost:4567/?name=brian') do |chunk|
36
+ chunk.should_not be_empty
37
+ streamed_response << chunk
38
+ end
39
+ resp.should be_nil
40
+ streamed_response.should eql(@response)
41
+ end
42
+ end
43
+
44
+ context "POST" do
45
+ it "should perform a basic request" do
46
+ resp = Streamly.post('localhost:4567', 'name=brian')
47
+ resp.should eql(@response)
48
+ end
49
+
50
+ it "should perform a basic request and stream the response to the caller" do
51
+ streamed_response = ''
52
+ resp = Streamly.post('localhost:4567', 'name=brian') do |chunk|
53
+ chunk.should_not be_empty
54
+ streamed_response << chunk
55
+ end
56
+ resp.should be_nil
57
+ streamed_response.should eql(@response)
58
+ end
59
+ end
60
+
61
+ context "PUT" do
62
+ it "should perform a basic request" do
63
+ resp = Streamly.put('localhost:4567', 'name=brian')
64
+ resp.should eql(@response)
65
+ end
66
+
67
+ it "should perform a basic request and stream the response to the caller" do
68
+ streamed_response = ''
69
+ resp = Streamly.put('localhost:4567', 'name=brian') do |chunk|
70
+ chunk.should_not be_empty
71
+ streamed_response << chunk
72
+ end
73
+ resp.should be_nil
74
+ streamed_response.should eql(@response)
75
+ end
76
+ end
77
+
78
+ context "DELETE" do
79
+ it "should perform a basic request" do
80
+ resp = Streamly.delete('localhost:4567/?name=brian').should eql(@response)
81
+ end
82
+
83
+ it "should perform a basic request and stream the response to the caller" do
84
+ streamed_response = ''
85
+ resp = Streamly.delete('localhost:4567/?name=brian') do |chunk|
86
+ chunk.should_not be_empty
87
+ streamed_response << chunk
88
+ end
89
+ resp.should be_nil
90
+ streamed_response.should eql(@response)
91
+ end
92
+ end
93
+ end
data/spec/sinatra.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
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --format specdoc
2
+ --colour
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'rubygems'
5
+ require 'streamly'
data/streamly.gemspec ADDED
@@ -0,0 +1,76 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{streamly}
5
+ s.version = "0.1.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Brian Lopez"]
9
+ s.date = %q{2009-07-27}
10
+ s.email = %q{seniorlopez@gmail.com}
11
+ s.extensions = ["ext/extconf.rb"]
12
+ s.extra_rdoc_files = [
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".gitignore",
17
+ "CHANGELOG.md",
18
+ "MIT-LICENSE",
19
+ "README.rdoc",
20
+ "Rakefile",
21
+ "VERSION.yml",
22
+ "benchmark/basic_request.rb",
23
+ "benchmark/streaming_json_request.rb",
24
+ "examples/basic/delete.rb",
25
+ "examples/basic/get.rb",
26
+ "examples/basic/head.rb",
27
+ "examples/basic/post.rb",
28
+ "examples/basic/put.rb",
29
+ "examples/streaming/delete.rb",
30
+ "examples/streaming/get.rb",
31
+ "examples/streaming/head.rb",
32
+ "examples/streaming/post.rb",
33
+ "examples/streaming/put.rb",
34
+ "ext/extconf.rb",
35
+ "ext/streamly.c",
36
+ "ext/streamly.h",
37
+ "lib/streamly.rb",
38
+ "spec/rcov.opts",
39
+ "spec/requests/request_spec.rb",
40
+ "spec/sinatra.rb",
41
+ "spec/spec.opts",
42
+ "spec/spec_helper.rb",
43
+ "streamly.gemspec"
44
+ ]
45
+ s.homepage = %q{http://github.com/brianmario/streamly}
46
+ s.rdoc_options = ["--charset=UTF-8"]
47
+ s.require_paths = ["lib", "ext"]
48
+ s.rubyforge_project = %q{streamly}
49
+ s.rubygems_version = %q{1.3.5}
50
+ s.summary = %q{A streaming REST client for Ruby, in C.}
51
+ s.test_files = [
52
+ "spec/requests/request_spec.rb",
53
+ "spec/sinatra.rb",
54
+ "spec/spec_helper.rb",
55
+ "examples/basic/delete.rb",
56
+ "examples/basic/get.rb",
57
+ "examples/basic/head.rb",
58
+ "examples/basic/post.rb",
59
+ "examples/basic/put.rb",
60
+ "examples/streaming/delete.rb",
61
+ "examples/streaming/get.rb",
62
+ "examples/streaming/head.rb",
63
+ "examples/streaming/post.rb",
64
+ "examples/streaming/put.rb"
65
+ ]
66
+
67
+ if s.respond_to? :specification_version then
68
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
69
+ s.specification_version = 3
70
+
71
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
72
+ else
73
+ end
74
+ else
75
+ end
76
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brianmario-streamly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Brian Lopez
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-27 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: seniorlopez@gmail.com
18
+ executables: []
19
+
20
+ extensions:
21
+ - ext/extconf.rb
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - .gitignore
26
+ - CHANGELOG.md
27
+ - MIT-LICENSE
28
+ - README.rdoc
29
+ - Rakefile
30
+ - VERSION.yml
31
+ - benchmark/basic_request.rb
32
+ - benchmark/streaming_json_request.rb
33
+ - examples/basic/delete.rb
34
+ - examples/basic/get.rb
35
+ - examples/basic/head.rb
36
+ - examples/basic/post.rb
37
+ - examples/basic/put.rb
38
+ - examples/streaming/delete.rb
39
+ - examples/streaming/get.rb
40
+ - examples/streaming/head.rb
41
+ - examples/streaming/post.rb
42
+ - examples/streaming/put.rb
43
+ - ext/extconf.rb
44
+ - ext/streamly.c
45
+ - ext/streamly.h
46
+ - lib/streamly.rb
47
+ - spec/rcov.opts
48
+ - spec/requests/request_spec.rb
49
+ - spec/sinatra.rb
50
+ - spec/spec.opts
51
+ - spec/spec_helper.rb
52
+ - streamly.gemspec
53
+ has_rdoc: false
54
+ homepage: http://github.com/brianmario/streamly
55
+ licenses:
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --charset=UTF-8
59
+ require_paths:
60
+ - lib
61
+ - ext
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ requirements: []
75
+
76
+ rubyforge_project: streamly
77
+ rubygems_version: 1.3.5
78
+ signing_key:
79
+ specification_version: 3
80
+ summary: A streaming REST client for Ruby, in C.
81
+ test_files:
82
+ - spec/requests/request_spec.rb
83
+ - spec/sinatra.rb
84
+ - spec/spec_helper.rb
85
+ - examples/basic/delete.rb
86
+ - examples/basic/get.rb
87
+ - examples/basic/head.rb
88
+ - examples/basic/post.rb
89
+ - examples/basic/put.rb
90
+ - examples/streaming/delete.rb
91
+ - examples/streaming/get.rb
92
+ - examples/streaming/head.rb
93
+ - examples/streaming/post.rb
94
+ - examples/streaming/put.rb