garage_client 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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