lhs 7.4.1 → 8.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 +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
|