wcc-api 0.1.1 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1d0d85a20f019b951bef1ae2da1975d22c541af6
4
- data.tar.gz: adb5cd3c3c01bc1492488a56125429c9ba0647d8
3
+ metadata.gz: 0c0b6f43132217782441bec2a54a9d046110bf83
4
+ data.tar.gz: dba5e3f10029efa9cb252474bd7763a3764cd9e4
5
5
  SHA512:
6
- metadata.gz: 74a49e4005e9a8b621aae1fb71513b30aa965e3a1575d58efaa1b7e2e7d03a967953a8b77c95eefbe56637510a870d3e168410fd2ef74cbfc2f024b038e93bd8
7
- data.tar.gz: f65f84b847eb889eeb9e66284465427ccb73f072706a43dbaf02390461676f066b94f814ecb4c7905cac57a9d6b079c2a29a82b8b315df245acbb61967642797
6
+ metadata.gz: 725780f28f63a96972d70e5c7cd4596c5e4a8bb22955123db4f8d0f30c9ed30aec5cba14a1ccc65359cb0178f1c487d2e431967861f20e87f4fa8a93b2b215e1
7
+ data.tar.gz: f46f334b4a6c7c202f22898039350a54e7ccd8de6724c9e88cfe19fe3333db6d09c0fa6a4ae80a4f473a42439e513d2327b4e40dc82f6141f9577c4a153364ef
data/lib/wcc/api.rb CHANGED
@@ -1,17 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'wcc'
2
4
  require 'wcc/api/version'
3
5
 
4
- module WCC
5
- module API
6
- PROJECT_ROOT = File.expand_path(File.join(__FILE__, '..', '..', '..'))
7
- end
6
+ module WCC::API
7
+ PROJECT_ROOT = File.expand_path(File.join(__FILE__, '..', '..', '..'))
8
8
  end
9
9
 
10
- if defined?(Rails)
11
- require 'wcc/api/railtie'
12
- end
10
+ require 'wcc/api/railtie' if defined?(Rails)
13
11
 
14
12
  require 'wcc/api/base_query'
15
13
  require 'wcc/api/json'
14
+ require 'wcc/api/controller_helpers'
15
+ require 'wcc/api/rest_client'
16
16
  require 'wcc/api/view_helpers'
17
-
@@ -0,0 +1,69 @@
1
+ module WCC::API::ActiveRecordShim
2
+ def self.included(base)
3
+ base.public_send :include, InstanceMethods
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module InstanceMethods
8
+ def attributes
9
+ raw.keys.each_with_object({}) do |key, h|
10
+ next unless respond_to?(key)
11
+
12
+ val = public_send(key)
13
+ h[key] =
14
+ if val.is_a? Array
15
+ val.map { |v| v.respond_to?(:to_h) ? v.to_h : v }
16
+ else
17
+ val.respond_to?(:to_h) ? val.to_h : val
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ def find(id)
25
+ client.public_send(endpoint).find(id)
26
+ end
27
+
28
+ def find_all(**filters)
29
+ client.public_send(endpoint).list(filters)
30
+ end
31
+
32
+ def find_by(**filters)
33
+ raise ArgumentError, "You must provide at least one filter" if filters.empty?
34
+
35
+ find_all(filters).first
36
+ end
37
+
38
+ def model_name
39
+ name
40
+ end
41
+
42
+ def table_name
43
+ endpoint
44
+ end
45
+
46
+ def unscoped
47
+ yield
48
+ end
49
+
50
+ def find_in_batches(options, &block)
51
+ options = options ? options.dup : {}
52
+ batch_size = options.delete(:batch_size) || 1000
53
+ skip_param = [:skip, :offset]
54
+
55
+ filter = {
56
+ limit: batch_size,
57
+ offset: options.delete(:start) || 0
58
+ }
59
+
60
+ find_all(filter).each_slice(batch_size, &block)
61
+ end
62
+
63
+ def where(**conditions)
64
+ # TODO: return a Query object that implements more of the ActiveRecord query interface
65
+ # https://guides.rubyonrails.org/active_record_querying.html#conditions
66
+ find_all(conditions)
67
+ end
68
+ end
69
+ end
@@ -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(limit offset filter)
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
- self.public_send("#{key}=", params[key]) if params.has_key?(key)
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 = (new_limit > MAX_LIMIT) ? MAX_LIMIT : new_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
2
4
  module JSON
