wcc-api 0.3.1 → 0.5.2

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
- SHA1:
3
- metadata.gz: 7bedfb36a9e68268018251cd3eed45b74c874e56
4
- data.tar.gz: 32a2c0d92acef030ba5f8c36f4f851352798fc0d
2
+ SHA256:
3
+ metadata.gz: 28ce3f8ce50bf220052fdf1f607457725083577e2085927ac565aa6f2c7a1c93
4
+ data.tar.gz: 16c1f3e5329e8b711c0cdab943d4c270c35b186f71875278776499b4e47b4323
5
5
  SHA512:
6
- metadata.gz: 6b8ea49a7dc7753e303fff2304bcade5fd9ea9090fc8b8c42d806cd007adaec3297828e20e154989f082206a5c65d7205728b6746117acbd0afb9c4ddfe065a8
7
- data.tar.gz: 4429bff8721f6677c354d97cec257f516af79aa9433c772696cad5be3395ac18c8305ba6d81988fcaf9125ba88f6458bee775dad96f6eb0ad9a760ae36d987e6
6
+ metadata.gz: da429643f12efb3b7c136324ce7598980efc85192c06fc566790a7e9440bd54a6051c646a1f931b80a15f0da0b52f40a6df60f53d2733b0195a3d1a963f26581
7
+ data.tar.gz: 1866949150576051ed35474f0f1b165c377ebcb5e3678d752eb01b2c5411a210b6ad1a0ca38e05cbd5ad1c1a6c320195ae0595d6de6935e98608510a17ed3708
@@ -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
@@ -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
@@ -13,7 +13,8 @@ module WCC::API
13
13
  attr_reader :client
14
14
  attr_reader :request
15
15
 
16
- def_delegators :raw_response, :code, :headers
16
+ def_delegators :raw_response, :status, :headers
17
+ alias_method :code, :status
17
18
 
18
19
  def body
19
20
  @body ||= ::JSON.parse(raw_body)
@@ -55,6 +56,7 @@ module WCC::API
55
56
 
56
57
  def next_page?
57
58
  return false unless collection_response?
59
+ return false if count.nil?
58
60
 
59
61
  page_items.length + skip < count
60
62
  end
@@ -62,11 +64,11 @@ module WCC::API
62
64
  def next_page
63
65
  return unless next_page?
64
66
 
65
- @next_page ||= @client.get(
67
+ next_page ||= @client.get(
66
68
  @request[:url],
67
69
  (@request[:query] || {}).merge(next_page_query)
68
70
  )
69
- @next_page.assert_ok!
71
+ next_page.assert_ok!
70
72
  end
71
73
 
72
74
  def assert_ok!
@@ -79,16 +81,7 @@ module WCC::API
79
81
  def each_page(&block)
80
82
  raise ArgumentError, 'Not a collection response' unless collection_response?
81
83
 
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
84
+ ret = PaginatingEnumerable.new(self)
92
85
 
93
86
  if block_given?
94
87
  ret.map(&block)
@@ -131,5 +124,25 @@ module WCC::API
131
124
  body['items']
132
125
  end
133
126
  end
127
+
128
+ class PaginatingEnumerable
129
+ include Enumerable
130
+
131
+ def initialize(initial_page)
132
+ raise ArgumentError, 'Must provide initial page' unless initial_page
133
+
134
+ @initial_page = initial_page
135
+ end
136
+
137
+ def each
138
+ page = @initial_page
139
+ yield page
140
+
141
+ while page.next_page?
142
+ page = page.next_page
143
+ yield page
144
+ end
145
+ end
146
+ end
134
147
  end
135
148
  end
@@ -4,31 +4,60 @@ require 'forwardable'
4
4
  gem 'typhoeus'
5
5
  require 'typhoeus'
6
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
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
32
60
  end
33
61
  end
34
62
  end
63
+
@@ -2,11 +2,23 @@
2
2
 
3
3
  require 'wcc'
4
4
  require_relative 'rest_client/response'
5
+ require_relative 'rest_client/builder'
6
+ require_relative 'active_record_shim'
5
7
 
6
8
  module WCC::API
7
9
  class RestClient
8
10
  attr_reader :api_url
9
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
+
10
22
  def initialize(api_url:, headers: nil, **options)
11
23
  # normalizes a URL to have a slash on the end
12
24
  @api_url = api_url.gsub(/\/+$/, '') + '/'
@@ -32,13 +44,39 @@ module WCC::API
32
44
  get_http(url, query))
