wcc-api 0.2.0 → 0.3.0
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/.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
|
-
|