streamly 0.1.3
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 +3 -0
- data/CHANGELOG.md +13 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +116 -0
- data/Rakefile +35 -0
- data/VERSION.yml +4 -0
- data/benchmark/basic_request.rb +33 -0
- data/benchmark/streaming_json_request.rb +42 -0
- data/examples/basic/delete.rb +9 -0
- data/examples/basic/get.rb +9 -0
- data/examples/basic/head.rb +9 -0
- data/examples/basic/post.rb +11 -0
- data/examples/basic/put.rb +11 -0
- data/examples/streaming/delete.rb +13 -0
- data/examples/streaming/get.rb +13 -0
- data/examples/streaming/head.rb +13 -0
- data/examples/streaming/post.rb +15 -0
- data/examples/streaming/put.rb +16 -0
- data/ext/extconf.rb +25 -0
- data/ext/streamly.c +349 -0
- data/ext/streamly.h +43 -0
- data/lib/streamly.rb +127 -0
- data/spec/rcov.opts +4 -0
- data/spec/requests/request_spec.rb +93 -0
- data/spec/sinatra.rb +18 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +5 -0
- data/streamly.gemspec +80 -0
- metadata +95 -0
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## 0.1.3 (August 19th, 2009)
|
4
|
+
* Fixed a bug where a username or password was specified, but not both
|
5
|
+
|
6
|
+
## 0.1.2 (July 28th, 2009)
|
7
|
+
* removing call to set CURLOPT_USERPWD to NULL as it would crash in linux
|
8
|
+
|
9
|
+
## 0.1.1 (July 27th, 2009)
|
10
|
+
* Version bump for Github's gem builder
|
11
|
+
|
12
|
+
## 0.1.0 (July 27th, 2009)
|
13
|
+
* 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,116 @@
|
|
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 HEAD request
|
18
|
+
|
19
|
+
Streamly.head('www.somehost.com')
|
20
|
+
|
21
|
+
Or streaming
|
22
|
+
|
23
|
+
Streamly.head('www.somehost.com') do |header_chunk|
|
24
|
+
# do something with header_chunk
|
25
|
+
end
|
26
|
+
|
27
|
+
You can also pass a Hash of headers
|
28
|
+
|
29
|
+
Streamly.head('www.somehost.com', {"User-Agent" => "Your Mom"})
|
30
|
+
|
31
|
+
=== A basic GET request
|
32
|
+
|
33
|
+
Streamly.get('www.somehost.com')
|
34
|
+
|
35
|
+
Or streaming
|
36
|
+
|
37
|
+
Streamly.get('www.somehost.com') do |body_chunk|
|
38
|
+
# do something with body_chunk
|
39
|
+
end
|
40
|
+
|
41
|
+
You can also pass a Hash of headers
|
42
|
+
|
43
|
+
Streamly.get('www.somehost.com', {"User-Agent" => "Your Mom"})
|
44
|
+
|
45
|
+
=== A basic POST request
|
46
|
+
|
47
|
+
Streamly.post('www.somehost.com', 'blah=foo')
|
48
|
+
|
49
|
+
Or streaming
|
50
|
+
|
51
|
+
Streamly.post('www.somehost.com', 'blah=foo') do |body_chunk|
|
52
|
+
# do something with body_chunk
|
53
|
+
end
|
54
|
+
|
55
|
+
You can also pass a Hash of headers
|
56
|
+
|
57
|
+
Streamly.post('www.somehost.com', 'blah=foo', {"User-Agent" => "Your Mom"})
|
58
|
+
|
59
|
+
=== A basic PUT request
|
60
|
+
|
61
|
+
Streamly.put('www.somehost.com', 'blah=foo')
|
62
|
+
|
63
|
+
Or streaming
|
64
|
+
|
65
|
+
Streamly.put('www.somehost.com', 'blah=foo') do |body_chunk|
|
66
|
+
# do something with body_chunk
|
67
|
+
end
|
68
|
+
|
69
|
+
You can also pass a Hash of headers
|
70
|
+
|
71
|
+
Streamly.put('www.somehost.com', 'blah=foo', {"User-Agent" => "Your Mom"})
|
72
|
+
|
73
|
+
=== A basic DELETE request
|
74
|
+
|
75
|
+
Streamly.delete('www.somehost.com')
|
76
|
+
|
77
|
+
Or streaming
|
78
|
+
|
79
|
+
Streamly.delete('www.somehost.com') do |body_chunk|
|
80
|
+
# do something with body_chunk
|
81
|
+
end
|
82
|
+
|
83
|
+
You can also pass a Hash of headers
|
84
|
+
|
85
|
+
Streamly.delete('www.somehost.com', {"User-Agent" => "Your Mom"})
|
86
|
+
|
87
|
+
== Benchmarks
|
88
|
+
|
89
|
+
Fetching 2,405,005 bytes of JSON from a local lighttpd server
|
90
|
+
|
91
|
+
* Streamly: 0.011s
|
92
|
+
* Shell out to curl: 0.046s
|
93
|
+
* rest-client: 0.205s
|
94
|
+
|
95
|
+
Streaming, and parsing 2,405,005 bytes of JSON from a local lighttpd server
|
96
|
+
|
97
|
+
* Streamly: 0.231s
|
98
|
+
* Shell out to curl: 0.341s
|
99
|
+
* rest-client: 0.447s
|
100
|
+
|
101
|
+
== Other Notes
|
102
|
+
|
103
|
+
This library was basically an exercise in dealing with libcurl in C.
|
104
|
+
|
105
|
+
== Special Thanks
|
106
|
+
|
107
|
+
There are quite a few *extremely* nice REST client libraries out there for Ruby today. I especially owe thanks
|
108
|
+
to the following projects. Without them I probably would have never had the inspiration to even take the time
|
109
|
+
to write this library. In Streamly, you'll find snippets of code, API patterns and examples from all 3 of these projects.
|
110
|
+
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!
|
111
|
+
|
112
|
+
* rest-client - https://github.com/adamwiggins/rest-client
|
113
|
+
* curb - https://github.com/taf2/curb
|
114
|
+
* patron - https://github.com/toland/patron
|
115
|
+
|
116
|
+
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,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 "Streamly"
|
14
|
+
x.report do
|
15
|
+
(ARGV[1] || 1).to_i.times do
|
16
|
+
Streamly.get(url)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
puts "Shell out to curl"
|
21
|
+
x.report do
|
22
|
+
(ARGV[1] || 1).to_i.times do
|
23
|
+
`curl -s --compressed #{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,42 @@
|
|
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 "Streamly"
|
15
|
+
parser = Yajl::Parser.new
|
16
|
+
parser.on_parse_complete = lambda {|obj| }
|
17
|
+
x.report do
|
18
|
+
(ARGV[1] || 1).to_i.times do
|
19
|
+
Streamly.get(url) do |chunk|
|
20
|
+
parser << chunk
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
puts "Shell out to curl"
|
26
|
+
parser = Yajl::Parser.new
|
27
|
+
parser.on_parse_complete = lambda {|obj| }
|
28
|
+
x.report do
|
29
|
+
(ARGV[1] || 1).to_i.times do
|
30
|
+
parser.parse `curl -s --compressed #{url}`
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
puts "rest-client"
|
35
|
+
parser = Yajl::Parser.new
|
36
|
+
parser.on_parse_complete = lambda {|obj| }
|
37
|
+
x.report do
|
38
|
+
(ARGV[1] || 1).to_i.times do
|
39
|
+
parser.parse RestClient.get(url, {"Accept-Encoding" => "identity, deflate, gzip"})
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -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 upload_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 header_str = rb_str_new2("");
|
66
|
+
|
67
|
+
rb_str_buf_cat(header_str, RSTRING_PTR(rb_ary_entry(header, 0)), RSTRING_LEN(rb_ary_entry(header, 0)));
|
68
|
+
rb_str_buf_cat(header_str, ": ", 2);
|
69
|
+
rb_str_buf_cat(header_str, RSTRING_PTR(rb_ary_entry(header, 1)), RSTRING_LEN(rb_ary_entry(header, 1)));
|
70
|
+
|
71
|
+
instance->request_headers = curl_slist_append(instance->request_headers, RSTRING_PTR(header_str));
|
72
|
+
rb_gc_mark(header_str);
|
73
|
+
return Qnil;
|
74
|
+
}
|
75
|
+
|
76
|
+
// Initially borrowed from Patron - http://github.com/toland/patron
|
77
|
+
// slightly modified for Streamly
|
78
|
+
static VALUE select_error(CURLcode code) {
|
79
|
+
VALUE error = Qnil;
|
80
|
+
|
81
|
+
switch (code) {
|
82
|
+
case CURLE_UNSUPPORTED_PROTOCOL:
|
83
|
+
error = eUnsupportedProtocol;
|
84
|
+
break;
|
85
|
+
case CURLE_URL_MALFORMAT:
|
86
|
+
error = eURLFormatError;
|
87
|
+
break;
|
88
|
+
case CURLE_COULDNT_RESOLVE_HOST:
|
89
|
+
error = eHostResolutionError;
|
90
|
+
break;
|
91
|
+
case CURLE_COULDNT_CONNECT:
|
92
|
+
error = eConnectionFailed;
|
93
|
+
break;
|
94
|
+
case CURLE_PARTIAL_FILE:
|
95
|
+
error = ePartialFileError;
|
96
|
+
break;
|
97
|
+
case CURLE_OPERATION_TIMEDOUT:
|
98
|
+
error = eTimeoutError;
|
99
|
+
break;
|
100
|
+
case CURLE_TOO_MANY_REDIRECTS:
|
101
|
+
error = eTooManyRedirects;
|
102
|
+
break;
|
103
|
+
default:
|
104
|
+
error = eStreamlyError;
|
105
|
+
}
|
106
|
+
|
107
|
+
return error;
|
108
|
+
}
|
109
|
+
|
110
|
+
/*
|
111
|
+
* Document-class: Streamly::Request
|
112
|
+
*
|
113
|
+
* A streaming REST client for Ruby that uses libcurl to do the heavy lifting.
|
114
|
+
* The API is almost exactly like rest-client, so users of that library should find it very familiar.
|
115
|
+
*/
|
116
|
+
/*
|
117
|
+
* Document-method: new
|
118
|
+
*
|
119
|
+
* call-seq: new(args)
|
120
|
+
*
|
121
|
+
* +args+ should be a Hash and is required
|
122
|
+
* This Hash should at least contain +:url+ and +:method+ keys.
|
123
|
+
* You may also provide the following optional keys:
|
124
|
+
* +:headers+ - should be a Hash of name/value pairs
|
125
|
+
* +:response_header_handler+ - can be a string or object that responds to #call
|
126
|
+
* If an object was passed, it's #call method will be called and passed the current chunk of data
|
127
|
+
* +:response_body_handler+ - can be a string or object that responds to #call
|
128
|
+
* If an object was passed, it's #call method will be called and passed the current chunk of data
|
129
|
+
* +:payload+ - If +:method+ is either +:post+ or +:put+ this will be used as the request body
|
130
|
+
*
|
131
|
+
*/
|
132
|
+
VALUE rb_streamly_new(int argc, VALUE * argv, VALUE klass) {
|
133
|
+
struct curl_instance * instance;
|
134
|
+
VALUE obj = Data_Make_Struct(klass, struct curl_instance, streamly_instance_mark, streamly_instance_free, instance);
|
135
|
+
rb_obj_call_init(obj, argc, argv);
|
136
|
+
return obj;
|
137
|
+
}
|
138
|
+
|
139
|
+
/*
|
140
|
+
* Document-method: initialize
|
141
|
+
*
|
142
|
+
* call-seq: initialize(args)
|
143
|
+
*
|
144
|
+
* +args+ should be a Hash and is required
|
145
|
+
* This Hash should at least contain +:url+ and +:method+ keys.
|
146
|
+
* You may also provide the following optional keys:
|
147
|
+
* +:headers+ - should be a Hash of name/value pairs
|
148
|
+
* +:response_header_handler+ - can be a string or object that responds to #call
|
149
|
+
* If an object was passed, it's #call method will be called and passed the current chunk of data
|
150
|
+
* +:response_body_handler+ - can be a string or object that responds to #call
|
151
|
+
* If an object was passed, it's #call method will be called and passed the current chunk of data
|
152
|
+
* +:payload+ - If +:method+ is either +:post+ or +:put+ this will be used as the request body
|
153
|
+
*
|
154
|
+
*/
|
155
|
+
VALUE rb_streamly_init(int argc, VALUE * argv, VALUE self) {
|
156
|
+
struct curl_instance * instance;
|
157
|
+
char * credential_sep = ":";
|
158
|
+
VALUE args, url, payload, headers, username, password, credentials;
|
159
|
+
|
160
|
+
GetInstance(self, instance);
|
161
|
+
instance->handle = curl_easy_init();
|
162
|
+
instance->request_headers = NULL;
|
163
|
+
instance->request_method = Qnil;
|
164
|
+
instance->request_payload_handler = Qnil;
|
165
|
+
instance->response_header_handler = Qnil;
|
166
|
+
instance->response_body_handler = Qnil;
|
167
|
+
instance->options = Qnil;
|
168
|
+
|
169
|
+
rb_scan_args(argc, argv, "10", &args);
|
170
|
+
|
171
|
+
// Ensure our args parameter is a hash
|
172
|
+
Check_Type(args, T_HASH);
|
173
|
+
|
174
|
+
instance->request_method = rb_hash_aref(args, sym_method);
|
175
|
+
url = rb_hash_aref(args, sym_url);
|
176
|
+
payload = rb_hash_aref(args, sym_payload);
|
177
|
+
headers = rb_hash_aref(args, sym_headers);
|
178
|
+
username = rb_hash_aref(args, sym_username);
|
179
|
+
password = rb_hash_aref(args, sym_password);
|
180
|
+
instance->response_header_handler = rb_hash_aref(args, sym_response_header_handler);
|
181
|
+
instance->response_body_handler = rb_hash_aref(args, sym_response_body_handler);
|
182
|
+
|
183
|
+
// First lets verify we have a :method key
|
184
|
+
if (NIL_P(instance->request_method)) {
|
185
|
+
rb_raise(eStreamlyError, "You must specify a :method");
|
186
|
+
} else {
|
187
|
+
// OK, a :method was specified, but if it's POST or PUT we require a :payload
|
188
|
+
if (instance->request_method == sym_post || instance->request_method == sym_put) {
|
189
|
+
if (NIL_P(payload)) {
|
190
|
+
rb_raise(eStreamlyError, "You must specify a :payload for POST and PUT requests");
|
191
|
+
}
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
// Now verify a :url was provided
|
196
|
+
if (NIL_P(url)) {
|
197
|
+
rb_raise(eStreamlyError, "You must specify a :url to request");
|
198
|
+
}
|
199
|
+
|
200
|
+
if (NIL_P(instance->response_header_handler)) {
|
201
|
+
instance->response_header_handler = rb_str_new2("");
|
202
|
+
}
|
203
|
+
if (instance->request_method != sym_head && NIL_P(instance->response_body_handler)) {
|
204
|
+
instance->response_body_handler = rb_str_new2("");
|
205
|
+
}
|
206
|
+
|
207
|
+
if (!NIL_P(headers)) {
|
208
|
+
Check_Type(headers, T_HASH);
|
209
|
+
rb_iterate(rb_each, headers, each_http_header, self);
|
210
|
+
curl_easy_setopt(instance->handle, CURLOPT_HTTPHEADER, instance->request_headers);
|
211
|
+
}
|
212
|
+
|
213
|
+
// So far so good, lets start setting up our request
|
214
|
+
|
215
|
+
// Set the type of request
|
216
|
+
if (instance->request_method == sym_head) {
|
217
|
+
curl_easy_setopt(instance->handle, CURLOPT_NOBODY, 1);
|
218
|
+
} else if (instance->request_method == sym_get) {
|
219
|
+
curl_easy_setopt(instance->handle, CURLOPT_HTTPGET, 1);
|
220
|
+
} else if (instance->request_method == sym_post) {
|
221
|
+
curl_easy_setopt(instance->handle, CURLOPT_POST, 1);
|
222
|
+
curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDS, RSTRING_PTR(payload));
|
223
|
+
curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDSIZE, RSTRING_LEN(payload));
|
224
|
+
|
225
|
+
// (multipart)
|
226
|
+
// curl_easy_setopt(instance->handle, CURLOPT_HTTPPOST, 1);
|
227
|
+
|
228
|
+
// TODO: get streaming upload working
|
229
|
+
// curl_easy_setopt(instance->handle, CURLOPT_READFUNCTION, &upload_data_handler);
|
230
|
+
// curl_easy_setopt(instance->handle, CURLOPT_READDATA, &instance->upload_stream);
|
231
|
+
// curl_easy_setopt(instance->handle, CURLOPT_INFILESIZE, len);
|
232
|
+
} else if (instance->request_method == sym_put) {
|
233
|
+
curl_easy_setopt(instance->handle, CURLOPT_CUSTOMREQUEST, "PUT");
|
234
|
+
curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDS, RSTRING_PTR(payload));
|
235
|
+
curl_easy_setopt(instance->handle, CURLOPT_POSTFIELDSIZE, RSTRING_LEN(payload));
|
236
|
+
|
237
|
+
// TODO: get streaming upload working
|
238
|
+
// curl_easy_setopt(instance->handle, CURLOPT_UPLOAD, 1);
|
239
|
+
// curl_easy_setopt(instance->handle, CURLOPT_READFUNCTION, &upload_data_handler);
|
240
|
+
// curl_easy_setopt(instance->handle, CURLOPT_READDATA, &instance->upload_stream);
|
241
|
+
// curl_easy_setopt(instance->handle, CURLOPT_INFILESIZE, len);
|
242
|
+
} else if (instance->request_method == sym_delete) {
|
243
|
+
curl_easy_setopt(instance->handle, CURLOPT_CUSTOMREQUEST, "DELETE");
|
244
|
+
}
|
245
|
+
|
246
|
+
// Other common options
|
247
|
+
curl_easy_setopt(instance->handle, CURLOPT_URL, RSTRING_PTR(url));
|
248
|
+
curl_easy_setopt(instance->handle, CURLOPT_FOLLOWLOCATION, 1);
|
249
|
+
curl_easy_setopt(instance->handle, CURLOPT_MAXREDIRS, 3);
|
250
|
+
|
251
|
+
// Response header handling
|
252
|
+
curl_easy_setopt(instance->handle, CURLOPT_HEADERFUNCTION, &header_handler);
|
253
|
+
curl_easy_setopt(instance->handle, CURLOPT_HEADERDATA, instance->response_header_handler);
|
254
|
+
|
255
|
+
// Response body handling
|
256
|
+
if (instance->request_method != sym_head) {
|
257
|
+
curl_easy_setopt(instance->handle, CURLOPT_ENCODING, "identity, deflate, gzip");
|
258
|
+
curl_easy_setopt(instance->handle, CURLOPT_WRITEFUNCTION, &data_handler);
|
259
|
+
curl_easy_setopt(instance->handle, CURLOPT_WRITEDATA, instance->response_body_handler);
|
260
|
+
}
|
261
|
+
|
262
|
+
curl_easy_setopt(instance, CURLOPT_USERPWD, NULL);
|
263
|
+
if (!NIL_P(username) || !NIL_P(password)) {
|
264
|
+
credentials = rb_str_new2("");
|
265
|
+
if (!NIL_P(username)) {
|
266
|
+
rb_str_buf_cat(credentials, RSTRING_PTR(username), RSTRING_LEN(username));
|
267
|
+
}
|
268
|
+
rb_str_buf_cat(credentials, credential_sep, 1);
|
269
|
+
if (!NIL_P(password)) {
|
270
|
+
rb_str_buf_cat(credentials, RSTRING_PTR(password), RSTRING_LEN(password));
|
271
|
+
}
|
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.3"
|
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,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
data/spec/spec_helper.rb
ADDED
data/streamly.gemspec
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{streamly}
|
8
|
+
s.version = "0.1.3"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Brian Lopez"]
|
12
|
+
s.date = %q{2010-02-11}
|
13
|
+
s.email = %q{seniorlopez@gmail.com}
|
14
|
+
s.extensions = ["ext/extconf.rb"]
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"CHANGELOG.md",
|
21
|
+
"MIT-LICENSE",
|
22
|
+
"README.rdoc",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION.yml",
|
25
|
+
"benchmark/basic_request.rb",
|
26
|
+
"benchmark/streaming_json_request.rb",
|
27
|
+
"examples/basic/delete.rb",
|
28
|
+
"examples/basic/get.rb",
|
29
|
+
"examples/basic/head.rb",
|
30
|
+
"examples/basic/post.rb",
|
31
|
+
"examples/basic/put.rb",
|
32
|
+
"examples/streaming/delete.rb",
|
33
|
+
"examples/streaming/get.rb",
|
34
|
+
"examples/streaming/head.rb",
|
35
|
+
"examples/streaming/post.rb",
|
36
|
+
"examples/streaming/put.rb",
|
37
|
+
"ext/extconf.rb",
|
38
|
+
"ext/streamly.c",
|
39
|
+
"ext/streamly.h",
|
40
|
+
"lib/streamly.rb",
|
41
|
+
"spec/rcov.opts",
|
42
|
+
"spec/requests/request_spec.rb",
|
43
|
+
"spec/sinatra.rb",
|
44
|
+
"spec/spec.opts",
|
45
|
+
"spec/spec_helper.rb",
|
46
|
+
"streamly.gemspec"
|
47
|
+
]
|
48
|
+
s.homepage = %q{http://github.com/brianmario/streamly}
|
49
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
50
|
+
s.require_paths = ["lib", "ext"]
|
51
|
+
s.rubyforge_project = %q{streamly}
|
52
|
+
s.rubygems_version = %q{1.3.5}
|
53
|
+
s.summary = %q{A streaming REST client for Ruby, in C.}
|
54
|
+
s.test_files = [
|
55
|
+
"spec/requests/request_spec.rb",
|
56
|
+
"spec/sinatra.rb",
|
57
|
+
"spec/spec_helper.rb",
|
58
|
+
"examples/basic/delete.rb",
|
59
|
+
"examples/basic/get.rb",
|
60
|
+
"examples/basic/head.rb",
|
61
|
+
"examples/basic/post.rb",
|
62
|
+
"examples/basic/put.rb",
|
63
|
+
"examples/streaming/delete.rb",
|
64
|
+
"examples/streaming/get.rb",
|
65
|
+
"examples/streaming/head.rb",
|
66
|
+
"examples/streaming/post.rb",
|
67
|
+
"examples/streaming/put.rb"
|
68
|
+
]
|
69
|
+
|
70
|
+
if s.respond_to? :specification_version then
|
71
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
72
|
+
s.specification_version = 3
|
73
|
+
|
74
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
75
|
+
else
|
76
|
+
end
|
77
|
+
else
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: streamly
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Lopez
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-02-11 00:00:00 -08: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: true
|
54
|
+
homepage: http://github.com/brianmario/streamly
|
55
|
+
licenses: []
|
56
|
+
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options:
|
59
|
+
- --charset=UTF-8
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
- ext
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
version:
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: "0"
|
74
|
+
version:
|
75
|
+
requirements: []
|
76
|
+
|
77
|
+
rubyforge_project: streamly
|
78
|
+
rubygems_version: 1.3.5
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: A streaming REST client for Ruby, in C.
|
82
|
+
test_files:
|
83
|
+
- spec/requests/request_spec.rb
|
84
|
+
- spec/sinatra.rb
|
85
|
+
- spec/spec_helper.rb
|
86
|
+
- examples/basic/delete.rb
|
87
|
+
- examples/basic/get.rb
|
88
|
+
- examples/basic/head.rb
|
89
|
+
- examples/basic/post.rb
|
90
|
+
- examples/basic/put.rb
|
91
|
+
- examples/streaming/delete.rb
|
92
|
+
- examples/streaming/get.rb
|
93
|
+
- examples/streaming/head.rb
|
94
|
+
- examples/streaming/post.rb
|
95
|
+
- examples/streaming/put.rb
|