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
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ Bundler::GemHelper.install_tasks
10
+ Bundler::GemHelper.tag_prefix = "#{Bundler::GemHelper.gemspec.name}-"
11
+
12
+ require 'rubocop/rake_task'
13
+ RuboCop::RakeTask.new
14
+
15
+ require 'rspec/core'
16
+ require 'rspec/core/rake_task'
17
+ RSpec::Core::RakeTask.new(:spec)
18
+
19
+ def default_task
20
+ if ENV['APPRAISAL_INITIALIZED'] || ENV['CI']
21
+ %i(rubocop spec)
22
+ else
23
+ require 'appraisal'
24
+ Appraisal::Task.new
25
+ %i(appraisal)
26
+ end
27
+ end
28
+
29
+ task(:default).clear.enhance(default_task)
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/alchemrest/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "alchemrest"
7
+ spec.version = Alchemrest::VERSION
8
+ spec.authors = ["Andrew Swerlick", "James Boyer"]
9
+ spec.email = ["andrew.swerlick@betterment.com", "james.boyer@betterment.com"]
10
+
11
+ spec.summary = %(
12
+ A library to help you transform third party api's into a set of classes and models
13
+ designed to work nicely with your domain.
14
+ )
15
+ spec.description = %(
16
+ Betterment's library for building robust, reliable, performant integrations with
17
+ third party apis, with a focus on making APIs work with the rest of your domain layer
18
+ not against it.
19
+ )
20
+ spec.license = "MIT"
21
+ spec.homepage = "https://github.com/Betterment/alchemrest"
22
+ spec.required_ruby_version = ">= 3.2"
23
+
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
+ spec.metadata["changelog_uri"] = "https://github.com/Betterment/alchemrest/CHANGELOG.md"
26
+
27
+ # Specify which files should be added to the gem when it is released.
28
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
29
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
30
+ `git ls-files -z`.split("\x0").reject do |f|
31
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
32
+ end
33
+ end
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
36
+ spec.require_paths = %w(lib)
37
+
38
+ rails_constraints = [">= 7.2", "< 8.1"]
39
+
40
+ spec.add_dependency 'activerecord', rails_constraints
41
+ spec.add_dependency 'activesupport', rails_constraints
42
+ spec.add_dependency "circuitbox", "~> 2.0.0"
43
+ spec.add_dependency "faraday", ">= 1.10", "< 3.0"
44
+ spec.add_dependency "memosa", ">= 0.8.2"
45
+ spec.add_dependency 'money', '>= 6.0'
46
+ spec.add_dependency 'morpher', '>=0.4.1'
47
+ spec.add_dependency 'multi_json', '~> 1.0'
48
+ spec.add_dependency 'mustermann-contrib', '>= 1.0'
49
+ spec.add_dependency 'railties', rails_constraints
50
+ spec.add_dependency 'sentry-ruby'
51
+ spec.add_dependency 'sorbet-runtime', '>= 0.5.0'
52
+
53
+ spec.add_development_dependency "activemodel"
54
+ spec.add_development_dependency 'appraisal'
55
+ spec.add_development_dependency "betterlint"
56
+ spec.add_development_dependency 'combustion'
57
+ spec.add_development_dependency "factory_bot"
58
+ spec.add_development_dependency "moneta"
59
+ spec.add_development_dependency 'mutant-rspec', '~> 0.13.4'
60
+ spec.add_development_dependency 'pg'
61
+ spec.add_development_dependency "pry"
62
+ spec.add_development_dependency 'rake', '~> 13.0'
63
+ spec.add_development_dependency "rspec", "~> 3.0"
64
+ spec.add_development_dependency "rspec_junit_formatter"
65
+ spec.add_development_dependency 'rubocop'
66
+ spec.add_development_dependency 'rubocop-rspec'
67
+ spec.add_development_dependency 'sorbet'
68
+ spec.add_development_dependency 'tapioca', '>= 0.16.6'
69
+ spec.add_development_dependency "timecop"
70
+ spec.add_development_dependency "webmock"
71
+ end
data/coach.yml ADDED
@@ -0,0 +1,5 @@
1
+ project_type: ruby_gem
2
+ ruby_version: 3.2.2
3
+ databases:
4
+ - type: postgresql
5
+ version: "15.2"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ class BankApi::Client < Alchemrest::Client
5
+ configure do |config|
6
+ config.connection.url = 'http://bank.example.com'
7
+ config.service_name = "bank"
8
+ config.use_kill_switch(true)
9
+ config.use_circuit_breaker(disabled_when: -> { ENV.fetch("DISABLE_BANK_API_CIRCUIT", nil) })
10
+ config.connection.customize do |c|
11
+ c.options[:open_timeout] = 4
12
+ c.options[:timeout] = 10
13
+ end
14
+ end
15
+
16
+ def build_response(raw_response)
17
+ Response.new(raw_response)
18
+ end
19
+
20
+ class Response < Alchemrest::Response
21
+ def error_details
22
+ error_data = body.fetch("errors", {})
23
+ case error_data
24
+ when Hash
25
+ error_data.symbolize_keys
26
+ else
27
+ error_data
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module BankApi
5
+ module Data
6
+ class Account < Alchemrest::Data
7
+ schema do |s|
8
+ {
9
+ required: {
10
+ name: s.string,
11
+ status: s.enum(%w(open locked)),
12
+ cards: s.many_of(BankApi::Data::Card),
13
+ },
14
+ optional: {
15
+ nickname: s.string,
16
+ },
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BankApi
4
+ module Data
5
+ class Ach < Alchemrest::Data
6
+ schema do |s|
7
+ {
8
+ required: {
9
+ source_type: s.enum(%w(ach)),
10
+ trace_number: s.from.string.where.max_length(15),
11
+ },
12
+ }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module BankApi
5
+ module Data
6
+ class BusinessAccount < Alchemrest::Data
7
+ schema do |s|
8
+ {
9
+ required: {
10
+ name: s.string,
11
+ status: s.enum(%w(open locked)),
12
+ ein: s.string,
13
+ id: s.string,
14
+ },
15
+ optional: {
16
+ nickname: s.string,
17
+ },
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module BankApi
5
+ module Data
6
+ class Card < Alchemrest::Data
7
+ schema do |s|
8
+ {
9
+ required: {
10
+ source_type: s.enum(%w(card)),
11
+ card_number: s.string,
12
+ expiration_date: s.from.string.to(Time).using(:utc, require_offset: false),
13
+ },
14
+ optional: {
15
+ secondary_user: s.one_of(User),
16
+ },
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module BankApi
5
+ module Data
6
+ class Check < Alchemrest::Data
7
+ schema do |s|
8
+ {
9
+ required: {
10
+ source_type: s.enum(%w(check)),
11
+ check_number: s.integer,
12
+ check_image_back_url: s.string,
13
+ check_image_front_url: s.string,
14
+ },
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module BankApi
5
+ module Data
6
+ class Product < Alchemrest::Data
7
+ schema do |s|
8
+ {
9
+ required: {
10
+ name: s.string,
11
+ interest_rate: PositiveInterestString.new(2),
12
+ partner_revenue_rate: s.from.string.where.matches(/^\d+(?:\.\d{1,4})?$/).to(BigDecimal).where("less than 10") do |input|
13
+ input < 10
14
+ end,
15
+ },
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module BankApi
5
+ module Data
6
+ class Transaction < Alchemrest::Data
7
+ schema do |s|
8
+ {
9
+ required: {
10
+ amount_cents: s.money(:cents),
11
+ status: s.enum(%w(completed pending)),
12
+ settled_at: s.from.string.to(Time).using(:local),
13
+ description: s.string,
14
+ account_id: s.string,
15
+ user_id: s.number,
16
+ },
17
+ optional: {
18
+ source: s.one_of(
19
+ check: BankApi::Data::Check,
20
+ card: BankApi::Data::Card,
21
+ ach: BankApi::Data::Ach,
22
+ discriminator: :source_type,
23
+ ),
24
+ },
25
+ }
26
+ end
27
+
28
+ def source_identifer
29
+ case (source = self.source)
30
+ when BankApi::Data::Check
31
+ source.check_number
32
+ when BankApi::Data::Card
33
+ source.card_number
34
+ when BankApi::Data::Ach
35
+ source.trace_number
36
+ when nil
37
+ nil
38
+ else
39
+ T.absurd(source)
40
+ end
41
+ end
42
+
43
+ configure_response_capture do
44
+ safe :account_id
45
+ omitted :description
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module BankApi
5
+ module Data
6
+ class User < Alchemrest::Data
7
+ schema do |s|
8
+ {
9
+ required: {
10
+ name: s.string,
11
+ status: s.enum(%w(open locked)),
12
+ date_of_birth: s.from.string.to(Time).using(:utc, require_offset: false),
13
+ account_ids: s.integer.array,
14
+ },
15
+ optional: {
16
+ nickname: s.string,
17
+ },
18
+ }
19
+ end
20
+
21
+ def age
22
+ # NOTE: This isn't actually correct for a variety of reasons, just an example
23
+ ActiveSupport::TimeZone["UTC"].now.year - date_of_birth.year
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'factory_bot'
4
+
5
+ Alchemrest::FactoryBot.enable!
6
+
7
+ FactoryBot.define do
8
+ alchemrest_factory :bank_api_user, class: 'BankApi::Data::User' do
9
+ name { 'Kevin' }
10
+ status { 'open' }
11
+ date_of_birth { '2021-01-01T00:00:00' }
12
+ account_ids { [1, 2] }
13
+ end
14
+
15
+ alchemrest_factory :bank_api_transaction, class: 'BankApi::Data::Transaction' do
16
+ sequence(:user_id)
17
+ amount_cents { 120_00 }
18
+ status { "completed" }
19
+ settled_at { "2023-10-10T00:00:00Z" }
20
+ description { "check transaction" }
21
+ source { association(:bank_api_check) }
22
+ account_id { "123456789" }
23
+
24
+ trait :from_ach do
25
+ source { association(:bank_api_ach) }
26
+ end
27
+
28
+ trait :from_check do
29
+ source { association(:bank_api_check) }
30
+ end
31
+
32
+ trait :from_card do
33
+ source { association(:bank_api_card) }
34
+ end
35
+ end
36
+
37
+ alchemrest_factory :bank_api_check, class: 'BankApi::Data::Check' do
38
+ source_type { "check" }
39
+ check_number { 100 }
40
+ check_image_back_url { "http://bank.example.com/api/v1/check_images/100/back.png" }
41
+ check_image_front_url { "http://bank.example.com/api/v1/check_images/100/front.png" }
42
+ end
43
+
44
+ alchemrest_factory :bank_api_ach, class: 'BankApi::Data::Ach' do
45
+ source_type { "ach" }
46
+ trace_number { '12354689' }
47
+ end
48
+
49
+ alchemrest_factory :bank_api_card, class: 'BankApi::Data::Card' do
50
+ source_type { 'card' }
51
+ card_number { '12346' }
52
+ expiration_date { '2023-10-10T00:00:00' }
53
+ end
54
+
55
+ alchemrest_factory :bank_api_product, class: 'BankApi::Data::Product' do
56
+ name { 'Savings Account' }
57
+ interest_rate { "4.00%" }
58
+ partner_revenue_rate { "0.0125" }
59
+ end
60
+
61
+ alchemrest_factory :bank_api_business_account, class: 'BankApi::Data::BusinessAccount' do
62
+ name { 'ACME Inc' }
63
+ status { 'open' }
64
+ ein { '123456789' }
65
+
66
+ id { SecureRandom.uuid }
67
+ end
68
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require "active_support/core_ext/string/indent"
5
+
6
+ module BankApi
7
+ # A simple, toy example of how to interact with the introspection interface in alchemrest.
8
+ # Outputs a tree like string showing the graph of all response objects defined in our examples
9
+ class GraphVisualization
10
+ extend T::Sig
11
+
12
+ sig { returns(T::Array[Alchemrest::Data::Graph]) }
13
+ def graphs
14
+ BankApi::Data.constants.sort.map do |name|
15
+ BankApi::Data.const_get(name).graph
16
+ end
17
+ end
18
+
19
+ def tree_string
20
+ io = StringIO.new
21
+ graphs.each do |graph|
22
+ print_graph!(graph, io)
23
+ end
24
+ io.string
25
+ end
26
+
27
+ private
28
+
29
+ sig { params(graph: Alchemrest::Data::Graph, io: StringIO, indent: Integer).void }
30
+ def print_graph!(graph, io, indent: 0)
31
+ io.puts(T.must(graph.type.name).indent(indent))
32
+ graph.fields.each do |key, field|
33
+ output_type = field.output_type
34
+ io.puts("- #{key} => #{output_type ? output_type.sorbet_type : 'unknown'}".indent(indent))
35
+ field.constraints.each do |constraint|
36
+ io.puts("* #{constraint.description}".indent(indent + 2))
37
+ end
38
+ end
39
+ graph.sub_graphs.each do |key, sub_graph|
40
+ io.puts "- #{key}"
41
+ print_graph!(sub_graph, io, indent: 2)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BankApi
4
+ class PositiveInterestString < Morpher::Transform
5
+ attr_reader :decimal_places
6
+
7
+ def initialize(decimal_places) # rubocop:disable Lint/MissingSuper
8
+ @decimal_places = decimal_places
9
+ end
10
+
11
+ def call(input)
12
+ return failure(error(message: "Expected a string of the form '<num>%", input:)) unless input[-1] == "%"
13
+
14
+ decimal = BigDecimal(input.chop)
15
+ if input.chop.split(".")[1].length != decimal_places
16
+ failure(error(message: "Expected #{input} to have #{decimal_places} decimal_places", input:))
17
+ elsif decimal.negative?
18
+ failure(error(message: "#{input} is less than 0%", input:))
19
+ else
20
+ success(decimal)
21
+ end
22
+ rescue ArgumentError => e
23
+ raise unless e.message.start_with? "invalid value for BigDecimal()"
24
+
25
+ failure(
26
+ error(
27
+ message: "Expected: #{input} to be a decimal",
28
+ input:,
29
+ ),
30
+ )
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module BankApi
5
+ module Requests
6
+ class DeleteUser < Alchemrest::Request
7
+ def initialize(id:)
8
+ @id = id
9
+ super()
10
+ end
11
+
12
+ endpoint :delete, '/api/v1/users/:id' do |url|
13
+ url.values = { id: @id }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module BankApi
5
+ module Requests
6
+ class GetBusinessAccount < Alchemrest::Request
7
+ include ActiveModel::Model
8
+
9
+ enable_response_capture
10
+
11
+ attr_accessor :token, :id
12
+
13
+ endpoint :get, '/api/business_accounts/:id' do |url|
14
+ url.values = { id: }
15
+ end
16
+
17
+ returns BankApi::Data::BusinessAccount
18
+
19
+ def headers
20
+ { Authorization: "Bearer #{token}" }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module BankApi
5
+ module Requests
6
+ class GetProducts < Alchemrest::Request
7
+ returns BankApi::Data::Product[]
8
+ endpoint :get, '/api/v1/products'
9
+ enable_response_capture
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BankApi
4
+ module Requests
5
+ class GetTransactions < Alchemrest::Request
6
+ include ActiveModel::Model
7
+ attr_accessor :account_id, :id, :amount
8
+
9
+ returns BankApi::Data::Transaction[]
10
+ endpoint :get, '/api/v1/users/:id/accounts/:account_id/transactions' do |url|
11
+ url.values = { id:, account_id: }
12
+ end
13
+ enable_response_capture
14
+
15
+ def response_transformer
16
+ super.insert_after(
17
+ Alchemrest::Response::Pipeline::ExtractPayload,
18
+ ExtractUserId.new(self),
19
+ )
20
+ end
21
+
22
+ class ExtractUserId < Alchemrest::Response::Pipeline::Transform
23
+ def initialize(request)
24
+ super()
25
+ @request = request
26
+ end
27
+
28
+ def call(payload)
29
+ success(payload.map { |item| item.merge("user_id" => @request.id) })
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module BankApi
5
+ module Requests
6
+ class GetUser < Alchemrest::Request
7
+ def initialize(id:) # rubocop:disable Lint/MissingSuper
8
+ @id = id
9
+ end
10
+
11
+ returns BankApi::Data::User
12
+ endpoint :get, '/api/v1/users/:id' do |url|
13
+ url.values = { id: @id }
14
+ url.query = { includeDetails: true }
15
+ end
16
+ enable_response_capture
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module BankApi
5
+ module Requests
6
+ class PostTransaction < Alchemrest::Request
7
+ include ActiveModel::Model
8
+ attr_accessor :account_id, :id, :amount
9
+
10
+ endpoint :post, '/api/v1/users/:id/accounts/:account_id/transactions' do |url|
11
+ url.values = { id:, account_id: }
12
+ end
13
+ enable_response_capture
14
+
15
+ def body
16
+ { amount: }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module BankApi
5
+ module Requests
6
+ class UpdateUser < Alchemrest::Request
7
+ def initialize(id:, name:, date_of_birth:)
8
+ @id = id
9
+ @name = name
10
+ @date_of_birth = date_of_birth
11
+ super()
12
+ end
13
+
14
+ endpoint :patch, '/api/v1/users/:id' do |url|
15
+ url.values = { id: @id }
16
+ end
17
+
18
+ def body
19
+ {
20
+ name: @name,
21
+ date_of_birth: @date_of_birth,
22
+ }
23
+ end
24
+
25
+ returns BankApi::Data::User, allow_empty_response: true
26
+ end
27
+ end
28
+ end