lhs 24.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. checksums.yaml +7 -0
  2. data/.bundler-version +1 -0
  3. data/.gitignore +39 -0
  4. data/.rubocop.localch.yml +325 -0
  5. data/.rubocop.yml +52 -0
  6. data/.ruby-version +1 -0
  7. data/Gemfile +4 -0
  8. data/Gemfile.activesupport4 +5 -0
  9. data/Gemfile.activesupport5 +4 -0
  10. data/LICENSE +674 -0
  11. data/README.md +2836 -0
  12. data/Rakefile +25 -0
  13. data/cider-ci.yml +6 -0
  14. data/cider-ci/bin/bundle +51 -0
  15. data/cider-ci/bin/ruby_install +8 -0
  16. data/cider-ci/bin/ruby_version +25 -0
  17. data/cider-ci/jobs/rspec-activesupport-4.yml +27 -0
  18. data/cider-ci/jobs/rspec-activesupport-5.yml +26 -0
  19. data/cider-ci/jobs/rspec-activesupport-latest.yml +24 -0
  20. data/cider-ci/jobs/rubocop.yml +18 -0
  21. data/cider-ci/task_components/bundle.yml +22 -0
  22. data/cider-ci/task_components/rspec.yml +37 -0
  23. data/cider-ci/task_components/rubocop.yml +29 -0
  24. data/cider-ci/task_components/ruby.yml +15 -0
  25. data/friday.yml +2 -0
  26. data/lhs.gemspec +43 -0
  27. data/lib/lhs.rb +100 -0
  28. data/lib/lhs/collection.rb +84 -0
  29. data/lib/lhs/complex.rb +158 -0
  30. data/lib/lhs/concerns/autoload_records.rb +55 -0
  31. data/lib/lhs/concerns/collection/handle_nested.rb +43 -0
  32. data/lib/lhs/concerns/collection/internal_collection.rb +49 -0
  33. data/lib/lhs/concerns/configuration.rb +20 -0
  34. data/lib/lhs/concerns/data/becomes.rb +18 -0
  35. data/lib/lhs/concerns/data/equality.rb +14 -0
  36. data/lib/lhs/concerns/data/json.rb +14 -0
  37. data/lib/lhs/concerns/data/to_hash.rb +14 -0
  38. data/lib/lhs/concerns/inspect.rb +70 -0
  39. data/lib/lhs/concerns/is_href.rb +15 -0
  40. data/lib/lhs/concerns/item/destroy.rb +38 -0
  41. data/lib/lhs/concerns/item/endpoint_lookup.rb +27 -0
  42. data/lib/lhs/concerns/item/save.rb +55 -0
  43. data/lib/lhs/concerns/item/update.rb +50 -0
  44. data/lib/lhs/concerns/item/validation.rb +61 -0
  45. data/lib/lhs/concerns/o_auth.rb +25 -0
  46. data/lib/lhs/concerns/option_blocks.rb +26 -0
  47. data/lib/lhs/concerns/proxy/accessors.rb +132 -0
  48. data/lib/lhs/concerns/proxy/create.rb +45 -0
  49. data/lib/lhs/concerns/proxy/link.rb +25 -0
  50. data/lib/lhs/concerns/proxy/problems.rb +27 -0
  51. data/lib/lhs/concerns/record/attribute_assignment.rb +25 -0
  52. data/lib/lhs/concerns/record/batch.rb +40 -0
  53. data/lib/lhs/concerns/record/chainable.rb +465 -0
  54. data/lib/lhs/concerns/record/configuration.rb +103 -0
  55. data/lib/lhs/concerns/record/create.rb +24 -0
  56. data/lib/lhs/concerns/record/custom_setters.rb +22 -0
  57. data/lib/lhs/concerns/record/destroy.rb +18 -0
  58. data/lib/lhs/concerns/record/endpoints.rb +108 -0
  59. data/lib/lhs/concerns/record/equality.rb +14 -0
  60. data/lib/lhs/concerns/record/find.rb +86 -0
  61. data/lib/lhs/concerns/record/find_by.rb +38 -0
  62. data/lib/lhs/concerns/record/first.rb +20 -0
  63. data/lib/lhs/concerns/record/href_for.rb +19 -0
  64. data/lib/lhs/concerns/record/last.rb +27 -0
  65. data/lib/lhs/concerns/record/mapping.rb +25 -0
  66. data/lib/lhs/concerns/record/merge.rb +26 -0
  67. data/lib/lhs/concerns/record/model.rb +23 -0
  68. data/lib/lhs/concerns/record/pagination.rb +47 -0
  69. data/lib/lhs/concerns/record/provider.rb +23 -0
  70. data/lib/lhs/concerns/record/relations.rb +26 -0
  71. data/lib/lhs/concerns/record/request.rb +620 -0
  72. data/lib/lhs/concerns/record/scope.rb +25 -0
  73. data/lib/lhs/concerns/record/tracing.rb +24 -0
  74. data/lib/lhs/concerns/record/update.rb +17 -0
  75. data/lib/lhs/config.rb +24 -0
  76. data/lib/lhs/data.rb +165 -0
  77. data/lib/lhs/endpoint.rb +12 -0
  78. data/lib/lhs/interceptors/auto_oauth/interceptor.rb +33 -0
  79. data/lib/lhs/interceptors/auto_oauth/thread_registry.rb +18 -0
  80. data/lib/lhs/interceptors/extended_rollbar/handler.rb +40 -0
  81. data/lib/lhs/interceptors/extended_rollbar/interceptor.rb +20 -0
  82. data/lib/lhs/interceptors/extended_rollbar/thread_registry.rb +19 -0
  83. data/lib/lhs/interceptors/request_cycle_cache/interceptor.rb +41 -0
  84. data/lib/lhs/interceptors/request_cycle_cache/thread_registry.rb +18 -0
  85. data/lib/lhs/item.rb +59 -0
  86. data/lib/lhs/pagination/base.rb +90 -0
  87. data/lib/lhs/pagination/link.rb +21 -0
  88. data/lib/lhs/pagination/offset.rb +22 -0
  89. data/lib/lhs/pagination/page.rb +18 -0
  90. data/lib/lhs/pagination/start.rb +22 -0
  91. data/lib/lhs/problems/base.rb +113 -0
  92. data/lib/lhs/problems/errors.rb +69 -0
  93. data/lib/lhs/problems/nested/base.rb +54 -0
  94. data/lib/lhs/problems/nested/errors.rb +16 -0
  95. data/lib/lhs/problems/nested/warnings.rb +15 -0
  96. data/lib/lhs/problems/warnings.rb +24 -0
  97. data/lib/lhs/proxy.rb +68 -0
  98. data/lib/lhs/railtie.rb +34 -0
  99. data/lib/lhs/record.rb +112 -0
  100. data/lib/lhs/rspec.rb +10 -0
  101. data/lib/lhs/test/stubbable_records.rb +34 -0
  102. data/lib/lhs/unprocessable.rb +6 -0
  103. data/lib/lhs/version.rb +5 -0
  104. data/script/ci/build.sh +18 -0
  105. data/spec/auto_oauth_spec.rb +169 -0
  106. data/spec/autoloading_spec.rb +48 -0
  107. data/spec/collection/accessors_spec.rb +31 -0
  108. data/spec/collection/collection_items_spec.rb +44 -0
  109. data/spec/collection/configurable_spec.rb +43 -0
  110. data/spec/collection/delegate_spec.rb +21 -0
  111. data/spec/collection/enumerable_spec.rb +27 -0
  112. data/spec/collection/href_spec.rb +17 -0
  113. data/spec/collection/meta_data_spec.rb +57 -0
  114. data/spec/collection/respond_to_spec.rb +20 -0
  115. data/spec/collection/to_a_spec.rb +34 -0
  116. data/spec/collection/to_ary_spec.rb +40 -0
  117. data/spec/collection/without_object_items_spec.rb +27 -0
  118. data/spec/complex/reduce_spec.rb +202 -0
  119. data/spec/concerns/record/request_spec.rb +78 -0
  120. data/spec/data/collection_spec.rb +56 -0
  121. data/spec/data/equality_spec.rb +23 -0
  122. data/spec/data/inspect_spec.rb +88 -0
  123. data/spec/data/is_item_or_collection_spec.rb +40 -0
  124. data/spec/data/item_spec.rb +106 -0
  125. data/spec/data/merge_spec.rb +27 -0
  126. data/spec/data/parent_spec.rb +39 -0
  127. data/spec/data/raw_spec.rb +48 -0
  128. data/spec/data/respond_to_spec.rb +26 -0
  129. data/spec/data/root_spec.rb +25 -0
  130. data/spec/data/select_spec.rb +27 -0
  131. data/spec/data/to_ary_spec.rb +28 -0
  132. data/spec/data/to_json_spec.rb +68 -0
  133. data/spec/dummy/Rakefile +8 -0
  134. data/spec/dummy/app/assets/images/.keep +0 -0
  135. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  136. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  137. data/spec/dummy/app/controllers/application_controller.rb +26 -0
  138. data/spec/dummy/app/controllers/automatic_authentication_controller.rb +29 -0
  139. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  140. data/spec/dummy/app/controllers/error_handling_with_chains_controller.rb +36 -0
  141. data/spec/dummy/app/controllers/extended_rollbar_controller.rb +10 -0
  142. data/spec/dummy/app/controllers/option_blocks_controller.rb +15 -0
  143. data/spec/dummy/app/controllers/request_cycle_cache_controller.rb +27 -0
  144. data/spec/dummy/app/helpers/application_helper.rb +4 -0
  145. data/spec/dummy/app/mailers/.keep +0 -0
  146. data/spec/dummy/app/models/.keep +0 -0
  147. data/spec/dummy/app/models/concerns/.keep +0 -0
  148. data/spec/dummy/app/models/dummy_customer.rb +6 -0
  149. data/spec/dummy/app/models/dummy_record.rb +6 -0
  150. data/spec/dummy/app/models/dummy_record_with_auto_oauth_provider.rb +6 -0
  151. data/spec/dummy/app/models/dummy_record_with_multiple_oauth_providers1.rb +7 -0
  152. data/spec/dummy/app/models/dummy_record_with_multiple_oauth_providers2.rb +7 -0
  153. data/spec/dummy/app/models/dummy_record_with_multiple_providers_per_endpoint.rb +6 -0
  154. data/spec/dummy/app/models/dummy_record_with_oauth.rb +7 -0
  155. data/spec/dummy/app/models/dummy_user.rb +6 -0
  156. data/spec/dummy/app/models/providers/customer_system.rb +7 -0
  157. data/spec/dummy/app/models/providers/internal_services.rb +7 -0
  158. data/spec/dummy/app/views/error_handling_with_chains/error.html.erb +1 -0
  159. data/spec/dummy/app/views/error_handling_with_chains/show.html.erb +3 -0
  160. data/spec/dummy/app/views/form_for.html.erb +5 -0
  161. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  162. data/spec/dummy/bin/bundle +5 -0
  163. data/spec/dummy/bin/rails +6 -0
  164. data/spec/dummy/bin/rake +6 -0
  165. data/spec/dummy/config.ru +6 -0
  166. data/spec/dummy/config/application.rb +16 -0
  167. data/spec/dummy/config/boot.rb +7 -0
  168. data/spec/dummy/config/environment.rb +7 -0
  169. data/spec/dummy/config/environments/development.rb +36 -0
  170. data/spec/dummy/config/environments/production.rb +77 -0
  171. data/spec/dummy/config/environments/test.rb +40 -0
  172. data/spec/dummy/config/initializers/assets.rb +10 -0
  173. data/spec/dummy/config/initializers/backtrace_silencers.rb +9 -0
  174. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  175. data/spec/dummy/config/initializers/filter_parameter_logging.rb +6 -0
  176. data/spec/dummy/config/initializers/inflections.rb +18 -0
  177. data/spec/dummy/config/initializers/mime_types.rb +6 -0
  178. data/spec/dummy/config/initializers/rollbar.rb +9 -0
  179. data/spec/dummy/config/initializers/session_store.rb +5 -0
  180. data/spec/dummy/config/initializers/wrap_parameters.rb +11 -0
  181. data/spec/dummy/config/locales/en.yml +23 -0
  182. data/spec/dummy/config/routes.rb +27 -0
  183. data/spec/dummy/config/secrets.yml +22 -0
  184. data/spec/dummy/lib/assets/.keep +0 -0
  185. data/spec/dummy/public/404.html +67 -0
  186. data/spec/dummy/public/422.html +67 -0
  187. data/spec/dummy/public/500.html +66 -0
  188. data/spec/dummy/public/favicon.ico +0 -0
  189. data/spec/endpoint/for_url_spec.rb +27 -0
  190. data/spec/extended_rollbar_spec.rb +67 -0
  191. data/spec/item/access_errors_spec.rb +33 -0
  192. data/spec/item/accessors_spec.rb +21 -0
  193. data/spec/item/add_error_spec.rb +21 -0
  194. data/spec/item/becomes_spec.rb +38 -0
  195. data/spec/item/blacklisted_keywords_spec.rb +30 -0
  196. data/spec/item/delegate_spec.rb +32 -0
  197. data/spec/item/destroy_spec.rb +113 -0
  198. data/spec/item/dig_spec.rb +29 -0
  199. data/spec/item/error_codes_spec.rb +55 -0
  200. data/spec/item/errors_spec.rb +324 -0
  201. data/spec/item/fetch_spec.rb +39 -0
  202. data/spec/item/getter_spec.rb +24 -0
  203. data/spec/item/internal_data_structure_spec.rb +37 -0
  204. data/spec/item/map_spec.rb +46 -0
  205. data/spec/item/nested_errors_spec.rb +28 -0
  206. data/spec/item/partial_update_spec.rb +170 -0
  207. data/spec/item/respond_to_spec.rb +31 -0
  208. data/spec/item/save_spec.rb +115 -0
  209. data/spec/item/setter_spec.rb +44 -0
  210. data/spec/item/translate_errors_spec.rb +257 -0
  211. data/spec/item/update_spec.rb +161 -0
  212. data/spec/item/validation_spec.rb +131 -0
  213. data/spec/item/warning_codes_spec.rb +55 -0
  214. data/spec/item/warnings_spec.rb +52 -0
  215. data/spec/option_blocks/ensure_reset_between_requests_spec.rb +23 -0
  216. data/spec/option_blocks/main_spec.rb +55 -0
  217. data/spec/pagination/link/current_page_spec.rb +19 -0
  218. data/spec/pagination/link/pages_left_spec.rb +36 -0
  219. data/spec/pagination/link/parallel_spec.rb +19 -0
  220. data/spec/pagination/link/total_spec.rb +46 -0
  221. data/spec/pagination/offset/pages_left_spec.rb +26 -0
  222. data/spec/pagination/parameters_spec.rb +61 -0
  223. data/spec/proxy/create_sub_resource_spec.rb +182 -0
  224. data/spec/proxy/load_spec.rb +75 -0
  225. data/spec/proxy/record_identification_spec.rb +35 -0
  226. data/spec/rails_helper.rb +13 -0
  227. data/spec/record/all_spec.rb +133 -0
  228. data/spec/record/attribute_assignment_spec.rb +28 -0
  229. data/spec/record/build_spec.rb +26 -0
  230. data/spec/record/cast_nested_data_spec.rb +78 -0
  231. data/spec/record/create_spec.rb +160 -0
  232. data/spec/record/creation_failed_spec.rb +55 -0
  233. data/spec/record/custom_setters_spec.rb +45 -0
  234. data/spec/record/definitions_spec.rb +29 -0
  235. data/spec/record/destroy_spec.rb +38 -0
  236. data/spec/record/dig_configuration_spec.rb +75 -0
  237. data/spec/record/dup_spec.rb +20 -0
  238. data/spec/record/endpoint_inheritance_spec.rb +65 -0
  239. data/spec/record/endpoint_options_spec.rb +52 -0
  240. data/spec/record/endpoint_priorities_spec.rb +26 -0
  241. data/spec/record/endpoints_spec.rb +96 -0
  242. data/spec/record/equality_spec.rb +27 -0
  243. data/spec/record/error_handling_integration_spec.rb +25 -0
  244. data/spec/record/error_handling_spec.rb +40 -0
  245. data/spec/record/expanded_spec.rb +69 -0
  246. data/spec/record/fetch_spec.rb +41 -0
  247. data/spec/record/find_by_chains_spec.rb +21 -0
  248. data/spec/record/find_by_spec.rb +76 -0
  249. data/spec/record/find_each_spec.rb +57 -0
  250. data/spec/record/find_in_batches_spec.rb +122 -0
  251. data/spec/record/find_in_parallel_spec.rb +67 -0
  252. data/spec/record/find_spec.rb +104 -0
  253. data/spec/record/first_spec.rb +39 -0
  254. data/spec/record/force_merge_spec.rb +56 -0
  255. data/spec/record/handle_includes_errors_spec.rb +32 -0
  256. data/spec/record/has_many_spec.rb +120 -0
  257. data/spec/record/has_one_spec.rb +116 -0
  258. data/spec/record/href_for_spec.rb +25 -0
  259. data/spec/record/ignore_errors_spec.rb +139 -0
  260. data/spec/record/immutable_chains_spec.rb +22 -0
  261. data/spec/record/includes_first_page_spec.rb +737 -0
  262. data/spec/record/includes_spec.rb +693 -0
  263. data/spec/record/includes_warning_spec.rb +46 -0
  264. data/spec/record/item_key_spec.rb +81 -0
  265. data/spec/record/items_created_key_configuration_spec.rb +37 -0
  266. data/spec/record/last_spec.rb +68 -0
  267. data/spec/record/loading_twice_spec.rb +19 -0
  268. data/spec/record/mapping_spec.rb +102 -0
  269. data/spec/record/model_name_spec.rb +17 -0
  270. data/spec/record/new_spec.rb +106 -0
  271. data/spec/record/options_getter_spec.rb +26 -0
  272. data/spec/record/options_spec.rb +164 -0
  273. data/spec/record/paginatable_collection_spec.rb +360 -0
  274. data/spec/record/pagination_chain_spec.rb +101 -0
  275. data/spec/record/pagination_links_spec.rb +72 -0
  276. data/spec/record/pagination_spec.rb +71 -0
  277. data/spec/record/persisted_spec.rb +52 -0
  278. data/spec/record/provider_spec.rb +41 -0
  279. data/spec/record/references_spec.rb +53 -0
  280. data/spec/record/relation_caching_spec.rb +121 -0
  281. data/spec/record/reload_by_id_spec.rb +43 -0
  282. data/spec/record/reload_spec.rb +65 -0
  283. data/spec/record/request_spec.rb +90 -0
  284. data/spec/record/save_spec.rb +40 -0
  285. data/spec/record/scope_chains_spec.rb +39 -0
  286. data/spec/record/select_spec.rb +17 -0
  287. data/spec/record/to_ary_spec.rb +65 -0
  288. data/spec/record/to_hash_spec.rb +22 -0
  289. data/spec/record/to_json_spec.rb +22 -0
  290. data/spec/record/tracing_spec.rb +154 -0
  291. data/spec/record/update_spec.rb +62 -0
  292. data/spec/record/where_chains_spec.rb +57 -0
  293. data/spec/record/where_spec.rb +62 -0
  294. data/spec/record/where_values_hash_spec.rb +32 -0
  295. data/spec/request_cycle_cache_spec.rb +106 -0
  296. data/spec/require_lhs_spec.rb +9 -0
  297. data/spec/spec_helper.rb +6 -0
  298. data/spec/stubs/all_spec.rb +72 -0
  299. data/spec/support/fixtures/json/feedback.json +11 -0
  300. data/spec/support/fixtures/json/feedbacks.json +174 -0
  301. data/spec/support/fixtures/json/localina_content_ad.json +23 -0
  302. data/spec/support/load_json.rb +5 -0
  303. data/spec/support/request_cycle_cache.rb +10 -0
  304. data/spec/support/reset.rb +67 -0
  305. data/spec/views/form_for_spec.rb +20 -0
  306. metadata +776 -0
