magnum-pi 0.1.0
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.
- 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
|