ghazel-curb 0.7.9.1 → 0.7.15.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/README +16 -3
- data/ext/curb.c +2 -0
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +25 -13
- data/ext/curb_multi.c +16 -24
- data/lib/curb.rb +39 -6
- data/lib/curl.rb +0 -1
- data/tests/tc_curl_easy.rb +36 -2
- metadata +5 -5
data/README
CHANGED
@@ -45,14 +45,19 @@ documentation with:
|
|
45
45
|
|
46
46
|
$ rake doc
|
47
47
|
|
48
|
-
##
|
48
|
+
## Usage & examples
|
49
|
+
|
50
|
+
Curb provides two classes:
|
51
|
+
|
52
|
+
+ Curl::Easy - simple API, for day-to-day tasks.
|
53
|
+
+ Curl::Multi - more advanced API, for operating on multiple URLs simultaneously.
|
49
54
|
|
50
55
|
### Simple fetch via HTTP:
|
51
56
|
|
52
57
|
c = Curl::Easy.perform("http://www.google.co.uk")
|
53
58
|
puts c.body_str
|
54
59
|
|
55
|
-
|
60
|
+
Same thing, more manual:
|
56
61
|
|
57
62
|
c = Curl::Easy.new("http://www.google.co.uk")
|
58
63
|
c.perform
|
@@ -65,7 +70,7 @@ documentation with:
|
|
65
70
|
curl.verbose = true
|
66
71
|
end
|
67
72
|
|
68
|
-
|
73
|
+
Same thing, more manual:
|
69
74
|
|
70
75
|
c = Curl::Easy.new("http://www.google.co.uk") do |curl|
|
71
76
|
curl.headers["User-Agent"] = "myapp-0.0"
|
@@ -74,6 +79,14 @@ documentation with:
|
|
74
79
|
|
75
80
|
c.perform
|
76
81
|
|
82
|
+
### HTTP basic authentication:
|
83
|
+
|
84
|
+
c = Curl::Easy.new("http://github.com/")
|
85
|
+
c.http_auth_types = :basic
|
86
|
+
c.username = 'foo'
|
87
|
+
c.password = 'bar'
|
88
|
+
c.perform
|
89
|
+
|
77
90
|
### Supplying custom handlers:
|
78
91
|
|
79
92
|
c = Curl::Easy.new("http://www.google.co.uk")
|
data/ext/curb.c
CHANGED
data/ext/curb.h
CHANGED
@@ -20,12 +20,12 @@
|
|
20
20
|
#include "curb_macros.h"
|
21
21
|
|
22
22
|
// These should be managed from the Rake 'release' task.
|
23
|
-
#define CURB_VERSION "0.7.
|
24
|
-
#define CURB_VER_NUM
|
23
|
+
#define CURB_VERSION "0.7.15"
|
24
|
+
#define CURB_VER_NUM 715
|
25
25
|
#define CURB_VER_MAJ 0
|
26
26
|
#define CURB_VER_MIN 7
|
27
|
-
#define CURB_VER_MIC
|
28
|
-
#define CURB_VER_PATCH
|
27
|
+
#define CURB_VER_MIC 1
|
28
|
+
#define CURB_VER_PATCH 5
|
29
29
|
|
30
30
|
|
31
31
|
// Maybe not yet defined in Ruby
|
data/ext/curb_easy.c
CHANGED
@@ -815,7 +815,7 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
|
|
815
815
|
the easy handle is active or until the upload
|
816
816
|
is complete or terminated... */
|
817
817
|
|
818
|
-
curl_easy_setopt(curl, CURLOPT_NOBODY,0);
|
818
|
+
curl_easy_setopt(curl, CURLOPT_NOBODY, 0);
|
819
819
|
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
|
820
820
|
curl_easy_setopt(curl, CURLOPT_READFUNCTION, (curl_read_callback)read_data_handler);
|
821
821
|
curl_easy_setopt(curl, CURLOPT_READDATA, rbce);
|
@@ -840,7 +840,7 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
|
|
840
840
|
|
841
841
|
if (rb_respond_to(data, rb_intern("read"))) {
|
842
842
|
VALUE stat = rb_funcall(data, rb_intern("stat"), 0);
|
843
|
-
if( stat ) {
|
843
|
+
if( stat && rb_hash_aref(headers, rb_str_new2("Content-Length")) == Qnil) {
|
844
844
|
VALUE size;
|
845
845
|
if( rb_hash_aref(headers, rb_str_new2("Expect")) == Qnil ) {
|
846
846
|
rb_hash_aset(headers, rb_str_new2("Expect"), rb_str_new2(""));
|
@@ -848,9 +848,13 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
|
|
848
848
|
size = rb_funcall(stat, rb_intern("size"), 0);
|
849
849
|
curl_easy_setopt(curl, CURLOPT_INFILESIZE, FIX2LONG(size));
|
850
850
|
}
|
851
|
-
else if( rb_hash_aref(headers, rb_str_new2("Transfer-Encoding")) == Qnil ) {
|
851
|
+
else if( rb_hash_aref(headers, rb_str_new2("Content-Length")) == Qnil && rb_hash_aref(headers, rb_str_new2("Transfer-Encoding")) == Qnil ) {
|
852
852
|
rb_hash_aset(headers, rb_str_new2("Transfer-Encoding"), rb_str_new2("chunked"));
|
853
853
|
}
|
854
|
+
else if( rb_hash_aref(headers, rb_str_new2("Content-Length")) ) {
|
855
|
+
VALUE size = rb_funcall(rb_hash_aref(headers, rb_str_new2("Content-Length")), rb_intern("to_i"), 0);
|
856
|
+
curl_easy_setopt(curl, CURLOPT_INFILESIZE, FIX2LONG(size));
|
857
|
+
}
|
854
858
|
}
|
855
859
|
else if (rb_respond_to(data, rb_intern("to_s"))) {
|
856
860
|
curl_easy_setopt(curl, CURLOPT_INFILESIZE, RSTRING_LEN(data));
|
@@ -1812,7 +1816,10 @@ static VALUE ruby_curl_easy_on_debug_set(int argc, VALUE *argv, VALUE self) {
|
|
1812
1816
|
/***********************************************
|
1813
1817
|
* This is an rb_iterate callback used to set up http headers.
|
1814
1818
|
*/
|
1815
|
-
static VALUE cb_each_http_header(VALUE header,
|
1819
|
+
static VALUE cb_each_http_header(VALUE header, VALUE wrap) {
|
1820
|
+
struct curl_slist **list;
|
1821
|
+
Data_Get_Struct(wrap, struct curl_slist *, list);
|
1822
|
+
|
1816
1823
|
VALUE header_str = Qnil;
|
1817
1824
|
|
1818
1825
|
//rb_p(header);
|
@@ -1841,7 +1848,10 @@ static VALUE cb_each_http_header(VALUE header, struct curl_slist **list) {
|
|
1841
1848
|
/***********************************************
|
1842
1849
|
* This is an rb_iterate callback used to set up ftp commands.
|
1843
1850
|
*/
|
1844
|
-
static VALUE cb_each_ftp_command(VALUE ftp_command,
|
1851
|
+
static VALUE cb_each_ftp_command(VALUE ftp_command, VALUE wrap) {
|
1852
|
+
struct curl_slist **list;
|
1853
|
+
Data_Get_Struct(wrap, struct curl_slist *, list);
|
1854
|
+
|
1845
1855
|
VALUE ftp_command_string = rb_obj_as_string(ftp_command);
|
1846
1856
|
*list = curl_slist_append(*list, StringValuePtr(ftp_command));
|
1847
1857
|
|
@@ -1903,16 +1913,16 @@ VALUE ruby_curl_easy_setup( ruby_curl_easy *rbce ) {
|
|
1903
1913
|
curl_easy_setopt(curl, CURLOPT_USERPWD, NULL);
|
1904
1914
|
}
|
1905
1915
|
|
1906
|
-
if (
|
1907
|
-
curl_easy_setopt(curl, CURLOPT_PROXY, rb_easy_get_str("proxy_url"));
|
1908
|
-
} else {
|
1916
|
+
if (rb_easy_nil("proxy_url")) {
|
1909
1917
|
curl_easy_setopt(curl, CURLOPT_PROXY, NULL);
|
1918
|
+
} else {
|
1919
|
+
curl_easy_setopt(curl, CURLOPT_PROXY, rb_easy_get_str("proxy_url"));
|
1910
1920
|
}
|
1911
1921
|
|
1912
|
-
if (
|
1913
|
-
curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, rb_easy_get_str("proxy_pwd"));
|
1914
|
-
} else {
|
1922
|
+
if (rb_easy_nil("proxypwd")) {
|
1915
1923
|
curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, NULL);
|
1924
|
+
} else {
|
1925
|
+
curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, rb_easy_get_str("proxypwd"));
|
1916
1926
|
}
|
1917
1927
|
|
1918
1928
|
// body/header procs
|
@@ -2118,7 +2128,8 @@ VALUE ruby_curl_easy_setup( ruby_curl_easy *rbce ) {
|
|
2118
2128
|
|
2119
2129
|
if (!rb_easy_nil("headers")) {
|
2120
2130
|
if (rb_easy_type_check("headers", T_ARRAY) || rb_easy_type_check("headers", T_HASH)) {
|
2121
|
-
|
2131
|
+
VALUE wrap = Data_Wrap_Struct(rb_cObject, 0, 0, hdrs);
|
2132
|
+
rb_iterate(rb_each, rb_easy_get("headers"), cb_each_http_header, wrap);
|
2122
2133
|
} else {
|
2123
2134
|
VALUE headers_str = rb_obj_as_string(rb_easy_get("headers"));
|
2124
2135
|
*hdrs = curl_slist_append(*hdrs, StringValuePtr(headers_str));
|
@@ -2132,7 +2143,8 @@ VALUE ruby_curl_easy_setup( ruby_curl_easy *rbce ) {
|
|
2132
2143
|
/* Setup FTP commands if necessary */
|
2133
2144
|
if (!rb_easy_nil("ftp_commands")) {
|
2134
2145
|
if (rb_easy_type_check("ftp_commands", T_ARRAY)) {
|
2135
|
-
|
2146
|
+
VALUE wrap = Data_Wrap_Struct(rb_cObject, 0, 0, cmds);
|
2147
|
+
rb_iterate(rb_each, rb_easy_get("ftp_commands"), cb_each_ftp_command, wrap);
|
2136
2148
|
}
|
2137
2149
|
|
2138
2150
|
if (*cmds) {
|
data/ext/curb_multi.c
CHANGED
@@ -39,13 +39,9 @@ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy);
|
|
39
39
|
static void rb_curl_multi_read_info(VALUE self, CURLM *mptr);
|
40
40
|
static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_running);
|
41
41
|
|
42
|
-
static void rb_curl_multi_mark_all_easy(VALUE key, VALUE rbeasy, ruby_curl_multi *rbcm) {
|
43
|
-
rb_gc_mark(rbeasy);
|
44
|
-
}
|
45
|
-
|
46
42
|
static void curl_multi_mark(ruby_curl_multi *rbcm) {
|
47
43
|
rb_gc_mark(rbcm->requests);
|
48
|
-
|
44
|
+
rb_gc_mark(rbcm->requests);
|
49
45
|
}
|
50
46
|
|
51
47
|
static void curl_multi_flush_easy(VALUE key, VALUE easy, ruby_curl_multi *rbcm) {
|
@@ -482,29 +478,23 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
|
|
482
478
|
|
483
479
|
if (timeout_milliseconds == 0) { /* no delay */
|
484
480
|
rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
|
481
|
+
rb_curl_multi_read_info( self, rbcm->handle );
|
482
|
+
if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
|
485
483
|
continue;
|
486
484
|
}
|
487
|
-
else if (timeout_milliseconds < 0) {
|
488
|
-
timeout_milliseconds = cCurlMutiDefaulttimeout; /* libcurl doesn't know how long to wait, use a default timeout */
|
489
|
-
}
|
490
485
|
|
491
|
-
if (timeout_milliseconds > cCurlMutiDefaulttimeout) {
|
492
|
-
timeout_milliseconds = cCurlMutiDefaulttimeout; /*
|
486
|
+
if (timeout_milliseconds < 0 || timeout_milliseconds > cCurlMutiDefaulttimeout) {
|
487
|
+
timeout_milliseconds = cCurlMutiDefaulttimeout; /* libcurl doesn't know how long to wait, use a default timeout */
|
488
|
+
/* or buggy versions libcurl sometimes reports huge timeouts... let's cap it */
|
493
489
|
}
|
494
490
|
|
495
491
|
tv.tv_sec = 0; /* never wait longer than 1 second */
|
496
492
|
tv.tv_usec = (int)(timeout_milliseconds * 1000); /* XXX: int is the right type for OSX, what about linux? */
|
497
493
|
|
498
|
-
if (timeout_milliseconds == 0) { /* no delay */
|
499
|
-
rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
|
500
|
-
continue;
|
501
|
-
}
|
502
|
-
|
503
|
-
if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
|
504
|
-
|
505
494
|
FD_ZERO(&fdread);
|
506
495
|
FD_ZERO(&fdwrite);
|
507
496
|
FD_ZERO(&fdexcep);
|
497
|
+
|
508
498
|
/* load the fd sets from the multi handle */
|
509
499
|
mcode = curl_multi_fdset(rbcm->handle, &fdread, &fdwrite, &fdexcep, &maxfd);
|
510
500
|
if (mcode != CURLM_OK) {
|
@@ -513,7 +503,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
|
|
513
503
|
|
514
504
|
#ifdef _WIN32
|
515
505
|
create_crt_fd(&fdread, &crt_fdread);
|
516
|
-
create_crt_fd(&fdwrite, &
|
506
|
+
create_crt_fd(&fdwrite, &crt_fdwrite);
|
517
507
|
create_crt_fd(&fdexcep, &crt_fdexcep);
|
518
508
|
#endif
|
519
509
|
|
@@ -521,7 +511,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
|
|
521
511
|
|
522
512
|
#ifdef _WIN32
|
523
513
|
cleanup_crt_fd(&fdread, &crt_fdread);
|
524
|
-
cleanup_crt_fd(&fdwrite, &
|
514
|
+
cleanup_crt_fd(&fdwrite, &crt_fdwrite);
|
525
515
|
cleanup_crt_fd(&fdexcep, &crt_fdexcep);
|
526
516
|
#endif
|
527
517
|
|
@@ -529,17 +519,19 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
|
|
529
519
|
case -1:
|
530
520
|
rb_raise(rb_eRuntimeError, "select(): %s", strerror(errno));
|
531
521
|
break;
|
532
|
-
case 0:
|
522
|
+
case 0: /* timeout */
|
523
|
+
default: /* action */
|
524
|
+
rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
|
533
525
|
rb_curl_multi_read_info( self, rbcm->handle );
|
534
526
|
if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
|
535
|
-
default:
|
536
|
-
rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
|
537
527
|
break;
|
538
528
|
}
|
539
529
|
}
|
540
|
-
|
541
|
-
if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
|
530
|
+
|
542
531
|
} while( rbcm->running );
|
532
|
+
|
533
|
+
rb_curl_multi_read_info( self, rbcm->handle );
|
534
|
+
if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
|
543
535
|
|
544
536
|
return Qtrue;
|
545
537
|
}
|
data/lib/curb.rb
CHANGED
@@ -77,7 +77,7 @@ module Curl
|
|
77
77
|
urls.each do|url|
|
78
78
|
url_confs << {:url => url, :method => :get}.merge(easy_options)
|
79
79
|
end
|
80
|
-
self.http(url_confs, multi_options) {|c,code,method| blk.call(c) }
|
80
|
+
self.http(url_confs, multi_options) {|c,code,method| blk.call(c) if blk }
|
81
81
|
end
|
82
82
|
|
83
83
|
# call-seq:
|
@@ -148,17 +148,25 @@ module Curl
|
|
148
148
|
#
|
149
149
|
def http(urls_with_config, multi_options={}, &blk)
|
150
150
|
m = Curl::Multi.new
|
151
|
+
|
152
|
+
# maintain a sane number of easy handles
|
153
|
+
multi_options[:max_connects] = max_connects = multi_options.key?(:max_connects) ? multi_options[:max_connects] : 10
|
154
|
+
|
155
|
+
free_handles = [] # keep a list of free easy handles
|
156
|
+
|
151
157
|
# configure the multi handle
|
152
158
|
multi_options.each { |k,v| m.send("#{k}=", v) }
|
153
159
|
callbacks = [:on_progress,:on_debug,:on_failure,:on_success,:on_body,:on_header]
|
154
160
|
|
155
|
-
|
156
|
-
c
|
161
|
+
add_free_handle = proc do|conf, easy|
|
162
|
+
c = conf.dup # avoid being destructive to input
|
157
163
|
url = c.delete(:url)
|
158
164
|
method = c.delete(:method)
|
159
165
|
headers = c.delete(:headers)
|
160
166
|
|
161
|
-
easy = Curl::Easy.new
|
167
|
+
easy = Curl::Easy.new if easy.nil?
|
168
|
+
|
169
|
+
easy.url = url
|
162
170
|
|
163
171
|
# assign callbacks
|
164
172
|
callbacks.each do |cb|
|
@@ -191,10 +199,35 @@ module Curl
|
|
191
199
|
#
|
192
200
|
c.each { |k,v| easy.send("#{k}=",v) }
|
193
201
|
|
194
|
-
easy.on_complete {|curl,code|
|
202
|
+
easy.on_complete {|curl,code|
|
203
|
+
free_handles << curl
|
204
|
+
blk.call(curl,code,method) if blk
|
205
|
+
}
|
195
206
|
m.add(easy)
|
196
207
|
end
|
197
|
-
|
208
|
+
|
209
|
+
max_connects.times do
|
210
|
+
conf = urls_with_config.pop
|
211
|
+
add_free_handle.call conf, nil
|
212
|
+
break if urls_with_config.empty?
|
213
|
+
end
|
214
|
+
|
215
|
+
consume_free_handles = proc do
|
216
|
+
# as we idle consume free handles
|
217
|
+
if urls_with_config.size > 0 && free_handles.size > 0
|
218
|
+
easy = free_handles.pop
|
219
|
+
conf = urls_with_config.pop
|
220
|
+
add_free_handle.call conf, easy
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
until urls_with_config.empty?
|
225
|
+
m.perform do
|
226
|
+
consume_free_handles.call
|
227
|
+
end
|
228
|
+
consume_free_handles.call
|
229
|
+
end
|
230
|
+
free_handles = nil
|
198
231
|
end
|
199
232
|
|
200
233
|
# call-seq:
|
data/lib/curl.rb
CHANGED
data/tests/tc_curl_easy.rb
CHANGED
@@ -224,7 +224,7 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
224
224
|
|
225
225
|
assert_equal $TEST_URL, c.url
|
226
226
|
assert_equal "http://some.proxy", c.proxy_url
|
227
|
-
|
227
|
+
|
228
228
|
c.proxy_url = nil
|
229
229
|
assert_equal $TEST_URL, c.url
|
230
230
|
assert_nil c.proxy_url
|
@@ -566,7 +566,7 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
566
566
|
def test_post_remote
|
567
567
|
curl = Curl::Easy.new(TestServlet.url)
|
568
568
|
curl.http_post([Curl::PostField.content('document_id', 5)])
|
569
|
-
assert_equal "POST\
|
569
|
+
assert_equal "POST\ndocument_id=5", curl.unescape(curl.body_str)
|
570
570
|
end
|
571
571
|
|
572
572
|
def test_post_remote_is_easy_handle
|
@@ -853,6 +853,40 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
853
853
|
assert_equal 'GET', curl.body_str
|
854
854
|
end
|
855
855
|
|
856
|
+
def test_easy_can_put_with_content_length
|
857
|
+
curl = Curl::Easy.new(TestServlet.url)
|
858
|
+
rd, wr = IO.pipe
|
859
|
+
buf = (("hello")* (1000 / 5))
|
860
|
+
|
861
|
+
producer = Thread.new do
|
862
|
+
5.times do
|
863
|
+
wr << buf
|
864
|
+
sleep 0.1 # act as a slow producer
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|
868
|
+
consumer = Thread.new do
|
869
|
+
|
870
|
+
#curl.verbose = true
|
871
|
+
curl.headers['Content-Length'] = buf.size * 5
|
872
|
+
curl.headers['User-Agent'] = "Something else"
|
873
|
+
curl.headers['Content-Type'] = "text/javascript"
|
874
|
+
curl.headers['Date'] = Time.now.httpdate
|
875
|
+
curl.headers['Host'] = 's3.amazonaws.com'
|
876
|
+
curl.headers['Accept'] = '*/*'
|
877
|
+
curl.headers['Authorization'] = 'Foo Bar Biz Baz'
|
878
|
+
curl.http_put(rd)
|
879
|
+
assert_match /^PUT/, curl.body_str
|
880
|
+
assert_match /hello$/, curl.body_str
|
881
|
+
curl.header_str
|
882
|
+
curl.body_str
|
883
|
+
end
|
884
|
+
|
885
|
+
producer.join
|
886
|
+
wr.close
|
887
|
+
consumer.join
|
888
|
+
|
889
|
+
end
|
856
890
|
|
857
891
|
include TestServerMethods
|
858
892
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ghazel-curb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 73
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 7
|
9
|
-
-
|
9
|
+
- 15
|
10
10
|
- 1
|
11
|
-
version: 0.7.
|
11
|
+
version: 0.7.15.1
|
12
12
|
platform: ruby
|
13
13
|
authors:
|
14
14
|
- Ross Bamford
|
@@ -17,7 +17,7 @@ autorequire:
|
|
17
17
|
bindir: bin
|
18
18
|
cert_chain: []
|
19
19
|
|
20
|
-
date:
|
20
|
+
date: 2011-03-20 00:00:00 -07:00
|
21
21
|
default_executable:
|
22
22
|
dependencies: []
|
23
23
|
|
@@ -102,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
102
|
requirements: []
|
103
103
|
|
104
104
|
rubyforge_project: curb
|
105
|
-
rubygems_version: 1.
|
105
|
+
rubygems_version: 1.5.2
|
106
106
|
signing_key:
|
107
107
|
specification_version: 3
|
108
108
|
summary: Ruby libcurl bindings
|