alchemrest 3.1.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 (245) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/.rubocop_todo.yml +242 -0
  5. data/.ruby-version +1 -0
  6. data/Appraisals +19 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +378 -0
  9. data/Rakefile +29 -0
  10. data/alchemrest.gemspec +71 -0
  11. data/coach.yml +5 -0
  12. data/examples/bank_api/client.rb +31 -0
  13. data/examples/bank_api/data/account.rb +21 -0
  14. data/examples/bank_api/data/ach.rb +16 -0
  15. data/examples/bank_api/data/business_account.rb +22 -0
  16. data/examples/bank_api/data/card.rb +21 -0
  17. data/examples/bank_api/data/check.rb +19 -0
  18. data/examples/bank_api/data/product.rb +20 -0
  19. data/examples/bank_api/data/transaction.rb +49 -0
  20. data/examples/bank_api/data/user.rb +27 -0
  21. data/examples/bank_api/factories.rb +68 -0
  22. data/examples/bank_api/graph_visualization.rb +45 -0
  23. data/examples/bank_api/positive_interest_string.rb +33 -0
  24. data/examples/bank_api/requests/delete_user.rb +17 -0
  25. data/examples/bank_api/requests/get_business_account.rb +24 -0
  26. data/examples/bank_api/requests/get_products.rb +12 -0
  27. data/examples/bank_api/requests/get_transactions.rb +34 -0
  28. data/examples/bank_api/requests/get_user.rb +19 -0
  29. data/examples/bank_api/requests/post_transaction.rb +20 -0
  30. data/examples/bank_api/requests/update_user.rb +28 -0
  31. data/examples/bank_api/root.rb +52 -0
  32. data/examples/bank_api.rb +33 -0
  33. data/gemfiles/faraday_2.gemfile +9 -0
  34. data/gemfiles/faraday_2.gemfile.lock +363 -0
  35. data/gemfiles/rails_7_0.gemfile.lock +341 -0
  36. data/gemfiles/rails_7_2.gemfile +9 -0
  37. data/gemfiles/rails_7_2.gemfile.lock +384 -0
  38. data/gemfiles/rails_8_0.gemfile +9 -0
  39. data/gemfiles/rails_8_0.gemfile.lock +385 -0
  40. data/lib/alchemrest/circuit_breaker.rb +84 -0
  41. data/lib/alchemrest/client/configuration/connection.rb +83 -0
  42. data/lib/alchemrest/client/configuration.rb +89 -0
  43. data/lib/alchemrest/client.rb +48 -0
  44. data/lib/alchemrest/cop.rb +8 -0
  45. data/lib/alchemrest/data/capture_configuration.rb +77 -0
  46. data/lib/alchemrest/data/field.rb +36 -0
  47. data/lib/alchemrest/data/graph.rb +40 -0
  48. data/lib/alchemrest/data/record.rb +60 -0
  49. data/lib/alchemrest/data/schema.rb +80 -0
  50. data/lib/alchemrest/data.rb +9 -0
  51. data/lib/alchemrest/endpoint_definition.rb +47 -0
  52. data/lib/alchemrest/error.rb +122 -0
  53. data/lib/alchemrest/factory_bot.rb +64 -0
  54. data/lib/alchemrest/faraday_middleware/external_api_instrumentation.rb +24 -0
  55. data/lib/alchemrest/faraday_middleware/json_parser.rb +30 -0
  56. data/lib/alchemrest/faraday_middleware/kill_switch.rb +22 -0
  57. data/lib/alchemrest/faraday_middleware/underscore_response.rb +24 -0
  58. data/lib/alchemrest/hash_path.rb +81 -0
  59. data/lib/alchemrest/http_request.rb +75 -0
  60. data/lib/alchemrest/kill_switch/adapters.rb +88 -0
  61. data/lib/alchemrest/kill_switch.rb +31 -0
  62. data/lib/alchemrest/railtie.rb +25 -0
  63. data/lib/alchemrest/request/endpoint.rb +29 -0
  64. data/lib/alchemrest/request/returns.rb +46 -0
  65. data/lib/alchemrest/request.rb +80 -0
  66. data/lib/alchemrest/request_definition/builder.rb +13 -0
  67. data/lib/alchemrest/request_definition.rb +26 -0
  68. data/lib/alchemrest/response/pipeline/extract_payload.rb +64 -0
  69. data/lib/alchemrest/response/pipeline/final.rb +11 -0
  70. data/lib/alchemrest/response/pipeline/omit.rb +46 -0
  71. data/lib/alchemrest/response/pipeline/sanitize.rb +59 -0
  72. data/lib/alchemrest/response/pipeline/transform.rb +26 -0
  73. data/lib/alchemrest/response/pipeline/was_successful.rb +29 -0
  74. data/lib/alchemrest/response/pipeline.rb +71 -0
  75. data/lib/alchemrest/response.rb +73 -0
  76. data/lib/alchemrest/response_captured_handler.rb +68 -0
  77. data/lib/alchemrest/result/halt.rb +15 -0
  78. data/lib/alchemrest/result/try_helpers.rb +16 -0
  79. data/lib/alchemrest/result.rb +128 -0
  80. data/lib/alchemrest/root.rb +77 -0
  81. data/lib/alchemrest/transforms/base_to_type_transform_registry.rb +42 -0
  82. data/lib/alchemrest/transforms/constrainable.rb +41 -0
  83. data/lib/alchemrest/transforms/constraint/block.rb +22 -0
  84. data/lib/alchemrest/transforms/constraint/greater_than.rb +19 -0
  85. data/lib/alchemrest/transforms/constraint/greater_than_or_eq.rb +19 -0
  86. data/lib/alchemrest/transforms/constraint/in_list.rb +19 -0
  87. data/lib/alchemrest/transforms/constraint/is_instance_of.rb +19 -0
  88. data/lib/alchemrest/transforms/constraint/is_uuid.rb +19 -0
  89. data/lib/alchemrest/transforms/constraint/less_than.rb +19 -0
  90. data/lib/alchemrest/transforms/constraint/less_than_or_eq.rb +19 -0
  91. data/lib/alchemrest/transforms/constraint/matches_regex.rb +19 -0
  92. data/lib/alchemrest/transforms/constraint/max_length.rb +19 -0
  93. data/lib/alchemrest/transforms/constraint/min_length.rb +19 -0
  94. data/lib/alchemrest/transforms/constraint.rb +17 -0
  95. data/lib/alchemrest/transforms/constraint_builder/for_number.rb +25 -0
  96. data/lib/alchemrest/transforms/constraint_builder/for_string.rb +21 -0
  97. data/lib/alchemrest/transforms/constraint_builder.rb +15 -0
  98. data/lib/alchemrest/transforms/date_transform.rb +30 -0
  99. data/lib/alchemrest/transforms/enum.rb +52 -0
  100. data/lib/alchemrest/transforms/epoch_time.rb +32 -0
  101. data/lib/alchemrest/transforms/from_chain.rb +15 -0
  102. data/lib/alchemrest/transforms/from_number/to_type_transform_registry.rb +18 -0
  103. data/lib/alchemrest/transforms/from_number.rb +47 -0
  104. data/lib/alchemrest/transforms/from_string/to_type_transform_registry.rb +17 -0
  105. data/lib/alchemrest/transforms/from_string.rb +36 -0
  106. data/lib/alchemrest/transforms/from_type/empty_to_type_transform_registry.rb +14 -0
  107. data/lib/alchemrest/transforms/from_type.rb +64 -0
  108. data/lib/alchemrest/transforms/iso_time.rb +58 -0
  109. data/lib/alchemrest/transforms/json_number.rb +26 -0
  110. data/lib/alchemrest/transforms/loose_hash.rb +96 -0
  111. data/lib/alchemrest/transforms/money_transform.rb +42 -0
  112. data/lib/alchemrest/transforms/number.rb +27 -0
  113. data/lib/alchemrest/transforms/output_type.rb +65 -0
  114. data/lib/alchemrest/transforms/to_decimal.rb +22 -0
  115. data/lib/alchemrest/transforms/to_type/from_string_to_time_selector.rb +29 -0
  116. data/lib/alchemrest/transforms/to_type/transforms_selector.rb +61 -0
  117. data/lib/alchemrest/transforms/to_type.rb +86 -0
  118. data/lib/alchemrest/transforms/typed.rb +32 -0
  119. data/lib/alchemrest/transforms/union.rb +44 -0
  120. data/lib/alchemrest/transforms/with_constraint.rb +26 -0
  121. data/lib/alchemrest/transforms.rb +93 -0
  122. data/lib/alchemrest/url_builder/encoders.rb +39 -0
  123. data/lib/alchemrest/url_builder/options.rb +33 -0
  124. data/lib/alchemrest/url_builder.rb +31 -0
  125. data/lib/alchemrest/version.rb +5 -0
  126. data/lib/alchemrest/webmock_helpers.rb +27 -0
  127. data/lib/alchemrest.rb +159 -0
  128. data/lib/generators/alchemrest/kill_switch_migration_generator.rb +27 -0
  129. data/lib/generators/alchemrest/templates/kill_switch_migration.rb.erb +17 -0
  130. data/lib/rubocop/cop/alchemrest/define_request_using_with_params.rb +53 -0
  131. data/lib/rubocop/cop/alchemrest/endpoint_definition_using_generic_params.rb +55 -0
  132. data/lib/rubocop/cop/alchemrest/request_hash_returning_block.rb +54 -0
  133. data/lib/rubocop/cop/alchemrest/time_transform_with_no_zone.rb +56 -0
  134. data/lib/tapioca/dsl/compilers/alchemrest_data.rb +84 -0
  135. data/lib/tapioca/dsl/compilers/alchemrest_root.rb +68 -0
  136. data/mutant.yml +16 -0
  137. data/rbi/alchemrest/result.rbi +80 -0
  138. data/rbi/alchemrest.rbi +246 -0
  139. data/sorbet/config +5 -0
  140. data/sorbet/rbi/gems/.gitattributes +1 -0
  141. data/sorbet/rbi/gems/abstract_type@0.0.7.rbi +41 -0
  142. data/sorbet/rbi/gems/actionpack@8.0.4.rbi +11733 -0
  143. data/sorbet/rbi/gems/actionview@8.0.4.rbi +6560 -0
  144. data/sorbet/rbi/gems/activemodel@8.0.4.rbi +2891 -0
  145. data/sorbet/rbi/gems/activesupport@8.0.4.rbi +9621 -0
  146. data/sorbet/rbi/gems/adamantium@0.2.0.rbi +144 -0
  147. data/sorbet/rbi/gems/addressable@2.8.7.rbi +779 -0
  148. data/sorbet/rbi/gems/anima@0.3.2.rbi +103 -0
  149. data/sorbet/rbi/gems/ast@2.4.2.rbi +107 -0
  150. data/sorbet/rbi/gems/base64@0.3.0.rbi +52 -0
  151. data/sorbet/rbi/gems/benchmark@0.5.0.rbi +153 -0
  152. data/sorbet/rbi/gems/bigdecimal@3.3.1.rbi +77 -0
  153. data/sorbet/rbi/gems/builder@3.3.0.rbi +9 -0
  154. data/sorbet/rbi/gems/circuitbox@2.0.0.rbi +297 -0
  155. data/sorbet/rbi/gems/concord@0.1.6.rbi +51 -0
  156. data/sorbet/rbi/gems/concurrent-ruby@1.3.5.rbi +4716 -0
  157. data/sorbet/rbi/gems/connection_pool@2.5.4.rbi +9 -0
  158. data/sorbet/rbi/gems/crack@1.0.0.rbi +110 -0
  159. data/sorbet/rbi/gems/crass@1.0.6.rbi +294 -0
  160. data/sorbet/rbi/gems/date@3.4.1.rbi +58 -0
  161. data/sorbet/rbi/gems/drb@2.2.3.rbi +639 -0
  162. data/sorbet/rbi/gems/equalizer@0.0.11.rbi +38 -0
  163. data/sorbet/rbi/gems/erubi@1.13.1.rbi +85 -0
  164. data/sorbet/rbi/gems/factory_bot@6.5.0.rbi +1529 -0
  165. data/sorbet/rbi/gems/faraday-em_http@1.0.0.rbi +181 -0
  166. data/sorbet/rbi/gems/faraday-em_synchrony@1.0.1.rbi +120 -0
  167. data/sorbet/rbi/gems/faraday-excon@1.1.0.rbi +128 -0
  168. data/sorbet/rbi/gems/faraday-httpclient@1.0.1.rbi +123 -0
  169. data/sorbet/rbi/gems/faraday-multipart@1.2.0.rbi +190 -0
  170. data/sorbet/rbi/gems/faraday-net_http@1.0.2.rbi +140 -0
  171. data/sorbet/rbi/gems/faraday-net_http_persistent@1.2.0.rbi +116 -0
  172. data/sorbet/rbi/gems/faraday-patron@1.0.0.rbi +119 -0
  173. data/sorbet/rbi/gems/faraday-rack@1.0.0.rbi +113 -0
  174. data/sorbet/rbi/gems/faraday-retry@1.0.3.rbi +149 -0
  175. data/sorbet/rbi/gems/faraday@1.10.5.rbi +1620 -0
  176. data/sorbet/rbi/gems/hansi@0.2.1.rbi +9 -0
  177. data/sorbet/rbi/gems/hashdiff@1.1.2.rbi +174 -0
  178. data/sorbet/rbi/gems/i18n@1.14.7.rbi +1328 -0
  179. data/sorbet/rbi/gems/ice_nine@0.11.2.rbi +145 -0
  180. data/sorbet/rbi/gems/io-console@0.8.0.rbi +9 -0
  181. data/sorbet/rbi/gems/json@2.9.1.rbi +282 -0
  182. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +8057 -0
  183. data/sorbet/rbi/gems/logger@1.7.0.rbi +260 -0
  184. data/sorbet/rbi/gems/loofah@2.24.0.rbi +571 -0
  185. data/sorbet/rbi/gems/memoizable@0.4.2.rbi +131 -0
  186. data/sorbet/rbi/gems/memosa@0.8.2.rbi +185 -0
  187. data/sorbet/rbi/gems/minitest@5.26.0.rbi +824 -0
  188. data/sorbet/rbi/gems/money@6.19.0.rbi +815 -0
  189. data/sorbet/rbi/gems/morpher@0.4.2.rbi +388 -0
  190. data/sorbet/rbi/gems/mprelude@0.1.0.rbi +140 -0
  191. data/sorbet/rbi/gems/multi_json@1.15.0.rbi +180 -0
  192. data/sorbet/rbi/gems/multipart-post@2.4.1.rbi +154 -0
  193. data/sorbet/rbi/gems/mustermann-contrib@3.0.3.rbi +9 -0
  194. data/sorbet/rbi/gems/mustermann@3.0.3.rbi +809 -0
  195. data/sorbet/rbi/gems/netrc@0.11.0.rbi +112 -0
  196. data/sorbet/rbi/gems/nokogiri@1.19.1.rbi +3412 -0
  197. data/sorbet/rbi/gems/parallel@1.26.3.rbi +234 -0
  198. data/sorbet/rbi/gems/parser@3.3.7.0.rbi +4877 -0
  199. data/sorbet/rbi/gems/pp@0.6.2.rbi +176 -0
  200. data/sorbet/rbi/gems/prettyprint@0.2.0.rbi +155 -0
  201. data/sorbet/rbi/gems/prism@1.5.1.rbi +26368 -0
  202. data/sorbet/rbi/gems/procto@0.0.3.rbi +9 -0
  203. data/sorbet/rbi/gems/psych@5.2.3.rbi +806 -0
  204. data/sorbet/rbi/gems/public_suffix@6.0.1.rbi +267 -0
  205. data/sorbet/rbi/gems/racc@1.8.1.rbi +120 -0
  206. data/sorbet/rbi/gems/rack-session@2.1.1.rbi +458 -0
  207. data/sorbet/rbi/gems/rack-test@2.2.0.rbi +405 -0
  208. data/sorbet/rbi/gems/rack@3.1.14.rbi +2774 -0
  209. data/sorbet/rbi/gems/rackup@2.2.1.rbi +132 -0
  210. data/sorbet/rbi/gems/rails-dom-testing@2.2.0.rbi +266 -0
  211. data/sorbet/rbi/gems/rails-html-sanitizer@1.6.2.rbi +545 -0
  212. data/sorbet/rbi/gems/railties@8.0.4.rbi +2150 -0
  213. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +333 -0
  214. data/sorbet/rbi/gems/rake@13.2.1.rbi +2054 -0
  215. data/sorbet/rbi/gems/rbi@0.2.3.rbi +3961 -0
  216. data/sorbet/rbi/gems/rdoc@6.13.1.rbi +6784 -0
  217. data/sorbet/rbi/gems/regexp_parser@2.11.3.rbi +3020 -0
  218. data/sorbet/rbi/gems/reline@0.6.0.rbi +9 -0
  219. data/sorbet/rbi/gems/rexml@3.4.2.rbi +1777 -0
  220. data/sorbet/rbi/gems/rubocop-ast@1.38.0.rbi +5293 -0
  221. data/sorbet/rbi/gems/rubocop@1.71.1.rbi +31846 -0
  222. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +980 -0
  223. data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +9 -0
  224. data/sorbet/rbi/gems/securerandom@0.4.1.rbi +33 -0
  225. data/sorbet/rbi/gems/sentry-ruby@5.22.1.rbi +3782 -0
  226. data/sorbet/rbi/gems/spoom@1.5.1.rbi +4321 -0
  227. data/sorbet/rbi/gems/stringio@3.1.2.rbi +9 -0
  228. data/sorbet/rbi/gems/tapioca@0.16.8.rbi +3399 -0
  229. data/sorbet/rbi/gems/thor@1.3.2.rbi +2012 -0
  230. data/sorbet/rbi/gems/thread_safe@0.3.6.rbi +711 -0
  231. data/sorbet/rbi/gems/timeout@0.4.4.rbi +80 -0
  232. data/sorbet/rbi/gems/tsort@0.2.0.rbi +50 -0
  233. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +1677 -0
  234. data/sorbet/rbi/gems/unicode-display_width@2.6.0.rbi +62 -0
  235. data/sorbet/rbi/gems/uri@1.1.0.rbi +760 -0
  236. data/sorbet/rbi/gems/useragent@0.16.11.rbi +9 -0
  237. data/sorbet/rbi/gems/webmock@3.24.0.rbi +1362 -0
  238. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +345 -0
  239. data/sorbet/rbi/gems/yard@0.9.37.rbi +8795 -0
  240. data/sorbet/rbi/gems/zeitwerk@2.7.1.rbi +589 -0
  241. data/sorbet/tapioca/config.yml +45 -0
  242. data/sorbet/tapioca/require.rb +8 -0
  243. data/sorbet/tapioca/sorbet/rbi/dsl/.gitattributes +1 -0
  244. data/sorbet/tapioca/sorbet/rbi/dsl/active_support/callbacks.rbi +23 -0
  245. metadata +737 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ # A utility for executing transformations against hashes scoped to specific parts of the hash
