wcc-api 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +41 -0
- data/.env.example +0 -0
- data/.rspec +2 -0
- data/.rubocop.yml +190 -0
- data/.rubocop_todo.yml +63 -0
- data/Gemfile +2 -0
- data/Rakefile +2 -1
- data/bin/rspec +18 -0
- data/lib/wcc/api.rb +6 -8
- data/lib/wcc/api/base_query.rb +10 -10
- data/lib/wcc/api/controller_helpers.rb +5 -3
- data/lib/wcc/api/json.rb +2 -0
- data/lib/wcc/api/json/pagination.rb +16 -13
- data/lib/wcc/api/railtie.rb +3 -1
- data/lib/wcc/api/rest_client.rb +83 -0
- data/lib/wcc/api/rest_client/api_error.rb +26 -0
- data/lib/wcc/api/rest_client/http_adapter.rb +19 -0
- data/lib/wcc/api/rest_client/response.rb +135 -0
- data/lib/wcc/api/rest_client/typhoeus_adapter.rb +34 -0
- data/lib/wcc/api/rspec.rb +2 -1
- data/lib/wcc/api/rspec/cache_header_examples.rb +15 -13
- data/lib/wcc/api/rspec/collection_matchers.rb +4 -2
- data/lib/wcc/api/rspec/pagination_examples.rb +8 -8
- data/lib/wcc/api/version.rb +3 -1
- data/lib/wcc/api/view_helpers.rb +3 -3
- data/spec/fixtures/contentful/entries.json +80 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/fixtures_helper.rb +8 -0
- data/spec/wcc/api/rest_client_spec.rb +266 -0
- data/wcc-api.gemspec +23 -13
- metadata +150 -10
data/lib/wcc/api/json.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WCC::API::JSON
|
2
4
|
class Pagination
|
3
5
|
attr_reader :query, :url_for
|
@@ -19,12 +21,12 @@ module WCC::API::JSON
|
|
19
21
|
json.sort query.sort if query.respond_to?(:sort)
|
20
22
|
json.filter query.filter
|
21
23
|
json._links do
|
22
|
-
json.self url_for.(base_url_params)
|
24
|
+
json.self url_for.call(base_url_params)
|
23
25
|
if has_previous_page?
|
24
|
-
json.previous url_for.(base_url_params.merge(offset: query.offset - query.limit))
|
26
|
+
json.previous url_for.call(base_url_params.merge(offset: query.offset - query.limit))
|
25
27
|
end
|
26
28
|
if has_next_page?
|
27
|
-
json.next url_for.(base_url_params.merge(offset: query.offset + query.limit))
|
29
|
+
json.next url_for.call(base_url_params.merge(offset: query.offset + query.limit))
|
28
30
|
end
|
29
31
|
end
|
30
32
|
end
|
@@ -41,17 +43,18 @@ module WCC::API::JSON
|
|
41
43
|
end
|
42
44
|
|
43
45
|
def base_url_params
|
44
|
-
@base_url_params ||=
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
46
|
+
@base_url_params ||=
|
47
|
+
{
|
48
|
+
filter: query.filter,
|
49
|
+
only_path: false
|
50
|
+
}.tap do |params|
|
51
|
+
if query.paging
|
52
|
+
params[:limit] = query.limit
|
53
|
+
params[:offset] = query.offset
|
54
|
+
end
|
55
|
+
params[:order_by] = query.order_by if query.respond_to?(:order_by)
|
56
|
+
params[:sort] = query.sort if query.respond_to?(:sort)
|
51
57
|
end
|
52
|
-
params[:order_by] = query.order_by if query.respond_to?(:order_by)
|
53
|
-
params[:sort] = query.sort if query.respond_to?(:sort)
|
54
|
-
end
|
55
58
|
end
|
56
59
|
end
|
57
60
|
end
|
data/lib/wcc/api/railtie.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'wcc/api'
|
2
4
|
|
3
5
|
module WCC
|
4
6
|
module API
|
5
7
|
class Railtie < Rails::Railtie
|
6
|
-
initializer 'wcc.api railtie initializer', group: :all do |
|
8
|
+
initializer 'wcc.api railtie initializer', group: :all do |_app|
|
7
9
|
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
8
10
|
inflect.acronym 'API'
|
9
11
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'rest_client/response'
|
4
|
+
|
5
|
+
module WCC::API
|
6
|
+
class RestClient
|
7
|
+
attr_reader :api_url
|
8
|
+
|
9
|
+
def initialize(api_url:, headers: nil, **options)
|
10
|
+
# normalizes a URL to have a slash on the end
|
11
|
+
@api_url = api_url.gsub(/\/+$/, '') + '/'
|
12
|
+
|
13
|
+
@adapter = RestClient.load_adapter(options[:adapter])
|
14
|
+
|
15
|
+
@options = options
|
16
|
+
@query_defaults = {}
|
17
|
+
@headers = {
|
18
|
+
'Accept' => 'application/json'
|
19
|
+
}.merge(headers || {}).freeze
|
20
|
+
@response_class = options[:response_class] || DefaultResponse
|
21
|
+
end
|
22
|
+
|
23
|
+
# performs an HTTP GET request to the specified path within the configured
|
24
|
+
# space and environment. Query parameters are merged with the defaults and
|
25
|
+
# appended to the request.
|
26
|
+
def get(path, query = {})
|
27
|
+
url = URI.join(@api_url, path)
|
28
|
+
|
29
|
+
@response_class.new(self,
|
30
|
+
{ url: url, query: query },
|
31
|
+
get_http(url, query))
|
32
|
+
end
|
33
|
+
|
34
|
+
ADAPTERS = {
|
35
|
+
http: ['http', '> 1.0', '< 3.0'],
|
36
|
+
typhoeus: ['typhoeus', '~> 1.0']
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
# This method is long due to the case statement,
|
40
|
+
# not really a better way to do it
|
41
|
+
def self.load_adapter(adapter)
|
42
|
+
case adapter
|
43
|
+
when nil
|
44
|
+
ADAPTERS.each do |a, spec|
|
45
|
+
begin
|
46
|
+
gem(*spec)
|
47
|
+
return load_adapter(a)
|
48
|
+
rescue Gem::LoadError
|
49
|
+
next
|
50
|
+
end
|
51
|
+
end
|
52
|
+
raise ArgumentError, 'Unable to load adapter! Please install one of '\
|
53
|
+
"#{ADAPTERS.values.map(&:join).join(',')}"
|
54
|
+
when :http
|
55
|
+
require_relative 'rest_client/http_adapter'
|
56
|
+
HttpAdapter.new
|
57
|
+
when :typhoeus
|
58
|
+
require_relative 'rest_client/typhoeus_adapter'
|
59
|
+
TyphoeusAdapter.new
|
60
|
+
else
|
61
|
+
unless adapter.respond_to?(:call)
|
62
|
+
raise ArgumentError, "Adapter #{adapter} is not invokeable! Please "\
|
63
|
+
"pass a proc or use one of #{ADAPTERS.keys}"
|
64
|
+
end
|
65
|
+
adapter
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def get_http(url, query, headers = {}, proxy = {})
|
72
|
+
headers = @headers.merge(headers || {})
|
73
|
+
|
74
|
+
q = @query_defaults.dup
|
75
|
+
q = q.merge(query) if query
|
76
|
+
|
77
|
+
resp = @adapter.call(url, q, headers, proxy)
|
78
|
+
|
79
|
+
resp = get_http(resp.headers['location'], nil, headers, proxy) if [301, 302, 307].include?(resp.code) && !@options[:no_follow_redirects]
|
80
|
+
resp
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WCC::API
|
4
|
+
class RestClient
|
5
|
+
class ApiError < StandardError
|
6
|
+
attr_reader :response
|
7
|
+
|
8
|
+
def self.[](code)
|
9
|
+
case code
|
10
|
+
when 404
|
11
|
+
NotFoundError
|
12
|
+
else
|
13
|
+
ApiError
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(response)
|
18
|
+
@response = response
|
19
|
+
super(response.error_message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class NotFoundError < ApiError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem 'http'
|
4
|
+
require 'http'
|
5
|
+
|
6
|
+
module WCC::API
|
7
|
+
class RestClient
|
8
|
+
class HttpAdapter
|
9
|
+
def call(url, query, headers = {}, proxy = {})
|
10
|
+
if proxy[:host]
|
11
|
+
HTTP[headers].via(proxy[:host], proxy[:port], proxy[:username], proxy[:password])
|
12
|
+
.get(url, params: query)
|
13
|
+
else
|
14
|
+
HTTP[headers].get(url, params: query)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require_relative 'api_error'
|
5
|
+
|
6
|
+
module WCC::API
|
7
|
+
class RestClient
|
8
|
+
class AbstractResponse
|
9
|
+
extend ::Forwardable
|
10
|
+
|
11
|
+
attr_reader :raw_response
|
12
|
+
attr_reader :raw_body
|
13
|
+
attr_reader :client
|
14
|
+
attr_reader :request
|
15
|
+
|
16
|
+
def_delegators :raw_response, :code, :headers
|
17
|
+
|
18
|
+
def body
|
19
|
+
@body ||= ::JSON.parse(raw_body)
|
20
|
+
end
|
21
|
+
alias_method :to_json, :body
|
22
|
+
|
23
|
+
def initialize(client, request, raw_response)
|
24
|
+
@client = client
|
25
|
+
@request = request
|
26
|
+
@raw_response = raw_response
|
27
|
+
@raw_body = raw_response.body.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def skip
|
31
|
+
throw new NotImplementedError, 'Please implement "skip" parsing in response class'
|
32
|
+
end
|
33
|
+
|
34
|
+
def count
|
35
|
+
throw new NotImplementedError, 'Please implement "count" parsing in response class'
|
36
|
+
end
|
37
|
+
|
38
|
+
def collection_response?
|
39
|
+
page_items.nil? ? false : true
|
40
|
+
end
|
41
|
+
|
42
|
+
def page_items
|
43
|
+
throw new NotImplementedError, 'Please implement "page_items" parsing in response class'
|
44
|
+
end
|
45
|
+
|
46
|
+
def error_message
|
47
|
+
parsed_message =
|
48
|
+
begin
|
49
|
+
body.dig('error', 'message') || body.dig('message')
|
50
|
+
rescue ::JSON::ParserError
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
parsed_message || "#{code}: #{raw_response.body}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def next_page?
|
57
|
+
return false unless collection_response?
|
58
|
+
|
59
|
+
page_items.length + skip < count
|
60
|
+
end
|
61
|
+
|
62
|
+
def next_page
|
63
|
+
return unless next_page?
|
64
|
+
|
65
|
+
@next_page ||= @client.get(
|
66
|
+
@request[:url],
|
67
|
+
(@request[:query] || {}).merge(next_page_query)
|
68
|
+
)
|
69
|
+
@next_page.assert_ok!
|
70
|
+
end
|
71
|
+
|
72
|
+
def assert_ok!
|
73
|
+
return self if code >= 200 && code < 300
|
74
|
+
|
75
|
+
raise ApiError[code], self
|
76
|
+
end
|
77
|
+
|
78
|
+
# This method has a bit of complexity that is better kept in one location
|
79
|
+
def each_page(&block)
|
80
|
+
raise ArgumentError, 'Not a collection response' unless collection_response?
|
81
|
+
|
82
|
+
ret =
|
83
|
+
Enumerator.new do |y|
|
84
|
+
y << self
|
85
|
+
|
86
|
+
if next_page?
|
87
|
+
next_page.each_page.each do |page|
|
88
|
+
y << page
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
if block_given?
|
94
|
+
ret.map(&block)
|
95
|
+
else
|
96
|
+
ret.lazy
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def items
|
101
|
+
return unless collection_response?
|
102
|
+
|
103
|
+
each_page.flat_map(&:page_items)
|
104
|
+
end
|
105
|
+
|
106
|
+
def first
|
107
|
+
raise ArgumentError, 'Not a collection response' unless collection_response?
|
108
|
+
|
109
|
+
page_items.first
|
110
|
+
end
|
111
|
+
|
112
|
+
def next_page_query
|
113
|
+
return unless collection_response?
|
114
|
+
|
115
|
+
{
|
116
|
+
skip: page_items.length + skip
|
117
|
+
}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class DefaultResponse < AbstractResponse
|
122
|
+
def skip
|
123
|
+
body['skip']
|
124
|
+
end
|
125
|
+
|
126
|
+
def count
|
127
|
+
body['total']
|
128
|
+
end
|
129
|
+
|
130
|
+
def page_items
|
131
|
+
body['items']
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
gem 'typhoeus'
|
5
|
+
require 'typhoeus'
|
6
|
+
|
7
|
+
module WCC::API
|
8
|
+
class RestClient
|
9
|
+
class TyphoeusAdapter
|
10
|
+
def call(url, query, headers = {}, proxy = {})
|
11
|
+
raise NotImplementedError, 'Proxying Not Yet Implemented' if proxy[:host]
|
12
|
+
|
13
|
+
TyphoeusAdapter::Response.new(
|
14
|
+
Typhoeus.get(
|
15
|
+
url,
|
16
|
+
params: query,
|
17
|
+
headers: headers
|
18
|
+
)
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
Response =
|
23
|
+
Struct.new(:raw) do
|
24
|
+
extend Forwardable
|
25
|
+
|
26
|
+
def_delegators :raw, :body, :to_s, :code, :headers
|
27
|
+
|
28
|
+
def status
|
29
|
+
raw.code
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/wcc/api/rspec.rb
CHANGED
@@ -1,41 +1,43 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples_for 'cached resource defaults' do
|
4
|
+
it 'sets etag' do
|
3
5
|
expect(response.etag).to_not be_nil
|
4
6
|
end
|
5
7
|
|
6
|
-
it
|
8
|
+
it 'sets public: true' do
|
7
9
|
expect(response.cache_control[:public]).to be_truthy
|
8
10
|
end
|
9
11
|
end
|
10
12
|
|
11
|
-
RSpec.shared_examples_for
|
12
|
-
include_examples
|
13
|
+
RSpec.shared_examples_for 'cached member resource' do
|
14
|
+
include_examples 'cached resource defaults'
|
13
15
|
|
14
|
-
it
|
16
|
+
it 'sets last modified' do
|
15
17
|
expect(response.last_modified).to_not be_nil
|
16
18
|
end
|
17
19
|
|
18
|
-
it
|
20
|
+
it 'does not set max age' do
|
19
21
|
expect(response.cache_control[:max_age]).to be_nil
|
20
22
|
end
|
21
23
|
|
22
|
-
it
|
24
|
+
it 'does not set must_revalidate' do
|
23
25
|
expect(response.cache_control[:must_revalidate]).to be_nil
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
27
|
-
RSpec.shared_examples_for
|
28
|
-
include_examples
|
29
|
+
RSpec.shared_examples_for 'cached collection resource' do |max_age|
|
30
|
+
include_examples 'cached resource defaults'
|
29
31
|
|
30
|
-
it
|
32
|
+
it 'sets max age' do
|
31
33
|
expect(response.cache_control[:max_age]).to eq(max_age.to_s)
|
32
34
|
end
|
33
35
|
|
34
|
-
it
|
36
|
+
it 'sets must_revalidate: true' do
|
35
37
|
expect(response.cache_control[:must_revalidate]).to be_truthy
|
36
38
|
end
|
37
39
|
|
38
|
-
it
|
40
|
+
it 'does not set last modified' do
|
39
41
|
expect(response.last_modified).to be_nil
|
40
42
|
end
|
41
43
|
end
|
@@ -1,7 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WCC::API::RSpec
|
2
4
|
module CollectionMatchers
|
3
5
|
def collection_match(coll1, coll2, matcher = :eq)
|
4
|
-
coll1
|
6
|
+
coll1 = coll1.to_a
|
7
|
+
coll2 = coll2.to_a
|
5
8
|
expect(coll1.size).to eq(coll2.size)
|
6
9
|
|
7
10
|
coll1.zip(coll2).each do |actual, expected|
|
@@ -10,4 +13,3 @@ module WCC::API::RSpec
|
|
10
13
|
end
|
11
14
|
end
|
12
15
|
end
|
13
|
-
|