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.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Providers
4
+ class InternalServices < LHS::Record
5
+ provider(oauth: true)
6
+ end
7
+ end
@@ -6,6 +6,7 @@ Rails.application.routes.draw do
6
6
  # Automatic Authentication
7
7
  get 'automatic_authentication/oauth' => 'automatic_authentication#o_auth'
8
8
  get 'automatic_authentication/oauth_with_multiple_providers' => 'automatic_authentication#o_auth_with_multiple_providers'
9
+ get 'automatic_authentication/oauth_with_provider' => 'automatic_authentication#o_auth_with_provider'
9
10
 
10
11
  # Request Cycle Cache
11
12
  get 'request_cycle_cache/simple' => 'request_cycle_cache#simple'
@@ -56,7 +56,7 @@ describe LHS::Item do
56
56
  .to_return(status: 200, body: data.to_json)
57
57
  stub_request(:get, "#{datastore}/v2/restaurants/1")
58
58
  .to_return(status: 200, body: { name: 'Casa Ferlin' }.to_json)
59
- item = Record.includes(:restaurant).find(1)
59
+ item = Record.includes_first_page(:restaurant).find(1)
60
60
  item.destroy
61
61
  end
62
62
  end
@@ -27,7 +27,7 @@ describe LHS::Proxy do
27
27
  .to_return(body: {
28
28
  items: [{ review: 'Nice restaurant' }]
29
29
  }.to_json)
30
- result = Search.where(what: 'Blumen').includes(place: :feedbacks)
30
+ result = Search.where(what: 'Blumen').includes_first_page(place: :feedbacks)
31
31
  expect(result.place.feedbacks).to be_kind_of Feedback
32
32
  expect(result.place.feedbacks.first.review).to eq 'Nice restaurant'
33
33
  end
@@ -52,7 +52,7 @@ describe LHS::Record do
52
52
  end
53
53
 
54
54
  it 'works in combination with include and includes' do
55
- records = Record.includes(:product).includes_all(:options).all(color: 'blue')
55
+ records = Record.includes_first_page(:product).includes(:options).all(color: 'blue')
56
56
  expect(records.length).to eq total
57
57
  expect(first_page_request).to have_been_requested.times(1)
58
58
  expect(second_page_request).to have_been_requested.times(1)
@@ -88,7 +88,7 @@ describe LHS::Record do
88
88
  stub_request(:get, "#{datastore}/products/LBC")
89
89
  .to_return(body: { name: 'Local Business Card' }.to_json)
90
90
  expect(lambda {
91
- Contract.includes(:product).where(entry_id: '123').all.first
91
+ Contract.includes_first_page(:product).where(entry_id: '123').all.first
92
92
  }).not_to raise_error # Multiple base endpoints found
93
93
  end
94
94
  end
@@ -25,7 +25,7 @@ describe LHS::Record do
25
25
 
26
26
  it 'allows to pass error_handling for includes to LHC' do
27
27
  handler = ->(_) { return { deleted: true } }
28
- record = Record.includes(:other).references(other: { error_handler: { LHC::NotFound => handler } }).find(id: 1)
28
+ record = Record.includes_first_page(:other).references(other: { error_handler: { LHC::NotFound => handler } }).find(id: 1)
29
29
 
30
30
  expect(record.other.deleted).to be(true)
31
31
  end
@@ -112,7 +112,7 @@ describe LHS::Record do
112
112
  end
113
113
 
114
114
  it 'explicit association configuration overrules href class casting' do
115
- place = Place.includes(:categories).find(1)
115
+ place = Place.includes_first_page(:categories).find(1)
116
116
  expect(place.categories.first).to be_kind_of NewCategory
117
117
  expect(place.categories.first.name).to eq('Pizza')
118
118
  end
@@ -108,7 +108,7 @@ describe LHS::Record do
108
108
  end
109
109
 
110
110
  it 'explicit association configuration overrules href class casting' do
111
- place = Place.includes(:category).find(1)
111
+ place = Place.includes_first_page(:category).find(1)
112
112
  expect(place.category).to be_kind_of NewCategory
113
113
  expect(place.category.name).to eq('Pizza')
114
114
  end
