abhay-typhoeus 0.0.22
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.textile +212 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/benchmarks/profile.rb +25 -0
- data/benchmarks/vs_nethttp.rb +35 -0
- data/examples/twitter.rb +21 -0
- data/ext/typhoeus/.gitignore +6 -0
- data/ext/typhoeus/extconf.rb +23 -0
- data/ext/typhoeus/native.c +11 -0
- data/ext/typhoeus/native.h +20 -0
- data/ext/typhoeus/typhoeus_easy.c +206 -0
- data/ext/typhoeus/typhoeus_easy.h +19 -0
- data/ext/typhoeus/typhoeus_multi.c +213 -0
- data/ext/typhoeus/typhoeus_multi.h +16 -0
- data/lib/typhoeus.rb +51 -0
- data/lib/typhoeus/.gitignore +1 -0
- data/lib/typhoeus/easy.rb +210 -0
- data/lib/typhoeus/filter.rb +28 -0
- data/lib/typhoeus/multi.rb +34 -0
- data/lib/typhoeus/remote.rb +306 -0
- data/lib/typhoeus/remote_method.rb +108 -0
- data/lib/typhoeus/remote_proxy_object.rb +48 -0
- data/lib/typhoeus/response.rb +17 -0
- data/lib/typhoeus/service.rb +20 -0
- data/profilers/valgrind.rb +24 -0
- data/spec/fixtures/result_set.xml +60 -0
- data/spec/servers/app.rb +24 -0
- data/spec/servers/delay_fixture_server.rb +66 -0
- data/spec/servers/method_server.rb +51 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/typhoeus/easy_spec.rb +148 -0
- data/spec/typhoeus/filter_spec.rb +35 -0
- data/spec/typhoeus/multi_spec.rb +82 -0
- data/spec/typhoeus/remote_method_spec.rb +141 -0
- data/spec/typhoeus/remote_proxy_object_spec.rb +73 -0
- data/spec/typhoeus/remote_spec.rb +699 -0
- data/spec/typhoeus/response_spec.rb +31 -0
- data/typhoeus.gemspec +88 -0
- metadata +104 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.*.sw?
|
data/README.textile
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
h1. Typhoeus
|
2
|
+
|
3
|
+
"http://github.com/pauldix/typhoeus/tree/master":http://github.com/pauldix/typhoeus/tree/master
|
4
|
+
|
5
|
+
"the mailing list":http://groups.google.com/group/typhoeus
|
6
|
+
|
7
|
+
Thanks to my employer "kgbweb":http://kgbweb.com for allowing me to release this as open source. Btw, we're hiring and we work on cool stuff like this every day. Get a hold of me if you rock at rails/js/html/css or if you have experience in search, information retrieval, and machine learning.
|
8
|
+
|
9
|
+
I also wanted to thank Todd A. Fisher. I ripped a good chunk of the c libcurl-multi code from his update to Curb. Awesome stuff Todd!
|
10
|
+
|
11
|
+
h2. Summary
|
12
|
+
|
13
|
+
Like a modern code version of the mythical beast with 100 serpent heads, Typhoeus runs HTTP requests in parallel while cleanly encapsulating handling logic. To be a little more specific, it's a library for accessing web services in Ruby. It's specifically designed for building RESTful service oriented architectures in Ruby that need to be fast enough to process calls to multiple services within the client's HTTP request/response life cycle.
|
14
|
+
|
15
|
+
Some of the awesome features are parallel request execution, memoization of request responses (so you don't make the same request multiple times in a single group), built in support for caching responses to memcached (or whatever), a nifty DSL for creating classes that make http calls and process responses, and mocking capability baked in. It uses libcurl and libcurl-multi to work this speedy magic. I wrote the c bindings myself so it's yet another Ruby libcurl library, but with some extra awesomeness added in.
|
16
|
+
|
17
|
+
h2. Installation
|
18
|
+
|
19
|
+
For now Typhoeus exists only on github. It requires you to have a current version of libcurl installed. I've tested this with 7.19.4.
|
20
|
+
<pre>
|
21
|
+
gem sources -a http://gems.github.com # if you haven't already
|
22
|
+
gem install pauldix-typhoeus
|
23
|
+
</pre>
|
24
|
+
If you're on Debian or Ubuntu and getting errors while trying to install, it could be because you don't have the latest version of libcurl installed. Do this to fix:
|
25
|
+
<pre>
|
26
|
+
sudo apt-get install libcurl4-gnutls-dev
|
27
|
+
</pre>
|
28
|
+
|
29
|
+
Another problem could be if you are running Mac Ports and you have libcurl installed through there. You need to uninstall it for Typhoeus to work! The version in Mac Ports is old and doesn't play nice. You should "download curl":http://curl.haxx.se/download.html and build from source. Then you'll have to install the gem again.
|
30
|
+
|
31
|
+
If you're still having issues, please let me know on "the mailing list":http://groups.google.com/group/typhoeus.
|
32
|
+
|
33
|
+
There's one other thing you should know. The Easy object (which is just a libcurl thing) allows you to set timeout values in milliseconds. However, for this to work you need to build libcurl with c-ares support built in. Unfortunately, I still haven't been able to get this to work on either my Mac or Linux. I'll have to figure that out.
|
34
|
+
|
35
|
+
h2. Usage
|
36
|
+
|
37
|
+
|
38
|
+
<pre>
|
39
|
+
require 'rubygems'
|
40
|
+
require 'typhoeus'
|
41
|
+
require 'json'
|
42
|
+
|
43
|
+
# here's an example for twitter search
|
44
|
+
# Including Typhoeus adds http methods like get, put, post, and delete.
|
45
|
+
# What's more interesting though is the stuff to build up what I call
|
46
|
+
# remote_methods.
|
47
|
+
class Twitter
|
48
|
+
include Typhoeus
|
49
|
+
remote_defaults :on_success => lambda {|response| JSON.parse(response.body)},
|
50
|
+
:on_failure => lambda {|response| puts "error code: #{response.code}"},
|
51
|
+
:base_uri => "http://search.twitter.com"
|
52
|
+
|
53
|
+
define_remote_method :search, :path => '/search.json'
|
54
|
+
define_remote_method :trends, :path => '/trends/:time_frame.json'
|
55
|
+
end
|
56
|
+
|
57
|
+
tweets = Twitter.search(:params => {:q => "railsconf"})
|
58
|
+
|
59
|
+
# if you look at the path argument for the :trends method, it has :time_frame.
|
60
|
+
# this tells it to add in a parameter called :time_frame that gets interpolated
|
61
|
+
# and inserted.
|
62
|
+
trends = Twitter.trends(:time_frame => :current)
|
63
|
+
|
64
|
+
# and then the calls don't actually happen until the first time you
|
65
|
+
# call a method on one of the objects returned from the remote_method
|
66
|
+
puts tweets.keys # it's a hash from parsed JSON
|
67
|
+
|
68
|
+
# you can also do things like override any of the default parameters
|
69
|
+
Twitter.search(:params => {:q => "hi"}, :on_success => lambda {|response| puts response.body})
|
70
|
+
|
71
|
+
# on_success and on_failure lambdas take a response object.
|
72
|
+
# It has four accesssors: code, body, headers, and time
|
73
|
+
|
74
|
+
# here's and example of memoization
|
75
|
+
twitter_searches = []
|
76
|
+
10.times do
|
77
|
+
twitter_searches << Twitter.search(:params => {:q => "railsconf"})
|
78
|
+
end
|
79
|
+
|
80
|
+
# this next part will actually make the call. However, it only makes one
|
81
|
+
# http request and parses the response once. The rest are memoized.
|
82
|
+
twitter_searches.each {|s| puts s.keys}
|
83
|
+
|
84
|
+
# you can also have it cache responses and do gets automatically
|
85
|
+
# here we define a remote method that caches the responses for 60 seconds
|
86
|
+
klass = Class.new do
|
87
|
+
include Typhoeus
|
88
|
+
|
89
|
+
define_remote_method :foo, :base_uri => "http://localhost:3001", :cache_responses => 60
|
90
|
+
end
|
91
|
+
|
92
|
+
klass.cache = some_memcached_instance_or_whatever
|
93
|
+
response = klass.foo
|
94
|
+
puts response.body # makes the request
|
95
|
+
|
96
|
+
second_response = klass.foo
|
97
|
+
puts response.body # pulls from the cache without making a request
|
98
|
+
|
99
|
+
# you can also pass timeouts on the define_remote_method or as a parameter
|
100
|
+
# Note that timeouts are in milliseconds.
|
101
|
+
Twitter.trends(:time_frame => :current, :timeout => 2000)
|
102
|
+
|
103
|
+
# you also get the normal get, put, post, and delete methods
|
104
|
+
class Remote
|
105
|
+
include Typhoeus
|
106
|
+
end
|
107
|
+
|
108
|
+
Remote.get("http://www.pauldix.net")
|
109
|
+
Remote.put("http://", :body => "this is a request body")
|
110
|
+
Remote.post("http://localhost:3001/posts.xml",
|
111
|
+
{:params => {:post => {:author => "paul", :title => "a title", :body => "a body"}}})
|
112
|
+
Remote.delete("http://localhost:3001/posts/1")
|
113
|
+
|
114
|
+
# you also have the ability to set request headers. So you can set your user agent and manually
|
115
|
+
Remote.get("http://www.pauldix.net", :headers => {"User-Agent" => "typhoeus", "If-None-Match" => "some etag"})
|
116
|
+
|
117
|
+
# and do things like basic HTTP authentication
|
118
|
+
require 'base64'
|
119
|
+
Remote.get("http://twitter.com/statuses/followers.json",
|
120
|
+
:headers => {"Authorization" => "Basic #{Base64.b64encode("login:password")}"})
|
121
|
+
|
122
|
+
# body and headers arguments also get passed through on defined remote methods.
|
123
|
+
class TwitterRestAPI
|
124
|
+
include Typhoeus
|
125
|
+
remote_defaults :on_success => lambda {|response| JSON.parse(response.body)},
|
126
|
+
:on_failure => lambda {|response| puts "error code: #{response.code}"},
|
127
|
+
:base_uri => "http://twitter.com"
|
128
|
+
|
129
|
+
define_remote_method :followers,
|
130
|
+
:path => '/statuses/followers.json',
|
131
|
+
:headers => {"Authorization" => "Basic #{Base64.b64encode("twitter_id:password")}"}
|
132
|
+
end
|
133
|
+
|
134
|
+
# The response object returned by get, put, post, and delete is passed to the on_success
|
135
|
+
# or on_failure lambda block if declared.
|
136
|
+
# The return value of the lambda block is then what is returned by the remote method invocation.
|
137
|
+
# The response object can do the following:
|
138
|
+
response.code # the http return code
|
139
|
+
response.body # the body of the response
|
140
|
+
response.headers # the response headers
|
141
|
+
response.time # the response time in seconds
|
142
|
+
|
143
|
+
# Typhoeus also has a nifty mocking framework built in
|
144
|
+
# mock all calls to get
|
145
|
+
Remote.mock(:get, :code => 200, :body => "whatever")
|
146
|
+
|
147
|
+
# here we mock calls to get for the url
|
148
|
+
Remote.mock(:get, :url => "http://pauldix.net", :code => 200, :body => "hi", :headers => "there", :time => 2)
|
149
|
+
|
150
|
+
# note that url, code, body, headers, and time are all optional parameters to mock.
|
151
|
+
# the first parameter can be either :get, :put, :post, or :delete
|
152
|
+
|
153
|
+
# you can also provide headers and body that are expected on the call. An exception will be raised if they don't match
|
154
|
+
Remote.mock(:get, :url => "http://pauldix.net", :expected_headers => {"If-None-Match" => "sldfkj234"})
|
155
|
+
Remote.mock(:put, :url => "http://pauldix.net", :expected_body => "this is a body!")
|
156
|
+
|
157
|
+
# using that mocking you could mock out the Twitter client like so:
|
158
|
+
Twitter.mock(:get, :body => '{"hi": "there"}')
|
159
|
+
# now any calls to trends, or search will get the mock and call the on_success handler. the response object will have that body.
|
160
|
+
# we could also mock out a failure like so
|
161
|
+
Twitter.mock(:get, :body => '{"fail": "oh noes!"}', :code => 500)
|
162
|
+
# now calls to a remote method will result in the on_failure handler being called
|
163
|
+
</pre>
|
164
|
+
|
165
|
+
The best place to see the functionality of what including Typhoeus in a class gives you is to look at the "remote_spec.rb"
|
166
|
+
|
167
|
+
h2. Benchmarks
|
168
|
+
|
169
|
+
I set up a benchmark to test how the parallel performance works vs Ruby's built in NET::HTTP. The setup was a local evented HTTP server that would take a request, sleep for 500 milliseconds and then issued a blank response. I set up the client to call this 20 times. Here are the results:
|
170
|
+
|
171
|
+
<pre>
|
172
|
+
net::http 0.030000 0.010000 0.040000 ( 10.054327)
|
173
|
+
typhoeus 0.020000 0.070000 0.090000 ( 0.508817)
|
174
|
+
</pre>
|
175
|
+
|
176
|
+
We can see from this that NET::HTTP performs as expected, taking 10 seconds to run 20 500ms requests. Typhoeus only takes 500ms (the time of the response that took the longest.) One other thing to note is that Typhoeus keeps a pool of libcurl Easy handles to use. For this benchmark I warmed the pool first. So if you test this out it may be a bit slower until the Easy handle pool has enough in it to run all the simultaneous requests. For some reason the easy handles can take quite some time to allocate.
|
177
|
+
|
178
|
+
h2. Next Steps
|
179
|
+
|
180
|
+
* Write up some more examples.
|
181
|
+
* Create a SimpleDB client library using Typhoeus.
|
182
|
+
* Create or get someone to create a CouchDB client library using Typhoeus.
|
183
|
+
* Add support for automatic retry, exponential back-off, and queuing for later.
|
184
|
+
* Add in the support for custom get and set methods on the cache.
|
185
|
+
* Add in support for integrated HTTP caching with Memcached.
|
186
|
+
|
187
|
+
h2. LICENSE
|
188
|
+
|
189
|
+
(The MIT License)
|
190
|
+
|
191
|
+
Copyright (c) 2009:
|
192
|
+
|
193
|
+
"Paul Dix":http://pauldix.net
|
194
|
+
|
195
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
196
|
+
a copy of this software and associated documentation files (the
|
197
|
+
'Software'), to deal in the Software without restriction, including
|
198
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
199
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
200
|
+
permit persons to whom the Software is furnished to do so, subject to
|
201
|
+
the following conditions:
|
202
|
+
|
203
|
+
The above copyright notice and this permission notice shall be
|
204
|
+
included in all copies or substantial portions of the Software.
|
205
|
+
|
206
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
207
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
208
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
209
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
210
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
211
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
212
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
begin
|
2
|
+
require './lib/typhoeus.rb'
|
3
|
+
rescue LoadError => err
|
4
|
+
puts "Typhoeus has not been built correctly: #{err.message}"
|
5
|
+
end
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'jeweler'
|
9
|
+
Jeweler::Tasks.new do |gemspec|
|
10
|
+
gemspec.name = "typhoeus"
|
11
|
+
gemspec.summary = "A library for interacting with web services (and building SOAs) at blinding speed."
|
12
|
+
gemspec.email = "paul@pauldix.net"
|
13
|
+
gemspec.homepage = "http://github.com/pauldix/typhoeus"
|
14
|
+
gemspec.authors = ["Paul Dix"]
|
15
|
+
|
16
|
+
gemspec.require_path = ['lib', 'ext']
|
17
|
+
gemspec.extra_rdoc_files = `git ls-files *.rdoc`.split("\n")
|
18
|
+
gemspec.files = `git ls-files`.split("\n")
|
19
|
+
gemspec.extensions = ["ext/typhoeus/extconf.rb"]
|
20
|
+
end
|
21
|
+
|
22
|
+
task :install do
|
23
|
+
# rm_rf "*.gem"
|
24
|
+
# puts `gem build typhoeus.gemspec`
|
25
|
+
# puts `sudo gem install typhoeus-#{Typhoeus::VERSION}.gem`
|
26
|
+
end
|
27
|
+
|
28
|
+
rescue LoadError
|
29
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler"
|
30
|
+
end
|
31
|
+
|
32
|
+
begin
|
33
|
+
require "spec"
|
34
|
+
require "spec/rake/spectask"
|
35
|
+
|
36
|
+
Spec::Rake::SpecTask.new do |t|
|
37
|
+
t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
|
38
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
39
|
+
end
|
40
|
+
|
41
|
+
desc "Run all the tests"
|
42
|
+
task :default => :spec
|
43
|
+
rescue LoadError => err
|
44
|
+
puts "RSpec not available"
|
45
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.22
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/typhoeus.rb'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'ruby-prof'
|
4
|
+
|
5
|
+
calls = 20
|
6
|
+
@klass = Class.new do
|
7
|
+
include Typhoeus
|
8
|
+
end
|
9
|
+
|
10
|
+
Typhoeus.init_easy_objects
|
11
|
+
|
12
|
+
RubyProf.start
|
13
|
+
|
14
|
+
responses = []
|
15
|
+
calls.times do |i|
|
16
|
+
responses << @klass.get("http://127.0.0.1:3000/#{i}")
|
17
|
+
end
|
18
|
+
|
19
|
+
responses.each {|r| }#raise unless r.response_body == "whatever"}
|
20
|
+
|
21
|
+
result = RubyProf.stop
|
22
|
+
|
23
|
+
# Print a flat profile to text
|
24
|
+
printer = RubyProf::FlatPrinter.new(result)
|
25
|
+
printer.print(STDOUT, 0)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/typhoeus.rb'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'benchmark'
|
5
|
+
include Benchmark
|
6
|
+
|
7
|
+
|
8
|
+
calls = 20
|
9
|
+
@klass = Class.new do
|
10
|
+
include Typhoeus
|
11
|
+
end
|
12
|
+
|
13
|
+
Typhoeus.init_easy_object_pool
|
14
|
+
|
15
|
+
benchmark do |t|
|
16
|
+
t.report("net::http") do
|
17
|
+
responses = []
|
18
|
+
|
19
|
+
calls.times do |i|
|
20
|
+
responses << open("http://127.0.0.1:3000/#{i}").read
|
21
|
+
end
|
22
|
+
|
23
|
+
responses.each {|r| raise unless r == "whatever"}
|
24
|
+
end
|
25
|
+
|
26
|
+
t.report("typhoeus") do
|
27
|
+
responses = []
|
28
|
+
|
29
|
+
calls.times do |i|
|
30
|
+
responses << @klass.get("http://127.0.0.1:3000/#{i}")
|
31
|
+
end
|
32
|
+
|
33
|
+
responses.each {|r| raise unless r.body == "whatever"}
|
34
|
+
end
|
35
|
+
end
|
data/examples/twitter.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/typhoeus.rb'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
class Twitter
|
6
|
+
include Typhoeus
|
7
|
+
remote_defaults :on_success => lambda {|response| JSON.parse(response.body)},
|
8
|
+
:on_failure => lambda {|response| puts "error code: #{response.code}"},
|
9
|
+
:base_uri => "http://search.twitter.com"
|
10
|
+
|
11
|
+
define_remote_method :search, :path => '/search.json'
|
12
|
+
define_remote_method :trends, :path => '/trends/:time_frame.json'
|
13
|
+
end
|
14
|
+
|
15
|
+
tweets = Twitter.search(:params => {:q => "railsconf"})
|
16
|
+
trends = Twitter.trends(:time_frame => :current)
|
17
|
+
|
18
|
+
# and then the calls don't actually happen until the first time you
|
19
|
+
# call a method on one of the objects returned from the remote_method
|
20
|
+
|
21
|
+
puts tweets.keys # it's a hash from parsed JSON
|
@@ -0,0 +1,23 @@
|
|
1
|
+
ENV["ARCHFLAGS"] = "-arch #{`uname -p` =~ /powerpc/ ? 'ppc' : 'i386'}"
|
2
|
+
|
3
|
+
require 'mkmf'
|
4
|
+
|
5
|
+
dir_config('curl')
|
6
|
+
|
7
|
+
if find_executable('curl-config')
|
8
|
+
$CFLAGS << " #{`curl-config --cflags`.strip}"
|
9
|
+
$LIBS << " #{`curl-config --libs`.strip}"
|
10
|
+
elsif !have_library('curl') or !have_header('curl/curl.h')
|
11
|
+
fail <<-EOM
|
12
|
+
Can't find libcurl or curl/curl.h
|
13
|
+
|
14
|
+
Try passing --with-curl-dir or --with-curl-lib and --with-curl-include
|
15
|
+
options to extconf.
|
16
|
+
EOM
|
17
|
+
end
|
18
|
+
|
19
|
+
if try_compile('int main() { return 0; }','-Wall')
|
20
|
+
$CFLAGS << ' -O3 -Wall -Wcast-qual -Wwrite-strings -Wconversion -Wmissing-noreturn -Winline'
|
21
|
+
end
|
22
|
+
|
23
|
+
create_makefile("typhoeus/native")
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#ifndef TYPHOEUS_NATIVE
|
2
|
+
#define TYPHOEUS_NATIVE
|
3
|
+
|
4
|
+
#include <ruby.h>
|
5
|
+
|
6
|
+
#ifndef RSTRING_PTR
|
7
|
+
#define RSTRING_PTR(s) (RSTRING(s)->ptr)
|
8
|
+
#endif
|
9
|
+
|
10
|
+
#ifndef RSTRING_LEN
|
11
|
+
#define RSTRING_LEN(s) (RSTRING(s)->len)
|
12
|
+
#endif
|
13
|
+
|
14
|
+
#include <curl/curl.h>
|
15
|
+
#include <curl/easy.h>
|
16
|
+
#include <curl/multi.h>
|
17
|
+
|
18
|
+
extern VALUE mTyphoeus;
|
19
|
+
|
20
|
+
#endif
|
@@ -0,0 +1,206 @@
|
|
1
|
+
#include <typhoeus_easy.h>
|
2
|
+
|
3
|
+
static VALUE idAppend;
|
4
|
+
VALUE cTyphoeusEasy;
|
5
|
+
|
6
|
+
static void dealloc(CurlEasy *curl_easy) {
|
7
|
+
if (curl_easy->request_chunk != NULL) {
|
8
|
+
free(curl_easy->request_chunk);
|
9
|
+
}
|
10
|
+
|
11
|
+
if (curl_easy->headers != NULL) {
|
12
|
+
curl_slist_free_all(curl_easy->headers);
|
13
|
+
}
|
14
|
+
|
15
|
+
curl_easy_cleanup(curl_easy->curl);
|
16
|
+
|
17
|
+
free(curl_easy);
|
18
|
+
}
|
19
|
+
|
20
|
+
static VALUE easy_setopt_string(VALUE self, VALUE opt_name, VALUE parameter) {
|
21
|
+
CurlEasy *curl_easy;
|
22
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
23
|
+
|
24
|
+
long opt = NUM2LONG(opt_name);
|
25
|
+
curl_easy_setopt(curl_easy->curl, opt, StringValuePtr(parameter));
|
26
|
+
return opt_name;
|
27
|
+
}
|
28
|
+
|
29
|
+
static VALUE easy_setopt_long(VALUE self, VALUE opt_name, VALUE parameter) {
|
30
|
+
CurlEasy *curl_easy;
|
31
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
32
|
+
|
33
|
+
long opt = NUM2LONG(opt_name);
|
34
|
+
curl_easy_setopt(curl_easy->curl, opt, NUM2LONG(parameter));
|
35
|
+
return opt_name;
|
36
|
+
}
|
37
|
+
|
38
|
+
static VALUE easy_getinfo_string(VALUE self, VALUE info) {
|
39
|
+
char *info_string;
|
40
|
+
CurlEasy *curl_easy;
|
41
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
42
|
+
|
43
|
+
long opt = NUM2LONG(info);
|
44
|
+
curl_easy_getinfo(curl_easy->curl, opt, &info_string);
|
45
|
+
|
46
|
+
return rb_str_new2(info_string);
|
47
|
+
}
|
48
|
+
|
49
|
+
static VALUE easy_getinfo_long(VALUE self, VALUE info) {
|
50
|
+
long info_long;
|
51
|
+
CurlEasy *curl_easy;
|
52
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
53
|
+
|
54
|
+
long opt = NUM2LONG(info);
|
55
|
+
curl_easy_getinfo(curl_easy->curl, opt, &info_long);
|
56
|
+
|
57
|
+
return LONG2NUM(info_long);
|
58
|
+
}
|
59
|
+
|
60
|
+
static VALUE easy_getinfo_double(VALUE self, VALUE info) {
|
61
|
+
double info_double = 0;
|
62
|
+
CurlEasy *curl_easy;
|
63
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
64
|
+
|
65
|
+
long opt = NUM2LONG(info);
|
66
|
+
curl_easy_getinfo(curl_easy->curl, opt, &info_double);
|
67
|
+
|
68
|
+
return rb_float_new(info_double);
|
69
|
+
}
|
70
|
+
|
71
|
+
static VALUE easy_perform(VALUE self) {
|
72
|
+
CurlEasy *curl_easy;
|
73
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
74
|
+
curl_easy_perform(curl_easy->curl);
|
75
|
+
|
76
|
+
return Qnil;
|
77
|
+
}
|
78
|
+
|
79
|
+
static size_t write_data_handler(char *stream, size_t size, size_t nmemb, VALUE val) {
|
80
|
+
rb_funcall(val, idAppend, 1, rb_str_new(stream, size * nmemb));
|
81
|
+
return size * nmemb;
|
82
|
+
}
|
83
|
+
|
84
|
+
static size_t read_callback(void *ptr, size_t size, size_t nmemb, void *data) {
|
85
|
+
int realsize = size * nmemb;
|
86
|
+
RequestChunk *mem = (RequestChunk *)data;
|
87
|
+
|
88
|
+
if (realsize > mem->size - mem->read) {
|
89
|
+
realsize = mem->size - mem->read;
|
90
|
+
}
|
91
|
+
|
92
|
+
if (realsize != 0) {
|
93
|
+
memcpy(ptr, &(mem->memory[mem->read]), realsize);
|
94
|
+
mem->read += realsize;
|
95
|
+
}
|
96
|
+
|
97
|
+
return realsize;
|
98
|
+
}
|
99
|
+
|
100
|
+
static void set_response_handlers(VALUE easy, CURL *curl) {
|
101
|
+
rb_iv_set(easy, "@response_body", rb_str_new2(""));
|
102
|
+
rb_iv_set(easy, "@response_header", rb_str_new2(""));
|
103
|
+
|
104
|
+
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)&write_data_handler);
|
105
|
+
curl_easy_setopt(curl, CURLOPT_WRITEDATA, rb_iv_get(easy, "@response_body"));
|
106
|
+
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, (curl_write_callback)&write_data_handler);
|
107
|
+
curl_easy_setopt(curl, CURLOPT_HEADERDATA, rb_iv_get(easy, "@response_header"));
|
108
|
+
}
|
109
|
+
|
110
|
+
static VALUE easy_reset(VALUE self) {
|
111
|
+
CurlEasy *curl_easy;
|
112
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
113
|
+
|
114
|
+
if (curl_easy->request_chunk != NULL) {
|
115
|
+
free(curl_easy->request_chunk);
|
116
|
+
curl_easy->request_chunk = NULL;
|
117
|
+
}
|
118
|
+
|
119
|
+
if (curl_easy->headers != NULL) {
|
120
|
+
curl_slist_free_all(curl_easy->headers);
|
121
|
+
curl_easy->headers = NULL;
|
122
|
+
}
|
123
|
+
|
124
|
+
curl_easy_reset(curl_easy->curl);
|
125
|
+
|
126
|
+
set_response_handlers(self, curl_easy->curl);
|
127
|
+
|
128
|
+
return Qnil;
|
129
|
+
}
|
130
|
+
|
131
|
+
static VALUE easy_add_header(VALUE self, VALUE header) {
|
132
|
+
CurlEasy *curl_easy;
|
133
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
134
|
+
|
135
|
+
curl_easy->headers = curl_slist_append(curl_easy->headers, RSTRING_PTR(header));
|
136
|
+
return header;
|
137
|
+
}
|
138
|
+
|
139
|
+
static VALUE easy_set_headers(VALUE self) {
|
140
|
+
CurlEasy *curl_easy;
|
141
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
142
|
+
|
143
|
+
curl_easy_setopt(curl_easy->curl, CURLOPT_HTTPHEADER, curl_easy->headers);
|
144
|
+
|
145
|
+
return Qnil;
|
146
|
+
}
|
147
|
+
|
148
|
+
static VALUE easy_set_request_body(VALUE self, VALUE data, VALUE content_length_header) {
|
149
|
+
CurlEasy *curl_easy;
|
150
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
151
|
+
|
152
|
+
curl_easy->request_chunk = ALLOC(RequestChunk);
|
153
|
+
curl_easy->request_chunk->size = RSTRING_LEN(data);
|
154
|
+
curl_easy->request_chunk->memory = StringValuePtr(data);
|
155
|
+
curl_easy->request_chunk->read = 0;
|
156
|
+
|
157
|
+
curl_easy_setopt(curl_easy->curl, CURLOPT_READFUNCTION, (curl_read_callback)read_callback);
|
158
|
+
curl_easy_setopt(curl_easy->curl, CURLOPT_READDATA, curl_easy->request_chunk);
|
159
|
+
curl_easy_setopt(curl_easy->curl, CURLOPT_INFILESIZE, RSTRING_LEN(data));
|
160
|
+
|
161
|
+
return Qnil;
|
162
|
+
}
|
163
|
+
|
164
|
+
static VALUE easy_escape(VALUE self, VALUE data, VALUE length) {
|
165
|
+
CurlEasy *curl_easy;
|
166
|
+
Data_Get_Struct(self, CurlEasy, curl_easy);
|
167
|
+
|
168
|
+
return rb_str_new2(curl_easy_escape(curl_easy->curl, StringValuePtr(data), NUM2INT(length)));
|
169
|
+
}
|
170
|
+
|
171
|
+
static VALUE version(VALUE self) {
|
172
|
+
return rb_str_new2(curl_version());
|
173
|
+
}
|
174
|
+
|
175
|
+
static VALUE new(int argc, VALUE *argv, VALUE klass) {
|
176
|
+
CURL *curl = curl_easy_init();
|
177
|
+
CurlEasy *curl_easy = ALLOC(CurlEasy);
|
178
|
+
curl_easy->curl = curl;
|
179
|
+
curl_easy->headers = NULL;
|
180
|
+
curl_easy->request_chunk = NULL;
|
181
|
+
VALUE easy = Data_Wrap_Struct(cTyphoeusEasy, 0, dealloc, curl_easy);
|
182
|
+
|
183
|
+
set_response_handlers(easy, curl);
|
184
|
+
|
185
|
+
rb_obj_call_init(easy, argc, argv);
|
186
|
+
|
187
|
+
return easy;
|
188
|
+
}
|
189
|
+
|
190
|
+
void init_typhoeus_easy() {
|
191
|
+
VALUE klass = cTyphoeusEasy = rb_define_class_under(mTyphoeus, "Easy", rb_cObject);
|
192
|
+
idAppend = rb_intern("<<");
|
193
|
+
rb_define_singleton_method(klass, "new", new, -1);
|
194
|
+
rb_define_private_method(klass, "easy_setopt_string", easy_setopt_string, 2);
|
195
|
+
rb_define_private_method(klass, "easy_setopt_long", easy_setopt_long, 2);
|
196
|
+
rb_define_private_method(klass, "easy_getinfo_string", easy_getinfo_string, 1);
|
197
|
+
rb_define_private_method(klass, "easy_getinfo_long", easy_getinfo_long, 1);
|
198
|
+
rb_define_private_method(klass, "easy_getinfo_double", easy_getinfo_double, 1);
|
199
|
+
rb_define_private_method(klass, "easy_perform", easy_perform, 0);
|
200
|
+
rb_define_private_method(klass, "easy_reset", easy_reset, 0);
|
201
|
+
rb_define_private_method(klass, "easy_set_request_body", easy_set_request_body, 1);
|
202
|
+
rb_define_private_method(klass, "easy_set_headers", easy_set_headers, 0);
|
203
|
+
rb_define_private_method(klass, "easy_add_header", easy_add_header, 1);
|
204
|
+
rb_define_private_method(klass, "easy_escape", easy_escape, 2);
|
205
|
+
rb_define_private_method(klass, "version", version, 0);
|
206
|
+
}
|