lhs 15.5.1 → 15.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 901e6857c26a457c44d35d426b042bc1b697e728867980ae3fbfa5a603538026
4
- data.tar.gz: c79f1df56929ec8f612f3efbf4fe8a3f767e3193cefc02f02cfa860602e3cf1e
3
+ metadata.gz: d0d357a05ee1bf7c6943779c92169c9a411a4b658e20a6adaf651f29d4e743e8
4
+ data.tar.gz: ad887ed271702978c90da76ec842ec36e616d1f2b549f82b72dfad337cb03805
5
5
  SHA512:
6
- metadata.gz: 809ae4fe85beeafc1fc720722a92197a5ffd0b248d19bd486a4acfd17283036e7ef321f65893ad3acc9cc2052c438ece6ea7d2e56b53b08b88b39943819c21a3
7
- data.tar.gz: '08d18c6e3cd495b780c905abdb7d1c729cf013ced5e6510e1416f597954b9c0037710053a7cc562053c4a0ff89dae7d264dee2d505dba579ff82ca3176cf860f'
6
+ metadata.gz: 86c1ecd1b8f80b395ca5295a04686c8d596ec36299c8a4e5b22ee917ed8fad83d07dbb778df0146f4a8e42dc3f2f29fa927389ce2376b1e354961560cd9149b2
7
+ data.tar.gz: 1704037a4744234e2c693eaa2a346f9be12d0f4bfac92790cb12c78671048c15013944632e4bfb410586231a2e37a9141a122753429ad3246eef460831b3b2ab
data/.gitignore CHANGED
@@ -36,3 +36,4 @@ Gemfile.*.lock
36
36
  bower.json
37
37
 
38
38
  spec/dummy/log/test.log
39
+ spec/dummy/tmp
data/.rubocop.yml CHANGED
@@ -44,3 +44,6 @@ Naming/PredicateName:
44
44
 
45
45
  RSpec/MessageSpies:
46
46
  Enabled: false
47
+
48
+ Style/RegexpLiteral:
49
+ Enabled: false
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
- # intializers/lhc.rb
13
+ # config/initializers/lhc.rb
14
+
16
15
  LHC.configure do |config|
17
- config.interceptors = [LHC::Caching]
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}/v2/records'
29
- endpoint '{+service}/v2/association/{association_id}/records'
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
- ## Where to store LHS::Records
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
- Please store all defined LHS::Records in `app/models` as they are not auto loaded by rails otherwise.
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
- ## Endpoints
161
+ end
162
+ ```
42
163
 
43
- You setup a LHS::Record by configuring one or multiple endpoints. You can also add request options for an endpoint (see following example).
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}/v2/association/{association_id}/records'
49
- endpoint '{+service}/v2/association/{association_id}/records/{id}'
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
- ### Configuring endpoint hosts
177
+ -> Check [LHC](https://github.com/local-ch/lhc) for more information about request options
57
178
 
58
- Please use placeholders when configuring hosts for endpoints. Otherwise LHS will match them strictly, which can result in problems when a services dynamically returns `hrefs` and mixes `http`, `https` or no protocol at all. See: [LHC Placeholder Configuration](https://github.com/local-ch/lhc/blob/master/docs/configuration.md#placeholders)
179
+ #### Configure endpoint hosts
59
180
 
60
- Please DO NOT mix host placeholders with endpoints (paths), LHS need to know what part of an endpoint is a host and what part of an endpoint is a path, if you use a placeholders in your records endpoint configuration:
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(:search_service, 'http://tel.search.ch')
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 '{+search_service}/api/search.json'
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(:search_service, 'http://tel.search.ch/api/search.json')
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 '{+search_service}'
220
+ endpoint '{+search}'
84
221
 
85
222
  end
86
223
  ```
87
224
 
88
- ### Endpoint clashing
225
+ #### Ambiguous endpoints
89
226
 
90
- If you try to setup a LHS::Record with clashing endpoints it will immediately raise an exception.
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}/v2/records'
96
- endpoint '{+service}/v2/something_else'
234
+ endpoint '{+service}/records'
235
+ endpoint '{+service}/bananas'
97
236
 
98
237
  end
99
- # raises: Clashing endpoints.
238
+
239
+ # raises: Ambiguous endpoints
100
240
  ```
101
241
 
102
- ## Find multiple records
242
+ ### Record inheritance
103
243
 
104
- You can query a service for records by using `where`.
244
+ You can inherit from previously defined records and also inherit endpoints that way:
105
245
 
106
246
  ```ruby
107
- Record.where(color: 'blue')
247
+ # app/models/base.rb
248
+
249
+ class Base < LHS::Record
250
+ endpoint '{+service}/records/{id}'
251
+ end
108
252
  ```
109
253
 
110
- This uses the `{+service}/v2/records` endpoint, cause `{association_id}` was not provided. In addition it would add `?color=blue` to the get parameters.
254
+ ```ruby
255
+ # app/models/record.rb
256
+
257
+ class Record < Base
258
+ end
259
+ ```
111
260
 
112
261
  ```ruby
113
- Record.where(association_id: 'fq-a81ngsl1d')
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
- Uses the `{+service}/v2/association/{association_id}/records` endpoint.
270
+ ### Find multiple records
117
271
 
118
- ### Expand plain collection of links
272
+ #### fetch
119
273
 
