dhs 1.0.0

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