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,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Response
5
+ class Pipeline
6
+ # This base class simply ensures that if
7
+ # a transform returns an alchemrest error that
8
+ # was created but not raised, then we'll set the
9
+ # backtrace so it can be traced to the transform
10
+ # that generated it. Only really relevant for
11
+ # implementors creating custom transforms.
12
+ class Transform < Morpher::Transform
13
+ def failure(error)
14
+ if error.respond_to?(:backtrace) && error.backtrace.nil?
15
+ error.set_backtrace(caller)
16
+ end
17
+ super
18
+ end
19
+
20
+ def final(value)
21
+ success(Final.new(value))
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Response
5
+ class Pipeline
6
+ # A transform that checks to see if a response was successful, as defined by `response.success?`.
7
+ # If the response is successful, it just returns itself. If not it calls `response.error`.
8
+ # Can be used as the first step in any pipeline's where you want to short circuit execution
9
+ # if the response has an http error code.
10
+ class WasSuccessful < Alchemrest::Response::Pipeline::Transform
11
+ def eql?(other)
12
+ instance_of?(other.class)
13
+ end
14
+
15
+ def ==(other)
16
+ instance_of?(other.class)
17
+ end
18
+
19
+ def call(response)
20
+ if response.success?
21
+ success(response)
22
+ else
23
+ failure(response.error)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Response
5
+ # The `Alchemrest::Result::Pipeline` is a class that orchestrates running a sequence of
6
+ # `Morpher::Transform` instances starting with an `Alchemrest::Response` instance and ending
7
+ # with an `Alchemrest::Result`. The pipeline is responsible for feeding the output of one
8
+ # transform into the input of the next when a transform succeeds, and wrapping errors in
9
+ # an `Alchemrest::Result::Error` when a transform fails. Transform steps must meet the following
10
+ # criteria
11
+ # 1. Implement a call method
12
+ # 2. Return a `Either::Right` prelude if the transform is successful
13
+ # 3. Return a `Either::Left` prelude that wraps either a `Morpher::Transform::Error` or
14
+ # one of our `Alchemrest.rescuable_exceptions if the transform is unsuccessful
15
+ class Pipeline
16
+ include Concord::Public.new(:steps)
17
+
18
+ def initialize(*steps)
19
+ super(steps)
20
+ end
21
+
22
+ def call(response)
23
+ final = steps.reduce(response) do |current, step|
24
+ either = step.call(current)
25
+ return build_error_result(either.from_left) if either.left?
26
+ break either.from_right.value if either.from_right.instance_of?(Final)
27
+
28
+ either.from_right
29
+ end
30
+
31
+ Result::Ok(final)
32
+ end
33
+
34
+ def insert_after(step_class, step)
35
+ index = steps.index { |s| s.instance_of?(step_class) }
36
+ raise ArgumentError, "Step #{step_class} not found" if index.nil?
37
+
38
+ self.class.new(*steps.insert(index + 1, step))
39
+ end
40
+
41
+ def append(step)
42
+ self.class.new(*steps.append(step))
43
+ end
44
+
45
+ def replace_with(step_class, step)
46
+ index = steps.index { |s| s.instance_of?(step_class) }
47
+ raise ArgumentError, "Step #{step_class} not found" if index.nil?
48
+
49
+ new_steps = steps.dup
50
+ new_steps[index] = step
51
+
52
+ self.class.new(*new_steps)
53
+ end
54
+
55
+ private
56
+
57
+ def build_error_result(error)
58
+ case error
59
+ when Morpher::Transform::Error
60
+ wrapped_error = Alchemrest::MorpherTransformError.new(error)
61
+ wrapped_error.set_backtrace(caller)
62
+ Alchemrest::Result::Error(wrapped_error)
63
+ when *Alchemrest.rescuable_exceptions
64
+ Alchemrest::Result::Error(error)
65
+ else
66
+ raise "Invalid error object #{error}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+ require 'memosa'
5
+
6
+ module Alchemrest
7
+ class Response < SimpleDelegator
8
+ extend Memosa
9
+
10
+ def data
11
+ body
12
+ end
13
+
14
+ def to_result
15
+ if success?
16
+ Result.Ok(self)
17
+ else
18
+ error.set_backtrace(caller)
19
+ Result.Error(error)
20
+ end
21
+ end
22
+
23
+ def error_details
24
+ "Error with HTTP status: #{status}" unless success?
25
+ end
26
+
27
+ def server_error?
28
+ (500..599).cover?(status)
29
+ end
30
+
31
+ def client_error?
32
+ (400..499).cover?(status)
33
+ end
34
+
35
+ def auth_error?
36
+ [401, 403].include?(status)
37
+ end
38
+
39
+ def not_found_error?
40
+ status == 404
41
+ end
42
+
43
+ def no_content_response?
44
+ status == 204
45
+ end
46
+
47
+ def request_failed?
48
+ status >= 500 && status <= 599
49
+ end
50
+
51
+ def timeout?
52
+ false
53
+ end
54
+
55
+ def circuit_open?
56
+ false
57
+ end
58
+
59
+ memoize def error
60
+ if server_error?
61
+ ServerError.new(self)
62
+ elsif auth_error?
63
+ AuthError.new(self)
64
+ elsif not_found_error?
65
+ NotFoundError.new(self)
66
+ elsif client_error?
67
+ ClientError.new(self)
68
+ else
69
+ ResponseError.new(self)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class ResponseCapturedHandler
5
+ LEGACY_ON_RESPONSE_CAPTURED_METHOD_DEFINITION_MESSAGE = <<~MSG
6
+ Defining Alchemrest.on_response_captured like
7
+
8
+ `Alchemrest.on_response_captured { |data, response, request| ... }` is deprecated.
9
+
10
+ Going forward, method defintions should take the form
11
+
12
+ `Alchemrest.on_response_captured { |identifier:, result:| ... }`
13
+
14
+ where identifier is a string that identifies the http method and endpoint the request was made to
15
+ and result is an `Alchemrest::Result` object that contains the captured response data, or an Error
16
+ instance.
17
+ MSG
18
+
19
+ include Anima.new(:request, :response)
20
+
21
+ def call
22
+ return unless request.response_capture_enabled
23
+
24
+ unless Alchemrest.on_response_captured
25
+ default_capture_method
26
+ return
27
+ end
28
+
29
+ case Alchemrest.on_response_captured.arity
30
+ in 3
31
+ legacy_capture_handler
32
+ in 1
33
+ Alchemrest.on_response_captured.call(identifier:, result: capture_pipeline_result)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def capture_pipeline_result
40
+ request.capture_transformer.call(response)
41
+ end
42
+
43
+ def identifier
44
+ request.identifier
45
+ end
46
+
47
+ def legacy_capture_handler
48
+ Alchemrest.deprecator.warn LEGACY_ON_RESPONSE_CAPTURED_METHOD_DEFINITION_MESSAGE
49
+ data = case capture_pipeline_result
50
+ in Result::Ok(value)
51
+ value
52
+ in Result::Error(e)
53
+ "Error transforming captured response data: #{e}"
54
+ end
55
+
56
+ Alchemrest.on_response_captured.call(data, response, request)
57
+ end
58
+
59
+ def default_capture_method
60
+ case capture_pipeline_result
61
+ in Result::Ok(value: data)
62
+ Alchemrest.logger.info("Captured Alchemrest response for '#{identifier}': '#{data}'")
63
+ in Result::Error({error:})
64
+ Alchemrest.logger.error("Failed to capture Alchemrest response for '#{identifier}': '#{error}'")
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Result
5
+ class Halt < StandardError
6
+ attr_reader :error
7
+
8
+ def initialize(original_error)
9
+ "Must be an Alchemrest::Result:Error object" unless original_error.is_a? Result::Error
10
+ @error = original_error
11
+ super(original_error.to_s)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Result
5
+ module TryHelpers
6
+ def self.unwrap(result)
7
+ case result
8
+ in Result::Ok(value)
9
+ value
10
+ else
11
+ raise Result::Halt, result
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Result
5
+ # Allow support for sorbet runtime type checking
6
+ def self.[](*_types)
7
+ self
8
+ end
9
+
10
+ class Ok < self
11
+ def initialize(value) # rubocop:disable Lint/MissingSuper
12
+ self.value = value
13
+ end
14
+
15
+ def ==(other)
16
+ other.is_a?(Ok) && other.value == value
17
+ end
18
+
19
+ def ok?
20
+ true
21
+ end
22
+
23
+ def deconstruct
24
+ [value]
25
+ end
26
+
27
+ def deconstruct_keys(_keys)
28
+ { value: }
29
+ end
30
+ end
31
+
32
+ class Error < self
33
+ def initialize(error) # rubocop:disable Lint/MissingSuper
34
+ case error
35
+ when String
36
+ self.error = Alchemrest::Error.new(error)
37
+ when *Alchemrest.rescuable_exceptions
38
+ self.error = error
39
+ else
40
+ raise ArgumentError, "Error must be a string or one of the types defined in Alchemrest.rescuable_exceptions"
41
+ end
42
+ end
43
+
44
+ def ==(other)
45
+ other.is_a?(Error) && other.error == error
46
+ end
47
+
48
+ def ok?
49
+ false
50
+ end
51
+
52
+ def deconstruct
53
+ [error]
54
+ end
55
+
56
+ def deconstruct_keys(_keys)
57
+ { error: }
58
+ end
59
+ end
60
+
61
+ def self.Ok(value)
62
+ Ok.new(value)
63
+ end
64
+
65
+ def self.Error(error)
66
+ Error.new(error)
67
+ end
68
+
69
+ def self.for
70
+ block_return_value = yield Alchemrest::Result::TryHelpers
71
+
72
+ case block_return_value
73
+ in Alchemrest::Result => result
74
+ result
75
+ else
76
+ Alchemrest::Result::Ok.new(block_return_value)
77
+ end
78
+ rescue Alchemrest::Result::Halt => e
79
+ e.error
80
+ end
81
+
82
+ def initialize(*)
83
+ raise 'Cannot create an instance of Alchemrest::Result. Use Alchemrest::Result::Ok or Alchemrest::Result::Error instead.'
84
+ end
85
+
86
+ def transform
87
+ raise ArgumentError, 'no block given' unless block_given?
88
+
89
+ if ok?
90
+ self.class::Ok(yield value)
91
+ else
92
+ self
93
+ end
94
+ end
95
+
96
+ # The pattern of raising, rescuing, re-raising, rescuing again, and then handling looks
97
+ # a little weird, but it's necessary to ensure our final result is an Alchemrest::ResultResecued
98
+ # error where `error.cause` contains the original exception. This way we have information on both
99
+ # the stack trace where the api error originated, and the stack trace of where we tried to
100
+ # rescue it.
101
+ def unwrap_or_rescue
102
+ raise ArgumentError, 'no error handler given' unless block_given?
103
+
104
+ unwrap_or_raise!
105
+ rescue *Alchemrest.rescuable_exceptions => e
106
+ begin
107
+ raise Alchemrest::ResultRescued, "Alchemrest rescued an unexpected result of type #{error.class}", cause: e
108
+ rescue Alchemrest::ResultRescued => rescued_error
109
+ Alchemrest.handle_rescued_result(rescued_error)
110
+ yield
111
+ end
112
+ end
113
+
114
+ def unwrap_or_raise!
115
+ raise error unless ok?
116
+
117
+ value
118
+ end
119
+
120
+ protected
121
+
122
+ attr_reader :value, :error
123
+
124
+ private
125
+
126
+ attr_writer :value, :error
127
+ end
128
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ class Root
5
+ @@default_params = -> { {} } # rubocop:disable Style/ClassVars
6
+
7
+ def self.use_client(client_class)
8
+ client = client_class.new
9
+ define_method(:client) { client }
10
+ end
11
+
12
+ def self.request_definitions
13
+ @request_definitions ||= superclass.respond_to?(:request_definitions) ? superclass.request_definitions.dup : {}
14
+ end
15
+
16
+ def self.define_request(name, request_class, &block) # rubocop:disable Style/ArgumentsForwarding
17
+ request_definitions[name] = RequestDefinition.new(name, request_class, &block) # rubocop:disable Style/ArgumentsForwarding
18
+
19
+ define_method(name) do |params = {}|
20
+ result = build_request(name, params).execute!
21
+
22
+ case result
23
+ in Alchemrest::Result::Error[error]
24
+ if self.class.error_handler
25
+ instance_exec(error, &self.class.error_handler)
26
+ end
27
+ else
28
+ nil
29
+ end
30
+
31
+ result
32
+ end
33
+ end
34
+
35
+ def self.error_handler
36
+ if superclass.respond_to?(:error_handler)
37
+ @error_handler ||= superclass.error_handler
38
+ end
39
+ end
40
+
41
+ def self.reset_error_handler!
42
+ @error_handler = nil
43
+ end
44
+
45
+ def self.on_alchemrest_error(&block)
46
+ @error_handler = block
47
+ end
48
+
49
+ def build_request(name, params = {})
50
+ request_definition = self.class.request_definitions[name]
51
+
52
+ raise "No request definition found for #{name}. Did you call `define_request :#{name} in your root class?" unless request_definition
53
+
54
+ request = request_definition.build_request(self, params)
55
+ client.build_http_request(request)
56
+ end
57
+
58
+ # @abstract Use `.use_client` or override `#client`
59
+ def client
60
+ raise Alchemrest::UndefinedClientError, <<~MSG
61
+ You forgot to specify a client. You can simply use `use_client` like this:
62
+
63
+ class #{self.class} < #{self.class.superclass}
64
+ use_client YourClient
65
+ end
66
+
67
+ Or, you can define your client at the instance level:
68
+
69
+ class #{self.class} < #{self.class.superclass}
70
+ def client
71
+ YourClient.new
72
+ end
73
+ end
74
+ MSG
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class BaseToTypeTransformRegistry
6
+ include AbstractType
7
+ include Concord.new(:from)
8
+
9
+ abstract_method :build_transforms
10
+
11
+ def initialize(*args)
12
+ super
13
+ @registry_hash = build_internal_registry_hash(build_transforms)
14
+ end
15
+
16
+ def resolve(type)
17
+ if @registry_hash.key?(type)
18
+ @registry_hash.fetch(type)
19
+ else
20
+ raise NoRegisteredTransformError.new(from:, to: type)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def build_internal_registry_hash(transforms)
27
+ transforms.to_h do |type, value|
28
+ new_value = if value.instance_of?(Array)
29
+ ToType.new(from:, to: type, use: value)
30
+ elsif value.instance_of?(Hash)
31
+ ToType::TransformsSelector.new(from, type, value)
32
+ elsif value.is_a?(ToType::TransformsSelector)
33
+ value
34
+ else
35
+ raise "Not a valid implementation of `def build_transforms`"
36
+ end
37
+ [type, new_value]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ # Calling Constrainable.with_additional_attributes(...) will return a module that
6
+ # can be included in a `Morpher::Transform` class to allow adding validation constraints
7
+ # using the `#where` method. We do this in both `ToType` and `FromType`
8
+ class Constrainable < Anima
9
+ def initialize(args = {})
10
+ additional_attributes = args[:additional_attributes]
11
+ super(*additional_attributes, :constraints)
12
+ end
13
+
14
+ def included(base)
15
+ base.include(InstanceMethods)
16
+ super
17
+ end
18
+
19
+ module InstanceMethods
20
+ def where(constraint_or_description, &block)
21
+ constraint = if block
22
+ Constraint::Block.new(constraint_or_description, &block)
23
+ elsif constraint_or_description.is_a?(Constraint)
24
+ constraint_or_description
25
+ else
26
+ raise ArgumentError, "Must provide an instance of Alchemrest::Transform::Constraint"
27
+ end
28
+
29
+ with(constraints: [*constraints, constraint])
30
+ end
31
+
32
+ private
33
+
34
+ def validate_constraints
35
+ constraints.map { |constraint| WithConstraint.new(constraint) }
36
+ .inject(Morpher::Transform::Sequence.new([])) { |sequence, transform| sequence.seq(transform) }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class Constraint
6
+ class Block < self
7
+ include Concord.new(:description, :block)
8
+ public :description
9
+
10
+ def initialize(description, &block)
11
+ raise ArgumentError, "Must include a predicate block" unless block
12
+
13
+ super(description, block)
14
+ end
15
+
16
+ def meets_conditions?(input)
17
+ block.call(input)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class Constraint
6
+ class GreaterThan < self
7
+ include Concord.new(:value)
8
+
9
+ def meets_conditions?(input)
10
+ input > value
11
+ end
12
+
13
+ def description
14
+ "greater than #{value}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class Constraint
6
+ class GreaterThanOrEq < self
7
+ include Concord.new(:value)
8
+
9
+ def meets_conditions?(input)
10
+ input >= value
11
+ end
12
+
13
+ def description
14
+ "greater than or equal to #{value}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class Constraint
6
+ class InList < self
7
+ include Concord.new(:list)
8
+
9
+ def meets_conditions?(input)
10
+ list.include?(input)
11
+ end
12
+
13
+ def description
14
+ "in list #{list}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class Constraint
6
+ class IsInstanceOf < self
7
+ include Concord::Public.new(:klass)
8
+
9
+ def meets_conditions?(input)
10
+ input.instance_of?(klass)
11
+ end
12
+
13
+ def description
14
+ "is an #{klass}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end