api-model 0.1.3 → 1.0.0

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: b46c6c4b3e683a5e7c5a3577611b72fb7b398e8e
4
- data.tar.gz: 89a47c86aecc1c2b66e1566511b2939335d2e385
3
+ metadata.gz: 11061671355750be204f15d713db7d0c7086dffb
4
+ data.tar.gz: adc621e2d2c3a14aa0fc0506f95be9b4ac56244c
5
5
  SHA512:
6
- metadata.gz: dab983c5d688a24d9916f7a2a73073a3b63e59d8cae7296e3b4eb554081f031f03b375690768258507fb1d07c7ceacb7779cd3f3a50ca780b87866627eb2c804
7
- data.tar.gz: fb188b0fed0aeb683e9c9c3e84fe979e79d0045c766e81b115c69bbf03c951ca396289955154e5de92ab17d8b4f31831a7a2ef2f280ae8b23b1226ed0309d6e8
6
+ metadata.gz: 6fe21b33dcb485f05b08c1da02c264a27e0e757e408aa72b39cbf178003a84b46d61d05585b6157e088c8fd614602bcf603f1fbe21d142aa340fde379101082b
7
+ data.tar.gz: 0a0e470c1ed7274175ea78e1cd00d3e5f529bedb9350e083a4df6403ddf32c6d25e095a9c062ddf96a7d74f5ce11d38ce5c27227dbb6145e2d4af0cb69ea1662
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- api-model (0.1.3)
4
+ api-model (1.0.0)
5
5
  activemodel
6
6
  activesupport
7
7
  hashie
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.3"
5
+ s.version = "1.0.0"
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
@@ -6,6 +6,8 @@ require 'hashie'
6
6
  require 'typhoeus'
7
7
  require 'ostruct'
8
8
 
9
+ require 'api_model/core_extensions/hash'
10
+
9
11
  require 'api_model/assignment'
10
12
  require 'api_model/initializer'
11
13
  require 'api_model/http_request'
@@ -3,7 +3,8 @@ module ApiModel
3
3
  include Initializer
4
4
 
5
5
  attr_accessor :host, :json_root, :headers, :raise_on_unauthenticated, :cache_settings,
6
- :raise_on_not_found, :cache_strategy, :parser, :builder, :raise_on_server_error
6
+ :raise_on_not_found, :cache_strategy, :parser, :builder, :raise_on_server_error,
7
+ :json_errors_root
7
8
 
8
9
  def self.from_inherited_config(config)
9
10
  new config.instance_values.reject {|k,v| v.blank? }
@@ -26,6 +27,10 @@ module ApiModel
26
27
  @cache_settings ||= {}
27
28
  @cache_settings.reverse_merge duration: 30.seconds, timeout: 2.seconds
28
29
  end
30
+
31
+ def json_errors_root
32
+ @json_errors_root ||= "errors"
33
+ end
29
34
  end
30
35
 
31
36
  module ConfigurationMethods
@@ -0,0 +1,16 @@
1
+ # When using ApiModel in a Rails application, Rails will try to ascertain what type of class
2
+ # an instance of ApiModel::Base is when attempting to build a polymorphic route. Since ApiModel::Base
3
+ # inherits from Hashie, which in turn inherits from Hash, Rails thinks that an instance of
4
+ # ApiModel::Base is in fact a hash instead of acting like an ActiveModel, which then causes
5
+ # it to fail to compute a route for the class.
6
+ #
7
+ # This is a hacky workaround, but should not interfere with any core functionality since it just
8
+ # calls super if the class is not a subclass of ApiModel::Base.
9
+ Hash.class_eval do
10
+
11
+ def self.===(klass)
12
+ return false if klass.class.ancestors.include? ApiModel::Base
13
+ super klass
14
+ end
15
+
16
+ end
@@ -14,7 +14,7 @@ module ApiModel
14
14
 
15
15
  def run
16
16
  run_callbacks :run do
17
- Log.debug "#{method.to_s.upcase} #{full_path} #{options}"
17
+ Log.debug "#{method.to_s.upcase} #{full_path} with headers: #{options[:headers]}"
18
18
  self.api_call = Typhoeus.send method, full_path, options
19
19
  Response.new self, config
20
20
  end
@@ -5,6 +5,9 @@ module ApiModel
5
5
  included do
6
6
  extend ActiveModel::Callbacks
7
7
  define_model_callbacks :save, :successful_save, :unsuccessful_save
8
+
9
+ property :persisted, default: false
10
+ alias_method :persisted?, :persisted
8
11
  end
9
12
 
10
13
  # Overrides Hashie::Trash to catch errors from trying to set properties which have not been defined
@@ -46,6 +49,7 @@ module ApiModel
46
49
  # but can be easily overriden by passing in ++:builder++ in the options hash.
47
50
  def save(path, body=nil, options={})
48
51
  request_method = options.delete(:request_method) || :put
