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