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
@@ -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