120
- Some endpoints could respond a plain list of links without any expanded data. Like search endpoints.
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
- ```json
124
- {
125
- "items" : [
126
- {"href": "http://local.ch/customer/1/accounts/1"},
127
- {"href": "http://local.ch/customer/1/accounts/2"},
128
- {"href": "http://local.ch/customer/1/accounts/3"}
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
- Account.where(customer_id: 123).expanded
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
- You can also apply options to `expanded` in order to apply anything on the requests made to expand the links:
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
- Account.where(customer_id: 123).expanded(auth: { bearer: access_token })
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
- ## Chaining where statements
314
+ #### Reuse/Dry where statements: Use scopes
145
315
 
146
- LHS supports chaining where statements.
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
- records = Record.where(color: 'blue')
156
- ...
157
- records.where(available: true).each do |record|
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
- The example would fetch records with the following parameters: `{color: blue, available: true}`.
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
- ## Where values hash
348
+ records = Record.all
165
349
 
166
- Returns a hash of where conditions.
167
- Common to use in tests, as where queries are not performing any HTTP-requests when no data is accessed.
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
- records = Record.where(color: 'blue').where(available: true).where(color: 'red')
358
+ # app/controllers/some_controller.rb
171
359
 
172
- expect(
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
- ## Scopes: Reuse where statements
364
+ #### all with unpaginated endpoints
184
365
 
185
- In order to make common where statements reusable you can organize them in scopes:
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
- endpoint 'records/'
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
- ## Error handling with chains
377
+ ```ruby
378
+ # app/controllers/some_controller.rb
200
379
 
201
- One benefit of chains is lazy evaluation. This means they get resolved when data is accessed. This makes it hard to catch errors with normal `rescue` blocks.
380
+ records = Record.all
202
381
 
