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 +4 -4
- data/README.md +21 -1
- data/lib/lhs/concerns/record/all.rb +62 -22
- data/lib/lhs/concerns/record/batch.rb +1 -1
- data/lib/lhs/concerns/record/chainable.rb +65 -9
- data/lib/lhs/concerns/record/pagination.rb +3 -3
- data/lib/lhs/concerns/record/request.rb +57 -7
- data/lib/lhs/pagination/base.rb +81 -0
- data/lib/lhs/pagination/offset.rb +14 -0
- data/lib/lhs/pagination/page.rb +10 -0
- data/lib/lhs/pagination/start.rb +14 -0
- data/lib/lhs/version.rb +1 -1
- data/spec/collection/configurable_spec.rb +1 -1
- data/spec/pagination/pages_left_spec.rb +1 -1
- data/spec/record/includes_all_spec.rb +119 -0
- data/spec/record/includes_warning_spec.rb +44 -0
- data/spec/record/paginatable_collection_spec.rb +6 -1
- metadata +9 -2
- data/lib/lhs/pagination.rb +0 -120
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e172deae82bb63cb78f7a2d96a261620ecf09af
|
4
|
+
data.tar.gz: b1caad27952e66468718b4f070b125c565fb39cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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(
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
171
|
+
push(Parameter.new(hash))
|
166
172
|
end
|
167
173
|
|
168
174
|
def options(hash = nil)
|
169
|
-
push
|
175
|
+
push(Option.new(hash))
|
170
176
|
end
|
171
177
|
|
172
178
|
def page(page)
|
173
|
-
push
|
179
|
+
push(Pagination.new(page: page))
|
174
180
|
end
|
175
181
|
|
176
182
|
def per(per)
|
177
|
-
push
|
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
|
188
|
+
push(Pagination.new(per: argument))
|
183
189
|
end
|
184
190
|
|
185
191
|
def handle(error_class, handler)
|
186
|
-
push
|
192
|
+
push(ErrorHandling.new(error_class => handler))
|
187
193
|
end
|
188
194
|
|
189
195
|
def includes(*args)
|
190
|
-
push
|
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
|
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::
|
18
|
+
LHS::Pagination::Page
|
19
19
|
when :start
|
20
|
-
LHS::
|
20
|
+
LHS::Pagination::Start
|
21
21
|
else
|
22
|
-
LHS::
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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? &&
|
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,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
@@ -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
|
@@ -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:
|
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:
|
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
|
data/lib/lhs/pagination.rb
DELETED
@@ -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
|