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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/.rubocop_todo.yml +242 -0
- data/.ruby-version +1 -0
- data/Appraisals +19 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +378 -0
- data/Rakefile +29 -0
- data/alchemrest.gemspec +71 -0
- data/coach.yml +5 -0
- data/examples/bank_api/client.rb +31 -0
- data/examples/bank_api/data/account.rb +21 -0
- data/examples/bank_api/data/ach.rb +16 -0
- data/examples/bank_api/data/business_account.rb +22 -0
- data/examples/bank_api/data/card.rb +21 -0
- data/examples/bank_api/data/check.rb +19 -0
- data/examples/bank_api/data/product.rb +20 -0
- data/examples/bank_api/data/transaction.rb +49 -0
- data/examples/bank_api/data/user.rb +27 -0
- data/examples/bank_api/factories.rb +68 -0
- data/examples/bank_api/graph_visualization.rb +45 -0
- data/examples/bank_api/positive_interest_string.rb +33 -0
- data/examples/bank_api/requests/delete_user.rb +17 -0
- data/examples/bank_api/requests/get_business_account.rb +24 -0
- data/examples/bank_api/requests/get_products.rb +12 -0
- data/examples/bank_api/requests/get_transactions.rb +34 -0
- data/examples/bank_api/requests/get_user.rb +19 -0
- data/examples/bank_api/requests/post_transaction.rb +20 -0
- data/examples/bank_api/requests/update_user.rb +28 -0
- data/examples/bank_api/root.rb +52 -0
- data/examples/bank_api.rb +33 -0
- data/gemfiles/faraday_2.gemfile +9 -0
- data/gemfiles/faraday_2.gemfile.lock +363 -0
- data/gemfiles/rails_7_0.gemfile.lock +341 -0
- data/gemfiles/rails_7_2.gemfile +9 -0
- data/gemfiles/rails_7_2.gemfile.lock +384 -0
- data/gemfiles/rails_8_0.gemfile +9 -0
- data/gemfiles/rails_8_0.gemfile.lock +385 -0
- data/lib/alchemrest/circuit_breaker.rb +84 -0
- data/lib/alchemrest/client/configuration/connection.rb +83 -0
- data/lib/alchemrest/client/configuration.rb +89 -0
- data/lib/alchemrest/client.rb +48 -0
- data/lib/alchemrest/cop.rb +8 -0
- data/lib/alchemrest/data/capture_configuration.rb +77 -0
- data/lib/alchemrest/data/field.rb +36 -0
- data/lib/alchemrest/data/graph.rb +40 -0
- data/lib/alchemrest/data/record.rb +60 -0
- data/lib/alchemrest/data/schema.rb +80 -0
- data/lib/alchemrest/data.rb +9 -0
- data/lib/alchemrest/endpoint_definition.rb +47 -0
- data/lib/alchemrest/error.rb +122 -0
- data/lib/alchemrest/factory_bot.rb +64 -0
- data/lib/alchemrest/faraday_middleware/external_api_instrumentation.rb +24 -0
- data/lib/alchemrest/faraday_middleware/json_parser.rb +30 -0
- data/lib/alchemrest/faraday_middleware/kill_switch.rb +22 -0
- data/lib/alchemrest/faraday_middleware/underscore_response.rb +24 -0
- data/lib/alchemrest/hash_path.rb +81 -0
- data/lib/alchemrest/http_request.rb +75 -0
- data/lib/alchemrest/kill_switch/adapters.rb +88 -0
- data/lib/alchemrest/kill_switch.rb +31 -0
- data/lib/alchemrest/railtie.rb +25 -0
- data/lib/alchemrest/request/endpoint.rb +29 -0
- data/lib/alchemrest/request/returns.rb +46 -0
- data/lib/alchemrest/request.rb +80 -0
- data/lib/alchemrest/request_definition/builder.rb +13 -0
- data/lib/alchemrest/request_definition.rb +26 -0
- data/lib/alchemrest/response/pipeline/extract_payload.rb +64 -0
- data/lib/alchemrest/response/pipeline/final.rb +11 -0
- data/lib/alchemrest/response/pipeline/omit.rb +46 -0
- data/lib/alchemrest/response/pipeline/sanitize.rb +59 -0
- data/lib/alchemrest/response/pipeline/transform.rb +26 -0
- data/lib/alchemrest/response/pipeline/was_successful.rb +29 -0
- data/lib/alchemrest/response/pipeline.rb +71 -0
- data/lib/alchemrest/response.rb +73 -0
- data/lib/alchemrest/response_captured_handler.rb +68 -0
- data/lib/alchemrest/result/halt.rb +15 -0
- data/lib/alchemrest/result/try_helpers.rb +16 -0
- data/lib/alchemrest/result.rb +128 -0
- data/lib/alchemrest/root.rb +77 -0
- data/lib/alchemrest/transforms/base_to_type_transform_registry.rb +42 -0
- data/lib/alchemrest/transforms/constrainable.rb +41 -0
- data/lib/alchemrest/transforms/constraint/block.rb +22 -0
- data/lib/alchemrest/transforms/constraint/greater_than.rb +19 -0
- data/lib/alchemrest/transforms/constraint/greater_than_or_eq.rb +19 -0
- data/lib/alchemrest/transforms/constraint/in_list.rb +19 -0
- data/lib/alchemrest/transforms/constraint/is_instance_of.rb +19 -0
- data/lib/alchemrest/transforms/constraint/is_uuid.rb +19 -0
- data/lib/alchemrest/transforms/constraint/less_than.rb +19 -0
- data/lib/alchemrest/transforms/constraint/less_than_or_eq.rb +19 -0
- data/lib/alchemrest/transforms/constraint/matches_regex.rb +19 -0
- data/lib/alchemrest/transforms/constraint/max_length.rb +19 -0
- data/lib/alchemrest/transforms/constraint/min_length.rb +19 -0
- data/lib/alchemrest/transforms/constraint.rb +17 -0
- data/lib/alchemrest/transforms/constraint_builder/for_number.rb +25 -0
- data/lib/alchemrest/transforms/constraint_builder/for_string.rb +21 -0
- data/lib/alchemrest/transforms/constraint_builder.rb +15 -0
- data/lib/alchemrest/transforms/date_transform.rb +30 -0
- data/lib/alchemrest/transforms/enum.rb +52 -0
- data/lib/alchemrest/transforms/epoch_time.rb +32 -0
- data/lib/alchemrest/transforms/from_chain.rb +15 -0
- data/lib/alchemrest/transforms/from_number/to_type_transform_registry.rb +18 -0
- data/lib/alchemrest/transforms/from_number.rb +47 -0
- data/lib/alchemrest/transforms/from_string/to_type_transform_registry.rb +17 -0
- data/lib/alchemrest/transforms/from_string.rb +36 -0
- data/lib/alchemrest/transforms/from_type/empty_to_type_transform_registry.rb +14 -0
- data/lib/alchemrest/transforms/from_type.rb +64 -0
- data/lib/alchemrest/transforms/iso_time.rb +58 -0
- data/lib/alchemrest/transforms/json_number.rb +26 -0
- data/lib/alchemrest/transforms/loose_hash.rb +96 -0
- data/lib/alchemrest/transforms/money_transform.rb +42 -0
- data/lib/alchemrest/transforms/number.rb +27 -0
- data/lib/alchemrest/transforms/output_type.rb +65 -0
- data/lib/alchemrest/transforms/to_decimal.rb +22 -0
- data/lib/alchemrest/transforms/to_type/from_string_to_time_selector.rb +29 -0
- data/lib/alchemrest/transforms/to_type/transforms_selector.rb +61 -0
- data/lib/alchemrest/transforms/to_type.rb +86 -0
- data/lib/alchemrest/transforms/typed.rb +32 -0
- data/lib/alchemrest/transforms/union.rb +44 -0
- data/lib/alchemrest/transforms/with_constraint.rb +26 -0
- data/lib/alchemrest/transforms.rb +93 -0
- data/lib/alchemrest/url_builder/encoders.rb +39 -0
- data/lib/alchemrest/url_builder/options.rb +33 -0
- data/lib/alchemrest/url_builder.rb +31 -0
- data/lib/alchemrest/version.rb +5 -0
- data/lib/alchemrest/webmock_helpers.rb +27 -0
- data/lib/alchemrest.rb +159 -0
- data/lib/generators/alchemrest/kill_switch_migration_generator.rb +27 -0
- data/lib/generators/alchemrest/templates/kill_switch_migration.rb.erb +17 -0
- data/lib/rubocop/cop/alchemrest/define_request_using_with_params.rb +53 -0
- data/lib/rubocop/cop/alchemrest/endpoint_definition_using_generic_params.rb +55 -0
- data/lib/rubocop/cop/alchemrest/request_hash_returning_block.rb +54 -0
- data/lib/rubocop/cop/alchemrest/time_transform_with_no_zone.rb +56 -0
- data/lib/tapioca/dsl/compilers/alchemrest_data.rb +84 -0
- data/lib/tapioca/dsl/compilers/alchemrest_root.rb +68 -0
- data/mutant.yml +16 -0
- data/rbi/alchemrest/result.rbi +80 -0
- data/rbi/alchemrest.rbi +246 -0
- data/sorbet/config +5 -0
- data/sorbet/rbi/gems/.gitattributes +1 -0
- data/sorbet/rbi/gems/abstract_type@0.0.7.rbi +41 -0
- data/sorbet/rbi/gems/actionpack@8.0.4.rbi +11733 -0
- data/sorbet/rbi/gems/actionview@8.0.4.rbi +6560 -0
- data/sorbet/rbi/gems/activemodel@8.0.4.rbi +2891 -0
- data/sorbet/rbi/gems/activesupport@8.0.4.rbi +9621 -0
- data/sorbet/rbi/gems/adamantium@0.2.0.rbi +144 -0
- data/sorbet/rbi/gems/addressable@2.8.7.rbi +779 -0
- data/sorbet/rbi/gems/anima@0.3.2.rbi +103 -0
- data/sorbet/rbi/gems/ast@2.4.2.rbi +107 -0
- data/sorbet/rbi/gems/base64@0.3.0.rbi +52 -0
- data/sorbet/rbi/gems/benchmark@0.5.0.rbi +153 -0
- data/sorbet/rbi/gems/bigdecimal@3.3.1.rbi +77 -0
- data/sorbet/rbi/gems/builder@3.3.0.rbi +9 -0
- data/sorbet/rbi/gems/circuitbox@2.0.0.rbi +297 -0
- data/sorbet/rbi/gems/concord@0.1.6.rbi +51 -0
- data/sorbet/rbi/gems/concurrent-ruby@1.3.5.rbi +4716 -0
- data/sorbet/rbi/gems/connection_pool@2.5.4.rbi +9 -0
- data/sorbet/rbi/gems/crack@1.0.0.rbi +110 -0
- data/sorbet/rbi/gems/crass@1.0.6.rbi +294 -0
- data/sorbet/rbi/gems/date@3.4.1.rbi +58 -0
- data/sorbet/rbi/gems/drb@2.2.3.rbi +639 -0
- data/sorbet/rbi/gems/equalizer@0.0.11.rbi +38 -0
- data/sorbet/rbi/gems/erubi@1.13.1.rbi +85 -0
- data/sorbet/rbi/gems/factory_bot@6.5.0.rbi +1529 -0
- data/sorbet/rbi/gems/faraday-em_http@1.0.0.rbi +181 -0
- data/sorbet/rbi/gems/faraday-em_synchrony@1.0.1.rbi +120 -0
- data/sorbet/rbi/gems/faraday-excon@1.1.0.rbi +128 -0
- data/sorbet/rbi/gems/faraday-httpclient@1.0.1.rbi +123 -0
- data/sorbet/rbi/gems/faraday-multipart@1.2.0.rbi +190 -0
- data/sorbet/rbi/gems/faraday-net_http@1.0.2.rbi +140 -0
- data/sorbet/rbi/gems/faraday-net_http_persistent@1.2.0.rbi +116 -0
- data/sorbet/rbi/gems/faraday-patron@1.0.0.rbi +119 -0
- data/sorbet/rbi/gems/faraday-rack@1.0.0.rbi +113 -0
- data/sorbet/rbi/gems/faraday-retry@1.0.3.rbi +149 -0
- data/sorbet/rbi/gems/faraday@1.10.5.rbi +1620 -0
- data/sorbet/rbi/gems/hansi@0.2.1.rbi +9 -0
- data/sorbet/rbi/gems/hashdiff@1.1.2.rbi +174 -0
- data/sorbet/rbi/gems/i18n@1.14.7.rbi +1328 -0
- data/sorbet/rbi/gems/ice_nine@0.11.2.rbi +145 -0
- data/sorbet/rbi/gems/io-console@0.8.0.rbi +9 -0
- data/sorbet/rbi/gems/json@2.9.1.rbi +282 -0
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +8057 -0
- data/sorbet/rbi/gems/logger@1.7.0.rbi +260 -0
- data/sorbet/rbi/gems/loofah@2.24.0.rbi +571 -0
- data/sorbet/rbi/gems/memoizable@0.4.2.rbi +131 -0
- data/sorbet/rbi/gems/memosa@0.8.2.rbi +185 -0
- data/sorbet/rbi/gems/minitest@5.26.0.rbi +824 -0
- data/sorbet/rbi/gems/money@6.19.0.rbi +815 -0
- data/sorbet/rbi/gems/morpher@0.4.2.rbi +388 -0
- data/sorbet/rbi/gems/mprelude@0.1.0.rbi +140 -0
- data/sorbet/rbi/gems/multi_json@1.15.0.rbi +180 -0
- data/sorbet/rbi/gems/multipart-post@2.4.1.rbi +154 -0
- data/sorbet/rbi/gems/mustermann-contrib@3.0.3.rbi +9 -0
- data/sorbet/rbi/gems/mustermann@3.0.3.rbi +809 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +112 -0
- data/sorbet/rbi/gems/nokogiri@1.19.1.rbi +3412 -0
- data/sorbet/rbi/gems/parallel@1.26.3.rbi +234 -0
- data/sorbet/rbi/gems/parser@3.3.7.0.rbi +4877 -0
- data/sorbet/rbi/gems/pp@0.6.2.rbi +176 -0
- data/sorbet/rbi/gems/prettyprint@0.2.0.rbi +155 -0
- data/sorbet/rbi/gems/prism@1.5.1.rbi +26368 -0
- data/sorbet/rbi/gems/procto@0.0.3.rbi +9 -0
- data/sorbet/rbi/gems/psych@5.2.3.rbi +806 -0
- data/sorbet/rbi/gems/public_suffix@6.0.1.rbi +267 -0
- data/sorbet/rbi/gems/racc@1.8.1.rbi +120 -0
- data/sorbet/rbi/gems/rack-session@2.1.1.rbi +458 -0
- data/sorbet/rbi/gems/rack-test@2.2.0.rbi +405 -0
- data/sorbet/rbi/gems/rack@3.1.14.rbi +2774 -0
- data/sorbet/rbi/gems/rackup@2.2.1.rbi +132 -0
- data/sorbet/rbi/gems/rails-dom-testing@2.2.0.rbi +266 -0
- data/sorbet/rbi/gems/rails-html-sanitizer@1.6.2.rbi +545 -0
- data/sorbet/rbi/gems/railties@8.0.4.rbi +2150 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +333 -0
- data/sorbet/rbi/gems/rake@13.2.1.rbi +2054 -0
- data/sorbet/rbi/gems/rbi@0.2.3.rbi +3961 -0
- data/sorbet/rbi/gems/rdoc@6.13.1.rbi +6784 -0
- data/sorbet/rbi/gems/regexp_parser@2.11.3.rbi +3020 -0
- data/sorbet/rbi/gems/reline@0.6.0.rbi +9 -0
- data/sorbet/rbi/gems/rexml@3.4.2.rbi +1777 -0
- data/sorbet/rbi/gems/rubocop-ast@1.38.0.rbi +5293 -0
- data/sorbet/rbi/gems/rubocop@1.71.1.rbi +31846 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +980 -0
- data/sorbet/rbi/gems/ruby2_keywords@0.0.5.rbi +9 -0
- data/sorbet/rbi/gems/securerandom@0.4.1.rbi +33 -0
- data/sorbet/rbi/gems/sentry-ruby@5.22.1.rbi +3782 -0
- data/sorbet/rbi/gems/spoom@1.5.1.rbi +4321 -0
- data/sorbet/rbi/gems/stringio@3.1.2.rbi +9 -0
- data/sorbet/rbi/gems/tapioca@0.16.8.rbi +3399 -0
- data/sorbet/rbi/gems/thor@1.3.2.rbi +2012 -0
- data/sorbet/rbi/gems/thread_safe@0.3.6.rbi +711 -0
- data/sorbet/rbi/gems/timeout@0.4.4.rbi +80 -0
- data/sorbet/rbi/gems/tsort@0.2.0.rbi +50 -0
- data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +1677 -0
- data/sorbet/rbi/gems/unicode-display_width@2.6.0.rbi +62 -0
- data/sorbet/rbi/gems/uri@1.1.0.rbi +760 -0
- data/sorbet/rbi/gems/useragent@0.16.11.rbi +9 -0
- data/sorbet/rbi/gems/webmock@3.24.0.rbi +1362 -0
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +345 -0
- data/sorbet/rbi/gems/yard@0.9.37.rbi +8795 -0
- data/sorbet/rbi/gems/zeitwerk@2.7.1.rbi +589 -0
- data/sorbet/tapioca/config.yml +45 -0
- data/sorbet/tapioca/require.rb +8 -0
- data/sorbet/tapioca/sorbet/rbi/dsl/.gitattributes +1 -0
- data/sorbet/tapioca/sorbet/rbi/dsl/active_support/callbacks.rbi +23 -0
- 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,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
|