magnum-pi 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +7 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.rdoc +5 -0
- data/Gemfile +14 -0
- data/README.rdoc +31 -0
- data/Rakefile +10 -0
- data/VERSION +1 -0
- data/examples/vimeo.rb +43 -0
- data/lib/magnum-pi.rb +14 -0
- data/lib/magnum-pi/api.rb +37 -0
- data/lib/magnum-pi/api/consumer.rb +92 -0
- data/lib/magnum-pi/api/instance.rb +23 -0
- data/lib/magnum-pi/api/resources.rb +17 -0
- data/lib/magnum-pi/api/scheme.rb +47 -0
- data/lib/magnum-pi/dsl.rb +62 -0
- data/lib/magnum-pi/gem_ext.rb +1 -0
- data/lib/magnum-pi/gem_ext/mechanize.rb +1 -0
- data/lib/magnum-pi/gem_ext/mechanize/http.rb +1 -0
- data/lib/magnum-pi/gem_ext/mechanize/http/agent.rb +60 -0
- data/lib/magnum-pi/version.rb +7 -0
- data/magnum-pi.gemspec +27 -0
- data/script/console +11 -0
- data/test/test_helper.rb +11 -0
- data/test/test_helper/coverage.rb +11 -0
- data/test/unit/api/test_consumer.rb +176 -0
- data/test/unit/api/test_instance.rb +53 -0
- data/test/unit/api/test_resources.rb +19 -0
- data/test/unit/api/test_scheme.rb +81 -0
- data/test/unit/test_api.rb +92 -0
- data/test/unit/test_dsl.rb +106 -0
- data/test/unit/test_magnum-pi.rb +29 -0
- metadata +195 -0
@@ -0,0 +1 @@
|
|
1
|
+
require "magnum-pi/gem_ext/mechanize"
|
@@ -0,0 +1 @@
|
|
1
|
+
require "magnum-pi/gem_ext/mechanize/http"
|
@@ -0,0 +1 @@
|
|
1
|
+
require "magnum-pi/gem_ext/mechanize/http/agent"
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Taken from: http://scottwb.com/blog/2013/11/09/defeating-the-infamous-mechanize-too-many-connection-resets-bug/
|
2
|
+
|
3
|
+
class Mechanize
|
4
|
+
class HTTP
|
5
|
+
class Agent
|
6
|
+
MAX_RESET_RETRIES = 10
|
7
|
+
|
8
|
+
# We need to replace the core Mechanize HTTP method:
|
9
|
+
#
|
10
|
+
# Mechanize::HTTP::Agent#fetch
|
11
|
+
#
|
12
|
+
# with a wrapper that handles the infamous "too many connection resets"
|
13
|
+
# Mechanize bug that is described here:
|
14
|
+
#
|
15
|
+
# https://github.com/sparklemotion/mechanize/issues/123
|
16
|
+
#
|
17
|
+
# The wrapper shuts down the persistent HTTP connection when it fails with
|
18
|
+
# this error, and simply tries again. In practice, this only ever needs to
|
19
|
+
# be retried once, but I am going to let it retry a few times
|
20
|
+
# (MAX_RESET_RETRIES), just in case.
|
21
|
+
#
|
22
|
+
def fetch_with_retry(
|
23
|
+
uri,
|
24
|
+
method = :get,
|
25
|
+
headers = {},
|
26
|
+
params = [],
|
27
|
+
referer = current_page,
|
28
|
+
redirects = 0
|
29
|
+
)
|
30
|
+
action = "#{method.to_s.upcase} #{uri.to_s}"
|
31
|
+
retry_count = 0
|
32
|
+
|
33
|
+
begin
|
34
|
+
fetch_without_retry(uri, method, headers, params, referer, redirects)
|
35
|
+
rescue Net::HTTP::Persistent::Error => e
|
36
|
+
# Pass on any other type of error.
|
37
|
+
raise unless e.message =~ /too many connection resets/
|
38
|
+
|
39
|
+
# Pass on the error if we've tried too many times.
|
40
|
+
if retry_count >= MAX_RESET_RETRIES
|
41
|
+
puts "**** WARN: Mechanize retried connection reset #{MAX_RESET_RETRIES} times and never succeeded: #{action}"
|
42
|
+
raise
|
43
|
+
end
|
44
|
+
|
45
|
+
# Otherwise, shutdown the persistent HTTP connection and try again.
|
46
|
+
puts "**** WARN: Mechanize retrying connection reset error: #{action}"
|
47
|
+
retry_count += 1
|
48
|
+
self.http.shutdown
|
49
|
+
retry
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Alias so #fetch actually uses our new #fetch_with_retry to wrap the
|
54
|
+
# old one aliased as #fetch_without_retry.
|
55
|
+
alias_method :fetch_without_retry, :fetch
|
56
|
+
alias_method :fetch, :fetch_with_retry
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/magnum-pi.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../lib/magnum-pi/version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Paul Engel"]
|
6
|
+
gem.email = ["pm_engel@icloud.com"]
|
7
|
+
gem.summary = %q{Create an easy interface to talk with APIs}
|
8
|
+
gem.description = %q{Create an easy interface to talk with APIs}
|
9
|
+
gem.homepage = "https://github.com/archan937/magnum-pi"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "magnum-pi"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = MagnumPI::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency "mechanize"
|
19
|
+
gem.add_dependency "oj"
|
20
|
+
gem.add_dependency "xml-simple"
|
21
|
+
|
22
|
+
gem.add_development_dependency "rake"
|
23
|
+
gem.add_development_dependency "pry"
|
24
|
+
gem.add_development_dependency "simplecov"
|
25
|
+
gem.add_development_dependency "minitest"
|
26
|
+
gem.add_development_dependency "mocha"
|
27
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler"
|
4
|
+
Bundler.require :default, :development
|
5
|
+
|
6
|
+
Dir[File.expand_path("../../examples/*.rb", __FILE__)].each do |example|
|
7
|
+
require example
|
8
|
+
end
|
9
|
+
|
10
|
+
puts "Loading MagnumPI development environment (#{MagnumPI::VERSION})"
|
11
|
+
Pry.start
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
if Dir.pwd == File.expand_path("../../..", __FILE__)
|
2
|
+
require "simplecov"
|
3
|
+
SimpleCov.coverage_dir "test/coverage"
|
4
|
+
SimpleCov.start do
|
5
|
+
add_group "MagnumPI", "lib"
|
6
|
+
add_group "Test suite", "test"
|
7
|
+
add_filter do |src|
|
8
|
+
src.filename.include?("gem_ext/mechanize")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require_relative "../../test_helper"
|
2
|
+
|
3
|
+
module Unit
|
4
|
+
module API
|
5
|
+
class TestConsumer < MiniTest::Test
|
6
|
+
|
7
|
+
class Consumer
|
8
|
+
include MagnumPI::API::Consumer
|
9
|
+
def to_params(url, *args)
|
10
|
+
args[0]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class IncompleteConsumer
|
15
|
+
include MagnumPI::API::Consumer
|
16
|
+
end
|
17
|
+
|
18
|
+
describe MagnumPI::API::Consumer do
|
19
|
+
before do
|
20
|
+
@consumer = Consumer.new
|
21
|
+
@consumer.stubs(:api).returns(
|
22
|
+
:uri => "http://foo.bar",
|
23
|
+
:format => :json
|
24
|
+
)
|
25
|
+
@consumer.stubs(:resources).returns(
|
26
|
+
:statistics => [:get, "stats", MagnumPI::API::Resources::Variable.new.tap{|var| var.name = :date}]
|
27
|
+
)
|
28
|
+
end
|
29
|
+
describe "#get" do
|
30
|
+
it "makes a GET request" do
|
31
|
+
response = mock
|
32
|
+
response.expects(:content).returns('{"name": "Paul Engel"}')
|
33
|
+
@consumer.expects(:request).with(:get, "http://foo.bar", {:foo => "bar"}).returns(response)
|
34
|
+
assert_equal({"name" => "Paul Engel"}, @consumer.get(:foo => "bar"))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
describe "#post" do
|
38
|
+
it "makes a POST request" do
|
39
|
+
response = mock
|
40
|
+
response.expects(:content).returns('{"name": "Paul Engel"}')
|
41
|
+
@consumer.expects(:request).with(:post, "http://foo.bar", {:foo => "bar"}).returns(response)
|
42
|
+
assert_equal({"name" => "Paul Engel"}, @consumer.post(:foo => "bar"))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
describe "#download" do
|
46
|
+
it "downloads using the Mechanize agent" do
|
47
|
+
response = mock
|
48
|
+
response.expects(:save_as).with("path/to/target")
|
49
|
+
@consumer.expects(:request).with(:get, "http://foo.bar", :foo => "bar").returns(response)
|
50
|
+
@consumer.download "path/to/target", :get, :foo => "bar"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
describe "#resource" do
|
54
|
+
it "interpolates passed variables and makes a request" do
|
55
|
+
@consumer.expects(:send).with(:get, "stats", "2014-03-20")
|
56
|
+
@consumer.resource :statistics, :date => "2014-03-20"
|
57
|
+
end
|
58
|
+
it "interpolates passed variables and downloads a file" do
|
59
|
+
@consumer.expects(:send).with(:download, "path/to/target", :get, "stats", "2014-03-20")
|
60
|
+
@consumer.resource :statistics, {:date => "2014-03-20"}, "path/to/target"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
describe "#method_missing" do
|
64
|
+
describe "corresponding resource" do
|
65
|
+
it "delegates to #resource" do
|
66
|
+
@consumer.expects(:resource).with(:statistics, :foo, :bar)
|
67
|
+
@consumer.statistics :foo, :bar
|
68
|
+
end
|
69
|
+
end
|
70
|
+
describe "no corresponding resource" do
|
71
|
+
it "raises a NoMethodError" do
|
72
|
+
assert_raises NoMethodError do
|
73
|
+
@consumer.foo
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
describe "#request" do
|
79
|
+
it "is delegated to the Mechanize agent" do
|
80
|
+
@consumer.send(:agent).expects(:send).with(:get, :foo, :bar)
|
81
|
+
@consumer.send(:request, :get, :foo, :bar)
|
82
|
+
end
|
83
|
+
it "raises an error when a Mechanize::ResponseCodeError occurs" do
|
84
|
+
agent = @consumer.send(:agent)
|
85
|
+
def agent.send(*args)
|
86
|
+
raise Mechanize::ResponseCodeError.new(Struct.new(:code).new)
|
87
|
+
end
|
88
|
+
assert_raises MagnumPI::API::Consumer::Error do
|
89
|
+
@consumer.get
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
describe "#agent" do
|
94
|
+
it "returns a memoized Mechanize agent" do
|
95
|
+
assert_equal Mechanize, @consumer.send(:agent).class
|
96
|
+
assert_equal @consumer.send(:agent).object_id, @consumer.send(:agent).object_id
|
97
|
+
end
|
98
|
+
it "is configured as expected" do
|
99
|
+
assert_equal OpenSSL::SSL::VERIFY_NONE, @consumer.send(:agent).verify_mode
|
100
|
+
assert_equal Mechanize::Download, @consumer.send(:agent).pluggable_parser.default
|
101
|
+
end
|
102
|
+
end
|
103
|
+
describe "#parse_args" do
|
104
|
+
it "derives the URL and params" do
|
105
|
+
@consumer.expects(:to_url).with({:foo => "bar"}).returns("http://foo.bar")
|
106
|
+
assert_equal ["http://foo.bar", {:foo => "bar"}], @consumer.send(:parse_args, {:foo => "bar"})
|
107
|
+
end
|
108
|
+
end
|
109
|
+
describe "#parse_resource_variables" do
|
110
|
+
it "returns an array containing arguments for making a request" do
|
111
|
+
resource = @consumer.resources[:statistics]
|
112
|
+
assert_equal [:get, "stats", nil], @consumer.send(:parse_resource_variables, resource, {})
|
113
|
+
assert_equal [:get, "stats", nil], @consumer.send(:parse_resource_variables, resource, {:DATE => "2014-03-20"})
|
114
|
+
assert_equal [:get, "stats", "2014-03-20"], @consumer.send(:parse_resource_variables, resource, {:date => "2014-03-20"})
|
115
|
+
assert_equal [:get, "stats", "2014-03-20"], @consumer.send(:parse_resource_variables, resource, {"date" => "2014-03-20"})
|
116
|
+
assert_equal [:get, "stats", "2014-03-20"], @consumer.send(:parse_resource_variables, resource, ["2014-03-20"])
|
117
|
+
assert_equal [:get, "stats", "2014-03-20"], @consumer.send(:parse_resource_variables, resource, "2014-03-20")
|
118
|
+
assert_raises ArgumentError do
|
119
|
+
@consumer.send :parse_resource_variables, resource, ["2014-03-20", "foo"]
|
120
|
+
end
|
121
|
+
|
122
|
+
resource = [:post, "foobar"]
|
123
|
+
assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, {})
|
124
|
+
assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, {:DATE => "2014-03-20"})
|
125
|
+
assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, {:date => "2014-03-20"})
|
126
|
+
assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, {"date" => "2014-03-20"})
|
127
|
+
assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, ["2014-03-20"])
|
128
|
+
assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, ["2014-03-20", "foo"])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
describe "#to_url" do
|
132
|
+
it "returns api[:uri]" do
|
133
|
+
assert_equal "http://foo.bar", @consumer.send(:to_url)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
describe "#to_params" do
|
137
|
+
it "raises a NotImplementedError" do
|
138
|
+
assert_raises NotImplementedError do
|
139
|
+
IncompleteConsumer.new.send :to_params, "url"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
describe "#parse_content" do
|
144
|
+
it "parses the passed content" do
|
145
|
+
assert_equal(
|
146
|
+
{
|
147
|
+
"foo" => "bar"
|
148
|
+
}, @consumer.send(:parse_content,
|
149
|
+
<<-JSON
|
150
|
+
{"foo": "bar"}
|
151
|
+
JSON
|
152
|
+
)
|
153
|
+
)
|
154
|
+
@consumer.expects(:api).returns :format => "xml"
|
155
|
+
assert_equal(
|
156
|
+
{
|
157
|
+
"bar" => ["BAR"],
|
158
|
+
"baz" => [{"hello" => "world", "content" => "Baz!"}]
|
159
|
+
}, @consumer.send(:parse_content,
|
160
|
+
<<-XML
|
161
|
+
<xml>
|
162
|
+
<bar>BAR</bar>
|
163
|
+
<baz hello="world">Baz!</baz>
|
164
|
+
</xml>
|
165
|
+
XML
|
166
|
+
)
|
167
|
+
)
|
168
|
+
@consumer.expects(:api).returns :format => "unknown"
|
169
|
+
assert_equal("foobar", @consumer.send(:parse_content, "foobar"))
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative "../../test_helper"
|
2
|
+
|
3
|
+
module Unit
|
4
|
+
module API
|
5
|
+
class TestInstance < MiniTest::Test
|
6
|
+
|
7
|
+
class Foo
|
8
|
+
include MagnumPI::API::Instance
|
9
|
+
end
|
10
|
+
|
11
|
+
describe MagnumPI::API::Instance do
|
12
|
+
describe ".initialize" do
|
13
|
+
it "defines @api and @resources" do
|
14
|
+
api = mock
|
15
|
+
api.expects(:finalize).returns(api)
|
16
|
+
resources = mock
|
17
|
+
resources.expects(:to_hash).returns(resources)
|
18
|
+
|
19
|
+
Foo.expects(:api).returns api
|
20
|
+
Foo.expects(:resources).returns resources
|
21
|
+
foo = Foo.new
|
22
|
+
|
23
|
+
assert_equal api, foo.instance_variable_get(:@api)
|
24
|
+
assert_equal resources, foo.instance_variable_get(:@resources)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
describe "instances" do
|
28
|
+
before do
|
29
|
+
api = mock
|
30
|
+
api.expects(:finalize).returns(api)
|
31
|
+
Foo.expects(:api).returns api
|
32
|
+
Foo.expects(:resources).returns Hash.new
|
33
|
+
end
|
34
|
+
describe "#api" do
|
35
|
+
it "returns @api" do
|
36
|
+
foo, api = Foo.new, api
|
37
|
+
foo.instance_variable_set :@api, api
|
38
|
+
assert_equal api, foo.send(:api)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
describe "#resources" do
|
42
|
+
it "returns @resources" do
|
43
|
+
foo, resources = Foo.new, resources
|
44
|
+
foo.instance_variable_set :@resources, resources
|
45
|
+
assert_equal resources, foo.send(:resources)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "../../test_helper"
|
2
|
+
|
3
|
+
module Unit
|
4
|
+
module API
|
5
|
+
class TestResources < MiniTest::Test
|
6
|
+
|
7
|
+
describe MagnumPI::API::Resources do
|
8
|
+
describe ".var" do
|
9
|
+
it "returns a MagnumPI::API::Resources::Variable instance" do
|
10
|
+
variable = MagnumPI::API::Resources.new.var("foo")
|
11
|
+
assert_equal MagnumPI::API::Resources::Variable, variable.class
|
12
|
+
assert_equal "foo", variable.name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative "../../test_helper"
|
2
|
+
|
3
|
+
module Unit
|
4
|
+
module API
|
5
|
+
class TestScheme < MiniTest::Test
|
6
|
+
|
7
|
+
describe MagnumPI::API::Scheme do
|
8
|
+
before do
|
9
|
+
@scheme = MagnumPI::API::Scheme.new
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#initialize" do
|
13
|
+
it "presets `uri` and `format`" do
|
14
|
+
assert_equal({:uri => String, :format => Symbol}, @scheme.instance_eval("_types"))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#class" do
|
19
|
+
it "returns MagnumPI::API::Scheme" do
|
20
|
+
assert_equal MagnumPI::API::Scheme, @scheme.class
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#finalize" do
|
25
|
+
it "foo" do
|
26
|
+
@scheme.foo String
|
27
|
+
@scheme.bar "bar"
|
28
|
+
object_id = @scheme.instance_eval("_values").object_id
|
29
|
+
|
30
|
+
assert_equal({:bar => "bar"}, @scheme.finalize)
|
31
|
+
assert_equal({:foo => "foo", :bar => "bar"}, @scheme.finalize(:foo => "foo"))
|
32
|
+
assert_equal object_id, @scheme.instance_eval("_values").object_id
|
33
|
+
assert_equal({:foo => "FOO", :bar => "bar"}, @scheme.finalize(:foo => "FOO"))
|
34
|
+
|
35
|
+
assert_raises ArgumentError do
|
36
|
+
@scheme.finalize :foo => :foo
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#_types" do
|
42
|
+
it "is an internal memoized hash" do
|
43
|
+
assert_equal true, @scheme.instance_eval("_types").is_a?(Hash)
|
44
|
+
assert_equal @scheme.instance_eval("_types").object_id, @scheme.instance_eval("_types").object_id
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "types" do
|
49
|
+
describe "defining" do
|
50
|
+
it "can only be done once" do
|
51
|
+
assert_equal({:uri => String, :format => Symbol}, @scheme.instance_eval("_types"))
|
52
|
+
@scheme.format String
|
53
|
+
assert_equal({:uri => String, :format => Symbol}, @scheme.instance_eval("_types"))
|
54
|
+
@scheme.foo String
|
55
|
+
assert_equal({:uri => String, :format => Symbol, :foo => String}, @scheme.instance_eval("_types"))
|
56
|
+
@scheme.foo Symbol
|
57
|
+
assert_equal({:uri => String, :format => Symbol, :foo => String}, @scheme.instance_eval("_types"))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
describe "validating" do
|
61
|
+
describe "when setting a valid value" do
|
62
|
+
it "allows value to be set" do
|
63
|
+
assert_equal({}, @scheme.instance_eval("_values"))
|
64
|
+
@scheme.format :json
|
65
|
+
assert_equal({:format => :json}, @scheme.instance_eval("_values"))
|
66
|
+
@scheme.format :xml
|
67
|
+
assert_equal({:format => :xml}, @scheme.instance_eval("_values"))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
describe "when setting an invalid value" do
|
71
|
+
it "raises an error" do
|
72
|
+
assert_equal "Invalid value for 'format': \"json\" (expected Symbol)", assert_raises(ArgumentError){ @scheme.format "json" }.message
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|