3
5
  end
@@ -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
- filter: query.filter,
46
- only_path: false
47
- }.tap do |params|
48
- if query.paging
49
- params[:limit] = query.limit
50
- params[:offset] = query.offset
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
@@ -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 |app|
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
@@ -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,137 @@
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, :status, :headers
17
+ alias_method :code, :status
18
+
19
+ def body
20
+ @body ||= ::JSON.parse(raw_body)
21
+ end
22
+ alias_method :to_json, :body
23
+
24
+ def initialize(client, request, raw_response)
25
+ @client = client
26
+ @request = request
27
+ @raw_response = raw_response
28
+ @raw_body = raw_response.body.to_s
29
+ end
30
+
31
+ def skip
32
+ throw new NotImplementedError, 'Please implement "skip" parsing in response class'
33
+ end
34
+
35
+ def count
36
+ throw new NotImplementedError, 'Please implement "count" parsing in response class'
37
+ end
38
+
39
+ def collection_response?
40
+ page_items.nil? ? false : true
41
+ end
42
+
43
+ def page_items
44
+ throw new NotImplementedError, 'Please implement "page_items" parsing in response class'
45
+ end
46
+
47
+ def error_message
48
+ parsed_message =
49
+ begin
50
+ body.dig('error', 'message') || body.dig('message')
51
+ rescue ::JSON::ParserError
52
+ nil
53
+ end
54
+ parsed_message || "#{code}: #{raw_response.body}"
55
+ end
56
+
57
+ def next_page?
58
+ return false unless collection_response?
59
+ return false if count.nil?
60
+
61
+ page_items.length + skip < count
62
+ end
63
+
64
+ def next_page
65
+ return unless next_page?
66
+
67
+ @next_page ||= @client.get(
68
+ @request[:url],
69
+ (@request[:query] || {}).merge(next_page_query)
70
+ )
71
+ @next_page.assert_ok!
72
+ end
73
+
74
+ def assert_ok!
75
+ return self if code >= 200 && code < 300
76
+
77
+ raise ApiError[code], self
78
+ end
79
+
80
+ # This method has a bit of complexity that is better kept in one location
81
+ def each_page(&block)
82
+ raise ArgumentError, 'Not a collection response' unless collection_response?
83
+
84
+ ret =
85
+ Enumerator.new do |y|
86
+ y << self
87
+
88
+ if next_page?
89
+ next_page.each_page.each do |page|
90
+ y << page
91
+ end
92
+ end
93
+ end
94
+
95
+ if block_given?
96
+ ret.map(&block)
97
+ else
98
+ ret.lazy
99
+ end
100
+ end
101
+
102
+ def items
103
+ return unless collection_response?
104
+
105
+ each_page.flat_map(&:page_items)
106
+ end
107
+
108
+ def first
109
+ raise ArgumentError, 'Not a collection response' unless collection_response?
110
+
111
+ page_items.first
112
+ end
113
+
114
+ def next_page_query
115
+ return unless collection_response?
116
+
117
+ {
118
+ skip: page_items.length + skip
119
+ }
120
+ end
121
+ end
122
+
123
+ class DefaultResponse < AbstractResponse
124
+ def skip
125
+ body['skip']
126
+ end
127
+
128
+ def count
129
+ body['total']
130
+ end
131
+
132
+ def page_items
133
+ body['items']
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ gem 'typhoeus'
5
+ require 'typhoeus'
6
+
7
+ class WCC::API::RestClient::TyphoeusAdapter
8
+ def get(url, params = {}, headers = {})
9
+ req = OpenStruct.new(params: params, headers: headers)
10
+ yield req if block_given?
11
+ Response.new(
12
+ Typhoeus.get(
13
+ url,
14
+ params: req.params,
15
+ headers: req.headers
16
+ )
17
+ )
18
+ end
19
+
20
+ def post(url, body, headers = {})
21
+ Response.new(
22
+ Typhoeus.post(
23
+ url,
24
+ body: body.is_a?(String) ? body : body.to_json,
25
+ headers: headers
26
+ )
27
+ )
28
+ end
29
+
30
+ def put(url, body, headers = {})
31
+ Response.new(
32
+ Typhoeus.put(
33
+ url,
34
+ body: body.is_a?(String) ? body : body.to_json,
35
+ headers: headers
36
+ )
37
+ )
38
+ end
39
+
40
+ def delete(url, query = {}, headers = {})
41
+ Response.new(
42
+ Typhoeus.delete(
43
+ url,
44
+ headers: headers
45
+ )
46
+ )
47
+ end
48
+
49
+ class Response < SimpleDelegator
50
+ def raw
51
+ __getobj__
52
+ end
53
+
54
+ def to_s
55
+ body&.to_s
56
+ end
57
+
58
+ def status
59
+ code
60
+ end
61
+ end
62
+ end
63
+
data/lib/wcc/api/rspec.rb CHANGED
@@ -1,3 +1,5 @@
1
- require 'wcc/api/rspec/pagination_examples'
2
- require 'wcc/api/rspec/collection_matchers'
1
+ # frozen_string_literal: true
3
2
 
