api-model 0.1.1 → 0.1.2

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 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