api-model 0.1.3 → 1.0.0

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