5
+ # defined by the `segments` of the path. The `Alchemrest::HashPath` class has one method `#walk`
6
+ # which will "walk" the path defined by the segments, yielding to a block provided by the calling code
7
+ # at each node it stops at. In that block, the calling code can transform the hash as it likes
8
+ #
9
+ # @example Using `Alchemrest::HashPath` to delete specific nodes from a hash
10
+ # input = { user: { accounts: [{account_id: 1, transactions: [{transaction_id: 1, amount: 100}] }] } }
11
+ # path = Alchemrest::HashPath.new([:user, :accounts, :transactions, :amount])
12
+ # path.walk(input) do |_path, node, remaining_segments|
13
+ # if(remaining_segments.count == 1)
14
+ # node.delete(remaining_segments.last.key)
15
+ # end
16
+ # end
17
+ # input #=> { user: { accounts: [{account_id: 1, transactions: [{transaction_id: 1}] }] } }
18
+ class HashPath
19
+ include Concord::Public.new(:segments)
20
+
21
+ # A helper method to quickly build a collection of hash paths from a rails style "StrongParams" hash arg.
22
+ # @example Create multiple hash paths from a strong params style hash
23
+ # Alchemrest::HashPath.build_collection(user: { accounts: { transactions: [:id, :amount] } }) #=>
24
+ # [
25
+ # Alchemrest::HashPath(segments: [:accounts, :transactions, :id]),
26
+ # Alchemrest::HashPath(segments: [:accounts, :transactions, :amount])
27
+ # ]
28
+ def self.build_collection(definition)
29
+ paths = []
30
+ visit_leaves(definition) do |value, path|
31
+ paths << (path + [value])
32
+ end
33
+ paths.map { |path| new(path) }
34
+ end
35
+
36
+ # "Walks" the path defined by the segments of the HashPath, including individual items in nested collections.
37
+ # For each items it walks, it yields to the provided block, passing the current full path to that item, it's value
38
+ # and the remaining segments of the path it has to walk. If it any point the input does not have a value for one
39
+ # of the segments, this method will stop traversing that portion of the hash. Note actions taken within the block
40
+ # are mutative, and will modify the original hash
41
+ def walk(input, &block)
42
+ traverse(input, segments, [], &block)
43
+ end
44
+
45
+ class << self
46
+ private
47
+
48
+ def visit_leaves(input, path = [], &block)
49
+ case input
50
+ when Hash
51
+ input.each_with_object(path) do |(key, value), path|
52
+ visit_leaves(value, path + [key], &block)
53
+ end
54
+ when Array
55
+ input.map { |i| visit_leaves(i, path, &block) }
56
+ else
57
+ yield input, path
58
+ end
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def traverse(node, remaining_segments, path, &block) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
65
+ yield path, node, remaining_segments
66
+
67
+ return if remaining_segments.empty?
68
+
69
+ current_segment = remaining_segments.first
70
+
71
+ if node.is_a?(Hash) && node.key?(current_segment)
72
+ rest_of_segments = remaining_segments[1..]
73
+ traverse(node[current_segment], rest_of_segments, path + [current_segment], &block)
74
+ elsif node.is_a?(Array) && node.any?(Hash)
75
+ node.each_with_index do |item, i|
76
+ traverse(item, remaining_segments, path + [i], &block) if item.is_a?(Hash)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module Alchemrest
6
+ class HttpRequest < SimpleDelegator
7
+ extend Memosa
8
+
9
+ def initialize(request, client)
10
+ @client = client
11
+ super(request)
12
+ end
13
+
14
+ # Have to disable this because mutant wans to
15
+ # push towards `to_str` which is not available in
16
+ # 7.1. Once we drop 7.1 support we can remove
17
+ # mutant:disable
18
+ def url
19
+ @client.connection.url_prefix.to_s.chop + path
20
+ end
21
+
22
+ def headers
23
+ { **default_headers, **super() }
24
+ end
25
+
26
+ def execute!
27
+ result = Result.for do |try|
28
+ raw_response = try.unwrap make_request!
29
+ transform_response(build_response(raw_response))
30
+ end
31
+
32
+ circuit_breaker.monitor!(result:)
33
+ result
34
+ end
35
+
36
+ private
37
+
38
+ def build_response(raw_response)
39
+ @client.build_response(raw_response)
40
+ end
41
+
42
+ memoize def circuit_breaker
43
+ @client.configuration.circuit_breaker
44
+ end
45
+
46
+ def make_request!
47
+ return Result::Error(CircuitOpenError.new) if circuit_breaker.open?
48
+
49
+ # We have to set the body in the block form, otherwise it will be ignored for
50
+ # delete requests. See https://github.com/lostisland/faraday/issues/693#issuecomment-466086832
51
+ response = @client.connection.public_send(http_method, path, nil, headers) do |req|
52
+ modify_faraday_request(req)
53
+ end
54
+ Result::Ok(response)
55
+ rescue Faraday::Error => e
56
+ handle_faraday_error(e)
57
+ rescue *Alchemrest.rescuable_exceptions => e
58
+ Result::Error(e)
59
+ end
60
+
61
+ def modify_faraday_request(req)
62
+ req.body = body unless http_method == 'get'
63
+ end
64
+
65
+ def handle_faraday_error(error)
66
+ if error.wrapped_exception.instance_of?(Net::OpenTimeout)
67
+ Result::Error(TimeoutError.new)
68
+ else
69
+ raise RequestFailedError
70
+ end
71
+ rescue Alchemrest::RequestFailedError => e
72
+ Result::Error(e)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ module Alchemrest
6
+ class KillSwitch
7
+ module Adapters
8
+ class Test
9
+ class Record
10
+ include Anima.new(:enabled)
11
+
12
+ alias enabled? enabled
13
+ end
14
+
15
+ private_constant :Record
16
+
17
+ def initialize
18
+ @records = {}
19
+ end
20
+
21
+ def ready?
22
+ true
23
+ end
24
+
25
+ def active?(service_name:)
26
+ load_record(service_name:).enabled?
27
+ end
28
+
29
+ def activate(service_name:)
30
+ set(service_name:, enabled: true)
31
+ end
32
+
33
+ def deactivate(service_name:)
34
+ set(service_name:, enabled: false)
35
+ end
36
+
37
+ private
38
+
39
+ def set(service_name:, enabled:)
40
+ record = load_record(service_name:).with(enabled:)
41
+ @records[service_name] = record
42
+ end
43
+
44
+ def load_record(service_name:)
45
+ raise 'service_name cannot be nil' unless service_name
46
+
47
+ @records[service_name] ||= Record.new enabled: false
48
+ end
49
+ end
50
+
51
+ class ActiveRecord
52
+ class Record < ::ActiveRecord::Base
53
+ self.table_name = 'alchemrest_kill_switches'
54
+ end
55
+
56
+ private_constant :Record
57
+
58
+ def ready?
59
+ Record.table_exists?
60
+ end
61
+
62
+ def active?(service_name:)
63
+ load_record(service_name:).enabled?
64
+ end
65
+
66
+ def activate(service_name:)
67
+ set(service_name:, enabled: true)
68
+ end
69
+
70
+ def deactivate(service_name:)
71
+ set(service_name:, enabled: false)
72
+ end
73
+
74
+ private
75
+
76
+ def set(service_name:, enabled:)
77
+ load_record(service_name:).update!(enabled:)
78
+ end
79
+
80
+ def load_record(service_name:)
81
+ raise 'service_name cannot be nil' unless service_name
82
+
83
+ Record.find_or_create_by!(service_name:)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class KillSwitch
5
+ include Anima.new(:service_name)
6
+
7
+ def initialize(service_name:)
8
+ raise ArgumentError, 'service_name is required' unless service_name
9
+
10
+ super
11
+ end
12
+
13
+ def active?
14
+ adapter.active?(service_name:)
15
+ end
16
+
17
+ def activate!
18
+ adapter.activate(service_name:)
19
+ end
20
+
21
+ def deactivate!
22
+ adapter.deactivate(service_name:)
23
+ end
24
+
25
+ private
26
+
27
+ def adapter
28
+ Alchemrest.kill_switch_adapter
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module Alchemrest
6
+ class Railtie < ::Rails::Railtie
7
+ initializer 'alchemrest.logger' do
8
+ Alchemrest.logger = Rails.logger
9
+ end
10
+
11
+ initializer 'alchemrest.configure_parameter_filter' do |app|
12
+ config.after_initialize do
13
+ Alchemrest.filter_parameters += app.config.filter_parameters
14
+ end
15
+ end
16
+
17
+ initializer 'alchemrest.deprecator' do |app|
18
+ app.deprecators[:alchemrest] = Alchemrest.deprecator
19
+ end
20
+
21
+ initializer 'alchemrest.deprecator.behavior' do |app|
22
+ Alchemrest.deprecator.behavior = app.config.active_support.deprecation
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Request
5
+ class Endpoint < Module
6
+ include Concord::Public.new(:endpoint_definition)
7
+
8
+ def included(_)
9
+ endpoint = self
10
+
11
+ define_method(:endpoint_definition) do
12
+ endpoint.endpoint_definition
13
+ end
14
+
15
+ include(InstanceMethods)
16
+ end
17
+
18
+ module InstanceMethods
19
+ def path
20
+ endpoint_definition.url_for(self)
21
+ end
22
+
23
+ def http_method
24
+ endpoint_definition.http_method
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Request
5
+ class Returns < Module
6
+ include Concord::Public.new(:domain_type, :path_to_payload, :allow_empty_response)
7
+
8
+ def included(request_class)
9
+ transformer_methods = Module.new
10
+ returns = self
11
+
12
+ transformer_methods.define_method(:response_transformer) do
13
+ super()
14
+ .append(Response::Pipeline::ExtractPayload.new(returns.path_to_payload, returns.allow_empty_response))
15
+ .append(returns.domain_type::TRANSFORM)
16
+ end
17
+
18
+ transformer_methods.define_method(:capture_transformer) do
19
+ super()
20
+ .replace_with(Response::Pipeline::Sanitize, returns.sanitize_step)
21
+ .append(returns.omit_step)
22
+ end
23
+
24
+ request_class.include(transformer_methods)
25
+ end
26
+
27
+ def sanitize_step
28
+ Response::Pipeline::Sanitize.new(
29
+ safe: HashPath.build_collection(capture_configuration.safe_paths),
30
+ )
31
+ end
32
+
33
+ def omit_step
34
+ Response::Pipeline::Omit.new(
35
+ HashPath.build_collection(capture_configuration.omitted_paths),
36
+ )
37
+ end
38
+
39
+ private
40
+
41
+ def capture_configuration
42
+ domain_type.capture_configuration.with_path_to_payload(path_to_payload)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mustermann/expander'
4
+
5
+ module Alchemrest
6
+ class Request
7
+ DEFAULT_HEADERS = IceNine.deep_freeze('User-Agent' => "Alchemrest/#{VERSION}")
8
+ private_constant(:DEFAULT_HEADERS)
9
+
10
+ def self.returns(domain_type, path_to_payload: nil, allow_empty_response: false)
11
+ include Returns.new(domain_type, path_to_payload, allow_empty_response)
12
+ end
13
+
14
+ def self.endpoint(http_method, template, &block)
15
+ builder_block = block
16
+ endpoint_definition = EndpointDefinition.new(template:, http_method: http_method.to_s, builder_block:)
17
+ include Endpoint.new(endpoint_definition)
18
+ end
19
+
20
+ def self.enable_response_capture
21
+ define_method(:response_capture_enabled) { true }
22
+ end
23
+
24
+ def self.disable_response_capture
25
+ define_method(:response_capture_enabled) { false }
26
+ end
27
+
28
+ def path
29
+ raise NotImplementedError, 'This is an abstract base method. Implement in your subclass.'
30
+ end
31
+
32
+ def http_method
33
+ raise NotImplementedError, 'This is an abstract base method. Implement in your subclass.'
34
+ end
35
+
36
+ def default_headers
37
+ DEFAULT_HEADERS
38
+ end
39
+
40
+ def headers
41
+ {}
42
+ end
43
+
44
+ def body
45
+ nil
46
+ end
47
+
48
+ def response_transformer
49
+ Response::Pipeline.new(
50
+ Response::Pipeline::WasSuccessful.new,
51
+ )
52
+ end
53
+
54
+ def capture_transformer
55
+ Response::Pipeline.new(
56
+ Response::Pipeline::ExtractPayload.new(nil, true),
57
+ Response::Pipeline::Sanitize.new,
58
+ )
59
+ end
60
+
61
+ def response_capture_enabled
62
+ true
63
+ end
64
+
65
+ def transform_response(response)
66
+ capture!(response:)
67
+ response_transformer.call(response)
68
+ end
69
+
70
+ def identifier
71
+ "#{http_method.upcase} #{path}"
72
+ end
73
+
74
+ private
75
+
76
+ def capture!(response:)
77
+ ResponseCapturedHandler.new(request: self, response:).call
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class RequestDefinition
5
+ class Builder
6
+ def initialize
7
+ @defaults = {}
8
+ end
9
+
10
+ attr_accessor :defaults
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ # A class that can be used to build an instance of an Alchemrest::Request class. The definition consists of the request class
5
+ # we want to create, and block that can extract default parameters from an Alchemrest::Root. When we want to build an
6
+ # instance of the request, call `build_request` and pass in the root as context, with any additional parameters.
7
+ class RequestDefinition
8
+ attr_reader :name
9
+
10
+ def initialize(name, request_class, &block)
11
+ @name = name
12
+ @request_class = request_class
13
+ @builder_block = block
14
+ end
15
+
16
+ def build_request(context, params = {})
17
+ builder = Builder.new
18
+
19
+ if @builder_block
20
+ context.instance_exec(builder, &@builder_block)
21
+ end
22
+
23
+ @request_class.new(**builder.defaults.merge(params))
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Response
5
+ class Pipeline
6
+ # A transform that can extract a nested payload from a response body. Takes in an
7
+ # array of symbols, strings, or integers that represent the path to the data you care about.
8
+ # This class assumes the response.body is a hash with string keys, so it calls to_s on all
9
+ # non integer path elements.
10
+ class ExtractPayload < Alchemrest::Response::Pipeline::Transform
11
+ include Concord::Public.new(:path_to_payload, :allow_empty_response)
12
+
13
+ # # All symbols will be converted to strings, and integers will be left alone.
14
+ # @param path_to_payload [Array<Symbol, String, Integer>] The path to the payload you care about
15
+ # @param _allow_empty_response [bool] Should HTTP 204 OkNoContent empty responses be allowed
16
+
17
+ def initialize(_path_to_payload = nil, _allow_empty_response = false) # rubocop:disable Style/OptionalBooleanParameter
18
+ super
19
+ end
20
+
21
+ def call(response)
22
+ # If response is empty and the status is 204 (No Content), return nil as the payload
23
+ if response.no_content_response?
24
+ return final(nil) if allow_empty_response?
25
+
26
+ return failure(ResponsePipelineError.new("Ok but empty response not allowed"))
27
+ end
28
+
29
+ payload = extract_payload(response)
30
+ if payload.nil?
31
+ failure(ResponsePipelineError.new("Response body did not contain expected payload at #{path_to_payload}"))
32
+ else
33
+ success(payload)
34
+ end
35
+ end
36
+
37
+ def extract_payload(response)
38
+ if path_to_payload.nil?
39
+ response.data
40
+ else
41
+ normalized_path = path_to_payload.map do |path_element|
42
+ if path_element.is_a?(Integer)
43
+ path_element
44
+ else
45
+ path_element.to_s
46
+ end
47
+ end
48
+ response.data.dig(*normalized_path)
49
+ end
50
+ # If our path tries to dig into a non hash, we'll get a TypeError. We want to rescue that and ensure
51
+ # our code continues to safely return a result.
52
+ rescue TypeError
53
+ nil
54
+ end
55
+
56
+ private
57
+
58
+ def allow_empty_response?
59
+ allow_empty_response
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Response
5
+ class Pipeline
6
+ class Final
7
+ include Concord::Public.new(:value)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Response
5
+ class Pipeline
6
+ class Omit < Morpher::Transform
7
+ include Concord::Public.new(:omit)
8
+ private_constant(*constants(false))
9
+ PRIMITIVE = Primitive.new(::Hash)
10
+
11
+ def initialize(omit = nil)
12
+ @omit = omit
13
+ super
14
+ end
15
+
16
+ def call(input)
17
+ if input.is_a?(::Array)
18
+ array.call(input)
19
+ else
20
+ PRIMITIVE
21
+ .call(input)
22
+ .bind { |i| omit_nodes(i) }
23
+ end
24
+ end
25
+
26
+ def omit_nodes(input)
27
+ dup = input.dup
28
+ if omit.present?
29
+ delete_from(dup)
30
+ end
31
+ success(dup)
32
+ end
33
+
34
+ def delete_from(input)
35
+ omit.each do |path|
36
+ path.walk(input) do |_path, node, remaining_segments|
37
+ if remaining_segments.count == 1
38
+ node.delete(remaining_segments.last)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Response
5
+ class Pipeline
6
+ class Sanitize < Morpher::Transform
7
+ include Anima.new(:safe)
8
+ PRIMITIVE = Primitive.new(::Hash)
9
+
10
+ def initialize(args = { safe: nil })
11
+ @safe = args[:safe]
12
+ super
13
+ end
14
+
15
+ def call(input)
16
+ if input.is_a?(::Array)
17
+ array.call(input)
18
+ else
19
+ PRIMITIVE
20
+ .call(input)
21
+ .bind { |i| sanitize(i) }
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def sanitize(input)
28
+ filtered = Alchemrest.parameter_filter.filter(input).with_indifferent_access
29
+ if safe.present?
30
+ copy(input, filtered)
31
+ end
32
+ success(filtered.deep_symbolize_keys)
33
+ end
34
+
35
+ def copy(original, filtered)
36
+ safe.each do |safe_path|
37
+ safe_path.walk(filtered) do |path, filtered_node, remaining_segments|
38
+ if remaining_segments.count == 1 && filtered_node.is_a?(::Hash)
39
+ next unless hash_has_key?(original, path, remaining_segments.last)
40
+
41
+ original_value = original.with_indifferent_access.dig(*(path + remaining_segments))
42
+ filtered_node[remaining_segments.last] = original_value
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def hash_has_key?(hash, path, key)
49
+ if path.empty?
50
+ hash.with_indifferent_access.key?(key)
51
+ else
52
+ hash.with_indifferent_access.dig(*path)
53
+ .key?(key)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end