lhs 8.0.0 → 9.0.0

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
2
  SHA1:
3
- metadata.gz: 0e172deae82bb63cb78f7a2d96a261620ecf09af
4
- data.tar.gz: b1caad27952e66468718b4f070b125c565fb39cb
3
+ metadata.gz: a67846d69ae33c0fc39973108dcad1e2b996fd60
4
+ data.tar.gz: 231e57735eeb0ce6a71a567d311148a69257178a
5
5
  SHA512:
6
- metadata.gz: d0d4d2fc97519b154413f07dc2f379ca88f3f21dc5fa5b7f87e1354386f725a47edaf20169b88a1a721e8ababd46dfb11bf8d13f057157f8122c39f332af2649
7
- data.tar.gz: 354e6e31fc84edc91190a733c9c99a15d0a84be9d003e44ed951df18c70ce911d1380356930d85f8d281c99afeae71068ae9226af554fef0899271c5d73fd22c
6
+ metadata.gz: 91193cb240275698ce0e2571ff24588cc411f4111260663fce90258c20f2cf9c37b346abcefc40ca91661aeb6b46fff39c6ee4339dcb86e8677c81c605a6db3e
7
+ data.tar.gz: c13f28d8bea1e6e73dcb0595851195b0c3168eb002268b48b646a636cc52baaf14c0adb8e77708b1b9c6cf1d9416ebd238f4b0b8d71c92d0019ebe39bd672936
data/README.md CHANGED
@@ -233,7 +233,7 @@ You can apply options to the request chain. Those options will be forwarded to t
233
233
 
234
234
  **Be careful using methods for batch processing. They could result in a lot of HTTP requests!**
235
235
 
236
- `all` fetches all records from the service by doing multiple requests if necessary.
236
+ `all` fetches all records from the service by doing multiple requests and resolving endpoint pagination if necessary.
237
237
 
238
238
  ```ruby
239
239
  data = Record.all
@@ -241,6 +241,15 @@ data.count # 998
241
241
  data.length # 998
242
242
  ```
243
243
 
