lhs 21.3.1 → 23.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +40 -11
- data/lhs.gemspec +1 -1
- data/lib/lhs/concerns/record/chainable.rb +4 -4
- data/lib/lhs/concerns/record/request.rb +18 -24
- data/lib/lhs/concerns/record/update.rb +17 -0
- data/lib/lhs/record.rb +6 -3
- data/lib/lhs/version.rb +1 -1
- data/spec/item/destroy_spec.rb +1 -1
- data/spec/proxy/record_identification_spec.rb +1 -1
- data/spec/record/all_spec.rb +1 -1
- data/spec/record/endpoints_spec.rb +1 -1
- data/spec/record/handle_includes_errors_spec.rb +1 -1
- data/spec/record/has_many_spec.rb +1 -1
- data/spec/record/has_one_spec.rb +1 -1
- data/spec/record/includes_first_page_spec.rb +737 -0
- data/spec/record/includes_spec.rb +545 -579
- data/spec/record/includes_warning_spec.rb +1 -1
- data/spec/record/mapping_spec.rb +2 -2
- data/spec/record/references_spec.rb +1 -1
- data/spec/record/relation_caching_spec.rb +3 -3
- data/spec/record/update_spec.rb +62 -0
- metadata +9 -6
- data/spec/record/includes_all_spec.rb +0 -693
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bdbfabed0cdff17a569b59c362c34716507eb5447b6e54f4b2903cf057736d9a
|
4
|
+
data.tar.gz: 2ac0567f16a82722d370de8a75a1cdabc412266957047d4ed24e87a00c655f36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4abe219f4c365168b7b6abd9095bb1f72cf8bbf5dc2591ff8457062f50590254c6321b0d244cbac07a5a7cc88c3915cdf135dd9b9185984f7cd029fb152274ef
|
7
|
+
data.tar.gz: 80a97943d67cc14578f23ad6d20074cb329155f54abf89812f3872f84b1c698dacafa12f6616302b1d68787db735ac211295642cc44688033ee85a61be4cce51
|
data/README.md
CHANGED
@@ -99,6 +99,8 @@ record.review # "Lunch was great
|
|
99
99
|
* [Change/Update existing records](#changeupdate-existing-records)
|
100
100
|
* [save](#save)
|
101
101
|
* [update](#update)
|
102
|
+
* [Directly via Record](#directly-via-record)
|
103
|
+
* [per Instance](#per-instance)
|
102
104
|
* [partial_update](#partial_update)
|
103
105
|
* [Endpoint url parameter injection during record creation/change](#endpoint-url-parameter-injection-during-record-creationchange)
|
104
106
|
* [Record validation](#record-validation)
|
@@ -119,8 +121,8 @@ record.review # "Lunch was great
|
|
119
121
|
* [Record getters](#record-getters)
|
120
122
|
* [Include linked resources (hyperlinks and hypermedia)](#include-linked-resources-hyperlinks-and-hypermedia)
|
121
123
|
* [Generate links from parameters](#generate-links-from-parameters)
|
122
|
-
* [Ensure the whole linked collection is included
|
123
|
-
* [Include the first linked page
|
124
|
+
* [Ensure the whole linked collection is included with includes](#ensure-the-whole-linked-collection-is-included-with-includes)
|
125
|
+
* [Include only the first linked page of a linked collection: includes_first_page](#include-only-the-first-linked-page-of-a-linked-collection-includes_first_page)
|
124
126
|
* [Include various levels of linked data](#include-various-levels-of-linked-data)
|
125
127
|
* [Identify and cast known records when including records](#identify-and-cast-known-records-when-including-records)
|
126
128
|
* [Apply options for requests performed to fetch included records](#apply-options-for-requests-performed-to-fetch-included-records)
|
@@ -152,6 +154,7 @@ record.review # "Lunch was great
|
|
152
154
|
|
153
155
|
|
154
156
|
|
157
|
+
|
155
158
|
## Installation/Startup checklist
|
156
159
|
|
157
160
|
- [ ] Install LHS gem, preferably via `Gemfile`
|
@@ -1607,6 +1610,21 @@ POST https://service.example.com/records/1z-5r1fkaj { body: "{ 'name': 'Starbuck
|
|
1607
1610
|
|
1608
1611
|
##### update
|
1609
1612
|
|
1613
|
+
###### Directly via Record
|
1614
|
+
|
1615
|
+
```ruby
|
1616
|
+
# app/controllers/some_controller.rb
|
1617
|
+
|
1618
|
+
Record.update(id: '1z-5r1fkaj', name: 'Steve')
|
1619
|
+
|
1620
|
+
```
|
1621
|
+
```
|
1622
|
+
GET https://service.example.com/records/1z-5r1fkaj
|
1623
|
+
{ name: 'Steve' }
|
1624
|
+
```
|
1625
|
+
|
1626
|
+
###### per Instance
|
1627
|
+
|
1610
1628
|
`update` persists the whole object after new parameters are applied through arguments.
|
1611
1629
|
|
1612
1630
|
`update` will return false if persisting fails. `update!` instead will raise an exception.
|
@@ -2093,7 +2111,7 @@ In a service-oriented architecture using [hyperlinks](https://en.wikipedia.org/w
|
|
2093
2111
|
|
2094
2112
|
When fetching records with LHS, you can specify in advance all the linked resources that you want to include in the results.
|
2095
2113
|
|
2096
|
-
With `includes`
|
2114
|
+
With `includes` LHS ensures that all matching and explicitly linked resources are loaded and merged (even if the linked resources are paginated).
|
2097
2115
|
|
2098
2116
|
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
2117
|
|
@@ -2112,16 +2130,16 @@ Presence.create(place: { href: Place.href_for(123) })
|
|
2112
2130
|
POST '/presences' { place: { href: "http://datastore/places/123" } }
|
2113
2131
|
```
|
2114
2132
|
|
2115
|
-
#### Ensure the whole linked collection is included
|
2133
|
+
#### Ensure the whole linked collection is included with includes
|
2116
2134
|
|
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 `
|
2135
|
+
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
2136
|
|
2119
2137
|
LHS will ensure that all linked resources are around by loading all pages (parallelized/performance optimized).
|
2120
2138
|
|
2121
2139
|
```ruby
|
2122
2140
|
# app/controllers/some_controller.rb
|
2123
2141
|
|
2124
|
-
customer = Customer.
|
2142
|
+
customer = Customer.includes(contracts: :products).find(1)
|
2125
2143
|
```
|
2126
2144
|
```
|
2127
2145
|
> GET https://service.example.com/customers/1
|
@@ -2148,14 +2166,14 @@ customer.contracts.first.products.first.name # Local Business Card
|
|
2148
2166
|
|
2149
2167
|
```
|
2150
2168
|
|
2151
|
-
#### Include the first linked page
|
2169
|
+
#### Include only the first linked page of a linked collection: includes_first_page
|
2152
2170
|
|
2153
|
-
`
|
2171
|
+
`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
2172
|
|
2155
2173
|
```ruby
|
2156
2174
|
# app/controllers/some_controller.rb
|
2157
2175
|
|
2158
|
-
customer = Customer.
|
2176
|
+
customer = Customer.includes_first_page(contracts: :products).find(1)
|
2159
2177
|
```
|
2160
2178
|
```
|
2161
2179
|
> GET https://service.example.com/customers/1
|
@@ -2179,7 +2197,7 @@ customer.contracts.first.products.first.name # Local Business Card
|
|
2179
2197
|
|
2180
2198
|
#### Include various levels of linked data
|
2181
2199
|
|
2182
|
-
The method syntax of `includes`
|
2200
|
+
The method syntax of `includes` allows you include hyperlinks stored in deep nested data strutures:
|
2183
2201
|
|
2184
2202
|
Some examples:
|
2185
2203
|
|
@@ -2199,7 +2217,7 @@ Record.includes(campaign: [:entry, :user])
|
|
2199
2217
|
|
2200
2218
|
#### Identify and cast known records when including records
|
2201
2219
|
|
2202
|
-
When including linked resources with `includes
|
2220
|
+
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
2221
|
|
2204
2222
|
That also means that options for endpoints of linked resources are applied when requesting those in addition.
|
2205
2223
|
|
@@ -2261,6 +2279,17 @@ In parallel:
|
|
2261
2279
|
GET https://service.example.com/places/4 { headers: { 'Authentication': 'Bearer 123' } }
|
2262
2280
|
```
|
2263
2281
|
|
2282
|
+
Here is another example, if you want to ignore errors, that occure while you fetch included resources:
|
2283
|
+
|
2284
|
+
```ruby
|
2285
|
+
# app/controllers/some_controller.rb
|
2286
|
+
|
2287
|
+
feedback = Feedback
|
2288
|
+
.includes(campaign: :entry)
|
2289
|
+
.references(campaign: { ignored_errors: [LHC::NotFound] })
|
2290
|
+
.find(12345)
|
2291
|
+
```
|
2292
|
+
|
2264
2293
|
### Record batch processing
|
2265
2294
|
|
2266
2295
|
**Be careful using methods for batch processing. They could result in a lot of HTTP requests!**
|
data/lhs.gemspec
CHANGED
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
|
|
24
24
|
|
25
25
|
s.add_dependency 'activemodel'
|
26
26
|
s.add_dependency 'activesupport', '>= 4.2.11'
|
27
|
-
s.add_dependency 'lhc', '>=
|
27
|
+
s.add_dependency 'lhc', '>= 11.2.0', '< 12'
|
28
28
|
s.add_dependency 'local_uri'
|
29
29
|
|
30
30
|
s.add_development_dependency 'capybara'
|
@@ -64,11 +64,11 @@ class LHS::Record
|
|
64
64
|
chain
|
65
65
|
end
|
66
66
|
|
67
|
-
def
|
67
|
+
def includes_first_page(*args)
|
68
68
|
Chain.new(self, Include.new(Chain.unfold(args)))
|
69
69
|
end
|
70
70
|
|
71
|
-
def
|
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
|
262
|
+
def includes_first_page(*args)
|
263
263
|
push(Include.new(Chain.unfold(args)))
|
264
264
|
end
|
265
265
|
|
266
|
-
def
|
266
|
+
def includes(*args)
|
267
267
|
chain = push(Include.new(Chain.unfold(args)))
|
268
268
|
chain.include_all!(args)
|
269
269
|
chain
|
@@ -138,6 +138,7 @@ class LHS::Record
|
|
138
138
|
end
|
139
139
|
|
140
140
|
def extend_base_item!(data, addition, key)
|
141
|
+
return if addition.nil?
|
141
142
|
if addition.collection?
|
142
143
|
extend_base_item_with_collection!(data, addition, key)
|
143
144
|
else # simple case merges hash into hash
|
@@ -187,7 +188,7 @@ class LHS::Record
|
|
187
188
|
options = extend_with_reference(options, reference)
|
188
189
|
addition = load_include(options, data, sub_includes, reference)
|
189
190
|
extend_raw_data!(data, addition, included)
|
190
|
-
expand_addition!(data, included, options)
|
191
|
+
expand_addition!(data, included, options) unless expanded_data?(addition)
|
191
192
|
end
|
192
193
|
end
|
193
194
|
|
@@ -205,28 +206,24 @@ class LHS::Record
|
|
205
206
|
def expand_addition!(data, included, reference)
|
206
207
|
addition = data[included]
|
207
208
|
options = options_for_data(addition)
|
208
|
-
options = extend_with_reference(options, reference
|
209
|
+
options = extend_with_reference(options, reference)
|
209
210
|
record = record_for_options(options) || self
|
210
211
|
options = convert_options_to_endpoints(options) if record_for_options(options)
|
211
|
-
expanded_data =
|
212
|
-
record.request(options)
|
213
|
-
rescue LHC::NotFound
|
214
|
-
LHS::Data.new({}, data, record)
|
215
|
-
end
|
212
|
+
expanded_data = record.request(options)
|
216
213
|
extend_raw_data!(data, expanded_data, included)
|
217
214
|
end
|
218
215
|
|
219
|
-
def
|
216
|
+
def expanded_data?(addition)
|
220
217
|
return false if addition.blank?
|
221
218
|
if addition.item?
|
222
|
-
(addition._raw.keys - [:href]).
|
219
|
+
(addition._raw.keys - [:href]).any?
|
223
220
|
elsif addition.collection?
|
224
|
-
addition.
|
221
|
+
addition.any? do |item|
|
225
222
|
next if item.blank?
|
226
223
|
if item._raw.is_a?(Hash)
|
227
|
-
(item._raw.keys - [:href]).
|
224
|
+
(item._raw.keys - [:href]).any?
|
228
225
|
elsif item._raw.is_a?(Array)
|
229
|
-
item.any? { |item| (item._raw.keys - [:href]).
|
226
|
+
item.any? { |item| (item._raw.keys - [:href]).any? }
|
230
227
|
end
|
231
228
|
end
|
232
229
|
end
|
@@ -234,7 +231,8 @@ class LHS::Record
|
|
234
231
|
|
235
232
|
# Extends request options with options provided for this reference
|
236
233
|
def extend_with_reference(options, reference)
|
237
|
-
return options
|
234
|
+
return options if reference.blank?
|
235
|
+
reference = reference.except(:url)
|
238
236
|
options ||= {}
|
239
237
|
if options.is_a?(Array)
|
240
238
|
options.map { |request_options| request_options.merge(reference) if request_options.present? }
|
@@ -348,18 +346,14 @@ class LHS::Record
|
|
348
346
|
end
|
349
347
|
|
350
348
|
# Load additional resources that are requested with include
|
351
|
-
def load_include(options,
|
349
|
+
def load_include(options, _data, sub_includes, references)
|
352
350
|
record = record_for_options(options) || self
|
353
351
|
options = convert_options_to_endpoints(options) if record_for_options(options)
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
load_include_simple!(options, record)
|
360
|
-
end
|
361
|
-
rescue LHC::NotFound
|
362
|
-
LHS::Data.new({}, data, record)
|
352
|
+
prepare_options_for_include_request!(options, sub_includes, references)
|
353
|
+
if references && references[:all] # include all linked resources
|
354
|
+
load_include_all!(options, record, sub_includes, references)
|
355
|
+
else # simply request first page/batch
|
356
|
+
load_include_simple!(options, record)
|
363
357
|
end
|
364
358
|
end
|
365
359
|
|
@@ -372,7 +366,7 @@ class LHS::Record
|
|
372
366
|
|
373
367
|
def load_include_simple!(options, record)
|
374
368
|
data = record.request(options)
|
375
|
-
warn "[WARNING] You included `#{options[:url]}`, but this endpoint is paginated. You might want to use `includes_all` instead of `includes` (https://github.com/local-ch/lhs#includes_all-for-paginated-endpoints)." if paginated?(data._raw)
|
369
|
+
warn "[WARNING] You included `#{options[:url]}`, but this endpoint is paginated. You might want to use `includes_all` instead of `includes` (https://github.com/local-ch/lhs#includes_all-for-paginated-endpoints)." if data && paginated?(data._raw)
|
376
370
|
data
|
377
371
|
end
|
378
372
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
class LHS::Record
|
6
|
+
|
7
|
+
module Update
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
class <<self
|
12
|
+
alias_method :update, :create
|
13
|
+
alias_method :update!, :create!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/lhs/record.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class LHS::Record
|
4
|
+
autoload :AttributeAssignment,
|
5
|
+
'lhs/concerns/record/attribute_assignment'
|
4
6
|
autoload :Batch,
|
5
7
|
'lhs/concerns/record/batch'
|
6
8
|
autoload :Chainable,
|
@@ -45,9 +47,10 @@ class LHS::Record
|
|
45
47
|
'lhs/concerns/record/scope'
|
46
48
|
autoload :Tracing,
|
47
49
|
'lhs/concerns/record/tracing'
|
48
|
-
autoload :
|
49
|
-
'lhs/concerns/record/
|
50
|
+
autoload :Update,
|
51
|
+
'lhs/concerns/record/update'
|
50
52
|
|
53
|
+
include AttributeAssignment
|
51
54
|
include Batch
|
52
55
|
include Chainable
|
53
56
|
include Configuration
|
@@ -72,7 +75,7 @@ class LHS::Record
|
|
72
75
|
include Relations
|
73
76
|
include Scope
|
74
77
|
include Tracing
|
75
|
-
include
|
78
|
+
include Update
|
76
79
|
|
77
80
|
delegate :_proxy, :_endpoint, :merge_raw!, :select, :becomes, :respond_to?, to: :_data
|
78
81
|
|
data/lib/lhs/version.rb
CHANGED
data/spec/item/destroy_spec.rb
CHANGED
@@ -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.
|
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').
|
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
|
data/spec/record/all_spec.rb
CHANGED
@@ -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.
|
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.
|
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.
|
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.
|
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
|
data/spec/record/has_one_spec.rb
CHANGED
@@ -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.
|
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
|