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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: af939e7e3edf3d3f1e9236a531ec288140df7e84
4
- data.tar.gz: e01192448c015b9a0a02c8e9b801741db322e078
3
+ metadata.gz: 820bf511fd4b03fc1a8d9523717d6f5a880f4ed5
4
+ data.tar.gz: c412f230bde85508e50b9ccb4615b79522b8d70e
5
5
  SHA512:
6
- metadata.gz: 53e12bde27910eb5115106868b1cfc6b9897c47d7c8e0a53c42b493cb6c3eca3c5be403fe30b051aa2b3cf5ac190c0c207fe7ec6509d689f978e547cd6ada548
7
- data.tar.gz: 8ed44b655edc400dfc2f263ba5bae51f3ab874b61725b18b92709863fccfb698d2b0695a3f169f1ea17a73d65ecfdd632406666e97efc4603c39e0447b0e5b57
6
+ metadata.gz: 4300025629a98a9c70f6c8f81cb9b0311f5a439256bdf0a5a68d3ba0595fc67049af6e7fb17add9452b5cbb875feb221b755aa690764186ce28f00c8c79d46a9
7
+ data.tar.gz: fbf370237c20cbc647435cf72feb51d5732cc9bf4f78aaa67dfebf247d7798bba4cd72c56e527ab994e1c73e5862d9ea9e99f6fbb570220bee1753935f915cf0
data/.rspec CHANGED
@@ -1,2 +1,2 @@
1
1
  --color
2
- --format documentation
2
+ --format progress
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- api-model (0.1.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.5)
56
- ethon (~> 0.6.1)
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
+ ```
@@ -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.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"
@@ -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/cache_stategies/no_cache'
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
@@ -0,0 +1,11 @@
1
+ module ApiModel
2
+ module Builder
3
+ class Hash
4
+
5
+ def build(response)
6
+ response if response.is_a?(Hash)
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,5 @@
1
1
  module ApiModel
2
- module CacheStrategies
2
+ module CacheStrategy
3
3
  class NoCache
4
4
 
5
5
  def initialize(*args)
@@ -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 :post, path, options.merge(body: body)
18
+ call_api method, path, options.merge(body: body)
11
19
  end
12
20
 
13
21
  def call_api(method, path, options={})
@@ -15,7 +15,7 @@ module ApiModel
15
15
  end
16
16
 
17
17
  def cache_strategy
18
- @cache_strategy ||= ApiModel::CacheStrategies::NoCache
18
+ @cache_strategy ||= ApiModel::CacheStrategy::NoCache
19
19
  end
20
20
 
21
21
  def parser
@@ -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::CacheStrategies::NoCache
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.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-07 00:00:00.000000000 Z
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/cache_stategies/no_cache.rb
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.0.6
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