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,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class LooseHash < Morpher::Transform
6
+ include Anima.new(:allow_additional_properties, :optional, :required)
7
+
8
+ KEY_MESSAGE = 'Missing keys: %<missing>s, Unexpected keys: %<unexpected>s'
9
+ PRIMITIVE = Primitive.new(::Hash)
10
+
11
+ private_constant(*constants(false))
12
+
13
+ # Apply transformation to input
14
+ #
15
+ # @param [Object] input
16
+ #
17
+ # @return [Either<Error, Object>]
18
+ def call(input)
19
+ PRIMITIVE
20
+ .call(input)
21
+ .lmap { |e| lift_error(e) }
22
+ .bind { |o| reject_keys(o) }
23
+ .bind { |o| transform(o) }
24
+ end
25
+
26
+ private
27
+
28
+ def transform(input)
29
+ transform_required(input).bind do |required|
30
+ transform_optional(input).fmap(&required.public_method(:merge))
31
+ end
32
+ end
33
+
34
+ def transform_required(input)
35
+ transform_keys(required, input)
36
+ end
37
+
38
+ memoize def defaults
39
+ optional.map(&:value).product([nil]).to_h
40
+ end
41
+
42
+ def transform_optional(input)
43
+ transform_keys(
44
+ optional.select { |key| input.key?(key.value) },
45
+ input,
46
+ ).fmap(&defaults.public_method(:merge))
47
+ end
48
+
49
+ def transform_keys(keys, input)
50
+ success(
51
+ keys
52
+ .to_h do |key|
53
+ [
54
+ key.value,
55
+ coerce_key(key, input).from_right do |error|
56
+ return failure(error)
57
+ end,
58
+ ]
59
+ end,
60
+ )
61
+ end
62
+
63
+ def coerce_key(key, input)
64
+ key.call(input.fetch(key.value)).lmap do |error|
65
+ error(input: input, cause: error)
66
+ end
67
+ end
68
+
69
+ def reject_keys(input)
70
+ keys = input.keys
71
+ unexpected = allow_additional_properties ? [] : (keys - allowed_keys)
72
+ missing = required_keys - keys
73
+ unexpected_properties_exist = allow_additional_properties || unexpected.empty?
74
+
75
+ if unexpected_properties_exist && missing.empty?
76
+ success(input)
77
+ else
78
+ failure(
79
+ error(
80
+ input: input,
81
+ message: format(KEY_MESSAGE, missing: missing, unexpected: unexpected),
82
+ ),
83
+ )
84
+ end
85
+ end
86
+
87
+ memoize def allowed_keys
88
+ required_keys + optional.map(&:value)
89
+ end
90
+
91
+ memoize def required_keys
92
+ required.map(&:value)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class MoneyTransform < Morpher::Transform
6
+ include Concord.new(:unit)
7
+
8
+ private_constant(*constants(false))
9
+
10
+ def call(input)
11
+ case input
12
+ when Numeric
13
+ success(into_money(input))
14
+ else
15
+ failure_error(input)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def into_money(amount)
22
+ case unit
23
+ when :cents
24
+ Money.from_cents(amount)
25
+ when :dollars
26
+ Money.from_amount(amount)
27
+ else
28
+ raise "Invalid unit #{unit}"
29
+ end
30
+ end
31
+
32
+ def failure_error(input)
33
+ failure(
34
+ error(
35
+ message: "Expected: Numeric but got #{input.class}",
36
+ input: input,
37
+ ),
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class Number < Morpher::Transform
6
+ def call(input)
7
+ case input
8
+ when Numeric
9
+ success(input)
10
+ else
11
+ failure_error(input)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def failure_error(input)
18
+ failure(
19
+ error(
20
+ message: "Expected: Numeric but got #{input.class}",
21
+ input: input,
22
+ ),
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ # A wrapper around other transforms that provides metadata about the return type of the transform.
6
+ # Note this does not do anything to actually enforce that return type, although we may want to change
7
+ # that in the future. It's main purpose is to provide metadata we can use to inspect the schema of
8
+ # an `Alchemrest::Data` class.
9
+ class OutputType < T::Struct
10
+ const :sorbet_type, T.any(Class, T::Types::Base)
11
+ const :constraints, T::Array[Alchemrest::Transforms::Constraint]
12
+
13
+ def self.simple(sorbet_type)
14
+ new(sorbet_type:, constraints: [])
15
+ end
16
+
17
+ def graph
18
+ graphs = graph_types.select { |type| has_graph?(type) }.map(&:graph)
19
+
20
+ if graphs.size == 1
21
+ graphs.sole
22
+ end
23
+ end
24
+
25
+ def with(args)
26
+ OutputType.new({ sorbet_type:, constraints: }.merge(args))
27
+ end
28
+
29
+ def ==(other)
30
+ sorbet_type == other.sorbet_type && constraints.to_set == other.constraints.to_set
31
+ end
32
+
33
+ private
34
+
35
+ def has_graph?(type)
36
+ type.respond_to?(:<=) && type < Data
37
+ end
38
+
39
+ def graph_types
40
+ recursively_unwrap_raw_type(sorbet_type)
41
+ end
42
+
43
+ def recursively_unwrap_raw_type(type)
44
+ current_types = case type
45
+ in T::Types::Simple
46
+ [type.raw_type]
47
+ in T::Types::TypedArray => array
48
+ [array.type]
49
+ in T::Types::Union => union
50
+ union.types
51
+ else
52
+ [type]
53
+ end
54
+
55
+ current_types.map { |current_type|
56
+ if current_type.instance_of?(T::Types::TypedArray) || current_type.instance_of?(T::Types::Simple)
57
+ recursively_unwrap_raw_type(current_type)
58
+ else
59
+ current_type
60
+ end
61
+ }.flatten
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class ToDecimal < Morpher::Transform
6
+ def call(input)
7
+ success(BigDecimal(input, 0))
8
+ rescue TypeError, ArgumentError
9
+ cannot_make_decimal(input)
10
+ end
11
+
12
+ def cannot_make_decimal(input)
13
+ failure(
14
+ error(
15
+ message: "Expected #{input} to be castable to BigDecimal",
16
+ input:,
17
+ ),
18
+ )
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class ToType
6
+ class FromStringToTimeSelector < TransformsSelector
7
+ def initialize(from)
8
+ super(from, nil, {})
9
+ end
10
+
11
+ def using(timezone_identifier, require_offset: true)
12
+ use, to = case timezone_identifier
13
+ in :utc
14
+ [[IsoTime.new(to_timezone: 'UTC', require_offset:)], ActiveSupport::TimeWithZone]
15
+ in :local
16
+ [[IsoTime.new(to_timezone: Time.zone.name, require_offset:)], ActiveSupport::TimeWithZone]
17
+ in :offset
18
+ raise ArgumentError, "require_offset cannot be false when using :offset" unless require_offset
19
+
20
+ [[IsoTime.new(to_timezone: nil, require_offset: true)], Time]
21
+ in String
22
+ [[IsoTime.new(to_timezone: timezone_identifier, require_offset:)], ActiveSupport::TimeWithZone]
23
+ end
24
+ ToType.new(from:, to:, use:)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class ToType
6
+ class TransformsSelector
7
+ INVALID_KEY_OR_TRANSFORM_ERROR_MESSAGE = "Must provide a symbol key or a Morpher::Transform"
8
+ INVALID_TRANSFORMS_HASH_ERROR_MESSAGE = "transform_options must a Hash with symbol keys, and values of Morpher::Transform[]"
9
+
10
+ include Concord.new(:from, :to, :transform_options)
11
+
12
+ def initialize(*)
13
+ super
14
+ unless transform_options_valid?
15
+ raise ArgumentError, INVALID_TRANSFORMS_HASH_ERROR_MESSAGE
16
+ end
17
+ end
18
+
19
+ def using(key_or_transform)
20
+ raise ArgumentError, INVALID_KEY_OR_TRANSFORM_ERROR_MESSAGE unless a_symbol_or_transform?(key_or_transform)
21
+
22
+ transforms_to_use = resolve_transforms_array(key_or_transform)
23
+
24
+ unless transforms_to_use
25
+ raise NoTransformOptionForNameError.new(from:, to:, name: key_or_transform, options: transform_options)
26
+ end
27
+
28
+ ToType.new(from:, to:, use: transforms_to_use)
29
+ end
30
+
31
+ def options
32
+ transform_options.keys
33
+ end
34
+
35
+ private
36
+
37
+ def transform_options_valid?
38
+ transform_options.instance_of?(Hash) &&
39
+ transform_options.keys.all?(Symbol) &&
40
+ transform_options.values.all? { |value| transform_array?(value) }
41
+ end
42
+
43
+ def a_symbol_or_transform?(value)
44
+ value.instance_of?(Symbol) || value.is_a?(Morpher::Transform)
45
+ end
46
+
47
+ def transform_array?(value)
48
+ value.instance_of?(Array) && value.all?(Morpher::Transform)
49
+ end
50
+
51
+ def resolve_transforms_array(value)
52
+ if value.is_a?(Morpher::Transform)
53
+ [value]
54
+ elsif transform_options.key?(value)
55
+ transform_options.fetch(value)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ # A class for taking in an input and transforming it so something else. To initialize this class
6
+ # you must provide a `FromType` transform that first validates that the input is actually transformable
7
+ # (via the `from:` param) and an array of transforms that will actualy do the transformation
8
+ # (via the `use:` param) If you want to run additional validations after transformation, you can use the `#where` method.
9
+ class ToType < Morpher::Transform
10
+ include Alchemrest::Transforms::Constrainable.new(additional_attributes: %i(to from use))
11
+ DEFAULTS = { constraints: [] }.freeze
12
+
13
+ def self.using(args, &block)
14
+ new(**args, use: [Success.new(block)])
15
+ end
16
+
17
+ def initialize(args)
18
+ super(**DEFAULTS.merge(args))
19
+
20
+ raise ArgumentError, ":to must be a Class" unless to.instance_of?(Class)
21
+ raise ArgumentError, ":from must be a FromType transform" unless from.is_a?(FromType)
22
+
23
+ unless use.instance_of?(::Array) && !use.empty? && use.all?(Morpher::Transform)
24
+ raise ArgumentError, ":use must be an array of Morpher::Transform instances"
25
+ end
26
+ end
27
+
28
+ def output_type
29
+ OutputType.new(sorbet_type: to, constraints: all_constraints)
30
+ end
31
+
32
+ def using(transforms)
33
+ with(use: transforms)
34
+ end
35
+
36
+ def call(input)
37
+ transform.call(input)
38
+ end
39
+
40
+ def all_constraints
41
+ [*from.constraints, *constraints]
42
+ end
43
+
44
+ def constraints_for(type)
45
+ raise ArgumentError, "Must provide a Class" unless type.instance_of?(Class)
46
+
47
+ unless [self, from].map { |transform| transform.output_type.sorbet_type }.include?(type)
48
+ raise ArgumentError,
49
+ "`type` must be either the to type (#{output_type.sorbet_type}) or the " \
50
+ "from type (#{from.output_type.sorbet_type}), was #{type}"
51
+ end
52
+
53
+ [self, from].select { |transform| transform.output_type.sorbet_type == type }.flat_map(&:constraints)
54
+ end
55
+
56
+ def array
57
+ Typed.new(transform: super(), output_type: output_type.with(sorbet_type: T::Array[(output_type.sorbet_type)]))
58
+ end
59
+
60
+ def maybe
61
+ Typed.new(transform: super(), output_type: output_type.with(sorbet_type: T.nilable(output_type.sorbet_type)))
62
+ end
63
+
64
+ private
65
+
66
+ def transform
67
+ Sequence.new([
68
+ from,
69
+ *use,
70
+ validate_output,
71
+ validate_constraints,
72
+ ])
73
+ end
74
+
75
+ def validate_output
76
+ Block.capture("Alchemrest::Transform::ToType") do |current|
77
+ if current.is_a?(to)
78
+ success(current)
79
+ else
80
+ failure("Transform chain created an ouput of type: #{current.class}. Expected: #{to}")
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ # A wrapper around other transforms that provides metadata about the return type of the transform.
6
+ # Note this does not do anything to actually enforce that return type, although we may want to change
7
+ # that in the future. It's main purpose is to provide metadata we can use to inspect the schema of
8
+ # an `Alchemrest::Data` class.
9
+ class Typed < Morpher::Transform
10
+ include Anima.new(:transform, :output_type)
11
+ include Adamantium::Mutable
12
+
13
+ def call(input)
14
+ transform.call(input)
15
+ end
16
+
17
+ def array
18
+ Typed.new(
19
+ transform: super(),
20
+ output_type: output_type.with(sorbet_type: T::Array[output_type.sorbet_type]),
21
+ )
22
+ end
23
+
24
+ def maybe
25
+ Typed.new(
26
+ transform: super(),
27
+ output_type: output_type.with(sorbet_type: T.nilable(output_type.sorbet_type)),
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class Union < Morpher::Transform
6
+ include Anima.new(:types, :discriminator)
7
+
8
+ private_constant(*constants(false))
9
+
10
+ def call(input)
11
+ type_key = input&.fetch(discriminator.to_s)
12
+ if type_key.nil?
13
+ klass_not_found_failure_error(input)
14
+ else
15
+ perform_transformation(input, type_key)
16
+ end
17
+ end
18
+
19
+ def output_type
20
+ OutputType.simple(T.any(*types.values))
21
+ end
22
+
23
+ private
24
+
25
+ def perform_transformation(input, type_key)
26
+ klass = types[type_key.to_sym]
27
+ if klass
28
+ klass::TRANSFORM.call(input)
29
+ else
30
+ klass_not_found_failure_error(input)
31
+ end
32
+ end
33
+
34
+ def klass_not_found_failure_error(input)
35
+ failure(
36
+ error(
37
+ message: "Expected discriminator #{discriminator} to produce a value which is one of #{types.keys.join(',')} but got #{input&.fetch(discriminator).nil? ? 'nil' : input[discriminator]}", # rubocop:disable Layout/LineLength
38
+ input: input,
39
+ ),
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class WithConstraint < Morpher::Transform
6
+ MESSAGE = %(Input %<actual>p does not meet the constraint "%<description>s")
7
+ include Concord.new(:constraint)
8
+
9
+ def initialize(constraint)
10
+ raise ArgumentError, "Must provide an instance of Alchemrest::Transform::Constraint" unless constraint.is_a?(Constraint)
11
+
12
+ super
13
+ end
14
+
15
+ def call(input)
16
+ if constraint.meets_conditions?(input)
17
+ success(input)
18
+ else
19
+ failure(
20
+ error(input:, message: format(MESSAGE, actual: input, description: constraint.description)),
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ # The Transforms module is passed into the block provided to `Alchemrest::Data.schema`, giving developers an easy way to
5
+ # define the transforms for a given data class
6
+ #
7
+ # @example Using `Alchemrest::Transforms` inside a `schema` block
8
+ # class User < Alchemrest::Data
9
+ # schema do |s|
10
+ # # `s` is `Alchemrest::Transforms`
11
+ # end
12
+ # end
13
+ module Transforms
14
+ # A transformation that results in an integer number. Must be a true integer in JSON, not a string as number. Will
15
+ # result in a `Alchemrest::MorpherTransformError` if not an integer number.
16
+ def self.integer
17
+ Typed.new(transform: Morpher::Transform::INTEGER, output_type: OutputType.simple(Integer))
18
+ end
19
+
20
+ # A transformation that results in a string. Must be a true string in JSON, we will not coerce booleans or number to strings. Will
21
+ # result in a `Alchemrest::MorpherTransformError` if not a string.
22
+ def self.string
23
+ Typed.new(transform: Morpher::Transform::STRING, output_type: OutputType.simple(String))
24
+ end
25
+
26
+ # A transformation that results in a float. Must be a float in JSON, we will coerce strings into numbers. Will
27
+ # result in a `Alchemrest::MorpherTransformError` if not a float. Note, if the api sometimes returns a float and
28
+ # sometimes returns an integer, see {#number} instead.
29
+ def self.float
30
+ Typed.new(transform: Morpher::Transform::FLOAT, output_type: OutputType.simple(Float))
31
+ end
32
+
33
+ def self.from
34
+ FromChain
35
+ end
36
+
37
+ # A transformation that results in some kind of numeric type, generally either a Float or Integer. If the input is not
38
+ # Numeric, will result in a `Alchemrest::MorpherTransformError`
39
+ def self.number
40
+ Typed.new(transform: Number.new, output_type: OutputType.simple(T.any(Float, Integer)))
41
+ end
42
+
43
+ # A transformation that results in a boolean. If the input is not a boolean, will result in a `Alchemrest::MorpherTransformError`
44
+ def self.boolean
45
+ Typed.new(transform: Morpher::Transform::BOOLEAN, output_type: OutputType.simple(T::Boolean))
46
+ end
47
+
48
+ # A transformation that results in a Date object.
49
+ # If the input is not an iso8601 date string, will result in a `Alchemrest::MorpherTransformError`
50
+ def self.date
51
+ Typed.new(transform: DateTransform.new, output_type: OutputType.simple(Date))
52
+ end
53
+
54
+ # A transformation that creates a {Money} object. You can indicate
55
+ # whether the original amount is in dollars our cents via the unit
56
+ # param. If the original value is not numeric, it will result in a
57
+ # `Alchemrest::MorpherTransformError`
58
+ #
59
+ # @param [Symbol] (:dollars | :cents) the unit for the original amount
60
+ def self.money(unit)
61
+ Typed.new(transform: MoneyTransform.new(unit), output_type: OutputType.simple(Money))
62
+ end
63
+
64
+ # A transformation that results in a {Symbol, String} from a predefined list.
65
+ # If the original value is not a string from the predefined list, it will
66
+ # result in a `Alchemrest::MorpherTransformError`
67
+ #
68
+ # @param [Array<Symbol>] the list of valid values for the field
69
+ def self.enum(enum)
70
+ Typed.new(transform: Enum.new(enum), output_type: OutputType.simple(T.any(Symbol, String)))
71
+ end
72
+
73
+ # A transformation that results in an Alchemrest::Data class. You can
74
+ # either provide a klass or a Hash with a `discriminator` value for
75
+ # polymorphic data types
76
+ #
77
+ # @param [Class, Hash<Symbol, Class>] Either an `Alchemrest::Data` class,
78
+ # or a hash of classes along with a `discriminator` key for polymorphic
79
+ # use cases
80
+ def self.one_of(klass_or_hash)
81
+ if klass_or_hash.instance_of? Hash
82
+ klasses = klass_or_hash.except(:discriminator)
83
+ Union.new(types: klasses, discriminator: klass_or_hash.fetch(:discriminator))
84
+ else
85
+ klass_or_hash::TRANSFORM
86
+ end
87
+ end
88
+
89
+ def self.many_of(klass)
90
+ klass::TRANSFORM.array
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,39 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Alchemrest
5
+ class UrlBuilder
6
+ module Encoders
7
+ def self.find(name)
8
+ case name
9
+ in :rack
10
+ RackEncoded.new
11
+ in :form
12
+ FormUrlEncoded.new
13
+ end
14
+ end
15
+
16
+ class RackEncoded
17
+ def call(query)
18
+ Rack::Utils.build_nested_query(query)
19
+ end
20
+ end
21
+
22
+ class FormUrlEncoded
23
+ def call(query)
24
+ URI.encode_www_form(query)
25
+ end
26
+ end
27
+
28
+ class Custom
29
+ def initialize(&block)
30
+ @block = block
31
+ end
32
+
33
+ def call(query)
34
+ @block.call(query)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end