garage_client 2.1.1
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 +7 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +40 -0
- data/Gemfile +8 -0
- data/Guardfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +196 -0
- data/Rakefile +8 -0
- data/garage_client.gemspec +36 -0
- data/gemfiles/Gemfile.faraday-0.8.x +4 -0
- data/lib/garage_client.rb +33 -0
- data/lib/garage_client/cachers/base.rb +44 -0
- data/lib/garage_client/client.rb +93 -0
- data/lib/garage_client/configuration.rb +51 -0
- data/lib/garage_client/error.rb +37 -0
- data/lib/garage_client/request.rb +38 -0
- data/lib/garage_client/request/json_encoded.rb +59 -0
- data/lib/garage_client/resource.rb +63 -0
- data/lib/garage_client/response.rb +123 -0
- data/lib/garage_client/response/cacheable.rb +27 -0
- data/lib/garage_client/response/raise_http_exception.rb +34 -0
- data/lib/garage_client/version.rb +3 -0
- data/spec/features/configuration_spec.rb +46 -0
- data/spec/fixtures/example.yaml +56 -0
- data/spec/fixtures/examples.yaml +60 -0
- data/spec/fixtures/examples_dictionary.yaml +60 -0
- data/spec/fixtures/examples_without_pagination.yaml +58 -0
- data/spec/garage_client/cacher_spec.rb +55 -0
- data/spec/garage_client/client_spec.rb +228 -0
- data/spec/garage_client/configuration_spec.rb +106 -0
- data/spec/garage_client/error_spec.rb +37 -0
- data/spec/garage_client/request/json_encoded_spec.rb +66 -0
- data/spec/garage_client/resource_spec.rb +102 -0
- data/spec/garage_client/response_spec.rb +450 -0
- data/spec/garage_client_spec.rb +48 -0
- data/spec/spec_helper.rb +56 -0
- metadata +275 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module GarageClient
|
2
|
+
class Configuration
|
3
|
+
DEFAULTS = {
|
4
|
+
adapter: :net_http,
|
5
|
+
cacher: nil,
|
6
|
+
headers: {
|
7
|
+
'Accept' => 'application/json',
|
8
|
+
'User-Agent' => "garage_client #{GarageClient::VERSION}",
|
9
|
+
},
|
10
|
+
path_prefix: '/v1',
|
11
|
+
verbose: false,
|
12
|
+
}
|
13
|
+
|
14
|
+
def self.keys
|
15
|
+
DEFAULTS.keys + [:endpoint]
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
@options = options
|
20
|
+
end
|
21
|
+
|
22
|
+
def options
|
23
|
+
@options ||= {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def reset
|
27
|
+
@options = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
DEFAULTS.keys.each do |key|
|
31
|
+
define_method(key) do
|
32
|
+
options.fetch(key, DEFAULTS[key])
|
33
|
+
end
|
34
|
+
|
35
|
+
define_method("#{key}=") do |value|
|
36
|
+
options[key] = value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def endpoint
|
41
|
+
options[:endpoint] or raise 'Configuration error: missing endpoint'
|
42
|
+
end
|
43
|
+
|
44
|
+
def endpoint=(value)
|
45
|
+
options[:endpoint] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
alias :default_headers :headers
|
49
|
+
alias :default_headers= :headers=
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module GarageClient
|
2
|
+
class Error < StandardError
|
3
|
+
attr_accessor :response
|
4
|
+
|
5
|
+
def initialize(response = nil)
|
6
|
+
@response = response
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
case
|
11
|
+
when String === response
|
12
|
+
response
|
13
|
+
when response.respond_to?(:[]) && response[:method].respond_to?(:upcase) && response[:url].is_a?(URI::HTTP)
|
14
|
+
"#{response[:method].upcase} #{response[:url]} #{response[:status]}: #{response[:body]}"
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# HTTP level
|
22
|
+
class Unauthorized < Error; end
|
23
|
+
class Forbidden < Error; end
|
24
|
+
class NotFound < Error; end
|
25
|
+
class NotAcceptable < Error; end
|
26
|
+
class Conflict < Error; end
|
27
|
+
class UnsupportedMediaType < Error; end
|
28
|
+
class UnprocessableEntity < Error; end
|
29
|
+
|
30
|
+
# Remote Server
|
31
|
+
class InternalServerError < Error; end
|
32
|
+
class ServiceUnavailable < Error; end
|
33
|
+
|
34
|
+
# GarageClient Client
|
35
|
+
class UnsupportedResource < Error; end
|
36
|
+
class InvalidResponseType < Error; end
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module GarageClient
|
2
|
+
module Request
|
3
|
+
MIME_DICT = 'application/vnd.cookpad.dictionary+json'
|
4
|
+
|
5
|
+
def get(path, params = nil, options = {})
|
6
|
+
request(:get, path, params, nil, options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def post(path, body = nil, options = {})
|
10
|
+
request(:post, path, {}, body, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def put(path, body = nil, options = {})
|
14
|
+
request(:put, path, {}, body, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete(path, options = {})
|
18
|
+
request(:delete, path, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def request(method, path, params = {}, body = nil, options = {})
|
23
|
+
response = conn.send(method) do |request|
|
24
|
+
request.url(path, params)
|
25
|
+
request.body = body if body
|
26
|
+
request.headers.update(options[:headers]) if options[:headers]
|
27
|
+
end
|
28
|
+
options[:raw] ? response : GarageClient::Response.new(self, response)
|
29
|
+
end
|
30
|
+
|
31
|
+
def request_with_prefix(method, path, *args)
|
32
|
+
path = "#{path_prefix}#{path}" unless path.start_with?(path_prefix)
|
33
|
+
request_without_prefix(method, path, *args)
|
34
|
+
end
|
35
|
+
alias request_without_prefix request
|
36
|
+
alias request request_with_prefix
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module GarageClient
|
2
|
+
module Request
|
3
|
+
class JsonEncoded < Faraday::Middleware
|
4
|
+
def call(env)
|
5
|
+
request = Request.new(env)
|
6
|
+
|
7
|
+
if request.json_compatible?
|
8
|
+
env[:request_headers]["Content-Type"] ||= "application/json"
|
9
|
+
env[:body] = env[:body].to_json
|
10
|
+
end
|
11
|
+
|
12
|
+
@app.call(env)
|
13
|
+
end
|
14
|
+
|
15
|
+
class Request
|
16
|
+
attr_reader :env
|
17
|
+
|
18
|
+
def initialize(env)
|
19
|
+
@env = env
|
20
|
+
end
|
21
|
+
|
22
|
+
def json_compatible?
|
23
|
+
has_json_compatible_body? && has_json_compatible_content_type?
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def has_json_compatible_content_type?
|
29
|
+
headers["Content-Type"].nil? || headers["Content-Type"] == "application/json"
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_json_compatible_body?
|
33
|
+
case body
|
34
|
+
when nil
|
35
|
+
false
|
36
|
+
when Array, Hash
|
37
|
+
true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def body
|
42
|
+
env[:body]
|
43
|
+
end
|
44
|
+
|
45
|
+
def headers
|
46
|
+
env[:request_headers]
|
47
|
+
end
|
48
|
+
|
49
|
+
def has_json_content_type?
|
50
|
+
headers["Content-Type"] == "application/json"
|
51
|
+
end
|
52
|
+
|
53
|
+
def has_content_type?
|
54
|
+
!headers["Content-Type"].nil?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "hashie"
|
2
|
+
|
3
|
+
module GarageClient
|
4
|
+
class Resource
|
5
|
+
attr_accessor :data, :client
|
6
|
+
|
7
|
+
def self.resource?(hash)
|
8
|
+
hash.kind_of?(Hash) && hash.has_key?('_links')
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(client, data)
|
12
|
+
@client = client
|
13
|
+
@data = Hashie::Mash.new(data)
|
14
|
+
end
|
15
|
+
|
16
|
+
def properties
|
17
|
+
@properties ||= data.keys.map(&:to_sym)
|
18
|
+
end
|
19
|
+
|
20
|
+
def links
|
21
|
+
@links ||= data._links ? data._links.keys.map(&:to_sym) : []
|
22
|
+
end
|
23
|
+
|
24
|
+
def self_path
|
25
|
+
@self_path ||= data._links.self.href
|
26
|
+
end
|
27
|
+
|
28
|
+
def update(body = nil, options = {})
|
29
|
+
client.put(self_path, body, options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def destroy(options = {})
|
33
|
+
client.delete(self_path, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(name, *args, &block)
|
37
|
+
if properties.include?(name)
|
38
|
+
value = data[name]
|
39
|
+
if self.class.resource?(value)
|
40
|
+
GarageClient::Resource.new(client, value)
|
41
|
+
else
|
42
|
+
value
|
43
|
+
end
|
44
|
+
elsif links.include?(name)
|
45
|
+
path = data._links[name].href
|
46
|
+
client.get(path, *args)
|
47
|
+
elsif nested_resource_creation_method?(name)
|
48
|
+
path = data._links[name.to_s.sub(/create_/, '')].href
|
49
|
+
client.post(path, *args)
|
50
|
+
else
|
51
|
+
super
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def respond_to?(name)
|
56
|
+
super || !!(properties.include?(name) || links.include?(name) || nested_resource_creation_method?(name))
|
57
|
+
end
|
58
|
+
|
59
|
+
def nested_resource_creation_method?(name)
|
60
|
+
!!(name =~ /\Acreate_(.+)\z/ && links.include?($1.to_sym))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require "link_header"
|
2
|
+
|
3
|
+
module GarageClient
|
4
|
+
class Response
|
5
|
+
MIME_DICT = %r{application/vnd\.cookpad\.dictionary\+(json|x-msgpack)}
|
6
|
+
ACCEPT_BODY_TYPES = [Array, Hash, NilClass]
|
7
|
+
|
8
|
+
attr_accessor :client, :response
|
9
|
+
|
10
|
+
def initialize(client, response)
|
11
|
+
@client = client
|
12
|
+
@response = response
|
13
|
+
|
14
|
+
# Faraday's Net::Http adapter returns '' if response is nil.
|
15
|
+
# Changes from faraday v0.9.0. faraday/f41ffaabb72d3700338296c79a2084880e6a9843
|
16
|
+
#
|
17
|
+
# GarageClient::Response#body should be always a String when Faraday
|
18
|
+
# became v1.0.0. Because 0.9.0 seems to be not stable.
|
19
|
+
response.env[:body] = nil if response.env[:body] == ''
|
20
|
+
|
21
|
+
unless ACCEPT_BODY_TYPES.any? {|type| type === response.body }
|
22
|
+
raise GarageClient::InvalidResponseType, "Invalid response type (#{response.body.class}): #{response.body}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def link
|
27
|
+
@link ||= response.headers['Link']
|
28
|
+
end
|
29
|
+
|
30
|
+
def total_count
|
31
|
+
unless @total_count
|
32
|
+
@total_count = response.headers['X-List-TotalCount']
|
33
|
+
@total_count = @total_count.to_i if @total_count
|
34
|
+
end
|
35
|
+
@total_count
|
36
|
+
end
|
37
|
+
|
38
|
+
def body
|
39
|
+
@body ||= case response.body
|
40
|
+
when Array
|
41
|
+
response.body.map {|res| GarageClient::Resource.new(client, res) }
|
42
|
+
when Hash
|
43
|
+
if dictionary_response?
|
44
|
+
Hash[response.body.map {|id, res| [id, GarageClient::Resource.new(client, res)] }]
|
45
|
+
else
|
46
|
+
GarageClient::Resource.new(client, response.body)
|
47
|
+
end
|
48
|
+
when NilClass
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def next_page_path
|
54
|
+
next_page_link.try(:href)
|
55
|
+
end
|
56
|
+
|
57
|
+
def prev_page_path
|
58
|
+
prev_page_link.try(:href)
|
59
|
+
end
|
60
|
+
|
61
|
+
def first_page_path
|
62
|
+
first_page_link.try(:href)
|
63
|
+
end
|
64
|
+
|
65
|
+
def last_page_path
|
66
|
+
last_page_link.try(:href)
|
67
|
+
end
|
68
|
+
|
69
|
+
def has_next_page?
|
70
|
+
!!next_page_link
|
71
|
+
end
|
72
|
+
|
73
|
+
def has_prev_page?
|
74
|
+
!!prev_page_link
|
75
|
+
end
|
76
|
+
|
77
|
+
def has_first_page?
|
78
|
+
!!first_page_link
|
79
|
+
end
|
80
|
+
|
81
|
+
def has_last_page?
|
82
|
+
!!last_page_link
|
83
|
+
end
|
84
|
+
|
85
|
+
def next_page_link
|
86
|
+
parsed_link_header.try(:find_link, %w[rel next])
|
87
|
+
end
|
88
|
+
|
89
|
+
def prev_page_link
|
90
|
+
parsed_link_header.try(:find_link, %w[rel prev])
|
91
|
+
end
|
92
|
+
|
93
|
+
def first_page_link
|
94
|
+
parsed_link_header.try(:find_link, %w[rel first])
|
95
|
+
end
|
96
|
+
|
97
|
+
def last_page_link
|
98
|
+
parsed_link_header.try(:find_link, %w[rel last])
|
99
|
+
end
|
100
|
+
|
101
|
+
def respond_to?(name, *args)
|
102
|
+
super || body.respond_to?(name, *args)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def method_missing(name, *args, &block)
|
108
|
+
if body.respond_to?(name)
|
109
|
+
body.send(name, *args, &block)
|
110
|
+
else
|
111
|
+
super
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def dictionary_response?
|
116
|
+
response.headers['Content-Type'] =~ MIME_DICT
|
117
|
+
end
|
118
|
+
|
119
|
+
def parsed_link_header
|
120
|
+
@parsed_link_header ||= LinkHeader.parse(link) if link
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "faraday_middleware"
|
2
|
+
|
3
|
+
module GarageClient
|
4
|
+
class Response
|
5
|
+
class Cacheable < Faraday::Response::Middleware
|
6
|
+
register_middleware cache: self
|
7
|
+
|
8
|
+
def initialize(app, args)
|
9
|
+
super(app)
|
10
|
+
@cacher_class = args[:cacher]
|
11
|
+
validate!
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
@cacher_class.new(env).call { @app.call(env) }
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def validate!
|
21
|
+
unless @cacher_class
|
22
|
+
raise ArgumentError, "You must pass cacher_class to #{self.class}.new"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'garage_client/error'
|
2
|
+
require 'faraday_middleware'
|
3
|
+
|
4
|
+
module GarageClient
|
5
|
+
class Response
|
6
|
+
class RaiseHttpException < Faraday::Response::Middleware
|
7
|
+
def call(env)
|
8
|
+
@app.call(env).on_complete do |response|
|
9
|
+
resp = response
|
10
|
+
case response[:status].to_i
|
11
|
+
when 401
|
12
|
+
raise GarageClient::Unauthorized.new(resp)
|
13
|
+
when 403
|
14
|
+
raise GarageClient::Forbidden.new(resp)
|
15
|
+
when 404
|
16
|
+
raise GarageClient::NotFound.new(resp)
|
17
|
+
when 406
|
18
|
+
raise GarageClient::NotAcceptable.new(resp)
|
19
|
+
when 409
|
20
|
+
raise GarageClient::Conflict.new(resp)
|
21
|
+
when 415
|
22
|
+
raise GarageClient::UnsupportedMediaType.new(resp)
|
23
|
+
when 422
|
24
|
+
raise GarageClient::UnprocessableEntity.new(resp)
|
25
|
+
when 500
|
26
|
+
raise GarageClient::InternalServerError.new(resp)
|
27
|
+
when 503
|
28
|
+
raise GarageClient::ServiceUnavailable.new(resp)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|