wcc-api 0.1.0 → 0.5.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/Guardfile +32 -0
- data/Rakefile +2 -1
- data/bin/rspec +18 -0
- data/lib/wcc/api.rb +7 -8
- data/lib/wcc/api/active_record_shim.rb +69 -0
- data/lib/wcc/api/base_query.rb +10 -10
- data/lib/wcc/api/controller_helpers.rb +17 -0
- data/lib/wcc/api/json.rb +2 -0
- data/lib/wcc/api/json/pagination.rb +19 -14
- data/lib/wcc/api/railtie.rb +3 -1
- data/lib/wcc/api/rest_client.rb +238 -0
- data/lib/wcc/api/rest_client/api_error.rb +26 -0
- data/lib/wcc/api/rest_client/builder.rb +82 -0
- data/lib/wcc/api/rest_client/http_adapter.rb +19 -0
- data/lib/wcc/api/rest_client/response.rb +137 -0
- data/lib/wcc/api/rest_client/typhoeus_adapter.rb +63 -0
- data/lib/wcc/api/rspec.rb +4 -2
- data/lib/wcc/api/rspec/cache_header_examples.rb +43 -0
- 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 +296 -0
- data/wcc-api.gemspec +26 -13
- metadata +195 -14
data/lib/wcc/api/base_query.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WCC::API
|
2
4
|
class BaseQuery
|
3
|
-
attr_reader :scope, :paging
|
4
|
-
attr_accessor :limit, :offset
|
5
|
+
attr_reader :scope, :paging, :limit, :offset
|
5
6
|
attr_accessor :filter
|
6
7
|
|
7
8
|
MAX_LIMIT = 50
|
@@ -15,7 +16,7 @@ module WCC::API
|
|
15
16
|
end
|
16
17
|
|
17
18
|
def permitted_keys
|
18
|
-
%i
|
19
|
+
%i[limit offset filter]
|
19
20
|
end
|
20
21
|
|
21
22
|
def default_scope
|
@@ -27,11 +28,11 @@ module WCC::API
|
|
27
28
|
@paging = paging
|
28
29
|
set_defaults
|
29
30
|
permitted_keys.each do |key|
|
30
|
-
|
31
|
+
public_send("#{key}=", params[key]) if params.key?(key)
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
34
|
-
def call(scope=self.scope)
|
35
|
+
def call(scope = self.scope)
|
35
36
|
scope = scope.dup
|
36
37
|
scope = paged(scope)
|
37
38
|
scope = ordered(scope)
|
@@ -39,7 +40,7 @@ module WCC::API
|
|
39
40
|
scope
|
40
41
|
end
|
41
42
|
|
42
|
-
def paged(scope=self.scope)
|
43
|
+
def paged(scope = self.scope)
|
43
44
|
if paging
|
44
45
|
scope
|
45
46
|
.limit(limit)
|
@@ -49,11 +50,11 @@ module WCC::API
|
|
49
50
|
end
|
50
51
|
end
|
51
52
|
|
52
|
-
def ordered(scope=self.scope)
|
53
|
+
def ordered(scope = self.scope)
|
53
54
|
scope
|
54
55
|
end
|
55
56
|
|
56
|
-
def filtered(scope=self.scope)
|
57
|
+
def filtered(scope = self.scope)
|
57
58
|
scope
|
58
59
|
end
|
59
60
|
|
@@ -65,7 +66,7 @@ module WCC::API
|
|
65
66
|
|
66
67
|
def limit=(new_limit)
|
67
68
|
new_limit = new_limit.to_i
|
68
|
-
@limit =
|
69
|
+
@limit = new_limit > MAX_LIMIT ? MAX_LIMIT : new_limit
|
69
70
|
end
|
70
71
|
|
71
72
|
def offset=(new_offset)
|
@@ -85,4 +86,3 @@ module WCC::API
|
|
85
86
|
end
|
86
87
|
end
|
87
88
|
end
|
88
|
-
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WCC::API
|
4
|
+
module ControllerHelpers
|
5
|
+
private
|
6
|
+
|
7
|
+
def set_cache_headers(scope_or_record, options = {})
|
8
|
+
options = { public: true, must_revalidate: true }.merge!(options)
|
9
|
+
|
10
|
+
if expiry = options.delete(:expiry)
|
11
|
+
expires_in expiry, options.slice(:public, :must_revalidate)
|
12
|
+
end
|
13
|
+
|
14
|
+
fresh_when scope_or_record, options.slice(:etag, :public, :last_modified)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
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
|
@@ -33,23 +35,26 @@ module WCC::API::JSON
|
|
33
35
|
private
|
34
36
|
|
35
37
|
def has_previous_page?
|
36
|
-
query.offset - query.limit >= 0
|
38
|
+
query.paging && query.offset - query.limit >= 0
|
37
39
|
end
|
38
40
|
|
39
41
|
def has_next_page?
|
40
|
-
query.offset + query.limit < query.total
|
42
|
+
query.paging && query.offset + query.limit < query.total
|
41
43
|
end
|
42
44
|
|
43
45
|
def base_url_params
|
44
|
-
@base_url_params ||=
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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)
|
57
|
+
end
|
53
58
|
end
|
54
59
|
end
|
55
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,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'wcc'
|
4
|
+
require_relative 'rest_client/response'
|
5
|
+
require_relative 'rest_client/builder'
|
6
|
+
require_relative 'active_record_shim'
|
7
|
+
|
8
|
+
module WCC::API
|
9
|
+
class RestClient
|
10
|
+
attr_reader :api_url
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_reader :resources, :params
|
14
|
+
|
15
|
+
def rest_client(&block)
|
16
|
+
builder = Builder.new(self)
|
17
|
+
builder.instance_exec(&block)
|
18
|
+
builder.apply
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(api_url:, headers: nil, **options)
|
23
|
+
# normalizes a URL to have a slash on the end
|
24
|
+
@api_url = api_url.gsub(/\/+$/, '') + '/'
|
25
|
+
|
26
|
+
@adapter = RestClient.load_adapter(options[:adapter])
|
27
|
+
|
28
|
+
@options = options
|
29
|
+
@query_defaults = {}
|
30
|
+
@headers = {
|
31
|
+
'Accept' => 'application/json'
|
32
|
+
}.merge(headers || {}).freeze
|
33
|
+
@response_class = options[:response_class] || DefaultResponse
|
34
|
+
end
|
35
|
+
|
36
|
+
# performs an HTTP GET request to the specified path within the configured
|
37
|
+
# space and environment. Query parameters are merged with the defaults and
|
38
|
+
# appended to the request.
|
39
|
+
def get(path, query = {})
|
40
|
+
url = URI.join(@api_url, path)
|
41
|
+
|
42
|
+
@response_class.new(self,
|
43
|
+
{ url: url, query: query },
|
44
|
+
get_http(url, query))
|
45
|
+
end
|
46
|
+
|
47
|
+
def post(path, body = {})
|
48
|
+
url = URI.join(@api_url, path)
|
49
|
+
|
50
|
+
@response_class.new(self,
|
51
|
+
{ url: url },
|
52
|
+
post_http(url,
|
53
|
+
body.to_json,
|
54
|
+
headers: { 'Content-Type': 'application/json' }))
|
55
|
+
end
|
56
|
+
|
57
|
+
def put(path, body = {})
|
58
|
+
url = URI.join(@api_url, path)
|
59
|
+
|
60
|
+
@response_class.new(self,
|
61
|
+
{ url: url },
|
62
|
+
put_http(url,
|
63
|
+
body.to_json,
|
64
|
+
headers: { 'Content-Type': 'application/json' }))
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete(path)
|
68
|
+
url = URI.join(@api_url, path)
|
69
|
+
|
70
|
+
@response_class.new(self,
|
71
|
+
{ url: url },
|
72
|
+
delete_http(url))
|
73
|
+
end
|
74
|
+
|
75
|
+
ADAPTERS = {
|
76
|
+
faraday: ['faraday', '~> 0.9'],
|
77
|
+
typhoeus: ['typhoeus', '~> 1.0']
|
78
|
+
}.freeze
|
79
|
+
|
80
|
+
def self.load_adapter(adapter)
|
81
|
+
case adapter
|
82
|
+
when nil
|
83
|
+
ADAPTERS.each do |a, spec|
|
84
|
+
begin
|
85
|
+
gem(*spec)
|
86
|
+
return load_adapter(a)
|
87
|
+
rescue Gem::LoadError
|
88
|
+
next
|
89
|
+
end
|
90
|
+
end
|
91
|
+
raise ArgumentError, 'Unable to load adapter! Please install one of '\
|
92
|
+
"#{ADAPTERS.values.map(&:join).join(',')}"
|
93
|
+
when :faraday
|
94
|
+
require 'faraday'
|
95
|
+
::Faraday.new do |faraday|
|
96
|
+
faraday.response :logger, (Rails.logger if defined?(Rails)), { headers: false, bodies: false }
|
97
|
+
faraday.adapter :net_http
|
98
|
+
end
|
99
|
+
when :typhoeus
|
100
|
+
require_relative 'rest_client/typhoeus_adapter'
|
101
|
+
TyphoeusAdapter.new
|
102
|
+
else
|
103
|
+
unless adapter.respond_to?(:get)
|
104
|
+
raise ArgumentError, "Adapter #{adapter} is not invokeable! Please "\
|
105
|
+
"pass use one of #{ADAPTERS.keys} or create a Faraday-compatible adapter"
|
106
|
+
end
|
107
|
+
adapter
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def get_http(url, query, headers = {})
|
114
|
+
headers = @headers.merge(headers || {})
|
115
|
+
|
116
|
+
q = @query_defaults.dup
|
117
|
+
q = q.merge(query) if query
|
118
|
+
|
119
|
+
resp = @adapter.get(url, q, headers)
|
120
|
+
|
121
|
+
resp = get_http(resp.headers['location'], nil, headers) if [301, 302, 307].include?(resp.status) && !@options[:no_follow_redirects]
|
122
|
+
resp
|
123
|
+
end
|
124
|
+
|
125
|
+
def post_http(url, body, headers: {})
|
126
|
+
headers = @headers.merge(headers || {})
|
127
|
+
|
128
|
+
resp = @adapter.post(url, body, headers)
|
129
|
+
|
130
|
+
resp = get_http(resp.headers['location'], nil, headers) if [301, 302, 307].include?(resp.status) && !@options[:no_follow_redirects]
|
131
|
+
resp
|
132
|
+
end
|
133
|
+
|
134
|
+
def put_http(url, body, headers: {})
|
135
|
+
headers = @headers.merge(headers || {})
|
136
|
+
|
137
|
+
resp = @adapter.put(url, body, headers)
|
138
|
+
|
139
|
+
resp = get_http(resp.headers['location'], nil, headers) if [301, 302, 307].include?(resp.status) && !@options[:no_follow_redirects]
|
140
|
+
resp
|
141
|
+
end
|
142
|
+
|
143
|
+
def delete_http(url, headers: {})
|
144
|
+
headers = @headers.merge(headers || {})
|
145
|
+
|
146
|
+
resp = @adapter.delete(url, {}, headers)
|
147
|
+
|
148
|
+
resp = get_http(resp.headers['location'], nil, headers) if [301, 302, 307].include?(resp.status) && !@options[:no_follow_redirects]
|
149
|
+
resp
|
150
|
+
end
|
151
|
+
|
152
|
+
class Resource
|
153
|
+
attr_reader :client, :endpoint, :model, :options
|
154
|
+
|
155
|
+
def initialize(client, endpoint, model, options)
|
156
|
+
@client = client
|
157
|
+
@endpoint = endpoint
|
158
|
+
@model = model
|
159
|
+
@options = options
|
160
|
+
end
|
161
|
+
|
162
|
+
def find(id, query = {})
|
163
|
+
query = (options[:query] || {}).merge(query)
|
164
|
+
resp = client.get("#{endpoint}/#{id}", query)
|
165
|
+
resp.assert_ok!
|
166
|
+
body = options[:key] ? resp.body[options[:key]] : resp.body
|
167
|
+
model.new(body, resp.headers.freeze)
|
168
|
+
end
|
169
|
+
|
170
|
+
def list(**filters)
|
171
|
+
query = extract_params(filters)
|
172
|
+
query = (options[:query] || {}).merge(query)
|
173
|
+
query = query.merge!(apply_filters(filters, options[:filters]))
|
174
|
+
resp = client.get(endpoint, query)
|
175
|
+
resp.assert_ok!
|
176
|
+
resp.items.map { |s| model.new(s) }
|
177
|
+
end
|
178
|
+
|
179
|
+
def create(body)
|
180
|
+
resp = client.post(endpoint, body)
|
181
|
+
resp.assert_ok!
|
182
|
+
maybe_model_from_response(resp)
|
183
|
+
end
|
184
|
+
|
185
|
+
def update(id, body)
|
186
|
+
resp = client.put("#{endpoint}/#{id}", body)
|
187
|
+
resp.assert_ok!
|
188
|
+
maybe_model_from_response(resp)
|
189
|
+
end
|
190
|
+
|
191
|
+
def destroy(id)
|
192
|
+
resp = client.delete("#{endpoint}/#{id}")
|
193
|
+
resp.assert_ok!
|
194
|
+
maybe_model_from_response(resp)
|
195
|
+
end
|
196
|
+
|
197
|
+
protected
|
198
|
+
|
199
|
+
def maybe_model_from_response(resp)
|
200
|
+
return true if resp.status == 204
|
201
|
+
return true unless resp.raw_body.present?
|
202
|
+
|
203
|
+
body = options[:key] ? resp.body[options[:key]] : resp.body
|
204
|
+
return true unless body.present?
|
205
|
+
|
206
|
+
model.new(body, resp.headers.freeze)
|
207
|
+
end
|
208
|
+
|
209
|
+
def extract_params(filters)
|
210
|
+
filters.each_with_object({}) do |(k, _v), h|
|
211
|
+
k_s = k.to_s
|
212
|
+
h[k_s] = filters.delete(k) if client.class.params.include?(k_s)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def filter_key(filter_name)
|
217
|
+
filter_name
|
218
|
+
end
|
219
|
+
|
220
|
+
def apply_filters(filters, expected_filters)
|
221
|
+
defaults = default_filters(expected_filters) || {}
|
222
|
+
filters.each_with_object(defaults) do |(k, v), h|
|
223
|
+
k = k.to_s
|
224
|
+
raise ArgumentError, "Unknown filter '#{k}'" unless expected_filters.include?(k)
|
225
|
+
|
226
|
+
h[filter_key(k)] = v
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def default_filters(expected_filters)
|
231
|
+
options[:default_filters]&.each_with_object({}) do |(k, v), h|
|
232
|
+
k = k.to_s
|
233
|
+
h[filter_key(k)] = v if expected_filters.include?(k)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
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,82 @@
|
|
1
|
+
module WCC::API
|
2
|
+
class RestClient
|
3
|
+
class Builder
|
4
|
+
def initialize(klass)
|
5
|
+
@klass = klass
|
6
|
+
end
|
7
|
+
|
8
|
+
def params(*params)
|
9
|
+
@params = params.map(&:to_s)
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_writer :resource_class
|
13
|
+
def resource_class
|
14
|
+
@resource_class ||=
|
15
|
+
@klass.const_get("Resource") || WCC::API::RestClient::Resource
|
16
|
+
end
|
17
|
+
|
18
|
+
def resource(endpoint, model:, **options, &block)
|
19
|
+
@resources ||= {}
|
20
|
+
|
21
|
+
resource_class = options[:resource_class] || self.resource_class
|
22
|
+
if block_given?
|
23
|
+
resource_class = Class.new(resource_class, &block)
|
24
|
+
end
|
25
|
+
@resources[endpoint] = options.merge({
|
26
|
+
resource_class: resource_class,
|
27
|
+
model: model,
|
28
|
+
})
|
29
|
+
end
|
30
|
+
|
31
|
+
def apply
|
32
|
+
closed_params = (@params || []).freeze
|
33
|
+
resources = @resources
|
34
|
+
klass = @klass
|
35
|
+
|
36
|
+
klass.class_exec do
|
37
|
+
define_singleton_method :params do
|
38
|
+
closed_params
|
39
|
+
end
|
40
|
+
|
41
|
+
define_singleton_method :default do
|
42
|
+
@default ||= new
|
43
|
+
end
|
44
|
+
|
45
|
+
define_singleton_method :default= do |client|
|
46
|
+
@default = client
|
47
|
+
end
|
48
|
+
|
49
|
+
resources.each do |(endpoint, options)|
|
50
|
+
attr_name = options[:attribute] || endpoint.downcase
|
51
|
+
resource_class = options[:resource_class]
|
52
|
+
|
53
|
+
define_method attr_name do
|
54
|
+
instance_variable_get("@#{attr_name}") ||
|
55
|
+
instance_variable_set("@#{attr_name}",
|
56
|
+
resource_class.new(self, endpoint, options[:model], @options.merge(options))
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
resources.each do |(endpoint, options)|
|
63
|
+
options[:model].class_exec do
|
64
|
+
define_singleton_method :client do
|
65
|
+
klass.default
|
66
|
+
end
|
67
|
+
|
68
|
+
define_singleton_method :endpoint do
|
69
|
+
endpoint
|
70
|
+
end
|
71
|
+
|
72
|
+
define_singleton_method :key do
|
73
|
+
options[:key]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
options[:model].send(:include, WCC::API::ActiveRecordShim)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|