lhs 7.4.1 → 8.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: 6716a12c5196762c84c68b946df362158e9c62f0
4
- data.tar.gz: 594c16d1e84a6873c77560ae6ae229149c7e4813
3
+ metadata.gz: 0e172deae82bb63cb78f7a2d96a261620ecf09af
4
+ data.tar.gz: b1caad27952e66468718b4f070b125c565fb39cb
5
5
  SHA512:
6
- metadata.gz: 8ac1f9e2374f8aeddaeb1c22640183279e78665e5576650367ff6a1474e96113dd4c22e03899d34829b2ad3bbdbbc916189e153434734a94e52becb1c6f5bc35
7
- data.tar.gz: 9b42e020507260c37d906112bfbc99945c077a9546857ef41858d55c86cc069fe0abec4ffb816957f9d78cdecef24edb13738ee3cdd35b26e25ce8af14b6b519
6
+ metadata.gz: d0d4d2fc97519b154413f07dc2f379ca88f3f21dc5fa5b7f87e1354386f725a47edaf20169b88a1a721e8ababd46dfb11bf8d13f057157f8122c39f332af2649
7
+ data.tar.gz: 354e6e31fc84edc91190a733c9c99a15d0a84be9d003e44ed951df18c70ce911d1380356930d85f8d281c99afeae71068ae9226af554fef0899271c5d73fd22c
data/README.md CHANGED
@@ -415,10 +415,30 @@ record.ratings # {:quality=>3}
415
415
 
416
416
  ## Include linked resources
417
417
 
418
- When fetching records, you can specify in advance all the linked resources that you want to include in the results. With `includes`, LHS ensures that all matching and explicitly linked resources are loaded and merged, if they're included in the server response.
418
+ When fetching records, you can specify in advance all the linked resources that you want to include in the results. With `includes` or `includes_all` (to enforce fetching all remote objects for paginated endpoints), LHS ensures that all matching and explicitly linked resources are loaded and merged.
419
419
 
