lhs 24.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+