wcc-api 0.3.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
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