33
45
  end
34
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
+
35
75
  ADAPTERS = {
36
- http: ['http', '> 1.0', '< 3.0'],
76
+ faraday: ['faraday', '~> 0.9'],
37
77
  typhoeus: ['typhoeus', '~> 1.0']
38
78
  }.freeze
39
79
 
40
- # This method is long due to the case statement,
41
- # not really a better way to do it
42
80
  def self.load_adapter(adapter)
43
81
  case adapter
44
82
  when nil
@@ -52,16 +90,19 @@ module WCC::API
52
90
  end
53
91
  raise ArgumentError, 'Unable to load adapter! Please install one of '\
54
92
  "#{ADAPTERS.values.map(&:join).join(',')}"
55
- when :http
56
- require_relative 'rest_client/http_adapter'
57
- HttpAdapter.new
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
58
99
  when :typhoeus
59
100
  require_relative 'rest_client/typhoeus_adapter'
60
101
  TyphoeusAdapter.new
61
102
  else
62
- unless adapter.respond_to?(:call)
103
+ unless adapter.respond_to?(:get)
63
104
  raise ArgumentError, "Adapter #{adapter} is not invokeable! Please "\
64
- "pass a proc or use one of #{ADAPTERS.keys}"
105
+ "pass use one of #{ADAPTERS.keys} or create a Faraday-compatible adapter"
65
106
  end
66
107
  adapter
67
108
  end
@@ -69,16 +110,129 @@ module WCC::API
69
110
 
70
111
  private
71
112
 
72
- def get_http(url, query, headers = {}, proxy = {})
113
+ def get_http(url, query, headers = {})
73
114
  headers = @headers.merge(headers || {})
74
115
 
75
116
  q = @query_defaults.dup
76
117
  q = q.merge(query) if query
77
118
 
78
- resp = @adapter.call(url, q, headers, proxy)
119
+ resp = @adapter.get(url, q, headers)
79
120
 
80
- resp = get_http(resp.headers['location'], nil, headers, proxy) if [301, 302, 307].include?(resp.code) && !@options[:no_follow_redirects]
121
+ resp = get_http(resp.headers['location'], nil, headers) if [301, 302, 307].include?(resp.status) && !@options[:no_follow_redirects]
81
122
  resp
82
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
83
237
  end
84
238
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module WCC
4
4
  module API
5
- VERSION = '0.3.1'
5
+ VERSION = '0.5.2'
6
6
  end
7
7
  end
data/wcc-api.gemspec CHANGED
@@ -15,21 +15,22 @@ Gem::Specification.new do |spec|
15
15
  spec.homepage = 'https://github.com/watermarkchurch/wcc-api'
16
16
  spec.license = 'MIT'
17
17
 
18
- spec.files = `git ls-files -z`.split("\x0")
19
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.files = Dir['lib/**/*'] + %w[LICENSE.txt README.md wcc-api.gemspec]
20
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
20
  spec.require_paths = ['lib']
22
21
 
23
22
  spec.add_dependency 'wcc-base'
24
23
 
25
- spec.add_development_dependency 'bundler', '~> 1.6'
26
24
  spec.add_development_dependency 'dotenv', '~> 0.10.0'
27
- spec.add_development_dependency 'http', '> 1.0', '< 3.0'
25
+ spec.add_development_dependency 'faraday', '~> 0.9'
26
+ spec.add_development_dependency 'guard', '~> 2.15'
27
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
28
+ spec.add_development_dependency 'guard-rubocop', '~> 1.3'
28
29
  spec.add_development_dependency 'httplog', '~> 1.0'
29
30
  spec.add_development_dependency 'rake', '~> 12.3'
30
31
  spec.add_development_dependency 'rspec', '~> 3.3'
31
32
  spec.add_development_dependency 'rspec_junit_formatter', '~> 0.3.0'
32
- spec.add_development_dependency 'rubocop'
33
+ spec.add_development_dependency 'rubocop', '0.69.0'
33
34
  spec.add_development_dependency 'typhoeus', '~> 1.3'
34
35
  spec.add_development_dependency 'webmock', '~> 3.0'
35
36
  end