420
420
  The implementation is heavily influenced by [http://guides.rubyonrails.org/active_record_class_querying](http://guides.rubyonrails.org/active_record_class_querying.html#eager-loading-associations) and you should read it to understand this feature in all its glory.
421
421
 
422
+ ### `includes_all` for paginated endpoints
423
+
424
+ In case endpoints are paginated and you are certain that you'll need all objects of a set and not only the first page/batch, use `includes_all`.
425
+
426
+ LHS will ensure that all linked resources are around by loading all pages (parallelized/performance optimized).
427
+
428
+ ```ruby
429
+ customer = Customer.includes_all(contracts: :products).find(1)
430
+
431
+ # GET http://datastore/customers/1
432
+ # GET http://datastore/customers/1/contracts?limit=100
433
+ # GET http://datastore/customers/1/contracts?limit=10&offset=10
434
+ # GET http://datastore/customers/1/contracts?limit=10&offset=20
435
+ # GET http://datastore/products?limit=100
436
+ # GET http://datastore/products?limit=10&offset=10
437
+
438
+ customer.contracts.length # 33
439
+ customer.contracts.first.products.length # 22
440
+ ```
441
+
422
442
  ### One-Level `includes`
423
443
 
424
444
  ```ruby
@@ -11,10 +11,12 @@ class LHS::Record
11
11
  # compute the amount of left over requests, do all the the left over requests
12
12
  # for the following pages and concatenate all the results in order to return
13
13
  # all the objects for a given resource.
14
- def all(params = {})
15
- limit = params[limit_key] || LHS::Pagination::DEFAULT_LIMIT
16
- data = request(params: params.merge(limit_key => limit))
17
- request_all_the_rest(data, params) if paginated?(data._raw)
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)
18
20
  data._record.new(LHS::Data.new(data, nil, self))
19
21
  end
20
22
 
@@ -32,26 +34,64 @@ class LHS::Record
32
34
  end
33
35
  end
34
36
 
35
- def request_all_the_rest(data, params)
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)
36
50
  pagination = data._record.pagination(data)
37
- if pagination.pages_left
38
- last_data = data
39
- pagination.pages_left.times do |_index|
40
- return data if last_data.length.zero?
41
- pagination = data._record.pagination(last_data)
42
- response_data = request(
43
- params: params.merge(
44
- data._record.limit_key => pagination.limit,
45
- data._record.pagination_key => pagination.next_offset
46
- )
47
- )
48
- data._raw[items_key].concat all_items_from response_data
49
- data._raw[limit_key] = response_data._raw[limit_key]
50
- data._raw[total_key] = response_data._raw[total_key]
51
- data._raw[pagination_key] = response_data._raw[pagination_key]
52
- last_data = response_data
53
- end
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
+ )
54
93
  end
94
+ batch_options
55
95
  end
56
96
  end
57
97
  end
@@ -20,7 +20,7 @@ class LHS::Record
20
20
  def find_in_batches(options = {})
21
21
  raise 'No block given' unless block_given?
22
22
  start = options[:start] || 1
23
- batch_size = options[:batch_size] || LHS::Pagination::DEFAULT_LIMIT
23
+ batch_size = options[:batch_size] || LHS::Pagination::Base::DEFAULT_LIMIT
24
24
  params = options[:params] || {}
25
25
  loop do # as suggested by Matz
26
26
  data = request(params: params.merge(limit_key => batch_size, pagination_key => start))
@@ -39,6 +39,12 @@ class LHS::Record
39
39
  Chain.new(self, Include.new(Chain.unfold(args)))
40
40
  end
41
41
 
42
+ def includes_all(*args)
43
+ chain = Chain.new(self, Include.new(Chain.unfold(args)))
44
+ chain.include_all!(args)
45
+ chain
46
+ end
47
+
42
48
  def references(*args)
43
49
  Chain.new(self, Reference.new(Chain.unfold(args)))
44
50
  end
@@ -162,36 +168,42 @@ class LHS::Record
162
168
  alias validate valid?
163
169
 
164
170
  def where(hash = nil)
165
- push Parameter.new(hash)
171
+ push(Parameter.new(hash))
166
172
  end
167
173
 
168
174
  def options(hash = nil)
169
- push Option.new(hash)
175
+ push(Option.new(hash))
170
176
  end
171
177
 
172
178
  def page(page)
173
- push Pagination.new(page: page)
179
+ push(Pagination.new(page: page))
174
180
  end
175
181
 
176
182
  def per(per)
177
- push Pagination.new(per: per)
183
+ push(Pagination.new(per: per))
178
184
  end
179
185
 
180
186
  def limit(argument = nil)
181
187
  return resolve.limit if argument.blank?
182
- push Pagination.new(per: argument)
188
+ push(Pagination.new(per: argument))
183
189
  end
184
190
 
185
191
  def handle(error_class, handler)
186
- push ErrorHandling.new(error_class => handler)
192
+ push(ErrorHandling.new(error_class => handler))
187
193
  end
188
194
 
189
195
  def includes(*args)
190
- push Include.new(Chain.unfold(args))
196
+ push(Include.new(Chain.unfold(args)))
197
+ end
198
+
199
+ def includes_all(*args)
200
+ chain = push(Include.new(Chain.unfold(args)))
201
+ chain.include_all!(args)
202
+ chain
191
203
  end
192
204
 
193
205
  def references(*args)
194
- push Reference.new(Chain.unfold(args))
206
+ push(Reference.new(Chain.unfold(args)))
195
207
  end
196
208
 
197
209
  def find(*args)
@@ -235,6 +247,14 @@ class LHS::Record
235
247
  chain_references
236
248
  end
237
249
 
250
+ # Adds additional .references(name_of_linked_resource: { all: true })
251
+ # to all linked resources included with includes_all
252
+ def include_all!(args)
253
+ includes_all_to_references(args).each do |reference|
254
+ _links.push(reference)
255
+ end
256
+ end
257
+
238
258
  protected
239
259
 
240
260
  def method_missing(name, *args, &block)
@@ -265,6 +285,42 @@ class LHS::Record
265
285
 
266
286
  private
267
287
 
288
+ # Translates includes_all(resources:) to the internal datastructure
289
+ # references(resource: { all: true })
290
+ def includes_all_to_references(args, parent = nil)
291
+ references = []
292
+ if args.is_a?(Array)
293
+ includes_all_to_references_for_arrays!(references, args, parent)
294
+ elsif args.is_a?(Hash)
295
+ includes_all_to_references_for_hash!(references, args, parent)
296
+ elsif args.is_a?(Symbol)
297
+ includes_all_to_references_for_symbol!(references, args, parent)
298
+ end
299
+ references
300
+ end
301
+
302
+ def includes_all_to_references_for_arrays!(references, args, parent)
303
+ args.each do |part|
304
+ references.concat(includes_all_to_references(part, parent))
305
+ end
306
+ end
307
+
308
+ def includes_all_to_references_for_hash!(references, args, parent)
309
+ args.each do |key, value|
310
+ parent ||= { all: true }
311
+ references.concat([Reference.new(key => parent)])
312
+ references.concat(includes_all_to_references(value, parent))
313
+ end
314
+ end
315
+
316
+ def includes_all_to_references_for_symbol!(references, args, parent)
317
+ if parent.present?
318
+ parent[args] = { all: true }
319
+ else
320
+ references.concat([Reference.new(args => { all: true })])
321
+ end
322
+ end
323
+
268
324
  def push(link)
269
325
  clone = self.clone
270
326
  clone._links = _links + [link].compact
@@ -306,7 +362,7 @@ class LHS::Record
306
362
  def resolve_pagination(links)
307
363
  return {} if links.empty?
308
364
  page = 1
309
- per = LHS::Pagination::DEFAULT_LIMIT
365
+ per = LHS::Pagination::Base::DEFAULT_LIMIT
310
366
  links.each do |link|
311
367
  page = link[:page] if link[:page].present?
312
368
  per = link[:per] if link[:per].present?
@@ -15,11 +15,11 @@ class LHS::Record
15
15
  def pagination_class
16
16
  case pagination_strategy.to_sym
17
17
  when :page
18
- LHS::PagePagination
18
+ LHS::Pagination::Page
19
19
  when :start
20
- LHS::StartPagination
20
+ LHS::Pagination::Start
21
21
  else
22
- LHS::OffsetPagination
22
+ LHS::Pagination::Offset
23
23
  end
24
24
  end
25
25
 
@@ -8,6 +8,7 @@ class LHS::Record
8
8
  module ClassMethods
9
9
  def request(options)
10
10
  options ||= {}
11
+ options = options.deep_dup
11
12
  if options.is_a? Array
12
13
  multiple_requests(options)
13
14
  else
@@ -164,17 +165,66 @@ class LHS::Record
164
165
  record = record_for_options(options) || self
165
166
  options = convert_options_to_endpoints(options) if record_for_options(options)
166
167
  begin
167
- if options.is_a?(Array)
168
- options.each { |options| options.merge!(including: sub_includes, referencing: references) if sub_includes.present? }
169
- elsif sub_includes.present?
170
- options.merge!(including: sub_includes, referencing: references)
168
+ prepare_options_for_include_request!(options, sub_includes, references)
169
+ if references && references[:all] # include all linked resources
170
+ prepare_options_for_include_all_request!(options)
171
+ data = load_all_included!(record, options)
172
+ references.delete(:all) # for this all remote objects have been fetched
173
+ continue_including(data, sub_includes, references)
174
+ else # simply request first page/batch
175
+ data = record.request(options)
176
+ warn "[WARNING] You included `#{options[:url]}`, but this endpoint is paginated. You might want to use `includes_all` instead of `includes` (https://github.com/local-ch/lhs#includes_all-for-paginated-endpoints)." if paginated?(data._raw)
177
+ data
171
178
  end
172
- record.request(options)
173
179
  rescue LHC::NotFound
174
180
  LHS::Data.new({}, data, record)
175
181
  end
176
182
  end
177
183
 
184
+ # Continues loading included resources after one complete batch/level has been fetched
185
+ def continue_including(data, including, referencing)
186
+ handle_includes(including, data, referencing) if including.present? && data.present?
187
+ data
188
+ end
189
+
190
+ # Loads all included/linked resources,
191
+ # paginates itself to ensure all records are fetched
192
+ def load_all_included!(record, options)
193
+ data = record.request(options)
194
+ load_and_merge_all_the_rest!(data, options)
195
+ data
196
+ end
197
+
198
+ def prepare_options_for_include_all_request!(options)
199
+ if options.is_a?(Array)
200
+ options.each do |option|
201
+ prepare_option_for_include_all_request!(option)
202
+ end
203
+ else
204
+ prepare_option_for_include_all_request!(options)
205
+ end
206
+ options
207
+ end
208
+
209
+ # When including all resources on one level, don't forward :includes & :references
210
+ # as we have to fetch all resources on this level first, before we continue_including
211
+ def prepare_option_for_include_all_request!(option)
212
+ option[:params] ||= {}
213
+ option[:params].merge!(limit_key => option.fetch(:params, {}).fetch(limit_key, LHS::Pagination::Base::DEFAULT_LIMIT))
214
+ option.delete(:including)
215
+ option.delete(:referencing)
216
+ option
217
+ end
218
+
219
+ def prepare_options_for_include_request!(options, sub_includes, references)
220
+ if options.is_a?(Array)
221
+ options.each { |option| option.merge!(including: sub_includes, referencing: references) if sub_includes.present? }
222
+ elsif sub_includes.present?
223
+ options.merge!(including: sub_includes, referencing: references)
224
+ end
225
+ options || {}
226
+ end
227
+
178
228
  # Merge explicit params nested in 'params' namespace with original hash.
179
229
  def merge_explicit_params!(params)
180
230
  return true unless params
@@ -195,7 +245,7 @@ class LHS::Record
195
245
  referencing = LHS::Complex.reduce options.compact.map { |options| options.delete(:referencing) }.compact
196
246
  data = restore_with_nils(data, locate_nils(options)) # nil objects in data provide location information for mapping
197
247
  data = LHS::Data.new(data, nil, self)
198
- handle_includes(including, data, referencing) if including.present? && !data.empty?
248
+ handle_includes(including, data, referencing) if including.present? && data.present?
199
249
  data
200
250
  end
201
251
 
@@ -274,7 +324,7 @@ class LHS::Record
274
324
  endpoint = find_endpoint(options[:params])
275
325
  response = LHC.request(process_options(options, endpoint))
276
326
  data = LHS::Data.new(response.body, nil, self, response.request, endpoint)
277
- handle_includes(including, data, referencing) if including
327
+ handle_includes(including, data, referencing) if including.present? && data.present?
278
328
  data
279
329
  end
280
330
 
@@ -0,0 +1,81 @@
1
+ # Pagination is used to navigate paginateable collections
2
+ module LHS::Pagination
3
+ class Base
4
+
5
+ DEFAULT_LIMIT = 100
6
+
7
+ delegate :_record, to: :data
8
+ attr_accessor :data
9
+
10
+ def initialize(data)
11
+ self.data = data
12
+ end
13
+
14
+ # as standard in Rails' ActiveRecord count is not summing up, but using the number provided from data source
15
+ def count
16
+ total
17
+ end
18
+
19
+ def total
20
+ data._raw[_record.total_key.to_sym]
21
+ end
22
+
23
+ def limit
24
+ data._raw[_record.limit_key.to_sym] || DEFAULT_LIMIT
25
+ end
26
+
27
+ def offset
28
+ data._raw[_record.pagination_key.to_sym].presence || 0
29
+ end
30
+ alias current_page offset
31
+ alias start offset
32
+
33
+ def pages_left
34
+ total_pages - current_page
35
+ end
36
+
37
+ def next_offset(_step = 1)
38
+ raise 'to be implemented in subclass'
39
+ end
40
+
41
+ def current_page
42
+ raise 'to be implemented in subclass'
43
+ end
44
+
45
+ def first_page
46
+ 1
47
+ end
48
+
49
+ def last_page
50
+ total_pages
51
+ end
52
+
53
+ def next?
54
+ data._raw[:next].present?
55
+ end
56
+
57
+ def previous?
58
+ data._raw[:previous].present?
59
+ end
60
+
61
+ def prev_page
62
+ current_page - 1
63
+ end
64
+
65
+ def next_page
66
+ current_page + 1
67
+ end
68
+
69
+ def limit_value
70
+ limit
71
+ end
72
+
73
+ def total_pages
74
+ (total.to_f / limit).ceil
75
+ end
76
+
77
+ def self.page_to_offset(page, _limit)
78
+ page.to_i
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,14 @@
1
+ class LHS::Pagination::Offset < LHS::Pagination::Base
2
+
3
+ def current_page
4
+ (offset + limit) / limit
5
+ end
6
+
7
+ def next_offset(step = 1)
8
+ offset + limit * step
9
+ end
10
+
11
+ def self.page_to_offset(page, limit = DEFAULT_LIMIT)
12
+ (page.to_i - 1) * limit.to_i
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ class LHS::Pagination::Page < LHS::Pagination::Base
2
+
3
+ def current_page
4
+ offset
5
+ end
6
+
7
+ def next_offset(step = 1)
8
+ current_page + step
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ class LHS::Pagination::Start < LHS::Pagination::Base
2
+
3
+ def current_page
4
+ (offset + limit - 1) / limit
5
+ end
6
+
7
+ def next_offset(step = 1)
8
+ offset + limit * step
9
+ end
10
+
11
+ def self.page_to_offset(page, limit = DEFAULT_LIMIT)
12
+ (page.to_i - 1) * limit.to_i + 1
13
+ end
14
+ end
data/lib/lhs/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module LHS
2
- VERSION = "7.4.1"
2
+ VERSION = "8.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(type: :phonebook, size: 10)
34
+ results = Search.all(params: { 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
@@ -8,7 +8,7 @@ describe LHS::Record do
8
8
  LHS::Data.new(data_hash, nil, Record)
9
9
  end
10
10
 
11
- let(:pagination) { LHS::OffsetPagination.new(data) }
11
+ let(:pagination) { LHS::Pagination::Offset.new(data) }
12
12
 
13
13
  before(:each) do
14
14
  class Record < LHS::Record
@@ -0,0 +1,119 @@
1
+ require 'rails_helper'
2
+
3
+ describe LHS::Record do
4
+ context 'includes all' do
5
+ before(:each) do
6
+ class Customer < LHS::Record
7
+ endpoint 'http://datastore/customers/:id'
8
+ end
9
+ end
10
+
11
+ let(:amount_of_contracts) { 33 }
12
+ let(:amount_of_products) { 22 }
13
+
14
+ let!(:customer_request) do
15
+ stub_request(:get, 'http://datastore/customers/1')
16
+ .to_return(
17
+ body: {
18
+ contracts: { href: 'http://datastore/customers/1/contracts' }
19
+ }.to_json
20
+ )
21
+ end
22
+
23
+ let!(:contracts_request) do
24
+ stub_request(:get, "http://datastore/customers/1/contracts?limit=100")
25
+ .to_return(
26
+ body: {
27
+ items: 10.times.map do
28
+ {
29
+ products: { href: 'http://datastore/products' }
30
+ }
31
+ end,
32
+ limit: 10,
33
+ offset: 0,
34
+ total: amount_of_contracts
35
+ }.to_json
36
+ )
37
+ end
38
+
39
+ def additional_contracts_request(offset, amount)
40
+ stub_request(:get, "http://datastore/customers/1/contracts?limit=10&offset=#{offset}")
41
+ .to_return(
42
+ body: {
43
+ items: amount.times.map do
44
+ {
45
+ products: { href: 'http://datastore/products' }
46
+ }
47
+ end,
48
+ limit: 10,
49
+ offset: offset,
50
+ total: amount_of_contracts
51
+ }.to_json
52
+ )
53
+ end
54
+
55
+ let!(:contracts_request_page_2) do
56
+ additional_contracts_request(10, 10)
57
+ end
58
+
59
+ let!(:contracts_request_page_3) do
60
+ additional_contracts_request(20, 10)
61
+ end
62
+
63
+ let!(:contracts_request_page_4) do
64
+ additional_contracts_request(30, 3)
65
+ end
66
+
67
+ let!(:products_request) do
68
+ stub_request(:get, "http://datastore/products?limit=100")
69
+ .to_return(
70
+ body: {
71
+ items: 10.times.map do
72
+ { name: 'LBC' }
73
+ end,
74
+ limit: 10,
75
+ offset: 0,
76
+ total: amount_of_products
77
+ }.to_json
78
+ )
79
+ end
80
+
81
+ def additional_products_request(offset, amount)
82
+ stub_request(:get, "http://datastore/products?limit=10&offset=#{offset}")
83
+ .to_return(
84
+ body: {
85
+ items: amount.times.map do
86
+ { name: 'LBC' }
87
+ end,
88
+ limit: 10,
89
+ offset: offset,
90
+ total: amount_of_products
91
+ }.to_json
92
+ )
93
+ end
94
+
95
+ let!(:products_request_page_2) do
96
+ additional_products_request(10, 10)
97
+ end
98
+
99
+ let!(:products_request_page_3) do
100
+ additional_products_request(20, 2)
101
+ end
102
+
103
+ it 'includes all linked business objects no matter pagination' do
104
+ customer = Customer
105
+ .includes_all(contracts: :products)
106
+ .find(1)
107
+ expect(customer.contracts.length).to eq amount_of_contracts
108
+ expect(customer.contracts.first.products.length).to eq amount_of_products
109
+ expect(customer_request).to have_been_requested.at_least_once
110
+ expect(contracts_request).to have_been_requested.at_least_once
111
+ expect(contracts_request_page_2).to have_been_requested.at_least_once
112
+ expect(contracts_request_page_3).to have_been_requested.at_least_once
113
+ expect(contracts_request_page_4).to have_been_requested.at_least_once
114
+ expect(products_request).to have_been_requested.at_least_once
115
+ expect(products_request_page_2).to have_been_requested.at_least_once
116
+ expect(products_request_page_3).to have_been_requested.at_least_once
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,44 @@
1
+ require 'rails_helper'
2
+
3
+ describe LHS::Record do
4
+ context 'includes warning' do
5
+ before(:each) do
6
+ class Customer < LHS::Record
7
+ endpoint 'http://datastore/customers/:id'
8
+ end
9
+ end
10
+
11
+ let!(:customer_request) do
12
+ stub_request(:get, 'http://datastore/customers/1')
13
+ .to_return(
14
+ body: {
15
+ contracts: { href: 'http://datastore/customers/1/contracts' }
16
+ }.to_json
17
+ )
18
+ end
19
+
20
+ let!(:contracts_request) do
21
+ stub_request(:get, "http://datastore/customers/1/contracts")
22
+ .to_return(
23
+ body: {
24
+ items: 10.times.map do
25
+ {
26
+ products: { href: 'http://datastore/products' }
27
+ }
28
+ end,
29
+ limit: 10,
30
+ offset: 0,
31
+ total: 33
32
+ }.to_json
33
+ )
34
+ end
35
+
36
+ it 'warns if linked data was simply included but is paginated' do
37
+ expect(lambda {
38
+ Customer.includes(:contracts).find(1)
39
+ }).to output(
40
+ "[WARNING] You included `http://datastore/customers/1/contracts`, but this endpoint is paginated. You might want to use `includes_all` instead of `includes` (https://github.com/local-ch/lhs#includes_all-for-paginated-endpoints).\n"
41
+ ).to_stderr
42
+ end
43
+ end
44
+ end
@@ -16,7 +16,12 @@ describe LHS::Record do
16
16
  stub_request(:get, "#{datastore}/feedbacks?limit=100")
17
17
  .to_return(
18
18
  status: 200,
19
- body: { items: [], total: 300, offset: 0 }.to_json
19
+ body: { items: [], total: 200, offset: 0 }.to_json
20
+ )
21
+ stub_request(:get, "#{datastore}/feedbacks?limit=100&offset=100")
22
+ .to_return(
23
+ status: 200,
24
+ body: { items: [], total: 200, offset: 0 }.to_json
20
25
  )
21
26
  all = Record.all
22
27
  expect(all).to be_kind_of Record
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lhs
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.4.1
4
+ version: 8.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - https://github.com/local-ch/lhs/graphs/contributors
@@ -225,7 +225,10 @@ files:
225
225
  - lib/lhs/endpoint.rb
226
226
  - lib/lhs/errors.rb
227
227
  - lib/lhs/item.rb
228
- - lib/lhs/pagination.rb
228
+ - lib/lhs/pagination/base.rb
229
+ - lib/lhs/pagination/offset.rb
230
+ - lib/lhs/pagination/page.rb
231
+ - lib/lhs/pagination/start.rb
229
232
  - lib/lhs/proxy.rb
230
233
  - lib/lhs/record.rb
231
234
  - lib/lhs/version.rb
@@ -329,7 +332,9 @@ files:
329
332
  - spec/record/find_spec.rb
330
333
  - spec/record/first_spec.rb
331
334
  - spec/record/immutable_chains_spec.rb
335
+ - spec/record/includes_all_spec.rb
332
336
  - spec/record/includes_spec.rb
337
+ - spec/record/includes_warning_spec.rb
333
338
  - spec/record/loading_twice_spec.rb
334
339
  - spec/record/mapping_spec.rb
335
340
  - spec/record/model_name_spec.rb
@@ -483,7 +488,9 @@ test_files:
483
488
  - spec/record/find_spec.rb
484
489
  - spec/record/first_spec.rb
485
490
  - spec/record/immutable_chains_spec.rb
491
+ - spec/record/includes_all_spec.rb
486
492
  - spec/record/includes_spec.rb
493
+ - spec/record/includes_warning_spec.rb
487
494
  - spec/record/loading_twice_spec.rb
488
495
  - spec/record/mapping_spec.rb
489
496
  - spec/record/model_name_spec.rb
@@ -1,120 +0,0 @@
1
- # Pagination is used to navigate paginateable collections
2
- class LHS::Pagination
3
-
4
- DEFAULT_LIMIT = 100
5
-
6
- delegate :_record, to: :data
7
- attr_accessor :data
8
-
9
- def initialize(data)
10
- self.data = data
11
- end
12
-
13
- # as standard in Rails' ActiveRecord count is not summing up, but using the number provided from data source
14
- def count
15
- total
16
- end
17
-
18
- def total
19
- data._raw[_record.total_key.to_sym]
20
- end
21
-
22
- def limit
23
- data._raw[_record.limit_key.to_sym] || LHS::Pagination::DEFAULT_LIMIT
24
- end
25
-
26
- def offset
27
- data._raw[_record.pagination_key.to_sym].presence || 0
28
- end
29
- alias current_page offset
30
- alias start offset
31
-
32
- def pages_left
33
- total_pages - current_page
34
- end
35
-
36
- def next_offset
37
- raise 'to be implemented in subclass'
38
- end
39
-
40
- def current_page
41
- raise 'to be implemented in subclass'
42
- end
43
-
44
- def first_page
45
- 1
46
- end
47
-
48
- def last_page
49
- total_pages
50
- end
51
-
52
- def next?
53
- data._raw[:next].present?
54
- end
55
-
56
- def previous?
57
- data._raw[:previous].present?
58
- end
59
-
60
- def prev_page
61
- current_page - 1
62
- end
63
-
64
- def next_page
65
- current_page + 1
66
- end
67
-
68
- def limit_value
69
- limit
70
- end
71
-
72
- def total_pages
73
- (total.to_f / limit).ceil
74
- end
75
-
76
- def self.page_to_offset(page, _limit)
77
- page.to_i
78
- end
79
- end
80
-
81
- class LHS::PagePagination < LHS::Pagination
82
-
83
- def current_page
84
- offset
85
- end
86
-
87
- def next_offset
88
- current_page + 1
89
- end
90
- end
91
-
92
- class LHS::StartPagination < LHS::Pagination
93
-
94
- def current_page
95
- (offset + limit - 1) / limit
96
- end
97
-
98
- def next_offset
99
- offset + limit
100
- end
101
-
102
- def self.page_to_offset(page, limit = LHS::Pagination::DEFAULT_LIMIT)
103
- (page.to_i - 1) * limit.to_i + 1
104
- end
105
- end
106
-
107
- class LHS::OffsetPagination < LHS::Pagination
108
-
109
- def current_page
110
- (offset + limit) / limit
111
- end
112
-
113
- def next_offset
114
- offset + limit
115
- end
116
-
117
- def self.page_to_offset(page, limit = LHS::Pagination::DEFAULT_LIMIT)
118
- (page.to_i - 1) * limit.to_i
119
- end
120
- end