lhs 8.0.0 → 9.0.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 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