dhs 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ class DHS::Record
6
+
7
+ module HrefFor
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ def href_for(args = nil)
12
+ return unless [Integer, String].include?(args.class)
13
+ params = { id: args }
14
+ find_endpoint(params).compile(params)
15
+ end
16
+ alias url_for href_for
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ class DHS::Record
6
+
7
+ module Last
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ def last(options = nil)
12
+ options = trace!(options)
13
+ first_batch = find_by({}, options).parent
14
+ if first_batch.paginated?
15
+ pagination = first_batch._pagination
16
+ find_by({ pagination_key => pagination.class.page_to_offset(pagination.last_page, pagination.limit) }, options)
17
+ else
18
+ first_batch.last
19
+ end
20
+ end
21
+
22
+ def last!(options = nil)
23
+ find_by!({}, trace!(options))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ class DHS::Record
6
+
7
+ # Mapping allows to configure some accessors that access data using a provided proc
8
+ module Mapping
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+ def mapping
13
+ @mapping ||= {}
14
+ end
15
+
16
+ def mapping=(mapping)
17
+ @mapping = mapping
18
+ end
19
+
20
+ def map(name, block)
21
+ mapping[name] = block
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ class DHS::Record
6
+
7
+ module Merge
8
+ extend ActiveSupport::Concern
9
+
10
+ def merge(other)
11
+ _record.new(_data.to_h.merge(other.to_h))
12
+ end
13
+
14
+ def merge!(other)
15
+ _data._raw.merge!(other.to_h)
16
+ end
17
+
18
+ def deep_merge(other)
19
+ _record.new(_data.to_h.deep_merge(other.to_h))
20
+ end
21
+
22
+ def deep_merge!(other)
23
+ _data._raw.deep_merge!(other.to_h)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_model'
5
+
6
+ class DHS::Record
7
+
8
+ module Model
9
+ extend ActiveSupport::Concern
10
+
11
+ def to_model
12
+ self
13
+ end
14
+
15
+ def persisted?
16
+ href.present?
17
+ end
18
+
19
+ included do
20
+ extend ActiveModel::Naming
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ class DHS::Record
6
+
7
+ module Pagination
8
+ extend ActiveSupport::Concern
9
+ # Kaminari-Interface
10
+ delegate :current_page, :first_page, :last_page, :prev_page, :next_page, :limit_value, :total_pages, to: :_pagination
11
+
12
+ def paginated?(raw = nil)
13
+ self.class.paginated?(raw || _raw)
14
+ end
15
+
16
+ def _pagination
17
+ self.class.pagination(_data)
18
+ end
19
+
20
+ module ClassMethods
21
+ def pagination_class
22
+ case pagination_strategy.to_sym
23
+ when :page
24
+ DHS::Pagination::Page
25
+ when :total_pages
26
+ DHS::Pagination::TotalPages
27
+ when :start
28
+ DHS::Pagination::Start
29
+ when :link
30
+ DHS::Pagination::Link
31
+ else
32
+ DHS::Pagination::Offset
33
+ end
34
+ end
35
+
36
+ def pagination(data)
37
+ pagination_class.new(data)
38
+ end
39
+
40
+ # Checks if given raw is paginated or not
41
+ def paginated?(raw)
42
+ raw.is_a?(Hash) && (
43
+ raw.dig(*total_key).present? ||
44
+ raw.dig(*limit_key(:body)).present?
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext'
5
+
6
+ class DHS::Record
7
+
8
+ # A provider can define options used for that specific provider
9
+ module Provider
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ class_attribute :provider_options unless defined? provider_options
14
+ self.provider_options = nil
15
+ end
16
+
17
+ module ClassMethods
18
+ def provider(options = nil)
19
+ self.provider_options = options
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/class/attribute'
5
+
6
+ class DHS::Record
7
+
8
+ module Relations
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ class_attribute :_relations
13
+ self._relations = {}
14
+ end
15
+
16
+ module ClassMethods
17
+ def has_many(*options)
18
+ name = options[0]
19
+ options = options[1] || {}
20
+ _relations[name] = { record_class_name: options.fetch(:class_name, name.to_s.singularize.classify) }
21
+ end
22
+
23
+ alias has_one has_many
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,581 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/object'
5
+
6
+ class DHS::Record
7
+
8
+ module Request
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+ def request(options)
13
+ options ||= {}
14
+ options = deep_merge_with_option_blocks(options)
15
+ options = options.freeze
16
+ if options.is_a?(Array)
17
+ multiple_requests(
18
+ filter_empty_request_options(options)
19
+ )
20
+ else
21
+ single_request(options)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def deep_merge_with_option_blocks(options)
28
+ return options if DHS::OptionBlocks::CurrentOptionBlock.options.blank?
29
+ if options.is_a?(Hash)
30
+ options.deep_merge(DHS::OptionBlocks::CurrentOptionBlock.options)
31
+ elsif options.is_a?(Array)
32
+ options.map do |option|
33
+ return DHS::OptionBlocks::CurrentOptionBlock.options unless option
34
+ option.deep_merge(DHS::OptionBlocks::CurrentOptionBlock.options)
35
+ end
36
+ end
37
+ end
38
+
39
+ def single_request_load_and_merge_remaining_objects!(data, options, endpoint)
40
+ return if options[:all].blank? || !paginated
41
+ load_and_merge_remaining_objects!(
42
+ data: data,
43
+ options: process_options(options, endpoint),
44
+ load_not_paginated_collection: true
45
+ )
46
+ end
47
+
48
+ def filter_empty_request_options(options)
49
+ options.map do |option|
50
+ option if !option || !option.key?(:url) || !option[:url].nil?
51
+ end
52
+ end
53
+
54
+ # Applies limit to the first request of an all request chain
55
+ # Tries to apply an high value for limit and reacts on the limit
56
+ # returned by the endpoint to make further requests
57
+ def apply_limit!(options)
58
+ return if !paginated || options[:all].blank?
59
+ options[:params] ||= {}
60
+ options[:params] = options[:params].merge(limit_key(:parameter) => options[:params][limit_key(:parameter)] || DHS::Pagination::Base::DEFAULT_LIMIT)
61
+ end
62
+
63
+ # Convert URLs in options to endpoint templates
64
+ def convert_options_to_endpoints(options)
65
+ if options.is_a?(Array)
66
+ options.map { |request_options| convert_options_to_endpoint(request_options) }
67
+ else
68
+ convert_options_to_endpoint(options)
69
+ end
70
+ end
71
+
72
+ def convert_options_to_endpoint(options)
73
+ return if options.blank?
74
+ url = options[:url]
75
+ endpoint = DHS::Endpoint.for_url(url)
76
+ return unless endpoint
77
+ template = endpoint.url
78
+ new_options = options.deep_merge(
79
+ params: DHC::Endpoint.values_as_params(template, url).merge(values_from_get_params(url, options))
80
+ )
81
+ new_options[:url] = template
82
+ new_options
83
+ end
84
+
85
+ # Extracts values from url's get parameters
86
+ # and return them as a ruby hash
87
+ def values_from_get_params(url, options)
88
+ uri = parse_uri(url, options)
89
+ return {} if uri.query.blank?
90
+ params = Rack::Utils.parse_nested_query(uri.query).deep_symbolize_keys
91
+ params
92
+ end
93
+
94
+ def parse_uri(url, options)
95
+ URI.parse(
96
+ if url.match(Addressable::Template::EXPRESSION)
97
+ compute_url(options[:params], url)
98
+ else
99
+ url
100
+ end
101
+ )
102
+ end
103
+
104
+ def handle_includes(includes, data, references = {})
105
+ references ||= {}
106
+ if includes.is_a? Hash
107
+ includes.each { |included, sub_includes| handle_include(included, data, sub_includes, references[included]) }
108
+ elsif includes.is_a? Array
109
+ includes.each do |included|
110
+ handle_includes(included, data, references)
111
+ end
112
+ else
113
+ handle_include(includes, data, nil, references[includes])
114
+ end
115
+ data.clear_cache! if data.present? # as we just included new nested resources
116
+ end
117
+
118
+ def handle_include(included, data, sub_includes = nil, reference = nil)
119
+ if data.blank? || skip_loading_includes?(data, included)
120
+ handle_skip_include(included, data, sub_includes, reference)
121
+ else
122
+ options = options_for_data(data, included)
123
+ options = extend_with_reference(options, reference)
124
+ addition = load_existing_includes(options, data, sub_includes, reference)
125
+ data.extend!(addition, included)
126
+ expand_addition!(data, included, reference) unless expanded_data?(addition)
127
+ end
128
+ end
129
+
130
+ def handle_skip_include(included, data, sub_includes = nil, reference = nil)
131
+ return if sub_includes.blank?
132
+ handle_includes(sub_includes, data[included], reference)
133
+ end
134
+
135
+ def options_for_data(data, included = nil)
136
+ return options_for_multiple(data, included) if data.collection?
137
+ return options_for_nested_items(data, included) if included && data[included].collection?
138
+ url_option_for(data, included)
139
+ end
140
+
141
+ def expand_addition!(data, included, reference)
142
+ addition = data[included]
143
+ options = options_for_data(addition)
144
+ options = extend_with_reference(options, reference)
145
+ record = record_for_options(options) || self
146
+ options = convert_options_to_endpoints(options) if record_for_options(options)
147
+ expanded_data = record.request(options)
148
+ data.extend!(expanded_data, included)
149
+ end
150
+
151
+ def expanded_data?(addition)
152
+ return false if addition.blank?
153
+ if addition.item?
154
+ (addition._raw.keys - [:href]).any?
155
+ elsif addition.collection?
156
+ addition.any? do |item|
157
+ next if item.blank?
158
+ if item._raw.is_a?(Hash)
159
+ (item._raw.keys - [:href]).any?
160
+ elsif item._raw.is_a?(Array)
161
+ item.any? { |item| (item._raw.keys - [:href]).any? }
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ # Extends request options with options provided for this reference
168
+ def extend_with_reference(options, reference)
169
+ return options if reference.blank?
170
+ reference = reference.except(:url)
171
+ options ||= {}
172
+ if options.is_a?(Array)
173
+ options.map { |request_options| request_options.merge(reference) if request_options.present? }
174
+ elsif options.present?
175
+ options.merge(reference)
176
+ end
177
+ end
178
+
179
+ def skip_loading_includes?(data, included)
180
+ if data.collection?
181
+ data.to_a.none? { |item| item[included].present? }
182
+ elsif data.dig(included).blank?
183
+ true
184
+ elsif data[included].item? && data[included][:href].blank?
185
+ true
186
+ else
187
+ !data._raw.key?(included)
188
+ end
189
+ end
190
+
191
+ # After fetching the first page,
192
+ # we can evaluate if there are further remote objects remaining
193
+ # and after preparing all the requests that have to be made in order to fetch all
194
+ # remote items during this batch, they are fetched in parallel
195
+ def load_and_merge_remaining_objects!(data:, options:, load_not_paginated_collection: false)
196
+ if paginated?(data._raw)
197
+ load_and_merge_paginated_collection!(data, options)
198
+ elsif data.collection? && paginated?(data.first.try(:_raw))
199
+ load_and_merge_set_of_paginated_collections!(data, options)
200
+ elsif load_not_paginated_collection && data.collection?
201
+ warn('[Warning] "all" has been requested, but endpoint does not provide pagination meta data. If you just want to fetch the first response, use "where" or "fetch".')
202
+ load_and_merge_not_paginated_collection!(data, options)
203
+ end
204
+ end
205
+
206
+ def load_and_merge_not_paginated_collection!(data, options)
207
+ return if data.length.zero?
208
+ options = options.is_a?(Hash) ? options : {}
209
+ limit = options.dig(:params, limit_key(:parameter)) || pagination_class::DEFAULT_LIMIT
210
+ offset = options.dig(:params, pagination_key(:parameter)) || pagination_class::DEFAULT_OFFSET
211
+ options[:params] = options.fetch(:params, {}).merge(
212
+ limit_key(:parameter) => limit,
213
+ pagination_key(:parameter) => pagination_class.next_offset(
214
+ offset,
215
+ limit
216
+ )
217
+ )
218
+ additional_data = data._record.request(options)
219
+ additional_data.each do |item_data|
220
+ data.concat(input: data._raw, items: [item_data], record: self)
221
+ end
222
+ end
223
+
224
+ # sets nested data for a source object that needs to be accessed with a given path e.g. [:response, :total]
225
+ def set_nested_data(source, path, value)
226
+ return source[path] = value unless path.is_a?(Array)
227
+ path = path.dup
228
+ last = path.pop
229
+ path.inject(source, :fetch)[last] = value
230
+ end
231
+
232
+ def load_and_merge_paginated_collection!(data, options)
233
+ set_nested_data(data._raw, limit_key(:body), data.length) if data._raw.dig(*limit_key(:body)).blank? && !data.length.zero?
234
+ pagination = data._record.pagination(data)
235
+ return data unless pagination.pages_left?
236
+ record = data._record
237
+ if pagination.parallel?
238
+ load_and_merge_parallel_requests!(record, data, pagination, options)
239
+ else
240
+ load_and_merge_sequential_requests!(record, data, options, data._raw.dig(:next, :href), pagination)
241
+ end
242
+ end
243
+
244
+ def load_and_merge_parallel_requests!(record, data, pagination, options)
245
+ record.request(
246
+ options_for_next_batch(record, pagination, options)
247
+ ).each do |batch_data|
248
+ merge_batch_data_with_parent!(batch_data, data)
249
+ end
250
+ end
251
+
252
+ def load_and_merge_sequential_requests!(record, data, options, next_link, pagination)
253
+ warn '[WARNING] You are loading all pages from a resource paginated with links only. As this is performed sequentially, it can result in very poor performance! (https://github.com/DePayFi/dhs#pagination-strategy-link).'
254
+ while next_link.present?
255
+ page_data = record.request(
256
+ options.except(:all).merge(url: next_link)
257
+ )
258
+ next_link = page_data._raw.dig(:next, :href)
259
+ merge_batch_data_with_parent!(page_data, data, pagination)
260
+ end
261
+ end
262
+
263
+ def load_and_merge_set_of_paginated_collections!(data, options)
264
+ options_for_next_batch = []
265
+ options.each_with_index do |element, index|
266
+ next if element.nil?
267
+ record = data[index]._record
268
+ pagination = record.pagination(data[index])
269
+ next unless pagination.pages_left?
270
+ options_for_next_batch.push(
271
+ options_for_next_batch(record, pagination, options[index]).tap do |options|
272
+ options.each do |option|
273
+ option[:merge_with_index] = index
274
+ end
275
+ end
276
+ )
277
+ end
278
+ data._record.request(options_for_next_batch.flatten).each do |batch_data|
279
+ merge_batch_data_with_parent!(batch_data, data[batch_data._request.options[:merge_with_index]])
280
+ end
281
+ end
282
+
283
+ def load_existing_includes(options, data, sub_includes, references)
284
+ if data.collection? && data.any?(&:blank?)
285
+ # filter only existing items
286
+ loaded_includes = load_include(options.compact, data.compact, sub_includes, references)
287
+ # fill up skipped items before returning
288
+ data.each_with_index do |item, index|
289
+ next if item.present?
290
+ loaded_includes.insert(index, {})
291
+ end
292
+ loaded_includes
293
+ else
294
+ load_include(options, data, sub_includes, references)
295
+ end
296
+ end
297
+
298
+ # Load additional resources that are requested with include
299
+ def load_include(options, _data, sub_includes, references)
300
+ record = record_for_options(options) || self
301
+ options = convert_options_to_endpoints(options) if record_for_options(options)
302
+ prepare_options_for_include_request!(options, sub_includes, references)
303
+ if references && references[:all] # include all linked resources
304
+ load_include_all!(options, record, sub_includes, references)
305
+ else # simply request first page/batch
306
+ load_include_simple!(options, record)
307
+ end
308
+ end
309
+
310
+ def load_include_all!(options, record, sub_includes, references)
311
+ prepare_options_for_include_all_request!(options)
312
+ data = load_all_included!(record, options)
313
+ references.delete(:all) # for this reference all remote objects have been fetched
314
+ continue_including(data, sub_includes, references)
315
+ end
316
+
317
+ def load_include_simple!(options, record)
318
+ data = record.request(options)
319
+ warn "[WARNING] You included `#{options[:url]}`, but this endpoint is paginated. You might want to use `includes_all` instead of `includes` (https://github.com/DePayFi/dhs#includes_all-for-paginated-endpoints)." if data && paginated?(data._raw)
320
+ data
321
+ end
322
+
323
+ # Continues loading included resources after one complete batch/level has been fetched
324
+ def continue_including(data, included, reference)
325
+ return data if included.blank? || data.blank?
326
+ expand_data!(data, included, reference) unless expanded_data?(data)
327
+ handle_includes(included, data, reference)
328
+ data
329
+ end
330
+
331
+ def expand_data!(data, _included, reference)
332
+ options = options_for_data(data)
333
+ options = extend_with_reference(options, reference)
334
+ record = record_for_options(options) || self
335
+ options = convert_options_to_endpoints(options) if record_for_options(options)
336
+ expanded_data = record.request(options)
337
+ data.extend!(expanded_data)
338
+ end
339
+
340
+ # Loads all included/linked resources,
341
+ # paginates itself to ensure all records are fetched
342
+ def load_all_included!(record, options)
343
+ data = record.request(options)
344
+ pagination = data._record.pagination(data)
345
+ load_and_merge_remaining_objects!(data: data, options: options) if pagination.parallel?
346
+ data
347
+ end
348
+
349
+ def prepare_options_for_include_all_request!(options)
350
+ if options.is_a?(Array)
351
+ options.each do |option|
352
+ prepare_option_for_include_all_request!(option)
353
+ end
354
+ else
355
+ prepare_option_for_include_all_request!(options)
356
+ end
357
+ options
358
+ end
359
+
360
+ # When including all resources on one level, don't forward :includes & :references
361
+ # as we have to fetch all resources on this level first, before we continue_including
362
+ def prepare_option_for_include_all_request!(option)
363
+ return option if option.blank? || option[:url].nil?
364
+ uri = parse_uri(option[:url], option)
365
+ get_params = Rack::Utils.parse_nested_query(uri.query)
366
+ .symbolize_keys
367
+ .except(limit_key(:parameter), pagination_key(:parameter))
368
+ option[:params] ||= {}
369
+ option[:params].reverse_merge!(get_params)
370
+ option[:params][limit_key(:parameter)] ||= DHS::Pagination::Base::DEFAULT_LIMIT
371
+ option[:url] = option[:url].gsub("?#{uri.query}", '')
372
+ option.delete(:including)
373
+ option.delete(:referencing)
374
+ option
375
+ end
376
+
377
+ def prepare_options_for_include_request!(options, sub_includes, references)
378
+ if options.is_a?(Array)
379
+ options.each { |option| option.merge!(including: sub_includes, referencing: references) if sub_includes.present? }
380
+ elsif sub_includes.present?
381
+ options.merge!(including: sub_includes, referencing: references)
382
+ end
383
+ options || {}
384
+ end
385
+
386
+ def merge_batch_data_with_parent!(batch_data, parent_data, pagination = nil)
387
+ parent_data.concat(input: parent_data._raw, items: batch_data.raw_items, record: self)
388
+ return if pagination.present? && pagination.is_a?(DHS::Pagination::Link)
389
+ [limit_key(:body), total_key, pagination_key(:body)].each do |pagination_attribute|
390
+ set_nested_data(
391
+ parent_data._raw,
392
+ pagination_attribute,
393
+ batch_data._raw.dig(*pagination_attribute)
394
+ )
395
+ end
396
+ end
397
+
398
+ # Merge explicit params nested in 'params' namespace with original hash.
399
+ def merge_explicit_params!(params)
400
+ return true unless params
401
+ explicit_params = params[:params]
402
+ params.delete(:params)
403
+ params.merge!(explicit_params) if explicit_params
404
+ end
405
+
406
+ def multiple_requests(options)
407
+ options = options.map do |option|
408
+ next if option.blank?
409
+ process_options(option, find_endpoint(option[:params], option.fetch(:url, nil)))
410
+ end
411
+ data = DHC.request(options.compact).map do |response|
412
+ DHS::Data.new(response.body, nil, self, response.request)
413
+ end
414
+ including = DHS::Complex.reduce(options.compact.map { |options| options.delete(:including) }.compact)
415
+ referencing = DHS::Complex.reduce(options.compact.map { |options| options.delete(:referencing) }.compact)
416
+ data = restore_with_nils(data, locate_nils(options)) # nil objects in data provide location information for mapping
417
+ data = DHS::Data.new(data, nil, self)
418
+ handle_includes(including, data, referencing) if including.present? && data.present?
419
+ data
420
+ end
421
+
422
+ def locate_nils(array)
423
+ nils = []
424
+ array.each_with_index { |value, index| nils << index if value.nil? }
425
+ nils
426
+ end
427
+
428
+ def restore_with_nils(array, nils)
429
+ array = array.dup
430
+ nils.sort.each { |index| array.insert(index, nil) }
431
+ array
432
+ end
433
+
434
+ def options_for_multiple(data, key = nil)
435
+ data.map do |item|
436
+ url_option_for(item, key)
437
+ end.flatten
438
+ end
439
+
440
+ def options_for_nested_items(data, key = nil)
441
+ data[key].map do |item|
442
+ url_option_for(item)
443
+ end.flatten
444
+ end
445
+
446
+ def options_for_next_batch(record, pagination, options)
447
+ batch_options = []
448
+ pagination.pages_left.times do |index|
449
+ page_options = {
450
+ params: {
451
+ record.limit_key(:parameter) => pagination.limit,
452
+ record.pagination_key(:parameter) => pagination.next_offset(index + 1)
453
+ }
454
+ }
455
+ batch_options.push(
456
+ options.deep_dup.deep_merge(page_options)
457
+ )
458
+ end
459
+ batch_options
460
+ end
461
+
462
+ # Merge explicit params and take configured endpoints options as base
463
+ def process_options(options, endpoint)
464
+ ignored_errors = options[:ignored_errors]
465
+ options = options.deep_dup
466
+ options[:ignored_errors] = ignored_errors if ignored_errors.present?
467
+ options[:params]&.deep_symbolize_keys!
468
+ options[:rescue] = merge_error_handlers(options[:rescue]) if options[:rescue]
469
+ options = (provider_options || {})
470
+ .deep_merge(endpoint.options || {})
471
+ .deep_merge(options)
472
+ options[:url] = compute_url!(options[:params]) unless options.key?(:url)
473
+ merge_explicit_params!(options[:params])
474
+ options.delete(:params) if options[:params]&.empty?
475
+ inject_interceptors!(options)
476
+ options
477
+ end
478
+
479
+ def inject_interceptors!(options)
480
+ if DHS.config.request_cycle_cache_enabled
481
+ inject_interceptor!(
482
+ options,
483
+ DHS::Interceptors::RequestCycleCache::Interceptor,
484
+ DHC::Caching,
485
+ "[WARNING] Can't enable request cycle cache as DHC::Caching interceptor is not enabled/configured (see https://github.com/DePayFi/dhc/blob/master/README.md#caching-interceptor)!"
486
+ )
487
+ end
488
+
489
+ endpoint = find_endpoint(options[:params], options.fetch(:url, nil))
490
+ if auto_oauth? || (endpoint.options&.dig(:oauth) && DHS.config.auto_oauth) || options[:oauth]
491
+ inject_interceptor!(
492
+ options.merge!(record: self),
493
+ DHS::Interceptors::AutoOauth::Interceptor,
494
+ DHC::Auth,
495
+ "[WARNING] Can't enable auto oauth as DHC::Auth interceptor is not enabled/configured (see https://github.com/DePayFi/dhc/blob/master/README.md#authentication-interceptor)!"
496
+ )
497
+ end
498
+ end
499
+
500
+ def inject_interceptor!(options, interceptor, dependecy, warning)
501
+ interceptors = options[:interceptors] || DHC.config.interceptors
502
+ if interceptors.include?(dependecy)
503
+ # Ensure interceptor is prepend
504
+ interceptors.unshift(interceptor)
505
+ options[:interceptors] = interceptors
506
+ else
507
+ warn(warning)
508
+ end
509
+ end
510
+
511
+ # DHC supports only one error handler, merge all error handlers to one
512
+ # and reraise
513
+ def merge_error_handlers(handlers)
514
+ lambda do |response|
515
+ return_data = nil
516
+ error_class = DHC::Error.find(response)
517
+ error = error_class.new(error_class, response)
518
+ handlers = handlers.map(&:to_a).to_a.select { |handler_error_class, _| error.is_a? handler_error_class }
519
+ raise(error) unless handlers.any?
520
+ handlers.each do |_, handler|
521
+ handlers_return = handler.call(response)
522
+ return_data = handlers_return if handlers_return.present?
523
+ end
524
+ return return_data
525
+ end
526
+ end
527
+
528
+ def record_for_options(options)
529
+ records = []
530
+ if options.is_a?(Array)
531
+ options.compact.each do |option|
532
+ record = DHS::Record.for_url(option[:url])
533
+ next unless record
534
+ records.push(record)
535
+ end
536
+ raise 'Found more than one record that could be used to do the request' if records.uniq.count > 1
537
+ records.uniq.first
538
+ else # Hash
539
+ DHS::Record.for_url(options[:url])
540
+ end
541
+ end
542
+
543
+ def single_request(options)
544
+ options ||= {}
545
+ options = options.dup
546
+ including = options.delete(:including)
547
+ referencing = options.delete(:referencing)
548
+ endpoint = find_endpoint(options[:params], options.fetch(:url, nil))
549
+ apply_limit!(options)
550
+ response = DHC.request(process_options(options, endpoint))
551
+ return nil if !response.success? && response.error_ignored?
552
+ data = DHS::Data.new(response.body, nil, self, response.request, endpoint)
553
+ single_request_load_and_merge_remaining_objects!(data, options, endpoint)
554
+ expand_items(data, options[:expanded]) if data.collection? && options[:expanded]
555
+ handle_includes(including, data, referencing) if including.present? && data.present?
556
+ data
557
+ end
558
+
559
+ def expand_items(data, expand_options)
560
+ expand_options = {} unless expand_options.is_a?(Hash)
561
+ options = data.map do |item|
562
+ expand_options.merge(url: item.href)
563
+ end
564
+ expanded_data = request(options)
565
+ data.each_with_index do |item, index|
566
+ item.merge_raw!(expanded_data[index])
567
+ end
568
+ end
569
+
570
+ def url_option_for(item, key = nil)
571
+ link = key ? item[key] : item
572
+ return if link.blank?
573
+ return { url: link.href } unless link.collection?
574
+
575
+ link.map do |item|
576
+ { url: item.href } if item.present? && item.href.present?
577
+ end.compact
578
+ end
579
+ end
580
+ end
581
+ end