api-model 0.0.2 → 0.0.3
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 +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
|