lhs 21.3.0 → 23.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.
@@ -3,725 +3,691 @@
3
3
  require 'rails_helper'
4
4
 
5
5
  describe LHS::Record do
6
- let(:datastore) { 'http://local.ch/v2' }
7
- before { LHC.config.placeholder('datastore', datastore) }
8
-
9
- let(:stub_campaign_request) do
10
- stub_request(:get, "#{datastore}/content-ads/51dfc5690cf271c375c5a12d")
11
- .to_return(body: {
12
- 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d",
13
- 'entry' => { 'href' => "#{datastore}/local-entries/lakj35asdflkj1203va" },
14
- 'user' => { 'href' => "#{datastore}/users/lakj35asdflkj1203va" }
15
- }.to_json)
16
- end
17
-
18
- let(:stub_entry_request) do
19
- stub_request(:get, "#{datastore}/local-entries/lakj35asdflkj1203va")
20
- .to_return(body: { 'name' => 'Casa Ferlin' }.to_json)
21
- end
22
-
23
- let(:stub_user_request) do
24
- stub_request(:get, "#{datastore}/users/lakj35asdflkj1203va")
25
- .to_return(body: { 'name' => 'Mario' }.to_json)
26
- end
27
-
28
- context 'singlelevel includes' do
6
+ context 'includes all' do
29
7
  before do
30
- class LocalEntry < LHS::Record
31
- endpoint '{+datastore}/local-entries'
32
- endpoint '{+datastore}/local-entries/{id}'
8
+ class Customer < LHS::Record
9
+ endpoint 'http://datastore/customers/{id}'
33
10
  end
11
+
34
12
  class User < LHS::Record
35
- endpoint '{+datastore}/users'
36
- endpoint '{+datastore}/users/{id}'
37
- end
38
- class Favorite < LHS::Record
39
- endpoint '{+datastore}/favorites'
40
- endpoint '{+datastore}/favorites/{id}'
41
- end
42
- stub_request(:get, "#{datastore}/local-entries/1")
43
- .to_return(body: { company_name: 'local.ch' }.to_json)
44
- stub_request(:get, "#{datastore}/users/1")
45
- .to_return(body: { name: 'Mario' }.to_json)
46
- stub_request(:get, "#{datastore}/favorites/1")
47
- .to_return(body: {
48
- local_entry: { href: "#{datastore}/local-entries/1" },
49
- user: { href: "#{datastore}/users/1" }
50
- }.to_json)
13
+ configuration pagination_strategy: 'link'
14
+ endpoint 'http://datastore/users'
15
+ end
51
16
  end
52
17
 
53
- it 'includes a resource' do
54
- favorite = Favorite.includes(:local_entry).find(1)
55
- expect(favorite.local_entry.company_name).to eq 'local.ch'
18
+ let(:amount_of_contracts) { 33 }
19
+ let(:amount_of_products) { 22 }
20
+ let(:amount_of_users_1st_page) { 10 }
21
+ let(:amount_of_users_2nd_page) { 3 }
22
+ let(:amount_of_users) { amount_of_users_1st_page + amount_of_users_2nd_page }
23
+
24
+ let!(:customer_request) do
25
+ stub_request(:get, 'http://datastore/customers/1')
26
+ .to_return(
27
+ body: {
28
+ contracts: { href: 'http://datastore/customers/1/contracts' },
29
+ users: { href: 'http://datastore/users?limit=10' }
30
+ }.to_json
31
+ )
56
32
  end
57
33
 
58
- it 'duplicates a class' do
59
- expect(Favorite.object_id).not_to eq(Favorite.includes(:local_entry).object_id)
34
+ #
35
+ # Contracts
36
+ #
37
+
38
+ let!(:contracts_request) do
39
+ stub_request(:get, "http://datastore/customers/1/contracts?limit=100")
40
+ .to_return(
41
+ body: {
42
+ items: 10.times.map do
43
+ {
44
+ products: { href: 'http://datastore/products' }
45
+ }
46
+ end,
47
+ limit: 10,
48
+ offset: 0,
49
+ total: amount_of_contracts
50
+ }.to_json
51
+ )
60
52
  end
61
53
 
62
- it 'includes a list of resources' do
63
- favorite = Favorite.includes(:local_entry, :user).find(1)
64
- expect(favorite.local_entry).to be_kind_of LocalEntry
65
- expect(favorite.local_entry.company_name).to eq 'local.ch'
66
- expect(favorite.user.name).to eq 'Mario'
54
+ def additional_contracts_request(offset, amount)
55
+ stub_request(:get, "http://datastore/customers/1/contracts?limit=10&offset=#{offset}")
56
+ .to_return(
57
+ body: {
58
+ items: amount.times.map do
59
+ {
60
+ products: { href: 'http://datastore/products' }
61
+ }
62
+ end,
63
+ limit: 10,
64
+ offset: offset,
65
+ total: amount_of_contracts
66
+ }.to_json
67
+ )
67
68
  end
68
69
 
69
- it 'includes an array of resources' do
70
- favorite = Favorite.includes([:local_entry, :user]).find(1)
71
- expect(favorite.local_entry.company_name).to eq 'local.ch'
72
- expect(favorite.user.name).to eq 'Mario'
70
+ let!(:contracts_request_page_2) do
71
+ additional_contracts_request(10, 10)
73
72
  end
74
- end
75
73
 
76
- context 'multilevel includes' do
77
- before do
78
- class Feedback < LHS::Record
79
- endpoint '{+datastore}/feedbacks'
80
- endpoint '{+datastore}/feedbacks/{id}'
81
- end
82
- stub_campaign_request
83
- stub_entry_request
84
- stub_user_request
74
+ let!(:contracts_request_page_3) do
75
+ additional_contracts_request(20, 10)
85
76
  end
86
77
 
