api-model 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -0
- data/Gemfile.lock +7 -5
- data/README.md +3 -0
- data/api-model.gemspec +2 -1
- data/lib/api-model.rb +28 -22
- data/lib/api_model/configuration.rb +39 -0
- data/lib/api_model/http_request.rb +20 -7
- data/lib/api_model/initializer.rb +4 -3
- data/lib/api_model/response.rb +34 -9
- data/lib/api_model/rest_methods.rb +21 -0
- data/spec/api-model/api_model_spec.rb +130 -0
- data/spec/api-model/configuration_spec.rb +78 -0
- data/spec/api-model/http_request_spec.rb +74 -0
- data/spec/{lib → api-model}/initializer_spec.rb +5 -5
- data/spec/api-model/response_spec.rb +186 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/fixtures/cars.yml +84 -0
- data/spec/support/fixtures/errors.yml +57 -0
- data/spec/support/fixtures/posts.yml +56 -0
- data/spec/support/fixtures/users.yml +57 -0
- data/spec/support/mock_models/banana.rb +3 -6
- data/spec/support/mock_models/blog_post.rb +2 -1
- data/spec/support/mock_models/car.rb +11 -0
- data/spec/support/mock_models/multiple_hosts.rb +6 -2
- data/spec/support/mock_models/user.rb +4 -0
- metadata +39 -12
- data/spec/lib/api_host_spec.rb +0 -20
- data/spec/lib/api_model_spec.rb +0 -34
- data/spec/lib/http_request_spec.rb +0 -43
- data/spec/lib/response_spec.rb +0 -89
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/mock_models/banana'
|
3
|
+
require 'support/mock_models/multiple_hosts'
|
4
|
+
|
5
|
+
describe ApiModel, "Configuration" do
|
6
|
+
|
7
|
+
after(:each) do
|
8
|
+
Banana.reset_api_configuration
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "api_host" do
|
12
|
+
it "should set the api host for all classes which inherit ApiModel::Base" do
|
13
|
+
ApiModel::Base.api_config do |config|
|
14
|
+
config.host = "foobarbaz.com"
|
15
|
+
end
|
16
|
+
|
17
|
+
Banana.api_model_configuration.host.should eq "foobarbaz.com"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should not override different classes configurations" do
|
21
|
+
MultipleHostsFoo.api_model_configuration.host.should eq("http://foo.com")
|
22
|
+
MultipleHostsBar.api_model_configuration.host.should eq("http://bar.com")
|
23
|
+
MultipleHostsNone.api_model_configuration.host.should be_nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "json_root" do
|
28
|
+
it 'should be possible to set on a class' do
|
29
|
+
Banana.api_config do |config|
|
30
|
+
config.json_root = "foo_bar"
|
31
|
+
end
|
32
|
+
|
33
|
+
Banana.api_model_configuration.json_root.should eq "foo_bar"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "headers" do
|
38
|
+
it 'should create default headers for content type and accepts' do
|
39
|
+
headers = Banana.api_model_configuration.headers
|
40
|
+
headers["Content-Type"].should eq "application/json; charset=utf-8"
|
41
|
+
headers["Accept"].should eq "application/json"
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should be possible to set new headers' do
|
45
|
+
ApiModel::Base.api_config { |config| config.headers = { foo: "bar" } }
|
46
|
+
Banana.api_model_configuration.headers[:foo].should eq "bar"
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should retain the default headers when you add a new one' do
|
50
|
+
ApiModel::Base.api_config { |config| config.headers = { foo: "bar" } }
|
51
|
+
|
52
|
+
headers = Banana.api_model_configuration.headers
|
53
|
+
headers.should have_key "Accept"
|
54
|
+
headers.should have_key "Content-Type"
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should be possible to override default headers' do
|
58
|
+
ApiModel::Base.api_config { |config| config.headers = { "Accept" => "image/gif" } }
|
59
|
+
Banana.api_model_configuration.headers["Accept"].should eq "image/gif"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should not unset other config values when you set a new one' do
|
64
|
+
ApiModel::Base.api_config { |c| c.host = "foo.com" }
|
65
|
+
Banana.api_config { |c| c.json_root = "banana" }
|
66
|
+
|
67
|
+
Banana.api_model_configuration.host.should eq "foo.com"
|
68
|
+
Banana.api_model_configuration.json_root.should eq "banana"
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should override config values from the superclass if it is changed' do
|
72
|
+
ApiModel::Base.api_config { |c| c.host = "will-go.com" }
|
73
|
+
Banana.api_config { |c| c.host = "new-host.com" }
|
74
|
+
|
75
|
+
Banana.api_model_configuration.host.should eq "new-host.com"
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/mock_models/blog_post'
|
3
|
+
|
4
|
+
describe ApiModel::HttpRequest do
|
5
|
+
|
6
|
+
describe "default attributes" do
|
7
|
+
subject { ApiModel::HttpRequest.new }
|
8
|
+
|
9
|
+
it "should default #method to :get" do
|
10
|
+
subject.method.should eq :get
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should default #options to a hash with headers" do
|
14
|
+
subject.options.should be_a Hash
|
15
|
+
subject.options[:headers].should_not be_nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "callbacks" do
|
20
|
+
class ApiModel::HttpRequest
|
21
|
+
before_run :do_something_before_run
|
22
|
+
def do_something_before_run; end
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should be possible to set callbacks on the run method' do
|
26
|
+
ApiModel::HttpRequest.any_instance.should_receive(:do_something_before_run).once
|
27
|
+
VCR.use_cassette('posts') { BlogPost.get_json "http://api-model-specs.com/single_post"}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "using api_host" do
|
32
|
+
let(:blog_post) do
|
33
|
+
BlogPost.api_config do |config|
|
34
|
+
config.host = "http://api-model-specs.com"
|
35
|
+
end
|
36
|
+
|
37
|
+
VCR.use_cassette('posts') do
|
38
|
+
BlogPost.get_json "/single_post"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should be used with #path to generate a #full_path" do
|
43
|
+
blog_post.http_response.api_call.request.url.should eq "http://api-model-specs.com/single_post"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "headers" do
|
48
|
+
let :request_headers do
|
49
|
+
BlogPost.api_config { |config| config.host = "http://api-model-specs.com" }
|
50
|
+
blog_post = VCR.use_cassette('posts') { BlogPost.get_json "/single_post" }
|
51
|
+
blog_post.http_response.api_call.request.options[:headers]
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should use the default content type header' do
|
55
|
+
request_headers["Content-Type"].should eq ApiModel::Configuration.new.headers["Content-Type"]
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should use the default accept header' do
|
59
|
+
request_headers["Accept"].should eq ApiModel::Configuration.new.headers["Accept"]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "sending a GET request" do
|
64
|
+
let(:request) { ApiModel::HttpRequest.new path: "http://api-model-specs.com/posts", method: :get }
|
65
|
+
|
66
|
+
it "should use typhoeus to send a request" do
|
67
|
+
VCR.use_cassette('posts') do
|
68
|
+
request.run
|
69
|
+
end
|
70
|
+
|
71
|
+
request.api_call.success?.should eq true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -3,6 +3,10 @@ require 'support/mock_models/banana'
|
|
3
3
|
|
4
4
|
describe ApiModel::Initializer do
|
5
5
|
|
6
|
+
Banana.class_eval do
|
7
|
+
include ApiModel::Initializer
|
8
|
+
end
|
9
|
+
|
6
10
|
let(:banana) { Banana.new color: "yellow", size: "large" }
|
7
11
|
|
8
12
|
it "should set attributes when initializing with a hash" do
|
@@ -16,16 +20,12 @@ describe ApiModel::Initializer do
|
|
16
20
|
expect(banana.size).to eq "small"
|
17
21
|
end
|
18
22
|
|
19
|
-
it "should not
|
23
|
+
it "should not blow up if update_attributes is called with nil" do
|
20
24
|
expect {
|
21
25
|
banana.update_attributes nil
|
22
26
|
}.to_not raise_error
|
23
27
|
end
|
24
28
|
|
25
|
-
it "should run callbacks on initialize" do
|
26
|
-
banana.ripe.should eq true
|
27
|
-
end
|
28
|
-
|
29
29
|
it "should log if an attempt was made to set an attribute which is not defined" do
|
30
30
|
ApiModel::Log.should_receive(:debug).with "Could not set foo on Banana"
|
31
31
|
Banana.new foo: "bar"
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/mock_models/blog_post'
|
3
|
+
require 'support/mock_models/user'
|
4
|
+
|
5
|
+
describe ApiModel::Response do
|
6
|
+
|
7
|
+
let(:valid_response) do
|
8
|
+
VCR.use_cassette('posts') do
|
9
|
+
ApiModel::HttpRequest.new(path: "http://api-model-specs.com/single_post", method: :get, builder: BlogPost).run
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "parsing the json body" do
|
14
|
+
it "should produce a hash given valid json" do
|
15
|
+
valid_response.json_response_body.should be_a(Hash)
|
16
|
+
valid_response.json_response_body["name"].should eq "foo"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should catch errors from parsing invalid json" do
|
20
|
+
valid_response.stub_chain(:http_response, :api_call, :body).and_return "blah"
|
21
|
+
ApiModel::Log.should_receive(:info).with "Could not parse JSON response: blah"
|
22
|
+
|
23
|
+
expect {
|
24
|
+
valid_response.json_response_body
|
25
|
+
}.to_not raise_error
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "using a custom json root on the response body" do
|
30
|
+
let :users do
|
31
|
+
User.api_config do |c|
|
32
|
+
c.json_root = "users"
|
33
|
+
end
|
34
|
+
VCR.use_cassette('users') { User.get_json "http://api-model-specs.com/users" }
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should use the json root to build from' do
|
38
|
+
users.should be_a Array
|
39
|
+
users.size.should eq 3
|
40
|
+
|
41
|
+
users.each do |user|
|
42
|
+
user.should be_a User
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "using a multi-level json root on the response body" do
|
48
|
+
let :user_search do
|
49
|
+
VCR.use_cassette('users') { User.get_json "http://api-model-specs.com/search" }
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should use the deep json root to build from' do
|
53
|
+
User.api_config { |c| c.json_root = "search.results.users" }
|
54
|
+
|
55
|
+
user_search.should be_a Array
|
56
|
+
user_search.size.should eq 3
|
57
|
+
|
58
|
+
user_search.each do |user|
|
59
|
+
user.should be_a User
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should raise a ApiModel::ResponseBuilderError exception if the hash does not contain the key' do
|
64
|
+
User.api_config { |c| c.json_root = "search.results.users.foo" }
|
65
|
+
|
66
|
+
expect {
|
67
|
+
user_search
|
68
|
+
}.to raise_error(ApiModel::ResponseBuilderError)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#build" do
|
73
|
+
it "should use the builder.build method if present" do
|
74
|
+
builder = double
|
75
|
+
builder.should_receive(:build).with something: "foo"
|
76
|
+
|
77
|
+
valid_response.build builder, something: "foo"
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should use builder.new if there's no builder.build method" do
|
81
|
+
builder = double
|
82
|
+
builder.should_receive(:new).with something_else: "hi"
|
83
|
+
|
84
|
+
valid_response.build builder, something_else: "hi"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "#build_objects" do
|
89
|
+
let(:single_object) do
|
90
|
+
valid_response.stub(:json_response_body).and_return name: "foo"
|
91
|
+
valid_response.build_objects
|
92
|
+
end
|
93
|
+
|
94
|
+
let(:array_of_objects) do
|
95
|
+
valid_response.stub(:json_response_body).and_return [{name: "foo"}, {name: "bar"}]
|
96
|
+
valid_response.build_objects
|
97
|
+
end
|
98
|
+
|
99
|
+
let(:empty_response) do
|
100
|
+
valid_response.stub(:json_response_body).and_return nil
|
101
|
+
valid_response.build_objects
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should build a single object" do
|
105
|
+
single_object.should be_a(BlogPost)
|
106
|
+
single_object.name.should eq "foo"
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should build an array of objects" do
|
110
|
+
array_of_objects[0].should be_a(BlogPost)
|
111
|
+
array_of_objects[0].name.should eq "foo"
|
112
|
+
|
113
|
+
array_of_objects[1].should be_a(BlogPost)
|
114
|
+
array_of_objects[1].name.should eq "bar"
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should include the ApiModel::HttpRequest object" do
|
118
|
+
single_object.http_response.should be_a(ApiModel::HttpRequest)
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should include the #json_response_body" do
|
122
|
+
single_object.json_response_body.should eq name: "foo"
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'should return nil if the api returns an empty body' do
|
126
|
+
empty_response.should be_nil
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "passing core methods down to the built class" do
|
131
|
+
ApiModel::Response::FALL_THROUGH_METHODS.each do |fall_trhough_method|
|
132
|
+
it "should pass ##{fall_trhough_method} on the built object class" do
|
133
|
+
allow_message_expectations_on_nil
|
134
|
+
valid_response.objects.should_receive(fall_trhough_method)
|
135
|
+
valid_response.send fall_trhough_method
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "raising exceptions" do
|
141
|
+
describe "for requests which return a 401" do
|
142
|
+
let :api_request do
|
143
|
+
VCR.use_cassette('errors') do
|
144
|
+
BlogPost.get_json "http://api-model-specs.com/needs_auth"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'should raise an ApiModel::UnauthenticatedError if raise_on_unauthenticated is true' do
|
149
|
+
BlogPost.api_config { |c| c.raise_on_unauthenticated = true }
|
150
|
+
expect {
|
151
|
+
api_request
|
152
|
+
}.to raise_error(ApiModel::UnauthenticatedError)
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'should not raise an ApiModel::UnauthenticatedError if raise_on_unauthenticated is false' do
|
156
|
+
BlogPost.api_config { |c| c.raise_on_unauthenticated = false }
|
157
|
+
expect {
|
158
|
+
api_request
|
159
|
+
}.to_not raise_error
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "for requests which return a 404" do
|
164
|
+
let :api_request do
|
165
|
+
VCR.use_cassette('errors') do
|
166
|
+
BlogPost.get_json "http://api-model-specs.com/not_found"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should raise an ApiModel::NotFoundError if raise_on_not_found is true' do
|
171
|
+
BlogPost.api_config { |c| c.raise_on_not_found = true }
|
172
|
+
expect {
|
173
|
+
api_request
|
174
|
+
}.to raise_error(ApiModel::NotFoundError)
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'should not raise an ApiModel::NotFoundError if raise_on_not_found is false' do
|
178
|
+
BlogPost.api_config { |c| c.raise_on_not_found = false }
|
179
|
+
expect {
|
180
|
+
api_request
|
181
|
+
}.to_not raise_error
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -8,4 +8,13 @@ require 'api-model'
|
|
8
8
|
VCR.configure do |c|
|
9
9
|
c.cassette_library_dir = 'spec/support/fixtures'
|
10
10
|
c.hook_into :webmock # or :fakeweb
|
11
|
+
end
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
|
15
|
+
# Reset any config changes after each spec
|
16
|
+
config.after(:each) do
|
17
|
+
ApiModel::Base.reset_api_configuration
|
18
|
+
end
|
19
|
+
|
11
20
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: get
|
5
|
+
uri: http://cars.com/one_convertable
|
6
|
+
headers:
|
7
|
+
User-Agent:
|
8
|
+
- Typhoeus - https://github.com/typhoeus/typhoeus
|
9
|
+
response:
|
10
|
+
status:
|
11
|
+
code: 200
|
12
|
+
message: OK
|
13
|
+
headers:
|
14
|
+
Server:
|
15
|
+
- nginx/1.4.1
|
16
|
+
Date:
|
17
|
+
- Thu, 28 Nov 2013 16:02:56 GMT
|
18
|
+
Content-Type:
|
19
|
+
- text/plain; charset=utf-8
|
20
|
+
Content-Length:
|
21
|
+
- '248'
|
22
|
+
Connection:
|
23
|
+
- keep-alive
|
24
|
+
body:
|
25
|
+
encoding: UTF-8
|
26
|
+
string: "{\"numberOfDoors\":2,\"top_speed\":60}"
|
27
|
+
http_version:
|
28
|
+
recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
|
29
|
+
|
30
|
+
- request:
|
31
|
+
method: get
|
32
|
+
uri: http://cars.com/fast_ones
|
33
|
+
headers:
|
34
|
+
User-Agent:
|
35
|
+
- Typhoeus - https://github.com/typhoeus/typhoeus
|
36
|
+
response:
|
37
|
+
status:
|
38
|
+
code: 200
|
39
|
+
message: OK
|
40
|
+
headers:
|
41
|
+
Server:
|
42
|
+
- nginx/1.4.1
|
43
|
+
Date:
|
44
|
+
- Thu, 28 Nov 2013 16:02:56 GMT
|
45
|
+
Content-Type:
|
46
|
+
- text/plain; charset=utf-8
|
47
|
+
Content-Length:
|
48
|
+
- '248'
|
49
|
+
Connection:
|
50
|
+
- keep-alive
|
51
|
+
body:
|
52
|
+
encoding: UTF-8
|
53
|
+
string: "[{\"numberOfDoors\":2,\"top_speed\":60},{\"numberOfDoors\":4,\"top_speed\":30,\"name\":\"Ford\"}]"
|
54
|
+
http_version:
|
55
|
+
recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
|
56
|
+
|
57
|
+
- request:
|
58
|
+
method: get
|
59
|
+
uri: http://cars.com/new_model
|
60
|
+
headers:
|
61
|
+
User-Agent:
|
62
|
+
- Typhoeus - https://github.com/typhoeus/typhoeus
|
63
|
+
response:
|
64
|
+
status:
|
65
|
+
code: 200
|
66
|
+
message: OK
|
67
|
+
headers:
|
68
|
+
Server:
|
69
|
+
- nginx/1.4.1
|
70
|
+
Date:
|
71
|
+
- Thu, 28 Nov 2013 16:02:56 GMT
|
72
|
+
Content-Type:
|
73
|
+
- text/plain; charset=utf-8
|
74
|
+
Content-Length:
|
75
|
+
- '248'
|
76
|
+
Connection:
|
77
|
+
- keep-alive
|
78
|
+
body:
|
79
|
+
encoding: UTF-8
|
80
|
+
string: "{\"numberOfDoors\":2,\"top_speed\":60,\"shiney\":true}"
|
81
|
+
http_version:
|
82
|
+
recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
|
83
|
+
|
84
|
+
recorded_with: VCR 2.8.0
|