@@ -0,0 +1,737 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
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
29
+ before do
30
+ class LocalEntry < LHS::Record
31
+ endpoint '{+datastore}/local-entries'
32
+ endpoint '{+datastore}/local-entries/{id}'
33
+ end
34
+ 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)
51
+ end
52
+
53
+ it 'includes a resource' do
54
+ favorite = Favorite.includes_first_page(:local_entry).find(1)
55
+ expect(favorite.local_entry.company_name).to eq 'local.ch'
56
+ end
57
+
58
+ it 'duplicates a class' do
59
+ expect(Favorite.object_id).not_to eq(Favorite.includes_first_page(:local_entry).object_id)
60
+ end
61
+
62
+ it 'includes a list of resources' do
63
+ favorite = Favorite.includes_first_page(: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'
67
+ end
68
+
69
+ it 'includes an array of resources' do
70
+ favorite = Favorite.includes_first_page([:local_entry, :user]).find(1)
71
+ expect(favorite.local_entry.company_name).to eq 'local.ch'
72
+ expect(favorite.user.name).to eq 'Mario'
73
+ end
74
+ end
75
+
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
85
+ end
86
+
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_first_page(campaign: :entry).where(has_reviews: true)
99
+ expect(feedbacks.first.campaign.entry.name).to eq 'Casa Ferlin'
100
+ end
101
+
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)
108
+
109
+ feedbacks = Feedback.includes_first_page(campaign: :entry).find(123)
110
+ expect(feedbacks.campaign.entry.name).to eq 'Casa Ferlin'
111
+ end
112
+
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)
119
+
120
+ feedbacks = Feedback.includes_first_page(campaign: [:entry, :user]).find(123)
121
+ expect(feedbacks.campaign.entry.name).to eq 'Casa Ferlin'
122
+ expect(feedbacks.campaign.user.name).to eq 'Mario'
123
+ end
124
+
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_first_page(: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'
137
+ end
138
+
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
153
+
154
+ let(:interceptor) { spy('interceptor') }
155
+
156
+ before do
157
+ class Entry < LHS::Record
158
+ endpoint '{+datastore}/local-entries/{id}'
159
+ end
160
+ LHC.config.interceptors = [interceptor]
161
+ end
162
+
163
+ it 'uses interceptors for included links from known services' do
164
+ stub_feedback_request
165
+ stub_entry_request
166
+ expect(Feedback.includes_first_page(:entry).where.first.entry.name).to eq 'Casa Ferlin'
167
+ expect(interceptor).to have_received(:before_request).twice
168
+ end
169
+ end
170
+
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
177
+
178
+ class OptionalChild < LHS::Record
179
+ endpoint '{+datastore}/local-children/{id}'
180
+ end
181
+ end
182
+
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_first_page(: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
194
+ end
195
+
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)
212
+
213
+ stub_request(:get, "#{datastore}/local-children/1")
214
+ .to_return(status: 200, body: {
215
+ href: "#{datastore}/local_children/1",
216
+ name: 'RspecOptionalChild1'
217
+ }.to_json)
218
+
219
+ child = Parent.includes_first_page(:optional_child).where[1].optional_child
220
+ expect(child).not_to be nil
221
+ expect(child.name).to eq 'RspecOptionalChild1'
222
+ end
223
+ end
224
+ end
225
+
226
+ context 'links pointing to nowhere' do
227
+ before do
228
+ class Feedback < LHS::Record
229
+ endpoint '{+datastore}/feedbacks'
230
+ endpoint '{+datastore}/feedbacks/{id}'
231
+ end
232
+
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)
238
+
239
+ stub_request(:get, "#{datastore}/content-ads/51dfc5690cf271c375c5a12d")
240
+ .to_return(status: 404)
241
+ end
242
+
243
+ it 'raises LHC::NotFound for links that cannot be included' do
244
+ expect(-> {
245
+ Feedback.includes_first_page(campaign: :entry).find(123)
246
+ }).to raise_error LHC::NotFound
247
+ end
248
+
249
+ it 'ignores LHC::NotFound for links that cannot be included if configured so with reference options' do
250
+ feedback = Feedback
251
+ .includes_first_page(campaign: :entry)
252
+ .references(campaign: { ignored_errors: [LHC::NotFound] })
253
+ .find(123)
254
+ expect(feedback.campaign._raw.keys.length).to eq 1
255
+ end
256
+ end
257
+
258
+ context 'modules' do
259
+ before do
260
+ module Services
261
+ class LocalEntry < LHS::Record
262
+ endpoint '{+datastore}/local-entries'
263
+ end
264
+
265
+ class Feedback < LHS::Record
266
+ endpoint '{+datastore}/feedbacks'
267
+ end
268
+ end
269
+ stub_request(:get, "http://local.ch/v2/feedbacks?id=123")
270
+ .to_return(body: [].to_json)
271
+ end
272
+
273
+ it 'works with modules' do
274
+ Services::Feedback.includes_first_page(campaign: :entry).find(123)
275
+ end
276
+ end
277
+
278
+ context 'arrays' do
279
+ before do
280
+ class Place < LHS::Record
281
+ endpoint '{+datastore}/place'
282
+ endpoint '{+datastore}/place/{id}'
283
+ end
284
+ end
285
+
286
+ let!(:place_request) do
287
+ stub_request(:get, "#{datastore}/place/1")
288
+ .to_return(body: {
289
+ 'relations' => [
290
+ { 'href' => "#{datastore}/place/relations/2" },
291
+ { 'href' => "#{datastore}/place/relations/3" }
292
+ ]
293
+ }.to_json)
294
+ end
295
+
296
+ let!(:relation_request_1) do
297
+ stub_request(:get, "#{datastore}/place/relations/2")
298
+ .to_return(body: { name: 'Category' }.to_json)
299
+ end
300
+
301
+ let!(:relation_request_2) do
302
+ stub_request(:get, "#{datastore}/place/relations/3")
303
+ .to_return(body: { name: 'ZeFrank' }.to_json)
304
+ end
305
+
306
+ it 'includes items of arrays' do
307
+ place = Place.includes_first_page(:relations).find(1)
308
+ expect(place.relations.first.name).to eq 'Category'
309
+ expect(place.relations[1].name).to eq 'ZeFrank'
310
+ end
311
+
312
+ context 'parallel with empty links' do
313
+ let!(:place_request_2) do
314
+ stub_request(:get, "#{datastore}/place/2")
315
+ .to_return(body: {
316
+ 'relations' => []
317
+ }.to_json)
318
+ end
319
+
320
+ it 'loads places in parallel and merges included data properly' do
321
+ place = Place.includes_first_page(:relations).find(2, 1)
322
+ expect(place[0].relations.empty?).to be true
323
+ expect(place[1].relations[0].name).to eq 'Category'
324
+ expect(place[1].relations[1].name).to eq 'ZeFrank'
325
+ end
326
+ end
327
+ end
328
+
329
+ context 'empty collections' do
330
+ it 'skips including empty collections' do
331
+ class Place < LHS::Record
332
+ endpoint '{+datastore}/place'
333
+ endpoint '{+datastore}/place/{id}'
334
+ end
335
+
336
+ stub_request(:get, "#{datastore}/place/1")
337
+ .to_return(body: {
338
+ 'available_products' => {
339
+ "url" => "#{datastore}/place/1/products",
340
+ "items" => []
341
+ }
342
+ }.to_json)
343
+
344
+ place = Place.includes_first_page(:available_products).find(1)
345
+ expect(place.available_products.empty?).to eq true
346
+ end
347
+ end
348
+
349
+ context 'extend items with arrays' do
350
+ it 'extends base items with arrays' do
351
+ class Place < LHS::Record
352
+ endpoint '{+datastore}/place'
353
+ endpoint '{+datastore}/place/{id}'
354
+ end
355
+
356
+ stub_request(:get, "#{datastore}/place/1")
357
+ .to_return(body: {
358
+ 'contracts' => {
359
+ 'items' => [{ 'href' => "#{datastore}/place/1/contacts/1" }]
360
+ }
361
+ }.to_json)
362
+
363
+ stub_request(:get, "#{datastore}/place/1/contacts/1")
364
+ .to_return(body: {
365
+ 'products' => { 'href' => "#{datastore}/place/1/contacts/1/products" }
366
+ }.to_json)
367
+
368
+ place = Place.includes_first_page(:contracts).find(1)
369
+ expect(place.contracts.first.products.href).to eq "#{datastore}/place/1/contacts/1/products"
370
+ end
371
+ end
372
+
373
+ context 'unexpanded response when requesting the included collection' do
374
+ before do
375
+ class Customer < LHS::Record
376
+ endpoint '{+datastore}/customer/{id}'
377
+ end
378
+ end
379
+
380
+ let!(:customer_request) do
381
+ stub_request(:get, "#{datastore}/customer/1")
382
+ .to_return(body: {
383
+ places: {
384
+ href: "#{datastore}/places"
385
+ }
386
+ }.to_json)
387
+ end
388
+
389
+ let!(:places_request) do
390
+ stub_request(:get, "#{datastore}/places")
391
+ .to_return(body: {
392
+ items: [{ href: "#{datastore}/places/1" }]
393
+ }.to_json)
394
+ end
395
+
396
+ let!(:place_request) do
397
+ stub_request(:get, "#{datastore}/places/1")
398
+ .to_return(body: {
399
+ name: 'Casa Ferlin'
400
+ }.to_json)
401
+ end
402
+
403
+ it 'loads the collection and the single items, if not already expanded' do
404
+ place = Customer.includes_first_page(:places).find(1).places.first
405
+ assert_requested(place_request)
406
+ expect(place.name).to eq 'Casa Ferlin'
407
+ end
408
+
409
+ context 'forwarding options' do
410
+ let!(:places_request) do
411
+ stub_request(:get, "#{datastore}/places")
412
+ .with(headers: { 'Authorization' => 'Bearer 123' })
413
+ .to_return(
414
+ body: {
415
+ items: [{ href: "#{datastore}/places/1" }]
416
+ }.to_json
417
+ )
418
+ end
419
+
420
+ let!(:place_request) do
421
+ stub_request(:get, "#{datastore}/places/1")
422
+ .with(headers: { 'Authorization' => 'Bearer 123' })
423
+ .to_return(
424
+ body: {
425
+ name: 'Casa Ferlin'
426
+ }.to_json
427
+ )
428
+ end
429
+
430
+ it 'forwards options used to expand those unexpanded items' do
431
+ place = Customer
432
+ .includes_first_page(:places)
433
+ .references(places: { headers: { 'Authorization' => 'Bearer 123' } })
434
+ .find(1)
435
+ .places.first
436
+ assert_requested(place_request)
437
+ expect(place.name).to eq 'Casa Ferlin'
438
+ end
439
+ end
440
+ end
441
+
442
+ context 'includes with options' do
443
+ before do
444
+ class Customer < LHS::Record
445
+ endpoint '{+datastore}/customers/{id}'
446
+ endpoint '{+datastore}/customers'
447
+ end
448
+
449
+ class Place < LHS::Record
450
+ endpoint '{+datastore}/places'
451
+ end
452
+
453
+ stub_request(:get, "#{datastore}/places?forwarded_params=123")
454
+ .to_return(body: {
455
+ 'items' => [{ id: 1 }]
456
+ }.to_json)
457
+ end
458
+
459
+ it 'forwards includes options to requests made for those includes' do
460
+ stub_request(:get, "#{datastore}/customers/1")
461
+ .to_return(body: {
462
+ 'places' => {
463
+ 'href' => "#{datastore}/places"
464
+ }
465
+ }.to_json)
466
+ customer = Customer
467
+ .includes_first_page(:places)
468
+ .references(places: { params: { forwarded_params: 123 } })
469
+ .find(1)
470
+ expect(customer.places.first.id).to eq 1
471
+ end
472
+
473
+ it 'is chain-able' do
474
+ stub_request(:get, "#{datastore}/customers?name=Steve")
475
+ .to_return(body: [
476
+ 'places' => {
477
+ 'href' => "#{datastore}/places"
478
+ }
479
+ ].to_json)
480
+ customers = Customer
481
+ .where(name: 'Steve')
482
+ .references(places: { params: { forwarded_params: 123 } })
483
+ .includes_first_page(:places)
484
+ expect(customers.first.places.first.id).to eq 1
485
+ end
486
+ end
487
+
488
+ context 'more complex examples' do
489
+ before do
490
+ class Place < LHS::Record
491
+ endpoint 'http://datastore/places/{id}'
492
+ end
493
+ end
494
+
495
+ it 'forwards complex references' do
496
+ stub_request(:get, "http://datastore/places/123?limit=1&forwarded_params=for_place")
497
+ .to_return(body: {
498
+ 'contracts' => {
499
+ 'href' => "http://datastore/places/123/contracts"
500
+ }
501
+ }.to_json)
502
+ stub_request(:get, "http://datastore/places/123/contracts?forwarded_params=for_contracts")
503
+ .to_return(body: {
504
+ href: "http://datastore/places/123/contracts?forwarded_params=for_contracts",
505
+ items: [
506
+ { product: { 'href' => "http://datastore/products/llo" } }
507
+ ]
508
+ }.to_json)
509
+ stub_request(:get, "http://datastore/products/llo?forwarded_params=for_product")
510
+ .to_return(body: {
511
+ 'href' => "http://datastore/products/llo",
512
+ 'name' => 'Local Logo'
513
+ }.to_json)
514
+ place = Place
515
+ .options(params: { forwarded_params: 'for_place' })
516
+ .includes_first_page(contracts: :product)
517
+ .references(
518
+ contracts: {
519
+ params: { forwarded_params: 'for_contracts' },
520
+ product: { params: { forwarded_params: 'for_product' } }
521
+ }
522
+ )
523
+ .find_by(id: '123')
524
+ expect(
525
+ place.contracts.first.product.name
526
+ ).to eq 'Local Logo'
527
+ end
528
+
529
+ it 'expands empty arrays' do
530
+ stub_request(:get, "http://datastore/places/123")
531
+ .to_return(body: {
532
+ 'contracts' => {
533
+ 'href' => "http://datastore/places/123/contracts"
534
+ }
535
+ }.to_json)
536
+ stub_request(:get, "http://datastore/places/123/contracts")
537
+ .to_return(body: {
538
+ href: "http://datastore/places/123/contracts",
539
+ items: []
540
+ }.to_json)
541
+ place = Place.includes_first_page(:contracts).find('123')
542
+ expect(place.contracts.collection?).to eq true
543
+ expect(
544
+ place.contracts.as_json
545
+ ).to eq('href' => 'http://datastore/places/123/contracts', 'items' => [])
546
+ expect(place.contracts.to_a).to eq([])
547
+ end
548
+ end
549
+
550
+ context 'include and merge arrays when calling find in parallel' do
551
+ before do
552
+ class Place < LHS::Record
553
+ endpoint 'http://datastore/places/{id}'
554
+ end
555
+ stub_request(:get, 'http://datastore/places/1')
556
+ .to_return(body: {
557
+ category_relations: [{ href: 'http://datastore/category/1' }, { href: 'http://datastore/category/2' }]
558
+ }.to_json)
559
+ stub_request(:get, 'http://datastore/places/2')
560
+ .to_return(body: {
561
+ category_relations: [{ href: 'http://datastore/category/2' }, { href: 'http://datastore/category/1' }]
562
+ }.to_json)
563
+ stub_request(:get, "http://datastore/category/1").to_return(body: { name: 'Food' }.to_json)
564
+ stub_request(:get, "http://datastore/category/2").to_return(body: { name: 'Drinks' }.to_json)
565
+ end
566
+
567
+ it 'includes and merges linked resources in case of an array of links' do
568
+ places = Place
569
+ .includes_first_page(:category_relations)
570
+ .find(1, 2)
571
+ expect(places[0].category_relations[0].name).to eq 'Food'
572
+ expect(places[1].category_relations[0].name).to eq 'Drinks'
573
+ end
574
+ end
575
+
576
+ context 'single href with array response' do
577
+ it 'extends base items with arrays' do
578
+ class Sector < LHS::Record
579
+ endpoint '{+datastore}/sectors'
580
+ endpoint '{+datastore}/sectors/{id}'
581
+ end
582
+
583
+ stub_request(:get, "#{datastore}/sectors")
584
+ .with(query: hash_including(key: 'my_service'))
585
+ .to_return(body: [
586
+ {
587
+ href: "#{datastore}/sectors/1",
588
+ services: {
589
+ href: "#{datastore}/sectors/1/services"
590
+ },
591
+ keys: [
592
+ {
593
+ key: 'my_service',
594
+ language: 'de'
595
+ }
596
+ ]
597
+ }
598
+ ].to_json)
599
+
600
+ stub_request(:get, "#{datastore}/sectors/1/services")
601
+ .to_return(body: [
602
+ {
603
+ href: "#{datastore}/services/s1",
604
+ price_in_cents: 9900,
605
+ key: 'my_service_service_1'
606
+ },
607
+ {
608
+ href: "#{datastore}/services/s2",
609
+ price_in_cents: 19900,
610
+ key: 'my_service_service_2'
611
+ }
612
+ ].to_json)
613
+
614
+ sector = Sector.includes_first_page(:services).find_by(key: 'my_service')
615
+ expect(sector.services.length).to eq 2
616
+ expect(sector.services.first.key).to eq 'my_service_service_1'
617
+ end
618
+ end
619
+
620
+ context 'include for POST/create' do
621
+
622
+ before do
623
+ class Record < LHS::Record
624
+ endpoint 'https://records'
625
+ end
626
+ stub_request(:post, 'https://records/')
627
+ .with(body: { color: 'blue' }.to_json)
628
+ .to_return(
629
+ body: {
630
+ color: 'blue',
631
+ alternative_categories: [
632
+ { href: 'https://categories/blue' }
633
+ ]
634
+ }.to_json
635
+ )
636
+ stub_request(:get, 'https://categories/blue')
637
+ .to_return(
638
+ body: {
639
+ name: 'blue'
640
+ }.to_json
641
+ )
642
+ end
643
+
644
+ it 'includes the resources from the post response' do
645
+ records = Record.includes_first_page(:alternative_categories).create(color: 'blue')
646
+ expect(records.alternative_categories.first.name).to eq 'blue'
647
+ end
648
+ end
649
+
650
+ context 'nested within another structure' do
651
+ before do
652
+ class Place < LHS::Record
653
+ endpoint 'https://places/{id}'
654
+ end
655
+ stub_request(:get, "https://places/1")
656
+ .to_return(body: {
657
+ customer: {
658
+ salesforce: {
659
+ href: 'https://salesforce/customers/1'
660
+ }
661
+ }
662
+ }.to_json)
663
+ end
664
+
665
+ let!(:nested_request) do
666
+ stub_request(:get, "https://salesforce/customers/1")
667
+ .to_return(body: {
668
+ name: 'Steve'
669
+ }.to_json)
670
+ end
671
+
672
+ it 'includes data that has been nested in an additional structure' do
673
+ place = Place.includes_first_page(customer: :salesforce).find(1)
674
+ expect(nested_request).to have_been_requested
675
+ expect(place.customer.salesforce.name).to eq 'Steve'
676
+ end
677
+
678
+ context 'included data has a configured record endpoint option' do
679
+ before do
680
+ class SalesforceCustomer < LHS::Record
681
+ endpoint 'https://salesforce/customers/{id}', headers: { 'Authorization': 'Bearer 123' }
682
+ end
683
+ end
684
+
685
+ let!(:nested_request) do
686
+ stub_request(:get, "https://salesforce/customers/1")
687
+ .with(headers: { 'Authorization' => 'Bearer 123' })
688
+ .to_return(body: {
689
+ name: 'Steve'
690
+ }.to_json)
691
+ end
692
+
693
+ it 'includes data that has been nested in an additional structure' do
694
+ place = Place.includes_first_page(customer: :salesforce).find(1)
695
+ expect(nested_request).to have_been_requested
696
+ expect(place.customer.salesforce.name).to eq 'Steve'
697
+ end
698
+ end
699
+ end
700
+
701
+ context 'include empty structures' do
702
+ before do
703
+ class Place < LHS::Record
704
+ endpoint 'https://places/{id}'
705
+ end
706
+ stub_request(:get, "https://places/1")
707
+ .to_return(body: {
708
+ id: '123'
709
+ }.to_json)
710
+ end
711
+
712
+ it 'skips includes when there is nothing and also does not raise an exception' do
713
+ expect(-> {
714
+ Place.includes_first_page(contracts: :product).find(1)
715
+ }).not_to raise_exception
716
+ end
717
+ end
718
+
719
+ context 'include partially empty structures' do
720
+ before do
721
+ class Place < LHS::Record
722
+ endpoint 'https://places/{id}'
723
+ end
724
+ stub_request(:get, "https://places/1")
725
+ .to_return(body: {
726
+ id: '123',
727
+ customer: {}
728
+ }.to_json)
729
+ end
730
+
731
+ it 'skips includes when there is nothing and also does not raise an exception' do
732
+ expect(-> {
733
+ Place.includes_first_page(customer: :salesforce).find(1)
734
+ }).not_to raise_exception
735
+ end
736
+ end
737
+ end