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 +4 -4
- data/Gemfile.lock +1 -1
- data/api-model.gemspec +1 -1
- data/lib/api-model.rb +2 -0
- data/lib/api_model/configuration.rb +6 -1
- data/lib/api_model/core_extensions/hash.rb +16 -0
- data/lib/api_model/http_request.rb +1 -1
- data/lib/api_model/instance_methods.rb +5 -1
- data/lib/api_model/response.rb +16 -7
- data/spec/api-model/api_model_spec.rb +38 -0
- data/spec/api-model/configuration_spec.rb +18 -1
- data/spec/support/fixtures/posts.yml +58 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 11061671355750be204f15d713db7d0c7086dffb
|
4
|
+
data.tar.gz: adc621e2d2c3a14aa0fc0506f95be9b4ac56244c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6fe21b33dcb485f05b08c1da02c264a27e0e757e408aa72b39cbf178003a84b46d61d05585b6157e088c8fd614602bcf603f1fbe21d142aa340fde379101082b
|
7
|
+
data.tar.gz: 0a0e470c1ed7274175ea78e1cd00d3e5f529bedb9350e083a4df6403ddf32c6d25e095a9c062ddf96a7d74f5ce11d38ce5c27227dbb6145e2d4af0cb69ea1662
|
data/Gemfile.lock
CHANGED
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.
|
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
@@ -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.
|
64
|
+
set_errors_from_hash response.fetch_from_body(errors_root)
|
61
65
|
end
|
62
66
|
end
|
63
67
|
|
data/lib/api_model/response.rb
CHANGED
@@ -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
|
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.
|
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-
|
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
|