api-model 0.0.2 → 0.0.3
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/.travis.yml +5 -0
- data/Gemfile.lock +7 -5
- data/README.md +3 -0
- data/api-model.gemspec +2 -1
- data/lib/api-model.rb +28 -22
- data/lib/api_model/configuration.rb +39 -0
- data/lib/api_model/http_request.rb +20 -7
- data/lib/api_model/initializer.rb +4 -3
- data/lib/api_model/response.rb +34 -9
- data/lib/api_model/rest_methods.rb +21 -0
- data/spec/api-model/api_model_spec.rb +130 -0
- data/spec/api-model/configuration_spec.rb +78 -0
- data/spec/api-model/http_request_spec.rb +74 -0
- data/spec/{lib → api-model}/initializer_spec.rb +5 -5
- data/spec/api-model/response_spec.rb +186 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/fixtures/cars.yml +84 -0
- data/spec/support/fixtures/errors.yml +57 -0
- data/spec/support/fixtures/posts.yml +56 -0
- data/spec/support/fixtures/users.yml +57 -0
- data/spec/support/mock_models/banana.rb +3 -6
- data/spec/support/mock_models/blog_post.rb +2 -1
- data/spec/support/mock_models/car.rb +11 -0
- data/spec/support/mock_models/multiple_hosts.rb +6 -2
- data/spec/support/mock_models/user.rb +4 -0
- metadata +39 -12
- data/spec/lib/api_host_spec.rb +0 -20
- data/spec/lib/api_model_spec.rb +0 -34
- data/spec/lib/http_request_spec.rb +0 -43
- data/spec/lib/response_spec.rb +0 -89
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 75b2ddb171371b895e8ecf655cfd69a56368738b
|
4
|
+
data.tar.gz: a0165241ef3855d10e044d9cdc8b821e3114a6a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c377a0408bcc44930c807b07ef4274bf15ab5ddb6db7e790c421e39c8ec4dcfeaf71496c3fd0d57188c0d6821b32646a5b8c96dafdf8e9166ff4e00e34510fb4
|
7
|
+
data.tar.gz: 480a76b37a1800abcbccc9da0365cda8a8332af301e5e997d0b281a451403e7339a7a93433dd9ea814ca4b6113aa26b5fe287b1a5e2a3b8ff1e6aa6937cbd8a8
|
data/.travis.yml
ADDED
data/Gemfile.lock
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
api-model (0.0.
|
4
|
+
api-model (0.0.3)
|
5
5
|
activemodel
|
6
6
|
activesupport
|
7
|
+
hashie
|
7
8
|
typhoeus
|
8
9
|
|
9
10
|
GEM
|
10
11
|
remote: https://rubygems.org/
|
11
12
|
specs:
|
12
|
-
activemodel (4.0.
|
13
|
-
activesupport (= 4.0.
|
13
|
+
activemodel (4.0.2)
|
14
|
+
activesupport (= 4.0.2)
|
14
15
|
builder (~> 3.1.0)
|
15
|
-
activesupport (4.0.
|
16
|
+
activesupport (4.0.2)
|
16
17
|
i18n (~> 0.6, >= 0.6.4)
|
17
18
|
minitest (~> 4.2)
|
18
19
|
multi_json (~> 1.3)
|
@@ -29,7 +30,8 @@ GEM
|
|
29
30
|
ffi (>= 1.3.0)
|
30
31
|
mime-types (~> 1.18)
|
31
32
|
ffi (1.9.0)
|
32
|
-
|
33
|
+
hashie (2.0.5)
|
34
|
+
i18n (0.6.9)
|
33
35
|
method_source (0.8.2)
|
34
36
|
mime-types (1.25.1)
|
35
37
|
minitest (4.7.5)
|
data/README.md
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.0.
|
5
|
+
s.version = "0.0.3"
|
6
6
|
s.authors = ["Damien Timewell"]
|
7
7
|
s.email = ["mail@damientimewell.com"]
|
8
8
|
s.homepage = "https://github.com/iZettle/api-model"
|
@@ -13,6 +13,7 @@ Gem::Specification.new do |s|
|
|
13
13
|
s.add_dependency 'activesupport'
|
14
14
|
s.add_dependency 'activemodel'
|
15
15
|
s.add_dependency 'typhoeus'
|
16
|
+
s.add_dependency 'hashie'
|
16
17
|
|
17
18
|
s.add_development_dependency "rspec"
|
18
19
|
s.add_development_dependency "pry"
|
data/lib/api-model.rb
CHANGED
@@ -2,42 +2,48 @@ require 'active_model'
|
|
2
2
|
require 'active_support'
|
3
3
|
require 'active_support/core_ext'
|
4
4
|
require 'logger'
|
5
|
+
require 'hashie'
|
5
6
|
|
6
7
|
require 'api_model/initializer'
|
7
8
|
require 'api_model/http_request'
|
8
9
|
require 'api_model/response'
|
10
|
+
require 'api_model/rest_methods'
|
11
|
+
require 'api_model/configuration'
|
9
12
|
|
10
13
|
module ApiModel
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
Log = Logger.new STDOUT
|
15
|
+
|
16
|
+
class ResponseBuilderError < StandardError; end
|
17
|
+
class UnauthenticatedError < StandardError; end
|
18
|
+
class NotFoundError < StandardError; end
|
19
|
+
|
20
|
+
if defined?(Rails)
|
21
|
+
class Railtie < Rails::Railtie
|
22
|
+
initializer "api-model" do
|
23
|
+
ApiModel.send :remove_const, :Log
|
24
|
+
ApiModel::Log = Rails.logger
|
25
|
+
end
|
26
|
+
end
|
16
27
|
end
|
17
28
|
|
18
|
-
class Base
|
29
|
+
class Base < Hashie::Trash
|
19
30
|
include ActiveModel::Conversion
|
20
31
|
include ActiveModel::Validations
|
32
|
+
include ActiveModel::Serialization
|
21
33
|
extend ActiveModel::Naming
|
22
34
|
extend ActiveModel::Callbacks
|
23
35
|
|
24
|
-
|
36
|
+
extend RestMethods
|
37
|
+
include ConfigurationMethods
|
25
38
|
|
26
|
-
|
27
|
-
|
39
|
+
# Overrides Hashie::Trash to catch errors from trying to set properties which have not been defined
|
40
|
+
# and defines it automatically
|
41
|
+
def property_exists?(property_name)
|
42
|
+
super property_name
|
43
|
+
rescue NoMethodError
|
44
|
+
Log.debug "Could not set #{property_name} on #{self.class.name}. Defining it now."
|
45
|
+
self.class.property property_name.to_sym
|
28
46
|
end
|
29
|
-
|
30
|
-
def self.api_host
|
31
|
-
@api_host || ""
|
32
|
-
end
|
33
|
-
|
34
|
-
def self.get_json(path, options={})
|
35
|
-
# TODO - tidy this up...
|
36
|
-
builder = options.delete(:builder) || self
|
37
|
-
options[:api_host] = api_host
|
38
|
-
|
39
|
-
HttpRequest.run(options.merge(path: path)).build_objects builder
|
40
|
-
end
|
41
|
-
|
42
47
|
end
|
48
|
+
|
43
49
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ApiModel
|
2
|
+
class Configuration
|
3
|
+
include Initializer
|
4
|
+
|
5
|
+
attr_accessor :host, :json_root, :headers, :raise_on_unauthenticated, :raise_on_not_found
|
6
|
+
|
7
|
+
def self.from_inherited_config(config)
|
8
|
+
new config.instance_values.reject {|k,v| v.blank? }
|
9
|
+
end
|
10
|
+
|
11
|
+
def headers
|
12
|
+
@headers ||= {}
|
13
|
+
@headers.reverse_merge "Content-Type" => "application/json; charset=utf-8", "Accept" => "application/json"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ConfigurationMethods
|
18
|
+
extend ActiveSupport::Concern
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
|
22
|
+
def reset_api_configuration
|
23
|
+
@_api_config = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def api_model_configuration
|
27
|
+
@_api_config || superclass.api_model_configuration
|
28
|
+
rescue
|
29
|
+
@_api_config = Configuration.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def api_config
|
33
|
+
@_api_config = Configuration.from_inherited_config api_model_configuration
|
34
|
+
yield @_api_config
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -2,15 +2,17 @@ module ApiModel
|
|
2
2
|
class HttpRequest
|
3
3
|
include ApiModel::Initializer
|
4
4
|
|
5
|
-
attr_accessor :path, :method, :options, :api_call, :
|
5
|
+
attr_accessor :path, :method, :options, :api_call, :builder, :config
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
after_initialize :set_default_options
|
8
|
+
|
9
|
+
define_model_callbacks :run
|
10
10
|
|
11
11
|
def run
|
12
|
-
|
13
|
-
|
12
|
+
run_callbacks :run do
|
13
|
+
self.api_call = Typhoeus.send method, full_path, options
|
14
|
+
Response.new self, config
|
15
|
+
end
|
14
16
|
end
|
15
17
|
|
16
18
|
def method
|
@@ -23,7 +25,18 @@ module ApiModel
|
|
23
25
|
|
24
26
|
def full_path
|
25
27
|
return path if path =~ /^http/
|
26
|
-
"#{
|
28
|
+
"#{config.host}#{path}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def request_method
|
32
|
+
api_call.request.original_options[:method]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def set_default_options
|
38
|
+
options[:headers] ||= {}
|
39
|
+
options[:headers].reverse_merge! config.headers if config.try(:headers)
|
27
40
|
end
|
28
41
|
|
29
42
|
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
module ApiModel
|
2
2
|
module Initializer
|
3
|
+
extend ActiveSupport::Concern
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
5
|
+
included do
|
6
|
+
extend ActiveModel::Callbacks
|
7
|
+
define_model_callbacks :initialize
|
7
8
|
end
|
8
9
|
|
9
10
|
def initialize(values={})
|
data/lib/api_model/response.rb
CHANGED
@@ -9,16 +9,20 @@ module ApiModel
|
|
9
9
|
|
10
10
|
attr_accessor :http_response, :objects
|
11
11
|
|
12
|
-
def initialize(http_response)
|
12
|
+
def initialize(http_response, config)
|
13
13
|
@http_response = http_response
|
14
|
+
@_config = config || Configuration.new
|
14
15
|
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
if
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
def build_objects
|
18
|
+
raise UnauthenticatedError if @_config.raise_on_unauthenticated && http_response.api_call.response_code == 401
|
19
|
+
raise NotFoundError if @_config.raise_on_not_found && http_response.api_call.response_code == 404
|
20
|
+
return if json_response_body.nil?
|
21
|
+
|
22
|
+
if response_build_hash.is_a? Array
|
23
|
+
self.objects = response_build_hash.collect{ |hash| build http_response.builder, hash }
|
24
|
+
elsif response_build_hash.is_a? Hash
|
25
|
+
self.objects = self.build http_response.builder, response_build_hash
|
22
26
|
end
|
23
27
|
|
24
28
|
self
|
@@ -33,9 +37,9 @@ module ApiModel
|
|
33
37
|
end
|
34
38
|
|
35
39
|
def json_response_body
|
36
|
-
JSON.parse http_response.body
|
40
|
+
@json_response_body ||= JSON.parse http_response.api_call.body
|
37
41
|
rescue JSON::ParserError
|
38
|
-
Log.info "Could not parse JSON response: #{http_response.body}"
|
42
|
+
Log.info "Could not parse JSON response: #{http_response.api_call.body}"
|
39
43
|
return nil
|
40
44
|
end
|
41
45
|
|
@@ -53,5 +57,26 @@ module ApiModel
|
|
53
57
|
objects.send method_name, *args, &block
|
54
58
|
end
|
55
59
|
|
60
|
+
private
|
61
|
+
|
62
|
+
# If the model config defines a json root, use it on the json_response_body
|
63
|
+
# to dig down in to the hash.
|
64
|
+
#
|
65
|
+
# The root for a deeply nested hash will come in as a string with key names split
|
66
|
+
# with a colon.
|
67
|
+
def response_build_hash
|
68
|
+
if @_config.json_root.present?
|
69
|
+
begin
|
70
|
+
@_config.json_root.split(".").inject(json_response_body) do |hash,key|
|
71
|
+
hash.fetch(key)
|
72
|
+
end
|
73
|
+
rescue
|
74
|
+
raise ResponseBuilderError, "Could not find key #{@_config.json_root} in:\n#{json_response_body}"
|
75
|
+
end
|
76
|
+
else
|
77
|
+
json_response_body
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
56
81
|
end
|
57
82
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ApiModel
|
2
|
+
module RestMethods
|
3
|
+
|
4
|
+
def get_json(path, params={}, options={})
|
5
|
+
call_api :get, path, options.merge(params: params)
|
6
|
+
end
|
7
|
+
|
8
|
+
def post_json(path, body=nil, options={})
|
9
|
+
body = body.to_json if body.is_a?(Hash)
|
10
|
+
call_api :post, path, options.merge(body: body)
|
11
|
+
end
|
12
|
+
|
13
|
+
def call_api(method, path, options={})
|
14
|
+
request = HttpRequest.new path: path, method: method, config: api_model_configuration
|
15
|
+
request.builder = options.delete(:builder) || self
|
16
|
+
request.options.merge! options
|
17
|
+
request.run.build_objects
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'support/mock_models/blog_post'
|
3
|
+
require 'support/mock_models/car'
|
4
|
+
|
5
|
+
describe ApiModel do
|
6
|
+
|
7
|
+
describe "sending different types of requests" do
|
8
|
+
before do
|
9
|
+
BlogPost.api_config do |config|
|
10
|
+
config.host = "http://api-model-specs.com"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should be possible to send a GET request" do
|
15
|
+
get_request = VCR.use_cassette('posts') { BlogPost.get_json "/single_post" }
|
16
|
+
get_request.http_response.request_method.should eq :get
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should be possible to send a POST request" do
|
20
|
+
post_request = VCR.use_cassette('posts') { BlogPost.post_json "/posts" }
|
21
|
+
post_request.http_response.request_method.should eq :post
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should be possible to send a POST request with a hash as body' do
|
25
|
+
post_request = VCR.use_cassette('posts') { BlogPost.post_json "/create_with_json", name: "foobarbaz" }
|
26
|
+
post_request.http_response.api_call.request.options[:body].should eq "{\"name\":\"foobarbaz\"}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "retrieving a single object" do
|
31
|
+
describe "with the default builder" do
|
32
|
+
let(:blog_post) do
|
33
|
+
VCR.use_cassette('posts') do
|
34
|
+
BlogPost.get_json "http://api-model-specs.com/single_post"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should run the request and objectify the response hash" do
|
39
|
+
blog_post.should be_a(BlogPost)
|
40
|
+
blog_post.name.should eq "foo"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "with a custom builder" do
|
45
|
+
let(:custom_built_blog_post) do
|
46
|
+
VCR.use_cassette('posts') do
|
47
|
+
BlogPost.get_json "http://api-model-specs.com/single_post", {}, builder: BlogPost::CustomBuilder.new
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should be possible to use a custom builder class when objectifing" do
|
52
|
+
custom_built_blog_post.should be_a(BlogPost)
|
53
|
+
custom_built_blog_post.title.should eq "FOOBAR"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "using Hashie to build with properties" do
|
59
|
+
describe "with a single object response" do
|
60
|
+
let :car do
|
61
|
+
VCR.use_cassette('cars') { Car.get_json "http://cars.com/one_convertable" }
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should build the correct object' do
|
65
|
+
car.should be_a(Car)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should correctly rename properties' do
|
69
|
+
car.number_of_doors.should eq 2
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should correctly transform properties' do
|
73
|
+
car.top_speed.should eq 600
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should let you define custom methods as normal' do
|
77
|
+
car.is_fast?.should be_true
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should let you respect default values for properties' do
|
81
|
+
car.name.should eq "Ferrari"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "with a collection of objects response" do
|
86
|
+
let :cars do
|
87
|
+
VCR.use_cassette('cars') { Car.get_json "http://cars.com/fast_ones" }
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'should build an array of the correct objects' do
|
91
|
+
cars.should be_a(Array)
|
92
|
+
cars.collect { |car| car.should be_a(Car) }
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'should correctly rename properties' do
|
96
|
+
cars.last.number_of_doors.should eq 4
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should correctly transform properties' do
|
100
|
+
cars.last.top_speed.should eq 300
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should let you define custom methods as normal' do
|
104
|
+
cars.last.is_fast?.should be_false
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should respect default values for properties, but also override them' do
|
108
|
+
cars.first.name.should eq "Ferrari"
|
109
|
+
cars.last.name.should eq "Ford"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "with a single object which has properties which are undefined" do
|
114
|
+
let :new_car do
|
115
|
+
VCR.use_cassette('cars') { Car.get_json "http://cars.com/new_model" }
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should not raise an exception" do
|
119
|
+
expect {
|
120
|
+
new_car
|
121
|
+
}.to_not raise_error
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'should define the missing property on the fly' do
|
125
|
+
new_car.shiney.should eq true
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|