87
- it 'includes linked resources while fetching multiple resources from one service' do
88
- stub_request(:get, "#{datastore}/feedbacks?has_reviews=true")
89
- .to_return(status: 200, body: {
90
- items: [
91
- {
92
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
93
- 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" }
94
- }
95
- ]
96
- }.to_json)
97
-
98
- feedbacks = Feedback.includes(campaign: :entry).where(has_reviews: true)
99
- expect(feedbacks.first.campaign.entry.name).to eq 'Casa Ferlin'
78
+ let!(:contracts_request_page_4) do
79
+ additional_contracts_request(30, 3)
100
80
  end
101
81
 
102
- it 'includes linked resources while fetching a single resource from one service' do
103
- stub_request(:get, "#{datastore}/feedbacks/123")
104
- .to_return(status: 200, body: {
105
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
106
- 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" }
107
- }.to_json)
82
+ #
83
+ # Products
84
+ #
108
85
 
109
- feedbacks = Feedback.includes(campaign: :entry).find(123)
110
- expect(feedbacks.campaign.entry.name).to eq 'Casa Ferlin'
86
+ let!(:products_request) do
87
+ stub_request(:get, "http://datastore/products?limit=100")
88
+ .to_return(
89
+ body: {
90
+ items: 10.times.map do
91
+ { name: 'LBC' }
92
+ end,
93
+ limit: 10,
94
+ offset: 0,
95
+ total: amount_of_products
96
+ }.to_json
97
+ )
111
98
  end
112
99
 
113
- it 'includes linked resources with array while fetching a single resource from one service' do
114
- stub_request(:get, "#{datastore}/feedbacks/123")
115
- .to_return(status: 200, body: {
116
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
117
- 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" }
118
- }.to_json)
100
+ def additional_products_request(offset, amount)
101
+ stub_request(:get, "http://datastore/products?limit=10&offset=#{offset}")
102
+ .to_return(
103
+ body: {
104
+ items: amount.times.map do
105
+ { name: 'LBC' }
106
+ end,
107
+ limit: 10,
108
+ offset: offset,
109
+ total: amount_of_products
110
+ }.to_json
111
+ )
112
+ end
119
113
 
120
- feedbacks = Feedback.includes(campaign: [:entry, :user]).find(123)
121
- expect(feedbacks.campaign.entry.name).to eq 'Casa Ferlin'
122
- expect(feedbacks.campaign.user.name).to eq 'Mario'
114
+ let!(:products_request_page_2) do
115
+ additional_products_request(10, 10)
123
116
  end
124
117
 
125
- it 'includes list of linked resources while fetching a single resource from one service' do
126
- stub_request(:get, "#{datastore}/feedbacks/123")
127
- .to_return(status: 200, body: {
128
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
129
- 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" },
130
- 'user' => { 'href' => "#{datastore}/users/lakj35asdflkj1203va" }
131
- }.to_json)
132
-
133
- feedbacks = Feedback.includes(:user, campaign: [:entry, :user]).find(123)
134
- expect(feedbacks.campaign.entry.name).to eq 'Casa Ferlin'
135
- expect(feedbacks.campaign.user.name).to eq 'Mario'
136
- expect(feedbacks.user.name).to eq 'Mario'
118
+ let!(:products_request_page_3) do
119
+ additional_products_request(20, 2)
137
120
  end
138
121
 
139
- context 'include objects from known services' do
140
- let(:stub_feedback_request) do
141
- stub_request(:get, "#{datastore}/feedbacks")
142
- .to_return(status: 200, body: {
143
- items: [
144
- {
145
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
146
- 'entry' => {
147
- 'href' => "#{datastore}/local-entries/lakj35asdflkj1203va"
148
- }
149
- }
150
- ]
151
- }.to_json)
152
- end
122
+ #
123
+ # Users
124
+ #
153
125
 
154
- let(:interceptor) { spy('interceptor') }
126
+ let!(:users_request) do
127
+ stub_request(:get, 'http://datastore/users?limit=10')
128
+ .to_return(
129
+ body: {
130
+ items: amount_of_users_1st_page.times.map do
131
+ { name: 'Hans Muster' }
132
+ end,
133
+ limit: 10,
134
+ next: { href: 'http://datastore/users?for_user_id=10&limit=10' }
135
+ }.to_json
136
+ )
137
+ end
155
138
 
156
- before do
157
- class Entry < LHS::Record
158
- endpoint '{+datastore}/local-entries/{id}'
159
- end
160
- LHC.config.interceptors = [interceptor]
161
- end
139
+ let!(:users_request_page_2) do
140
+ stub_request(:get, 'http://datastore/users?for_user_id=10&limit=10')
141
+ .to_return(
142
+ body: {
143
+ items: amount_of_users_2nd_page.times.map do
144
+ { name: 'Lisa Müller' }
145
+ end,
146
+ limit: 10,
147
+ next: { href: 'http://datastore/users?for_user_id=13&limit=10' }
148
+ }.to_json
149
+ )
150
+ end
162
151
 
163
- it 'uses interceptors for included links from known services' do
164
- stub_feedback_request
165
- stub_entry_request
166
- expect(Feedback.includes(:entry).where.first.entry.name).to eq 'Casa Ferlin'
167
- expect(interceptor).to have_received(:before_request).twice
168
- end
152
+ let!(:users_request_page_3) do
153
+ stub_request(:get, 'http://datastore/users?for_user_id=13&limit=10')
154
+ .to_return(
155
+ body: {
156
+ items: [],
157
+ limit: 10
158
+ }.to_json
159
+ )
169
160
  end
170
161
 
171
- context 'includes not present in response' do
172
- before do
173
- class Parent < LHS::Record
174
- endpoint '{+datastore}/local-parents'
175
- endpoint '{+datastore}/local-parents/{id}'
176
- end
162
+ it 'includes all linked business objects no matter pagination' do
163
+ customer = nil
177
164
 
