lhs 21.3.1 → 22.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c43753bff8b039bf6b5b2384333afcc1d09da6c61f1616b06751c7a023e0fbc0
4
- data.tar.gz: ee5cd17ace1e7f82fc8a713dc493543e800658bd76909ddb026951d84f31c2df
3
+ metadata.gz: a749e7862bb20791a4a25fa5fdce92461ebfceaf6e30602c7e600f7342a69806
4
+ data.tar.gz: eceb2d57d258630583394079f495474320354db45e15c205f66005b570abc448
5
5
  SHA512:
6
- metadata.gz: 14d9ddf1745b15e1c635060eec564a3e578f31829e24dede722ca5509a1dffeefec3fb0daaeb27c94f3c9854314d41fe9042cccfbd04ca3fdc2bab3a32bb9120
7
- data.tar.gz: ac97fc333b97f8492bc519b401b68377af441e398486ceaea183d8b006015b691d1aad9a0f518ebf5367cf4427098d767e96354813da94807021195a38f3e997
6
+ metadata.gz: 9432edd5f7d4c2d76f873d21bb3e79c2bc54965e64d3a2df7aa8c06f64dbea479274adde5ea4d1f1acf3e128d4cc7af1fa8fb41bc801b6b3629e6ddbf1156667
7
+ data.tar.gz: 719d291ec00314bf8061cbe72a6abab98e1466a7162dcd174588ffc57823b8509951eb2ed28a4be3d6b6e938bd0791ff26cab9f6d478dd49f50cf28139b3cdbf
data/README.md CHANGED
@@ -2093,7 +2093,7 @@ In a service-oriented architecture using [hyperlinks](https://en.wikipedia.org/w
2093
2093
 
2094
2094
  When fetching records with LHS, you can specify in advance all the linked resources that you want to include in the results.
2095
2095
 
2096
- 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.
2096
+ With `includes` LHS ensures that all matching and explicitly linked resources are loaded and merged (even if the linked resources are paginated).
2097
2097
 
2098
2098
  Including linked resources/records is heavily influenced by [https://guides.rubyonrails.org/active_record_querying.html](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) and you should read it to understand this feature in all it's glory.
2099
2099
 
@@ -2112,16 +2112,16 @@ Presence.create(place: { href: Place.href_for(123) })
2112
2112
  POST '/presences' { place: { href: "http://datastore/places/123" } }
2113
2113
  ```
2114
2114
 
2115
- #### Ensure the whole linked collection is included: includes_all
2115
+ #### Ensure the whole linked collection is included with includes
2116
2116
 
2117
- 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`.
2117
+ 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`.
2118
2118
 
2119
2119
  LHS will ensure that all linked resources are around by loading all pages (parallelized/performance optimized).
2120
2120
 
2121
2121
  ```ruby
2122
2122
  # app/controllers/some_controller.rb
2123
2123
 
2124
- customer = Customer.includes_all(contracts: :products).find(1)
2124
+ customer = Customer.includes(contracts: :products).find(1)
2125
2125
  ```
2126
2126
  ```
2127
2127
  > GET https://service.example.com/customers/1
@@ -2148,14 +2148,14 @@ customer.contracts.first.products.first.name # Local Business Card
2148
2148
 
2149
2149
  ```
2150
2150
 
2151
- #### Include the first linked page or single item is included: include
2151
+ #### Include only the first linked page of a linked collection: includes_first_page
2152
2152
 
2153
- `includes` includes the first page/response when loading the linked resource. **If the endpoint is paginated, only the first page will be included.**
2153
+ `includes_first_page` includes the first page/response when loading the linked resource. **If the endpoint is paginated, only the first page will be included.**
2154
2154
 
2155
2155
  ```ruby
2156
2156
  # app/controllers/some_controller.rb
2157
2157
 
2158
- customer = Customer.includes(contracts: :products).find(1)
2158
+ customer = Customer.includes_first_page(contracts: :products).find(1)
2159
2159
  ```
2160
2160
  ```
2161
2161
  > GET https://service.example.com/customers/1
@@ -2179,7 +2179,7 @@ customer.contracts.first.products.first.name # Local Business Card
2179
2179
 
2180
2180
  #### Include various levels of linked data
2181
2181
 
2182
- The method syntax of `includes` and `includes_all`, allows you include hyperlinks stored in deep nested data strutures:
2182
+ The method syntax of `includes` allows you include hyperlinks stored in deep nested data strutures:
2183
2183
 
2184
2184
  Some examples:
2185
2185
 
@@ -2199,7 +2199,7 @@ Record.includes(campaign: [:entry, :user])
2199
2199
 
2200
2200
  #### Identify and cast known records when including records
2201
2201
 
2202
- When including linked resources with `includes` or `includes_all`, already defined records and their endpoints and configurations are used to make the requests to fetch the additional data.
2202
+ When including linked resources with `includes`, already defined records and their endpoints and configurations are used to make the requests to fetch the additional data.
2203
2203
 
2204
2204
  That also means that options for endpoints of linked resources are applied when requesting those in addition.
2205
2205
 
@@ -64,11 +64,11 @@ class LHS::Record
64
64
  chain
65
65
  end
66
66
 
67
- def includes(*args)
67
+ def includes_first_page(*args)
68
68
  Chain.new(self, Include.new(Chain.unfold(args)))
69
69
  end
70
70
 
71
- def includes_all(*args)
71
+ def includes(*args)
72
72
  chain = Chain.new(self, Include.new(Chain.unfold(args)))
73
73
  chain.include_all!(args)
74
74
  chain
@@ -259,11 +259,11 @@ class LHS::Record
259
259
  push(ErrorHandling.new(error_class => handler))
260
260
  end
261
261
 
262
- def includes(*args)
262
+ def includes_first_page(*args)
263
263
  push(Include.new(Chain.unfold(args)))
264
264
  end
265
265
 
266
- def includes_all(*args)
266
+ def includes(*args)
267
267
  chain = push(Include.new(Chain.unfold(args)))
268
268
  chain.include_all!(args)
269
269
  chain
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LHS
4
- VERSION = '21.3.1'
4
+ VERSION = '22.0.0'
5
5
  end
@@ -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,727 @@
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
+ it 'sets nil for links that cannot be included' 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
+
242
+ feedback = Feedback.includes_first_page(campaign: :entry).find(123)
243
+ expect(feedback.campaign._raw.keys.count).to eq 1
244
+ expect(feedback.campaign.href).to be_present
245
+ end
246
+ end
247
+
248
+ context 'modules' do
249
+ before do
250
+ module Services
251
+ class LocalEntry < LHS::Record
252
+ endpoint '{+datastore}/local-entries'
253
+ end
254
+
255
+ class Feedback < LHS::Record
256
+ endpoint '{+datastore}/feedbacks'
257
+ end
258
+ end
259
+ stub_request(:get, "http://local.ch/v2/feedbacks?id=123")
260
+ .to_return(body: [].to_json)
261
+ end
262
+
263
+ it 'works with modules' do
264
+ Services::Feedback.includes_first_page(campaign: :entry).find(123)
265
+ end
266
+ end
267
+
268
+ context 'arrays' do
269
+ before do
270
+ class Place < LHS::Record
271
+ endpoint '{+datastore}/place'
272
+ endpoint '{+datastore}/place/{id}'
273
+ end
274
+ end
275
+
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
285
+
286
+ let!(:relation_request_1) do
287
+ stub_request(:get, "#{datastore}/place/relations/2")
288
+ .to_return(body: { name: 'Category' }.to_json)
289
+ end
290
+
291
+ let!(:relation_request_2) do
292
+ stub_request(:get, "#{datastore}/place/relations/3")
293
+ .to_return(body: { name: 'ZeFrank' }.to_json)
294
+ end
295
+
296
+ it 'includes items of arrays' do
297
+ place = Place.includes_first_page(:relations).find(1)
298
+ expect(place.relations.first.name).to eq 'Category'
299
+ expect(place.relations[1].name).to eq 'ZeFrank'
300
+ end
301
+
302
+ context 'parallel with empty links' do
303
+ let!(:place_request_2) do
304
+ stub_request(:get, "#{datastore}/place/2")
305
+ .to_return(body: {
306
+ 'relations' => []
307
+ }.to_json)
308
+ end
309
+
310
+ it 'loads places in parallel and merges included data properly' do
311
+ place = Place.includes_first_page(: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'
315
+ end
316
+ end
317
+ end
318
+
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}'
324
+ end
325
+
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_first_page(: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}'
344
+ end
345
+
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)
352
+
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)
357
+
358
+ place = Place.includes_first_page(:contracts).find(1)
359
+ expect(place.contracts.first.products.href).to eq "#{datastore}/place/1/contacts/1/products"
360
+ end
361
+ end
362
+
363
+ context 'unexpanded response when requesting the included collection' do
364
+ before do
365
+ class Customer < LHS::Record
366
+ endpoint '{+datastore}/customer/{id}'
367
+ end
368
+ end
369
+
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)
377
+ end
378
+
379
+ let!(:places_request) do
380
+ stub_request(:get, "#{datastore}/places")
381
+ .to_return(body: {
382
+ items: [{ href: "#{datastore}/places/1" }]
383
+ }.to_json)
384
+ end
385
+
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
392
+
393
+ it 'loads the collection and the single items, if not already expanded' do
394
+ place = Customer.includes_first_page(:places).find(1).places.first
395
+ assert_requested(place_request)
396
+ expect(place.name).to eq 'Casa Ferlin'
397
+ end
398
+
399
+ context 'forwarding options' do
400
+ let!(:places_request) do
401
+ stub_request(:get, "#{datastore}/places")
402
+ .with(headers: { 'Authorization' => 'Bearer 123' })
403
+ .to_return(
404
+ body: {
405
+ items: [{ href: "#{datastore}/places/1" }]
406
+ }.to_json
407
+ )
408
+ end
409
+
410
+ let!(:place_request) do
411
+ stub_request(:get, "#{datastore}/places/1")
412
+ .with(headers: { 'Authorization' => 'Bearer 123' })
413
+ .to_return(
414
+ body: {
415
+ name: 'Casa Ferlin'
416
+ }.to_json
417
+ )
418
+ end
419
+
420
+ it 'forwards options used to expand those unexpanded items' do
421
+ place = Customer
422
+ .includes_first_page(: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'
428
+ end
429
+ end
430
+ end
431
+
432
+ context 'includes with options' do
433
+ before do
434
+ class Customer < LHS::Record
435
+ endpoint '{+datastore}/customers/{id}'
436
+ endpoint '{+datastore}/customers'
437
+ end
438
+
439
+ class Place < LHS::Record
440
+ endpoint '{+datastore}/places'
441
+ end
442
+
443
+ stub_request(:get, "#{datastore}/places?forwarded_params=123")
444
+ .to_return(body: {
445
+ 'items' => [{ id: 1 }]
446
+ }.to_json)
447
+ end
448
+
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_first_page(:places)
458
+ .references(places: { params: { forwarded_params: 123 } })
459
+ .find(1)
460
+ expect(customer.places.first.id).to eq 1
461
+ end
462
+
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_first_page(:places)
474
+ expect(customers.first.places.first.id).to eq 1
475
+ end
476
+ end
477
+
478
+ context 'more complex examples' do
479
+ before do
480
+ class Place < LHS::Record
481
+ endpoint 'http://datastore/places/{id}'
482
+ end
483
+ end
484
+
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_first_page(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
518
+
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_first_page(: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
539
+
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}'
544
+ 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
+
557
+ it 'includes and merges linked resources in case of an array of links' do
558
+ places = Place
559
+ .includes_first_page(: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
565
+
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_first_page(: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'
607
+ end
608
+ end
609
+
610
+ context 'include for POST/create' do
611
+
612
+ before do
613
+ class Record < LHS::Record
614
+ endpoint 'https://records'
615
+ end
616
+ stub_request(:post, 'https://records/')
617
+ .with(body: { color: 'blue' }.to_json)
618
+ .to_return(
619
+ body: {
620
+ color: 'blue',
621
+ alternative_categories: [
622
+ { href: 'https://categories/blue' }
623
+ ]
624
+ }.to_json
625
+ )
626
+ stub_request(:get, 'https://categories/blue')
627
+ .to_return(
628
+ body: {
629
+ name: 'blue'
630
+ }.to_json
631
+ )
632
+ end
633
+
634
+ it 'includes the resources from the post response' do
635
+ records = Record.includes_first_page(:alternative_categories).create(color: 'blue')
636
+ expect(records.alternative_categories.first.name).to eq 'blue'
637
+ end
638
+ end
639
+
640
+ context 'nested within another structure' do
641
+ before do
642
+ class Place < LHS::Record
643
+ endpoint 'https://places/{id}'
644
+ 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
+
662
+ it 'includes data that has been nested in an additional structure' do
663
+ place = Place.includes_first_page(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
673
+ end
674
+
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)
681
+ end
682
+
683
+ it 'includes data that has been nested in an additional structure' do
684
+ place = Place.includes_first_page(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
690
+
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
701
+
702
+ it 'skips includes when there is nothing and also does not raise an exception' do
703
+ expect(-> {
704
+ Place.includes_first_page(contracts: :product).find(1)
705
+ }).not_to raise_exception
706
+ end
707
+ end
708
+
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)
719
+ end
720
+
721
+ it 'skips includes when there is nothing and also does not raise an exception' do
722
+ expect(-> {
723
+ Place.includes_first_page(customer: :salesforce).find(1)
724
+ }).not_to raise_exception
725
+ end
726
+ end
727
+ end