lhs 15.5.1 → 15.6.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/README.md +1630 -780
- data/lhs.gemspec +2 -2
- data/lib/lhs/concerns/record/chainable.rb +4 -0
- data/lib/lhs/concerns/record/configuration.rb +7 -0
- data/lib/lhs/concerns/record/request.rb +4 -2
- data/lib/lhs/version.rb +1 -1
- data/spec/autoloading_spec.rb +1 -1
- data/spec/dummy/app/controllers/error_handling_with_chains_controller.rb +34 -0
- data/spec/dummy/app/models/record.rb +4 -0
- data/spec/dummy/app/views/error_handling_with_chains/error.html.erb +1 -0
- data/spec/dummy/app/views/error_handling_with_chains/show.html.erb +3 -0
- data/spec/dummy/config/routes.rb +6 -0
- data/spec/pagination/parameters_spec.rb +2 -2
- data/spec/record/all_spec.rb +26 -0
- data/spec/record/error_handling_integration_spec.rb +23 -0
- data/spec/record/{chain_error_handling_spec.rb → error_handling_spec.rb} +0 -0
- data/spec/record/fetch_spec.rb +24 -9
- data/spec/{request_cycle_cache/main_spec.rb → request_cycle_cache_spec.rb} +8 -8
- data/spec/support/reset.rb +43 -0
- metadata +24 -13
- data/spec/support/cleanup.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d0d357a05ee1bf7c6943779c92169c9a411a4b658e20a6adaf651f29d4e743e8
|
4
|
+
data.tar.gz: ad887ed271702978c90da76ec842ec36e616d1f2b549f82b72dfad337cb03805
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86c1ecd1b8f80b395ca5295a04686c8d596ec36299c8a4e5b22ee917ed8fad83d07dbb778df0146f4a8e42dc3f2f29fa927389ce2376b1e354961560cd9149b2
|
7
|
+
data.tar.gz: 1704037a4744234e2c693eaa2a346f9be12d0f4bfac92790cb12c78671048c15013944632e4bfb410586231a2e37a9141a122753429ad3246eef460831b3b2ab
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
LHS
|
2
2
|
===
|
3
3
|
|
4
|
-
LHS uses [LHC](//github.com/local-ch/LHC) for http requests.
|
4
|
+
LHS uses [LHC](//github.com/local-ch/LHC) for advanced http requests.
|
5
5
|
|
6
6
|
## Quickstart
|
7
7
|
|
@@ -9,334 +9,587 @@ LHS uses [LHC](//github.com/local-ch/LHC) for http requests.
|
|
9
9
|
gem 'lhs'
|
10
10
|
```
|
11
11
|
|
12
|
-
LHS comes with Request Cycle Cache – enabled by default. It requires [LHC Caching Interceptor](https://github.com/local-ch/lhc/blob/master/docs/interceptors/caching.md) to be enabled:
|
13
|
-
|
14
12
|
```ruby
|
15
|
-
#
|
13
|
+
# config/initializers/lhc.rb
|
14
|
+
|
16
15
|
LHC.configure do |config|
|
17
|
-
config.
|
16
|
+
config.placeholder(:service, 'https://my.service.dev')
|
18
17
|
end
|
19
18
|
```
|
20
19
|
|
21
|
-
## Very Short Introduction
|
22
|
-
|
23
|
-
Access data that is provided by an http JSON service with ease using a LHS::Record.
|
24
|
-
|
25
20
|
```ruby
|
21
|
+
# app/models/record.rb
|
22
|
+
|
26
23
|
class Record < LHS::Record
|
27
24
|
|
28
|
-
endpoint '{+service}/
|
29
|
-
endpoint '{+service}/
|
25
|
+
endpoint '{+service}/records'
|
26
|
+
endpoint '{+service}/records/{id}'
|
30
27
|
|
31
28
|
end
|
32
|
-
|
33
|
-
record = Record.find_by(email: 'somebody@mail.com') #<Record>
|
34
|
-
record.review # "Lunch was great"
|
35
29
|
```
|
36
30
|
|
37
|
-
|
31
|
+
```ruby
|
32
|
+
# app/controllers/application_controller.rb
|
33
|
+
|
34
|
+
record = Record.find_by(email: 'somebody@mail.com')
|
35
|
+
record.review # "Lunch was great
|
36
|
+
```
|
37
|
+
|
38
|
+
## Table of contents
|
39
|
+
* [LHS](#lhs)
|
40
|
+
* [Quickstart](#quickstart)
|
41
|
+
* [Table of contents](#table-of-contents)
|
42
|
+
* [Installation/Startup checklist](#installationstartup-checklist)
|
43
|
+
* [Record](#record)
|
44
|
+
* [Endpoints](#endpoints)
|
45
|
+
* [Configure endpoint hosts](#configure-endpoint-hosts)
|
46
|
+
* [Ambiguous endpoints](#ambiguous-endpoints)
|
47
|
+
* [Record inheritance](#record-inheritance)
|
48
|
+
* [Find multiple records](#find-multiple-records)
|
49
|
+
* [where](#where)
|
50
|
+
* [Reuse/Dry where statements: Use scopes](#reusedry-where-statements-use-scopes)
|
51
|
+
* [Retrieve the amount of a collection of items: count vs. length](#retrieve-the-amount-of-a-collection-of-items-count-vs-length)
|
52
|
+
* [Find single records](#find-single-records)
|
53
|
+
* [find](#find)
|
54
|
+
* [find_by](#find_by)
|
55
|
+
* [first](#first)
|
56
|
+
* [last](#last)
|
57
|
+
* [Work with retrieved data](#work-with-retrieved-data)
|
58
|
+
* [Automatic detection/conversion of collections](#automatic-detectionconversion-of-collections)
|
59
|
+
* [Map complex data for easy access](#map-complex-data-for-easy-access)
|
60
|
+
* [Access and identify nested records](#access-and-identify-nested-records)
|
61
|
+
* [Relations / Associations](#relations--associations)
|
62
|
+
* [has_many](#has_many)
|
63
|
+
* [has_one](#has_one)
|
64
|
+
* [Unwrap nested items from the response body](#unwrap-nested-items-from-the-response-body)
|
65
|
+
* [Determine collections from the response body](#determine-collections-from-the-response-body)
|
66
|
+
* [Chain complex queries](#chain-complex-queries)
|
67
|
+
* [Chain where queries](#chain-where-queries)
|
68
|
+
* [Expand plain collections of links: expanded](#expand-plain-collections-of-links-expanded)
|
69
|
+
* [Error handling with chains](#error-handling-with-chains)
|
70
|
+
* [Resolve chains: fetch](#resolve-chains-fetch)
|
71
|
+
* [Add request options to a query chain: options](#add-request-options-to-a-query-chain-options)
|
72
|
+
* [Control pagination within a query chain](#control-pagination-within-a-query-chain)
|
73
|
+
* [Record pagination](#record-pagination)
|
74
|
+
* [Pagination strategy](#pagination-strategy)
|
75
|
+
* [Pagination strategy: offset (default)](#pagination-strategy-offset-default)
|
76
|
+
* [Pagination strategy: page](#pagination-strategy-page)
|
77
|
+
* [Pagination strategy: start](#pagination-strategy-start)
|
78
|
+
* [Pagination keys](#pagination-keys)
|
79
|
+
* [limit_key](#limit_key)
|
80
|
+
* [pagination_key](#pagination_key)
|
81
|
+
* [total_key](#total_key)
|
82
|
+
* [Pagination links](#pagination-links)
|
83
|
+
* [next?](#next)
|
84
|
+
* [previous?](#previous)
|
85
|
+
* [Kaminari support (limited)](#kaminari-support-limited)
|
86
|
+
* [Build, create and update records](#build-create-and-update-records)
|
87
|
+
* [Create new records](#create-new-records)
|
88
|
+
* [create](#create)
|
89
|
+
* [Unwrap nested data when creation response nests created record data](#unwrap-nested-data-when-creation-response-nests-created-record-data)
|
90
|
+
* [Create records through associations: Nested sub resources](#create-records-through-associations-nested-sub-resources)
|
91
|
+
* [Start building new records](#start-building-new-records)
|
92
|
+
* [Change/Update existing records](#changeupdate-existing-records)
|
93
|
+
* [save](#save)
|
94
|
+
* [update](#update)
|
95
|
+
* [partial_update](#partial_update)
|
96
|
+
* [Endpoint url parameter injection during record creation/change](#endpoint-url-parameter-injection-during-record-creationchange)
|
97
|
+
* [Record validation](#record-validation)
|
98
|
+
* [Configure record validations](#configure-record-validations)
|
99
|
+
* [HTTP Status Codes for validation errors](#http-status-codes-for-validation-errors)
|
100
|
+
* [Reset validation errors](#reset-validation-errors)
|
101
|
+
* [Add validation errors](#add-validation-errors)
|
102
|
+
* [Validation errors for nested data](#validation-errors-for-nested-data)
|
103
|
+
* [Translation of validation errors](#translation-of-validation-errors)
|
104
|
+
* [Validation error types: errors vs. warnings](#validation-error-types-errors-vs-warnings)
|
105
|
+
* [Persistance failed: errors](#persistance-failed-errors)
|
106
|
+
* [Persistance succeeded: warnings](#persistance-succeeded-warnings)
|
107
|
+
* [Using ActiveModel::Validations none the less](#using-activemodelvalidations-none-the-less)
|
108
|
+
* [Use form_helper to create and update records](#use-form_helper-to-create-and-update-records)
|
109
|
+
* [Destroy records](#destroy-records)
|
110
|
+
* [Record getters and setters](#record-getters-and-setters)
|
111
|
+
* [Record setters](#record-setters)
|
112
|
+
* [Record getters](#record-getters)
|
113
|
+
* [Include linked resources (hyperlinks and hypermedia)](#include-linked-resources-hyperlinks-and-hypermedia)
|
114
|
+
* [Ensure the whole linked collection is included: includes_all](#ensure-the-whole-linked-collection-is-included-includes_all)
|
115
|
+
* [Include the first linked page or single item is included: include](#include-the-first-linked-page-or-single-item-is-included-include)
|
116
|
+
* [Include various levels of linked data](#include-various-levels-of-linked-data)
|
117
|
+
* [Identify and cast known records when including records](#identify-and-cast-known-records-when-including-records)
|
118
|
+
* [Apply options for requests performed to fetch included records](#apply-options-for-requests-performed-to-fetch-included-records)
|
119
|
+
* [Record batch processing](#record-batch-processing)
|
120
|
+
* [all](#all)
|
121
|
+
* [Using all, when endpoint does not implement response pagination meta data](#using-all-when-endpoint-does-not-implement-response-pagination-meta-data)
|
122
|
+
* [find_each](#find_each)
|
123
|
+
* [find_in_batches](#find_in_batches)
|
124
|
+
* [Convert/Cast specific record types: becomes](#convertcast-specific-record-types-becomes)
|
125
|
+
* [Request Cycle Cache](#request-cycle-cache)
|
126
|
+
* [Change store for LHS' request cycle cache](#change-store-for-lhs-request-cycle-cache)
|
127
|
+
* [Disable request cycle cache](#disable-request-cycle-cache)
|
128
|
+
* [Testing with LHS](#testing-with-lhs)
|
129
|
+
* [Test helper for request cycle cache](#test-helper-for-request-cycle-cache)
|
130
|
+
* [Test query chains](#test-query-chains)
|
131
|
+
* [By explicitly resolving the chain: fetch](#by-explicitly-resolving-the-chain-fetch)
|
132
|
+
* [Without resolving the chain: where_values_hash](#without-resolving-the-chain-where_values_hash)
|
133
|
+
* [License](#license)
|
134
|
+
|
135
|
+
## Installation/Startup checklist
|
136
|
+
|
137
|
+
- [ ] Install LHS gem, preferably via `Gemfile`
|
138
|
+
- [ ] Configure [LHC](https://github.com/local-ch/lhc) via an `config/initializers/lhc.rb` (See: https://github.com/local-ch/lhc#configuration)
|
139
|
+
- [ ] Add `LHC::Caching` to `LHC.config.interceptors` to facilitate LHS' [Request Cycle Cache](#request-cycle-cache)
|
140
|
+
- [ ] Store all LHS::Records in `app/models` for autoload/preload reasons
|
141
|
+
- [ ] Request data from services via `LHS` from within your rails controllers
|
142
|
+
|
143
|
+
## Record
|
144
|
+
|
145
|
+
### Endpoints
|
146
|
+
|
147
|
+
> Endpoint, the entry point to a service, a process, or a queue or topic destination in service-oriented architecture
|
148
|
+
|
149
|
+
Start a record with configuring one or multiple endpoints.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
# app/models/record.rb
|
153
|
+
|
154
|
+
class Record < LHS::Record
|
38
155
|
|
39
|
-
|
156
|
+
endpoint '{+service}/records'
|
157
|
+
endpoint '{+service}/records/{id}'
|
158
|
+
endpoint '{+service}/accociation/{accociation_id}/records'
|
159
|
+
endpoint '{+service}/accociation/{accociation_id}/records/{id}'
|
40
160
|
|
41
|
-
|
161
|
+
end
|
162
|
+
```
|
42
163
|
|
43
|
-
You
|
164
|
+
You can also add request options to be used with configured endpoints:
|
44
165
|
|
45
166
|
```ruby
|
167
|
+
# app/models/record.rb
|
168
|
+
|
46
169
|
class Record < LHS::Record
|
47
170
|
|
48
|
-
endpoint '{+service}/
|
49
|
-
endpoint '{+service}/
|
50
|
-
endpoint '{+service}/v2/records', auth: { basic: 'PASSWORD' }
|
51
|
-
endpoint '{+service}/v2/records/{id}', auth: { basic: 'PASSWORD' }
|
171
|
+
endpoint '{+service}/records', auth: { bearer: -> { access_token } }
|
172
|
+
endpoint '{+service}/records/{id}', auth: { bearer: -> { access_token } }
|
52
173
|
|
53
174
|
end
|
54
175
|
```
|
55
176
|
|
56
|
-
|
177
|
+
-> Check [LHC](https://github.com/local-ch/lhc) for more information about request options
|
57
178
|
|
58
|
-
|
179
|
+
#### Configure endpoint hosts
|
59
180
|
|
60
|
-
|
181
|
+
It's common practice to use different hosts accross different environments in a service-oriented architecture.
|
182
|
+
|
183
|
+
Use [LHC placeholders](https://github.com/local-ch/lhc#configuring-placeholders) to configure different hosts per environment:
|
61
184
|
|
62
|
-
**DO**
|
63
185
|
```ruby
|
186
|
+
# config/initializers/lhc.rb
|
187
|
+
|
64
188
|
LHC.configure do |config|
|
65
|
-
config.placeholder(:
|
189
|
+
config.placeholder(:search, ENV['SEARCH'])
|
66
190
|
end
|
191
|
+
```
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
# app/models/record.rb
|
67
195
|
|
68
196
|
class Record < LHS::Record
|
69
197
|
|
70
|
-
endpoint '{+
|
198
|
+
endpoint '{+search}/api/search.json'
|
71
199
|
|
72
200
|
end
|
73
201
|
```
|
74
202
|
|
75
|
-
**DON'T
|
203
|
+
**DON'T!**
|
204
|
+
|
205
|
+
Please DO NOT mix host placeholders with and endpoint's resource path, as otherwise LHS will not work properly.
|
206
|
+
|
76
207
|
```ruby
|
208
|
+
# config/initializers/lhc.rb
|
209
|
+
|
77
210
|
LHC.configure do |config|
|
78
|
-
config.placeholder(:
|
211
|
+
config.placeholder(:search, 'http://tel.search.ch/api/search.json')
|
79
212
|
end
|
213
|
+
```
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
# app/models/record.rb
|
80
217
|
|
81
218
|
class Record < LHS::Record
|
82
219
|
|
83
|
-
endpoint '{+
|
220
|
+
endpoint '{+search}'
|
84
221
|
|
85
222
|
end
|
86
223
|
```
|
87
224
|
|
88
|
-
|
225
|
+
#### Ambiguous endpoints
|
89
226
|
|
90
|
-
If you try to setup a
|
227
|
+
If you try to setup a Record with ambiguous endpoints, LHS will immediately raise an exception:
|
91
228
|
|
92
229
|
```ruby
|
230
|
+
# app/models/record.rb
|
231
|
+
|
93
232
|
class Record < LHS::Record
|
94
233
|
|
95
|
-
endpoint '{+service}/
|
96
|
-
endpoint '{+service}/
|
234
|
+
endpoint '{+service}/records'
|
235
|
+
endpoint '{+service}/bananas'
|
97
236
|
|
98
237
|
end
|
99
|
-
|
238
|
+
|
239
|
+
# raises: Ambiguous endpoints
|
100
240
|
```
|
101
241
|
|
102
|
-
|
242
|
+
### Record inheritance
|
103
243
|
|
104
|
-
You can
|
244
|
+
You can inherit from previously defined records and also inherit endpoints that way:
|
105
245
|
|
106
246
|
```ruby
|
107
|
-
|
247
|
+
# app/models/base.rb
|
248
|
+
|
249
|
+
class Base < LHS::Record
|
250
|
+
endpoint '{+service}/records/{id}'
|
251
|
+
end
|
108
252
|
```
|
109
253
|
|
110
|
-
|
254
|
+
```ruby
|
255
|
+
# app/models/record.rb
|
256
|
+
|
257
|
+
class Record < Base
|
258
|
+
end
|
259
|
+
```
|
111
260
|
|
112
261
|
```ruby
|
113
|
-
|
262
|
+
# app/controllers/some_controller.rb
|
263
|
+
|
264
|
+
Record.find(1)
|
265
|
+
```
|
266
|
+
```
|
267
|
+
GET https://service.example.com/records/1
|
114
268
|
```
|
115
269
|
|
116
|
-
|
270
|
+
### Find multiple records
|
117
271
|
|
118
|
-
|
272
|
+
#### fetch
|
119
273
|
|
120
|
-
|
121
|
-
If you want to have LHS expand those items, use `expanded` as part of a Query-Chain:
|
274
|
+
In case you want to just fetch the records endpoint, without applying any further queries or want to handle pagination, you can simply call `fetch`:
|
122
275
|
|
123
|
-
```
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
]
|
130
|
-
}
|
131
|
-
end
|
276
|
+
```ruby
|
277
|
+
# app/controllers/some_controller.rb
|
278
|
+
|
279
|
+
records = Record.fetch
|
280
|
+
|
281
|
+
```
|
132
282
|
```
|
283
|
+
GET https://service.example.com/records
|
284
|
+
```
|
285
|
+
|
286
|
+
#### where
|
287
|
+
|
288
|
+
You can query a service for records by using `where`:
|
133
289
|
|
134
290
|
```ruby
|
135
|
-
|
291
|
+
# app/controllers/some_controller.rb
|
292
|
+
|
293
|
+
Record.where(color: 'blue')
|
294
|
+
|
295
|
+
```
|
296
|
+
```
|
297
|
+
GET https://service.example.com/records?color=blue
|
136
298
|
```
|
137
299
|
|
138
|
-
|
300
|
+
If the provided parameter – `color: 'blue'` in this case – is not part of the endpoint path, it will be added as query parameter.
|
139
301
|
|
140
302
|
```ruby
|
141
|
-
|
303
|
+
# app/controllers/some_controller.rb
|
304
|
+
|
305
|
+
Record.where(accociation_id: '12345')
|
306
|
+
|
307
|
+
```
|
142
308
|
```
|
309
|
+
GET https://service.example.com/accociation/12345/records
|
310
|
+
```
|
311
|
+
|
312
|
+
If the provided parameter – `accociation_id` in this case – is part of the endpoint path, it will be injected into the path:
|
143
313
|
|
144
|
-
|
314
|
+
#### Reuse/Dry where statements: Use scopes
|
145
315
|
|
146
|
-
|
147
|
-
That allows you to chain multiple where-queries:
|
316
|
+
In order to reuse/dry where statements organize them in scopes:
|
148
317
|
|
149
318
|
```ruby
|
319
|
+
# app/models/record.rb
|
320
|
+
|
150
321
|
class Record < LHS::Record
|
151
|
-
endpoint 'records/'
|
152
|
-
endpoint 'records/{id}'
|
153
|
-
end
|
154
322
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
323
|
+
endpoint '{+service}/records'
|
324
|
+
endpoint '{+service}/records/{id}'
|
325
|
+
|
326
|
+
scope :blue, -> { where(color: 'blue') }
|
327
|
+
scope :available, ->(state) { where(available: state) }
|
328
|
+
|
159
329
|
end
|
160
330
|
```
|
161
331
|
|
162
|
-
|
332
|
+
```ruby
|
333
|
+
# app/controllers/some_controller.rb
|
334
|
+
|
335
|
+
records = Record.blue.available(true)
|
336
|
+
```
|
337
|
+
```
|
338
|
+
GET https://service.example.com/records?color=blue&available=true
|
339
|
+
```
|
340
|
+
|
341
|
+
#### all
|
342
|
+
|
343
|
+
You can fetch all remote records by using `all`. Pagination will be performed automatically (See: [Record pagination](#record-pagination))
|
344
|
+
|
345
|
+
```ruby
|
346
|
+
# app/controllers/some_controller.rb
|
163
347
|
|
164
|
-
|
348
|
+
records = Record.all
|
165
349
|
|
166
|
-
|
167
|
-
|
350
|
+
```
|
351
|
+
```
|
352
|
+
GET https://service.example.com/records?limit=100
|
353
|
+
GET https://service.example.com/records?limit=100&offset=100
|
354
|
+
GET https://service.example.com/records?limit=100&offset=200
|
355
|
+
```
|
168
356
|
|
169
357
|
```ruby
|
170
|
-
|
358
|
+
# app/controllers/some_controller.rb
|
171
359
|
|
172
|
-
|
173
|
-
records
|
174
|
-
).to have_requested(:get, %r{records/})
|
175
|
-
.with(query: hash_including(color: 'blue', available: true))
|
176
|
-
# will fail as no http request is made (no data requested)
|
360
|
+
records.size # 300
|
177
361
|
|
178
|
-
expect(
|
179
|
-
records.where_values_hash
|
180
|
-
).to eq {color: 'red', available: true}
|
181
362
|
```
|
182
363
|
|
183
|
-
|
364
|
+
#### all with unpaginated endpoints
|
184
365
|
|
185
|
-
In
|
366
|
+
In case your record endpoints are not implementing any pagination, configure it to be `paginated: false`. Pagination will not be performed automatically in those cases:
|
186
367
|
|
187
368
|
```ruby
|
369
|
+
# app/models/record.rb
|
370
|
+
|
188
371
|
class Record < LHS::Record
|
189
|
-
|
190
|
-
endpoint 'records/{id}'
|
191
|
-
scope :blue, -> { where(color: 'blue') }
|
192
|
-
scope :available, ->(state) { where(available: state) }
|
372
|
+
configuration paginated: false
|
193
373
|
end
|
194
374
|
|
195
|
-
records = Record.blue.available(true)
|
196
|
-
The example would fetch records with the following parameters: `{color: blue, visible: true}`.
|
197
375
|
```
|
198
376
|
|
199
|
-
|
377
|
+
```ruby
|
378
|
+
# app/controllers/some_controller.rb
|
200
379
|
|
201
|
-
|
380
|
+
records = Record.all
|
202
381
|
|
203
|
-
|
382
|
+
```
|
383
|
+
```
|
384
|
+
GET https://service.example.com/records
|
385
|
+
```
|
204
386
|
|
205
|
-
|
387
|
+
#### Retrieve the amount of a collection of items: count vs. length
|
206
388
|
|
207
|
-
|
208
|
-
record = Record.where(color: 'blue')
|
209
|
-
.handle(LHC::BadRequest, ->(error){ show_error })
|
210
|
-
.handle(LHC::Unauthorized, ->(error){ authorize })
|
211
|
-
```
|
389
|
+
The different behavior of `count` and `length` is based on ActiveRecord's behavior.
|
212
390
|
|
213
|
-
|
391
|
+
`count` The total number of items available remotly via the provided endpoint/api, communicated via pagination meta data.
|
214
392
|
|
215
|
-
|
393
|
+
`length` The number of items already loaded from the endpoint/api and kept in memmory right now. In case of a paginated endpoint this can differ to what `count` returns, as it depends on how many pages have been loaded already.
|
216
394
|
|
217
|
-
|
218
|
-
|
395
|
+
### Find single records
|
396
|
+
|
397
|
+
#### find
|
398
|
+
|
399
|
+
`find` finds a unique record by unique identifier (usually `id` or `href`). If no record is found an error is raised.
|
219
400
|
|
220
401
|
```ruby
|
221
|
-
|
222
|
-
|
402
|
+
Record.find(123)
|
403
|
+
```
|
404
|
+
```
|
405
|
+
GET https://service.example.com/records/123
|
223
406
|
```
|
224
407
|
|
225
|
-
|
408
|
+
```ruby
|
409
|
+
Record.find('https://anotherservice.example.com/records/123')
|
410
|
+
```
|
411
|
+
```
|
412
|
+
GET https://anotherservice.example.com/records/123
|
413
|
+
```
|
226
414
|
|
227
|
-
|
415
|
+
`find` can also be used to find a single unique record with parameters:
|
228
416
|
|
229
417
|
```ruby
|
230
|
-
|
418
|
+
Record.find(another_identifier: 456)
|
419
|
+
```
|
420
|
+
```
|
421
|
+
GET https://service.example.com/records?another_identifier=456
|
231
422
|
```
|
232
423
|
|
233
|
-
|
234
|
-
|
235
|
-
`find` finds a unique record by unique identifier (usually id or href).
|
424
|
+
You can also fetch multiple records by `id` in parallel:
|
236
425
|
|
237
426
|
```ruby
|
238
|
-
|
239
|
-
|
427
|
+
Record.find(1, 2, 3)
|
428
|
+
```
|
429
|
+
```
|
430
|
+
# In parallel:
|
431
|
+
GET https://service.example.com/records/1
|
432
|
+
GET https://service.example.com/records/2
|
433
|
+
GET https://service.example.com/records/3
|
240
434
|
```
|
241
435
|
|
242
|
-
|
436
|
+
#### find_by
|
243
437
|
|
244
|
-
`
|
438
|
+
`find_by` finds the first record matching the specified conditions. If no record is found, `nil` is returned.
|
439
|
+
|
440
|
+
`find_by!` raises `LHC::NotFound` if nothing was found.
|
245
441
|
|
246
442
|
```ruby
|
247
|
-
|
443
|
+
Record.find_by(color: 'blue')
|
444
|
+
```
|
445
|
+
```
|
446
|
+
GET https://service.example.com/records?color=blue
|
248
447
|
```
|
249
448
|
|
250
|
-
|
449
|
+
#### first
|
251
450
|
|
252
|
-
If no record is found, `nil` is returned.
|
451
|
+
`first` is an alias for finding the first record without parameters. If no record is found, `nil` is returned.
|
253
452
|
|
254
|
-
`
|
453
|
+
`first!` raises `LHC::NotFound` if nothing was found.
|
255
454
|
|
256
455
|
```ruby
|
257
|
-
|
258
|
-
|
456
|
+
Record.first
|
457
|
+
```
|
458
|
+
```
|
459
|
+
GET https://service.example.com/records?limit=1
|
259
460
|
```
|
260
461
|
|
261
|
-
`first`
|
462
|
+
`first` can also be used with options:
|
262
463
|
|
263
464
|
```ruby
|
264
|
-
|
465
|
+
Record.first(params: { color: :blue })
|
466
|
+
```
|
467
|
+
```
|
468
|
+
GET https://service.example.com/records?color=blue&limit=1
|
265
469
|
```
|
266
470
|
|
267
|
-
|
471
|
+
#### last
|
268
472
|
|
269
|
-
`
|
473
|
+
`last` is an alias for finding the last record without parameters. If no record is found, `nil` is returned.
|
270
474
|
|
271
|
-
`
|
475
|
+
`last!` raises `LHC::NotFound` if nothing was found.
|
272
476
|
|
273
477
|
```ruby
|
274
|
-
Record.
|
478
|
+
Record.last
|
275
479
|
```
|
276
480
|
|
277
|
-
`last`
|
481
|
+
`last` can also be used with options:
|
278
482
|
|
279
483
|
```ruby
|
280
|
-
|
484
|
+
Record.last(params: { color: :blue })
|
281
485
|
```
|
282
486
|
|
283
|
-
|
487
|
+
### Work with retrieved data
|
284
488
|
|
285
|
-
|
286
|
-
|
287
|
-
`last` can also be used with options:
|
489
|
+
After fetching [single](#find-single-records) or [multiple](#find-multiple-records) records you can navigate the received data with ease:
|
288
490
|
|
289
491
|
```ruby
|
290
|
-
Record.
|
492
|
+
records = Record.where(color: 'blue')
|
493
|
+
records.length # 4
|
494
|
+
records.count # 400
|
495
|
+
record = records.first
|
496
|
+
record.type # 'Business'
|
497
|
+
record[:type] # 'Business'
|
498
|
+
record['type'] # 'Business'
|
291
499
|
```
|
292
500
|
|
293
|
-
|
501
|
+
#### Automatic detection/conversion of collections
|
502
|
+
|
503
|
+
How to configure endpoints for automatic collection detection?
|
504
|
+
|
505
|
+
LHS detects automatically if the responded data is a single business object or a set of business objects (collection).
|
506
|
+
|
507
|
+
Conventionally, when the responds contains an `items` key `{ items: [] }` it's treated as a collection, but also if the responds contains a plain raw array: `[{ href: '' }]` it's also treated as a collection.
|
508
|
+
|
509
|
+
If you need to configure the attribute of the response providing the collection, configure `items_key` as explained here: (Determine collections from the response body)[#determine-collections-from-the-response-body]
|
510
|
+
|
511
|
+
#### Map complex data for easy access
|
294
512
|
|
295
|
-
|
513
|
+
To influence how data is accessed, simply create methods inside your Record to access complex data structures:
|
296
514
|
|
297
515
|
```ruby
|
298
|
-
|
516
|
+
# app/models/record.rb
|
517
|
+
|
518
|
+
class Record < LHS::Record
|
519
|
+
|
520
|
+
endpoint '{+service}/records'
|
521
|
+
|
522
|
+
def name
|
523
|
+
dig(:addresses, :first, :business, :identities, :first, :name)
|
524
|
+
end
|
525
|
+
end
|
299
526
|
```
|
300
527
|
|
301
|
-
|
528
|
+
#### Access and identify nested records
|
529
|
+
|
530
|
+
Nested records, in nested data, are automatically casted to the correct Record class, when they provide an `href` and that `href` matches any defined endpoint of any defined Record:
|
302
531
|
|
303
532
|
```ruby
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
533
|
+
# app/models/place.rb
|
534
|
+
|
535
|
+
class Place < LHS::Record
|
536
|
+
endpoint '{+service}/places'
|
537
|
+
endpoint '{+service}/places/{id}'
|
538
|
+
|
539
|
+
def name
|
540
|
+
dig(:addresses, :first, :business, :identities, :first, :name)
|
541
|
+
end
|
542
|
+
end
|
308
543
|
```
|
309
544
|
|
310
|
-
|
545
|
+
```ruby
|
546
|
+
# app/models/favorite.rb
|
311
547
|
|
312
|
-
|
548
|
+
class Favorite < LHS::Record
|
549
|
+
endpoint '{+service}/favorites'
|
550
|
+
endpoint '{+service}/favorites/{id}'
|
551
|
+
end
|
552
|
+
```
|
313
553
|
|
314
554
|
```ruby
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
555
|
+
# app/controllers/some_controller.rb
|
556
|
+
|
557
|
+
favorite = Favorite.includes(:place).find(123)
|
558
|
+
favorite.place.name # local.ch AG
|
559
|
+
```
|
560
|
+
```
|
561
|
+
GET https://service.example.com/favorites/123
|
562
|
+
|
563
|
+
{... place: { href: 'https://service.example.com/places/456' }}
|
564
|
+
|
565
|
+
GET https://service.example.com/places/456
|
320
566
|
```
|
321
567
|
|
322
|
-
|
568
|
+
If automatic detection of nested records does not work, make sure your Records are stored in `app/models`! See: (Insallation/Startup checklist)[#installation-startup-checklist]
|
323
569
|
|
324
|
-
|
570
|
+
##### Relations / Associations
|
325
571
|
|
326
|
-
|
572
|
+
Typically nested data is automatically casted when accessed (See: [Access and identify nested records](#access-and-identify-nested-records)), but sometimes API's don't provide dedicated endpoints to retrieve these records.
|
573
|
+
In those cases, those records are only available through other records and don't have an `href` on their own and can't be casted automatically, when accessed.
|
327
574
|
|
328
|
-
|
575
|
+
To be able to implement Record-specific logic for those nested records, you can define relations/associations.
|
576
|
+
|
577
|
+
###### has_many
|
329
578
|
|
330
|
-
### Relations
|
331
579
|
```ruby
|
580
|
+
# app/models/location.rb
|
332
581
|
|
333
582
|
class Location < LHS::Record
|
334
583
|
|
335
|
-
endpoint '
|
584
|
+
endpoint '{+service}/locations/{id}'
|
336
585
|
|
337
586
|
has_many :listings
|
338
587
|
|
339
588
|
end
|
589
|
+
```
|
590
|
+
|
591
|
+
```ruby
|
592
|
+
# app/models/listing.rb
|
340
593
|
|
341
594
|
class Listing < LHS::Record
|
342
595
|
|
@@ -344,20 +597,57 @@ class Listing < LHS::Record
|
|
344
597
|
type == 'SUPPORTED'
|
345
598
|
end
|
346
599
|
end
|
600
|
+
```
|
601
|
+
|
602
|
+
```ruby
|
603
|
+
# app/controllers/some_controller.rb
|
347
604
|
|
348
605
|
Location.find(1).listings.first.supported? # true
|
606
|
+
```
|
607
|
+
```
|
608
|
+
GET https://service.example.com/locations/1
|
609
|
+
{... listings: [{ type: 'SUPPORTED' }] }
|
610
|
+
```
|
611
|
+
|
612
|
+
`class_name`: Specify the class name of the relation. Use it only if that name can't be inferred from the relation name. So has_many :photos will by default be linked to the Photo class, but if the real class name is e.g. CustomPhoto or namespaced Custom::Photo, you'll have to specify it with this option.
|
613
|
+
|
614
|
+
```ruby
|
615
|
+
# app/models/custom/location.rb
|
616
|
+
|
617
|
+
module Custom
|
618
|
+
class Location < LHS::Record
|
619
|
+
endpoint '{+service}/locations'
|
620
|
+
endpoint '{+service}/locations/{id}'
|
621
|
+
|
622
|
+
has_many :photos, class_name: 'Custom::Photo'
|
623
|
+
end
|
624
|
+
end
|
625
|
+
```
|
626
|
+
|
627
|
+
```ruby
|
628
|
+
# app/models/custom/photo.rb
|
349
629
|
|
630
|
+
module Custom
|
631
|
+
class Photo < LHS::Record
|
632
|
+
end
|
633
|
+
end
|
350
634
|
```
|
351
635
|
|
636
|
+
###### has_one
|
637
|
+
|
352
638
|
```ruby
|
639
|
+
# app/models/transaction.rb
|
353
640
|
|
354
641
|
class Transaction < LHS::Record
|
355
642
|
|
356
|
-
endpoint '
|
643
|
+
endpoint '{+service}/transaction/{id}'
|
357
644
|
|
358
645
|
has_one :user
|
359
|
-
|
360
646
|
end
|
647
|
+
```
|
648
|
+
|
649
|
+
```ruby
|
650
|
+
# app/models/user.rb
|
361
651
|
|
362
652
|
class User < LHS::Record
|
363
653
|
|
@@ -365,1019 +655,1564 @@ class User < LHS::Record
|
|
365
655
|
self[:email_address]
|
366
656
|
end
|
367
657
|
end
|
368
|
-
|
369
|
-
Transaction.find(1).user.email_address # steve@local.ch
|
370
|
-
|
371
658
|
```
|
372
659
|
|
373
|
-
|
374
|
-
|
375
|
-
In case you have to configure relations, the following relation options are available:
|
660
|
+
```ruby
|
661
|
+
# app/controllers/some_controller.rb
|
376
662
|
|
377
|
-
|
663
|
+
Transaction.find(1).user.email_address # steve@local.ch
|
664
|
+
```
|
665
|
+
```
|
666
|
+
GET https://service.example.com/transaction/1
|
667
|
+
{... user: { email_address: 'steve@local.ch' } }
|
668
|
+
```
|
378
669
|
|
379
|
-
e.g.
|
670
|
+
`class_name`: Specify the class name of the relation. Use it only if that name can't be inferred from the relation name. So has_many :photos will by default be linked to the Photo class, but if the real class name is e.g. CustomPhoto or namespaced Custom::Photo, you'll have to specify it with this option.
|
380
671
|
|
381
672
|
```ruby
|
382
|
-
|
673
|
+
# app/models/custom/location.rb
|
674
|
+
|
675
|
+
module Custom
|
383
676
|
class Location < LHS::Record
|
384
|
-
endpoint '
|
385
|
-
endpoint '
|
677
|
+
endpoint '{+service}/locations'
|
678
|
+
endpoint '{+service}/locations/{id}'
|
386
679
|
|
387
|
-
|
680
|
+
has_one :photo, class_name: 'Custom::Photo'
|
388
681
|
end
|
389
682
|
end
|
683
|
+
```
|
390
684
|
|
391
|
-
|
685
|
+
```ruby
|
686
|
+
# app/models/custom/photo.rb
|
687
|
+
|
688
|
+
module Custom
|
392
689
|
class Photo < LHS::Record
|
393
690
|
end
|
394
691
|
end
|
395
692
|
```
|
396
693
|
|
397
|
-
|
694
|
+
#### Unwrap nested items from the response body
|
695
|
+
|
696
|
+
If the actual item data is mixed with meta data in the response body, LHS allows you to configure a record in a way to automatically unwrap items from within nested response data.
|
697
|
+
|
698
|
+
`item_key` is used to unwrap the actual object from within the response body.
|
398
699
|
|
399
|
-
|
700
|
+
```ruby
|
701
|
+
# app/models/location.rb
|
702
|
+
|
703
|
+
class Location < LHS::Record
|
704
|
+
configuration item_key: [:response, :location]
|
705
|
+
end
|
706
|
+
```
|
400
707
|
|
401
708
|
```ruby
|
402
|
-
|
403
|
-
options = { auth: { bearer: '123456' } }
|
709
|
+
# app/controllers/some_controller.rb
|
404
710
|
|
405
|
-
|
711
|
+
location = Location.find(123)
|
712
|
+
location.id # 123
|
713
|
+
```
|
714
|
+
```
|
715
|
+
GET https://service.example.com/locations/123
|
716
|
+
{... response: { location: { id: 123 } } }
|
717
|
+
```
|
406
718
|
|
407
|
-
|
408
|
-
active_records = AuthenticatedRecord.where(active: true)
|
719
|
+
#### Determine collections from the response body
|
409
720
|
|
410
|
-
|
721
|
+
`items_key` key used to determine the collection of items of the current page (e.g. `docs`, `items`, etc.), defaults to 'items':
|
411
722
|
|
412
|
-
|
413
|
-
|
414
|
-
# All further requests are made from scratch and not based on the previous options
|
415
|
-
record.name = 'Walter'
|
723
|
+
```ruby
|
724
|
+
# app/models/search.rb
|
416
725
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
authenticated_record.destroy
|
421
|
-
authenticated_record.update(name: 'Steve')
|
726
|
+
class Search < LHS::Record
|
727
|
+
configuration items_key: :docs
|
728
|
+
end
|
422
729
|
```
|
423
730
|
|
424
|
-
|
425
|
-
|
426
|
-
By default, LHS does not perform the same http request during one request cycle multiple times.
|
731
|
+
```ruby
|
732
|
+
# app/controllers/some_controller.rb
|
427
733
|
|
428
|
-
|
734
|
+
search_result = Search.where(q: 'Starbucks')
|
735
|
+
search_result.first.address # Bahnhofstrasse 5, 8000 Zürich
|
736
|
+
```
|
737
|
+
```
|
738
|
+
GET https://service.example.com/search?q=Starbucks
|
739
|
+
{... docs: [... {... address: 'Bahnhofstrasse 5, 8000 Zürich' }] }
|
740
|
+
```
|
429
741
|
|
430
|
-
|
742
|
+
### Chain complex queries
|
431
743
|
|
432
|
-
|
744
|
+
> [Method chaining](https://en.wikipedia.org/wiki/Method_chaining), also known as named parameter idiom, is a common syntax for invoking multiple method calls in object-oriented programming languages. Each method returns an object, allowing the calls to be chained together without requiring variables to store the intermediate results
|
433
745
|
|
434
|
-
|
746
|
+
In order to simplify and enhance preparing complex queries for performing single or multiple requests, LHS implements query chains to find single or multiple records.
|
435
747
|
|
436
|
-
|
437
|
-
LHS.config.request_cycle_cache_enabled = false
|
438
|
-
```
|
748
|
+
LHS query chains do [lazy evaluation](https://de.wikipedia.org/wiki/Lazy_Evaluation) to only perform as many requests as needed, when the data to be retrieved is actually needed.
|
439
749
|
|
440
|
-
|
750
|
+
Any method, accessing the content of the data to be retrieved, is resolving the chain in place – like `.each`, `.first`, `.some_attribute_name`. Nevertheless, if you just want to resolve the chain in place, and nothing else, `fetch` should be the method of your choice:
|
441
751
|
|
442
752
|
```ruby
|
443
|
-
|
444
|
-
```
|
753
|
+
# app/controllers/some_controller.rb
|
445
754
|
|
446
|
-
|
447
|
-
|
448
|
-
**Be careful using methods for batch processing. They could result in a lot of HTTP requests!**
|
755
|
+
Record.where(color: 'blue').fetch
|
756
|
+
```
|
449
757
|
|
450
|
-
|
758
|
+
#### Chain where queries
|
451
759
|
|
452
760
|
```ruby
|
453
|
-
|
454
|
-
|
455
|
-
|
761
|
+
# app/controllers/some_controller.rb
|
762
|
+
|
763
|
+
records = Record.where(color: 'blue')
|
764
|
+
[...]
|
765
|
+
records.where(available: true).each do |record|
|
766
|
+
[...]
|
767
|
+
end
|
768
|
+
```
|
769
|
+
```
|
770
|
+
GET https://service.example.com/records?color=blue&available=true
|
456
771
|
```
|
457
772
|
|
458
|
-
|
773
|
+
In case you wan't to check/debug the current values for where in the chain, you can use `where_values_hash`:
|
459
774
|
|
460
775
|
```ruby
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
# All three are doing the same thing: fetching all records with the color 'blue' from the endpoint while resolving pagingation if endpoint is paginated
|
776
|
+
records.where_values_hash
|
777
|
+
|
778
|
+
# {color: 'blue', available: true}
|
465
779
|
```
|
466
780
|
|
467
|
-
|
781
|
+
#### Expand plain collections of links: expanded
|
468
782
|
|
469
|
-
|
783
|
+
Some endpoints could respond only with a plain list of links and without any expanded data, like search results.
|
470
784
|
|
471
|
-
`
|
785
|
+
Use `expanded` to have LHS expand that data, by performing necessary requests in parallel:
|
472
786
|
|
473
787
|
```ruby
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
break if record.some_attribute == some_value
|
478
|
-
end
|
788
|
+
# app/controllers/some_controller.rb
|
789
|
+
|
790
|
+
Search.where(what: 'Cafe').expanded
|
479
791
|
```
|
792
|
+
```
|
793
|
+
GET https://service.example.com/search?what=Cafe
|
794
|
+
{...
|
795
|
+
"items" : [
|
796
|
+
{"href": "https://service.example.com/records/1"},
|
797
|
+
{"href": "https://service.example.com/records/2"},
|
798
|
+
{"href": "https://service.example.com/records/3"}
|
799
|
+
]
|
800
|
+
}
|
801
|
+
|
802
|
+
In parallel:
|
803
|
+
> GET https://service.example.com/records/1
|
804
|
+
< {... name: 'Cafe Einstein'}
|
805
|
+
> GET https://service.example.com/records/2
|
806
|
+
< {... name: 'Starbucks'}
|
807
|
+
> GET https://service.example.com/records/3
|
808
|
+
< {... name: 'Plaza Cafe'}
|
809
|
+
|
810
|
+
{
|
811
|
+
...
|
812
|
+
"items" : [
|
813
|
+
{
|
814
|
+
"href": "https://service.example.com/records/1",
|
815
|
+
"name": 'Cafe Einstein',
|
816
|
+
...
|
817
|
+
},
|
818
|
+
{
|
819
|
+
"href": "https://service.example.com/records/2",
|
820
|
+
"name": 'Starbucks',
|
821
|
+
...
|
822
|
+
},
|
823
|
+
{
|
824
|
+
"href": "https://service.example.com/records/3",
|
825
|
+
"name": 'Plaza Cafe',
|
826
|
+
...
|
827
|
+
}
|
828
|
+
]
|
829
|
+
}
|
830
|
+
```
|
831
|
+
|
832
|
+
You can also apply request options to `expanded`. Those options will be used to perform the additional requests to expand the data:
|
480
833
|
|
481
|
-
`find_in_batches` is used by `find_each` and processes batches.
|
482
834
|
```ruby
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
break if records.first.name == some_value
|
487
|
-
end
|
835
|
+
# app/controllers/some_controller.rb
|
836
|
+
|
837
|
+
Search.where(what: 'Cafe').expanded(auth: { bearer: access_token })
|
488
838
|
```
|
489
839
|
|
490
|
-
|
840
|
+
#### Error handling with chains
|
841
|
+
|
842
|
+
One benefit of chains is lazy evaluation. But that also means they only get resolved when data is accessed. This makes it hard to catch errors with normal `rescue` blocks:
|
491
843
|
|
492
844
|
```ruby
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
845
|
+
# app/controllers/some_controller.rb
|
846
|
+
|
847
|
+
def show
|
848
|
+
@records = Record.where(color: blue) # returns a chain, nothing is resolved, no http requests are performed
|
849
|
+
rescue => e
|
850
|
+
# never ending up here, because the http requests are actually performed in the view, when the query chain is resolved
|
851
|
+
end
|
498
852
|
```
|
499
853
|
|
500
|
-
|
854
|
+
```ruby
|
855
|
+
# app/views/some/view.haml
|
856
|
+
|
857
|
+
= @records.each do |record| # .each resolves the query chain, leads to http requests beeing performed, which might raises an exception
|
858
|
+
= record.name
|
859
|
+
```
|
501
860
|
|
502
|
-
|
861
|
+
To simplify error handling with chains, you can also chain error handlers to be resolved, as part of the chain.
|
503
862
|
|
504
|
-
LHS
|
863
|
+
If you need to render some different view in Rails based on an LHS error raised during rendering the view, please proceed as following:
|
505
864
|
|
506
865
|
```ruby
|
507
|
-
|
508
|
-
|
866
|
+
# app/controllers/some_controller.rb
|
867
|
+
|
868
|
+
def show
|
869
|
+
@records = Record
|
870
|
+
.handle(LHC::Error, ->(error){ handle_error(error) })
|
871
|
+
.where(color: 'blue')
|
872
|
+
render 'show'
|
873
|
+
render_error if @error
|
874
|
+
end
|
875
|
+
|
876
|
+
private
|
877
|
+
|
878
|
+
def handle_error(error)
|
879
|
+
@error = error
|
880
|
+
nil
|
509
881
|
end
|
510
882
|
|
511
|
-
|
512
|
-
#
|
513
|
-
|
883
|
+
def render_error
|
884
|
+
self.response_body = nil # required to not raise AbstractController::DoubleRenderError
|
885
|
+
render 'error'
|
886
|
+
end
|
887
|
+
```
|
514
888
|
```
|
889
|
+
> GET https://service.example.com/records?color=blue
|
890
|
+
< 406
|
891
|
+
```
|
892
|
+
|
893
|
+
In case no matching error handler is found the error gets re-raised.
|
515
894
|
|
516
|
-
|
895
|
+
-> Read more about [LHC error types/classes](https://github.com/local-ch/lhc#exceptions)
|
517
896
|
|
897
|
+
If you want to inject values for the failing records, that might not have been found, you can inject values for them with error handlers:
|
898
|
+
|
899
|
+
```ruby
|
900
|
+
# app/controllers/some_controller.rb
|
901
|
+
|
902
|
+
data = Record
|
903
|
+
.handle(LHC::Unauthorized, ->(response) { Record.new(name: 'unknown') })
|
904
|
+
.find(1, 2, 3)
|
905
|
+
|
906
|
+
data[1].name # 'unknown'
|
518
907
|
```
|
519
|
-
Not allowed to set or change foreign key: content_ad_id!
|
520
908
|
```
|
909
|
+
In parallel:
|
910
|
+
> GET https://service.example.com/records/1
|
911
|
+
< 200
|
912
|
+
> GET https://service.example.com/records/2
|
913
|
+
< 400
|
914
|
+
> GET https://service.example.com/records/3
|
915
|
+
< 200
|
916
|
+
```
|
917
|
+
|
918
|
+
-> Read more about [LHC error types/classes](https://github.com/local-ch/lhc#exceptions)
|
521
919
|
|
522
|
-
|
920
|
+
**If an error handler returns `nil` an empty LHS::Record is returned, not `nil`!**
|
523
921
|
|
524
|
-
|
922
|
+
In case you want to ignore errors and continue working with `nil` in those cases,
|
923
|
+
please use `ignore`:
|
525
924
|
|
526
925
|
```ruby
|
527
|
-
|
528
|
-
endpoint '{+service}/reviews'
|
529
|
-
end
|
926
|
+
# app/controllers/some_controller.rb
|
530
927
|
|
531
|
-
|
532
|
-
|
533
|
-
|
928
|
+
record = Record.ignore(LHC::NotFound).find_by(color: 'blue')
|
929
|
+
|
930
|
+
record # nil
|
931
|
+
```
|
932
|
+
|
933
|
+
#### Resolve chains: fetch
|
934
|
+
|
935
|
+
In case you need to resolve a query chain in place, use `fetch`:
|
936
|
+
|
937
|
+
```ruby
|
938
|
+
# app/controllers/some_controller.rb
|
939
|
+
|
940
|
+
records = Record.where(color: 'blue').fetch
|
534
941
|
```
|
535
942
|
|
536
|
-
####
|
943
|
+
#### Add request options to a query chain: options
|
944
|
+
|
945
|
+
You can apply options to the request chain. Those options will be forwarded to the request perfomed by the chain/query:
|
946
|
+
|
537
947
|
```ruby
|
538
|
-
|
539
|
-
# Review#1
|
540
|
-
# :href => '{+service}/reviews/1
|
541
|
-
# :text => 'Simply awesome'
|
542
|
-
# :comment => { :href => '{+service}/reviews/1/comments }
|
948
|
+
# app/controllers/some_controller.rb
|
543
949
|
|
544
|
-
|
545
|
-
# Comment#1
|
546
|
-
# :href => '{+service}/reviews/1/comments
|
547
|
-
# :text => 'Thank you!'
|
950
|
+
options = { auth: { bearer: '123456' } } # authenticated with OAuth token
|
548
951
|
|
549
|
-
review
|
550
|
-
# Review#1
|
551
|
-
# :href => '{+service}/reviews/1
|
552
|
-
# :text => 'Simply awesome'
|
553
|
-
# :comment => { :href => '{+service}/reviews/1/comments, :text => 'Thank you!' }
|
554
952
|
```
|
555
953
|
|
556
|
-
|
954
|
+
```ruby
|
955
|
+
# app/controllers/some_controller.rb
|
956
|
+
|
957
|
+
AuthenticatedRecord = Record.options(options)
|
958
|
+
|
959
|
+
```
|
557
960
|
|
558
|
-
#### Expanded collection
|
559
961
|
```ruby
|
560
|
-
|
561
|
-
# Review#1
|
562
|
-
# :href => '{+service}/reviews/1'
|
563
|
-
# :text => 'Simply awesome'
|
564
|
-
# :comments => { :href => '{+service}/reviews/1/comments, :items => [] }
|
962
|
+
# app/controllers/some_controller.rb
|
565
963
|
|
566
|
-
|
567
|
-
# Comment#1
|
568
|
-
# :href => '{+service}/reviews/1/comments/1'
|
569
|
-
# :text => 'Thank you!'
|
964
|
+
blue_records = AuthenticatedRecord.where(color: 'blue')
|
570
965
|
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
# :text => 'Simply awesome'
|
575
|
-
# :comments => { :href => '{+service}/reviews/1/comments, :items => [{ :href => '{+service}/reviews/1/comments/1', :text => 'Thank you!' }] }
|
966
|
+
```
|
967
|
+
```
|
968
|
+
GET https://service.example.com/records?color=blue { headers: { 'Authentication': 'Bearer 123456' } }
|
576
969
|
```
|
577
970
|
|
578
|
-
#### Not expanded collection
|
579
971
|
```ruby
|
580
|
-
|
581
|
-
# Review#1
|
582
|
-
# :href => '{+service}/reviews/1'
|
583
|
-
# :text => 'Simply awesome'
|
584
|
-
# :comments => { :href => '{+service}/reviews/1/comments' }
|
972
|
+
# app/controllers/some_controller.rb
|
585
973
|
|
586
|
-
|
587
|
-
# Comment#1
|
588
|
-
# :href => '{+service}/reviews/1/comments/1'
|
589
|
-
# :text => 'Thank you!'
|
974
|
+
AuthenticatedRecord.create(color: 'red')
|
590
975
|
|
591
|
-
review
|
592
|
-
# Review#1
|
593
|
-
# :href => '{+service}/reviews/1
|
594
|
-
# :text => 'Simply awesome'
|
595
|
-
# :comments => { :href => '{+service}/reviews/1/comments', :items => [{ :href => '{+service}/reviews/1/comments/1', :text => 'Thank you!' }] }
|
596
976
|
```
|
977
|
+
```
|
978
|
+
POST https://service.example.com/records { body: '{ color: "red" }' }, headers: { 'Authentication': 'Bearer 123456' } }
|
979
|
+
```
|
980
|
+
|
981
|
+
```ruby
|
982
|
+
# app/controllers/some_controller.rb
|
597
983
|
|
598
|
-
|
984
|
+
record = AuthenticatedRecord.find(123)
|
599
985
|
|
600
|
-
|
986
|
+
```
|
987
|
+
```
|
988
|
+
GET https://service.example.com/records/123 { headers: { 'Authentication': 'Bearer 123456' } }
|
989
|
+
```
|
601
990
|
|
602
991
|
```ruby
|
603
|
-
|
604
|
-
|
992
|
+
# app/controllers/some_controller.rb
|
993
|
+
|
994
|
+
authenticated_record = record.options(options) # starting a new chain based on the found record
|
995
|
+
|
605
996
|
```
|
606
997
|
|
607
|
-
|
998
|
+
```ruby
|
999
|
+
# app/controllers/some_controller.rb
|
1000
|
+
|
1001
|
+
authenticated_record.valid?
|
608
1002
|
|
609
|
-
|
1003
|
+
```
|
1004
|
+
```
|
1005
|
+
POST https://service.example.com/records/validate { body: '{...}', headers: { 'Authentication': 'Bearer 123456' } }
|
1006
|
+
```
|
610
1007
|
|
611
|
-
|
1008
|
+
```ruby
|
1009
|
+
# app/controllers/some_controller.rb
|
612
1010
|
|
613
|
-
|
614
|
-
|
1011
|
+
authenticated_record.save
|
1012
|
+
```
|
1013
|
+
```
|
1014
|
+
POST https://service.example.com/records { body: '{...}', headers: { 'Authentication': 'Bearer 123456' } }
|
1015
|
+
```
|
615
1016
|
|
616
1017
|
```ruby
|
1018
|
+
# app/controllers/some_controller.rb
|
617
1019
|
|
618
|
-
|
619
|
-
def ratings=(values)
|
620
|
-
super(
|
621
|
-
values.map { |k, v| { name: k, value: v } }
|
622
|
-
)
|
623
|
-
end
|
624
|
-
end
|
1020
|
+
authenticated_record.destroy
|
625
1021
|
|
626
|
-
|
627
|
-
|
628
|
-
|
1022
|
+
```
|
1023
|
+
```
|
1024
|
+
DELETE https://service.example.com/records/123 { headers: { 'Authentication': 'Bearer 123456' } }
|
1025
|
+
```
|
629
1026
|
|
630
|
-
|
631
|
-
|
1027
|
+
```ruby
|
1028
|
+
# app/controllers/some_controller.rb
|
632
1029
|
|
1030
|
+
authenticated_record.update(name: 'Steve')
|
1031
|
+
|
1032
|
+
```
|
633
1033
|
```
|
1034
|
+
POST https://service.example.com/records/123 { body: '{...}', headers: { 'Authentication': 'Bearer 123456' } }
|
1035
|
+
```
|
1036
|
+
|
1037
|
+
#### Control pagination within a query chain
|
1038
|
+
|
1039
|
+
`page` sets the page that you want to request.
|
1040
|
+
|
1041
|
+
`per` sets the amount of items requested per page.
|
634
1042
|
|
635
|
-
|
1043
|
+
`limit` is an alias for `per`. **But without providing arguments, it resolves the query and provides the current response limit per page**
|
636
1044
|
|
637
1045
|
```ruby
|
638
|
-
|
639
|
-
def ratings=(values)
|
640
|
-
super(
|
641
|
-
values.map { |k, v| { name: k, value: v } }
|
642
|
-
)
|
643
|
-
end
|
1046
|
+
# app/controllers/some_controller.rb
|
644
1047
|
|
645
|
-
|
646
|
-
super.map { |r| [r[:name], r[:value]] }]
|
647
|
-
end
|
648
|
-
end
|
1048
|
+
Record.page(3).per(20).where(color: 'blue')
|
649
1049
|
|
650
|
-
|
651
|
-
|
652
|
-
|
1050
|
+
```
|
1051
|
+
```
|
1052
|
+
GET https://service.example.com/records?offset=40&limit=20&color=blue
|
1053
|
+
```
|
653
1054
|
|
654
|
-
|
655
|
-
|
1055
|
+
```ruby
|
1056
|
+
# app/controllers/some_controller.rb
|
1057
|
+
|
1058
|
+
Record.page(3).per(20).where(color: 'blue')
|
656
1059
|
|
1060
|
+
```
|
1061
|
+
```
|
1062
|
+
GET https://service.example.com/records?offset=40&limit=20&color=blue
|
657
1063
|
```
|
658
1064
|
|
659
|
-
|
1065
|
+
The applied pagination strategy depends on whats configured for the particular record: See [Record pagination](#record-pagination)
|
660
1066
|
|
661
|
-
|
1067
|
+
### Record pagination
|
662
1068
|
|
663
|
-
|
1069
|
+
You can configure pagination on a per record base.
|
1070
|
+
LHS differentiates between the [pagination strategy](#pagination-strategy) (how items/pages are navigated and calculated) and [pagination keys](#pagination-keys) (how stuff is named and accessed).
|
664
1071
|
|
665
|
-
|
1072
|
+
#### Pagination strategy
|
666
1073
|
|
667
|
-
|
1074
|
+
##### Pagination strategy: offset (default)
|
668
1075
|
|
669
|
-
|
1076
|
+
The offset pagination strategy is LHS's default pagination strategy, so nothing needs to be (re-)configured.
|
1077
|
+
|
1078
|
+
The `offset` pagination strategy starts with 0 and offsets by the amount of items, thay you've already recived – typically `limit`.
|
670
1079
|
|
671
1080
|
```ruby
|
672
|
-
|
1081
|
+
# app/models/record.rb
|
673
1082
|
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
1083
|
+
class Search < LHS::Record
|
1084
|
+
endpoint '{+service}/search'
|
1085
|
+
end
|
1086
|
+
```
|
1087
|
+
|
1088
|
+
```ruby
|
1089
|
+
# app/controllers/some_controller.rb
|
1090
|
+
|
1091
|
+
Record.all
|
680
1092
|
|
681
|
-
customer.contracts.length # 33
|
682
|
-
customer.contracts.first.products.length # 22
|
683
1093
|
```
|
1094
|
+
```
|
1095
|
+
GET https://service.example.com/records?limit=100
|
1096
|
+
{
|
1097
|
+
items: [{...}, ...]
|
1098
|
+
total: 300,
|
1099
|
+
limit: 100,
|
1100
|
+
offset: 0
|
1101
|
+
}
|
1102
|
+
In parallel:
|
1103
|
+
GET https://service.example.com/records?limit=100&offset=100
|
1104
|
+
GET https://service.example.com/records?limit=100&offset=200
|
1105
|
+
```
|
1106
|
+
|
1107
|
+
##### Pagination strategy: page
|
684
1108
|
|
685
|
-
|
1109
|
+
In comparison to the `offset` strategy, the `page` strategy just increases by 1 (page) and sends the next batch of items for the next page.
|
686
1110
|
|
687
1111
|
```ruby
|
688
|
-
|
689
|
-
|
690
|
-
|
1112
|
+
# app/models/record.rb
|
1113
|
+
|
1114
|
+
class Search < LHS::Record
|
1115
|
+
configuration pagination_strategy: 'page', pagination_key: 'page'
|
1116
|
+
|
1117
|
+
endpoint '{+service}/search'
|
1118
|
+
end
|
691
1119
|
```
|
692
1120
|
|
693
|
-
|
694
|
-
|
1121
|
+
```ruby
|
1122
|
+
# app/controllers/some_controller.rb
|
1123
|
+
|
1124
|
+
Record.all
|
1125
|
+
|
1126
|
+
```
|
1127
|
+
```
|
1128
|
+
GET https://service.example.com/records?limit=100
|
695
1129
|
{
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
"localch_account" : {
|
701
|
-
"href" : "http://datastore/v2/localch-accounts/6bSss0y93lK0MrVsgdNNdg"
|
702
|
-
}
|
703
|
-
}
|
704
|
-
]
|
1130
|
+
items: [{...}, ...]
|
1131
|
+
total: 300,
|
1132
|
+
limit: 100,
|
1133
|
+
page: 1
|
705
1134
|
}
|
1135
|
+
In parallel:
|
1136
|
+
GET https://service.example.com/records?limit=100&page=2
|
1137
|
+
GET https://service.example.com/records?limit=100&page=3
|
706
1138
|
```
|
707
1139
|
|
708
|
-
|
709
|
-
|
1140
|
+
##### Pagination strategy: start
|
1141
|
+
|
1142
|
+
In comparison to the `offset` strategy, the `start` strategy indicates with which item the current page starts.
|
1143
|
+
Typically it starts with 1 and if you get 100 items per page, the next start is 101.
|
1144
|
+
|
1145
|
+
```ruby
|
1146
|
+
# app/models/record.rb
|
1147
|
+
|
1148
|
+
class Search < LHS::Record
|
1149
|
+
configuration pagination_strategy: 'start', pagination_key: 'startAt'
|
1150
|
+
|
1151
|
+
endpoint '{+service}/search'
|
1152
|
+
end
|
1153
|
+
```
|
1154
|
+
|
1155
|
+
```ruby
|
1156
|
+
# app/controllers/some_controller.rb
|
1157
|
+
|
1158
|
+
Record.all
|
1159
|
+
|
1160
|
+
```
|
1161
|
+
```
|
1162
|
+
GET https://service.example.com/records?limit=100
|
710
1163
|
{
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
"localch_account" : {
|
716
|
-
"href" : "http://datastore/v2/localch-accounts/6bSss0y93lK0MrVsgdNNdg",
|
717
|
-
"id" : "6bSss0y93lK0MrVsgdNNdg",
|
718
|
-
"name" : "Myriam",
|
719
|
-
"phone" : "12345678",
|
720
|
-
"email" : "email@gmail.com"
|
721
|
-
}
|
722
|
-
}
|
723
|
-
]
|
1164
|
+
items: [{...}, ...]
|
1165
|
+
total: 300,
|
1166
|
+
limit: 100,
|
1167
|
+
page: 1
|
724
1168
|
}
|
1169
|
+
In parallel:
|
1170
|
+
GET https://service.example.com/records?limit=100&startAt=101
|
1171
|
+
GET https://service.example.com/records?limit=100&startAt=201
|
725
1172
|
```
|
726
1173
|
|
727
|
-
|
1174
|
+
#### Pagination keys
|
1175
|
+
|
1176
|
+
##### limit_key
|
1177
|
+
|
1178
|
+
`limit_key` sets the key used to indicate how many items you want to retrieve per page e.g. `size`, `limit`, etc.
|
1179
|
+
In case the `limit_key` parameter differs for how it needs to be requested from how it's provided in the reponse, use `body` and `parameter` subkeys.
|
728
1180
|
|
729
1181
|
```ruby
|
730
|
-
|
731
|
-
|
732
|
-
|
1182
|
+
# app/models/record.rb
|
1183
|
+
|
1184
|
+
class Record < LHS::Record
|
1185
|
+
configuration limit_key: { body: [:pagination, :max], parameter: :max }
|
1186
|
+
|
1187
|
+
endpoint '{+service}/records'
|
1188
|
+
end
|
733
1189
|
```
|
734
1190
|
|
735
|
-
|
1191
|
+
```ruby
|
1192
|
+
# app/controllers/some_controller.rb
|
1193
|
+
|
1194
|
+
records = Record.where(color: 'blue')
|
1195
|
+
records.limit # 20
|
1196
|
+
```
|
1197
|
+
```
|
1198
|
+
GET https://service.example.com/records?color=blue&max=100
|
1199
|
+
{ ...
|
1200
|
+
items: [...],
|
1201
|
+
pagination: { max: 20 }
|
1202
|
+
}
|
1203
|
+
```
|
1204
|
+
|
1205
|
+
##### pagination_key
|
1206
|
+
|
1207
|
+
`pagination_key` defines which key to use to paginate a page (e.g. `offset`, `page`, `startAt` etc.).
|
1208
|
+
In case the `limit_key` parameter differs for how it needs to be requested from how it's provided in the reponse, use `body` and `parameter` subkeys.
|
736
1209
|
|
737
1210
|
```ruby
|
738
|
-
|
739
|
-
claims = Claims.includes(:localch_account, :entry).where(place_id: 'huU90mB_6vAfUdVz_uDoyA')
|
1211
|
+
# app/models/record.rb
|
740
1212
|
|
741
|
-
|
742
|
-
|
1213
|
+
class Record < LHS::Record
|
1214
|
+
configuration pagination_key: { body: [:pagination, :page], parameter: :page }, pagination_strategy: :page
|
743
1215
|
|
744
|
-
|
745
|
-
|
1216
|
+
endpoint '{+service}/records'
|
1217
|
+
end
|
746
1218
|
```
|
747
1219
|
|
748
|
-
|
1220
|
+
```ruby
|
1221
|
+
# app/controllers/some_controller.rb
|
749
1222
|
|
750
|
-
|
751
|
-
|
752
|
-
|
1223
|
+
records = Record.where(color: 'blue').all
|
1224
|
+
records.length # 300
|
1225
|
+
```
|
1226
|
+
```
|
1227
|
+
GET https://service.example.com/records?color=blue&limit=100
|
1228
|
+
{... pagination: { page: 1 } }
|
1229
|
+
In parallel:
|
1230
|
+
GET https://service.example.com/records?color=blue&limit=100&page=2
|
1231
|
+
{... pagination: { page: 2 } }
|
1232
|
+
GET https://service.example.com/records?color=blue&limit=100&page=3
|
1233
|
+
{... pagination: { page: 3 } }
|
1234
|
+
```
|
1235
|
+
|
1236
|
+
##### total_key
|
753
1237
|
|
754
|
-
|
1238
|
+
`total_key` defines which key to user for pagination to describe the total amount of remote items (e.g. `total`, `totalResults`, etc.).
|
755
1239
|
|
756
1240
|
```ruby
|
757
|
-
|
1241
|
+
# app/models/record.rb
|
758
1242
|
|
759
|
-
|
760
|
-
|
1243
|
+
class Record < LHS::Record
|
1244
|
+
configuration total_key: [:pagination, :total]
|
761
1245
|
|
1246
|
+
endpoint '{+service}/records'
|
762
1247
|
end
|
1248
|
+
```
|
763
1249
|
|
764
|
-
|
1250
|
+
```ruby
|
1251
|
+
# app/controllers/some_controller.rb
|
765
1252
|
|
766
|
-
|
767
|
-
|
1253
|
+
records = Record.where(color: 'blue').fetch
|
1254
|
+
records.length # 100
|
1255
|
+
records.count # 300
|
1256
|
+
```
|
1257
|
+
```
|
1258
|
+
GET https://service.example.com/records?color=blue&limit=100
|
1259
|
+
{... pagination: { total: 300 } }
|
1260
|
+
```
|
768
1261
|
|
769
|
-
|
1262
|
+
#### Pagination links
|
770
1263
|
|
771
|
-
|
772
|
-
|
1264
|
+
##### next?
|
1265
|
+
|
1266
|
+
`next?` Tells you if there is a next link or not.
|
1267
|
+
|
1268
|
+
```ruby
|
1269
|
+
# app/controllers/some_controller.rb
|
1270
|
+
|
1271
|
+
@records = Record.where(color: 'blue').fetch
|
1272
|
+
```
|
773
1273
|
```
|
1274
|
+
GET https://service.example.com/records?color=blue&limit=100
|
1275
|
+
{... items: [...], next: 'https://service.example.com/records?color=blue&limit=100&offset=100' }
|
1276
|
+
```
|
1277
|
+
|
1278
|
+
```ruby
|
1279
|
+
# app/views/some_view.haml
|
774
1280
|
|
775
|
-
|
1281
|
+
- if @records.next?
|
1282
|
+
= render partial: 'next_arrow'
|
1283
|
+
```
|
776
1284
|
|
777
|
-
|
1285
|
+
##### previous?
|
778
1286
|
|
1287
|
+
`previous?` Tells you if there is a previous link or not.
|
1288
|
+
|
1289
|
+
```ruby
|
1290
|
+
# app/controllers/some_controller.rb
|
1291
|
+
|
1292
|
+
@records = Record.where(color: 'blue').fetch
|
1293
|
+
```
|
1294
|
+
```
|
1295
|
+
GET https://service.example.com/records?color=blue&limit=100
|
1296
|
+
{... items: [...], previous: 'https://service.example.com/records?color=blue&limit=100&offset=100' }
|
779
1297
|
```
|
780
1298
|
|
781
|
-
|
1299
|
+
```ruby
|
1300
|
+
# app/views/some_view.haml
|
782
1301
|
|
1302
|
+
- if @records.previous?
|
1303
|
+
= render partial: 'previous_arrow'
|
783
1304
|
```
|
784
1305
|
|
785
|
-
|
1306
|
+
#### Kaminari support (limited)
|
786
1307
|
|
787
|
-
|
1308
|
+
LHS implements an interface that makes it partially working with Kaminari.
|
1309
|
+
|
1310
|
+
The kaminari’s page parameter is in params[:page]. For example, you can use kaminari to render paginations based on LHS Records. Typically, your code will look like this:
|
788
1311
|
|
789
1312
|
```ruby
|
790
|
-
|
791
|
-
|
1313
|
+
# controller
|
1314
|
+
@items = Record.page(params[:page]).per(100)
|
1315
|
+
```
|
792
1316
|
|
793
|
-
|
794
|
-
|
795
|
-
|
1317
|
+
```ruby
|
1318
|
+
# view
|
1319
|
+
= paginate @items
|
1320
|
+
```
|
796
1321
|
|
797
|
-
|
1322
|
+
### Build, create and update records
|
1323
|
+
|
1324
|
+
#### Create new records
|
1325
|
+
|
1326
|
+
##### create
|
1327
|
+
|
1328
|
+
`create` will return false if persisting fails. `create!` instead will an raise exception.
|
1329
|
+
|
1330
|
+
`create` always builds the data of the local object first, before it tries to sync with an endpoint. So even if persisting fails, the local object is build.
|
1331
|
+
|
1332
|
+
```ruby
|
1333
|
+
# app/controllers/some_controller.rb
|
1334
|
+
|
1335
|
+
record = Record.create(
|
1336
|
+
text: 'Hello world'
|
1337
|
+
)
|
1338
|
+
|
1339
|
+
```
|
798
1340
|
```
|
1341
|
+
POST https://service.example.com/records { body: "{ 'text' : 'Hello world' }" }
|
1342
|
+
```
|
1343
|
+
|
1344
|
+
-> See [record validation](#record-validation) for how to handle validation errors when creating records.
|
799
1345
|
|
800
|
-
|
1346
|
+
###### Unwrap nested data when creation response nests created record data
|
801
1347
|
|
802
|
-
|
1348
|
+
`item_created_key` key used to merge record data thats nested in the creation response body:
|
803
1349
|
|
804
1350
|
```ruby
|
805
|
-
|
806
|
-
|
1351
|
+
# app/models/location.rb
|
1352
|
+
|
1353
|
+
class Location < LHS::Record
|
1354
|
+
|
1355
|
+
configuration item_created_key: [:response, :location]
|
807
1356
|
|
808
|
-
def name
|
809
|
-
addresses.first.business.identities.first.name
|
810
|
-
end
|
811
1357
|
end
|
1358
|
+
```
|
812
1359
|
|
813
|
-
|
814
|
-
|
1360
|
+
```ruby
|
1361
|
+
# app/controllers/some_controller.rb
|
1362
|
+
|
1363
|
+
location.create(lat: '47.3920152', long: '8.5127981')
|
1364
|
+
location.address # Förrlibuckstrasse 62, 8005 Zürich
|
1365
|
+
```
|
1366
|
+
```
|
1367
|
+
POST https://service.example.com/locations { body: "{ 'lat': '47.3920152', long: '8.5127981' }" }
|
1368
|
+
{... { response: { location: {... address: 'Förrlibuckstrasse 62, 8005 Zürich' } } } }
|
1369
|
+
```
|
1370
|
+
|
1371
|
+
###### Create records through associations: Nested sub resources
|
1372
|
+
|
1373
|
+
```ruby
|
1374
|
+
# app/models/restaurant.rb
|
1375
|
+
|
1376
|
+
class Restaurant < LHS::Record
|
1377
|
+
endpoint '{+service}/restaurants/{id}'
|
815
1378
|
end
|
816
1379
|
|
817
|
-
favorite = Favorite.includes(:place).find(1)
|
818
|
-
favorite.place.name # local.ch AG
|
819
1380
|
```
|
820
1381
|
|
821
|
-
|
1382
|
+
```ruby
|
1383
|
+
# app/models/feedback.rb
|
822
1384
|
|
823
|
-
|
1385
|
+
class Feedback < LHS::Record
|
1386
|
+
endpoint '{+service}/restaurants/{restaurant_id}/feedbacks'
|
1387
|
+
end
|
824
1388
|
|
825
|
-
|
1389
|
+
```
|
826
1390
|
|
827
1391
|
```ruby
|
828
|
-
|
829
|
-
|
1392
|
+
# app/controllers/some_controller.rb
|
1393
|
+
|
1394
|
+
restaurant = Restaurant.find(1)
|
1395
|
+
```
|
1396
|
+
```
|
1397
|
+
GET https://service.example.com/restaurants/1
|
1398
|
+
{... reviews: { href: 'https://service.example.com/restaurants/1/reviews' }}
|
1399
|
+
```
|
1400
|
+
|
1401
|
+
```ruby
|
1402
|
+
# app/controllers/some_controller.rb
|
1403
|
+
|
1404
|
+
restaurant.reviews.create(
|
1405
|
+
text: 'Simply awesome!'
|
1406
|
+
)
|
1407
|
+
```
|
1408
|
+
```
|
1409
|
+
POST https://service.example.com/restaurants/1/reviews { body: "{ 'text': 'Simply awesome!' }" }
|
830
1410
|
```
|
831
1411
|
|
832
|
-
|
1412
|
+
#### Start building new records
|
833
1413
|
|
834
|
-
|
1414
|
+
With `new` or `build` you can start building new records from scratch, which can be persisted with `save`:
|
835
1415
|
|
836
1416
|
```ruby
|
837
|
-
|
838
|
-
|
839
|
-
|
1417
|
+
# app/controllers/some_controller.rb
|
1418
|
+
|
1419
|
+
record = Record.new # or Record.build
|
1420
|
+
record.name = 'Starbucks'
|
1421
|
+
record.save
|
1422
|
+
```
|
1423
|
+
```
|
1424
|
+
POST https://service.example.com/records { body: "{ 'name' : 'Starbucks' }" }
|
840
1425
|
```
|
841
1426
|
|
842
|
-
|
1427
|
+
#### Change/Update existing records
|
843
1428
|
|
844
|
-
|
1429
|
+
##### save
|
845
1430
|
|
846
|
-
`
|
1431
|
+
`save` persist the whole object in it's current state.
|
1432
|
+
|
1433
|
+
`save` will return `false` if persisting fails. `save!` instead will raise an exception.
|
847
1434
|
|
848
1435
|
```ruby
|
1436
|
+
# app/controllers/some_controller.rb
|
1437
|
+
|
849
1438
|
record = Record.find('1z-5r1fkaj')
|
850
|
-
|
1439
|
+
|
1440
|
+
```
|
1441
|
+
```
|
1442
|
+
GET https://service.example.com/records/1z-5r1fkaj
|
1443
|
+
{ name: 'Starbucks', recommended: null }
|
1444
|
+
```
|
1445
|
+
|
1446
|
+
```ruby
|
1447
|
+
# app/controllers/some_controller.rb
|
1448
|
+
|
1449
|
+
record.recommended = true
|
1450
|
+
record.save
|
1451
|
+
|
1452
|
+
```
|
851
1453
|
```
|
1454
|
+
POST https://service.example.com/records/1z-5r1fkaj { body: "{ 'name': 'Starbucks', 'recommended': true }" }
|
1455
|
+
```
|
1456
|
+
|
1457
|
+
-> See [record validation](#record-validation) for how to handle validation errors when updating records.
|
852
1458
|
|
853
|
-
|
1459
|
+
##### update
|
854
1460
|
|
855
|
-
|
1461
|
+
`update` persists the whole object after new parameters are applied through arguments.
|
1462
|
+
|
1463
|
+
`update` will return false if persisting fails. `update!` instead will an raise exception.
|
1464
|
+
|
1465
|
+
`update` always updates the data of the local object first, before it tries to sync with an endpoint. So even if persisting fails, the local object is updated.
|
856
1466
|
|
857
1467
|
```ruby
|
858
|
-
|
859
|
-
endpoint '{+customers}/{id}'
|
860
|
-
end
|
1468
|
+
# app/controllers/some_controller.rb
|
861
1469
|
|
862
|
-
|
863
|
-
# GET http://customers/123
|
864
|
-
# { id: '123', name: 'My old company name' }
|
1470
|
+
record = Record.find('1z-5r1fkaj')
|
865
1471
|
|
866
|
-
|
867
|
-
|
868
|
-
|
1472
|
+
```
|
1473
|
+
```
|
1474
|
+
GET https://service.example.com/records/1z-5r1fkaj
|
1475
|
+
{ name: 'Starbucks', recommended: null }
|
869
1476
|
```
|
870
1477
|
|
871
|
-
|
1478
|
+
```ruby
|
1479
|
+
# app/controllers/some_controller.rb
|
1480
|
+
|
1481
|
+
record.update(recommended: true)
|
872
1482
|
|
873
1483
|
```
|
874
|
-
|
1484
|
+
```
|
1485
|
+
POST https://service.example.com/records/1z-5r1fkaj { body: "{ 'name': 'Starbucks', 'recommended': true }" }
|
875
1486
|
```
|
876
1487
|
|
877
|
-
|
1488
|
+
-> See [record validation](#record-validation) for how to handle validation errors when updating records.
|
878
1489
|
|
879
|
-
|
1490
|
+
##### partial_update
|
880
1491
|
|
881
|
-
|
1492
|
+
`partial_update` updates just the provided parameters.
|
882
1493
|
|
883
1494
|
`partial_update` will return false if persisting fails. `partial_update!` instead will an raise exception.
|
884
1495
|
|
885
1496
|
`partial_update` always updates the data of the local object first, before it tries to sync with an endpoint. So even if persisting fails, the local object is updated.
|
886
1497
|
|
887
1498
|
```ruby
|
1499
|
+
# app/controllers/some_controller.rb
|
1500
|
+
|
888
1501
|
record = Record.find('1z-5r1fkaj')
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
1502
|
+
|
1503
|
+
```
|
1504
|
+
```
|
1505
|
+
GET https://service.example.com/records/1z-5r1fkaj
|
1506
|
+
{ name: 'Starbucks', recommended: null }
|
1507
|
+
```
|
1508
|
+
|
1509
|
+
```ruby
|
1510
|
+
# app/controllers/some_controller.rb
|
1511
|
+
|
1512
|
+
record.partial_update(recommended: true)
|
1513
|
+
|
1514
|
+
```
|
1515
|
+
```
|
1516
|
+
POST https://service.example.com/records/1z-5r1fkaj { body: "{ 'name': 'Starbucks', 'recommended': true }" }
|
1517
|
+
```
|
1518
|
+
|
1519
|
+
-> See [record validation](#record-validation) for how to handle validation errors when updating records.
|
1520
|
+
|
1521
|
+
#### Endpoint url parameter injection during record creation/change
|
1522
|
+
|
1523
|
+
LHS injects parameters provided to `create`, `update`, `partial_update`, `save` etc. into an endpoint's URL when matching:
|
1524
|
+
|
1525
|
+
```ruby
|
1526
|
+
# app/models/feedback.rb
|
1527
|
+
|
1528
|
+
class Feedback << LHS::Record
|
1529
|
+
endpoint '{+service}/records/{record_id}/feedbacks'
|
1530
|
+
end
|
1531
|
+
```
|
1532
|
+
|
1533
|
+
```ruby
|
1534
|
+
# app/controllers/some_controller.rb
|
1535
|
+
|
1536
|
+
Feedback.create(record_id: 51232, text: 'Great Restaurant!')
|
1537
|
+
```
|
1538
|
+
```
|
1539
|
+
POST https://service.example.com/records/51232/feedbacks { body: "{ 'text' : 'Great Restaurant!' }" }
|
1540
|
+
```
|
1541
|
+
|
1542
|
+
#### Record validation
|
1543
|
+
|
1544
|
+
In order to validate records before persisting them, you can use the `valid?` (`validate` alias) method.
|
1545
|
+
|
1546
|
+
It's **not recommended** to validate records anywhere, including application side validation via `ActiveModel::Validations`, except, if you validate them via the same endpoint/service, that also creates them.
|
1547
|
+
|
1548
|
+
The specific endpoint has to support validations without persistence. An endpoint has to be enabled (opt-in) in your record configurations:
|
1549
|
+
|
1550
|
+
```ruby
|
1551
|
+
# app/models/user.rb
|
1552
|
+
|
1553
|
+
class User < LHS::Record
|
1554
|
+
|
1555
|
+
endpoint '{+service}/users', validates: { params: { persist: false } }
|
1556
|
+
|
1557
|
+
end
|
1558
|
+
```
|
1559
|
+
|
1560
|
+
```ruby
|
1561
|
+
# app/controllers/some_controller.rb
|
1562
|
+
|
1563
|
+
user = User.build(email: 'i\'m not an email address')
|
1564
|
+
|
1565
|
+
unless user.valid?
|
1566
|
+
@errors = user.errors
|
1567
|
+
render 'new' and return
|
1568
|
+
end
|
1569
|
+
```
|
1570
|
+
```
|
1571
|
+
POST https://service.example.com/users?persist=false { body: '{ "email" : "i'm not an email address"}' }
|
1572
|
+
{
|
1573
|
+
"field_errors": [{
|
1574
|
+
"path": ["email"],
|
1575
|
+
"code": "WRONG_FORMAT",
|
1576
|
+
"message": "The property value's format is incorrect."
|
1577
|
+
}],
|
1578
|
+
"message": "Email must have the correct format."
|
893
1579
|
}
|
894
1580
|
```
|
895
1581
|
|
896
|
-
|
1582
|
+
The functionalities of `LHS::Errors` pretty much follow those of `ActiveModel::Validation`:
|
897
1583
|
|
898
|
-
|
899
|
-
|
1584
|
+
```ruby
|
1585
|
+
# app/views/some_view.haml
|
900
1586
|
|
901
|
-
|
1587
|
+
@errors.any? # true
|
1588
|
+
@errors.include?(:email) # true
|
1589
|
+
@errors[:email] # ['WRONG_FORMAT']
|
1590
|
+
@errors.messages # {:email=>["Translated error message that this value has the wrong format"]}
|
1591
|
+
@errors.codes # {:email=>["WRONG_FORMAT"]}
|
1592
|
+
@errors.message # Email must have the correct format."
|
1593
|
+
```
|
1594
|
+
|
1595
|
+
##### Configure record validations
|
1596
|
+
|
1597
|
+
The parameters passed to the `validates` endpoint option are used to perform record validations:
|
902
1598
|
|
903
1599
|
```ruby
|
904
|
-
|
905
|
-
|
906
|
-
|
1600
|
+
# app/models/user.rb
|
1601
|
+
|
1602
|
+
class User < LHS::Record
|
1603
|
+
|
1604
|
+
endpoint '{+service}/users', validates: { params: { persist: false } } # will add ?persist=false to the request
|
1605
|
+
endpoint '{+service}/users', validates: { params: { publish: false } } # will add ?publish=false to the request
|
1606
|
+
endpoint '{+service}/users', validates: { params: { validates: true } } # will add ?validates=true to the request
|
1607
|
+
endpoint '{+service}/users', validates: { path: 'validate' } # will perform a validation via ...users/validate
|
1608
|
+
|
907
1609
|
end
|
1610
|
+
```
|
908
1611
|
|
909
|
-
|
910
|
-
|
1612
|
+
##### HTTP Status Codes for validation errors
|
1613
|
+
|
1614
|
+
The HTTP status code received from the endpoint when performing validations on a record, is available through the errors object:
|
1615
|
+
|
1616
|
+
```ruby
|
1617
|
+
# app/controllers/some_controller.rb
|
1618
|
+
|
1619
|
+
record.save
|
1620
|
+
record.errors.status_code # 400
|
1621
|
+
```
|
1622
|
+
|
1623
|
+
##### Reset validation errors
|
1624
|
+
|
1625
|
+
Clear the error messages like:
|
1626
|
+
|
1627
|
+
```ruby
|
1628
|
+
# app/controllers/some_controller.rb
|
1629
|
+
|
1630
|
+
record.errors.clear
|
1631
|
+
```
|
1632
|
+
|
1633
|
+
##### Add validation errors
|
1634
|
+
|
1635
|
+
In case you want to add application side validation errors, even though it's not recommended, do it as following:
|
1636
|
+
|
1637
|
+
```ruby
|
1638
|
+
user.errors.add(:name, 'WRONG_FORMAT')
|
1639
|
+
```
|
1640
|
+
|
1641
|
+
##### Validation errors for nested data
|
1642
|
+
|
1643
|
+
If you work with complex data structures, you sometimes need to have validation errors delegated/scoped to nested data.
|
1644
|
+
|
1645
|
+
This features makes `LHS::Record`s compatible with how Rails or Simpleform renders/builds forms and especially error messages:
|
1646
|
+
|
1647
|
+
```ruby
|
1648
|
+
# app/controllers/some_controller.rb
|
1649
|
+
|
1650
|
+
unless @customer.save
|
1651
|
+
@errors = @customer.errors
|
911
1652
|
end
|
1653
|
+
```
|
1654
|
+
```
|
1655
|
+
POST https://service.example.com/customers { body: "{ 'address' : { 'street': 'invalid', housenumber: '' } }" }
|
1656
|
+
{
|
1657
|
+
"field_errors": [{
|
1658
|
+
"path": ["address", "street"],
|
1659
|
+
"code": "REQUIRED_PROPERTY_VALUE_INCORRECT",
|
1660
|
+
"message": "The property value is incorrect."
|
1661
|
+
},{
|
1662
|
+
"path": ["address", "housenumber"],
|
1663
|
+
"code": "REQUIRED_PROPERTY_VALUE",
|
1664
|
+
"message": "The property value is required."
|
1665
|
+
}],
|
1666
|
+
"message": "Some data is invalid."
|
1667
|
+
}
|
1668
|
+
```
|
912
1669
|
|
913
|
-
|
914
|
-
|
915
|
-
|
1670
|
+
```ruby
|
1671
|
+
# app/views/some_view.haml
|
1672
|
+
|
1673
|
+
= form_for @customer, as: :customer do |customer_form|
|
1674
|
+
|
1675
|
+
= fields_for 'customer[:address]', @customer.address, do |address_form|
|
1676
|
+
|
1677
|
+
= fields_for 'customer[:address][:street]', @customer.address.street, do |street_form|
|
1678
|
+
|
1679
|
+
= street_form.input :name
|
1680
|
+
= street_form.input :house_number
|
1681
|
+
```
|
1682
|
+
|
1683
|
+
This would render nested forms and would also render nested form errors for nested data structures.
|
1684
|
+
|
1685
|
+
You can also access those nested errors like:
|
1686
|
+
|
1687
|
+
```ruby
|
1688
|
+
@customer.address.errors
|
1689
|
+
@customer.address.street.errors
|
1690
|
+
```
|
1691
|
+
|
1692
|
+
##### Translation of validation errors
|
1693
|
+
|
1694
|
+
If a translation exists for one of the following translation keys, LHS will provide a translated error (also in the following order) rather than the plain error message/code, when building forms or accessing `@errors.messages`:
|
1695
|
+
|
1696
|
+
```ruby
|
1697
|
+
lhs.errors.records.<record_name>.attributes.<attribute_name>.<error_code>
|
1698
|
+
e.g. lhs.errors.records.customer.attributes.name.unsupported_property_value
|
1699
|
+
|
1700
|
+
lhs.errors.records.<record_name>.<error_code>
|
1701
|
+
e.g. lhs.errors.records.customer.unsupported_property_value
|
1702
|
+
|
1703
|
+
lhs.errors.messages.<error_code>
|
1704
|
+
e.g. lhs.errors.messages.unsupported_property_value
|
1705
|
+
|
1706
|
+
lhs.errors.attributes.<attribute_name>.<error_code>
|
1707
|
+
e.g. lhs.errors.attributes.name.unsupported_property_value
|
1708
|
+
|
1709
|
+
lhs.errors.fallback_message
|
1710
|
+
```
|
1711
|
+
|
1712
|
+
##### Validation error types: errors vs. warnings
|
1713
|
+
|
1714
|
+
###### Persistance failed: errors
|
1715
|
+
|
1716
|
+
If an endpoint returns errors in the response body, that is enough to interpret it as: persistance failed.
|
1717
|
+
The response status code in this scenario is neglected.
|
1718
|
+
|
1719
|
+
###### Persistance succeeded: warnings
|
1720
|
+
|
1721
|
+
In some cases, you need non blocking meta information about potential problems with the created record, so called warnings.
|
1722
|
+
|
1723
|
+
If the API endpoint implements warnings, returned when validating, they are provided just as `errors` (same interface and methods) through the `warnings` attribute:
|
1724
|
+
|
1725
|
+
```ruby
|
1726
|
+
# app/controllres/some_controller.rb
|
1727
|
+
|
1728
|
+
@presence = Presence.options(params: { synchronize: false }).create(
|
1729
|
+
place: { href: 'http://storage/places/1' }
|
1730
|
+
)
|
1731
|
+
```
|
1732
|
+
```
|
1733
|
+
POST https://service.example.com/presences { body: '{ "place": { "href": "http://storage/places/1" } }' }
|
1734
|
+
{
|
1735
|
+
field_warnings: [{
|
1736
|
+
code: 'WILL_BE_RESIZED',
|
1737
|
+
path: ['place', 'photos', 0],
|
1738
|
+
message: 'This photo is too small and will be resized.'
|
1739
|
+
}
|
1740
|
+
}
|
1741
|
+
```
|
1742
|
+
|
1743
|
+
```ruby
|
1744
|
+
|
1745
|
+
presence.warnings.any? # true
|
1746
|
+
presence.place.photos[0].warnings.messages.first # 'This photo is too small and will be resized.'
|
1747
|
+
|
1748
|
+
```
|
1749
|
+
|
1750
|
+
##### Using `ActiveModel::Validations` none the less
|
1751
|
+
|
1752
|
+
If you are using `ActiveModel::Validations`, even though it's not recommended, and you add errors to the LHS::Record instance, then those errors will be overwritten by the errors from `ActiveModel::Validations` when using `save` or `valid?`.
|
1753
|
+
|
1754
|
+
So in essence, mixing `ActiveModel::Validations` and LHS built-in validations (via endpoints), is not compatible, yet.
|
1755
|
+
|
1756
|
+
[Open issue](https://github.com/local-ch/lhs/issues/159)
|
1757
|
+
|
1758
|
+
#### Use form_helper to create and update records
|
1759
|
+
|
1760
|
+
Rails `form_for` view-helper can be used in combination with instances of `LHS::Record`s to autogenerate forms:
|
1761
|
+
|
1762
|
+
```ruby
|
1763
|
+
<%= form_for(@instance, url: '/create') do |f| %>
|
1764
|
+
<%= f.text_field :name %>
|
1765
|
+
<%= f.text_area :text %>
|
1766
|
+
<%= f.submit "Create" %>
|
1767
|
+
<% end %>
|
916
1768
|
```
|
917
1769
|
|
918
|
-
|
1770
|
+
### Destroy records
|
1771
|
+
|
1772
|
+
`destroy` deletes a record.
|
1773
|
+
|
1774
|
+
```ruby
|
1775
|
+
# app/controllers/some_controller.rb
|
919
1776
|
|
920
|
-
|
1777
|
+
record = Record.find('1z-5r1fkaj')
|
1778
|
+
```
|
1779
|
+
```
|
1780
|
+
GET https://service.example.com/records/1z-5r1fkaj
|
1781
|
+
```
|
921
1782
|
|
922
1783
|
```ruby
|
923
|
-
|
924
|
-
|
1784
|
+
# app/controllers/some_controller.rb
|
1785
|
+
|
1786
|
+
record.destroy
|
1787
|
+
```
|
1788
|
+
```
|
1789
|
+
DELETE https://service.example.com/records/1z-5r1fkaj
|
925
1790
|
```
|
926
1791
|
|
927
1792
|
You can also destroy records directly without fetching them first:
|
928
1793
|
|
929
1794
|
```ruby
|
930
|
-
|
1795
|
+
# app/controllers/some_controller.rb
|
1796
|
+
|
1797
|
+
destroyed_record = Record.destroy('1z-5r1fkaj')
|
1798
|
+
```
|
1799
|
+
```
|
1800
|
+
DELETE https://service.example.com/records/1z-5r1fkaj
|
931
1801
|
```
|
932
1802
|
|
933
1803
|
or with parameters:
|
934
1804
|
|
935
1805
|
```ruby
|
936
|
-
|
1806
|
+
# app/controllers/some_controller.rb
|
1807
|
+
|
1808
|
+
destroyed_records = Record.destroy(name: 'Steve')
|
1809
|
+
```
|
1810
|
+
```
|
1811
|
+
DELETE https://service.example.com/records?name='Steve'
|
937
1812
|
```
|
938
1813
|
|
939
|
-
|
1814
|
+
### Record getters and setters
|
940
1815
|
|
941
|
-
|
1816
|
+
Sometimes it is neccessary to implement custom getters and setters and convert data to a processable (endpoint) format behind the scenes.
|
942
1817
|
|
943
|
-
|
1818
|
+
#### Record setters
|
944
1819
|
|
945
|
-
|
1820
|
+
You can define setter methods in `LHS::Record`s that will be used by initializers (`new`) and setter methods, that convert data provided, before storing it in the record and persisting it with a remote endpoint:
|
946
1821
|
|
947
1822
|
```ruby
|
948
|
-
|
949
|
-
endpoint '{+service}/v2/users', validates: { params: { persist: false } }
|
950
|
-
end
|
1823
|
+
# app/models/user.rb
|
951
1824
|
|
952
|
-
|
953
|
-
unless user.valid?
|
954
|
-
fail(user.errors[:email])
|
955
|
-
end
|
1825
|
+
class Feedback < LHS::Record
|
956
1826
|
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
1827
|
+
def ratings=(values)
|
1828
|
+
super(
|
1829
|
+
values.map { |k, v| { name: k, value: v } }
|
1830
|
+
)
|
1831
|
+
end
|
1832
|
+
end
|
963
1833
|
```
|
964
1834
|
|
965
|
-
The parameters passed to the `validates` endpoint option are used to perform the validation:
|
966
|
-
|
967
1835
|
```ruby
|
968
|
-
|
969
|
-
endpoint '{+service}/v2/users', validates: { params: { publish: false } } # will add ?publish=false to the request
|
970
|
-
endpoint '{+service}/v2/users', validates: { params: { validates: true } } # will add ?validates=true to the request
|
971
|
-
endpoint '{+service}/v2/users', validates: { path: 'validate' } # will perform a validation via :service/v2/users/validate
|
972
|
-
```
|
973
|
-
|
974
|
-
### HTTP Status Codes for validation errors
|
975
|
-
|
976
|
-
LHS provides the http status code received when performing validations on a record, through the errors object:
|
1836
|
+
# app/controllers/some_controller.rb
|
977
1837
|
|
978
|
-
|
979
|
-
record.
|
980
|
-
record.errors.status_code #400
|
1838
|
+
record = Record.new(ratings: { quality: 3 })
|
1839
|
+
record.ratings # [{ :name=>:quality, :value=>3 }]
|
981
1840
|
```
|
982
1841
|
|
983
|
-
|
1842
|
+
#### Record getters
|
984
1843
|
|
985
|
-
|
1844
|
+
If you implement accompanying getter methods, the whole data conversion would be internal only:
|
986
1845
|
|
987
1846
|
```ruby
|
988
|
-
|
989
|
-
```
|
1847
|
+
# app/models/user.rb
|
990
1848
|
|
991
|
-
|
1849
|
+
class Feedback < LHS::Record
|
992
1850
|
|
993
|
-
|
1851
|
+
def ratings=(values)
|
1852
|
+
super(
|
1853
|
+
values.map { |k, v| { name: k, value: v } }
|
1854
|
+
)
|
1855
|
+
end
|
994
1856
|
|
995
|
-
|
996
|
-
|
1857
|
+
def ratings
|
1858
|
+
super.map { |r| [r[:name], r[:value]] }]
|
1859
|
+
end
|
1860
|
+
end
|
997
1861
|
```
|
998
1862
|
|
999
|
-
|
1863
|
+
```ruby
|
1864
|
+
# app/controllers/some_controller.rb
|
1000
1865
|
|
1001
|
-
|
1866
|
+
record = Record.new(ratings: { quality: 3 })
|
1867
|
+
record.ratings # {:quality=>3}
|
1868
|
+
```
|
1002
1869
|
|
1003
|
-
|
1870
|
+
### Include linked resources (hyperlinks and hypermedia)
|
1004
1871
|
|
1005
|
-
|
1006
|
-
# controller.rb
|
1007
|
-
unless @customer.save
|
1008
|
-
@errors = @customer.errors
|
1009
|
-
end
|
1872
|
+
In a service-oriented architecture using [hyperlinks](https://en.wikipedia.org/wiki/Hyperlink)/[hypermedia](https://en.wikipedia.org/wiki/Hypermedia), records/resources can contain hyperlinks to other records/resources.
|
1010
1873
|
|
1011
|
-
|
1012
|
-
= form_for @customer, as: :customer do |customer_form|
|
1874
|
+
When fetching records with LHS, you can specify in advance all the linked resources that you want to include in the results.
|
1013
1875
|
|
1014
|
-
|
1876
|
+
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.
|
1015
1877
|
|
1016
|
-
|
1878
|
+
Including linked resources/records is heavily influenced by [http://guides.rubyonrails.org/active_record_class_querying](http://guides.rubyonrails.org/active_record_class_querying.html#eager-loading-associations) and you should read it to understand this feature in all it's glo
|
1017
1879
|
|
1018
|
-
|
1019
|
-
= street_form.input :house_number
|
1020
|
-
```
|
1880
|
+
#### Ensure the whole linked collection is included: includes_all
|
1021
1881
|
|
1022
|
-
|
1882
|
+
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`.
|
1023
1883
|
|
1024
|
-
|
1884
|
+
LHS will ensure that all linked resources are around by loading all pages (parallelized/performance optimized).
|
1025
1885
|
|
1026
1886
|
```ruby
|
1027
|
-
|
1028
|
-
|
1887
|
+
# app/controllers/some_controller.rb
|
1888
|
+
|
1889
|
+
customer = Customer.includes_all(contracts: :products).find(1)
|
1890
|
+
```
|
1891
|
+
```
|
1892
|
+
> GET https://service.example.com/customers/1
|
1893
|
+
< {... contracts: { href: 'https://service.example.com/customers/1/contracts' } }
|
1894
|
+
> GET https://service.example.com/customers/1/contracts?limit=100
|
1895
|
+
< {... items: [...], limit: 10, offset: 0, total: 32 }
|
1896
|
+
In parallel:
|
1897
|
+
> GET https://service.example.com/customers/1/contracts?limit=10&offset=10
|
1898
|
+
< {... products: [{ href: 'https://service.example.com/product/LBC' }] }
|
1899
|
+
> GET https://service.example.com/customers/1/contracts?limit=10&offset=20
|
1900
|
+
< {... products: [{ href: 'https://service.example.com/product/LBB' }] }
|
1901
|
+
In parallel:
|
1902
|
+
> GET https://service.example.com/product/LBC
|
1903
|
+
< {... name: 'Local Business Card' }
|
1904
|
+
> GET https://service.example.com/product/LBB
|
1905
|
+
< {... name: 'Local Business Basic' }
|
1029
1906
|
```
|
1030
1907
|
|
1031
|
-
|
1908
|
+
```ruby
|
1909
|
+
# app/controllers/some_controller.rb
|
1032
1910
|
|
1033
|
-
|
1034
|
-
|
1911
|
+
customer.contracts.length # 32
|
1912
|
+
customer.contracts.first.products.first.name # Local Business Card
|
1035
1913
|
|
1036
|
-
```ruby
|
1037
|
-
lhs.errors.records.customer.attributes.name.unsupported_property_value
|
1038
|
-
lhs.errors.records.customer.unsupported_property_value
|
1039
|
-
lhs.errors.messages.unsupported_property_value
|
1040
|
-
lhs.errors.attributes.name.unsupported_property_value
|
1041
|
-
lhs.errors.fallback_message
|
1042
1914
|
```
|
1043
1915
|
|
1044
|
-
|
1045
|
-
If you are using `ActiveModel::Validations` and add errors to the LHS::Record instance - as described above - then those errors will be overwritten by the errors from `ActiveModel::Validations` when using `save` or `valid?`. [Open issue](https://github.com/local-ch/lhs/issues/159)
|
1046
|
-
|
1047
|
-
### Blocking errors, original "errors"
|
1916
|
+
#### Include the first linked page or single item is included: include
|
1048
1917
|
|
1049
|
-
|
1918
|
+
`includes` includes the first page/response when loading the linked resource. **If the endpoint is paginated, only the first page will be included.**
|
1050
1919
|
|
1051
|
-
|
1920
|
+
```ruby
|
1921
|
+
# app/controllers/some_controller.rb
|
1052
1922
|
|
1923
|
+
customer = Customer.includes(contracts: :products).find(1)
|
1053
1924
|
```
|
1054
|
-
{
|
1055
|
-
company_name: 'localsearch',
|
1056
|
-
field_errors: [{
|
1057
|
-
code: 'REQUIRED_PROPERTY_VALUE',
|
1058
|
-
path: ['place', 'opening_hours']
|
1059
|
-
}
|
1060
|
-
}
|
1061
1925
|
```
|
1062
|
-
|
1063
|
-
|
1926
|
+
> GET https://service.example.com/customers/1
|
1927
|
+
< {... contracts: { href: 'https://service.example.com/customers/1/contracts' } }
|
1928
|
+
> GET https://service.example.com/customers/1/contracts?limit=100
|
1929
|
+
< {... items: [...], limit: 10, offset: 0, total: 32 }
|
1930
|
+
In parallel:
|
1931
|
+
> GET https://service.example.com/product/LBC
|
1932
|
+
< {... name: 'Local Business Card' }
|
1933
|
+
> GET https://service.example.com/product/LBB
|
1934
|
+
< {... name: 'Local Business Basic' }
|
1935
|
+
```
|
1064
1936
|
|
1065
1937
|
```ruby
|
1066
|
-
|
1067
|
-
place: { href: 'http://storage/places/1' }
|
1068
|
-
)
|
1069
|
-
|
1070
|
-
presence.errors.any? # true
|
1071
|
-
presence.place.errors.messages[:opening_hours] # ['This field needs to be present']
|
1072
|
-
presence.place.errors.codes[:opening_hours] # ['REQUIRED_PROPERTY_VALUE']
|
1073
|
-
```
|
1938
|
+
# app/controllers/some_controller.rb
|
1074
1939
|
|
1075
|
-
|
1940
|
+
customer.contracts.length # 10
|
1941
|
+
customer.contracts.first.products.first.name # Local Business Card
|
1076
1942
|
|
1077
|
-
|
1943
|
+
```
|
1078
1944
|
|
1079
|
-
|
1945
|
+
#### Include various levels of linked data
|
1080
1946
|
|
1081
|
-
|
1082
|
-
{
|
1083
|
-
field_warnings: [{
|
1084
|
-
code: 'WILL_BE_RESIZED',
|
1085
|
-
path: ['place', 'photos', 0],
|
1086
|
-
message: 'The image will be resized.'
|
1087
|
-
}
|
1088
|
-
}
|
1089
|
-
```
|
1947
|
+
The method syntax of `includes` and `includes_all`, allows you include hyperlinks stored in deep nested data strutures:
|
1090
1948
|
|
1091
|
-
|
1949
|
+
Some examples:
|
1092
1950
|
|
1093
1951
|
```ruby
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1952
|
+
Record.includes(:localch_account, :entry)
|
1953
|
+
# Includes localch_account -> entry
|
1954
|
+
# { localch_account: { href: '...', entry: { href: '...' } } }
|
1097
1955
|
|
1098
|
-
|
1099
|
-
|
1956
|
+
Record.includes([:localch_account, :entry])
|
1957
|
+
# Includes localch_account and entry
|
1958
|
+
# { localch_account: { href: '...' }, entry: { href: '...' } }
|
1959
|
+
|
1960
|
+
Record.includes(campaign: [:entry, :user])
|
1961
|
+
# Includes campaign and entry and user from campaign
|
1962
|
+
# { campaign: { href: '...' , entry: { href: '...' }, user: { href: '...' } } }
|
1100
1963
|
```
|
1101
1964
|
|
1102
|
-
|
1965
|
+
#### Identify and cast known records when including records
|
1103
1966
|
|
1104
|
-
|
1967
|
+
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.
|
1105
1968
|
|
1106
|
-
|
1969
|
+
That also means that options for endpoints of linked resources are applied when requesting those in addition.
|
1107
1970
|
|
1108
|
-
|
1971
|
+
This applies for example a records endpoint configuration even though it's fetched/included through another record:
|
1109
1972
|
|
1110
|
-
*Example 1 "offset"-strategy (default configuration)*
|
1111
1973
|
```ruby
|
1112
|
-
#
|
1113
|
-
{
|
1114
|
-
items: [{...}, ...]
|
1115
|
-
total: 300,
|
1116
|
-
limit: 100,
|
1117
|
-
offset: 0
|
1118
|
-
}
|
1119
|
-
# Next 'pages' are navigated with offset: 100, offset: 200, ...
|
1974
|
+
# app/models/favorite.rb
|
1120
1975
|
|
1121
|
-
|
1122
|
-
class Results < LHS::Record
|
1123
|
-
endpoint 'results'
|
1124
|
-
end
|
1125
|
-
```
|
1976
|
+
class Favorite < LHS::Record
|
1126
1977
|
|
1127
|
-
|
1128
|
-
|
1129
|
-
# API response
|
1130
|
-
{
|
1131
|
-
docs: [{...}, ...]
|
1132
|
-
totalPages: 3,
|
1133
|
-
limit: 100,
|
1134
|
-
page: 1
|
1135
|
-
}
|
1136
|
-
# Next 'pages' are navigated with page: 1, offset: 2, ...
|
1978
|
+
endpoint '{+service}/users/{user_id}/favorites', auth: { basic: { username: 'steve', password: 'can' } }
|
1979
|
+
endpoint '{+service}/users/{user_id}/favorites/:id', auth: { basic: { username: 'steve', password: 'can' } }
|
1137
1980
|
|
1138
|
-
# How LHS has to be configured
|
1139
|
-
class Results < LHS::Record
|
1140
|
-
configuration items_key: 'docs', total_key: 'totalPages', pagination_key: 'page', pagination_strategy: 'page'
|
1141
|
-
endpoint 'results'
|
1142
1981
|
end
|
1143
1982
|
```
|
1144
1983
|
|
1145
|
-
*Example 3 "start"-strategy and naming configuration*
|
1146
1984
|
```ruby
|
1147
|
-
#
|
1148
|
-
{
|
1149
|
-
results: [{...}, ...]
|
1150
|
-
total: 300,
|
1151
|
-
badgeSize: 100,
|
1152
|
-
startAt: 1
|
1153
|
-
}
|
1154
|
-
# Next 'pages' are navigated with startWith: 101, startWith: 201, ...
|
1155
|
-
|
1156
|
-
# How LHS has to be configured
|
1157
|
-
class Results < LHS::Record
|
1158
|
-
configuration items_key: 'results', limit_key: 'badgeSize', pagination_key: 'startAt', pagination_strategy: 'start'
|
1159
|
-
endpoint 'results'
|
1160
|
-
end
|
1161
|
-
```
|
1985
|
+
# app/models/place.rb
|
1162
1986
|
|
1163
|
-
|
1987
|
+
class Place < LHS::Record
|
1164
1988
|
|
1165
|
-
|
1989
|
+
endpoint '{+service}/v2/places', auth: { basic: { username: 'steve', password: 'can' } }
|
1990
|
+
endpoint '{+service}/v2/places/{id}', auth: { basic: { username: 'steve', password: 'can' } }
|
1166
1991
|
|
1167
|
-
```ruby
|
1168
|
-
class Search < LHS::Record
|
1169
|
-
configuration items_key: 'searchResults', total_key: 'total', limit_key: 'limit', pagination_key: 'offset', pagination_strategy: 'offset'
|
1170
|
-
endpoint 'https://search'
|
1171
1992
|
end
|
1172
1993
|
```
|
1173
1994
|
|
1174
|
-
|
1175
|
-
|
1176
|
-
`items_key` key used to determine items of the current page (e.g. `docs`, `items`, etc.).
|
1177
|
-
|
1178
|
-
`item_created_key` key used to merge record data thats nested in the creation response body.
|
1179
|
-
|
1180
|
-
`limit_key` key used to work with page limits (e.g. `size`, `limit`, etc.)
|
1995
|
+
```ruby
|
1996
|
+
# app/controllers/some_controller.rb
|
1181
1997
|
|
1182
|
-
|
1998
|
+
Favorite.includes(:place).where(user_id: current_user.id)
|
1183
1999
|
|
1184
|
-
```
|
1185
|
-
|
2000
|
+
```
|
2001
|
+
```
|
2002
|
+
> GET https://service.example.com/users/123/favorites { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }
|
2003
|
+
< {... items: [... { place: { href: 'https://service.example.com/place/456' } } ] }
|
2004
|
+
In parallel:
|
2005
|
+
> GET https://service.example.com/place/456 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }
|
2006
|
+
> GET https://service.example.com/place/789 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }
|
2007
|
+
> GET https://service.example.com/place/1112 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }
|
2008
|
+
> GET https://service.example.com/place/5423 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }
|
1186
2009
|
```
|
1187
2010
|
|
1188
|
-
|
2011
|
+
#### Apply options for requests performed to fetch included records
|
1189
2012
|
|
1190
|
-
|
2013
|
+
Use `references` to apply request options to requests performed to fetch included records:
|
1191
2014
|
|
1192
2015
|
```ruby
|
1193
|
-
|
2016
|
+
# app/controllers/some_controller.rb
|
2017
|
+
|
2018
|
+
Favorite.includes(:place).references(place: { auth: { bearer: '123' }}).where(user_id: 1)
|
2019
|
+
```
|
2020
|
+
```
|
2021
|
+
GET https://service.example.com/users/1/favorites
|
2022
|
+
{... items: [... { place: { href: 'https://service.example.com/places/2' } }] }
|
2023
|
+
In parallel:
|
2024
|
+
GET https://service.example.com/places/2 { headers: { 'Authentication': 'Bearer 123' } }
|
2025
|
+
GET https://service.example.com/places/3 { headers: { 'Authentication': 'Bearer 123' } }
|
2026
|
+
GET https://service.example.com/places/4 { headers: { 'Authentication': 'Bearer 123' } }
|
1194
2027
|
```
|
1195
2028
|
|
1196
|
-
|
2029
|
+
### Record batch processing
|
1197
2030
|
|
1198
|
-
|
2031
|
+
**Be careful using methods for batch processing. They could result in a lot of HTTP requests!**
|
1199
2032
|
|
1200
|
-
|
2033
|
+
#### all
|
1201
2034
|
|
1202
|
-
|
1203
|
-
{
|
1204
|
-
"response": {
|
1205
|
-
"location": {
|
1206
|
-
"id": 123
|
1207
|
-
}
|
1208
|
-
}
|
1209
|
-
}
|
1210
|
-
```
|
2035
|
+
`all` fetches all records from the service by doing multiple requests, best-effort parallelization, and resolving endpoint pagination if necessary:
|
1211
2036
|
|
1212
2037
|
```ruby
|
1213
|
-
|
1214
|
-
configuration item_key: [:response, :location]
|
1215
|
-
end
|
1216
|
-
|
1217
|
-
location = Location.find(123)
|
1218
|
-
location.id # 123
|
2038
|
+
records = Record.all
|
1219
2039
|
```
|
1220
|
-
|
1221
|
-
### Configure complex accessors for nested data
|
1222
|
-
|
1223
|
-
If items, limit, pagination, total etc. is nested in the responding objects, use complex data structures for configuring a record.
|
1224
|
-
|
1225
2040
|
```
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
2041
|
+
> GET https://service.example.com/records?limit=100
|
2042
|
+
< {...
|
2043
|
+
items: [...]
|
2044
|
+
total: 900,
|
2045
|
+
limit: 100,
|
2046
|
+
offset: 0
|
2047
|
+
}
|
2048
|
+
In parallel:
|
2049
|
+
> GET https://service.example.com/records?limit=100&offset=100
|
2050
|
+
> GET https://service.example.com/records?limit=100&offset=200
|
2051
|
+
> GET https://service.example.com/records?limit=100&offset=300
|
2052
|
+
> GET https://service.example.com/records?limit=100&offset=400
|
2053
|
+
> GET https://service.example.com/records?limit=100&offset=500
|
2054
|
+
> GET https://service.example.com/records?limit=100&offset=600
|
2055
|
+
> GET https://service.example.com/records?limit=100&offset=700
|
2056
|
+
> GET https://service.example.com/records?limit=100&offset=800
|
1234
2057
|
```
|
1235
2058
|
|
2059
|
+
`all` is chainable and has the same interface like `where`:
|
2060
|
+
|
1236
2061
|
```ruby
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1240
|
-
end
|
2062
|
+
Record.where(color: 'blue').all
|
2063
|
+
Record.all.where(color: 'blue')
|
2064
|
+
Record.all(color: 'blue')
|
1241
2065
|
```
|
1242
2066
|
|
1243
|
-
|
1244
|
-
|
1245
|
-
```
|
1246
|
-
POST /businesses
|
1247
|
-
response: {
|
1248
|
-
business: {
|
1249
|
-
id: 123
|
1250
|
-
}
|
1251
|
-
}
|
1252
|
-
```
|
2067
|
+
All three are doing the same thing: fetching all records with the color 'blue' from the endpoint while resolving pagingation if endpoint is paginated.
|
1253
2068
|
|
1254
|
-
|
1255
|
-
class Business < LHS::Record
|
1256
|
-
configuration item_created_key: [:response, :business]
|
1257
|
-
endpoint 'http://uberall/businesses'
|
1258
|
-
end
|
2069
|
+
##### Using all, when endpoint does not implement response pagination meta data
|
1259
2070
|
|
1260
|
-
|
1261
|
-
business.id # 123
|
1262
|
-
```
|
2071
|
+
In case an API does not provide pagination information in the repsponse data (limit, offset and total), LHS keeps on loading pages when requesting `all` until the first empty page responds.
|
1263
2072
|
|
1264
|
-
|
2073
|
+
#### find_each
|
1265
2074
|
|
1266
|
-
|
2075
|
+
`find_each` is a more fine grained way to process single records that are fetched in batches.
|
1267
2076
|
|
1268
2077
|
```ruby
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
2078
|
+
Record.find_each(start: 50, batch_size: 20, params: { has_reviews: true }) do |record|
|
2079
|
+
# Iterates over each record. Starts with record no. 50 and fetches 20 records each batch.
|
2080
|
+
record
|
2081
|
+
break if record.some_attribute == some_value
|
2082
|
+
end
|
1274
2083
|
```
|
1275
2084
|
|
1276
|
-
|
2085
|
+
#### find_in_batches
|
1277
2086
|
|
1278
|
-
|
1279
|
-
class Record < LHS::Record
|
1280
|
-
endpoint '{+service}/records'
|
1281
|
-
configuration pagination_strategy: 'page'
|
1282
|
-
end
|
1283
|
-
Record.page(3).per(20).where(color: 'blue')
|
1284
|
-
# /records?page=3&limit=20&color=blue
|
1285
|
-
```
|
2087
|
+
`find_in_batches` is used by `find_each` and processes batches.
|
1286
2088
|
|
1287
2089
|
```ruby
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
1293
|
-
# /records?start=41&limit=20&color=blue
|
2090
|
+
Record.find_in_batches(start: 50, batch_size: 20, params: { has_reviews: true }) do |records|
|
2091
|
+
# Iterates over multiple records (batch size is 20). Starts with record no. 50 and fetches 20 records each batch.
|
2092
|
+
records
|
2093
|
+
break if records.first.name == some_value
|
2094
|
+
end
|
1294
2095
|
```
|
1295
2096
|
|
1296
|
-
|
2097
|
+
### Convert/Cast specific record types: becomes
|
1297
2098
|
|
1298
|
-
|
2099
|
+
Based on [ActiveRecord's implementation](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-becomes), LHS implements `becomes`, too.
|
1299
2100
|
|
1300
|
-
|
2101
|
+
It's a way to convert records of a certain type A to another certain type B.
|
1301
2102
|
|
1302
|
-
|
2103
|
+
_NOTE: RPC-style actions, that are discouraged in REST anyway, are utilizable with this functionality, too. See the following example:_
|
1303
2104
|
|
1304
2105
|
```ruby
|
1305
|
-
#
|
1306
|
-
@items = Record.page(params[:page]).per(100)
|
1307
|
-
```
|
2106
|
+
# app/models/location.rb
|
1308
2107
|
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
2108
|
+
class Location < LHS::Record
|
2109
|
+
endpoint '{+service}/locations'
|
2110
|
+
endpoint '{+service}/locations/{id}'
|
2111
|
+
end
|
1312
2112
|
```
|
1313
2113
|
|
1314
|
-
|
2114
|
+
```ruby
|
2115
|
+
# app/models/synchronization.rb
|
1315
2116
|
|
1316
|
-
|
2117
|
+
class Synchronization < LHS::Record
|
2118
|
+
endpoint '{+service}/locations/{id}/sync'
|
2119
|
+
end
|
2120
|
+
```
|
1317
2121
|
|
1318
|
-
|
2122
|
+
```ruby
|
2123
|
+
# app/controllers/some_controller.rb
|
1319
2124
|
|
1320
|
-
|
2125
|
+
location = Location.find(1)
|
2126
|
+
```
|
2127
|
+
```
|
2128
|
+
GET https://service.example.com/location/1
|
2129
|
+
```
|
1321
2130
|
|
2131
|
+
```ruby
|
2132
|
+
# app/controllers/some_controller.rb
|
1322
2133
|
|
1323
|
-
|
2134
|
+
synchronization = location.becomes(Synchronization)
|
2135
|
+
synchronization.save!
|
2136
|
+
```
|
2137
|
+
```
|
2138
|
+
POST https://service.example.com/location/1/sync { body: '{ ... }' }
|
2139
|
+
```
|
1324
2140
|
|
1325
|
-
|
2141
|
+
## Request Cycle Cache
|
1326
2142
|
|
1327
|
-
LHS
|
2143
|
+
By default, LHS does not perform the same http request multiple times during one request/response cycle.
|
1328
2144
|
|
1329
|
-
|
2145
|
+
```ruby
|
2146
|
+
# app/models/user.rb
|
1330
2147
|
|
1331
|
-
|
2148
|
+
class User < LHS::Record
|
2149
|
+
endpoint '{+service}/users/{id}'
|
2150
|
+
end
|
2151
|
+
```
|
1332
2152
|
|
1333
2153
|
```ruby
|
1334
|
-
|
1335
|
-
|
2154
|
+
# app/models/location.rb
|
2155
|
+
|
2156
|
+
class Location < LHS::Record
|
2157
|
+
endpoint '{+service}/locations/{id}'
|
1336
2158
|
end
|
1337
2159
|
```
|
1338
2160
|
|
1339
|
-
## form_for Helper
|
1340
|
-
Rails `form_for` view-helper can be used in combination with instances of LHS::Record to autogenerate forms:
|
1341
2161
|
```ruby
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
2162
|
+
# app/controllers/some_controller.rb
|
2163
|
+
|
2164
|
+
def index
|
2165
|
+
@user = User.find(1)
|
2166
|
+
@locations = Location.includes(:owner).find(2)
|
2167
|
+
end
|
2168
|
+
```
|
2169
|
+
```
|
2170
|
+
GET https://service.example.com/users/1
|
2171
|
+
GET https://service.example.com/location/2
|
2172
|
+
{... owner: { href: 'https://service.example.com/users/1' } }
|
2173
|
+
From cache:
|
2174
|
+
GET https://service.example.com/users/1
|
1347
2175
|
```
|
1348
2176
|
|
1349
|
-
|
2177
|
+
It uses the [LHC Caching Interceptor](https://github.com/local-ch/lhc#caching-interceptor) as caching mechanism base and sets a unique request id for every request cycle with Railties to ensure data is just cached within one request cycle and not shared with other requests.
|
1350
2178
|
|
1351
|
-
|
2179
|
+
Only GET requests are considered for caching by using LHC Caching Interceptor's `cache_methods` option internally and considers request headers when caching requests, so requests with different headers are not served from cache.
|
1352
2180
|
|
1353
|
-
|
2181
|
+
The LHS Request Cycle Cache is opt-out, so it's enabled by default and will require you to enable the [LHC Caching Interceptor](https://github.com/local-ch/lhc#caching-interceptor) in your project.
|
1354
2182
|
|
1355
|
-
|
2183
|
+
### Change store for LHS' request cycle cache
|
1356
2184
|
|
1357
|
-
|
2185
|
+
By default the LHS Request Cycle Cache will use `ActiveSupport::Cache::MemoryStore` as its cache store. Feel free to configure a cache that is better suited for your needs by:
|
1358
2186
|
|
1359
|
-
|
2187
|
+
```ruby
|
2188
|
+
# config/initializers/lhc.rb
|
1360
2189
|
|
1361
|
-
|
1362
|
-
|
1363
|
-
endpoint 'records/{id}'
|
2190
|
+
LHC.configure do |config|
|
2191
|
+
config.request_cycle_cache = ActiveSupport::Cache::MemoryStore.new
|
1364
2192
|
end
|
2193
|
+
```
|
1365
2194
|
|
1366
|
-
|
1367
|
-
|
2195
|
+
### Disable request cycle cache
|
2196
|
+
|
2197
|
+
If you want to disable the LHS Request Cycle Cache, simply disable it within configuration:
|
2198
|
+
|
2199
|
+
```ruby
|
2200
|
+
# config/initializers/lhc.rb
|
1368
2201
|
|
1369
|
-
|
2202
|
+
LHC.configure do |config|
|
2203
|
+
config.request_cycle_cache_enabled = false
|
2204
|
+
end
|
1370
2205
|
```
|
1371
2206
|
|
1372
|
-
## Testing
|
2207
|
+
## Testing with LHS
|
1373
2208
|
|
1374
|
-
[WebMock](https://github.com/bblimke/webmock)
|
2209
|
+
**Best practice in regards of testing applications using LHS, is to let LHS fetch your records, actually perform HTTP requests and [WebMock](https://github.com/bblimke/webmock) to stub/mock those http requests/responses.**
|
1375
2210
|
|
1376
|
-
|
1377
|
-
This follows the [Black Box Testing](https://en.wikipedia.org/wiki/Black-box_testing) approach and prevents you from building up constraints to LHS' internal structures/mechanisms, which will break when we change internal things.
|
1378
|
-
LHS provides interfaces that result in HTTP requests, this is what you should test.
|
2211
|
+
This follows the [Black Box Testing](https://en.wikipedia.org/wiki/Black-box_testing) approach and prevents you from creating constraints to LHS' internal structures and mechanisms, which will break as soon as we change internals.
|
1379
2212
|
|
1380
2213
|
```ruby
|
2214
|
+
# specs/*/some_spec.rb
|
2215
|
+
|
1381
2216
|
let(:contracts) do
|
1382
2217
|
[
|
1383
2218
|
{number: '1'},
|
@@ -1386,8 +2221,8 @@ let(:contracts) do
|
|
1386
2221
|
]
|
1387
2222
|
end
|
1388
2223
|
|
1389
|
-
before
|
1390
|
-
stub_request(:get, "
|
2224
|
+
before do
|
2225
|
+
stub_request(:get, "https://service.example.com/contracts")
|
1391
2226
|
.to_return(
|
1392
2227
|
body: {
|
1393
2228
|
items: contracts,
|
@@ -1406,29 +2241,43 @@ it 'displays contracts' do
|
|
1406
2241
|
end
|
1407
2242
|
```
|
1408
2243
|
|
1409
|
-
|
2244
|
+
### Test helper for request cycle cache
|
1410
2245
|
|
1411
|
-
|
2246
|
+
In order to not run into caching issues during your tests, when (request cycle cache)[#request-cycle-cache] is enabled, simply require the following helper in your tests:
|
1412
2247
|
|
1413
2248
|
```ruby
|
1414
|
-
|
2249
|
+
# spec/spec_helper.rb
|
2250
|
+
|
2251
|
+
require 'lhs/test/request_cycle_cache_helper'
|
1415
2252
|
```
|
1416
2253
|
|
1417
2254
|
This will initialize a MemoryStore cache for LHC::Caching interceptor and resets the cache before every test.
|
1418
2255
|
|
1419
|
-
|
2256
|
+
### Test query chains
|
2257
|
+
|
2258
|
+
#### By explicitly resolving the chain: fetch
|
1420
2259
|
|
1421
|
-
|
1422
|
-
Common to use in tests, as where queries are not performing any HTTP-requests when no data is accessed.
|
2260
|
+
Use `fetch` in tests to resolve chains in place and expect WebMock stubs to be requested.
|
1423
2261
|
|
1424
2262
|
```ruby
|
2263
|
+
# specs/*/some_spec.rb
|
2264
|
+
|
1425
2265
|
records = Record.where(color: 'blue').where(available: true).where(color: 'red')
|
1426
2266
|
|
1427
2267
|
expect(
|
1428
|
-
records
|
2268
|
+
records.fetch
|
1429
2269
|
).to have_requested(:get, %r{records/})
|
1430
2270
|
.with(query: hash_including(color: 'blue', available: true))
|
1431
|
-
|
2271
|
+
```
|
2272
|
+
|
2273
|
+
#### Without resolving the chain: where_values_hash
|
2274
|
+
|
2275
|
+
As `where` chains are not resolving to HTTP-requests when no data is accessed, you can use `where_values_hash` to access the values that would be used to resolve the chain, and test those:
|
2276
|
+
|
2277
|
+
```ruby
|
2278
|
+
# specs/*/some_spec.rb
|
2279
|
+
|
2280
|
+
records = Record.where(color: 'blue').where(available: true).where(color: 'red')
|
1432
2281
|
|
1433
2282
|
expect(
|
1434
2283
|
records.where_values_hash
|
@@ -1438,3 +2287,4 @@ expect(
|
|
1438
2287
|
## License
|
1439
2288
|
|
1440
2289
|
[GNU Affero General Public License Version 3.](https://www.gnu.org/licenses/agpl-3.0.en.html)
|
2290
|
+
|