244
+ `all` is chainable and has the same interface like `where` (See: [Find multiple records](https://github.com/local-ch/lhs#find-multiple-records))
245
+
246
+ ```ruby
247
+ Record.where(color: 'blue').all
248
+ Record.all.where(color: 'blue')
249
+ Record.all(color: 'blue')
250
+ # All three are doing the same thing: fetching all records with the color 'blue' from the endpoint while resolving pagingation if endpoint is paginated
251
+ ```
252
+
244
253
  [Count vs. Length](#count-vs-length)
245
254
 
246
255
  `find_each` is a more fine grained way to process single records that are fetched in batches.
@@ -844,6 +853,62 @@ end
844
853
  Example.find(1) # GET records/1
845
854
  ```
846
855
 
856
+ ## Testing: How to write tests when using LHS
857
+
858
+ [WebMock](https://github.com/bblimke/webmock)!
859
+
860
+ Best practice is to let LHS fetch your records and Webmock to stub/mock endpoints responses.
861
+ This follows the [Black Box Testing](https://en.wikipedia.org/wiki/Black-box_testing) approach and prevents you from building up constraints to LHS' internal structures/mechanisms, which will break when we change internal things.
862
+ LHS provides interfaces that result in HTTP requests, this is what you should test.
863
+
864
+ ```ruby
865
+ let(:contracts) do
866
+ [
867
+ {number: '1'},
868
+ {number: '2'},
869
+ {number: '3'}
870
+ ]
871
+ end
872
+
873
+ before(:each) do
874
+ stub_request(:get, "http://datastore/user/:id/contracts")
875
+ .to_return(
876
+ body: {
877
+ items: contracts,
878
+ limit: 10,
879
+ total: contracts.length,
880
+ offset: 0
881
+ }.to_json
882
+ )
883
+ end
884
+
885
+ it 'displays contracts' do
886
+ visit 'contracts'
887
+ contracts.each do |contract|
888
+ expect(page).to have_content(contract[:number])
889
+ end
890
+ end
891
+ ```
892
+
893
+ ## Where values hash
894
+
895
+ Returns a hash of where conditions.
896
+ Common to use in tests, as where queries are not performing any HTTP-requests when no data is accessed.
897
+
898
+ ```ruby
899
+ records = Record.where(color: 'blue').where(available: true).where(color: 'red')
900
+
901
+ expect(
902
+ records
903
+ ).to have_requested(:get, %r{records/})
904
+ .with(query: hash_including(color: 'blue', available: true))
905
+ # will fail as no http request is made (no data requested)
906
+
907
+ expect(
908
+ records.where_values_hash
909
+ ).to eq {color: 'red', available: true}
910
+ ```
911
+
847
912
  ## License
848
913
 
849
914
  [GNU Affero General Public License Version 3.](https://www.gnu.org/licenses/agpl-3.0.en.html)
@@ -34,6 +34,14 @@ class LHS::Collection < LHS::Proxy
34
34
  false
35
35
  end
36
36
 
37
+ def raw_items
38
+ if _raw.is_a?(Array)
39
+ _raw
40
+ else
41
+ _raw[items_key]
42
+ end
43
+ end
44
+
37
45
  protected
38
46
 
39
47
  def method_missing(name, *args, &block)
@@ -15,6 +15,12 @@ class LHS::Record
15
15
  Chain.new(self, Parameter.new(hash))
16
16
  end
17
17
 
18
+ def all(hash = nil)
19
+ chain = Chain.new(self, Parameter.new(hash))
20
+ chain._links.push(Option.new(all: true))
21
+ chain
22
+ end
23
+
18
24
  def options(hash = nil)
19
25
  Chain.new(self, Option.new(hash))
20
26
  end
@@ -171,6 +177,10 @@ class LHS::Record
171
177
  push(Parameter.new(hash))
172
178
  end
173
179
 
180
+ def all(hash = nil)
181
+ push([Parameter.new(hash), Option.new(all: true)])
182
+ end
183
+
174
184
  def options(hash = nil)
175
185
  push(Option.new(hash))
176
186
  end
@@ -323,7 +333,7 @@ class LHS::Record
323
333
 
324
334
  def push(link)
325
335
  clone = self.clone
326
- clone._links = _links + [link].compact
336
+ clone._links = _links + [link].flatten.compact
327
337
  clone
328
338
  end
329
339
 
@@ -18,6 +18,14 @@ class LHS::Record
18
18
 
19
19
  private
20
20
 
21
+ # Applies limit to the first request of an all request chain
22
+ # Tries to apply an high value for limit and reacts on the limit
23
+ # returned by the endpoint to make further requests
24
+ def apply_limit!(options)
25
+ options[:params] ||= {}
26
+ options[:params] = options[:params].merge(limit_key => options[:params][limit_key] || LHS::Pagination::Base::DEFAULT_LIMIT)
27
+ end
28
+
21
29
  # Convert URLs in options to endpoint templates
22
30
  def convert_options_to_endpoints(options)
23
31
  if options.is_a?(Array)
@@ -160,6 +168,42 @@ class LHS::Record
160
168
  end
161
169
  end
162
170
 
171
+ # After fetching the first page,
172
+ # we can evaluate if there are further remote objects remaining
173
+ # and after preparing all the requests that have to be made in order to fetch all
174
+ # remote items during this batch, they are fetched in parallel
175
+ def load_and_merge_remaining_objects!(data, options)
176
+ if paginated?(data._raw)
177
+ load_and_merge_paginated_collection!(data, options)
178
+ elsif data.collection? && paginated?(data.first._raw)
179
+ load_and_merge_set_of_paginated_collections!(data, options)
180
+ end
181
+ end
182
+
183
+ def load_and_merge_paginated_collection!(data, options)
184
+ pagination = data._record.pagination(data)
185
+ return data if pagination.pages_left.zero?
186
+ record = data._record
187
+ record.request(
188
+ options_for_next_batch(record, pagination, options)
189
+ ).each do |batch_data|
190
+ merge_batch_data_with_parent!(batch_data, data)
191
+ end
192
+ end
193
+
194
+ def load_and_merge_set_of_paginated_collections!(data, options)
195
+ options_for_this_batch = []
196
+ options.each_with_index do |_, index|
197
+ record = data[index]._record
198
+ pagination = record.pagination(data[index])
199
+ next if pagination.pages_left.zero?
200
+ options_for_this_batch.push(options_for_next_batch(record, pagination, options[index], data[index]))
201
+ end
202
+ data._record.request(options_for_this_batch.flatten).each do |batch_data|
203
+ merge_batch_data_with_parent!(batch_data, batch_data._request.options[:parent_data])
204
+ end
205
+ end
206
+
163
207
  # Load additional resources that are requested with include
164
208
  def load_include(options, data, sub_includes, references)
165
209
  record = record_for_options(options) || self
@@ -191,10 +235,15 @@ class LHS::Record
191
235
  # paginates itself to ensure all records are fetched
192
236
  def load_all_included!(record, options)
193
237
  data = record.request(options)
194
- load_and_merge_all_the_rest!(data, options)
238
+ load_and_merge_remaining_objects!(data, options)
195
239
  data
196
240
  end
197
241
 
242
+ # Checks if given raw is paginated or not
243
+ def paginated?(raw)
244
+ !!(raw.is_a?(Hash) && raw[total_key] && raw[pagination_key])
245
+ end
246
+
198
247
  def prepare_options_for_include_all_request!(options)
199
248
  if options.is_a?(Array)
200
249
  options.each do |option|
@@ -225,6 +274,13 @@ class LHS::Record
225
274
  options || {}
226
275
  end
227
276
 
277
+ def merge_batch_data_with_parent!(batch_data, parent_data)
278
+ parent_data._raw[items_key].concat batch_data.raw_items
279
+ parent_data._raw[limit_key] = batch_data._raw[limit_key]
280
+ parent_data._raw[total_key] = batch_data._raw[total_key]
281
+ parent_data._raw[pagination_key] = batch_data._raw[pagination_key]
282
+ end
283
+
228
284
  # Merge explicit params nested in 'params' namespace with original hash.
229
285
  def merge_explicit_params!(params)
230
286
  return true unless params
@@ -273,8 +329,26 @@ class LHS::Record
273
329
  end
274
330
  end
275
331
 
332
+ def options_for_next_batch(record, pagination, options, parent_data = nil)
333
+ batch_options = []
334
+ pagination.pages_left.times do |index|
335
+ page_options = {
336
+ params: {
337
+ record.limit_key => pagination.limit,
338
+ record.pagination_key => pagination.next_offset(index + 1)
339
+ }
340
+ }
341
+ page_options[:parent_data] = parent_data if parent_data
342
+ batch_options.push(
343
+ options.deep_dup.deep_merge(page_options)
344
+ )
345
+ end
346
+ batch_options
347
+ end
348
+
276
349
  # Merge explicit params and take configured endpoints options as base
277
350
  def process_options(options, endpoint)
351
+ options = options.deep_dup
278
352
  options[:params].deep_symbolize_keys! if options[:params]
279
353
  options[:error_handler] = merge_error_handlers(options[:error_handler]) if options[:error_handler]
280
354
  options = (endpoint.options || {}).merge(options)
@@ -318,12 +392,14 @@ class LHS::Record
318
392
 
319
393
  def single_request(options)
320
394
  options ||= {}
395
+ options = options.dup
321
396
  including = options.delete(:including)
322
397
  referencing = options.delete(:referencing)
323
- options = options.dup
324
398
  endpoint = find_endpoint(options[:params])
399
+ apply_limit!(options) if options[:all]
325
400
  response = LHC.request(process_options(options, endpoint))
326
401
  data = LHS::Data.new(response.body, nil, self, response.request, endpoint)
402
+ load_and_merge_remaining_objects!(data, process_options(options, endpoint)) if paginated?(data._raw) && options[:all]
327
403
  handle_includes(including, data, referencing) if including.present? && data.present?
328
404
  data
329
405
  end
@@ -1,7 +1,6 @@
1
1
  Dir[File.dirname(__FILE__) + '/concerns/record/*.rb'].each { |file| require file }
2
2
 
3
3
  class LHS::Record
4
- include All
5
4
  include Batch
6
5
  include Chainable
7
6
  include Configuration
@@ -1,3 +1,3 @@
1
1
  module LHS
2
- VERSION = "8.0.0"
2
+ VERSION = "9.0.0"
3
3
  end
@@ -31,7 +31,7 @@ describe LHS::Collection do
31
31
 
32
32
  context 'lets you configure how to deal with collections' do
33
33
  it 'initalises and gives access to collections according to configuration' do
34
- results = Search.all(params: { type: :phonebook, size: 10 })
34
+ results = Search.all(type: :phonebook, size: 10)
35
35
  expect(results.count).to eq total
36
36
  expect(results.total).to eq total
37
37
  expect(results.limit).to eq limit
@@ -1,23 +1,61 @@
1
1
  require 'rails_helper'
2
2
 
3
3
  describe LHS::Record do
4
- let(:datastore) do
5
- 'http://datastore/v2'
6
- end
7
-
8
4
  before(:each) do
9
- LHC.config.placeholder('datastore', datastore)
10
5
  class Record < LHS::Record
11
- endpoint ':datastore/feedbacks'
6
+ endpoint 'http://datastore/feedbacks'
12
7
  end
13
8
  end
14
9
 
15
10
  context 'all' do
16
11
  it 'is querying endpoint without pagination when using all' do
17
- stub_request(:get, "#{datastore}/feedbacks?limit=100").to_return(status: 200, body: { items: 300.times.map { { foo: 'bar' } }, total: 300 }.to_json)
12
+ stub_request(:get, "http://datastore/feedbacks?limit=100").to_return(body: { items: 300.times.map { { foo: 'bar' } }, total: 300 }.to_json)
18
13
  records = Record.all
19
14
  expect(records).to be_kind_of Record
20
15
  expect(records.size).to eq(300)
21
16
  end
17
+
18
+ context 'is chainable with where and works like where' do
19
+ let(:total) { 22 }
20
+ let(:limit) { 10 }
21
+ let!(:first_page_request) do
22
+ stub_request(:get, "http://datastore/feedbacks?color=blue&limit=100")
23
+ .to_return(body: { items: 10.times.map { { foo: 'bar' } }, total: total, limit: limit, offset: 0 }.to_json)
24
+ end
25
+ let!(:second_page_request) do
26
+ stub_request(:get, "http://datastore/feedbacks?color=blue&limit=#{limit}&offset=10")
27
+ .to_return(body: { items: 10.times.map { { foo: 'bar' } }, total: total, limit: limit, offset: 10 }.to_json)
28
+ end
29
+ let!(:third_page_request) do
30
+ stub_request(:get, "http://datastore/feedbacks?color=blue&limit=#{limit}&offset=20")
31
+ .to_return(body: { items: 2.times.map { { foo: 'bar' } }, total: total, limit: limit, offset: 20 }.to_json)
32
+ end
33
+
34
+ it 'fetches all remote objects' do
35
+ records = Record.where(color: 'blue').all
36
+ expect(records.length).to eq total
37
+ expect(first_page_request).to have_been_requested.times(1)
38
+ expect(second_page_request).to have_been_requested.times(1)
39
+ expect(third_page_request).to have_been_requested.times(1)
40
+ records = Record.all.where(color: 'blue')
41
+ expect(records.length).to eq total
42
+ expect(first_page_request).to have_been_requested.times(2)
43
+ expect(second_page_request).to have_been_requested.times(2)
44
+ expect(third_page_request).to have_been_requested.times(2)
45
+ records = Record.all(color: 'blue')
46
+ expect(records.length).to eq total
47
+ expect(first_page_request).to have_been_requested.times(3)
48
+ expect(second_page_request).to have_been_requested.times(3)
49
+ expect(third_page_request).to have_been_requested.times(3)
50
+ end
51
+
52
+ it 'works in combination with include and includes' do
53
+ records = Record.includes(:product).includes_all(:options).all(color: 'blue')
54
+ expect(records.length).to eq total
55
+ expect(first_page_request).to have_been_requested.times(1)
56
+ expect(second_page_request).to have_been_requested.times(1)
57
+ expect(third_page_request).to have_been_requested.times(1)
58
+ end
59
+ end
22
60
  end
23
61
  end
@@ -34,7 +34,7 @@ describe LHS::Record do
34
34
  stub_request(:get, 'http://records?color=blue').to_return(body: [].to_json)
35
35
  Example.where(color: 'blue')
36
36
  expect(
37
- -> { Base.all }
37
+ -> { Base.all.first }
38
38
  ).to raise_error(RuntimeError, 'Compilation incomplete. Unable to find value for id.')
39
39
  end
40
40
  end
@@ -52,7 +52,7 @@ describe LHS::Record do
52
52
 
53
53
  it 'inherits endpoints based on ruby class_attribute behaviour' do
54
54
  request = stub_request(:get, 'http://records?limit=100').to_return(body: [].to_json)
55
- Base.all
55
+ Base.all.first
56
56
  assert_requested(request)
57
57
 
58
58
  request = stub_request(:get, 'http://examples/1').to_return(body: {}.to_json)
@@ -71,6 +71,7 @@ describe LHS::Record do
71
71
  body: { items: (201..300).to_a, limit: 100, total: 300, offset: 200 }.to_json
72
72
  )
73
73
  all = Record.all
74
+ all.first # fetch/resolve
74
75
  assert_requested last_request
75
76
  expect(all).to be_kind_of Record
76
77
  expect(all._data._proxy).to be_kind_of LHS::Collection
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lhs
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.0
4
+ version: 9.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - https://github.com/local-ch/lhs/graphs/contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-31 00:00:00.000000000 Z
11
+ date: 2017-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lhc
@@ -205,7 +205,6 @@ files:
205
205
  - lib/lhs/concerns/proxy/accessors.rb
206
206
  - lib/lhs/concerns/proxy/create.rb
207
207
  - lib/lhs/concerns/proxy/link.rb
208
- - lib/lhs/concerns/record/all.rb
209
208
  - lib/lhs/concerns/record/batch.rb
210
209
  - lib/lhs/concerns/record/chainable.rb
211
210
  - lib/lhs/concerns/record/configuration.rb
@@ -1,98 +0,0 @@
1
- require 'active_support'
2
-
3
- class LHS::Record
4
-
5
- module All
6
- extend ActiveSupport::Concern
7
-
8
- module ClassMethods
9
- # Should be an edge case but sometimes all objects from a certain resource
10
- # are required. In this case we load the first page with the default max limit,
11
- # compute the amount of left over requests, do all the the left over requests
12
- # for the following pages and concatenate all the results in order to return
13
- # all the objects for a given resource.
14
- def all(options = {})
15
- options ||= {}
16
- options[:params] ||= {}
17
- options[:params] = options[:params].merge(limit_key => options[:params][limit_key] || LHS::Pagination::Base::DEFAULT_LIMIT)
18
- data = request(options)
19
- load_and_merge_all_the_rest!(data, options) if paginated?(data._raw)
20
- data._record.new(LHS::Data.new(data, nil, self))
21
- end
22
-
23
- private
24
-
25
- def paginated?(raw)
26
- raw.is_a?(Hash) && raw[total_key] && raw[pagination_key]
27
- end
28
-
29
- def all_items_from(data)
30
- if data._raw.is_a?(Array)
31
- data._raw
32
- else
33
- data._raw[items_key]
34
- end
35
- end
36
-
37
- # After fetching the first page,
38
- # we can evaluate if there are further remote objects remaining
39
- # and after preparing all the requests that have to be made in order to fetch all
40
- # remote items during this batch, they are fetched in parallel
41
- def load_and_merge_all_the_rest!(data, options)
42
- if paginated?(data._raw)
43
- load_and_merge_paginated_collection!(data, options)
44
- elsif data.collection? && paginated?(data.first._raw)
45
- load_and_merge_set_of_paginated_collections!(data, options)
46
- end
47
- end
48
-
49
- def load_and_merge_paginated_collection!(data, options)
50
- pagination = data._record.pagination(data)
51
- return data if pagination.pages_left.zero?
52
- record = data._record
53
- record.request(
54
- options_for_next_batch(record, pagination, options)
55
- ).each do |batch_data|
56
- merge_batch_data_with_parent!(batch_data, data)
57
- end
58
- end
59
-
60
- def load_and_merge_set_of_paginated_collections!(data, options)
61
- options_for_this_batch = []
62
- options.each_with_index do |_, index|
63
- record = data[index]._record
64
- pagination = record.pagination(data[index])
65
- next if pagination.pages_left.zero?
66
- options_for_this_batch.push(options_for_next_batch(record, pagination, options[index], data[index]))
67
- end
68
- data._record.request(options_for_this_batch.flatten).each do |batch_data|
69
- merge_batch_data_with_parent!(batch_data, batch_data._request.options[:parent_data])
70
- end
71
- end
72
-
73
- def merge_batch_data_with_parent!(batch_data, parent_data)
74
- parent_data._raw[items_key].concat all_items_from batch_data
75
- parent_data._raw[limit_key] = batch_data._raw[limit_key]
76
- parent_data._raw[total_key] = batch_data._raw[total_key]
77
- parent_data._raw[pagination_key] = batch_data._raw[pagination_key]
78
- end
79
-
80
- def options_for_next_batch(record, pagination, options, parent_data = nil)
81
- batch_options = []
82
- pagination.pages_left.times do |index|
83
- page_options = {
84
- params: {
85
- record.limit_key => pagination.limit,
86
- record.pagination_key => pagination.next_offset(index + 1)
87
- }
88
- }
89
- page_options[:parent_data] = parent_data if parent_data
90
- batch_options.push(
91
- options.deep_dup.deep_merge(page_options)
92
- )
93
- end
94
- batch_options
95
- end
96
- end
97
- end
98
- end