203
- To simplify error handling with chains, you can also chain error handlers to be resolved, as part of the chain.
382
+ ```
383
+ ```
384
+ GET https://service.example.com/records
385
+ ```
204
386
 
205
- In case no matching error handler is found the error gets re-raised.
387
+ #### Retrieve the amount of a collection of items: count vs. length
206
388
 
207
- ```ruby
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
- [List of possible error classes](https://github.com/local-ch/lhc/tree/master/lib/lhc/errors)
391
+ `count` The total number of items available remotly via the provided endpoint/api, communicated via pagination meta data.
214
392
 
215
- If an error handler returns `nil` an empty LHS::Record is returned, not `nil`!
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
- In case you want to ignore errors and continue working with `nil` in those cases,
218
- please use `ignore`:
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
- record = Record.ignore(LHC::NotFound).find_by(color: 'blue')
222
- record # nil
402
+ Record.find(123)
403
+ ```
404
+ ```
405
+ GET https://service.example.com/records/123
223
406
  ```
224
407
 
225
- ## Resolve chains
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
- LHS Chains can be resolved with `fetch`, similar to ActiveRecord:
415
+ `find` can also be used to find a single unique record with parameters:
228
416
 
229
417
  ```ruby
230
- records = Record.where(color: 'blue').fetch
418
+ Record.find(another_identifier: 456)
419
+ ```
420
+ ```
421
+ GET https://service.example.com/records?another_identifier=456
231
422
  ```
232
423
 
233
- ## Find single records
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
- Record.find(123)
239
- Record.find('https://api.example.com/records/123')
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
- If no record is found an error is raised.
436
+ #### find_by
243
437
 
244
- `find` can also be used to find a single unique record with parameters:
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
- Record.find(association_id: 123, id: 456)
443
+ Record.find_by(color: 'blue')
444
+ ```
445
+ ```
446
+ GET https://service.example.com/records?color=blue
248
447
  ```
249
448
 
250
- `find_by` finds the first record matching the specified conditions.
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
- `find_by!` raises LHC::NotFound if nothing was found.
453
+ `first!` raises `LHC::NotFound` if nothing was found.
255
454
 
256
455
  ```ruby
257
- Record.find_by(id: 'z12f-3asm3ngals')
258
- Record.find_by(id: 'doesntexist') # nil
456
+ Record.first
457
+ ```
458
+ ```
459
+ GET https://service.example.com/records?limit=1
259
460
  ```
260
461
 
261
- `first` is an alias for finding the first record without parameters.
462
+ `first` can also be used with options:
262
463
 
263
464
  ```ruby
264
- Record.first
465
+ Record.first(params: { color: :blue })
466
+ ```
467
+ ```
468
+ GET https://service.example.com/records?color=blue&limit=1
265
469
  ```
266
470
 
267
- If no record is found, `nil` is returned.
471
+ #### last
268
472
 
269
- `first!` raises LHC::NotFound if nothing was found.
473
+ `last` is an alias for finding the last record without parameters. If no record is found, `nil` is returned.
270
474
 
271
- `first` can also be used with options:
475
+ `last!` raises `LHC::NotFound` if nothing was found.
272
476
 
273
477
  ```ruby
274
- Record.first(params: { color: :blue })
478
+ Record.last
275
479
  ```
276
480
 
277
- `last` is an alias for finding the last record without parameters.
481
+ `last` can also be used with options:
278
482
 
279
483
  ```ruby
280
- Record.last
484
+ Record.last(params: { color: :blue })
281
485
  ```
282
486
 
283
- If no record is found, `nil` is returned.
487
+ ### Work with retrieved data
284
488
 
285
- `last!` raises LHC::NotFound if nothing was found.
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.last(params: { color: :blue })
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
- # Find multiple single records in parallel
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
- In case you want to fetch multiple records by id in parallel, you can also do this with `find`:
513
+ To influence how data is accessed, simply create methods inside your Record to access complex data structures:
296
514
 
297
515
  ```ruby
298
- Record.find(1, 2, 3)
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
- 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:
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
- data = Record
305
- .handle(LHC::Unauthorized, ->(response) { Record.new(name: 'unknown') })
306
- .find(1, 2, 3)
307
- data[1].name # 'unknown'
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
- ## Navigate data
545
+ ```ruby
546
+ # app/models/favorite.rb
311
547
 
312
- After fetching [single](#find-single-records) or [multiple](#find-multiple-records) records you can navigate the received data with ease.
548
+ class Favorite < LHS::Record
549
+ endpoint '{+service}/favorites'
550
+ endpoint '{+service}/favorites/{id}'
551
+ end
552
+ ```
313
553
 
314
554
  ```ruby
315
- records = Record.where(color: 'blue')
316
- records.collection? # true
317
- record = records.first
318
- record.item? # true
319
- record.parent == records # true
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
- ## Relations
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
- Even though, nested data is automatically casted when accessed, see: [Nested records](#nested-records), sometimes api's don't provide dedicated endpoints to retrieve these records.
570
+ ##### Relations / Associations
325
571
 
326
- As those records also don't have an href, nested records can not be casted automatically, when accessed.
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
- Those kind of relations, you can still configure manually, using `has_many` and `has_one`:
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 'http://uberall/locations/{id}'
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 'http://myservice/transaction/{id}'
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
- ### Options
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
- `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. UberallPhoto or namespaced Uberall::Photo, you'll have to specify it with this option.
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
- module Uberall
673
+ # app/models/custom/location.rb
674
+
675
+ module Custom
383
676
  class Location < LHS::Record
384
- endpoint 'http://uberall/locations'
385
- endpoint 'http://uberall/locations/:id'
677
+ endpoint '{+service}/locations'
678
+ endpoint '{+service}/locations/{id}'
386
679
 
387
- has_many :photos, class_name: 'Uberall::Photo'
680
+ has_one :photo, class_name: 'Custom::Photo'
388
681
  end
389
682
  end
683
+ ```
390
684
 
391
- module Uberall
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
- ## Request based options
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
- You can apply options to the request chain. Those options will be forwarded to the request perfomed by the chain/query.
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
- # Authenticate with OAuth
403
- options = { auth: { bearer: '123456' } }
709
+ # app/controllers/some_controller.rb
404
710
 
405
- AuthenticatedRecord = Record.options(options)
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
- blue_records = AuthenticatedRecord.where(color: 'blue')
408
- active_records = AuthenticatedRecord.where(active: true)
719
+ #### Determine collections from the response body
409
720
 
410
- AuthenticatedRecord.create(color: 'red')
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
- record = AuthenticatedRecord.find(123)
413
- # Find resolves the current query and applies all options from the chain
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
- authenticated_record = record.options(options)
418
- authenticated_record.valid?
419
- authenticated_record.save
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
- ## Request Cycle Cache
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
- It uses the [LHC Caching Interceptor](https://github.com/local-ch/lhc/blob/master/docs/interceptors/caching.md) 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.
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
- 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.
742
+ ### Chain complex queries
431
743
 
432
- 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/blob/master/docs/interceptors/caching.md) in your project.
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
- If you want to disable the LHS Request Cycle Cache, simply disable it within configuration:
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
- ```ruby
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
- 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:
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
- LHS.config.request_cycle_cache = ActiveSupport::Cache::MemoryStore.new
444
- ```
753
+ # app/controllers/some_controller.rb
445
754
 
446
- ## Batch processing
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
- `all` fetches all records from the service by doing multiple requests and resolving endpoint pagination if necessary.
758
+ #### Chain where queries
451
759
 
452
760
  ```ruby
453
- data = Record.all
454
- data.count # 998
455
- data.length # 998
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
- `all` is chainable and has the same interface like `where` (See: [Find multiple records](https://github.com/local-ch/lhs#find-multiple-records))
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
- Record.where(color: 'blue').all
462
- Record.all.where(color: 'blue')
463
- Record.all(color: 'blue')
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
- In case an API does not provide pagination information (limit, offset and total), LHS keeps on loading pages when requesting `all` until the first empty page responds.
781
+ #### Expand plain collections of links: expanded
468
782
 
469
- [Count vs. Length](#count-vs-length)
783
+ Some endpoints could respond only with a plain list of links and without any expanded data, like search results.
470
784
 
471
- `find_each` is a more fine grained way to process single records that are fetched in batches.
785
+ Use `expanded` to have LHS expand that data, by performing necessary requests in parallel:
472
786
 
473
787
  ```ruby
474
- Record.find_each(start: 50, batch_size: 20, params: { has_reviews: true }) do |record|
475
- # Iterates over each record. Starts with record no. 50 and fetches 20 records each batch.
476
- record
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
- Record.find_in_batches(start: 50, batch_size: 20, params: { has_reviews: true }) do |records|
484
- # Iterates over multiple records (batch size is 20). Starts with record no. 50 and fetches 20 records each batch.
485
- records
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
- ## Create records
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
- record = Record.create(
494
- recommended: true,
495
- source_id: 'aaa',
496
- content_ad_id: '1z-5r1fkaj'
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
- See [Validation](#validation) for handling validation errors when creating records.
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
- ### Endpoint paramters and paramter injection during creation
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 injects body parameters to generate target urls, used for creation requests:
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
- class Favorite << LHS::Record
508
- endpoint '{+datastore}/content-ads/{content_ad_id}/feedbacks'
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
- Favorite.create(content_ad_id: 51232, text: 'Great Restaurant!')
512
- # POST http://datastore/content_ads/51232
513
- # body: '{ "text" : "Great Restaurant!" }'
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
- Because API's usually reject body paramters for foreign key attributes:
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
- We remove it from the body, if the information was instead transported through the URL.
920
+ **If an error handler returns `nil` an empty LHS::Record is returned, not `nil`!**
523
921
 
524
- ### Create records through associations (nested resources)
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
- class Review < LHS::Record
528
- endpoint '{+service}/reviews'
529
- end
926
+ # app/controllers/some_controller.rb
530
927
 
531
- class Comment < LHS::Record
532
- endpoint '{+service}/reviews/{review_id/}comments'
533
- end
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
- #### Item
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
- review = Review.find(1)
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
- review.comment.create(text: 'Thank you!')
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
- If the item already exists `ArgumentError` is raised.
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
- review = Review.includes(:comments).find(1)
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
- review.comments.create(text: 'Thank you!')
567
- # Comment#1
568
- # :href => '{+service}/reviews/1/comments/1'
569
- # :text => 'Thank you!'
964
+ blue_records = AuthenticatedRecord.where(color: 'blue')
570
965
 
571
- review
572
- # Review#1
573
- # :href => '{+service}/reviews/1'
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
- review = Review.find(1)
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
- review.comments.create(text: 'Thank you!')
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
- ## Build new records
984
+ record = AuthenticatedRecord.find(123)
599
985
 
600
- Build and persist new items from scratch are done either with `new` or it's alias `build`.
986
+ ```
987
+ ```
988
+ GET https://service.example.com/records/123 { headers: { 'Authentication': 'Bearer 123456' } }
989
+ ```
601
990
 
602
991
  ```ruby
603
- record = Record.new(recommended: true)
604
- record.save
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
- ### Endpoint parameters and paramter injection for saving records
998
+ ```ruby
999
+ # app/controllers/some_controller.rb
1000
+
1001
+ authenticated_record.valid?
608
1002
 
609
- See: [Endpoint paramters and paramter injection during creation](#endpoint-paramters-and-paramter-injection-during-creation)
1003
+ ```
1004
+ ```
1005
+ POST https://service.example.com/records/validate { body: '{...}', headers: { 'Authentication': 'Bearer 123456' } }
1006
+ ```
610
1007
 
611
- ## Custom setters and getters
1008
+ ```ruby
1009
+ # app/controllers/some_controller.rb
612
1010
 
613
- Sometimes it is the case that you want to have your custom getters and setters and convert the data to a processable format behind the scenes.
614
- The initializer will now use custom setter if one is defined:
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
- module RatingsConversions
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
- class Record < LHS::Record
627
- prepend RatingsConversions
628
- end
1022
+ ```
1023
+ ```
1024
+ DELETE https://service.example.com/records/123 { headers: { 'Authentication': 'Bearer 123456' } }
1025
+ ```
629
1026
 
630
- record = Record.new(ratings: { quality: 3 })
631
- record.ratings # [{ :name=>:quality, :value=>3 }]
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
- If you have an accompanying getter the whole data manipulation would be internal only.
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
- module RatingsConversions
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
- def ratings
646
- super.map { |r| [r[:name], r[:value]] }]
647
- end
648
- end
1048
+ Record.page(3).per(20).where(color: 'blue')
649
1049
 
650
- class Record < LHS::Record
651
- prepend RatingsConversions
652
- end
1050
+ ```
1051
+ ```
1052
+ GET https://service.example.com/records?offset=40&limit=20&color=blue
1053
+ ```
653
1054
 
654
- record = Record.new(ratings: { quality: 3 }) # [{ :name=>:quality, :value=>3 }]
655
- record.ratings # {:quality=>3}
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
- ## Include linked resources
1065
+ The applied pagination strategy depends on whats configured for the particular record: See [Record pagination](#record-pagination)
660
1066
 
661
- When fetching records, you can specify in advance all the linked resources that you want to include in the results. 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.
1067
+ ### Record pagination
662
1068
 
663
- The implementation 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 its glory.
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
- ### `includes_all` for paginated endpoints
1072
+ #### Pagination strategy
666
1073
 
667
- 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`.
1074
+ ##### Pagination strategy: offset (default)
668
1075
 
669
- LHS will ensure that all linked resources are around by loading all pages (parallelized/performance optimized).
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
- customer = Customer.includes_all(contracts: :products).find(1)
1081
+ # app/models/record.rb
673
1082
 
674
- # GET http://datastore/customers/1
675
- # GET http://datastore/customers/1/contracts?limit=100
676
- # GET http://datastore/customers/1/contracts?limit=10&offset=10
677
- # GET http://datastore/customers/1/contracts?limit=10&offset=20
678
- # GET http://datastore/products?limit=100
679
- # GET http://datastore/products?limit=10&offset=10
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
- ### One-Level `includes`
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
- # a claim has a localch_account
689
- claims = Claims.includes(:localch_account).where(place_id: 'huU90mB_6vAfUdVz_uDoyA')
690
- claims.first.localch_account.email # 'test@email.com'
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
- Before include:
694
- ```json
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
- "href" : "http://datastore/v2/places/huU90mB_6vAfUdVz_uDoyA/claims",
697
- "items" : [
698
- {
699
- "href" : "http://datastore/v2/localch-accounts/6bSss0y93lK0MrVsgdNNdg/claims/huU90mB_6vAfUdVz_uDoyA",
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
- After include:
709
- ```json
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
- "href" : "http://datastore/v2/places/huU90mB_6vAfUdVz_uDoyA/claims",
712
- "items" : [
713
- {
714
- "href" : "http://datastore/v2/localch-accounts/6bSss0y93lK0MrVsgdNNdg/claims/huU90mB_6vAfUdVz_uDoyA",
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
- ### Two-Level `includes`
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
- # a record has a association, which has an entry
731
- records = Record.includes(association: :entry).where(has_reviews: true)
732
- records.first.association.entry.name # 'Casa Ferlin'
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
- ### Multiple `includes`
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
- # list of includes
739
- claims = Claims.includes(:localch_account, :entry).where(place_id: 'huU90mB_6vAfUdVz_uDoyA')
1211
+ # app/models/record.rb
740
1212
 
741
- # array of includes
742
- claims = Claims.includes([:localch_account, :entry]).where(place_id: 'huU90mB_6vAfUdVz_uDoyA')
1213
+ class Record < LHS::Record
1214
+ configuration pagination_key: { body: [:pagination, :page], parameter: :page }, pagination_strategy: :page
743
1215
 
744
- # Two-level with array of includes
745
- records = Record.includes(campaign: [:entry, :user]).where(has_reviews: true)
1216
+ endpoint '{+service}/records'
1217
+ end
746
1218
  ```
747
1219
 
748
- ### Known LHS::Records are used to request linked resources
1220
+ ```ruby
1221
+ # app/controllers/some_controller.rb
749
1222
 
750
- When including linked resources with `includes`, known/defined services and endpoints are used to make those requests.
751
- That also means that options for endpoints of linked resources are applied when requesting those in addition.
752
- This allows you to include protected resources (e.g. Basic auth) as endpoint options for oauth authentication get applied.
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
- The [Auth Inteceptor](https://github.com/local-ch/lhc-core-interceptors#auth-interceptor) from [lhc-core-interceptors](https://github.com/local-ch/lhc-core-interceptors) is used to configure the following endpoints.
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
- class Favorite < LHS::Record
1241
+ # app/models/record.rb
758
1242
 
759
- endpoint '{+service}/{user_id}/favorites', auth: { basic: { username: 'steve', password: 'can' } }
760
- endpoint '{+service}/{user_id}/favorites/:id', auth: { basic: { username: 'steve', password: 'can' } }
1243
+ class Record < LHS::Record
1244
+ configuration total_key: [:pagination, :total]
761
1245
 
1246
+ endpoint '{+service}/records'
762
1247
  end
1248
+ ```
763
1249
 
764
- class Place < LHS::Record
1250
+ ```ruby
1251
+ # app/controllers/some_controller.rb
765
1252
 
766
- endpoint '{+service}/v2/places', auth: { basic: { username: 'steve', password: 'can' } }
767
- endpoint '{+service}/v2/places/{id}', auth: { basic: { username: 'steve', password: 'can' } }
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
- end
1262
+ #### Pagination links
770
1263
 
771
- Favorite.includes(:place).where(user_id: current_user.id)
772
- # Will include places and applies endpoint options to authenticate the request.
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
- ### Forward options used for request made to include referenced resources
1281
+ - if @records.next?
1282
+ = render partial: 'next_arrow'
1283
+ ```
776
1284
 
777
- Provide options to the requests made to include referenced resources:
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
- Favorite.includes(:place).references(place: { auth: { bearer: '123' }})
1299
+ ```ruby
1300
+ # app/views/some_view.haml
782
1301
 
1302
+ - if @records.previous?
1303
+ = render partial: 'previous_arrow'
783
1304
  ```
784
1305
 
785
- ## Map data
1306
+ #### Kaminari support (limited)
786
1307
 
787
- To influence how data is accessed/provided, you can use mappings to either map deep nested data or to manipulate data when its accessed. Simply create methods inside the LHS::Record. They can access underlying data:
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
- class LocalEntry < LHS::Record
791
- endpoint ':service/v2/local-entries'
1313
+ # controller
1314
+ @items = Record.page(params[:page]).per(100)
1315
+ ```
792
1316
 
793
- def name
794
- addresses.first.business.identities.first.name
795
- end
1317
+ ```ruby
1318
+ # view
1319
+ = paginate @items
1320
+ ```
796
1321
 
797
- end
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
- ## Nested records
1346
+ ###### Unwrap nested data when creation response nests created record data
801
1347
 
802
- Nested records (in nested data) are automatically casted when the href matches any defined endpoint of any LHS::Record.
1348
+ `item_created_key` key used to merge record data thats nested in the creation response body:
803
1349
 
804
1350
  ```ruby
805
- class Place < LHS::Record
806
- endpoint '{+service}/v2/places'
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
- class Favorite < LHS::Record
814
- endpoint '{+service}/v2/favorites'
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
- If automatic-detection of nested records does not work, make sure your LHS::Records are stored in `app/models`!
1382
+ ```ruby
1383
+ # app/models/feedback.rb
822
1384
 
823
- ## Setters
1385
+ class Feedback < LHS::Record
1386
+ endpoint '{+service}/restaurants/{restaurant_id}/feedbacks'
1387
+ end
824
1388
 
825
- You can change attributes of LHS::Records:
1389
+ ```
826
1390
 
827
1391
  ```ruby
828
- record = Record.find(id: 'z12f-3asm3ngals')
829
- rcord.recommended = false
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
- ## Save
1412
+ #### Start building new records
833
1413
 
834
- You can persist changes with `save`. `save` will return `false` if persisting fails. `save!` instead will raise an exception.
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
- record = Record.find('1z-5r1fkaj')
838
- record.recommended = false
839
- record.save
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
- ## Update
1427
+ #### Change/Update existing records
843
1428
 
844
- `update` will return false if persisting fails. `update!` instead will an raise exception.
1429
+ ##### save
845
1430
 
846
- `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.
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
- record.update(recommended: false)
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
- ### Endpoint paramters and paramter injection during updates
1459
+ ##### update
854
1460
 
855
- LHS injects body parameters to generate target urls, used for update requests:
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
- class Customer << LHS::Record
859
- endpoint '{+customers}/{id}'
860
- end
1468
+ # app/controllers/some_controller.rb
861
1469
 
862
- customer = Customer.find(123)
863
- # GET http://customers/123
864
- # { id: '123', name: 'My old company name' }
1470
+ record = Record.find('1z-5r1fkaj')
865
1471
 
866
- customer.update(name: 'My new company name')
867
- # POST http://customers/123
868
- # body: { "name": 'My new company name' }
1472
+ ```
1473
+ ```
1474
+ GET https://service.example.com/records/1z-5r1fkaj
1475
+ { name: 'Starbucks', recommended: null }
869
1476
  ```
870
1477
 
871
- Because API's usually reject body paramters for primary identifiers:
1478
+ ```ruby
1479
+ # app/controllers/some_controller.rb
1480
+
1481
+ record.update(recommended: true)
872
1482
 
873
1483
  ```
874
- Not allowed to change primary id!
1484
+ ```
1485
+ POST https://service.example.com/records/1z-5r1fkaj { body: "{ 'name': 'Starbucks', 'recommended': true }" }
875
1486
  ```
876
1487
 
877
- We remove it from the body, if the information was instead transported through the URL.
1488
+ -> See [record validation](#record-validation) for how to handle validation errors when updating records.
878
1489
 
879
- ## Partial Update
1490
+ ##### partial_update
880
1491
 
881
- Often you just want to update a single attribute on an existing record. As ActiveRecord's `update_attribute` skips validation, which is unlikely with api services, and `update_attributes` is just an alias for `update`, LHS introduces `partial_update` for that matter.
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
- record.partial_update(recommended: false)
890
- # POST /records/1z-5r1fkaj
891
- {
892
- recommended: true
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
- ## Becomes
1582
+ The functionalities of `LHS::Errors` pretty much follow those of `ActiveModel::Validation`:
897
1583
 
898
- Based on [ActiveRecord's implementation](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-becomes), LHS implements `becomes`, too.
899
- It's a way to convert records of a certain type A to another certain type B.
1584
+ ```ruby
1585
+ # app/views/some_view.haml
900
1586
 
901
- _NOTE: RPC-style actions, that are discouraged in REST anyway, are utilizable with this functionality, too. See the following example:_
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
- class Location < LHS::Record
905
- endpoint 'http://sync/locations'
906
- endpoint 'http://sync/locations/{id}'
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
- class Synchronization < LHS::Record
910
- endpoint 'http://sync/locations/{id}/sync'
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
- location = Location.find(1)
914
- synchronization = location.becomes(Synchronization)
915
- synchronization.save!
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
- ## Destroy
1770
+ ### Destroy records
1771
+
1772
+ `destroy` deletes a record.
1773
+
1774
+ ```ruby
1775
+ # app/controllers/some_controller.rb
919
1776
 
920
- You can delete records remotely by calling `destroy` on an LHS::Record.
1777
+ record = Record.find('1z-5r1fkaj')
1778
+ ```
1779
+ ```
1780
+ GET https://service.example.com/records/1z-5r1fkaj
1781
+ ```
921
1782
 
922
1783
  ```ruby
923
- record = Record.find('1z-5r1fkaj')
924
- record.destroy
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
- destroyed_record = Record.destroy('1z-5r1fkaj')
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
- destroyed_records = Record.destroy(name: 'Steve')
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
- ## Validation
1814
+ ### Record getters and setters
940
1815
 
941
- In order to validate LHS::Records before persisting them, you can use the `valid?` (`validate` alias) method.
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
- It's not recommended to validate records anywhere but with the endpoint that also provides to create them.
1818
+ #### Record setters
944
1819
 
945
- The specific endpoint has to support validations without persistence. An endpoint has to be enabled (opt-in) for validations in the service configuration.
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
- class User < LHS::Record
949
- endpoint '{+service}/v2/users', validates: { params: { persist: false } }
950
- end
1823
+ # app/models/user.rb
951
1824
 
952
- user = User.build(email: 'i\'m not an email address')
953
- unless user.valid?
954
- fail(user.errors[:email])
955
- end
1825
+ class Feedback < LHS::Record
956
1826
 
957
- user.errors #<LHS::Problems::Errors>
958
- user.errors.include?(:email) # true
959
- user.errors[:email] # ['REQUIRED_PROPERTY_VALUE']
960
- user.errors.messages # {:email=>["Translated error message that this value is required"]}
961
- user.errors.codes # {:email=>["REQUIRED_PROPERTY_VALUE"]}
962
- user.errors.message # email must be set when user is created."
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
- endpoint '{+service}/v2/users', validates: { params: { persist: false } } # will add ?persist=false to the request
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
- ```ruby
979
- record.save
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
- ### Reset validation errors
1842
+ #### Record getters
984
1843
 
985
- Clear the error messages. Compatible with [ActiveRecord](https://github.com/rails/rails/blob/6c8cf21584ced73ade45529d11463c74b5a0c58f/activemodel/lib/active_model/errors.rb#L85).
1844
+ If you implement accompanying getter methods, the whole data conversion would be internal only:
986
1845
 
987
1846
  ```ruby
988
- record.errors.clear
989
- ```
1847
+ # app/models/user.rb
990
1848
 
991
- ### Custom validation errors
1849
+ class Feedback < LHS::Record
992
1850
 
993
- In case you want to add custom validation errors to an instance of LHS::Record:
1851
+ def ratings=(values)
1852
+ super(
1853
+ values.map { |k, v| { name: k, value: v } }
1854
+ )
1855
+ end
994
1856
 
995
- ```ruby
996
- user.errors.add(:name, 'The name you provided is not valid.')
1857
+ def ratings
1858
+ super.map { |r| [r[:name], r[:value]] }]
1859
+ end
1860
+ end
997
1861
  ```
998
1862
 
999
- ### Validation errors for nested data
1863
+ ```ruby
1864
+ # app/controllers/some_controller.rb
1000
1865
 
1001
- If you work with complex data structures, you sometimes need to have validation errors delegated/scoped to nested data.
1866
+ record = Record.new(ratings: { quality: 3 })
1867
+ record.ratings # {:quality=>3}
1868
+ ```
1002
1869
 
1003
- This also makes LHS::Records compatible with how Rails or Simpleform renders/builds forms and especially error messages.
1870
+ ### Include linked resources (hyperlinks and hypermedia)
1004
1871
 
1005
- ```ruby
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
- # view.html
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
- = fields_for 'customer[:address]', @customer.address, do |address_form|
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
- = fields_for 'customer[:address][:street]', @customer.address.street, do |street_form|
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
- = street_form.input :name
1019
- = street_form.input :house_number
1020
- ```
1880
+ #### Ensure the whole linked collection is included: includes_all
1021
1881
 
1022
- Would render nested forms and would also render nested form errors for nested data structures.
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
- You can also access those nested errors like:
1884
+ LHS will ensure that all linked resources are around by loading all pages (parallelized/performance optimized).
1025
1885
 
1026
1886
  ```ruby
1027
- @customer.address.errors
1028
- @customer.address.street.errors
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
- ### Translation of validation errors
1908
+ ```ruby
1909
+ # app/controllers/some_controller.rb
1032
1910
 
1033
- Just like Activerecord, LHS tries to translate validation error messages.
1034
- If a translation exists for one of the following translation keys, LHS will take a translated error (also in the following order) rather than the plain error message/code:
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
- ### Know issue with `ActiveModel::Validations`
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
- The fact that records could have errors is not coupled to any response status code.
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
- LHS makes errors accessible, if they are present:
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
- LHS makes those errors available when accessing `.errors`:
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
- presence = Presence.create(
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
- ### Non blocking validation errors, so called warnings
1940
+ customer.contracts.length # 10
1941
+ customer.contracts.first.products.first.name # Local Business Card
1076
1942
 
1077
- In some cases, you need non blocking meta information about potential problems with the created record, so called warnings.
1943
+ ```
1078
1944
 
1079
- If the API endpoint implements warnings:
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
- LHS makes those warnings available:
1949
+ Some examples:
1092
1950
 
1093
1951
  ```ruby
1094
- presence = Presence.options(params: { synchronize: false }).create(
1095
- place: { href: 'http://storage/places/1' }
1096
- )
1952
+ Record.includes(:localch_account, :entry)
1953
+ # Includes localch_account -> entry
1954
+ # { localch_account: { href: '...', entry: { href: '...' } } }
1097
1955
 
1098
- presence.warnings.any? # true
1099
- presence.place.photos[0].warnings.messages.first # 'The photos will be resized'
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
- Warnings behave like [Validation Errors](#Validation) and implements the same interfaces and methods.
1965
+ #### Identify and cast known records when including records
1103
1966
 
1104
- ## Pagination
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
- LHS supports paginated APIs and it also supports various pagination strategies and by providing configuration possibilities.
1969
+ That also means that options for endpoints of linked resources are applied when requesting those in addition.
1107
1970
 
1108
- LHS differentiates between the *pagination strategy* (how items/pages are navigated) itself and *pagination keys* (how stuff is named).
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
- # API response
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
- # Nothing has to be configured in LHS because this is default pagination naming and strategy
1122
- class Results < LHS::Record
1123
- endpoint 'results'
1124
- end
1125
- ```
1976
+ class Favorite < LHS::Record
1126
1977
 
1127
- *Example 2 "page"-strategy and some naming configuration*
1128
- ```ruby
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
- # API response
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
- In case of paginated resources it's important to know the difference between [count vs. length](#count-vs-length)
1987
+ class Place < LHS::Record
1164
1988
 
1165
- ## Configuration of Records
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
- `item_key` key used to unwrap the actual object from within the response body.
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
- In case the `limit_key` parameter differs for where it's located in the body and how it's provided as get parameter, when retreiving pages, provide a hash with `body` and `parameter` key, to keep those two use cases separated:
1998
+ Favorite.includes(:place).where(user_id: current_user.id)
1183
1999
 
1184
- ```ruby
1185
- configuration limit_key: { body: [:response, :max], parameter: :max }
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
- `pagination_key` key used to paginate multiple pages (e.g. `offset`, `page`, `startAt` etc.).
2011
+ #### Apply options for requests performed to fetch included records
1189
2012
 
1190
- In case the `pagination_key` parameter differs for where it's located in the body and how it's provided as get parameter, when retreiving pages, provide a hash with `body` and `parameter` key, to keep those two use cases separated:
2013
+ Use `references` to apply request options to requests performed to fetch included records:
1191
2014
 
1192
2015
  ```ruby
1193
- configuration pagination_key: { body: [:response, :page], parameter: :page }
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
- `pagination_strategy` used to configure the strategy used for navigating (e.g. `offset`, `page`, `start`, etc.).
2029
+ ### Record batch processing
1197
2030
 
1198
- `total_key` key used to determine the total amount of items (e.g. `total`, `totalResults`, etc.).
2031
+ **Be careful using methods for batch processing. They could result in a lot of HTTP requests!**
1199
2032
 
1200
- ### Unwrap nested items
2033
+ #### all
1201
2034
 
1202
- ```json
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
- class Location < LHS::Record
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
- response: {
1227
- offset: 0,
1228
- max: 50,
1229
- count: 1,
1230
- businesses: [
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
- class Business < LHS::Record
1238
- configuration items_key: [:response, :businesses], limit_key: [:response, :max], pagination_key: [:response, :offset], total_key: [:response, :count], pagination_strategy: :offset
1239
- endpoint 'http://uberall/businesses'
1240
- end
2062
+ Record.where(color: 'blue').all
2063
+ Record.all.where(color: 'blue')
2064
+ Record.all(color: 'blue')
1241
2065
  ```
1242
2066
 
1243
- If record data after creation is nested in the response body, configure the record, so that it gets properl merged with the your record instance:
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
- ```ruby
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
- business = Business.create(name: 'localsearch')
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
- ### Pagination Chains
2073
+ #### find_each
1265
2074
 
1266
- You can use chainable pagination in combination with query chains:
2075
+ `find_each` is a more fine grained way to process single records that are fetched in batches.
1267
2076
 
1268
2077
  ```ruby
1269
- class Record < LHS::Record
1270
- endpoint ':service/records'
1271
- end
1272
- Record.page(3).per(20).where(color: 'blue')
1273
- # /records?offset=40&limit=20&color=blue
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
- The applied pagination strategy depends on the actual configured pagination, so the interface is the same for all strategies:
2085
+ #### find_in_batches
1277
2086
 
1278
- ```ruby
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
- class Record < LHS::Record
1289
- endpoint '{+service}/records'
1290
- configuration pagination_strategy: 'start'
1291
- end
1292
- Record.page(3).per(20).where(color: 'blue')
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
- `limit(argument)` is an alias for `per(argument)`. Take notice that `limit` without argument instead, makes the query resolve and provides the current limit from the responds.
2097
+ ### Convert/Cast specific record types: becomes
1297
2098
 
1298
- ### Partial Kaminari support
2099
+ Based on [ActiveRecord's implementation](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-becomes), LHS implements `becomes`, too.
1299
2100
 
1300
- LHS implements an interface that makes it partially working with Kaminari.
2101
+ It's a way to convert records of a certain type A to another certain type B.
1301
2102
 
1302
- 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:
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
- # controller
1306
- @items = Record.page(params[:page]).per(100)
1307
- ```
2106
+ # app/models/location.rb
1308
2107
 
1309
- ```ruby
1310
- # view
1311
- = paginate @items
2108
+ class Location < LHS::Record
2109
+ endpoint '{+service}/locations'
2110
+ endpoint '{+service}/locations/{id}'
2111
+ end
1312
2112
  ```
1313
2113
 
1314
- ### Pagination Links
2114
+ ```ruby
2115
+ # app/models/synchronization.rb
1315
2116
 
1316
- When endpoints provide indicators for current page position with links (like `next` and `previous`), LHS provides some functionalities to interact/use those links/information:
2117
+ class Synchronization < LHS::Record
2118
+ endpoint '{+service}/locations/{id}/sync'
2119
+ end
2120
+ ```
1317
2121
 
1318
- `next?` Tells you if there is a next link or not.
2122
+ ```ruby
2123
+ # app/controllers/some_controller.rb
1319
2124
 
1320
- `previous?` Tells you if there is a previous link or not.
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
- ## Automatic Detection of Collections
2134
+ synchronization = location.becomes(Synchronization)
2135
+ synchronization.save!
2136
+ ```
2137
+ ```
2138
+ POST https://service.example.com/location/1/sync { body: '{ ... }' }
2139
+ ```
1324
2140
 
1325
- How to configure endpoints for automatic collection detection?
2141
+ ## Request Cycle Cache
1326
2142
 
1327
- LHS detects automatically if the responded data is a single business object or a set of business objects (collection).
2143
+ By default, LHS does not perform the same http request multiple times during one request/response cycle.
1328
2144
 
1329
- 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.
2145
+ ```ruby
2146
+ # app/models/user.rb
1330
2147
 
1331
- In case the responds uses another key than `items`, you can configure it within an `LHS::Record`:
2148
+ class User < LHS::Record
2149
+ endpoint '{+service}/users/{id}'
2150
+ end
2151
+ ```
1332
2152
 
1333
2153
  ```ruby
1334
- class Results < LHS::Record
1335
- configuration items_key: 'docs'
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
- <%= form_for(@instance, url: '/create') do |f| %>
1343
- <%= f.text_field :name %>
1344
- <%= f.text_area :text %>
1345
- <%= f.submit "Create" %>
1346
- <% end %>
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
- ## Count vs. Length
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
- The behavior of `count` and `length` is based on ActiveRecord's behavior.
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
- `count` Determine the number of elements by taking the number of total elements that is provided by the endpoint/api.
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
- `length` This returns the number of elements loaded from an endpoint/api. In case of paginated resources this can be different to count, as it depends on how many pages have been loaded.
2183
+ ### Change store for LHS' request cycle cache
1356
2184
 
1357
- ## Inheritance
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
- You can inherit from previously defined records and also inherit endpoints that way:
2187
+ ```ruby
2188
+ # config/initializers/lhc.rb
1360
2189
 
1361
- ```
1362
- class Base < LHS::Record
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
- class Example < Base
1367
- end
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
- Example.find(1) # GET records/1
2202
+ LHC.configure do |config|
2203
+ config.request_cycle_cache_enabled = false
2204
+ end
1370
2205
  ```
1371
2206
 
1372
- ## Testing: How to write tests when using LHS
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
- Best practice is to let LHS fetch your records and Webmock to stub/mock endpoints responses.
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(:each) do
1390
- stub_request(:get, "http://datastore/user/:id/contracts")
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
- ## Test support (caching)
2244
+ ### Test helper for request cycle cache
1410
2245
 
1411
- Add to your spec_helper.rb:
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
- require 'lhs/test/request_cycle_cache_helper'
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
- ## Where values hash
2256
+ ### Test query chains
2257
+
2258
+ #### By explicitly resolving the chain: fetch
1420
2259
 
1421
- Returns a hash of where conditions.
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
- # will fail as no http request is made (no data requested)
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
+