52
+ errors_root = options.delete(:json_errors_root) || self.class.api_model_configuration.json_errors_root
49
53
 
50
54
  run_callbacks :save do
51
55
  response = self.class.call_api_with_json request_method, path, body, options.reverse_merge(builder: ApiModel::Builder::Hash.new)
@@ -57,7 +61,7 @@ module ApiModel
57
61
  end
58
62
  else
59
63
  run_callbacks :unsuccessful_save do
60
- set_errors_from_hash response.response_body["errors"]
64
+ set_errors_from_hash response.fetch_from_body(errors_root)
61
65
  end
62
66
  end
63
67
 
@@ -22,7 +22,7 @@ module ApiModel
22
22
  raise UnauthenticatedError if @_config.raise_on_unauthenticated && http_response.api_call.response_code == 401
23
23
  raise NotFoundError if @_config.raise_on_not_found && http_response.api_call.response_code == 404
24
24
  raise ServerError if @_config.raise_on_server_error && http_response.api_call.response_code == 500
25
- return if response_body.nil?
25
+ return self if response_body.nil?
26
26
 
27
27
  if response_build_hash.is_a? Array
28
28
  self.objects = response_build_hash.collect{ |hash| build http_response.builder, hash }
@@ -59,23 +59,32 @@ module ApiModel
59
59
  RUBY_EVAL
60
60
  end
61
61
 
62
+ # Pass though any method which is not defined to the built objects. This makes the response class
63
+ # quite transparent, and keeps the response acting like the built object, or array of objects.
62
64
  def method_missing(method_name, *args, &block)
63
65
  objects.send method_name, *args, &block
64
66
  end
65
67
 
68
+ # Uses a string notation split by colons to fetch nested keys from a hash. For example, if you have a hash
69
+ # which looks like:
70
+ #
71
+ # { foo: { bar: { baz: "Hello world" } } }
72
+ #
73
+ # Then calling ++fetch_from_body("foo.bar.baz")++ would return "Hello world"
74
+ def fetch_from_body(key_reference)
75
+ key_reference.split(".").inject(response_body) do |hash,key|
76
+ hash.fetch(key)
77
+ end
78
+ end
79
+
66
80
  private
67
81
 
68
82
  # If the model config defines a json root, use it on the response_body
69
83
  # to dig down in to the hash.
70
- #
71
- # The root for a deeply nested hash will come in as a string with key names split
72
- # with a colon.
73
84
  def response_build_hash
74
85
  if @_config.json_root.present?
75
86
  begin
76
- @_config.json_root.split(".").inject(response_body) do |hash,key|
77
- hash.fetch(key)
78
- end
87
+ fetch_from_body @_config.json_root
79
88
  rescue
80
89
  raise ResponseBuilderError, "Could not find key #{@_config.json_root} in:\n#{response_body}"
81
90
  end
@@ -183,6 +183,10 @@ describe ApiModel do
183
183
  BlogPost.api_config { |config| config.host = "http://api-model-specs.com" }
184
184
  end
185
185
 
186
+ after do
187
+ BlogPost.reset_api_configuration
188
+ end
189
+
186
190
  let(:blog_post) { BlogPost.new }
187
191
 
188
192
  # VCR will blow up if this was not a PUT, so no rspec expectations are needed here...
@@ -211,6 +215,22 @@ describe ApiModel do
211
215
  blog_post.errors[:name].should eq ["Cannot be blank"]
212
216
  end
213
217
 
218
+ it 'should be possible to change the error root when making the save call' do
219
+ expect {
220
+ VCR.use_cassette('posts') { blog_post.save "/post/with_nested_errors", {name: ""}, json_errors_root: "result.errors" }
221
+ }.to change{ blog_post.errors.size }.from(0).to(1)
222
+ blog_post.errors[:name].should eq ["Cannot be blank"]
223
+ end
224
+
225
+ it 'should respect the class default error root if one was not defined in the save call' do
226
+ BlogPost.api_config { |c| c.json_errors_root = "hello.errors" }
227
+
228
+ expect {
229
+ VCR.use_cassette('posts') { blog_post.save "/post/with_different_nested_errors", name: "" }
230
+ }.to change{ blog_post.errors.size }.from(0).to(1)
231
+ blog_post.errors[:name].should eq ["Cannot be blank"]
232
+ end
233
+
214
234
  describe "callbacks" do
215
235
  class BlogPost
216
236
  after_save :saved
@@ -239,6 +259,18 @@ describe ApiModel do
239
259
  end
240
260
  end
241
261
 
262
+ describe "persistance" do
263
+ it 'should not be persisted by default' do
264
+ BlogPost.new.persisted?.should be false
265
+ end
266
+
267
+ it 'should be posible to set an instance as persisted' do
268
+ blog_post = BlogPost.new
269
+ blog_post.persisted = true
270
+ blog_post.persisted?.should be_true
271
+ end
272
+ end
273
+
242
274
  describe "cache_id" do