178
- class OptionalChild < LHS::Record
179
- endpoint '{+datastore}/local-children/{id}'
180
- end
165
+ expect(lambda do
166
+ customer = Customer
167
+ .includes(:users, contracts: :products)
168
+ .find(1)
169
+ end).to output(
170
+ %r{\[WARNING\] You are loading all pages from a resource paginated with links only. As this is performed sequentially, it can result in very poor performance! \(https://github.com/local-ch/lhs#pagination-strategy-link\).}
171
+ ).to_stderr
172
+
173
+ expect(customer.users.length).to eq amount_of_users
174
+ expect(customer.contracts.length).to eq amount_of_contracts
175
+ expect(customer.contracts.first.products.length).to eq amount_of_products
176
+ expect(customer_request).to have_been_requested.at_least_once
177
+ expect(contracts_request).to have_been_requested.at_least_once
178
+ expect(contracts_request_page_2).to have_been_requested.at_least_once
179
+ expect(contracts_request_page_3).to have_been_requested.at_least_once
180
+ expect(contracts_request_page_4).to have_been_requested.at_least_once
181
+ expect(products_request).to have_been_requested.at_least_once
182
+ expect(products_request_page_2).to have_been_requested.at_least_once
183
+ expect(products_request_page_3).to have_been_requested.at_least_once
184
+ expect(users_request).to have_been_requested.at_least_once
185
+ expect(users_request_page_2).to have_been_requested.at_least_once
186
+ expect(users_request_page_3).to have_been_requested.at_least_once
187
+ end
188
+
189
+ context 'links already contain pagination parameters' do
190
+ let!(:customer_request) do
191
+ stub_request(:get, 'http://datastore/customers/1')
192
+ .to_return(
193
+ body: {
194
+ contracts: { href: 'http://datastore/customers/1/contracts?limit=5&offset=0' }
195
+ }.to_json
196
+ )
181
197
  end
182
198
 
183
- it 'handles missing but included fields in single object response' do
184
- stub_request(:get, "#{datastore}/local-parents/1")
185
- .to_return(status: 200, body: {
186
- 'href' => "#{datastore}/local-parents/1",
187
- 'name' => 'RspecName'
188
- }.to_json)
189
-
190
- parent = Parent.includes(:optional_children).find(1)
191
- expect(parent).not_to be nil
192
- expect(parent.name).to eq 'RspecName'
193
- expect(parent.optional_children).to be nil
199
+ let!(:contracts_request) do
200
+ stub_request(:get, "http://datastore/customers/1/contracts?limit=100")
201
+ .to_return(
202
+ body: {
203
+ items: 10.times.map do
204
+ {
205
+ products: { href: 'http://datastore/products' }
206
+ }
207
+ end,
208
+ limit: 10,
209
+ offset: 0,
210
+ total: amount_of_contracts
211
+ }.to_json
212
+ )
194
213
  end
195
214
 
196
- it 'handles missing but included fields in collection response' do
197
- stub_request(:get, "#{datastore}/local-parents")
198
- .to_return(status: 200, body: {
199
- items: [
200
- {
201
- 'href' => "#{datastore}/local-parents/1",
202
- 'name' => 'RspecParent'
203
- }, {
204
- 'href' => "#{datastore}/local-parents/2",
205
- 'name' => 'RspecParent2',
206
- 'optional_child' => {
207
- 'href' => "#{datastore}/local-children/1"
208
- }
209
- }
210
- ]
211
- }.to_json)
215
+ it 'overwrites existing pagination paramters if they are already contained in a string' do
216
+ expect(LHC).to receive(:request)
217
+ .with(url: "http://datastore/customers/1").and_call_original
218
+
219
+ expect(LHC).to receive(:request)
220
+ .with(url: "http://datastore/customers/1/contracts",
221
+ all: true,
222
+ params: { limit: 100 }).and_call_original
223
+
224
+ expect(LHC).to receive(:request)
225
+ .with([{ url: "http://datastore/customers/1/contracts",
226
+ all: true,
227
+ params: { limit: 10, offset: 10 } },
228
+ { url: "http://datastore/customers/1/contracts",
229
+ all: true,
230
+ params: { limit: 10, offset: 20 } },
231
+ { url: "http://datastore/customers/1/contracts",
232
+ all: true,
233
+ params: { limit: 10, offset: 30 } }]).and_call_original
234
+
235
+ customer = Customer
236
+ .includes(:contracts)
237
+ .find(1)
238
+ expect(customer.contracts.length).to eq amount_of_contracts
239
+ end
240
+ end
212
241
 
213
- stub_request(:get, "#{datastore}/local-children/1")
214
- .to_return(status: 200, body: {
215
- href: "#{datastore}/local_children/1",
216
- name: 'RspecOptionalChild1'
242
+ context 'includes for an empty array' do
243
+ before do
244
+ class Contract < LHS::Record
245
+ endpoint 'http://datastore/contracts/{id}'
246
+ end
247
+ stub_request(:get, %r{http://datastore/contracts/\d})
248
+ .to_return(body: {
249
+ options: nested_resources
217
250
  }.to_json)
218
-
219
- child = Parent.includes(:optional_child).where[1].optional_child
220
- expect(child).not_to be nil
221
- expect(child.name).to eq 'RspecOptionalChild1'
222
251
  end
223
- end
224
- end
225
252
 
226
- context 'links pointing to nowhere' do
227
- it 'sets nil for links that cannot be included' do
228
- class Feedback < LHS::Record
229
- endpoint '{+datastore}/feedbacks'
230
- endpoint '{+datastore}/feedbacks/{id}'
253
+ context 'empty array' do
254
+ let(:nested_resources) { [] }
255
+
256
+ it 'includes all in case of an empty array' do
257
+ expect(
258
+ -> { Contract.includes(:product).includes(:options).find(1) }
259
+ ).not_to raise_error
260
+ expect(
261
+ -> { Contract.includes(:product).includes(:options).find(1, 2) }
262
+ ).not_to raise_error
263
+ end
231
264
  end
232
265
 
233
- stub_request(:get, "#{datastore}/feedbacks/123")
234
- .to_return(status: 200, body: {
235
- 'href' => "#{datastore}/feedbacks/-Sc4_pYNpqfsudzhtivfkA",
236
- 'campaign' => { 'href' => "#{datastore}/content-ads/51dfc5690cf271c375c5a12d" }
237
- }.to_json)
266
+ context 'weird array without hrefs' do
267
+ before do
268
+ stub_request(:get, "http://datastore/options/1?limit=100")
269
+ .to_return(body: { type: 'REACH_EXT' }.to_json)
270
+ end
238
271
 
239
- stub_request(:get, "#{datastore}/content-ads/51dfc5690cf271c375c5a12d")
240
- .to_return(status: 404)
272
+ let(:nested_resources) { [{ href: 'http://datastore/options/1' }, { type: 'E_COMMERCE' }] }
241
273
 
242
- feedback = Feedback.includes(campaign: :entry).find(123)
243
- expect(feedback.campaign._raw.keys.count).to eq 1
244
- expect(feedback.campaign.href).to be_present
274
+ it 'includes in case of an unexpect objects within array' do
275
+ expect(
276
+ -> { Contract.includes(:product).includes(:options).find(1) }
277
+ ).not_to raise_error
278
+ expect(
279
+ -> { Contract.includes(:product).includes(:options).find(1, 2) }
280
+ ).not_to raise_error
281
+ end
282
+ end
245
283
  end
246
- end
247
284
 
248
- context 'modules' do
249
- before do
250
- module Services
251
- class LocalEntry < LHS::Record
252
- endpoint '{+datastore}/local-entries'
285
+ context 'include a known/identifiable record' do
286
+ before do
287
+ class Contract < LHS::Record
288
+ endpoint 'http://datastore/contracts/{id}'
253
289
  end
254
290
 
255
- class Feedback < LHS::Record
256
- endpoint '{+datastore}/feedbacks'
291
+ class Entry < LHS::Record
292
+ endpoint '{+datastore}/entry/v1/{id}.json'
257
293
  end
294
+
295
+ LHC.config.placeholder(:datastore, 'http://datastore')
258
296
  end
259
- stub_request(:get, "http://local.ch/v2/feedbacks?id=123")
260
- .to_return(body: [].to_json)
261
- end
262
297
 
263
- it 'works with modules' do
264
- Services::Feedback.includes(campaign: :entry).find(123)
265
- end
266
- end
298
+ let!(:customer_request) do
299
+ stub_request(:get, %r{http://datastore/customers/\d+})
300
+ .to_return(
301
+ body: {
302
+ contracts: [{ href: 'http://datastore/contracts/1' }, { href: 'http://datastore/contracts/2' }]
303
+ }.to_json
304
+ )
305
+ end
267
306
 
268
- context 'arrays' do
269
- before do
270
- class Place < LHS::Record
271
- endpoint '{+datastore}/place'
272
- endpoint '{+datastore}/place/{id}'
307
+ let!(:contracts_request) do
308
+ stub_request(:get, %r{http://datastore/contracts/\d+})
309
+ .to_return(
310
+ body: {
311
+ type: 'contract',
312
+ entry: { href: 'http://datastore/entry/v1/1.json' }
313
+ }.to_json
314
+ )
273
315
  end
274
- end
275
316
 
276
- let!(:place_request) do
277
- stub_request(:get, "#{datastore}/place/1")
278
- .to_return(body: {
279
- 'relations' => [
280
- { 'href' => "#{datastore}/place/relations/2" },
281
- { 'href' => "#{datastore}/place/relations/3" }
282
- ]
283
- }.to_json)
284
- end
317
+ let!(:entry_request) do
318
+ stub_request(:get, %r{http://datastore/entry/v1/\d+.json})
319
+ .to_return(
320
+ body: {
321
+ name: 'Casa Ferlin'
322
+ }.to_json
323
+ )
324
+ end
285
325
 
286
- let!(:relation_request_1) do
287
- stub_request(:get, "#{datastore}/place/relations/2")
288
- .to_return(body: { name: 'Category' }.to_json)
326
+ it 'loads included identifiable records without raising exceptions' do
327
+ customer = Customer.includes(contracts: :entry).find(1, 2).first
328
+ expect(customer.contracts.first.href).to eq 'http://datastore/contracts/1'
329
+ expect(customer.contracts.first.type).to eq 'contract'
330
+ expect(customer.contracts.first.entry.name).to eq 'Casa Ferlin'
331
+ end
289
332
  end
290
333
 
291
- let!(:relation_request_2) do
292
- stub_request(:get, "#{datastore}/place/relations/3")
293
- .to_return(body: { name: 'ZeFrank' }.to_json)
294
- end
334
+ context 'includes all for parallel loaded ids' do
335
+ before do
336
+ class Place < LHS::Record
337
+ endpoint 'http://datastore/places/{id}'
338
+ end
339
+ end
295
340
 
296
- it 'includes items of arrays' do
297
- place = Place.includes(:relations).find(1)
298
- expect(place.relations.first.name).to eq 'Category'
299
- expect(place.relations[1].name).to eq 'ZeFrank'
300
- end
341
+ let!(:place_request_1) do
342
+ stub_request(:get, %r{http://datastore/places/1})
343
+ .to_return(
344
+ body: {
345
+ category_relations: [
346
+ { href: 'http://datastore/category_relations/1' },
347
+ { href: 'http://datastore/category_relations/2' }
348
+ ]
349
+ }.to_json
350
+ )
351
+ end
301
352
 
302
- context 'parallel with empty links' do
303
353
  let!(:place_request_2) do
304
- stub_request(:get, "#{datastore}/place/2")
305
- .to_return(body: {
306
- 'relations' => []
307
- }.to_json)
354
+ stub_request(:get, %r{http://datastore/places/2})
355
+ .to_return(
356
+ body: {
357
+ category_relations: []
358
+ }.to_json
359
+ )
308
360
  end
309
361
 
310
- it 'loads places in parallel and merges included data properly' do
311
- place = Place.includes(:relations).find(2, 1)
312
- expect(place[0].relations.empty?).to be true
313
- expect(place[1].relations[0].name).to eq 'Category'
314
- expect(place[1].relations[1].name).to eq 'ZeFrank'
362
+ let!(:place_request_3) do
363
+ stub_request(:get, %r{http://datastore/places/3})
364
+ .to_return(
365
+ body: {
366
+ category_relations: [
367
+ { href: 'http://datastore/category_relations/1' },
368
+ { href: 'http://datastore/category_relations/3' }
369
+ ]
370
+ }.to_json
371
+ )
315
372
  end
316
- end
317
- end
318
373
 
319
- context 'empty collections' do
320
- it 'skips including empty collections' do
321
- class Place < LHS::Record
322
- endpoint '{+datastore}/place'
323
- endpoint '{+datastore}/place/{id}'
374
+ let!(:category_relation_request_1) do
375
+ stub_request(:get, %r{http://datastore/category_relations/1})
376
+ .to_return(
377
+ body: {
378
+ name: "Category 1"
379
+ }.to_json
380
+ )
324
381
  end
325
382
 
326
- stub_request(:get, "#{datastore}/place/1")
327
- .to_return(body: {
328
- 'available_products' => {
329
- "url" => "#{datastore}/place/1/products",
330
- "items" => []
331
- }
332
- }.to_json)
333
-
334
- place = Place.includes(:available_products).find(1)
335
- expect(place.available_products.empty?).to eq true
336
- end
337
- end
338
-
339
- context 'extend items with arrays' do
340
- it 'extends base items with arrays' do
341
- class Place < LHS::Record
342
- endpoint '{+datastore}/place'
343
- endpoint '{+datastore}/place/{id}'
383
+ let!(:category_relation_request_2) do
384
+ stub_request(:get, %r{http://datastore/category_relations/2})
385
+ .to_return(
386
+ body: {
387
+ name: "Category 2"
388
+ }.to_json
389
+ )
344
390
  end
345
391
 
346
- stub_request(:get, "#{datastore}/place/1")
347
- .to_return(body: {
348
- 'contracts' => {
349
- 'items' => [{ 'href' => "#{datastore}/place/1/contacts/1" }]
350
- }
351
- }.to_json)
392
+ let!(:category_relation_request_3) do
393
+ stub_request(:get, %r{http://datastore/category_relations/3})
394
+ .to_return(
395
+ body: {
396
+ name: "Category 3"
397
+ }.to_json
398
+ )
399
+ end
352
400
 
353
- stub_request(:get, "#{datastore}/place/1/contacts/1")
354
- .to_return(body: {
355
- 'products' => { 'href' => "#{datastore}/place/1/contacts/1/products" }
356
- }.to_json)
401
+ let(:category_name) { 'Category Relation' }
357
402
 
358
- place = Place.includes(:contracts).find(1)
359
- expect(place.contracts.first.products.href).to eq "#{datastore}/place/1/contacts/1/products"
403
+ it 'requests places in parallel and includes category relation' do
404
+ places = Place.includes(:category_relations).find(1, 2, 3)
405
+ expect(places[0].category_relations[0].name).to eq 'Category 1'
406
+ expect(places[0].category_relations[1].name).to eq 'Category 2'
407
+ expect(places[2].category_relations[0].name).to eq 'Category 1'
408
+ expect(places[2].category_relations[1].name).to eq 'Category 3'
409
+ end
360
410
  end
361
411
  end
362
412
 
363
- context 'unexpanded response when requesting the included collection' do
413
+ context 'Linked resources' do
364
414
  before do
365
- class Customer < LHS::Record
366
- endpoint '{+datastore}/customer/{id}'
415
+ stub_request(:get, 'http://datastore/places/1/contracts?offset=0&limit=10')
416
+ .to_return(
417
+ body: {
418
+ href: "http://datastore/v2/places/1/contracts?offset=0&limit=10",
419
+ items: [{ href: "http://datastore/v2/contracts/1" }],
420
+ offset: 0,
421
+ limit: 10,
422
+ total: 10
423
+ }.to_json
424
+ )
425
+
426
+ stub_request(:get, "http://datastore/v2/contracts/1")
427
+ .to_return(
428
+ body: {
429
+ customer: { name: 'Swisscom Directories AG' }
430
+ }.to_json
431
+ )
432
+
433
+ stub_request(:get, 'http://datastore/places/1?limit=1')
434
+ .to_return(
435
+ body: { href: 'http://datastore/places/1', contracts: { href: 'http://datastore/places/1/contracts?offset=0&limit=10' } }.to_json
436
+ )
437
+
438
+ class Place < LHS::Record
439
+ endpoint 'http://datastore/places/{id}'
367
440
  end
368
- end
369
441
 
370
- let!(:customer_request) do
371
- stub_request(:get, "#{datastore}/customer/1")
372
- .to_return(body: {
373
- places: {
374
- href: "#{datastore}/places"
375
- }
376
- }.to_json)
442
+ class Contract < LHS::Record
443
+ endpoint 'http://datastore/places/{place_id}/contracts'
444
+ end
377
445
  end
378
446
 
379
- let!(:places_request) do
380
- stub_request(:get, "#{datastore}/places")
381
- .to_return(body: {
382
- items: [{ href: "#{datastore}/places/1" }]
383
- }.to_json)
447
+ it 'does not use the root record endpoints when including nested records' do
448
+ place = Place
449
+ .includes(:contracts)
450
+ .find_by(id: 1)
451
+ expect(place.contracts.first.customer.name).to eq 'Swisscom Directories AG'
384
452
  end
453
+ end
385
454
 
386
- let!(:place_request) do
387
- stub_request(:get, "#{datastore}/places/1")
388
- .to_return(body: {
389
- name: 'Casa Ferlin'
390
- }.to_json)
391
- end
455
+ context 'nested includes all' do
456
+ context 'with optional children' do
392
457
 
393
- it 'loads the collection and the single items, if not already expanded' do
394
- place = Customer.includes(:places).find(1).places.first
395
- assert_requested(place_request)
396
- expect(place.name).to eq 'Casa Ferlin'
397
- end
458
+ let(:favorites_request_stub) do
459
+ stub_request(:get, %r{http://datastore/favorites})
460
+ .to_return(
461
+ body: {
462
+ items: [{
463
+ href: "http://datastore/favorites/1",
464
+ place: {
465
+ href: "http://datastore/places/1"
466
+ }
467
+ }, {
468
+ href: "http://datastore/favorite/2",
469
+ place: {
470
+ href: "http://datastore/places/2"
471
+ }
472
+ }, {
473
+ href: "http://datastore/favorite/3",
474
+ place: {
475
+ href: "http://datastore/places/3"
476
+ }
477
+ }],
478
+ total: 3,
479
+ offset: 0,
480
+ limit: 100
481
+ }.to_json
482
+ )
483
+ end
398
484
 
399
- context 'forwarding options' do
400
- let!(:places_request) do
401
- stub_request(:get, "#{datastore}/places")
402
- .with(headers: { 'Authorization' => 'Bearer 123' })
485
+ let(:place1_request_stub) do
486
+ stub_request(:get, %r{http://datastore/places/1})
403
487
  .to_return(
404
488
  body: {
405
- items: [{ href: "#{datastore}/places/1" }]
489
+ href: "http://datastore/places/1",
490
+ name: 'Place 1',
491
+ contracts: {
492
+ href: "http://datastore/places/1/contracts"
493
+ }
406
494
  }.to_json
407
495
  )
408
496
  end
409
497
 
410
- let!(:place_request) do
411
- stub_request(:get, "#{datastore}/places/1")
412
- .with(headers: { 'Authorization' => 'Bearer 123' })
498
+ let(:place2_request_stub) do
499
+ stub_request(:get, %r{http://datastore/places/2})
413
500
  .to_return(
414
501
  body: {
415
- name: 'Casa Ferlin'
502
+ href: "http://datastore/places/2",
503
+ name: 'Place 2'
416
504
  }.to_json
417
505
  )
418
506
  end
419
507
 
420
- it 'forwards options used to expand those unexpanded items' do
421
- place = Customer
422
- .includes(:places)
423
- .references(places: { headers: { 'Authorization' => 'Bearer 123' } })
424
- .find(1)
425
- .places.first
426
- assert_requested(place_request)
427
- expect(place.name).to eq 'Casa Ferlin'
508
+ let(:place3_request_stub) do
509
+ stub_request(:get, %r{http://datastore/places/3})
510
+ .to_return(
511
+ body: {
512
+ href: "http://datastore/places/3",
513
+ name: 'Place 3',
514
+ contracts: {
515
+ href: "http://datastore/places/3/contracts"
516
+ }
517
+ }.to_json
518
+ )
428
519
  end
429
- end
430
- end
431
520
 
432
- context 'includes with options' do
433
- before do
434
- class Customer < LHS::Record
435
- endpoint '{+datastore}/customers/{id}'
436
- endpoint '{+datastore}/customers'
521
+ let(:contracts_request_for_place1_stub) do
522
+ stub_request(:get, %r{http://datastore/places/1/contracts})
523
+ .to_return(
524
+ body: {
525
+ items: [{
526
+ href: "http://datastore/places/1/contracts/1",
527
+ name: 'Contract 1'
528
+ }],
529
+ total: 1,
530
+ offset: 0,
531
+ limit: 10
532
+ }.to_json
533
+ )
437
534
  end
438
535
 
439
- class Place < LHS::Record
440
- endpoint '{+datastore}/places'
536
+ let(:contracts_request_for_place3_stub) do
537
+ stub_request(:get, %r{http://datastore/places/3/contracts})
538
+ .to_return(
539
+ body: {
540
+ items: [{
541
+ href: "http://datastore/places/3/contracts/1",
542
+ name: 'Contract 3'
543
+ }],
544
+ total: 1,
545
+ offset: 0,
546
+ limit: 10
547
+ }.to_json
548
+ )
441
549
  end
442
550
 
443
- stub_request(:get, "#{datastore}/places?forwarded_params=123")
444
- .to_return(body: {
445
- 'items' => [{ id: 1 }]
446
- }.to_json)
447
- end
551
+ before do
552
+ class Favorite < LHS::Record
553
+ endpoint 'http://datastore/favorites'
554
+ end
448
555
 
449
- it 'forwards includes options to requests made for those includes' do
450
- stub_request(:get, "#{datastore}/customers/1")
451
- .to_return(body: {
452
- 'places' => {
453
- 'href' => "#{datastore}/places"
454
- }
455
- }.to_json)
456
- customer = Customer
457
- .includes(:places)
458
- .references(places: { params: { forwarded_params: 123 } })
459
- .find(1)
460
- expect(customer.places.first.id).to eq 1
461
- end
556
+ class Place < LHS::Record
557
+ endpoint 'http://datastore/places/{id}'
558
+ end
462
559
 
463
- it 'is chain-able' do
464
- stub_request(:get, "#{datastore}/customers?name=Steve")
465
- .to_return(body: [
466
- 'places' => {
467
- 'href' => "#{datastore}/places"
468
- }
469
- ].to_json)
470
- customers = Customer
471
- .where(name: 'Steve')
472
- .references(places: { params: { forwarded_params: 123 } })
473
- .includes(:places)
474
- expect(customers.first.places.first.id).to eq 1
475
- end
476
- end
560
+ class Contract < LHS::Record
561
+ endpoint 'http://datastore/places/{place_id}/contracts'
562
+ end
477
563
 
478
- context 'more complex examples' do
479
- before do
480
- class Place < LHS::Record
481
- endpoint 'http://datastore/places/{id}'
564
+ favorites_request_stub
565
+ place1_request_stub
566
+ place2_request_stub
567
+ place3_request_stub
568
+ contracts_request_for_place1_stub
569
+ contracts_request_for_place3_stub
482
570
  end
483
- end
484
571
 
485
- it 'forwards complex references' do
486
- stub_request(:get, "http://datastore/places/123?limit=1&forwarded_params=for_place")
487
- .to_return(body: {
488
- 'contracts' => {
489
- 'href' => "http://datastore/places/123/contracts"
490
- }
491
- }.to_json)
492
- stub_request(:get, "http://datastore/places/123/contracts?forwarded_params=for_contracts")
493
- .to_return(body: {
494
- href: "http://datastore/places/123/contracts?forwarded_params=for_contracts",
495
- items: [
496
- { product: { 'href' => "http://datastore/products/llo" } }
497
- ]
498
- }.to_json)
499
- stub_request(:get, "http://datastore/products/llo?forwarded_params=for_product")
500
- .to_return(body: {
501
- 'href' => "http://datastore/products/llo",
502
- 'name' => 'Local Logo'
503
- }.to_json)
504
- place = Place
505
- .options(params: { forwarded_params: 'for_place' })
506
- .includes(contracts: :product)
507
- .references(
508
- contracts: {
509
- params: { forwarded_params: 'for_contracts' },
510
- product: { params: { forwarded_params: 'for_product' } }
511
- }
512
- )
513
- .find_by(id: '123')
514
- expect(
515
- place.contracts.first.product.name
516
- ).to eq 'Local Logo'
517
- end
572
+ it 'includes nested objects when they exist' do
573
+ favorites = Favorite.includes(:place).includes(place: :contracts).all
518
574
 
519
- it 'expands empty arrays' do
520
- stub_request(:get, "http://datastore/places/123")
521
- .to_return(body: {
522
- 'contracts' => {
523
- 'href' => "http://datastore/places/123/contracts"
524
- }
525
- }.to_json)
526
- stub_request(:get, "http://datastore/places/123/contracts")
527
- .to_return(body: {
528
- href: "http://datastore/places/123/contracts",
529
- items: []
530
- }.to_json)
531
- place = Place.includes(:contracts).find('123')
532
- expect(place.contracts.collection?).to eq true
533
- expect(
534
- place.contracts.as_json
535
- ).to eq('href' => 'http://datastore/places/123/contracts', 'items' => [])
536
- expect(place.contracts.to_a).to eq([])
537
- end
538
- end
575
+ expect(favorites.first.place.name).to eq('Place 1')
576
+ expect(favorites.first.place.contracts.first.name).to eq('Contract 1')
577
+ end
539
578
 
540
- context 'include and merge arrays when calling find in parallel' do
541
- before do
542
- class Place < LHS::Record
543
- endpoint 'http://datastore/places/{id}'
579
+ it 'does not include nested objects when they are not there' do
580
+ favorites = Favorite.includes(:place).includes(place: :contracts).all
581
+
582
+ expect(favorites[1].place.name).to eq('Place 2')
583
+ expect(favorites[1].place.contracts).to be(nil)
544
584
  end
545
- stub_request(:get, 'http://datastore/places/1')
546
- .to_return(body: {
547
- category_relations: [{ href: 'http://datastore/category/1' }, { href: 'http://datastore/category/2' }]
548
- }.to_json)
549
- stub_request(:get, 'http://datastore/places/2')
550
- .to_return(body: {
551
- category_relations: [{ href: 'http://datastore/category/2' }, { href: 'http://datastore/category/1' }]
552
- }.to_json)
553
- stub_request(:get, "http://datastore/category/1").to_return(body: { name: 'Food' }.to_json)
554
- stub_request(:get, "http://datastore/category/2").to_return(body: { name: 'Drinks' }.to_json)
555
- end
556
585
 
557
- it 'includes and merges linked resources in case of an array of links' do
558
- places = Place
559
- .includes(:category_relations)
560
- .find(1, 2)
561
- expect(places[0].category_relations[0].name).to eq 'Food'
562
- expect(places[1].category_relations[0].name).to eq 'Drinks'
563
- end
564
- end
586
+ it 'does include and merges objects after nil objects in collections' do
587
+ favorites = Favorite.includes(:place).includes(place: :contracts).all
565
588
 
566
- context 'single href with array response' do
567
- it 'extends base items with arrays' do
568
- class Sector < LHS::Record
569
- endpoint '{+datastore}/sectors'
570
- endpoint '{+datastore}/sectors/{id}'
571
- end
572
-
573
- stub_request(:get, "#{datastore}/sectors")
574
- .with(query: hash_including(key: 'my_service'))
575
- .to_return(body: [
576
- {
577
- href: "#{datastore}/sectors/1",
578
- services: {
579
- href: "#{datastore}/sectors/1/services"
580
- },
581
- keys: [
582
- {
583
- key: 'my_service',
584
- language: 'de'
585
- }
586
- ]
587
- }
588
- ].to_json)
589
-
590
- stub_request(:get, "#{datastore}/sectors/1/services")
591
- .to_return(body: [
592
- {
593
- href: "#{datastore}/services/s1",
594
- price_in_cents: 9900,
595
- key: 'my_service_service_1'
596
- },
597
- {
598
- href: "#{datastore}/services/s2",
599
- price_in_cents: 19900,
600
- key: 'my_service_service_2'
601
- }
602
- ].to_json)
603
-
604
- sector = Sector.includes(:services).find_by(key: 'my_service')
605
- expect(sector.services.length).to eq 2
606
- expect(sector.services.first.key).to eq 'my_service_service_1'
589
+ expect(favorites.last.place.name).to eq('Place 3')
590
+ expect(favorites.last.place.contracts.first.name).to eq('Contract 3')
591
+ end
607
592
  end
608
593
  end
609
594
 
610
- context 'include for POST/create' do
595
+ context 'includes collection trough single item' do
611
596
 
612
597
  before do
613
- class Record < LHS::Record
614
- endpoint 'https://records'
598
+ class Place < LHS::Record
599
+ endpoint 'https://places/{id}'
615
600
  end
616
- stub_request(:post, 'https://records/')
617
- .with(body: { color: 'blue' }.to_json)
601
+
602
+ stub_request(:get, 'https://places/1')
603
+ .to_return(
604
+ body: {
605
+ customer: { href: 'https://customers/1' }
606
+ }.to_json
607
+ )
608
+
609
+ stub_request(:get, 'https://customers/1?limit=100')
618
610
  .to_return(
619
611
  body: {
620
- color: 'blue',
621
- alternative_categories: [
622
- { href: 'https://categories/blue' }
623
- ]
612
+ addresses: { 'href': 'https://customer/1/addresses' }
624
613
  }.to_json
625
614
  )
626
- stub_request(:get, 'https://categories/blue')
615
+
616
+ stub_request(:get, 'https://customer/1/addresses?limit=100')
627
617
  .to_return(
628
618
  body: {
629
- name: 'blue'
619
+ items: [
620
+ { city: 'Zurich', no: 1 },
621
+ { city: 'Zurich', no: 2 }
622
+ ],
623
+ total: 2
630
624
  }.to_json
631
625
  )
632
626
  end
633
627
 
634
- it 'includes the resources from the post response' do
635
- records = Record.includes(:alternative_categories).create(color: 'blue')
636
- expect(records.alternative_categories.first.name).to eq 'blue'
628
+ it 'includes a collection trough a single item without exceptions' do
629
+ place = Place
630
+ .includes(customer: :addresses)
631
+ .find(1)
632
+ expect(place.customer.addresses.map(&:no)).to eq [1, 2]
637
633
  end
638
634
  end
639
635
 
640
- context 'nested within another structure' do
636
+ context 'does not fail including all linked resources' do
637
+
641
638
  before do
642
- class Place < LHS::Record
643
- endpoint 'https://places/{id}'
639
+ class CustomerOnboardingToken < LHS::Record
640
+ endpoint 'https://token/{id}'
644
641
  end
645
- stub_request(:get, "https://places/1")
646
- .to_return(body: {
647
- customer: {
648
- salesforce: {
649
- href: 'https://salesforce/customers/1'
650
- }
651
- }
652
- }.to_json)
653
- end
654
-
655
- let!(:nested_request) do
656
- stub_request(:get, "https://salesforce/customers/1")
657
- .to_return(body: {
658
- name: 'Steve'
659
- }.to_json)
660
- end
661
642
 
662
- it 'includes data that has been nested in an additional structure' do
663
- place = Place.includes(customer: :salesforce).find(1)
664
- expect(nested_request).to have_been_requested
665
- expect(place.customer.salesforce.name).to eq 'Steve'
666
- end
667
-
668
- context 'included data has a configured record endpoint option' do
669
- before do
670
- class SalesforceCustomer < LHS::Record
671
- endpoint 'https://salesforce/customers/{id}', headers: { 'Authorization': 'Bearer 123' }
672
- end
643
+ class Place < LHS::Record
644
+ endpoint 'https://places/{id}'
673
645
  end
674
646
 
675
- let!(:nested_request) do
676
- stub_request(:get, "https://salesforce/customers/1")
677
- .with(headers: { 'Authorization' => 'Bearer 123' })
678
- .to_return(body: {
679
- name: 'Steve'
680
- }.to_json)
647
+ class AvailableAsset < LHS::Record
648
+ endpoint 'https://assets'
681
649
  end
682
650
 
683
- it 'includes data that has been nested in an additional structure' do
684
- place = Place.includes(customer: :salesforce).find(1)
685
- expect(nested_request).to have_been_requested
686
- expect(place.customer.salesforce.name).to eq 'Steve'
687
- end
688
- end
689
- end
651
+ stub_request(:get, 'https://token/1')
652
+ .to_return(
653
+ body: {
654
+ places: [{ href: 'https://places/1' }]
655
+ }.to_json
656
+ )
690
657
 
691
- context 'include empty structures' do
692
- before do
693
- class Place < LHS::Record
694
- endpoint 'https://places/{id}'
695
- end
696
- stub_request(:get, "https://places/1")
697
- .to_return(body: {
698
- id: '123'
699
- }.to_json)
700
- end
658
+ stub_request(:get, 'https://places/1?limit=100')
659
+ .to_return(
660
+ body: {
661
+ available_assets: { 'href': 'https://assets?limit=10&offset=0' }
662
+ }.to_json
663
+ )
701
664
 
702
- it 'skips includes when there is nothing and also does not raise an exception' do
703
- expect(-> {
704
- Place.includes(contracts: :product).find(1)
705
- }).not_to raise_exception
706
- end
707
- end
665
+ stub_request(:get, 'https://assets?limit=10&offset=0')
666
+ .to_return(
667
+ body: {
668
+ items: 10.times.map { { asset_code: 'CATEGORIES' } },
669
+ total: 17,
670
+ offset: 0,
671
+ limit: 10
672
+ }.to_json
673
+ )
708
674
 
709
- context 'include partially empty structures' do
710
- before do
711
- class Place < LHS::Record
712
- endpoint 'https://places/{id}'
713
- end
714
- stub_request(:get, "https://places/1")
715
- .to_return(body: {
716
- id: '123',
717
- customer: {}
718
- }.to_json)
675
+ stub_request(:get, 'https://assets?limit=10&offset=10')
676
+ .to_return(
677
+ body: {
678
+ items: 7.times.map { { asset_code: 'CATEGORIES' } },
679
+ total: 17,
680
+ offset: 0,
681
+ limit: 10
682
+ }.to_json
683
+ )
719
684
  end
720
685
 
721
- it 'skips includes when there is nothing and also does not raise an exception' do
722
- expect(-> {
723
- Place.includes(customer: :salesforce).find(1)
724
- }).not_to raise_exception
686
+ it 'includes a collection trough a single item without exceptions' do
687
+ token = CustomerOnboardingToken
688
+ .includes(places: :available_assets)
689
+ .find(1)
690
+ expect(token.places.first.available_assets.length).to eq 17
725
691
  end
726
692
  end
727
693
  end