@@ -0,0 +1,2836 @@
1
+ LHS
2
+ ===
3
+
4
+ LHS ia a Rails-Gem, providing an ActiveRecord like interface to access HTTP-JSON-Services from Rails Applications. Special features provided by this gem are: Multiple endpoint configuration per resource, active-record-like query-chains, scopes, error handling, relations, request cycle cache, batch processing, including linked resources (hypermedia), data maps (data accessing), nested-resource handling, ActiveModel like backend validation conversion, formbuilder-compatible, three types of pagination support, service configuration per resource, kaminari-support and much more.
5
+
6
+ LHS uses [LHC](//github.com/local-ch/LHC) for advanced http requests.
7
+
8
+ ## Quickstart
9
+
10
+ ```
11
+ gem 'lhs'
12
+ ```
13
+
14
+ ```ruby
15
+ # config/initializers/lhc.rb
16
+
17
+ LHC.configure do |config|
18
+ config.placeholder(:service, 'https://my.service.dev')
19
+ end
20
+ ```
21
+
22
+ ```ruby
23
+ # app/models/record.rb
24
+
25
+ class Record < LHS::Record
26
+
27
+ endpoint '{+service}/records'
28
+ endpoint '{+service}/records/{id}'
29
+
30
+ end
31
+ ```
32
+
33
+ ```ruby
34
+ # app/controllers/application_controller.rb
35
+
36
+ record = Record.find_by(email: 'somebody@mail.com')
37
+ record.review # "Lunch was great
38
+ ```
39
+
40
+ ## Table of contents
41
+ * [LHS](#lhs)
42
+ * [Quickstart](#quickstart)
43
+ * [Installation/Startup checklist](#installationstartup-checklist)
44
+ * [Record](#record)
45
+ * [Endpoints](#endpoints)
46
+ * [Configure endpoint hosts](#configure-endpoint-hosts)
47
+ * [Endpoint Priorities](#endpoint-priorities)
48
+ * [Provider](#provider)
49
+ * [Record inheritance](#record-inheritance)
50
+ * [Find multiple records](#find-multiple-records)
51
+ * [fetch](#fetch)
52
+ * [where](#where)
53
+ * [Reuse/Dry where statements: Use scopes](#reusedry-where-statements-use-scopes)
54
+ * [all](#all)
55
+ * [all with unpaginated endpoints](#all-with-unpaginated-endpoints)
56
+ * [Retrieve the amount of a collection of items: count vs. length](#retrieve-the-amount-of-a-collection-of-items-count-vs-length)
57
+ * [Find single records](#find-single-records)
58
+ * [find](#find)
59
+ * [find_by](#find_by)
60
+ * [first](#first)
61
+ * [last](#last)
62
+ * [Work with retrieved data](#work-with-retrieved-data)
63
+ * [Automatic detection/conversion of collections](#automatic-detectionconversion-of-collections)
64
+ * [Map complex data for easy access](#map-complex-data-for-easy-access)
65
+ * [Access and identify nested records](#access-and-identify-nested-records)
66
+ * [Relations / Associations](#relations--associations)
67
+ * [has_many](#has_many)
68
+ * [has_one](#has_one)
69
+ * [Unwrap nested items from the response body](#unwrap-nested-items-from-the-response-body)
70
+ * [Determine collections from the response body](#determine-collections-from-the-response-body)
71
+ * [Load additional data based on retrieved data](#load-additional-data-based-on-retrieved-data)
72
+ * [Chain complex queries](#chain-complex-queries)
73
+ * [Chain where queries](#chain-where-queries)
74
+ * [Expand plain collections of links: expanded](#expand-plain-collections-of-links-expanded)
75
+ * [Error handling with chains](#error-handling-with-chains)
76
+ * [Resolve chains: fetch](#resolve-chains-fetch)
77
+ * [Add request options to a query chain: options](#add-request-options-to-a-query-chain-options)
78
+ * [Control pagination within a query chain](#control-pagination-within-a-query-chain)
79
+ * [Record pagination](#record-pagination)
80
+ * [Pagination strategy](#pagination-strategy)
81
+ * [Pagination strategy: offset (default)](#pagination-strategy-offset-default)
82
+ * [Pagination strategy: page](#pagination-strategy-page)
83
+ * [Pagination strategy: start](#pagination-strategy-start)
84
+ * [Pagination strategy: link](#pagination-strategy-link)
85
+ * [Pagination keys](#pagination-keys)
86
+ * [limit_key](#limit_key)
87
+ * [pagination_key](#pagination_key)
88
+ * [total_key](#total_key)
89
+ * [Pagination links](#pagination-links)
90
+ * [next?](#next)
91
+ * [previous?](#previous)
92
+ * [Kaminari support (limited)](#kaminari-support-limited)
93
+ * [Build, create and update records](#build-create-and-update-records)
94
+ * [Create new records](#create-new-records)
95
+ * [create](#create)
96
+ * [Unwrap nested data when creation response nests created record data](#unwrap-nested-data-when-creation-response-nests-created-record-data)
97
+ * [Create records through associations: Nested sub resources](#create-records-through-associations-nested-sub-resources)
98
+ * [Start building new records](#start-building-new-records)
99
+ * [Change/Update existing records](#changeupdate-existing-records)
100
+ * [save](#save)
101
+ * [update](#update)
102
+ * [Directly via Record](#directly-via-record)
103
+ * [per Instance](#per-instance)
104
+ * [partial_update](#partial_update)
105
+ * [Endpoint url parameter injection during record creation/change](#endpoint-url-parameter-injection-during-record-creationchange)
106
+ * [Record validation](#record-validation)
107
+ * [Configure record validations](#configure-record-validations)
108
+ * [HTTP Status Codes for validation errors](#http-status-codes-for-validation-errors)
109
+ * [Reset validation errors](#reset-validation-errors)
110
+ * [Add validation errors](#add-validation-errors)
111
+ * [Validation errors for nested data](#validation-errors-for-nested-data)
112
+ * [Translation of validation errors](#translation-of-validation-errors)
113
+ * [Validation error types: errors vs. warnings](#validation-error-types-errors-vs-warnings)
114
+ * [Persistance failed: errors](#persistance-failed-errors)
115
+ * [Persistance succeeded: warnings](#persistance-succeeded-warnings)
116
+ * [Using ActiveModel::Validations none the less](#using-activemodelvalidations-none-the-less)
117
+ * [Use form_helper to create and update records](#use-form_helper-to-create-and-update-records)
118
+ * [Destroy records](#destroy-records)
119
+ * [Record getters and setters](#record-getters-and-setters)
120
+ * [Record setters](#record-setters)
121
+ * [Record getters](#record-getters)
122
+ * [Include linked resources (hyperlinks and hypermedia)](#include-linked-resources-hyperlinks-and-hypermedia)
123
+ * [Generate links from parameters](#generate-links-from-parameters)
124
+ * [Ensure the whole linked collection is included with includes](#ensure-the-whole-linked-collection-is-included-with-includes)
125
+ * [Include only the first linked page of a linked collection: includes_first_page](#include-only-the-first-linked-page-of-a-linked-collection-includes_first_page)
126
+ * [Include various levels of linked data](#include-various-levels-of-linked-data)
127
+ * [Identify and cast known records when including records](#identify-and-cast-known-records-when-including-records)
128
+ * [Apply options for requests performed to fetch included records](#apply-options-for-requests-performed-to-fetch-included-records)
129
+ * [Record batch processing](#record-batch-processing)
130
+ * [all](#all-1)
131
+ * [Using all, when endpoint does not implement response pagination meta data](#using-all-when-endpoint-does-not-implement-response-pagination-meta-data)
132
+ * [find_each](#find_each)
133
+ * [find_in_batches](#find_in_batches)
134
+ * [Convert/Cast specific record types: becomes](#convertcast-specific-record-types-becomes)
135
+ * [Assign attributes](#assign-attributes)
136
+ * [Request Cycle Cache](#request-cycle-cache)
137
+ * [Change store for LHS' request cycle cache](#change-store-for-lhs-request-cycle-cache)
138
+ * [Disable request cycle cache](#disable-request-cycle-cache)
139
+ * [Automatic Authentication (OAuth)](#automatic-authentication-oauth)
140
+ * [Configure multiple auth providers (even per endpoint)](#configure-multiple-auth-providers-even-per-endpoint)
141
+ * [Configure providers](#configure-providers)
142
+ * [Option Blocks](#option-blocks)
143
+ * [Request tracing](#request-tracing)
144
+ * [Extended Rollbar Logging](#extended-rollbar-logging)
145
+ * [Testing with LHS](#testing-with-lhs)
146
+ * [Test helper](#test-helper)
147
+ * [Stub](#stub)
148
+ * [stub_all](#stub_all)
149
+ * [Test query chains](#test-query-chains)
150
+ * [By explicitly resolving the chain: fetch](#by-explicitly-resolving-the-chain-fetch)
151
+ * [Without resolving the chain: where_values_hash](#without-resolving-the-chain-where_values_hash)
152
+ * [License](#license)
153
+
154
+
155
+
156
+
157
+
158
+ ## Installation/Startup checklist
159
+
160
+ - [ ] Install LHS gem, preferably via `Gemfile`
161
+ - [ ] Configure [LHC](https://github.com/local-ch/lhc) via an `config/initializers/lhc.rb` (See: https://github.com/local-ch/lhc#configuration)
162
+ - [ ] Add `LHC::Caching` to `LHC.config.interceptors` to facilitate LHS' [Request Cycle Cache](#request-cycle-cache)
163
+ - [ ] Store all LHS::Records in `app/models` for autoload/preload reasons
164
+ - [ ] Request data from services via `LHS` from within your rails controllers
165
+
166
+ ## Record
167
+
168
+ ### Endpoints
169
+
170
+ > Endpoint, the entry point to a service, a process, or a queue or topic destination in service-oriented architecture
171
+
172
+ Start a record with configuring one or multiple endpoints.
173
+
174
+ ```ruby
175
+ # app/models/record.rb
176
+
177
+ class Record < LHS::Record
178
+
179
+ endpoint '{+service}/records'
180
+ endpoint '{+service}/records/{id}'
181
+ endpoint '{+service}/accociation/{accociation_id}/records'
182
+ endpoint '{+service}/accociation/{accociation_id}/records/{id}'
183
+
184
+ end
185
+ ```
186
+
187
+ You can also add request options to be used with configured endpoints:
188
+
189
+ ```ruby
190
+ # app/models/record.rb
191
+
192
+ class Record < LHS::Record
193
+
194
+ endpoint '{+service}/records', auth: { bearer: -> { access_token } }
195
+ endpoint '{+service}/records/{id}', auth: { bearer: -> { access_token } }
196
+
197
+ end
198
+ ```
199
+
200
+ -> Check [LHC](https://github.com/local-ch/lhc) for more information about request options
201
+
202
+ #### Configure endpoint hosts
203
+
204
+ It's common practice to use different hosts accross different environments in a service-oriented architecture.
205
+
206
+ Use [LHC placeholders](https://github.com/local-ch/lhc#configuring-placeholders) to configure different hosts per environment:
207
+
208
+ ```ruby
209
+ # config/initializers/lhc.rb
210
+
211
+ LHC.configure do |config|
212
+ config.placeholder(:search, ENV['SEARCH'])
213
+ end
214
+ ```
215
+
216
+ ```ruby
217
+ # app/models/record.rb
218
+
219
+ class Record < LHS::Record
220
+
221
+ endpoint '{+search}/api/search.json'
222
+
223
+ end
224
+ ```
225
+
226
+ **DON'T!**
227
+
228
+ Please DO NOT mix host placeholders with and endpoint's resource path, as otherwise LHS will not work properly.
229
+
230
+ ```ruby
231
+ # config/initializers/lhc.rb
232
+
233
+ LHC.configure do |config|
234
+ config.placeholder(:search, 'http://tel.search.ch/api/search.json')
235
+ end
236
+ ```
237
+
238
+ ```ruby
239
+ # app/models/record.rb
240
+
241
+ class Record < LHS::Record
242
+
243
+ endpoint '{+search}'
244
+
245
+ end
246
+ ```
247
+
248
+ #### Endpoint Priorities
249
+
250
+ LHS uses endpoint configurations to determine what endpoint to use when data is requested, in a similiar way, routes are identified in Rails to map requests to controllers.
251
+
252
+ If they are ambiguous, LHS will always use the first one found:
253
+
254
+ ```ruby
255
+ # app/models/record.rb
256
+
257
+ class Record < LHS::Record
258
+
259
+ endpoint '{+service}/records'
260
+ endpoint '{+service}/bananas'
261
+
262
+ end
263
+ ```
264
+
265
+ ```ruby
266
+ # app/controllers/some_controller.rb
267
+
268
+ Record.fetch
269
+ ```
270
+ ```
271
+ GET https://service.example.com/records
272
+ ```
273
+
274
+ **Be aware that, if you configure ambigious endpoints accross multiple classes, the order of things is not deteministic. Ambigious endpoints accross multiple classes need to be avoided.**
275
+
276
+ ### Provider
277
+
278
+ Providers in LHS allow you to group shared endpoint options under a common provider.
279
+
280
+ ```ruby
281
+ # app/models/provider/base_record.rb
282
+
283
+ module Provider
284
+ class BaseRecord < LHS::Record
285
+ provider params: { api_key: 123 }
286
+ end
287
+ end
288
+ ```
289
+
290
+ Now every record, part of that particular provider can inherit the provider's `BaseRecord`.
291
+
292
+ ```ruby
293
+ # app/models/provider/account.rb
294
+
295
+ module Provider
296
+ class Account < BaseRecord
297
+ endpoint '{+host}/records'
298
+ endpoint '{+host}/records/{id}'
299
+ end
300
+ end
301
+ ```
302
+
303
+ ```ruby
304
+ # app/controllers/some_controller.rb
305
+
306
+ Provider::Account.find(1)
307
+ ```
308
+ ```
309
+ GET https://provider/records/1?api_key=123
310
+ ```
311
+
312
+ And requests made via those provider records apply the common provider options.
313
+
314
+ ### Record inheritance
315
+
316
+ You can inherit from previously defined records and also inherit endpoints that way:
317
+
318
+ ```ruby
319
+ # app/models/base.rb
320
+
321
+ class Base < LHS::Record
322
+ endpoint '{+service}/records/{id}'
323
+ end
324
+ ```
325
+
326
+ ```ruby
327
+ # app/models/record.rb
328
+
329
+ class Record < Base
330
+ end
331
+ ```
332
+
333
+ ```ruby
334
+ # app/controllers/some_controller.rb
335
+
336
+ Record.find(1)
337
+ ```
338
+ ```
339
+ GET https://service.example.com/records/1
340
+ ```
341
+
342
+ ### Find multiple records
343
+
344
+ #### fetch
345
+
346
+ 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`:
347
+
348
+ ```ruby
349
+ # app/controllers/some_controller.rb
350
+
351
+ records = Record.fetch
352
+
353
+ ```
354
+ ```
355
+ GET https://service.example.com/records
356
+ ```
357
+
358
+ #### where
359
+
360
+ You can query a service for records by using `where`:
361
+
362
+ ```ruby
363
+ # app/controllers/some_controller.rb
364
+
365
+ Record.where(color: 'blue')
366
+
367
+ ```
368
+ ```
369
+ GET https://service.example.com/records?color=blue
370
+ ```
371
+
372
+ If the provided parameter – `color: 'blue'` in this case – is not part of the endpoint path, it will be added as query parameter.
373
+
374
+ ```ruby
375
+ # app/controllers/some_controller.rb
376
+
377
+ Record.where(accociation_id: '12345')
378
+
379
+ ```
380
+ ```
381
+ GET https://service.example.com/accociation/12345/records
382
+ ```
383
+
384
+ If the provided parameter – `accociation_id` in this case – is part of the endpoint path, it will be injected into the path.
385
+
386
+ You can also provide hrefs to fetch multiple records:
387
+
388
+ ```ruby
389
+ # app/controllers/some_controller.rb
390
+
391
+ Record.where('https://service.example.com/accociation/12345/records')
392
+
393
+ ```
394
+ ```
395
+ GET https://service.example.com/accociation/12345/records
396
+ ```
397
+
398
+
399
+ #### Reuse/Dry where statements: Use scopes
400
+
401
+ In order to reuse/dry where statements organize them in scopes:
402
+
403
+ ```ruby
404
+ # app/models/record.rb
405
+
406
+ class Record < LHS::Record
407
+
408
+ endpoint '{+service}/records'
409
+ endpoint '{+service}/records/{id}'
410
+
411
+ scope :blue, -> { where(color: 'blue') }
412
+ scope :available, ->(state) { where(available: state) }
413
+
414
+ end
415
+ ```
416
+
417
+ ```ruby
418
+ # app/controllers/some_controller.rb
419
+
420
+ records = Record.blue.available(true)
421
+ ```
422
+ ```
423
+ GET https://service.example.com/records?color=blue&available=true
424
+ ```
425
+
426
+ #### all
427
+
428
+ You can fetch all remote records by using `all`. Pagination will be performed automatically (See: [Record pagination](#record-pagination))
429
+
430
+ ```ruby
431
+ # app/controllers/some_controller.rb
432
+
433
+ records = Record.all
434
+
435
+ ```
436
+ ```
437
+ GET https://service.example.com/records?limit=100
438
+ GET https://service.example.com/records?limit=100&offset=100
439
+ GET https://service.example.com/records?limit=100&offset=200
440
+ ```
441
+
442
+ ```ruby
443
+ # app/controllers/some_controller.rb
444
+
445
+ records.size # 300
446
+
447
+ ```
448
+
449
+ #### all with unpaginated endpoints
450
+
451
+ 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:
452
+
453
+ ```ruby
454
+ # app/models/record.rb
455
+
456
+ class Record < LHS::Record
457
+ configuration paginated: false
458
+ end
459
+
460
+ ```
461
+
462
+ ```ruby
463
+ # app/controllers/some_controller.rb
464
+
465
+ records = Record.all
466
+
467
+ ```
468
+ ```
469
+ GET https://service.example.com/records
470
+ ```
471
+
472
+ #### Retrieve the amount of a collection of items: count vs. length
473
+
474
+ The different behavior of `count` and `length` is based on ActiveRecord's behavior.
475
+
476
+ `count` The total number of items available remotly via the provided endpoint/api, communicated via pagination meta data.
477
+
478
+ `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.
479
+
480
+ ### Find single records
481
+
482
+ #### find
483
+
484
+ `find` finds a unique record by unique identifier (usually `id` or `href`). If no record is found an error is raised.
485
+
486
+ ```ruby
487
+ Record.find(123)
488
+ ```
489
+ ```
490
+ GET https://service.example.com/records/123
491
+ ```
492
+
493
+ ```ruby
494
+ Record.find('https://anotherservice.example.com/records/123')
495
+ ```
496
+ ```
497
+ GET https://anotherservice.example.com/records/123
498
+ ```
499
+
500
+ `find` can also be used to find a single unique record with parameters:
501
+
502
+ ```ruby
503
+ Record.find(another_identifier: 456)
504
+ ```
505
+ ```
506
+ GET https://service.example.com/records?another_identifier=456
507
+ ```
508
+
509
+ You can also fetch multiple records by `id` in parallel:
510
+
511
+ ```ruby
512
+ Record.find(1, 2, 3)
513
+ ```
514
+ ```
515
+ # In parallel:
516
+ GET https://service.example.com/records/1
517
+ GET https://service.example.com/records/2
518
+ GET https://service.example.com/records/3
519
+ ```
520
+
521
+ #### find_by
522
+
523
+ `find_by` finds the first record matching the specified conditions. If no record is found, `nil` is returned.
524
+
525
+ `find_by!` raises `LHC::NotFound` if nothing was found.
526
+
527
+ ```ruby
528
+ Record.find_by(color: 'blue')
529
+ ```
530
+ ```
531
+ GET https://service.example.com/records?color=blue
532
+ ```
533
+
534
+ #### first
535
+
536
+ `first` is an alias for finding the first record without parameters. If no record is found, `nil` is returned.
537
+
538
+ `first!` raises `LHC::NotFound` if nothing was found.
539
+
540
+ ```ruby
541
+ Record.first
542
+ ```
543
+ ```
544
+ GET https://service.example.com/records?limit=1
545
+ ```
546
+
547
+ `first` can also be used with options:
548
+
549
+ ```ruby
550
+ Record.first(params: { color: :blue })
551
+ ```
552
+ ```
553
+ GET https://service.example.com/records?color=blue&limit=1
554
+ ```
555
+
556
+ #### last
557
+
558
+ `last` is an alias for finding the last record without parameters. If no record is found, `nil` is returned.
559
+
560
+ `last!` raises `LHC::NotFound` if nothing was found.
561
+
562
+ ```ruby
563
+ Record.last
564
+ ```
565
+
566
+ `last` can also be used with options:
567
+
568
+ ```ruby
569
+ Record.last(params: { color: :blue })
570
+ ```
571
+
572
+ ### Work with retrieved data
573
+
574
+ After fetching [single](#find-single-records) or [multiple](#find-multiple-records) records you can navigate the received data with ease:
575
+
576
+ ```ruby
577
+ records = Record.where(color: 'blue')
578
+ records.length # 4
579
+ records.count # 400
580
+ record = records.first
581
+ record.type # 'Business'
582
+ record[:type] # 'Business'
583
+ record['type'] # 'Business'
584
+ ```
585
+
586
+ #### Automatic detection/conversion of collections
587
+
588
+ How to configure endpoints for automatic collection detection?
589
+
590
+ LHS detects automatically if the responded data is a single business object or a set of business objects (collection).
591
+
592
+ 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.
593
+
594
+ 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)
595
+
596
+ #### Map complex data for easy access
597
+
598
+ To influence how data is accessed, simply create methods inside your Record to access complex data structures:
599
+
600
+ ```ruby
601
+ # app/models/record.rb
602
+
603
+ class Record < LHS::Record
604
+
605
+ endpoint '{+service}/records'
606
+
607
+ def name
608
+ dig(:addresses, :first, :business, :identities, :first, :name)
609
+ end
610
+ end
611
+ ```
612
+
613
+ #### Access and identify nested records
614
+
615
+ 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:
616
+
617
+ ```ruby
618
+ # app/models/place.rb
619
+
620
+ class Place < LHS::Record
621
+ endpoint '{+service}/places'
622
+ endpoint '{+service}/places/{id}'
623
+
624
+ def name
625
+ dig(:addresses, :first, :business, :identities, :first, :name)
626
+ end
627
+ end
628
+ ```
629
+
630
+ ```ruby
631
+ # app/models/favorite.rb
632
+
633
+ class Favorite < LHS::Record
634
+ endpoint '{+service}/favorites'
635
+ endpoint '{+service}/favorites/{id}'
636
+ end
637
+ ```
638
+
639
+ ```ruby
640
+ # app/controllers/some_controller.rb
641
+
642
+ favorite = Favorite.includes(:place).find(123)
643
+ favorite.place.name # local.ch AG
644
+ ```
645
+ ```
646
+ GET https://service.example.com/favorites/123
647
+
648
+ {... place: { href: 'https://service.example.com/places/456' }}
649
+
650
+ GET https://service.example.com/places/456
651
+ ```
652
+
653
+ If automatic detection of nested records does not work, make sure your Records are stored in `app/models`! See: [Insallation/Startup checklist](#installationstartup-checklist)
654
+
655
+ ##### Relations / Associations
656
+
657
+ 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.
658
+ 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.
659
+
660
+ To be able to implement Record-specific logic for those nested records, you can define relations/associations.
661
+
662
+ ###### has_many
663
+
664
+ ```ruby
665
+ # app/models/location.rb
666
+
667
+ class Location < LHS::Record
668
+
669
+ endpoint '{+service}/locations/{id}'
670
+
671
+ has_many :listings
672
+
673
+ end
674
+ ```
675
+
676
+ ```ruby
677
+ # app/models/listing.rb
678
+
679
+ class Listing < LHS::Record
680
+
681
+ def supported?
682
+ type == 'SUPPORTED'
683
+ end
684
+ end
685
+ ```
686
+
687
+ ```ruby
688
+ # app/controllers/some_controller.rb
689
+
690
+ Location.find(1).listings.first.supported? # true
691
+ ```
692
+ ```
693
+ GET https://service.example.com/locations/1
694
+ {... listings: [{ type: 'SUPPORTED' }] }
695
+ ```
696
+
697
+ `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.
698
+
699
+ ```ruby
700
+ # app/models/custom/location.rb
701
+
702
+ module Custom
703
+ class Location < LHS::Record
704
+ endpoint '{+service}/locations'
705
+ endpoint '{+service}/locations/{id}'
706
+
707
+ has_many :photos, class_name: 'Custom::Photo'
708
+ end
709
+ end
710
+ ```
711
+
712
+ ```ruby
713
+ # app/models/custom/photo.rb
714
+
715
+ module Custom
716
+ class Photo < LHS::Record
717
+ end
718
+ end
719
+ ```
720
+
721
+ ###### has_one
722
+
723
+ ```ruby
724
+ # app/models/transaction.rb
725
+
726
+ class Transaction < LHS::Record
727
+
728
+ endpoint '{+service}/transaction/{id}'
729
+
730
+ has_one :user
731
+ end
732
+ ```
733
+
734
+ ```ruby
735
+ # app/models/user.rb
736
+
737
+ class User < LHS::Record
738
+
739
+ def email
740
+ self[:email_address]
741
+ end
742
+ end
743
+ ```
744
+
745
+ ```ruby
746
+ # app/controllers/some_controller.rb
747
+
748
+ Transaction.find(1).user.email_address # steve@local.ch
749
+ ```
750
+ ```
751
+ GET https://service.example.com/transaction/1
752
+ {... user: { email_address: 'steve@local.ch' } }
753
+ ```
754
+
755
+ `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.
756
+
757
+ ```ruby
758
+ # app/models/custom/location.rb
759
+
760
+ module Custom
761
+ class Location < LHS::Record
762
+ endpoint '{+service}/locations'
763
+ endpoint '{+service}/locations/{id}'
764
+
765
+ has_one :photo, class_name: 'Custom::Photo'
766
+ end
767
+ end
768
+ ```
769
+
770
+ ```ruby
771
+ # app/models/custom/photo.rb
772
+
773
+ module Custom
774
+ class Photo < LHS::Record
775
+ end
776
+ end
777
+ ```
778
+
779
+ #### Unwrap nested items from the response body
780
+
781
+ 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.
782
+
783
+ `item_key` is used to unwrap the actual object from within the response body.
784
+
785
+ ```ruby
786
+ # app/models/location.rb
787
+
788
+ class Location < LHS::Record
789
+ configuration item_key: [:response, :location]
790
+ end
791
+ ```
792
+
793
+ ```ruby
794
+ # app/controllers/some_controller.rb
795
+
796
+ location = Location.find(123)
797
+ location.id # 123
798
+ ```
799
+ ```
800
+ GET https://service.example.com/locations/123
801
+ {... response: { location: { id: 123 } } }
802
+ ```
803
+
804
+ #### Determine collections from the response body
805
+
806
+ `items_key` key used to determine the collection of items of the current page (e.g. `docs`, `items`, etc.), defaults to 'items':
807
+
808
+ ```ruby
809
+ # app/models/search.rb
810
+
811
+ class Search < LHS::Record
812
+ configuration items_key: :docs
813
+ end
814
+ ```
815
+
816
+ ```ruby
817
+ # app/controllers/some_controller.rb
818
+
819
+ search_result = Search.where(q: 'Starbucks')
820
+ search_result.first.address # Bahnhofstrasse 5, 8000 Zürich
821
+ ```
822
+ ```
823
+ GET https://service.example.com/search?q=Starbucks
824
+ {... docs: [... {... address: 'Bahnhofstrasse 5, 8000 Zürich' }] }
825
+ ```
826
+
827
+ #### Load additional data based on retrieved data
828
+
829
+ In order to load linked data from already retrieved data, you can use `load!` (or `reload!`).
830
+
831
+ ```ruby
832
+ # app/controllers/some_controller.rb
833
+
834
+ record = Record.find(1)
835
+ record.associated_thing.load!
836
+ ```
837
+ ```
838
+ GET https://things/4
839
+ { name: "Steve" }
840
+ ```
841
+ ```ruby
842
+ # app/controllers/some_controller.rb
843
+ record.associated_thing.name # Steve
844
+
845
+ record.associated_thing.load! # Does NOT create another request, as it is already loaded
846
+ record.associated_thing.reload! # Does request the data again from remote
847
+
848
+ ```
849
+ ```
850
+ GET https://things/4
851
+ { name: "Steve" }
852
+ ```
853
+
854
+ ### Chain complex queries
855
+
856
+ > [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
857
+
858
+ 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.
859
+
860
+ 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.
861
+
862
+ 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:
863
+
864
+ ```ruby
865
+ # app/controllers/some_controller.rb
866
+
867
+ Record.where(color: 'blue').fetch
868
+ ```
869
+
870
+ #### Chain where queries
871
+
872
+ ```ruby
873
+ # app/controllers/some_controller.rb
874
+
875
+ records = Record.where(color: 'blue')
876
+ [...]
877
+ records.where(available: true).each do |record|
878
+ [...]
879
+ end
880
+ ```
881
+ ```
882
+ GET https://service.example.com/records?color=blue&available=true
883
+ ```
884
+
885
+ In case you wan't to check/debug the current values for where in the chain, you can use `where_values_hash`:
886
+
887
+ ```ruby
888
+ records.where_values_hash
889
+
890
+ # {color: 'blue', available: true}
891
+ ```
892
+
893
+ #### Expand plain collections of links: expanded
894
+
895
+ Some endpoints could respond only with a plain list of links and without any expanded data, like search results.
896
+
897
+ Use `expanded` to have LHS expand that data, by performing necessary requests in parallel:
898
+
899
+ ```ruby
900
+ # app/controllers/some_controller.rb
901
+
902
+ Search.where(what: 'Cafe').expanded
903
+ ```
904
+ ```
905
+ GET https://service.example.com/search?what=Cafe
906
+ {...
907
+ "items" : [
908
+ {"href": "https://service.example.com/records/1"},
909
+ {"href": "https://service.example.com/records/2"},
910
+ {"href": "https://service.example.com/records/3"}
911
+ ]
912
+ }
913
+
914
+ In parallel:
915
+ > GET https://service.example.com/records/1
916
+ < {... name: 'Cafe Einstein'}
917
+ > GET https://service.example.com/records/2
918
+ < {... name: 'Starbucks'}
919
+ > GET https://service.example.com/records/3
920
+ < {... name: 'Plaza Cafe'}
921
+
922
+ {
923
+ ...
924
+ "items" : [
925
+ {
926
+ "href": "https://service.example.com/records/1",
927
+ "name": 'Cafe Einstein',
928
+ ...
929
+ },
930
+ {
931
+ "href": "https://service.example.com/records/2",
932
+ "name": 'Starbucks',
933
+ ...
934
+ },
935
+ {
936
+ "href": "https://service.example.com/records/3",
937
+ "name": 'Plaza Cafe',
938
+ ...
939
+ }
940
+ ]
941
+ }
942
+ ```
943
+
944
+ You can also apply request options to `expanded`. Those options will be used to perform the additional requests to expand the data:
945
+
946
+ ```ruby
947
+ # app/controllers/some_controller.rb
948
+
949
+ Search.where(what: 'Cafe').expanded(auth: { bearer: access_token })
950
+ ```
951
+
952
+ #### Error handling with chains
953
+
954
+ 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:
955
+
956
+ ```ruby
957
+ # app/controllers/some_controller.rb
958
+
959
+ def show
960
+ @records = Record.where(color: blue) # returns a chain, nothing is resolved, no http requests are performed
961
+ rescue => e
962
+ # never ending up here, because the http requests are actually performed in the view, when the query chain is resolved
963
+ end
964
+ ```
965
+
966
+ ```ruby
967
+ # app/views/some/view.haml
968
+
969
+ = @records.each do |record| # .each resolves the query chain, leads to http requests beeing performed, which might raises an exception
970
+ = record.name
971
+ ```
972
+
973
+ To simplify error handling with chains, you can also chain error handlers to be resolved, as part of the chain.
974
+
975
+ If you need to render some different view in Rails based on an LHS error raised during rendering the view, please proceed as following:
976
+
977
+ ```ruby
978
+ # app/controllers/some_controller.rb
979
+
980
+ def show
981
+ @records = Record
982
+ .rescue(LHC::Error, ->(error){ rescue_from(error) })
983
+ .where(color: 'blue')
984
+ render 'show'
985
+ render_error if @error
986
+ end
987
+
988
+ private
989
+
990
+ def rescue_from(error)
991
+ @error = error
992
+ nil
993
+ end
994
+
995
+ def render_error
996
+ self.response_body = nil # required to not raise AbstractController::DoubleRenderError
997
+ render 'error'
998
+ end
999
+ ```
1000
+ ```
1001
+ > GET https://service.example.com/records?color=blue
1002
+ < 406
1003
+ ```
1004
+
1005
+ In case no matching error handler is found the error gets re-raised.
1006
+
1007
+ -> Read more about [LHC error types/classes](https://github.com/local-ch/lhc#exceptions)
1008
+
1009
+ 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:
1010
+
1011
+ ```ruby
1012
+ # app/controllers/some_controller.rb
1013
+
1014
+ data = Record
1015
+ .rescue(LHC::Unauthorized, ->(response) { Record.new(name: 'unknown') })
1016
+ .find(1, 2, 3)
1017
+
1018
+ data[1].name # 'unknown'
1019
+ ```
1020
+ ```
1021
+ In parallel:
1022
+ > GET https://service.example.com/records/1
1023
+ < 200
1024
+ > GET https://service.example.com/records/2
1025
+ < 400
1026
+ > GET https://service.example.com/records/3
1027
+ < 200
1028
+ ```
1029
+
1030
+ -> Read more about [LHC error types/classes](https://github.com/local-ch/lhc#exceptions)
1031
+
1032
+ **If an error handler returns `nil` an empty LHS::Record is returned, not `nil`!**
1033
+
1034
+ In case you want to ignore errors and continue working with `nil` in those cases,
1035
+ please use `ignore`:
1036
+
1037
+ ```ruby
1038
+ # app/controllers/some_controller.rb
1039
+
1040
+ record = Record.ignore(LHC::NotFound).find_by(color: 'blue')
1041
+
1042
+ record # nil
1043
+ ```
1044
+
1045
+ #### Resolve chains: fetch
1046
+
1047
+ In case you need to resolve a query chain in place, use `fetch`:
1048
+
1049
+ ```ruby
1050
+ # app/controllers/some_controller.rb
1051
+
1052
+ records = Record.where(color: 'blue').fetch
1053
+ ```
1054
+
1055
+ #### Add request options to a query chain: options
1056
+
1057
+ You can apply options to the request chain. Those options will be forwarded to the request perfomed by the chain/query:
1058
+
1059
+ ```ruby
1060
+ # app/controllers/some_controller.rb
1061
+
1062
+ options = { auth: { bearer: '123456' } } # authenticated with OAuth token
1063
+
1064
+ ```
1065
+
1066
+ ```ruby
1067
+ # app/controllers/some_controller.rb
1068
+
1069
+ AuthenticatedRecord = Record.options(options)
1070
+
1071
+ ```
1072
+
1073
+ ```ruby
1074
+ # app/controllers/some_controller.rb
1075
+
1076
+ blue_records = AuthenticatedRecord.where(color: 'blue')
1077
+
1078
+ ```
1079
+ ```
1080
+ GET https://service.example.com/records?color=blue { headers: { 'Authentication': 'Bearer 123456' } }
1081
+ ```
1082
+
1083
+ ```ruby
1084
+ # app/controllers/some_controller.rb
1085
+
1086
+ AuthenticatedRecord.create(color: 'red')
1087
+
1088
+ ```
1089
+ ```
1090
+ POST https://service.example.com/records { body: '{ color: "red" }' }, headers: { 'Authentication': 'Bearer 123456' } }
1091
+ ```
1092
+
1093
+ ```ruby
1094
+ # app/controllers/some_controller.rb
1095
+
1096
+ record = AuthenticatedRecord.find(123)
1097
+
1098
+ ```
1099
+ ```
1100
+ GET https://service.example.com/records/123 { headers: { 'Authentication': 'Bearer 123456' } }
1101
+ ```
1102
+
1103
+ ```ruby
1104
+ # app/controllers/some_controller.rb
1105
+
1106
+ authenticated_record = record.options(options) # starting a new chain based on the found record
1107
+
1108
+ ```
1109
+
1110
+ ```ruby
1111
+ # app/controllers/some_controller.rb
1112
+
1113
+ authenticated_record.valid?
1114
+
1115
+ ```
1116
+ ```
1117
+ POST https://service.example.com/records/validate { body: '{...}', headers: { 'Authentication': 'Bearer 123456' } }
1118
+ ```
1119
+
1120
+ ```ruby
1121
+ # app/controllers/some_controller.rb
1122
+
1123
+ authenticated_record.save
1124
+ ```
1125
+ ```
1126
+ POST https://service.example.com/records { body: '{...}', headers: { 'Authentication': 'Bearer 123456' } }
1127
+ ```
1128
+
1129
+ ```ruby
1130
+ # app/controllers/some_controller.rb
1131
+
1132
+ authenticated_record.destroy
1133
+
1134
+ ```
1135
+ ```
1136
+ DELETE https://service.example.com/records/123 { headers: { 'Authentication': 'Bearer 123456' } }
1137
+ ```
1138
+
1139
+ ```ruby
1140
+ # app/controllers/some_controller.rb
1141
+
1142
+ authenticated_record.update(name: 'Steve')
1143
+
1144
+ ```
1145
+ ```
1146
+ POST https://service.example.com/records/123 { body: '{...}', headers: { 'Authentication': 'Bearer 123456' } }
1147
+ ```
1148
+
1149
+ #### Control pagination within a query chain
1150
+
1151
+ `page` sets the page that you want to request.
1152
+
1153
+ `per` sets the amount of items requested per page.
1154
+
1155
+ `limit` is an alias for `per`. **But without providing arguments, it resolves the query and provides the current response limit per page**
1156
+
1157
+ ```ruby
1158
+ # app/controllers/some_controller.rb
1159
+
1160
+ Record.page(3).per(20).where(color: 'blue')
1161
+
1162
+ ```
1163
+ ```
1164
+ GET https://service.example.com/records?offset=40&limit=20&color=blue
1165
+ ```
1166
+
1167
+ ```ruby
1168
+ # app/controllers/some_controller.rb
1169
+
1170
+ Record.page(3).per(20).where(color: 'blue')
1171
+
1172
+ ```
1173
+ ```
1174
+ GET https://service.example.com/records?offset=40&limit=20&color=blue
1175
+ ```
1176
+
1177
+ The applied pagination strategy depends on whats configured for the particular record: See [Record pagination](#record-pagination)
1178
+
1179
+ ### Record pagination
1180
+
1181
+ You can configure pagination on a per record base.
1182
+ 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).
1183
+
1184
+ #### Pagination strategy
1185
+
1186
+ ##### Pagination strategy: offset (default)
1187
+
1188
+ The offset pagination strategy is LHS's default pagination strategy, so nothing needs to be (re-)configured.
1189
+
1190
+ The `offset` pagination strategy starts with 0 and offsets by the amount of items, thay you've already recived – typically `limit`.
1191
+
1192
+ ```ruby
1193
+ # app/models/record.rb
1194
+
1195
+ class Search < LHS::Record
1196
+ endpoint '{+service}/search'
1197
+ end
1198
+ ```
1199
+
1200
+ ```ruby
1201
+ # app/controllers/some_controller.rb
1202
+
1203
+ Record.all
1204
+
1205
+ ```
1206
+ ```
1207
+ GET https://service.example.com/records?limit=100
1208
+ {
1209
+ items: [{...}, ...],
1210
+ total: 300,
1211
+ limit: 100,
1212
+ offset: 0
1213
+ }
1214
+ In parallel:
1215
+ GET https://service.example.com/records?limit=100&offset=100
1216
+ GET https://service.example.com/records?limit=100&offset=200
1217
+ ```
1218
+
1219
+ ##### Pagination strategy: page
1220
+
1221
+ 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.
1222
+
1223
+ ```ruby
1224
+ # app/models/record.rb
1225
+
1226
+ class Search < LHS::Record
1227
+ configuration pagination_strategy: 'page', pagination_key: 'page'
1228
+
1229
+ endpoint '{+service}/search'
1230
+ end
1231
+ ```
1232
+
1233
+ ```ruby
1234
+ # app/controllers/some_controller.rb
1235
+
1236
+ Record.all
1237
+
1238
+ ```
1239
+ ```
1240
+ GET https://service.example.com/records?limit=100
1241
+ {
1242
+ items: [{...}, ...],
1243
+ total: 300,
1244
+ limit: 100,
1245
+ page: 1
1246
+ }
1247
+ In parallel:
1248
+ GET https://service.example.com/records?limit=100&page=2
1249
+ GET https://service.example.com/records?limit=100&page=3
1250
+ ```
1251
+
1252
+ ##### Pagination strategy: start
1253
+
1254
+ In comparison to the `offset` strategy, the `start` strategy indicates with which item the current page starts.
1255
+ Typically it starts with 1 and if you get 100 items per page, the next start is 101.
1256
+
1257
+ ```ruby
1258
+ # app/models/record.rb
1259
+
1260
+ class Search < LHS::Record
1261
+ configuration pagination_strategy: 'start', pagination_key: 'startAt'
1262
+
1263
+ endpoint '{+service}/search'
1264
+ end
1265
+ ```
1266
+
1267
+ ```ruby
1268
+ # app/controllers/some_controller.rb
1269
+
1270
+ Record.all
1271
+
1272
+ ```
1273
+ ```
1274
+ GET https://service.example.com/records?limit=100
1275
+ {
1276
+ items: [{...}, ...],
1277
+ total: 300,
1278
+ limit: 100,
1279
+ page: 1
1280
+ }
1281
+ In parallel:
1282
+ GET https://service.example.com/records?limit=100&startAt=101
1283
+ GET https://service.example.com/records?limit=100&startAt=201
1284
+ ```
1285
+
1286
+ ##### Pagination strategy: link
1287
+
1288
+ The `link` strategy continuously follows in-response embedded links to following pages until the last page is reached (indicated by no more `next` link).
1289
+
1290
+ *WARNING*
1291
+
1292
+ Loading all pages from a resource paginated with links only can result in very poor performance, as pages can only be loaded sequentially!
1293
+
1294
+ ```ruby
1295
+ # app/models/record.rb
1296
+
1297
+ class Search < LHS::Record
1298
+ configuration pagination_strategy: 'link'
1299
+
1300
+ endpoint '{+service}/search'
1301
+ end
1302
+ ```
1303
+
1304
+ ```ruby
1305
+ # app/controllers/some_controller.rb
1306
+
1307
+ Record.all
1308
+
1309
+ ```
1310
+ ```
1311
+ GET https://service.example.com/records?limit=100
1312
+ {
1313
+ items: [{...}, ...],
1314
+ limit: 100,
1315
+ next: {
1316
+ href: 'https://service.example.com/records?from_record_id=p62qM5p0NK_qryO52Ze-eg&limit=100'
1317
+ }
1318
+ }
1319
+ Sequentially:
1320
+ GET https://service.example.com/records?from_record_id=p62qM5p0NK_qryO52Ze-eg&limit=100
1321
+ GET https://service.example.com/records?from_record_id=xcaoXBmuMyFFEcFDSgNgDQ&limit=100
1322
+ ```
1323
+
1324
+ #### Pagination keys
1325
+
1326
+ ##### limit_key
1327
+
1328
+ `limit_key` sets the key used to indicate how many items you want to retrieve per page e.g. `size`, `limit`, etc.
1329
+ 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.
1330
+
1331
+ ```ruby
1332
+ # app/models/record.rb
1333
+
1334
+ class Record < LHS::Record
1335
+ configuration limit_key: { body: [:pagination, :max], parameter: :max }
1336
+
1337
+ endpoint '{+service}/records'
1338
+ end
1339
+ ```
1340
+
1341
+ ```ruby
1342
+ # app/controllers/some_controller.rb
1343
+
1344
+ records = Record.where(color: 'blue')
1345
+ records.limit # 20
1346
+ ```
1347
+ ```
1348
+ GET https://service.example.com/records?color=blue&max=100
1349
+ { ...
1350
+ items: [...],
1351
+ pagination: { max: 20 }
1352
+ }
1353
+ ```
1354
+
1355
+ ##### pagination_key
1356
+
1357
+ `pagination_key` defines which key to use to paginate a page (e.g. `offset`, `page`, `startAt` etc.).
1358
+ 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.
1359
+
1360
+ ```ruby
1361
+ # app/models/record.rb
1362
+
1363
+ class Record < LHS::Record
1364
+ configuration pagination_key: { body: [:pagination, :page], parameter: :page }, pagination_strategy: :page
1365
+
1366
+ endpoint '{+service}/records'
1367
+ end
1368
+ ```
1369
+
1370
+ ```ruby
1371
+ # app/controllers/some_controller.rb
1372
+
1373
+ records = Record.where(color: 'blue').all
1374
+ records.length # 300
1375
+ ```
1376
+ ```
1377
+ GET https://service.example.com/records?color=blue&limit=100
1378
+ {... pagination: { page: 1 } }
1379
+ In parallel:
1380
+ GET https://service.example.com/records?color=blue&limit=100&page=2
1381
+ {... pagination: { page: 2 } }
1382
+ GET https://service.example.com/records?color=blue&limit=100&page=3
1383
+ {... pagination: { page: 3 } }
1384
+ ```
1385
+
1386
+ ##### total_key
1387
+
1388
+ `total_key` defines which key to user for pagination to describe the total amount of remote items (e.g. `total`, `totalResults`, etc.).
1389
+
1390
+ ```ruby
1391
+ # app/models/record.rb
1392
+
1393
+ class Record < LHS::Record
1394
+ configuration total_key: [:pagination, :total]
1395
+
1396
+ endpoint '{+service}/records'
1397
+ end
1398
+ ```
1399
+
1400
+ ```ruby
1401
+ # app/controllers/some_controller.rb
1402
+
1403
+ records = Record.where(color: 'blue').fetch
1404
+ records.length # 100
1405
+ records.count # 300
1406
+ ```
1407
+ ```
1408
+ GET https://service.example.com/records?color=blue&limit=100
1409
+ {... pagination: { total: 300 } }
1410
+ ```
1411
+
1412
+ #### Pagination links
1413
+
1414
+ ##### next?
1415
+
1416
+ `next?` Tells you if there is a next link or not.
1417
+
1418
+ ```ruby
1419
+ # app/controllers/some_controller.rb
1420
+
1421
+ @records = Record.where(color: 'blue').fetch
1422
+ ```
1423
+ ```
1424
+ GET https://service.example.com/records?color=blue&limit=100
1425
+ {... items: [...], next: 'https://service.example.com/records?color=blue&limit=100&offset=100' }
1426
+ ```
1427
+
1428
+ ```ruby
1429
+ # app/views/some_view.haml
1430
+
1431
+ - if @records.next?
1432
+ = render partial: 'next_arrow'
1433
+ ```
1434
+
1435
+ ##### previous?
1436
+
1437
+ `previous?` Tells you if there is a previous link or not.
1438
+
1439
+ ```ruby
1440
+ # app/controllers/some_controller.rb
1441
+
1442
+ @records = Record.where(color: 'blue').fetch
1443
+ ```
1444
+ ```
1445
+ GET https://service.example.com/records?color=blue&limit=100
1446
+ {... items: [...], previous: 'https://service.example.com/records?color=blue&limit=100&offset=100' }
1447
+ ```
1448
+
1449
+ ```ruby
1450
+ # app/views/some_view.haml
1451
+
1452
+ - if @records.previous?
1453
+ = render partial: 'previous_arrow'
1454
+ ```
1455
+
1456
+ #### Kaminari support (limited)
1457
+
1458
+ LHS implements an interface that makes it partially working with Kaminari.
1459
+
1460
+ 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:
1461
+
1462
+ ```ruby
1463
+ # controller
1464
+ @items = Record.page(params[:page]).per(100)
1465
+ ```
1466
+
1467
+ ```ruby
1468
+ # view
1469
+ = paginate @items
1470
+ ```
1471
+
1472
+ ### Build, create and update records
1473
+
1474
+ #### Create new records
1475
+
1476
+ ##### create
1477
+
1478
+ `create` will return the object in memory if persisting fails, providing validation errors in `.errors` (See [record validation](#record-validation)).
1479
+
1480
+ `create!` instead will raise an exception.
1481
+
1482
+ `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.
1483
+
1484
+ ```ruby
1485
+ # app/controllers/some_controller.rb
1486
+
1487
+ record = Record.create(
1488
+ text: 'Hello world'
1489
+ )
1490
+
1491
+ ```
1492
+ ```
1493
+ POST https://service.example.com/records { body: "{ 'text' : 'Hello world' }" }
1494
+ ```
1495
+
1496
+ -> See [record validation](#record-validation) for how to handle validation errors when creating records.
1497
+
1498
+ ###### Unwrap nested data when creation response nests created record data
1499
+
1500
+ `item_created_key` key used to merge record data thats nested in the creation response body:
1501
+
1502
+ ```ruby
1503
+ # app/models/location.rb
1504
+
1505
+ class Location < LHS::Record
1506
+
1507
+ configuration item_created_key: [:response, :location]
1508
+
1509
+ end
1510
+ ```
1511
+
1512
+ ```ruby
1513
+ # app/controllers/some_controller.rb
1514
+
1515
+ location.create(lat: '47.3920152', long: '8.5127981')
1516
+ location.address # Förrlibuckstrasse 62, 8005 Zürich
1517
+ ```
1518
+ ```
1519
+ POST https://service.example.com/locations { body: "{ 'lat': '47.3920152', long: '8.5127981' }" }
1520
+ {... { response: { location: {... address: 'Förrlibuckstrasse 62, 8005 Zürich' } } } }
1521
+ ```
1522
+
1523
+ ###### Create records through associations: Nested sub resources
1524
+
1525
+ ```ruby
1526
+ # app/models/restaurant.rb
1527
+
1528
+ class Restaurant < LHS::Record
1529
+ endpoint '{+service}/restaurants/{id}'
1530
+ end
1531
+
1532
+ ```
1533
+
1534
+ ```ruby
1535
+ # app/models/feedback.rb
1536
+
1537
+ class Feedback < LHS::Record
1538
+ endpoint '{+service}/restaurants/{restaurant_id}/feedbacks'
1539
+ end
1540
+
1541
+ ```
1542
+
1543
+ ```ruby
1544
+ # app/controllers/some_controller.rb
1545
+
1546
+ restaurant = Restaurant.find(1)
1547
+ ```
1548
+ ```
1549
+ GET https://service.example.com/restaurants/1
1550
+ {... reviews: { href: 'https://service.example.com/restaurants/1/reviews' }}
1551
+ ```
1552
+
1553
+ ```ruby
1554
+ # app/controllers/some_controller.rb
1555
+
1556
+ restaurant.reviews.create(
1557
+ text: 'Simply awesome!'
1558
+ )
1559
+ ```
1560
+ ```
1561
+ POST https://service.example.com/restaurants/1/reviews { body: "{ 'text': 'Simply awesome!' }" }
1562
+ ```
1563
+
1564
+ #### Start building new records
1565
+
1566
+ With `new` or `build` you can start building new records from scratch, which can be persisted with `save`:
1567
+
1568
+ ```ruby
1569
+ # app/controllers/some_controller.rb
1570
+
1571
+ record = Record.new # or Record.build
1572
+ record.name = 'Starbucks'
1573
+ record.save
1574
+ ```
1575
+ ```
1576
+ POST https://service.example.com/records { body: "{ 'name' : 'Starbucks' }" }
1577
+ ```
1578
+
1579
+ #### Change/Update existing records
1580
+
1581
+ ##### save
1582
+
1583
+ `save` persist the whole object in it's current state.
1584
+
1585
+ `save` will return `false` if persisting fails. `save!` instead will raise an exception.
1586
+
1587
+ ```ruby
1588
+ # app/controllers/some_controller.rb
1589
+
1590
+ record = Record.find('1z-5r1fkaj')
1591
+
1592
+ ```
1593
+ ```
1594
+ GET https://service.example.com/records/1z-5r1fkaj
1595
+ { name: 'Starbucks', recommended: null }
1596
+ ```
1597
+
1598
+ ```ruby
1599
+ # app/controllers/some_controller.rb
1600
+
1601
+ record.recommended = true
1602
+ record.save
1603
+
1604
+ ```
1605
+ ```
1606
+ POST https://service.example.com/records/1z-5r1fkaj { body: "{ 'name': 'Starbucks', 'recommended': true }" }
1607
+ ```
1608
+
1609
+ -> See [record validation](#record-validation) for how to handle validation errors when updating records.
1610
+
1611
+ ##### update
1612
+
1613
+ ###### Directly via Record
1614
+
1615
+ ```ruby
1616
+ # app/controllers/some_controller.rb
1617
+
1618
+ Record.update(id: '1z-5r1fkaj', name: 'Steve')
1619
+
1620
+ ```
1621
+ ```
1622
+ GET https://service.example.com/records/1z-5r1fkaj
1623
+ { name: 'Steve' }
1624
+ ```
1625
+
1626
+ ###### per Instance
1627
+
1628
+ `update` persists the whole object after new parameters are applied through arguments.
1629
+
1630
+ `update` will return false if persisting fails. `update!` instead will raise an exception.
1631
+
1632
+ `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.
1633
+
1634
+ ```ruby
1635
+ # app/controllers/some_controller.rb
1636
+
1637
+ record = Record.find('1z-5r1fkaj')
1638
+
1639
+ ```
1640
+ ```
1641
+ GET https://service.example.com/records/1z-5r1fkaj
1642
+ { name: 'Starbucks', recommended: null }
1643
+ ```
1644
+
1645
+ ```ruby
1646
+ # app/controllers/some_controller.rb
1647
+
1648
+ record.update(recommended: true)
1649
+
1650
+ ```
1651
+ ```
1652
+ POST https://service.example.com/records/1z-5r1fkaj { body: "{ 'name': 'Starbucks', 'recommended': true }" }
1653
+ ```
1654
+
1655
+ -> See [record validation](#record-validation) for how to handle validation errors when updating records.
1656
+
1657
+ You can use `update` and the end of query-chains:
1658
+
1659
+ ```ruby
1660
+ # app/controllers/some_controller.rb
1661
+
1662
+ record.options(method: :put).update(recommended: true)
1663
+
1664
+ ```
1665
+
1666
+ You can also pass explicit request options to `update`, by passing two explicit hashes:
1667
+
1668
+ ```ruby
1669
+ # app/controllers/some_controller.rb
1670
+
1671
+ record.update({ recommended: true }, { method: 'put' })
1672
+
1673
+ ```
1674
+
1675
+ ##### partial_update
1676
+
1677
+ `partial_update` updates just the provided parameters.
1678
+
1679
+ `partial_update` will return false if persisting fails. `partial_update!` instead will raise an exception.
1680
+
1681
+ `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.
1682
+
1683
+ ```ruby
1684
+ # app/controllers/some_controller.rb
1685
+
1686
+ record = Record.find('1z-5r1fkaj')
1687
+
1688
+ ```
1689
+ ```
1690
+ GET https://service.example.com/records/1z-5r1fkaj
1691
+ { name: 'Starbucks', recommended: null }
1692
+ ```
1693
+
1694
+ ```ruby
1695
+ # app/controllers/some_controller.rb
1696
+
1697
+ record.partial_update(recommended: true)
1698
+
1699
+ ```
1700
+ ```
1701
+ POST https://service.example.com/records/1z-5r1fkaj { body: "{ 'name': 'Starbucks', 'recommended': true }" }
1702
+ ```
1703
+
1704
+ -> See [record validation](#record-validation) for how to handle validation errors when updating records.
1705
+
1706
+ You can use `partial_update` at the end of query-chains:
1707
+
1708
+ ```ruby
1709
+ # app/controllers/some_controller.rb
1710
+
1711
+ record.options(method: :put).partial_update(recommended: true)
1712
+
1713
+ ```
1714
+
1715
+ You can also pass explicit request options to `partial_update`, by passing two explicit hashes:
1716
+
1717
+ ```ruby
1718
+ # app/controllers/some_controller.rb
1719
+
1720
+ record.partial_update({ recommended: true }, { method: 'put' })
1721
+
1722
+ ```
1723
+
1724
+ #### Endpoint url parameter injection during record creation/change
1725
+
1726
+ LHS injects parameters provided to `create`, `update`, `partial_update`, `save` etc. into an endpoint's URL when matching:
1727
+
1728
+ ```ruby
1729
+ # app/models/feedback.rb
1730
+
1731
+ class Feedback << LHS::Record
1732
+ endpoint '{+service}/records/{record_id}/feedbacks'
1733
+ end
1734
+ ```
1735
+
1736
+ ```ruby
1737
+ # app/controllers/some_controller.rb
1738
+
1739
+ Feedback.create(record_id: 51232, text: 'Great Restaurant!')
1740
+ ```
1741
+ ```
1742
+ POST https://service.example.com/records/51232/feedbacks { body: "{ 'text' : 'Great Restaurant!' }" }
1743
+ ```
1744
+
1745
+ #### Record validation
1746
+
1747
+ In order to validate records before persisting them, you can use the `valid?` (`validate` alias) method.
1748
+
1749
+ 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.
1750
+
1751
+ The specific endpoint has to support validations without persistence. An endpoint has to be enabled (opt-in) in your record configurations:
1752
+
1753
+ ```ruby
1754
+ # app/models/user.rb
1755
+
1756
+ class User < LHS::Record
1757
+
1758
+ endpoint '{+service}/users', validates: { params: { persist: false } }
1759
+
1760
+ end
1761
+ ```
1762
+
1763
+ ```ruby
1764
+ # app/controllers/some_controller.rb
1765
+
1766
+ user = User.build(email: 'i\'m not an email address')
1767
+
1768
+ unless user.valid?
1769
+ @errors = user.errors
1770
+ render 'new' and return
1771
+ end
1772
+ ```
1773
+ ```
1774
+ POST https://service.example.com/users?persist=false { body: '{ "email" : "i'm not an email address"}' }
1775
+ {
1776
+ "field_errors": [{
1777
+ "path": ["email"],
1778
+ "code": "WRONG_FORMAT",
1779
+ "message": "The property value's format is incorrect."
1780
+ }],
1781
+ "message": "Email must have the correct format."
1782
+ }
1783
+ ```
1784
+
1785
+ The functionalities of `LHS::Errors` pretty much follow those of `ActiveModel::Validation`:
1786
+
1787
+ ```ruby
1788
+ # app/views/some_view.haml
1789
+
1790
+ @errors.any? # true
1791
+ @errors.include?(:email) # true
1792
+ @errors[:email] # ['WRONG_FORMAT']
1793
+ @errors.messages # {:email=>["Translated error message that this value has the wrong format"]}
1794
+ @errors.codes # {:email=>["WRONG_FORMAT"]}
1795
+ @errors.message # Email must have the correct format."
1796
+ ```
1797
+
1798
+ ##### Configure record validations
1799
+
1800
+ The parameters passed to the `validates` endpoint option are used to perform record validations:
1801
+
1802
+ ```ruby
1803
+ # app/models/user.rb
1804
+
1805
+ class User < LHS::Record
1806
+
1807
+ endpoint '{+service}/users', validates: { params: { persist: false } } # will add ?persist=false to the request
1808
+ endpoint '{+service}/users', validates: { params: { publish: false } } # will add ?publish=false to the request
1809
+ endpoint '{+service}/users', validates: { params: { validates: true } } # will add ?validates=true to the request
1810
+ endpoint '{+service}/users', validates: { path: 'validate' } # will perform a validation via ...users/validate
1811
+
1812
+ end
1813
+ ```
1814
+
1815
+ ##### HTTP Status Codes for validation errors
1816
+
1817
+ The HTTP status code received from the endpoint when performing validations on a record, is available through the errors object:
1818
+
1819
+ ```ruby
1820
+ # app/controllers/some_controller.rb
1821
+
1822
+ record.save
1823
+ record.errors.status_code # 400
1824
+ ```
1825
+
1826
+ ##### Reset validation errors
1827
+
1828
+ Clear the error messages like:
1829
+
1830
+ ```ruby
1831
+ # app/controllers/some_controller.rb
1832
+
1833
+ record.errors.clear
1834
+ ```
1835
+
1836
+ ##### Add validation errors
1837
+
1838
+ In case you want to add application side validation errors, even though it's not recommended, do it as following:
1839
+
1840
+ ```ruby
1841
+ user.errors.add(:name, 'WRONG_FORMAT')
1842
+ ```
1843
+
1844
+ ##### Validation errors for nested data
1845
+
1846
+ If you work with complex data structures, you sometimes need to have validation errors delegated/scoped to nested data.
1847
+
1848
+ This features makes `LHS::Record`s compatible with how Rails or Simpleform renders/builds forms and especially error messages:
1849
+
1850
+ ```ruby
1851
+ # app/controllers/some_controller.rb
1852
+
1853
+ unless @customer.save
1854
+ @errors = @customer.errors
1855
+ end
1856
+ ```
1857
+ ```
1858
+ POST https://service.example.com/customers { body: "{ 'address' : { 'street': 'invalid', housenumber: '' } }" }
1859
+ {
1860
+ "field_errors": [{
1861
+ "path": ["address", "street"],
1862
+ "code": "REQUIRED_PROPERTY_VALUE_INCORRECT",
1863
+ "message": "The property value is incorrect."
1864
+ },{
1865
+ "path": ["address", "housenumber"],
1866
+ "code": "REQUIRED_PROPERTY_VALUE",
1867
+ "message": "The property value is required."
1868
+ }],
1869
+ "message": "Some data is invalid."
1870
+ }
1871
+ ```
1872
+
1873
+ ```ruby
1874
+ # app/views/some_view.haml
1875
+
1876
+ = form_for @customer, as: :customer do |customer_form|
1877
+
1878
+ = fields_for 'customer[:address]', @customer.address, do |address_form|
1879
+
1880
+ = fields_for 'customer[:address][:street]', @customer.address.street, do |street_form|
1881
+
1882
+ = street_form.input :name
1883
+ = street_form.input :house_number
1884
+ ```
1885
+
1886
+ This would render nested forms and would also render nested form errors for nested data structures.
1887
+
1888
+ You can also access those nested errors like:
1889
+
1890
+ ```ruby
1891
+ @customer.address.errors
1892
+ @customer.address.street.errors
1893
+ ```
1894
+
1895
+ ##### Translation of validation errors
1896
+
1897
+ 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`:
1898
+
1899
+ ```ruby
1900
+ lhs.errors.records.<record_name>.attributes.<attribute_name>.<error_code>
1901
+ e.g. lhs.errors.records.customer.attributes.name.unsupported_property_value
1902
+
1903
+ lhs.errors.records.<record_name>.<error_code>
1904
+ e.g. lhs.errors.records.customer.unsupported_property_value
1905
+
1906
+ lhs.errors.messages.<error_code>
1907
+ e.g. lhs.errors.messages.unsupported_property_value
1908
+
1909
+ lhs.errors.attributes.<attribute_name>.<error_code>
1910
+ e.g. lhs.errors.attributes.name.unsupported_property_value
1911
+
1912
+ lhs.errors.fallback_message
1913
+
1914
+ lhs.errors.records.<record_name>.attributes.<collection>.<attribute_name>.<error_code>
1915
+ e.g. lhs.errors.records.appointment_proposal.attributes.appointments.date_time.date_property_not_in_future
1916
+ ```
1917
+
1918
+ ##### Validation error types: errors vs. warnings
1919
+
1920
+ ###### Persistance failed: errors
1921
+
1922
+ If an endpoint returns errors in the response body, that is enough to interpret it as: persistance failed.
1923
+ The response status code in this scenario is neglected.
1924
+
1925
+ ###### Persistance succeeded: warnings
1926
+
1927
+ In some cases, you need non blocking meta information about potential problems with the created record, so called warnings.
1928
+
1929
+ If the API endpoint implements warnings, returned when validating, they are provided just as `errors` (same interface and methods) through the `warnings` attribute:
1930
+
1931
+ ```ruby
1932
+ # app/controllres/some_controller.rb
1933
+
1934
+ @presence = Presence.options(params: { synchronize: false }).create(
1935
+ place: { href: 'http://storage/places/1' }
1936
+ )
1937
+ ```
1938
+ ```
1939
+ POST https://service.example.com/presences { body: '{ "place": { "href": "http://storage/places/1" } }' }
1940
+ {
1941
+ field_warnings: [{
1942
+ code: 'WILL_BE_RESIZED',
1943
+ path: ['place', 'photos', 0],
1944
+ message: 'This photo is too small and will be resized.'
1945
+ }
1946
+ }
1947
+ ```
1948
+
1949
+ ```ruby
1950
+
1951
+ presence.warnings.any? # true
1952
+ presence.place.photos[0].warnings.messages.first # 'This photo is too small and will be resized.'
1953
+
1954
+ ```
1955
+
1956
+ ##### Using `ActiveModel::Validations` none the less
1957
+
1958
+ 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?`.
1959
+
1960
+ So in essence, mixing `ActiveModel::Validations` and LHS built-in validations (via endpoints), is not compatible, yet.
1961
+
1962
+ [Open issue](https://github.com/local-ch/lhs/issues/159)
1963
+
1964
+ #### Use form_helper to create and update records
1965
+
1966
+ Rails `form_for` view-helper can be used in combination with instances of `LHS::Record`s to autogenerate forms:
1967
+
1968
+ ```ruby
1969
+ <%= form_for(@instance, url: '/create') do |f| %>
1970
+ <%= f.text_field :name %>
1971
+ <%= f.text_area :text %>
1972
+ <%= f.submit "Create" %>
1973
+ <% end %>
1974
+ ```
1975
+
1976
+ ### Destroy records
1977
+
1978
+ `destroy` deletes a record.
1979
+
1980
+ ```ruby
1981
+ # app/controllers/some_controller.rb
1982
+
1983
+ record = Record.find('1z-5r1fkaj')
1984
+ ```
1985
+ ```
1986
+ GET https://service.example.com/records/1z-5r1fkaj
1987
+ ```
1988
+
1989
+ ```ruby
1990
+ # app/controllers/some_controller.rb
1991
+
1992
+ record.destroy
1993
+ ```
1994
+ ```
1995
+ DELETE https://service.example.com/records/1z-5r1fkaj
1996
+ ```
1997
+
1998
+ You can also destroy records directly without fetching them first:
1999
+
2000
+ ```ruby
2001
+ # app/controllers/some_controller.rb
2002
+
2003
+ destroyed_record = Record.destroy('1z-5r1fkaj')
2004
+ ```
2005
+ ```
2006
+ DELETE https://service.example.com/records/1z-5r1fkaj
2007
+ ```
2008
+
2009
+ or with parameters:
2010
+
2011
+ ```ruby
2012
+ # app/controllers/some_controller.rb
2013
+
2014
+ destroyed_records = Record.destroy(name: 'Steve')
2015
+ ```
2016
+ ```
2017
+ DELETE https://service.example.com/records?name='Steve'
2018
+ ```
2019
+
2020
+ ### Record getters and setters
2021
+
2022
+ Sometimes it is neccessary to implement custom getters and setters and convert data to a processable (endpoint) format behind the scenes.
2023
+
2024
+ #### Record setters
2025
+
2026
+ 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:
2027
+
2028
+ ```ruby
2029
+ # app/models/user.rb
2030
+
2031
+ class Feedback < LHS::Record
2032
+
2033
+ def ratings=(values)
2034
+ super(
2035
+ values.map { |k, v| { name: k, value: v } }
2036
+ )
2037
+ end
2038
+ end
2039
+ ```
2040
+
2041
+ ```ruby
2042
+ # app/controllers/some_controller.rb
2043
+
2044
+ record = Record.new(ratings: { quality: 3 })
2045
+ record.ratings # [{ :name=>:quality, :value=>3 }]
2046
+ ```
2047
+
2048
+ Setting attributes with other names:
2049
+
2050
+ ```ruby
2051
+ # app/models/booking.rb
2052
+
2053
+ class Booking < LHS::Record
2054
+
2055
+ def appointments_attributes=(values)
2056
+ self.appointments = values.map { |appointment| appointment[:id] }
2057
+ end
2058
+ end
2059
+ ```
2060
+
2061
+ or
2062
+
2063
+ ```ruby
2064
+ # app/models/booking.rb
2065
+
2066
+ class Booking < LHS::Record
2067
+
2068
+ def appointments_attributes=(values)
2069
+ self[:appointments] = values.map { |appointment| appointment[:id] }
2070
+ end
2071
+ end
2072
+ ```
2073
+
2074
+ ```ruby
2075
+ # app/controllers/some_controller.rb
2076
+
2077
+ booking.update(params)
2078
+ ```
2079
+
2080
+ #### Record getters
2081
+
2082
+ If you implement accompanying getter methods, the whole data conversion would be internal only:
2083
+
2084
+ ```ruby
2085
+ # app/models/user.rb
2086
+
2087
+ class Feedback < LHS::Record
2088
+
2089
+ def ratings=(values)
2090
+ super(
2091
+ values.map { |k, v| { name: k, value: v } }
2092
+ )
2093
+ end
2094
+
2095
+ def ratings
2096
+ super.map { |r| [r[:name], r[:value]] }]
2097
+ end
2098
+ end
2099
+ ```
2100
+
2101
+ ```ruby
2102
+ # app/controllers/some_controller.rb
2103
+
2104
+ record = Record.new(ratings: { quality: 3 })
2105
+ record.ratings # {:quality=>3}
2106
+ ```
2107
+
2108
+ ### Include linked resources (hyperlinks and hypermedia)
2109
+
2110
+ 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.
2111
+
2112
+ When fetching records with LHS, you can specify in advance all the linked resources that you want to include in the results.
2113
+
2114
+ With `includes` LHS ensures that all matching and explicitly linked resources are loaded and merged (even if the linked resources are paginated).
2115
+
2116
+ Including linked resources/records is heavily influenced by [https://guides.rubyonrails.org/active_record_querying.html](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) and you should read it to understand this feature in all it's glory.
2117
+
2118
+ #### Generate links from parameters
2119
+
2120
+ Sometimes you need to generate full hrefs/urls for records but you just have parameters that describe that record, like the ID.
2121
+
2122
+ For those usecases you can use `href_for(params)`:
2123
+
2124
+ ```ruby
2125
+ # app/controllers/some_controller.rb
2126
+
2127
+ Presence.create(place: { href: Place.href_for(123) })
2128
+ ```
2129
+ ```
2130
+ POST '/presences' { place: { href: "http://datastore/places/123" } }
2131
+ ```
2132
+
2133
+ #### Ensure the whole linked collection is included with includes
2134
+
2135
+ In case endpoints are paginated and you are certain that you'll need all objects of a set and not only the first page/batch, use `includes`.
2136
+
2137
+ LHS will ensure that all linked resources are around by loading all pages (parallelized/performance optimized).
2138
+
2139
+ ```ruby
2140
+ # app/controllers/some_controller.rb
2141
+
2142
+ customer = Customer.includes(contracts: :products).find(1)
2143
+ ```
2144
+ ```
2145
+ > GET https://service.example.com/customers/1
2146
+ < {... contracts: { href: 'https://service.example.com/customers/1/contracts' } }
2147
+ > GET https://service.example.com/customers/1/contracts?limit=100
2148
+ < {... items: [...], limit: 10, offset: 0, total: 32 }
2149
+ In parallel:
2150
+ > GET https://service.example.com/customers/1/contracts?limit=10&offset=10
2151
+ < {... products: [{ href: 'https://service.example.com/product/LBC' }] }
2152
+ > GET https://service.example.com/customers/1/contracts?limit=10&offset=20
2153
+ < {... products: [{ href: 'https://service.example.com/product/LBB' }] }
2154
+ In parallel:
2155
+ > GET https://service.example.com/product/LBC
2156
+ < {... name: 'Local Business Card' }
2157
+ > GET https://service.example.com/product/LBB
2158
+ < {... name: 'Local Business Basic' }
2159
+ ```
2160
+
2161
+ ```ruby
2162
+ # app/controllers/some_controller.rb
2163
+
2164
+ customer.contracts.length # 32
2165
+ customer.contracts.first.products.first.name # Local Business Card
2166
+
2167
+ ```
2168
+
2169
+ #### Include only the first linked page of a linked collection: includes_first_page
2170
+
2171
+ `includes_first_page` includes the first page/response when loading the linked resource. **If the endpoint is paginated, only the first page will be included.**
2172
+
2173
+ ```ruby
2174
+ # app/controllers/some_controller.rb
2175
+
2176
+ customer = Customer.includes_first_page(contracts: :products).find(1)
2177
+ ```
2178
+ ```
2179
+ > GET https://service.example.com/customers/1
2180
+ < {... contracts: { href: 'https://service.example.com/customers/1/contracts' } }
2181
+ > GET https://service.example.com/customers/1/contracts?limit=100
2182
+ < {... items: [...], limit: 10, offset: 0, total: 32 }
2183
+ In parallel:
2184
+ > GET https://service.example.com/product/LBC
2185
+ < {... name: 'Local Business Card' }
2186
+ > GET https://service.example.com/product/LBB
2187
+ < {... name: 'Local Business Basic' }
2188
+ ```
2189
+
2190
+ ```ruby
2191
+ # app/controllers/some_controller.rb
2192
+
2193
+ customer.contracts.length # 10
2194
+ customer.contracts.first.products.first.name # Local Business Card
2195
+
2196
+ ```
2197
+
2198
+ #### Include various levels of linked data
2199
+
2200
+ The method syntax of `includes` allows you include hyperlinks stored in deep nested data strutures:
2201
+
2202
+ Some examples:
2203
+
2204
+ ```ruby
2205
+ Record.includes(:localch_account, :entry)
2206
+ # Includes localch_account -> entry
2207
+ # { localch_account: { href: '...', entry: { href: '...' } } }
2208
+
2209
+ Record.includes([:localch_account, :entry])
2210
+ # Includes localch_account and entry
2211
+ # { localch_account: { href: '...' }, entry: { href: '...' } }
2212
+
2213
+ Record.includes(campaign: [:entry, :user])
2214
+ # Includes campaign and entry and user from campaign
2215
+ # { campaign: { href: '...' , entry: { href: '...' }, user: { href: '...' } } }
2216
+ ```
2217
+
2218
+ #### Identify and cast known records when including records
2219
+
2220
+ When including linked resources with `includes`, already defined records and their endpoints and configurations are used to make the requests to fetch the additional data.
2221
+
2222
+ That also means that options for endpoints of linked resources are applied when requesting those in addition.
2223
+
2224
+ This applies for example a records endpoint configuration even though it's fetched/included through another record:
2225
+
2226
+ ```ruby
2227
+ # app/models/favorite.rb
2228
+
2229
+ class Favorite < LHS::Record
2230
+
2231
+ endpoint '{+service}/users/{user_id}/favorites', auth: { basic: { username: 'steve', password: 'can' } }
2232
+ endpoint '{+service}/users/{user_id}/favorites/:id', auth: { basic: { username: 'steve', password: 'can' } }
2233
+
2234
+ end
2235
+ ```
2236
+
2237
+ ```ruby
2238
+ # app/models/place.rb
2239
+
2240
+ class Place < LHS::Record
2241
+
2242
+ endpoint '{+service}/v2/places', auth: { basic: { username: 'steve', password: 'can' } }
2243
+ endpoint '{+service}/v2/places/{id}', auth: { basic: { username: 'steve', password: 'can' } }
2244
+
2245
+ end
2246
+ ```
2247
+
2248
+ ```ruby
2249
+ # app/controllers/some_controller.rb
2250
+
2251
+ Favorite.includes(:place).where(user_id: current_user.id)
2252
+
2253
+ ```
2254
+ ```
2255
+ > GET https://service.example.com/users/123/favorites { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }
2256
+ < {... items: [... { place: { href: 'https://service.example.com/place/456' } } ] }
2257
+ In parallel:
2258
+ > GET https://service.example.com/place/456 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }
2259
+ > GET https://service.example.com/place/789 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }
2260
+ > GET https://service.example.com/place/1112 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }
2261
+ > GET https://service.example.com/place/5423 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }
2262
+ ```
2263
+
2264
+ #### Apply options for requests performed to fetch included records
2265
+
2266
+ Use `references` to apply request options to requests performed to fetch included records:
2267
+
2268
+ ```ruby
2269
+ # app/controllers/some_controller.rb
2270
+
2271
+ Favorite.includes(:place).references(place: { auth: { bearer: '123' }}).where(user_id: 1)
2272
+ ```
2273
+ ```
2274
+ GET https://service.example.com/users/1/favorites
2275
+ {... items: [... { place: { href: 'https://service.example.com/places/2' } }] }
2276
+ In parallel:
2277
+ GET https://service.example.com/places/2 { headers: { 'Authentication': 'Bearer 123' } }
2278
+ GET https://service.example.com/places/3 { headers: { 'Authentication': 'Bearer 123' } }
2279
+ GET https://service.example.com/places/4 { headers: { 'Authentication': 'Bearer 123' } }
2280
+ ```
2281
+
2282
+ Here is another example, if you want to ignore errors, that occure while you fetch included resources:
2283
+
2284
+ ```ruby
2285
+ # app/controllers/some_controller.rb
2286
+
2287
+ feedback = Feedback
2288
+ .includes(campaign: :entry)
2289
+ .references(campaign: { ignore: LHC::NotFound })
2290
+ .find(12345)
2291
+ ```
2292
+
2293
+ ### Record batch processing
2294
+
2295
+ **Be careful using methods for batch processing. They could result in a lot of HTTP requests!**
2296
+
2297
+ #### all
2298
+
2299
+ `all` fetches all records from the service by doing multiple requests, best-effort parallelization, and resolving endpoint pagination if necessary:
2300
+
2301
+ ```ruby
2302
+ records = Record.all
2303
+ ```
2304
+ ```
2305
+ > GET https://service.example.com/records?limit=100
2306
+ < {...
2307
+ items: [...]
2308
+ total: 900,
2309
+ limit: 100,
2310
+ offset: 0
2311
+ }
2312
+ In parallel:
2313
+ > GET https://service.example.com/records?limit=100&offset=100
2314
+ > GET https://service.example.com/records?limit=100&offset=200
2315
+ > GET https://service.example.com/records?limit=100&offset=300
2316
+ > GET https://service.example.com/records?limit=100&offset=400
2317
+ > GET https://service.example.com/records?limit=100&offset=500
2318
+ > GET https://service.example.com/records?limit=100&offset=600
2319
+ > GET https://service.example.com/records?limit=100&offset=700
2320
+ > GET https://service.example.com/records?limit=100&offset=800
2321
+ ```
2322
+
2323
+ `all` is chainable and has the same interface like `where`:
2324
+
2325
+ ```ruby
2326
+ Record.where(color: 'blue').all
2327
+ Record.all.where(color: 'blue')
2328
+ Record.all(color: 'blue')
2329
+ ```
2330
+
2331
+ All three are doing the same thing: fetching all records with the color 'blue' from the endpoint while resolving pagingation if endpoint is paginated.
2332
+
2333
+ ##### Using all, when endpoint does not implement response pagination meta data
2334
+
2335
+ 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.
2336
+
2337
+ #### find_each
2338
+
2339
+ `find_each` is a more fine grained way to process single records that are fetched in batches.
2340
+
2341
+ ```ruby
2342
+ Record.find_each(start: 50, batch_size: 20, params: { has_reviews: true }, headers: { 'Authorization': 'Bearer 123' }) do |record|
2343
+ # Iterates over each record. Starts with record no. 50 and fetches 20 records each batch.
2344
+ record
2345
+ break if record.some_attribute == some_value
2346
+ end
2347
+ ```
2348
+
2349
+ #### find_in_batches
2350
+
2351
+ `find_in_batches` is used by `find_each` and processes batches.
2352
+
2353
+ ```ruby
2354
+ Record.find_in_batches(start: 50, batch_size: 20, params: { has_reviews: true }, headers: { 'Authorization': 'Bearer 123' }) do |records|
2355
+ # Iterates over multiple records (batch size is 20). Starts with record no. 50 and fetches 20 records each batch.
2356
+ records
2357
+ break if records.first.name == some_value
2358
+ end
2359
+ ```
2360
+
2361
+ ### Convert/Cast specific record types: becomes
2362
+
2363
+ Based on [ActiveRecord's implementation](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-becomes), LHS implements `becomes`, too.
2364
+
2365
+ It's a way to convert records of a certain type A to another certain type B.
2366
+
2367
+ _NOTE: RPC-style actions, that are discouraged in REST anyway, are utilizable with this functionality, too. See the following example:_
2368
+
2369
+ ```ruby
2370
+ # app/models/location.rb
2371
+
2372
+ class Location < LHS::Record
2373
+ endpoint '{+service}/locations'
2374
+ endpoint '{+service}/locations/{id}'
2375
+ end
2376
+ ```
2377
+
2378
+ ```ruby
2379
+ # app/models/synchronization.rb
2380
+
2381
+ class Synchronization < LHS::Record
2382
+ endpoint '{+service}/locations/{id}/sync'
2383
+ end
2384
+ ```
2385
+
2386
+ ```ruby
2387
+ # app/controllers/some_controller.rb
2388
+
2389
+ location = Location.find(1)
2390
+ ```
2391
+ ```
2392
+ GET https://service.example.com/location/1
2393
+ ```
2394
+
2395
+ ```ruby
2396
+ # app/controllers/some_controller.rb
2397
+
2398
+ synchronization = location.becomes(Synchronization)
2399
+ synchronization.save!
2400
+ ```
2401
+ ```
2402
+ POST https://service.example.com/location/1/sync { body: '{ ... }' }
2403
+ ```
2404
+
2405
+ ### Assign attributes
2406
+
2407
+ Allows you to set the attributes by passing in a hash of attributes.
2408
+
2409
+ ```ruby
2410
+ entry = LocalEntry.new
2411
+ entry.assign_attributes(company_name: 'localsearch')
2412
+ entry.company_name # => 'localsearch'
2413
+ ```
2414
+
2415
+ ## Request Cycle Cache
2416
+
2417
+ By default, LHS does not perform the same http request multiple times during one request/response cycle.
2418
+
2419
+ ```ruby
2420
+ # app/models/user.rb
2421
+
2422
+ class User < LHS::Record
2423
+ endpoint '{+service}/users/{id}'
2424
+ end
2425
+ ```
2426
+
2427
+ ```ruby
2428
+ # app/models/location.rb
2429
+
2430
+ class Location < LHS::Record
2431
+ endpoint '{+service}/locations/{id}'
2432
+ end
2433
+ ```
2434
+
2435
+ ```ruby
2436
+ # app/controllers/some_controller.rb
2437
+
2438
+ def index
2439
+ @user = User.find(1)
2440
+ @locations = Location.includes(:owner).find(2)
2441
+ end
2442
+ ```
2443
+ ```
2444
+ GET https://service.example.com/users/1
2445
+ GET https://service.example.com/location/2
2446
+ {... owner: { href: 'https://service.example.com/users/1' } }
2447
+ From cache:
2448
+ GET https://service.example.com/users/1
2449
+ ```
2450
+
2451
+ 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.
2452
+
2453
+ 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.
2454
+
2455
+ 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.
2456
+
2457
+ ### Change store for LHS' request cycle cache
2458
+
2459
+ 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:
2460
+
2461
+ ```ruby
2462
+ # config/initializers/lhs.rb
2463
+
2464
+ LHS.configure do |config|
2465
+ config.request_cycle_cache = ActiveSupport::Cache::MemoryStore.new
2466
+ end
2467
+ ```
2468
+
2469
+ ### Disable request cycle cache
2470
+
2471
+ If you want to disable the LHS Request Cycle Cache, simply disable it within configuration:
2472
+
2473
+ ```ruby
2474
+ # config/initializers/lhs.rb
2475
+
2476
+ LHS.configure do |config|
2477
+ config.request_cycle_cache_enabled = false
2478
+ end
2479
+ ```
2480
+
2481
+ ## Automatic Authentication (OAuth)
2482
+
2483
+ LHS provides a way to have records automatically fetch and use OAuth authentication when performing requests within Rails.
2484
+
2485
+ In order to enable automatic oauth authentication, perform the following steps:
2486
+
2487
+ 1. Make sure LHS is configured to perform `auto_oauth`. Provide a block that, when executed in the controller context, returns a valid access_token/bearer_token.
2488
+ ```ruby
2489
+ # config/initializers/lhs.rb
2490
+
2491
+ LHS.configure do |config|
2492
+ config.auto_oauth = -> { access_token }
2493
+ end
2494
+ ```
2495
+
2496
+ 2. Opt-in records requiring oauth authentication:
2497
+
2498
+ ```ruby
2499
+ # app/models/record.rb
2500
+
2501
+ class Record < LHS::Record
2502
+ oauth
2503
+ # ...
2504
+ end
2505
+ ```
2506
+
2507
+ 3. Include the `LHS::OAuth` context into your application controller:
2508
+
2509
+ ```ruby
2510
+ # app/controllers/application_controller.rb
2511
+
2512
+ class ApplicationController < ActionController::Base
2513
+ include LHS::OAuth
2514
+
2515
+ # ...
2516
+ end
2517
+ ```
2518
+
2519
+ 4. Make sure you have the `LHC::Auth` interceptor enabled:
2520
+
2521
+ ```ruby
2522
+ # config/initializers/lhc.rb
2523
+
2524
+ LHC.configure do |config|
2525
+ config.interceptors = [LHC::Auth]
2526
+ end
2527
+ ```
2528
+
2529
+ Now you can perform requests based on the record that will be auto authenticated from now on:
2530
+
2531
+ ```ruby
2532
+ # app/controllers/some_controller.rb
2533
+
2534
+ Record.find(1)
2535
+ ```
2536
+ ```
2537
+ https://records/1
2538
+ Authentication: 'Bearer token-12345'
2539
+ ```
2540
+
2541
+ ### Configure multiple auth providers (even per endpoint)
2542
+
2543
+ In case you need to configure multiple auth provider access_tokens within your application,
2544
+ make sure you provide a proc returning a hash when configuring `auto_oauth`,
2545
+ naming every single provider and the responsive method to retrieve the access_tokens in the controller context:
2546
+
2547
+ ```ruby
2548
+ # config/initializers/lhs.rb
2549
+ LHS.configure do |config|
2550
+ config.auto_oauth = proc do
2551
+ {
2552
+ provider1: access_token_provider_1,
2553
+ provider2: access_token_provider_2
2554
+ }
2555
+ end
2556
+ end
2557
+ ```
2558
+
2559
+ Then make sure you either define which provider to use on a record level:
2560
+
2561
+ ```ruby
2562
+ # model/record.rb
2563
+ class Record < LHS::Record
2564
+ oauth(:provider1)
2565
+ #...
2566
+ end
2567
+ ```
2568
+
2569
+ or on an endpoint level:
2570
+
2571
+ ```ruby
2572
+ # model/record.rb
2573
+ class Record < LHS::Record
2574
+ endpoint 'https://service/records', oauth: :provider1
2575
+ #...
2576
+ end
2577
+ ```
2578
+
2579
+ ### Configure providers
2580
+
2581
+ If you're using LHS service providers, you can also configure auto auth on a provider level:
2582
+
2583
+ ```ruby
2584
+ # app/models/providers/localsearch.rb
2585
+ module Providers
2586
+ class Localsearch < LHS::Record
2587
+
2588
+ provider(
2589
+ oauth: true
2590
+ )
2591
+ end
2592
+ end
2593
+ ```
2594
+
2595
+ or with multiple auth providers:
2596
+
2597
+ ```ruby
2598
+ # app/models/providers/localsearch.rb
2599
+ module Providers
2600
+ class Localsearch < LHS::Record
2601
+
2602
+ provider(
2603
+ oauth: :provider_1
2604
+ )
2605
+ end
2606
+ end
2607
+ ```
2608
+
2609
+ ## Option Blocks
2610
+
2611
+ In order to apply options to all requests performed in a give block, LHS provides option blocks.
2612
+
2613
+ ```ruby
2614
+ # app/controllers/records_controller.rb
2615
+
2616
+ LHS.options(headers: { 'Tracking-Id' => 123 }) do
2617
+ Record.find(1)
2618
+ end
2619
+
2620
+ Record.find(2)
2621
+ ```
2622
+ ```
2623
+ GET https://records/1 { headers: { 'Tracking-Id' => '123' } }
2624
+ GET https://records/2 { headers: { } }
2625
+ ```
2626
+
2627
+ ## Request tracing
2628
+
2629
+ LHS supports tracing the source (in your application code) of http requests being made with methods like `find find_by find_by! first first! last last!`.
2630
+
2631
+ Following links, and using `includes` are not traced (just yet).
2632
+
2633
+ In order to enable tracing you need to enable it via LHS configuration:
2634
+
2635
+ ```ruby
2636
+ # config/initializers/lhs.rb
2637
+
2638
+ LHS.configure do |config|
2639
+ config.trace = Rails.env.development? || Rails.logger.level == 0 # debug
2640
+ end
2641
+ ```
2642
+
2643
+ ```ruby
2644
+ # app/controllers/application_controller.rb
2645
+
2646
+ code = Code.find(code: params[:code])
2647
+ ```
2648
+ ```
2649
+ Called from onboarding/app/controllers/concerns/access_code_concern.rb:11:in `access_code'
2650
+ ```
2651
+
2652
+ However, following links and includes won't get traced (just yet):
2653
+
2654
+ ```ruby
2655
+ # app/controllers/application_controller.rb
2656
+
2657
+ code = Code.includes(:places).find(123)
2658
+ ```
2659
+
2660
+ ```
2661
+ # Nothing is traced
2662
+ {
2663
+ places: [...]
2664
+ }
2665
+ ```
2666
+
2667
+ ```ruby
2668
+ code.places
2669
+ ```
2670
+ ```
2671
+ {
2672
+ token: "XYZABCDEF",
2673
+ places:
2674
+ [
2675
+ { href: "http://storage-stg.preprod-local.ch/v2/places/egZelgYhdlg" }
2676
+ ]
2677
+ }
2678
+ ```
2679
+
2680
+ ## Extended Rollbar Logging
2681
+
2682
+ In order to log all requests/responses prior to an exception reported by Rollbar in addition to the exception itself, use the `LHS::ExtendedRollbar` interceptor in combination with the rollbar processor/handler:
2683
+
2684
+ ```ruby
2685
+ # config/initializers/lhc.rb
2686
+
2687
+ LHC.configure do |config|
2688
+ config.interceptors = [LHS::ExtendedRollbar]
2689
+ end
2690
+ ```
2691
+
2692
+ ```ruby
2693
+ # config/initializers/rollbar.rb
2694
+
2695
+ Rollbar.configure do |config|
2696
+ config.before_process << LHS::Interceptors::ExtendedRollbar::Handler.init
2697
+ end
2698
+ ```
2699
+
2700
+ ## Testing with LHS
2701
+
2702
+ **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.**
2703
+
2704
+ 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.
2705
+
2706
+ ```ruby
2707
+ # specs/*/some_spec.rb
2708
+
2709
+ let(:contracts) do
2710
+ [
2711
+ {number: '1'},
2712
+ {number: '2'},
2713
+ {number: '3'}
2714
+ ]
2715
+ end
2716
+
2717
+ before do
2718
+ stub_request(:get, "https://service.example.com/contracts")
2719
+ .to_return(
2720
+ body: {
2721
+ items: contracts,
2722
+ limit: 10,
2723
+ total: contracts.length,
2724
+ offset: 0
2725
+ }.to_json
2726
+ )
2727
+ end
2728
+
2729
+ it 'displays contracts' do
2730
+ visit 'contracts'
2731
+ contracts.each do |contract|
2732
+ expect(page).to have_content(contract[:number])
2733
+ end
2734
+ end
2735
+ ```
2736
+
2737
+ ### Test helper
2738
+
2739
+ In order to load LHS test helpers into your tests, add the following to your spec helper:
2740
+
2741
+ ```ruby
2742
+ # spec/spec_helper.rb
2743
+
2744
+ require 'lhs/rspec'
2745
+ ```
2746
+
2747
+ This e.g. will prevent running into caching issues during your tests, when (request cycle cache)[#request-cycle-cache] is enabled.
2748
+ It will initialize a MemoryStore cache for LHC::Caching interceptor and resets the cache before every test.
2749
+
2750
+ #### Stub
2751
+
2752
+ LHS offers stub helpers that simplify stubbing https request to your apis through your defined Records.
2753
+
2754
+ ##### stub_all
2755
+
2756
+ `Record.stub_all(url, items, additional_options)`
2757
+
2758
+ ```ruby
2759
+ # your_spec.rb
2760
+
2761
+ before do
2762
+ class Record < LHS::Record
2763
+ endpoint 'https://records'
2764
+ end
2765
+
2766
+ Record.stub_all(
2767
+ 'https://records',
2768
+ 200.times.map{ |index| { name: "Item #{index}" } },
2769
+ headers: {
2770
+ 'Authorization' => 'Bearer 123'
2771
+ }
2772
+ )
2773
+ end
2774
+ ```
2775
+ ```
2776
+ GET https://records?limit=100
2777
+ GET https://records?limit=100&offset=100
2778
+ ```
2779
+
2780
+ LHS also uses Record configuration when stubbing all.
2781
+ ```ruby
2782
+ # your_spec.rb
2783
+
2784
+ before do
2785
+ class Record < LHS::Record
2786
+ configuration limit_key: :per_page, pagination_strategy: :page, pagination_key: :page
2787
+
2788
+ endpoint 'https://records'
2789
+ end
2790
+
2791
+ Record.stub_all(
2792
+ 'https://records',
2793
+ 200.times.map{ |index| { name: "Item #{index}" } }
2794
+ )
2795
+ end
2796
+ ```
2797
+ ```
2798
+ GET https://records?per_page=100
2799
+ GET https://records?per_page=100&page=2
2800
+ ```
2801
+
2802
+ ### Test query chains
2803
+
2804
+ #### By explicitly resolving the chain: fetch
2805
+
2806
+ Use `fetch` in tests to resolve chains in place and expect WebMock stubs to be requested.
2807
+
2808
+ ```ruby
2809
+ # specs/*/some_spec.rb
2810
+
2811
+ records = Record.where(color: 'blue').where(available: true).where(color: 'red')
2812
+
2813
+ expect(
2814
+ records.fetch
2815
+ ).to have_requested(:get, %r{records/})
2816
+ .with(query: hash_including(color: 'blue', available: true))
2817
+ ```
2818
+
2819
+ #### Without resolving the chain: where_values_hash
2820
+
2821
+ 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:
2822
+
2823
+ ```ruby
2824
+ # specs/*/some_spec.rb
2825
+
2826
+ records = Record.where(color: 'blue').where(available: true).where(color: 'red')
2827
+
2828
+ expect(
2829
+ records.where_values_hash
2830
+ ).to eq {color: 'red', available: true}
2831
+ ```
2832
+
2833
+ ## License
2834
+
2835
+ [GNU General Public License Version 3.](https://www.gnu.org/licenses/gpl-3.0.en.html)
2836
+