243
275
  it 'should use options and the request path to create an identifier for the cache' do
244
276
  BlogPost.cache_id("/box", params: { foo: "bar" }).should eq "/boxfoobar"
@@ -259,4 +291,10 @@ describe ApiModel do
259
291
  end
260
292
  end
261
293
 
294
+ describe "class equality" do
295
+ it 'should not be equal to a Hash even though it is technically a Hash subclass' do
296
+ (Hash === BlogPost.new).should be_false
297
+ end
298
+ end
299
+
262
300
  end
@@ -36,6 +36,17 @@ describe ApiModel, "Configuration" do
36
36
  end
37
37
  end
38
38
 
39
+ describe "json_errors_root" do
40
+ it 'should default to "errors"' do
41
+ Banana.api_model_configuration.json_errors_root.should eq "errors"
42
+ end
43
+
44
+ it 'should be possible to set on a class' do
45
+ Banana.api_config { |c| c.json_errors_root = "some.deep.errors" }
46
+ Banana.api_model_configuration.json_errors_root.should eq "some.deep.errors"
47
+ end
48
+ end
49
+
39
50
  describe "headers" do
40
51
  it 'should create default headers for content type and accepts' do
41
52
  headers = Banana.api_model_configuration.headers
@@ -56,10 +67,16 @@ describe ApiModel, "Configuration" do
56
67
  headers.should have_key "Content-Type"
57
68
  end
58
69
 
59
- it 'should be possible to override default headers' do
70
+ it 'should be possible to override default headers through ApiModel::Base' do
60
71
  ApiModel::Base.api_config { |config| config.headers = { "Accept" => "image/gif" } }
61
72
  Banana.api_model_configuration.headers["Accept"].should eq "image/gif"
62
73
  end
74
+
75
+ it 'should be possible to override custom headers through subclasses of ApiModel::Base' do
76
+ ApiModel::Base.api_config { |config| config.headers = { "Accept" => "image/gif" } }
77
+ Banana.api_config { |config| config.headers = { "Accept" => "application/x-www-form-urlencoded" } }
78
+ Banana.api_model_configuration.headers["Accept"].should eq "application/x-www-form-urlencoded"
79
+ end
63
80
  end
64
81
 
65
82
  describe "cache_strategy" do
@@ -222,4 +222,62 @@ http_interactions:
222
222
  http_version:
223
223
  recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
224
224
 
225
+ - request:
226
+ method: put
227
+ uri: http://api-model-specs.com/post/with_nested_errors
228
+ headers:
229
+ User-Agent:
230
+ - Typhoeus - https://github.com/typhoeus/typhoeus
231
+ body:
232
+ string: "{\"name\":\"\"}"
233
+ response:
234
+ status:
235
+ code: 400
236
+ message: OK
237
+ headers:
238
+ Server:
239
+ - nginx/1.4.1
240
+ Date:
241
+ - Thu, 28 Nov 2013 16:02:56 GMT
242
+ Content-Type:
243
+ - text/plain; charset=utf-8
244
+ Content-Length:
245
+ - '248'
246
+ Connection:
247
+ - keep-alive
248
+ body:
249
+ encoding: UTF-8
250
+ string: "{\"result\":{\"errors\":{\"name\":\"Cannot be blank\"}}}"
251
+ http_version:
252
+ recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
253
+
254
+ - request:
255
+ method: put
256
+ uri: http://api-model-specs.com/post/with_different_nested_errors
257
+ headers:
258
+ User-Agent:
259
+ - Typhoeus - https://github.com/typhoeus/typhoeus
260
+ body:
261
+ string: "{\"name\":\"\"}"
262
+ response:
263
+ status:
264
+ code: 400
265
+ message: OK
266
+ headers:
267
+ Server:
268
+ - nginx/1.4.1
269
+ Date:
270
+ - Thu, 28 Nov 2013 16:02:56 GMT
271
+ Content-Type:
272
+ - text/plain; charset=utf-8
273
+ Content-Length:
274
+ - '248'
275
+ Connection:
276
+ - keep-alive
277
+ body:
278
+ encoding: UTF-8
279
+ string: "{\"hello\":{\"errors\":{\"name\":\"Cannot be blank\"}}}"
280
+ http_version:
281
+ recorded_at: Thu, 28 Nov 2013 16:02:20 GMT
282
+
225
283
  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.3
4
+ version: 1.0.0
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-14 00:00:00.000000000 Z
11
+ date: 2014-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -144,6 +144,7 @@ files:
144
144
  - lib/api_model/cache_stategy/no_cache.rb
145
145
  - lib/api_model/class_methods.rb
146
146
  - lib/api_model/configuration.rb
147
+ - lib/api_model/core_extensions/hash.rb
147
148
  - lib/api_model/http_request.rb
148
149
  - lib/api_model/initializer.rb
149
150
  - lib/api_model/instance_methods.rb