3
+ require 'wcc/api/rspec/cache_header_examples'
4
+ require 'wcc/api/rspec/collection_matchers'
5
+ require 'wcc/api/rspec/pagination_examples'
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples_for 'cached resource defaults' do
4
+ it 'sets etag' do
5
+ expect(response.etag).to_not be_nil
6
+ end
7
+
8
+ it 'sets public: true' do
9
+ expect(response.cache_control[:public]).to be_truthy
10
+ end
11
+ end
12
+
13
+ RSpec.shared_examples_for 'cached member resource' do
14
+ include_examples 'cached resource defaults'
15
+
16
+ it 'sets last modified' do
17
+ expect(response.last_modified).to_not be_nil
18
+ end
19
+
20
+ it 'does not set max age' do
21
+ expect(response.cache_control[:max_age]).to be_nil
22
+ end
23
+
24
+ it 'does not set must_revalidate' do
25
+ expect(response.cache_control[:must_revalidate]).to be_nil
26
+ end
27
+ end
28
+
29
+ RSpec.shared_examples_for 'cached collection resource' do |max_age|
30
+ include_examples 'cached resource defaults'
31
+
32
+ it 'sets max age' do
33
+ expect(response.cache_control[:max_age]).to eq(max_age.to_s)
34
+ end
35
+
36
+ it 'sets must_revalidate: true' do
37
+ expect(response.cache_control[:must_revalidate]).to be_truthy
38
+ end
39
+
40
+ it 'does not set last modified' do
41
+ expect(response.last_modified).to be_nil
42
+ end
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, coll2 = coll1.to_a, coll2.to_a
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
-
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  RSpec.shared_examples_for :linked_pagination_object do |url_method, base_options, total|
2
- it "includes link to current page" do
4
+ it 'includes link to current page' do
3
5
  url = public_send(
4
6
  url_method,
5
7
  base_options.merge(limit: 2, offset: 2)
@@ -9,20 +11,19 @@ RSpec.shared_examples_for :linked_pagination_object do |url_method, base_options
9
11
  expect(subject['_links']['self']).to eq(url)
10
12
  end
11
13
 
12
- it "includes link to next page when there is a next page" do
14
+ it 'includes link to next page when there is a next page' do
13
15
  get public_send(url_method, limit: 2)
14
16
  url = public_send(url_method,
15
- base_options.merge(limit: 2, offset: 2)
16
- )
17
+ base_options.merge(limit: 2, offset: 2))
17
18
  expect(subject['_links']['next']).to eq(url)
18
19
  end
19
20
 
20
- it "does not include link to next page when this is the last page" do
21
+ it 'does not include link to next page when this is the last page' do
21
22
  get public_send(url_method, limit: 2, offset: total - 1)
