api-model 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -1
- data/Gemfile.lock +3 -3
- data/README.md +21 -0
- data/api-model.gemspec +1 -1
- data/lib/api-model.rb +2 -1
- data/lib/api_model/builder/hash.rb +11 -0
- data/lib/api_model/{cache_stategies → cache_stategy}/no_cache.rb +1 -1
- data/lib/api_model/class_methods.rb +9 -1
- data/lib/api_model/configuration.rb +1 -1
- data/lib/api_model/instance_methods.rb +56 -0
- data/spec/api-model/api_model_spec.rb +93 -0
- data/spec/api-model/configuration_spec.rb +1 -1
- data/spec/support/fixtures/posts.yml +112 -0
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 820bf511fd4b03fc1a8d9523717d6f5a880f4ed5
|
4
|
+
data.tar.gz: c412f230bde85508e50b9ccb4615b79522b8d70e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4300025629a98a9c70f6c8f81cb9b0311f5a439256bdf0a5a68d3ba0595fc67049af6e7fb17add9452b5cbb875feb221b755aa690764186ce28f00c8c79d46a9
|
7
|
+
data.tar.gz: fbf370237c20cbc647435cf72feb51d5732cc9bf4f78aaa67dfebf247d7798bba4cd72c56e527ab994e1c73e5862d9ea9e99f6fbb570220bee1753935f915cf0
|
data/.rspec
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
--color
|
2
|
-
--format
|
2
|
+
--format progress
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
api-model (0.1.
|
4
|
+
api-model (0.1.2)
|
5
5
|
activemodel
|
6
6
|
activesupport
|
7
7
|
hashie
|
@@ -52,8 +52,8 @@ GEM
|
|
52
52
|
slop (3.4.6)
|
53
53
|
thread_safe (0.1.3)
|
54
54
|
atomic
|
55
|
-
typhoeus (0.6.
|
56
|
-
ethon (~> 0.6.
|
55
|
+
typhoeus (0.6.7)
|
56
|
+
ethon (~> 0.6.2)
|
57
57
|
tzinfo (0.3.38)
|
58
58
|
vcr (2.8.0)
|
59
59
|
webmock (1.15.0)
|
data/README.md
CHANGED
@@ -253,3 +253,24 @@ These can of course be overridden by just re-defining them in the headers config
|
|
253
253
|
config.headers = { "Content-Type" => "application/soap+xml" }
|
254
254
|
end
|
255
255
|
```
|
256
|
+
|
257
|
+
### Logging requests
|
258
|
+
|
259
|
+
You can hook onto a callback on the `ApiModel::HttpRequest` class in order to perform tasks before, after or around an
|
260
|
+
API request. This is useful for logging requests. For example, if you wanted to add a custom NewRelic tracer, you could
|
261
|
+
add the following callback to make external API calls show up nicely in NewRelic:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
require 'new_relic/agent/method_tracer'
|
265
|
+
|
266
|
+
ApiModel::HttpRequest.class_eval do
|
267
|
+
include NewRelic::Agent::MethodTracer
|
268
|
+
around_run :trace_with_newrelic
|
269
|
+
|
270
|
+
def trace_with_newrelic
|
271
|
+
trace_execution_scoped(["API/#{self.method}/#{self.path}"]) do
|
272
|
+
yield
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
```
|
data/api-model.gemspec
CHANGED
@@ -2,7 +2,7 @@ $:.push File.expand_path("../lib", __FILE__)
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = "api-model"
|
5
|
-
s.version = "0.1.
|
5
|
+
s.version = "0.1.2"
|
6
6
|
s.authors = ["Damien Timewell"]
|
7
7
|
s.email = ["mail@damientimewell.com"]
|
8
8
|
s.homepage = "https://github.com/iZettle/api-model"
|
data/lib/api-model.rb
CHANGED
@@ -12,8 +12,9 @@ require 'api_model/response'
|
|
12
12
|
require 'api_model/class_methods'
|
13
13
|
require 'api_model/instance_methods'
|
14
14
|
require 'api_model/configuration'
|
15
|
-
require 'api_model/
|
15
|
+
require 'api_model/cache_stategy/no_cache'
|
16
16
|
require 'api_model/response_parser/json'
|
17
|
+
require 'api_model/builder/hash'
|
17
18
|
|
18
19
|
module ApiModel
|
19
20
|
Log = Logger.new STDOUT
|
@@ -6,8 +6,16 @@ module ApiModel
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def post_json(path, body=nil, options={})
|
9
|
+
call_api_with_json :post, path, body, options
|
10
|
+
end
|
11
|
+
|
12
|
+
def put_json(path, body=nil, options={})
|
13
|
+
call_api_with_json :put, path, body, options
|
14
|
+
end
|
15
|
+
|
16
|
+
def call_api_with_json(method, path, body=nil, options={})
|
9
17
|
body = body.to_json if body.is_a?(Hash)
|
10
|
-
call_api
|
18
|
+
call_api method, path, options.merge(body: body)
|
11
19
|
end
|
12
20
|
|
13
21
|
def call_api(method, path, options={})
|
@@ -1,5 +1,11 @@
|
|
1
1
|
module ApiModel
|
2
2
|
module InstanceMethods
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
extend ActiveModel::Callbacks
|
7
|
+
define_model_callbacks :save, :successful_save, :unsuccessful_save
|
8
|
+
end
|
3
9
|
|
4
10
|
# Overrides Hashie::Trash to catch errors from trying to set properties which have not been defined
|
5
11
|
# and defines it automatically
|
@@ -24,5 +30,55 @@ module ApiModel
|
|
24
30
|
end
|
25
31
|
end
|
26
32
|
|
33
|
+
# Convenience method to change attributes on an instance en-masse using a hash. This is
|
34
|
+
# useful for when an api response includes changed attributes and you want to update the current
|
35
|
+
# instance with the changes.
|
36
|
+
def update_attributes_from_hash(values={})
|
37
|
+
return unless values.present?
|
38
|
+
|
39
|
+
values.each do |key,value|
|
40
|
+
begin
|
41
|
+
public_send "#{key}=", value
|
42
|
+
rescue
|
43
|
+
Log.debug "Could not set #{key} on #{self.class.name}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sends a request to the api to update a resource. If the response was successful, then it will
|
49
|
+
# update the instance with any changes which the API has returned. If not, it will set ActiveModel
|
50
|
+
# errors.
|
51
|
+
#
|
52
|
+
# The default request type is PUT, but you can override this by setting ++:request_method++ in the
|
53
|
+
# options hash.
|
54
|
+
#
|
55
|
+
# It also includes 3 callbacks which you can hook onto; ++save++, which is the entire method, whether
|
56
|
+
# the API request was successful or not, and ++successful_save++ and ++unsuccessful_save++ which are
|
57
|
+
# triggered on successful or unsuccessful responses.
|
58
|
+
#
|
59
|
+
# By default it uses the ++ApiModel::Builder::Hash++ builder rather than using the normal method of
|
60
|
+
# using the class, or api config builders. This is to avoid building new objects from the response,
|
61
|
+
# but can be easily overriden by passing in ++:builder++ in the options hash.
|
62
|
+
def save(path, body=nil, options={})
|
63
|
+
request_method = options.delete(:request_method) || :put
|
64
|
+
|
65
|
+
run_callbacks :save do
|
66
|
+
response = self.class.call_api_with_json request_method, path, body, options.reverse_merge(builder: ApiModel::Builder::Hash.new)
|
67
|
+
response_success = response.http_response.api_call.success?
|
68
|
+
|
69
|
+
if response_success
|
70
|
+
run_callbacks :successful_save do
|
71
|
+
update_attributes_from_hash response.response_body
|
72
|
+
end
|
73
|
+
else
|
74
|
+
run_callbacks :unsuccessful_save do
|
75
|
+
set_errors_from_hash response.response_body["errors"]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
response_success
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
27
83
|
end
|
28
84
|
end
|
@@ -21,10 +21,20 @@ describe ApiModel do
|
|
21
21
|
post_request.http_response.request_method.should eq :post
|
22
22
|
end
|
23
23
|
|
24
|
+
it 'should be possible to send a PUT request' do
|
25
|
+
put_request = VCR.use_cassette('posts') { BlogPost.put_json "/post/1" }
|
26
|
+
put_request.http_response.request_method.should eq :put
|
27
|
+
end
|
28
|
+
|
24
29
|
it 'should be possible to send a POST request with a hash as body' do
|
25
30
|
post_request = VCR.use_cassette('posts') { BlogPost.post_json "/create_with_json", name: "foobarbaz" }
|
26
31
|
post_request.http_response.api_call.request.options[:body].should eq "{\"name\":\"foobarbaz\"}"
|
27
32
|
end
|
33
|
+
|
34
|
+
it 'should be possible to send a PUT request with a hash as body' do
|
35
|
+
post_request = VCR.use_cassette('posts') { BlogPost.put_json "/post/1", name: "foobarbaz" }
|
36
|
+
post_request.http_response.api_call.request.options[:body].should eq "{\"name\":\"foobarbaz\"}"
|
37
|
+
end
|
28
38
|
end
|
29
39
|
|
30
40
|
describe "retrieving a single object" do
|
@@ -148,6 +158,89 @@ describe ApiModel do
|
|
148
158
|
end
|
149
159
|
end
|
150
160
|
|
161
|
+
describe "updating attributes from a hash" do
|
162
|
+
let(:car) { Car.new }
|
163
|
+
|
164
|
+
it 'should change an existing attribute' do
|
165
|
+
car.name = "Chevvy"
|
166
|
+
expect {
|
167
|
+
car.update_attributes_from_hash name: "Ford"
|
168
|
+
}.to change{ car.name }.from("Chevvy").to("Ford")
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'should set an attribute if unset' do
|
172
|
+
expect {
|
173
|
+
car.update_attributes_from_hash number_of_doors: 2
|
174
|
+
}.to change{ car.number_of_doors }.from(nil).to(2)
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'should log if the attribute is not defined' do
|
178
|
+
ApiModel::Log.should_receive(:debug).with "Could not set age on Car"
|
179
|
+
car.update_attributes_from_hash age: 2
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "saving changes on an instance" do
|
184
|
+
before do
|
185
|
+
BlogPost.api_config { |config| config.host = "http://api-model-specs.com" }
|
186
|
+
end
|
187
|
+
|
188
|
+
let(:blog_post) { BlogPost.new }
|
189
|
+
|
190
|
+
# VCR will blow up if this was not a PUT, so no rspec expectations are needed here...
|
191
|
+
it 'should send a PUT request' do
|
192
|
+
VCR.use_cassette('posts') { blog_post.save "/post/1" }
|
193
|
+
end
|
194
|
+
|
195
|
+
# Same again here with VCR...
|
196
|
+
it 'should be possible to change the request type' do
|
197
|
+
VCR.use_cassette('posts') { blog_post.save "/post/update_with_post", nil, request_method: :post }
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'should be possible to send a JSON body in the same way a normal POST or PUT request would' do
|
201
|
+
VCR.use_cassette('posts') { blog_post.save "/post/2", name: "foobarbaz" }
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'should use #update_attributes_from_hash using the response body to update the instance' do
|
205
|
+
blog_post.should_receive(:update_attributes_from_hash).with "name" => "foobarbaz"
|
206
|
+
VCR.use_cassette('posts') { blog_post.save "/post/2", name: "foobarbaz" }
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'should set errors on the instance if the response contains an errors hash' do
|
210
|
+
expect {
|
211
|
+
VCR.use_cassette('posts') { blog_post.save "/post/with_errors", name: "" }
|
212
|
+
}.to change{ blog_post.errors.size }.from(0).to(1)
|
213
|
+
blog_post.errors[:name].should eq ["Cannot be blank"]
|
214
|
+
end
|
215
|
+
|
216
|
+
describe "callbacks" do
|
217
|
+
class BlogPost
|
218
|
+
after_save :saved
|
219
|
+
after_successful_save :yay_it_saved
|
220
|
+
after_unsuccessful_save :oh_no_it_didnt_save
|
221
|
+
|
222
|
+
def saved; end
|
223
|
+
def yay_it_saved; end
|
224
|
+
def oh_no_it_didnt_save; end
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'should run a callback around the whole save method' do
|
228
|
+
blog_post.should_receive(:saved).once
|
229
|
+
VCR.use_cassette('posts') { blog_post.save "/post/1" }
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'should run a callback around the handling of a successful response' do
|
233
|
+
blog_post.should_receive(:yay_it_saved).once
|
234
|
+
VCR.use_cassette('posts') { blog_post.save "/post/1" }
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'should run a callback around the handling of a unsuccessful response' do
|
238
|
+
blog_post.should_receive(:oh_no_it_didnt_save).once
|
239
|
+
VCR.use_cassette('posts') { blog_post.save "/post/with_errors", name: "" }
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
151
244
|
describe "cache_id" do
|
152
245
|
it 'should use options and the request path to create an identifier for the cache' do
|
153
246
|
BlogPost.cache_id("/box", params: { foo: "bar" }).should eq "/boxfoobar"
|
@@ -64,7 +64,7 @@ describe ApiModel, "Configuration" do
|
|
64
64
|
|
65
65
|
describe "cache_strategy" do
|
66
66
|
it 'should default to NoCache' do
|
67
|
-
ApiModel::Base.api_model_configuration.cache_strategy.should eq ApiModel::
|
67
|
+
ApiModel::Base.api_model_configuration.cache_strategy.should eq ApiModel::CacheStrategy::NoCache
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
@@ -54,6 +54,60 @@ http_interactions:
|
|
54
54
|
http_version:
|
55
55
|
recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
|
56
56
|
|
57
|
+
- request:
|
58
|
+
method: put
|
59
|
+
uri: http://api-model-specs.com/post/1
|
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: "{\"name\":\"something_else\"}"
|
81
|
+
http_version:
|
82
|
+
recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
|
83
|
+
|
84
|
+
- request:
|
85
|
+
method: post
|
86
|
+
uri: http://api-model-specs.com/post/update_with_post
|
87
|
+
headers:
|
88
|
+
User-Agent:
|
89
|
+
- Typhoeus - https://github.com/typhoeus/typhoeus
|
90
|
+
response:
|
91
|
+
status:
|
92
|
+
code: 200
|
93
|
+
message: OK
|
94
|
+
headers:
|
95
|
+
Server:
|
96
|
+
- nginx/1.4.1
|
97
|
+
Date:
|
98
|
+
- Thu, 28 Nov 2013 16:02:56 GMT
|
99
|
+
Content-Type:
|
100
|
+
- text/plain; charset=utf-8
|
101
|
+
Content-Length:
|
102
|
+
- '248'
|
103
|
+
Connection:
|
104
|
+
- keep-alive
|
105
|
+
body:
|
106
|
+
encoding: UTF-8
|
107
|
+
string: "{\"name\":\"something_else\"}"
|
108
|
+
http_version:
|
109
|
+
recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
|
110
|
+
|
57
111
|
- request:
|
58
112
|
method: get
|
59
113
|
uri: http://api-model-specs.com/single_post
|
@@ -110,4 +164,62 @@ http_interactions:
|
|
110
164
|
http_version:
|
111
165
|
recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
|
112
166
|
|
167
|
+
- request:
|
168
|
+
method: put
|
169
|
+
uri: http://api-model-specs.com/post/2
|
170
|
+
headers:
|
171
|
+
User-Agent:
|
172
|
+
- Typhoeus - https://github.com/typhoeus/typhoeus
|
173
|
+
body:
|
174
|
+
string: "{\"name\":\"foobarbaz\"}"
|
175
|
+
response:
|
176
|
+
status:
|
177
|
+
code: 200
|
178
|
+
message: OK
|
179
|
+
headers:
|
180
|
+
Server:
|
181
|
+
- nginx/1.4.1
|
182
|
+
Date:
|
183
|
+
- Thu, 28 Nov 2013 16:02:56 GMT
|
184
|
+
Content-Type:
|
185
|
+
- text/plain; charset=utf-8
|
186
|
+
Content-Length:
|
187
|
+
- '248'
|
188
|
+
Connection:
|
189
|
+
- keep-alive
|
190
|
+
body:
|
191
|
+
encoding: UTF-8
|
192
|
+
string: "{\"name\":\"foobarbaz\"}"
|
193
|
+
http_version:
|
194
|
+
recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
|
195
|
+
|
196
|
+
- request:
|
197
|
+
method: put
|
198
|
+
uri: http://api-model-specs.com/post/with_errors
|
199
|
+
headers:
|
200
|
+
User-Agent:
|
201
|
+
- Typhoeus - https://github.com/typhoeus/typhoeus
|
202
|
+
body:
|
203
|
+
string: "{\"name\":\"\"}"
|
204
|
+
response:
|
205
|
+
status:
|
206
|
+
code: 400
|
207
|
+
message: OK
|
208
|
+
headers:
|
209
|
+
Server:
|
210
|
+
- nginx/1.4.1
|
211
|
+
Date:
|
212
|
+
- Thu, 28 Nov 2013 16:02:56 GMT
|
213
|
+
Content-Type:
|
214
|
+
- text/plain; charset=utf-8
|
215
|
+
Content-Length:
|
216
|
+
- '248'
|
217
|
+
Connection:
|
218
|
+
- keep-alive
|
219
|
+
body:
|
220
|
+
encoding: UTF-8
|
221
|
+
string: "{\"errors\":{\"name\":\"Cannot be blank\"}}"
|
222
|
+
http_version:
|
223
|
+
recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
|
224
|
+
|
113
225
|
recorded_with: VCR 2.8.0
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api-model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Damien Timewell
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-01-
|
11
|
+
date: 2014-01-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -139,7 +139,8 @@ files:
|
|
139
139
|
- README.md
|
140
140
|
- api-model.gemspec
|
141
141
|
- lib/api-model.rb
|
142
|
-
- lib/api_model/
|
142
|
+
- lib/api_model/builder/hash.rb
|
143
|
+
- lib/api_model/cache_stategy/no_cache.rb
|
143
144
|
- lib/api_model/class_methods.rb
|
144
145
|
- lib/api_model/configuration.rb
|
145
146
|
- lib/api_model/http_request.rb
|
@@ -182,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
182
183
|
version: '0'
|
183
184
|
requirements: []
|
184
185
|
rubyforge_project:
|
185
|
-
rubygems_version: 2.
|
186
|
+
rubygems_version: 2.2.1
|
186
187
|
signing_key:
|
187
188
|
specification_version: 4
|
188
189
|
summary: A simple way of interacting with rest APIs
|