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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +1 -0
  4. data/CHANGELOG.md +40 -0
  5. data/Gemfile +8 -0
  6. data/Guardfile +7 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +196 -0
  9. data/Rakefile +8 -0
  10. data/garage_client.gemspec +36 -0
  11. data/gemfiles/Gemfile.faraday-0.8.x +4 -0
  12. data/lib/garage_client.rb +33 -0
  13. data/lib/garage_client/cachers/base.rb +44 -0
  14. data/lib/garage_client/client.rb +93 -0
  15. data/lib/garage_client/configuration.rb +51 -0
  16. data/lib/garage_client/error.rb +37 -0
  17. data/lib/garage_client/request.rb +38 -0
  18. data/lib/garage_client/request/json_encoded.rb +59 -0
  19. data/lib/garage_client/resource.rb +63 -0
  20. data/lib/garage_client/response.rb +123 -0
  21. data/lib/garage_client/response/cacheable.rb +27 -0
  22. data/lib/garage_client/response/raise_http_exception.rb +34 -0
  23. data/lib/garage_client/version.rb +3 -0
  24. data/spec/features/configuration_spec.rb +46 -0
  25. data/spec/fixtures/example.yaml +56 -0
  26. data/spec/fixtures/examples.yaml +60 -0
  27. data/spec/fixtures/examples_dictionary.yaml +60 -0
  28. data/spec/fixtures/examples_without_pagination.yaml +58 -0
  29. data/spec/garage_client/cacher_spec.rb +55 -0
  30. data/spec/garage_client/client_spec.rb +228 -0
  31. data/spec/garage_client/configuration_spec.rb +106 -0
  32. data/spec/garage_client/error_spec.rb +37 -0
  33. data/spec/garage_client/request/json_encoded_spec.rb +66 -0
  34. data/spec/garage_client/resource_spec.rb +102 -0
  35. data/spec/garage_client/response_spec.rb +450 -0
  36. data/spec/garage_client_spec.rb +48 -0
  37. data/spec/spec_helper.rb +56 -0
  38. 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
@@ -0,0 +1,3 @@
1
+ module GarageClient
2
+ VERSION = '2.1.1'
3
+ end