lhs 21.2.3 → 22.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +249 -116
  3. data/lhs.gemspec +1 -1
  4. data/lib/lhs.rb +8 -0
  5. data/lib/lhs/concerns/o_auth.rb +25 -0
  6. data/lib/lhs/concerns/record/chainable.rb +4 -4
  7. data/lib/lhs/concerns/record/configuration.rb +28 -11
  8. data/lib/lhs/concerns/record/request.rb +29 -8
  9. data/lib/lhs/config.rb +1 -1
  10. data/lib/lhs/interceptors/auto_oauth/interceptor.rb +33 -0
  11. data/lib/lhs/interceptors/auto_oauth/thread_registry.rb +18 -0
  12. data/lib/lhs/version.rb +1 -1
  13. data/spec/auto_oauth_spec.rb +169 -0
  14. data/spec/dummy/app/controllers/application_controller.rb +15 -0
  15. data/spec/dummy/app/controllers/automatic_authentication_controller.rb +29 -0
  16. data/spec/dummy/app/models/dummy_record_with_auto_oauth_provider.rb +6 -0
  17. data/spec/dummy/app/models/dummy_record_with_multiple_oauth_providers1.rb +7 -0
  18. data/spec/dummy/app/models/dummy_record_with_multiple_oauth_providers2.rb +7 -0
  19. data/spec/dummy/app/models/dummy_record_with_multiple_providers_per_endpoint.rb +6 -0
  20. data/spec/dummy/app/models/dummy_record_with_oauth.rb +7 -0
  21. data/spec/dummy/app/models/providers/internal_services.rb +7 -0
  22. data/spec/dummy/config/routes.rb +5 -0
  23. data/spec/item/destroy_spec.rb +1 -1
  24. data/spec/proxy/record_identification_spec.rb +1 -1
  25. data/spec/record/all_spec.rb +1 -1
  26. data/spec/record/endpoints_spec.rb +1 -1
  27. data/spec/record/error_handling_integration_spec.rb +1 -1
  28. data/spec/record/handle_includes_errors_spec.rb +1 -1
  29. data/spec/record/has_many_spec.rb +1 -1
  30. data/spec/record/has_one_spec.rb +1 -1
  31. data/spec/record/includes_first_page_spec.rb +727 -0
  32. data/spec/record/includes_spec.rb +546 -561
  33. data/spec/record/includes_warning_spec.rb +1 -1
  34. data/spec/record/mapping_spec.rb +2 -2
  35. data/spec/record/references_spec.rb +1 -1
  36. data/spec/record/relation_caching_spec.rb +3 -3
  37. data/spec/request_cycle_cache_spec.rb +3 -3
  38. metadata +27 -8
  39. data/spec/record/includes_all_spec.rb +0 -693
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AutomaticAuthenticationController < ApplicationController
4
+
5
+ def o_auth
6
+ render json: {
7
+ record: DummyRecordWithOauth.find(1).as_json,
8
+ records: DummyRecordWithOauth.where(color: 'blue').as_json
9
+ }
10
+ end
11
+
12
+ def o_auth_with_multiple_providers
13
+ render json: {
14
+ record: DummyRecordWithMultipleOauthProviders1.find(1).as_json,
15
+ records: DummyRecordWithMultipleOauthProviders2.where(color: 'blue').as_json,
16
+ per_endpoint: {
17
+ record: DummyRecordWithMultipleOauthProvidersPerEndpoint.find(1).as_json,
18
+ records: DummyRecordWithMultipleOauthProvidersPerEndpoint.where(color: 'blue').as_json
19
+ }
20
+ }
21
+ end
22
+
23
+ def o_auth_with_provider
24
+ render json: {
25
+ record: DummyRecordWithAutoOauthProvider.find(1).as_json,
26
+ records: DummyRecordWithAutoOauthProvider.where(color: 'blue').as_json
27
+ }
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DummyRecordWithAutoOauthProvider < Providers::InternalServices
4
+ endpoint 'http://internalservice/v2/records'
5
+ endpoint 'http://internalservice/v2/records/{id}'
6
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DummyRecordWithMultipleOauthProviders1 < LHS::Record
4
+ oauth(:provider1)
5
+ endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_1'
6
+ endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_1/{id}'
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DummyRecordWithMultipleOauthProviders2 < LHS::Record
4
+ oauth(:provider2)
5
+ endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_2'
6
+ endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_2/{id}'
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DummyRecordWithMultipleOauthProvidersPerEndpoint < LHS::Record
4
+ endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_per_endpoint', oauth: :provider1
5
+ endpoint 'http://datastore/v2/records_with_multiple_oauth_providers_per_endpoint/{id}', oauth: :provider2
6
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DummyRecordWithOauth < LHS::Record
4
+ oauth
5
+ endpoint 'http://datastore/v2/records_with_oauth'
6
+ endpoint 'http://datastore/v2/records_with_oauth/{id}'
7
+ end
@@ -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
@@ -3,6 +3,11 @@
3
3
  Rails.application.routes.draw do
4
4
  root 'application#root'
5
5
 
6
+ # Automatic Authentication
7
+ get 'automatic_authentication/oauth' => 'automatic_authentication#o_auth'
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'
10
+
6
11
  # Request Cycle Cache
7
12
  get 'request_cycle_cache/simple' => 'request_cycle_cache#simple'
8
13
  get 'request_cycle_cache/no_caching_interceptor' => 'request_cycle_cache#no_caching_interceptor'
@@ -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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails_helper'
4
- require 'lhc/test/cache_helper.rb'
4
+ require 'lhc/rspec'
5
5
 
6
6
  describe 'Error handling with chains', type: :request do
7
7
  let!(:request) do
@@ -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