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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class Constraint
6
+ class IsUuid < MatchesRegex
7
+ REGEX = /\A((?:\h{8})-(?:\h{4})-(?:\h{4})-(?:\h{4})-(?:\h{12}))\z/
8
+
9
+ def initialize
10
+ super(REGEX)
11
+ end
12
+
13
+ def description
14
+ "is a UUID string"
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 LessThan < self
7
+ include Concord.new(:value)
8
+
9
+ def meets_conditions?(input)
10
+ input < value
11
+ end
12
+
13
+ def description
14
+ "less 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 LessThanOrEq < self
7
+ include Concord.new(:value)
8
+
9
+ def meets_conditions?(input)
10
+ input <= value
11
+ end
12
+
13
+ def description
14
+ "less 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 MatchesRegex < self
7
+ include Concord.new(:regex)
8
+
9
+ def meets_conditions?(input)
10
+ input.match?(regex)
11
+ end
12
+
13
+ def description
14
+ "matches #{regex.inspect}"
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 MaxLength < self
7
+ include Concord.new(:max_length)
8
+
9
+ def meets_conditions?(input)
10
+ input.length <= max_length
11
+ end
12
+
13
+ def description
14
+ "max length of #{max_length}"
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 MinLength < self
7
+ include Concord.new(:min_length)
8
+
9
+ def meets_conditions?(input)
10
+ input.length >= min_length
11
+ end
12
+
13
+ def description
14
+ "min length of #{min_length}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ # The Alchemrest::Transforms::Constraint class is an abstract base that all other constraints should inherit from
6
+ # A constraint is basically just a predicate with metadata. The `meets_conditions?` method is the actual predicate,
7
+ # and description is a human-readable phrase that describes that predicate. We recommend using a phrase that will
8
+ # read well in a sentence like "<input> does not meet the constraint <description>", since that is how it will
9
+ # display in error messages
10
+ class Constraint
11
+ include AbstractType
12
+
13
+ abstract_method :meets_conditions?
14
+ abstract_method :description
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class ConstraintBuilder
6
+ class ForNumber < self
7
+ CONSTRAINT_METHODS = %i(greater_than less_than greater_than_or_eq less_than_or_eq positive non_negative integer).freeze
8
+
9
+ def greater_than(...) = apply_constraint(Constraint::GreaterThan.new(...))
10
+
11
+ def less_than(...) = apply_constraint(Constraint::LessThan.new(...))
12
+
13
+ def greater_than_or_eq(...) = apply_constraint(Constraint::GreaterThanOrEq.new(...))
14
+
15
+ def less_than_or_eq(...) = apply_constraint(Constraint::LessThanOrEq.new(...))
16
+
17
+ def positive = apply_constraint(Constraint::GreaterThan.new(0))
18
+
19
+ def non_negative = apply_constraint(Constraint::GreaterThanOrEq.new(0))
20
+
21
+ def integer = apply_constraint(Constraint::IsInstanceOf.new(Integer))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class ConstraintBuilder
6
+ class ForString < self
7
+ CONSTRAINT_METHODS = %i(max_length min_length matches in must_be_uuid).freeze
8
+
9
+ def max_length(...) = apply_constraint(Constraint::MaxLength.new(...))
10
+
11
+ def min_length(...) = apply_constraint(Constraint::MinLength.new(...))
12
+
13
+ def matches(...) = apply_constraint(Constraint::MatchesRegex.new(...))
14
+
15
+ def in(...) = apply_constraint(Constraint::InList.new(...))
16
+
17
+ def must_be_uuid = apply_constraint(Constraint::IsUuid.new)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class ConstraintBuilder
6
+ include Concord.new(:constrainable)
7
+
8
+ private
9
+
10
+ def apply_constraint(constraint)
11
+ constrainable.where(constraint)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/time'
4
+
5
+ module Alchemrest
6
+ module Transforms
7
+ class DateTransform < Morpher::Transform
8
+ def call(input)
9
+ if input.instance_of?(String)
10
+ begin
11
+ success(Date.iso8601(input))
12
+ rescue ArgumentError
13
+ not_a_valid_iso_date_string(input)
14
+ end
15
+ else
16
+ not_a_valid_iso_date_string(input)
17
+ end
18
+ end
19
+
20
+ def not_a_valid_iso_date_string(input)
21
+ failure(
22
+ error(
23
+ message: "Expected #{input} to be an iso date string",
24
+ input: input,
25
+ ),
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class Enum < Morpher::Transform
6
+ include Concord.new(:values)
7
+
8
+ private_constant(*constants(false))
9
+
10
+ def call(input)
11
+ case values
12
+ when ::Hash
13
+ evaluate_hash(input)
14
+ when ::Array
15
+ evaluate_array(input)
16
+ else
17
+ failure_error(input)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def evaluate_array(input)
24
+ if values.include?(input)
25
+ success(input)
26
+ elsif input.is_a?(String) && values.include?(input.to_sym)
27
+ success(input.to_sym)
28
+ else
29
+ failure_error(input)
30
+ end
31
+ end
32
+
33
+ def evaluate_hash(input)
34
+ value = values[input]
35
+ if value
36
+ success(value)
37
+ else
38
+ failure_error(input)
39
+ end
40
+ end
41
+
42
+ def failure_error(input)
43
+ failure(
44
+ error(
45
+ message: "Expected: enum value from #{values} but got: #{input.inspect}",
46
+ input: input,
47
+ ),
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/time'
4
+
5
+ module Alchemrest
6
+ module Transforms
7
+ class EpochTime < Morpher::Transform
8
+ include Concord.new(:unit)
9
+
10
+ def call(input)
11
+ if input.is_a?(Integer)
12
+ as_seconds = case unit
13
+ when :seconds
14
+ input
15
+ when :milliseconds
16
+ input / 1000
17
+ else
18
+ raise "Invalid unit #{unit}"
19
+ end
20
+ success(Time.at(as_seconds).in_time_zone("UTC"))
21
+ else
22
+ failure(
23
+ error(
24
+ message: "Expected #{input} to be an epoch integer",
25
+ input: input,
26
+ ),
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ module FromChain
6
+ def self.number
7
+ FromNumber.new
8
+ end
9
+
10
+ def self.string
11
+ FromString.new
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class FromNumber
6
+ class ToTypeTransformRegistry < BaseToTypeTransformRegistry
7
+ def build_transforms
8
+ {
9
+ Money => {
10
+ cents: [MoneyTransform.new(:cents)],
11
+ dollars: [MoneyTransform.new(:dollars)],
12
+ },
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class FromNumber < FromType
6
+ OVERRIDES = { type: T.any(Integer, Float) }.freeze
7
+ CONSTRAINT_BUILDER_CLASS = ConstraintBuilder::ForNumber
8
+
9
+ def initialize(args = {})
10
+ super(**args, **OVERRIDES)
11
+ end
12
+
13
+ def where(constraint_or_description = nil, &block)
14
+ if constraint_or_description
15
+ super
16
+ elsif block
17
+ raise ArgumentError, "Must provide a constraint or description"
18
+ else
19
+ self
20
+ end
21
+ end
22
+
23
+ def output_type_name
24
+ "Number"
25
+ end
26
+
27
+ delegate(*CONSTRAINT_BUILDER_CLASS::CONSTRAINT_METHODS, to: :constraint_builder)
28
+
29
+ private
30
+
31
+ def constraint_builder
32
+ CONSTRAINT_BUILDER_CLASS.new(self)
33
+ end
34
+
35
+ def transform
36
+ Sequence.new([
37
+ JsonNumber.new,
38
+ validate_constraints,
39
+ ])
40
+ end
41
+
42
+ memoize def to_type_transform_registry
43
+ ToTypeTransformRegistry.new(self)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class FromString
6
+ class ToTypeTransformRegistry < BaseToTypeTransformRegistry
7
+ def build_transforms
8
+ {
9
+ Time => ToType::FromStringToTimeSelector.new(from),
10
+ Date => [DateTransform.new],
11
+ BigDecimal => [ToDecimal.new],
12
+ }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class FromString < FromType
6
+ OVERRIDES = { type: String }.freeze
7
+ CONSTRAINT_BUILDER_CLASS = ConstraintBuilder::ForString
8
+
9
+ def initialize(args = {})
10
+ super(**args, **OVERRIDES)
11
+ end
12
+
13
+ def where(constraint_or_description = nil, &block)
14
+ if constraint_or_description
15
+ super
16
+ elsif block
17
+ raise ArgumentError, "Must provide a constraint or description"
18
+ else
19
+ self
20
+ end
21
+ end
22
+
23
+ delegate(*CONSTRAINT_BUILDER_CLASS::CONSTRAINT_METHODS, to: :constraint_builder)
24
+
25
+ private
26
+
27
+ def constraint_builder
28
+ CONSTRAINT_BUILDER_CLASS.new(self)
29
+ end
30
+
31
+ def to_type_transform_registry
32
+ ToTypeTransformRegistry.new(self)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class FromType
6
+ class EmptyToTypeTransformRegistry < BaseToTypeTransformRegistry
7
+ # mutant:disable
8
+ def build_transforms
9
+ {}
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ # Base class for transforms where we want to validate that the input is a particular
6
+ # type before operating on it. Additional validations can be chained off by use of the
7
+ # #where method. On success this transform will return the input, unchanged. For a transform
8
+ # that will actually change the input into something else, you can call #to and pass a block
9
+ # which will return a `ToType` transform that will run this transform, and then your block.
10
+ class FromType < Morpher::Transform
11
+ include Alchemrest::Transforms::Constrainable.new(additional_attributes: %i(type))
12
+ include Adamantium::Mutable
13
+ DEFAULTS = { constraints: [] }.freeze
14
+
15
+ def initialize(args)
16
+ super(**DEFAULTS.merge(args))
17
+ end
18
+
19
+ def output_type
20
+ OutputType.new(sorbet_type: type, constraints:)
21
+ end
22
+
23
+ def output_type_name
24
+ type.name
25
+ end
26
+
27
+ def call(input)
28
+ transform.call(input)
29
+ end
30
+
31
+ def to(type, &block)
32
+ if block.nil?
33
+ to_type_transform_registry.resolve(type)
34
+ else
35
+ ToType.using(to: type, from: self, &block)
36
+ end
37
+ rescue Alchemrest::NoRegisteredTransformError
38
+ raise ArgumentError,
39
+ "No transform registered to transform #{output_type.sorbet_type} to #{type}. Perhaps you should use the block form?"
40
+ end
41
+
42
+ def array
43
+ Typed.new(transform: super(), output_type: output_type.with(sorbet_type: T::Array[output_type.sorbet_type]))
44
+ end
45
+
46
+ def maybe
47
+ Typed.new(transform: super(), output_type: output_type.with(sorbet_type: T.nilable(output_type.sorbet_type)))
48
+ end
49
+
50
+ private
51
+
52
+ memoize def to_type_transform_registry
53
+ EmptyToTypeTransformRegistry.new(self)
54
+ end
55
+
56
+ def transform
57
+ Sequence.new([
58
+ Primitive.new(type),
59
+ validate_constraints,
60
+ ])
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/time'
4
+
5
+ module Alchemrest
6
+ module Transforms
7
+ class IsoTime < Morpher::Transform
8
+ include Anima.new(:to_timezone, :require_offset)
9
+
10
+ def call(input)
11
+ return not_a_valid_iso_string(input) unless input.instance_of?(String)
12
+ return missing_required_offset(input) unless has_offset?(input) || !require_offset
13
+
14
+ begin
15
+ success(parse(input))
16
+ rescue ArgumentError
17
+ not_a_valid_iso_string(input)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def parse(input)
24
+ if to_timezone
25
+ ActiveSupport::TimeZone[to_timezone].iso8601(input)
26
+ else
27
+ Time.iso8601(input)
28
+ end
29
+ end
30
+
31
+ def has_offset?(input)
32
+ parts = input.split("T")
33
+ time = parts.last
34
+ # checking for an offset
35
+
36
+ /[Z+-]/.match?(time)
37
+ end
38
+
39
+ def missing_required_offset(input)
40
+ failure(
41
+ error(
42
+ message: "Expected #{input} to have a valid ISO timezone offset",
43
+ input: input,
44
+ ),
45
+ )
46
+ end
47
+
48
+ def not_a_valid_iso_string(input)
49
+ failure(
50
+ error(
51
+ message: "Expected #{input} to be iso datetime string",
52
+ input: input,
53
+ ),
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemrest
4
+ module Transforms
5
+ class JsonNumber < Morpher::Transform
6
+ def call(input)
7
+ if input.instance_of?(Integer) || input.instance_of?(Float)
8
+ success(input)
9
+ else
10
+ failure_error(input)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def failure_error(input)
17
+ failure(
18
+ error(
19
+ message: "Expected: Float, Integer but got #{input.class}",
20
+ input:,
21
+ ),
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end