22
23
  expect(subject['_links']['next']).to be_nil
23
24
  end
24
25
 
25
- it "includes link to previous page when there is a previous page" do
26
+ it 'includes link to previous page when there is a previous page' do
26
27
  get public_send(url_method, limit: 2, offset: 2)
27
28
  url = public_send(
28
29
  url_method,
@@ -31,9 +32,8 @@ RSpec.shared_examples_for :linked_pagination_object do |url_method, base_options
31
32
  expect(subject['_links']['previous']).to eq(url)
32
33
  end
33
34
 
34
- it "does not include link to next page when this is the last page" do
35
+ it 'does not include link to next page when this is the last page' do
35
36
  get public_send(url_method, limit: 2)
36
37
  expect(subject['_links']['previous']).to be_nil
37
38
  end
38
39
  end
39
-
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WCC
2
4
  module API
3
- VERSION = "0.1.1"
5
+ VERSION = '0.5.1'
4
6
  end
5
7
  end
@@ -1,12 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WCC::API
2
4
  module ViewHelpers
3
-
4
5
  def api_pagination_for(query:)
5
6
  WCC::API::JSON::Pagination.new(
6
7
  query,
7
- url_for: -> (params) { url_for(params) }
8
+ url_for: ->(params) { url_for(params) }
8
9
  ).to_builder
9
10
  end
10
-
11
11
  end
12
12
  end
data/wcc-api.gemspec CHANGED
@@ -1,25 +1,37 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'wcc/api/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "wcc-api"
8
+ spec.name = 'wcc-api'
8
9
  spec.version = WCC::API::VERSION
9
- spec.authors = ["Watermark Dev Team"]
10
- spec.email = ["dev@watermark.org"]
11
- spec.summary = %q{Holds common code used in our applications that host APIs.}
12
- spec.description = %q{holds common code used in our applications that host APIs.}
13
- spec.homepage = "https://github.com/watermarkchurch/wcc-api"
14
- spec.license = "MIT"
10
+ spec.authors = ['Watermark Dev']
11
+ spec.email = ['dev@watermark.org']
12
+ spec.summary =
13
+ spec.description = 'Holds common code used in our applications that host ' \
14
+ 'APIs and those that consume them.'
15
+ spec.homepage = 'https://github.com/watermarkchurch/wcc-api'
16
+ spec.license = 'MIT'
15
17
 
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.files = Dir['lib/**/*'] + %w[LICENSE.txt README.md wcc-api.gemspec]
18
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
+ spec.require_paths = ['lib']
20
21
 
21
- spec.add_dependency "wcc-base"
22
+ spec.add_dependency 'wcc-base'
22
23
 
23
- spec.add_development_dependency "bundler", "~> 1.6"
24
- spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency 'bundler', '~> 1.6'
25
+ spec.add_development_dependency 'dotenv', '~> 0.10.0'
26
+ spec.add_development_dependency 'faraday', '~> 0.9'
27
+ spec.add_development_dependency 'guard', '~> 2.15'
28
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
29
+ spec.add_development_dependency 'guard-rubocop', '~> 1.3'
30
+ spec.add_development_dependency 'httplog', '~> 1.0'
31
+ spec.add_development_dependency 'rake', '~> 12.3'
32
+ spec.add_development_dependency 'rspec', '~> 3.3'
33
+ spec.add_development_dependency 'rspec_junit_formatter', '~> 0.3.0'
34
+ spec.add_development_dependency 'rubocop', '0.69.0'
35
+ spec.add_development_dependency 'typhoeus', '~> 1.3'
36
+ spec.add_development_dependency 'webmock', '~> 3.0'
25
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wcc-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
- - Watermark Dev Team
8
- autorequire:
7
+ - Watermark Dev
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-12 00:00:00.000000000 Z
11
+ date: 2021-03-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: wcc-base
@@ -38,38 +38,199 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dotenv
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.10.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.10.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.9'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.9'
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.15'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.15'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.7'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.7'
97
+ - !ruby/object:Gem::Dependency
98
+ name: guard-rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.3'
111
+ - !ruby/object:Gem::Dependency
112
+ name: httplog
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.0'
41
125
  - !ruby/object:Gem::Dependency
42
126
  name: rake
43
127
  requirement: !ruby/object:Gem::Requirement
44
128
  requirements:
45
129
  - - "~>"
46
130
  - !ruby/object:Gem::Version
47
- version: '10.0'
131
+ version: '12.3'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '12.3'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '3.3'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '3.3'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rspec_junit_formatter
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 0.3.0
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 0.3.0
167
+ - !ruby/object:Gem::Dependency
168
+ name: rubocop
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - '='
172
+ - !ruby/object:Gem::Version
173
+ version: 0.69.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - '='
179
+ - !ruby/object:Gem::Version
180
+ version: 0.69.0
181
+ - !ruby/object:Gem::Dependency
182
+ name: typhoeus
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '1.3'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '1.3'
195
+ - !ruby/object:Gem::Dependency
196
+ name: webmock
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '3.0'
48
202
  type: :development
49
203
  prerelease: false
50
204
  version_requirements: !ruby/object:Gem::Requirement
51
205
  requirements:
52
206
  - - "~>"
53
207
  - !ruby/object:Gem::Version
54
- version: '10.0'
55
- description: holds common code used in our applications that host APIs.
208
+ version: '3.0'
209
+ description: Holds common code used in our applications that host APIs and those that
210
+ consume them.
56
211
  email:
57
212
  - dev@watermark.org
58
213
  executables: []
59
214
  extensions: []
60
215
  extra_rdoc_files: []
61
216
  files:
62
- - ".gitignore"
63
- - Gemfile
64
217
  - LICENSE.txt
65
218
  - README.md
66
- - Rakefile
67
219
  - lib/wcc/api.rb
220
+ - lib/wcc/api/active_record_shim.rb
68
221
  - lib/wcc/api/base_query.rb
222
+ - lib/wcc/api/controller_helpers.rb
69
223
  - lib/wcc/api/json.rb
70
224
  - lib/wcc/api/json/pagination.rb
71
225
  - lib/wcc/api/railtie.rb
226
+ - lib/wcc/api/rest_client.rb
227
+ - lib/wcc/api/rest_client/api_error.rb
228
+ - lib/wcc/api/rest_client/builder.rb
229
+ - lib/wcc/api/rest_client/http_adapter.rb
230
+ - lib/wcc/api/rest_client/response.rb
231
+ - lib/wcc/api/rest_client/typhoeus_adapter.rb
72
232
  - lib/wcc/api/rspec.rb
233
+ - lib/wcc/api/rspec/cache_header_examples.rb
73
234
  - lib/wcc/api/rspec/collection_matchers.rb
74
235
  - lib/wcc/api/rspec/pagination_examples.rb
75
236
  - lib/wcc/api/version.rb
@@ -79,7 +240,7 @@ homepage: https://github.com/watermarkchurch/wcc-api
79
240
  licenses:
80
241
  - MIT
81
242
  metadata: {}
82
- post_install_message:
243
+ post_install_message:
83
244
  rdoc_options: []
84
245
  require_paths:
85
246
  - lib
@@ -94,9 +255,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
255
  - !ruby/object:Gem::Version
95
256
  version: '0'
96
257
  requirements: []
97
- rubyforge_project:
98
- rubygems_version: 2.4.2
99
- signing_key:
258
+ rubyforge_project:
259
+ rubygems_version: 2.6.11
260
+ signing_key:
100
261
  specification_version: 4
101
- summary: Holds common code used in our applications that host APIs.
262
+ summary: Holds common code used in our applications that host APIs and those that
263
+ consume them.
102
264
  test_files: []
data/.gitignore DELETED
@@ -1,14 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- *.bundle
11
- *.so
12
- *.o
13
- *.a
14
- mkmf.log
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in wcc-api.gemspec
4
- gemspec
data/Rakefile DELETED
@@ -1,2 +0,0 @@
1
- require "bundler/gem_tasks"
2
-