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,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
# A utility for executing transformations against hashes scoped to specific parts of the hash
|
|
5
|
+
# defined by the `segments` of the path. The `Alchemrest::HashPath` class has one method `#walk`
|
|
6
|
+
# which will "walk" the path defined by the segments, yielding to a block provided by the calling code
|
|
7
|
+
# at each node it stops at. In that block, the calling code can transform the hash as it likes
|
|
8
|
+
#
|
|
9
|
+
# @example Using `Alchemrest::HashPath` to delete specific nodes from a hash
|
|
10
|
+
# input = { user: { accounts: [{account_id: 1, transactions: [{transaction_id: 1, amount: 100}] }] } }
|
|
11
|
+
# path = Alchemrest::HashPath.new([:user, :accounts, :transactions, :amount])
|
|
12
|
+
# path.walk(input) do |_path, node, remaining_segments|
|
|
13
|
+
# if(remaining_segments.count == 1)
|
|
14
|
+
# node.delete(remaining_segments.last.key)
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
# input #=> { user: { accounts: [{account_id: 1, transactions: [{transaction_id: 1}] }] } }
|
|
18
|
+
class HashPath
|
|
19
|
+
include Concord::Public.new(:segments)
|
|
20
|
+
|
|
21
|
+
# A helper method to quickly build a collection of hash paths from a rails style "StrongParams" hash arg.
|
|
22
|
+
# @example Create multiple hash paths from a strong params style hash
|
|
23
|
+
# Alchemrest::HashPath.build_collection(user: { accounts: { transactions: [:id, :amount] } }) #=>
|
|
24
|
+
# [
|
|
25
|
+
# Alchemrest::HashPath(segments: [:accounts, :transactions, :id]),
|
|
26
|
+
# Alchemrest::HashPath(segments: [:accounts, :transactions, :amount])
|
|
27
|
+
# ]
|
|
28
|
+
def self.build_collection(definition)
|
|
29
|
+
paths = []
|
|
30
|
+
visit_leaves(definition) do |value, path|
|
|
31
|
+
paths << (path + [value])
|
|
32
|
+
end
|
|
33
|
+
paths.map { |path| new(path) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# "Walks" the path defined by the segments of the HashPath, including individual items in nested collections.
|
|
37
|
+
# For each items it walks, it yields to the provided block, passing the current full path to that item, it's value
|
|
38
|
+
# and the remaining segments of the path it has to walk. If it any point the input does not have a value for one
|
|
39
|
+
# of the segments, this method will stop traversing that portion of the hash. Note actions taken within the block
|
|
40
|
+
# are mutative, and will modify the original hash
|
|
41
|
+
def walk(input, &block)
|
|
42
|
+
traverse(input, segments, [], &block)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def visit_leaves(input, path = [], &block)
|
|
49
|
+
case input
|
|
50
|
+
when Hash
|
|
51
|
+
input.each_with_object(path) do |(key, value), path|
|
|
52
|
+
visit_leaves(value, path + [key], &block)
|
|
53
|
+
end
|
|
54
|
+
when Array
|
|
55
|
+
input.map { |i| visit_leaves(i, path, &block) }
|
|
56
|
+
else
|
|
57
|
+
yield input, path
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def traverse(node, remaining_segments, path, &block) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
65
|
+
yield path, node, remaining_segments
|
|
66
|
+
|
|
67
|
+
return if remaining_segments.empty?
|
|
68
|
+
|
|
69
|
+
current_segment = remaining_segments.first
|
|
70
|
+
|
|
71
|
+
if node.is_a?(Hash) && node.key?(current_segment)
|
|
72
|
+
rest_of_segments = remaining_segments[1..]
|
|
73
|
+
traverse(node[current_segment], rest_of_segments, path + [current_segment], &block)
|
|
74
|
+
elsif node.is_a?(Array) && node.any?(Hash)
|
|
75
|
+
node.each_with_index do |item, i|
|
|
76
|
+
traverse(item, remaining_segments, path + [i], &block) if item.is_a?(Hash)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'delegate'
|
|
4
|
+
|
|
5
|
+
module Alchemrest
|
|
6
|
+
class HttpRequest < SimpleDelegator
|
|
7
|
+
extend Memosa
|
|
8
|
+
|
|
9
|
+
def initialize(request, client)
|
|
10
|
+
@client = client
|
|
11
|
+
super(request)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Have to disable this because mutant wans to
|
|
15
|
+
# push towards `to_str` which is not available in
|
|
16
|
+
# 7.1. Once we drop 7.1 support we can remove
|
|
17
|
+
# mutant:disable
|
|
18
|
+
def url
|
|
19
|
+
@client.connection.url_prefix.to_s.chop + path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def headers
|
|
23
|
+
{ **default_headers, **super() }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def execute!
|
|
27
|
+
result = Result.for do |try|
|
|
28
|
+
raw_response = try.unwrap make_request!
|
|
29
|
+
transform_response(build_response(raw_response))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
circuit_breaker.monitor!(result:)
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def build_response(raw_response)
|
|
39
|
+
@client.build_response(raw_response)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
memoize def circuit_breaker
|
|
43
|
+
@client.configuration.circuit_breaker
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def make_request!
|
|
47
|
+
return Result::Error(CircuitOpenError.new) if circuit_breaker.open?
|
|
48
|
+
|
|
49
|
+
# We have to set the body in the block form, otherwise it will be ignored for
|
|
50
|
+
# delete requests. See https://github.com/lostisland/faraday/issues/693#issuecomment-466086832
|
|
51
|
+
response = @client.connection.public_send(http_method, path, nil, headers) do |req|
|
|
52
|
+
modify_faraday_request(req)
|
|
53
|
+
end
|
|
54
|
+
Result::Ok(response)
|
|
55
|
+
rescue Faraday::Error => e
|
|
56
|
+
handle_faraday_error(e)
|
|
57
|
+
rescue *Alchemrest.rescuable_exceptions => e
|
|
58
|
+
Result::Error(e)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def modify_faraday_request(req)
|
|
62
|
+
req.body = body unless http_method == 'get'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def handle_faraday_error(error)
|
|
66
|
+
if error.wrapped_exception.instance_of?(Net::OpenTimeout)
|
|
67
|
+
Result::Error(TimeoutError.new)
|
|
68
|
+
else
|
|
69
|
+
raise RequestFailedError
|
|
70
|
+
end
|
|
71
|
+
rescue Alchemrest::RequestFailedError => e
|
|
72
|
+
Result::Error(e)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_record'
|
|
4
|
+
|
|
5
|
+
module Alchemrest
|
|
6
|
+
class KillSwitch
|
|
7
|
+
module Adapters
|
|
8
|
+
class Test
|
|
9
|
+
class Record
|
|
10
|
+
include Anima.new(:enabled)
|
|
11
|
+
|
|
12
|
+
alias enabled? enabled
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private_constant :Record
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@records = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ready?
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def active?(service_name:)
|
|
26
|
+
load_record(service_name:).enabled?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def activate(service_name:)
|
|
30
|
+
set(service_name:, enabled: true)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def deactivate(service_name:)
|
|
34
|
+
set(service_name:, enabled: false)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def set(service_name:, enabled:)
|
|
40
|
+
record = load_record(service_name:).with(enabled:)
|
|
41
|
+
@records[service_name] = record
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def load_record(service_name:)
|
|
45
|
+
raise 'service_name cannot be nil' unless service_name
|
|
46
|
+
|
|
47
|
+
@records[service_name] ||= Record.new enabled: false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class ActiveRecord
|
|
52
|
+
class Record < ::ActiveRecord::Base
|
|
53
|
+
self.table_name = 'alchemrest_kill_switches'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private_constant :Record
|
|
57
|
+
|
|
58
|
+
def ready?
|
|
59
|
+
Record.table_exists?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def active?(service_name:)
|
|
63
|
+
load_record(service_name:).enabled?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def activate(service_name:)
|
|
67
|
+
set(service_name:, enabled: true)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def deactivate(service_name:)
|
|
71
|
+
set(service_name:, enabled: false)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def set(service_name:, enabled:)
|
|
77
|
+
load_record(service_name:).update!(enabled:)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def load_record(service_name:)
|
|
81
|
+
raise 'service_name cannot be nil' unless service_name
|
|
82
|
+
|
|
83
|
+
Record.find_or_create_by!(service_name:)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class KillSwitch
|
|
5
|
+
include Anima.new(:service_name)
|
|
6
|
+
|
|
7
|
+
def initialize(service_name:)
|
|
8
|
+
raise ArgumentError, 'service_name is required' unless service_name
|
|
9
|
+
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def active?
|
|
14
|
+
adapter.active?(service_name:)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def activate!
|
|
18
|
+
adapter.activate(service_name:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def deactivate!
|
|
22
|
+
adapter.deactivate(service_name:)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def adapter
|
|
28
|
+
Alchemrest.kill_switch_adapter
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
|
|
5
|
+
module Alchemrest
|
|
6
|
+
class Railtie < ::Rails::Railtie
|
|
7
|
+
initializer 'alchemrest.logger' do
|
|
8
|
+
Alchemrest.logger = Rails.logger
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer 'alchemrest.configure_parameter_filter' do |app|
|
|
12
|
+
config.after_initialize do
|
|
13
|
+
Alchemrest.filter_parameters += app.config.filter_parameters
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer 'alchemrest.deprecator' do |app|
|
|
18
|
+
app.deprecators[:alchemrest] = Alchemrest.deprecator
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
initializer 'alchemrest.deprecator.behavior' do |app|
|
|
22
|
+
Alchemrest.deprecator.behavior = app.config.active_support.deprecation
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Request
|
|
5
|
+
class Endpoint < Module
|
|
6
|
+
include Concord::Public.new(:endpoint_definition)
|
|
7
|
+
|
|
8
|
+
def included(_)
|
|
9
|
+
endpoint = self
|
|
10
|
+
|
|
11
|
+
define_method(:endpoint_definition) do
|
|
12
|
+
endpoint.endpoint_definition
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
include(InstanceMethods)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module InstanceMethods
|
|
19
|
+
def path
|
|
20
|
+
endpoint_definition.url_for(self)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def http_method
|
|
24
|
+
endpoint_definition.http_method
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Request
|
|
5
|
+
class Returns < Module
|
|
6
|
+
include Concord::Public.new(:domain_type, :path_to_payload, :allow_empty_response)
|
|
7
|
+
|
|
8
|
+
def included(request_class)
|
|
9
|
+
transformer_methods = Module.new
|
|
10
|
+
returns = self
|
|
11
|
+
|
|
12
|
+
transformer_methods.define_method(:response_transformer) do
|
|
13
|
+
super()
|
|
14
|
+
.append(Response::Pipeline::ExtractPayload.new(returns.path_to_payload, returns.allow_empty_response))
|
|
15
|
+
.append(returns.domain_type::TRANSFORM)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
transformer_methods.define_method(:capture_transformer) do
|
|
19
|
+
super()
|
|
20
|
+
.replace_with(Response::Pipeline::Sanitize, returns.sanitize_step)
|
|
21
|
+
.append(returns.omit_step)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
request_class.include(transformer_methods)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sanitize_step
|
|
28
|
+
Response::Pipeline::Sanitize.new(
|
|
29
|
+
safe: HashPath.build_collection(capture_configuration.safe_paths),
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def omit_step
|
|
34
|
+
Response::Pipeline::Omit.new(
|
|
35
|
+
HashPath.build_collection(capture_configuration.omitted_paths),
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def capture_configuration
|
|
42
|
+
domain_type.capture_configuration.with_path_to_payload(path_to_payload)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mustermann/expander'
|
|
4
|
+
|
|
5
|
+
module Alchemrest
|
|
6
|
+
class Request
|
|
7
|
+
DEFAULT_HEADERS = IceNine.deep_freeze('User-Agent' => "Alchemrest/#{VERSION}")
|
|
8
|
+
private_constant(:DEFAULT_HEADERS)
|
|
9
|
+
|
|
10
|
+
def self.returns(domain_type, path_to_payload: nil, allow_empty_response: false)
|
|
11
|
+
include Returns.new(domain_type, path_to_payload, allow_empty_response)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.endpoint(http_method, template, &block)
|
|
15
|
+
builder_block = block
|
|
16
|
+
endpoint_definition = EndpointDefinition.new(template:, http_method: http_method.to_s, builder_block:)
|
|
17
|
+
include Endpoint.new(endpoint_definition)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.enable_response_capture
|
|
21
|
+
define_method(:response_capture_enabled) { true }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.disable_response_capture
|
|
25
|
+
define_method(:response_capture_enabled) { false }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def path
|
|
29
|
+
raise NotImplementedError, 'This is an abstract base method. Implement in your subclass.'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def http_method
|
|
33
|
+
raise NotImplementedError, 'This is an abstract base method. Implement in your subclass.'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def default_headers
|
|
37
|
+
DEFAULT_HEADERS
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def headers
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def body
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def response_transformer
|
|
49
|
+
Response::Pipeline.new(
|
|
50
|
+
Response::Pipeline::WasSuccessful.new,
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def capture_transformer
|
|
55
|
+
Response::Pipeline.new(
|
|
56
|
+
Response::Pipeline::ExtractPayload.new(nil, true),
|
|
57
|
+
Response::Pipeline::Sanitize.new,
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def response_capture_enabled
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def transform_response(response)
|
|
66
|
+
capture!(response:)
|
|
67
|
+
response_transformer.call(response)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def identifier
|
|
71
|
+
"#{http_method.upcase} #{path}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def capture!(response:)
|
|
77
|
+
ResponseCapturedHandler.new(request: self, response:).call
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
# A class that can be used to build an instance of an Alchemrest::Request class. The definition consists of the request class
|
|
5
|
+
# we want to create, and block that can extract default parameters from an Alchemrest::Root. When we want to build an
|
|
6
|
+
# instance of the request, call `build_request` and pass in the root as context, with any additional parameters.
|
|
7
|
+
class RequestDefinition
|
|
8
|
+
attr_reader :name
|
|
9
|
+
|
|
10
|
+
def initialize(name, request_class, &block)
|
|
11
|
+
@name = name
|
|
12
|
+
@request_class = request_class
|
|
13
|
+
@builder_block = block
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_request(context, params = {})
|
|
17
|
+
builder = Builder.new
|
|
18
|
+
|
|
19
|
+
if @builder_block
|
|
20
|
+
context.instance_exec(builder, &@builder_block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@request_class.new(**builder.defaults.merge(params))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Response
|
|
5
|
+
class Pipeline
|
|
6
|
+
# A transform that can extract a nested payload from a response body. Takes in an
|
|
7
|
+
# array of symbols, strings, or integers that represent the path to the data you care about.
|
|
8
|
+
# This class assumes the response.body is a hash with string keys, so it calls to_s on all
|
|
9
|
+
# non integer path elements.
|
|
10
|
+
class ExtractPayload < Alchemrest::Response::Pipeline::Transform
|
|
11
|
+
include Concord::Public.new(:path_to_payload, :allow_empty_response)
|
|
12
|
+
|
|
13
|
+
# # All symbols will be converted to strings, and integers will be left alone.
|
|
14
|
+
# @param path_to_payload [Array<Symbol, String, Integer>] The path to the payload you care about
|
|
15
|
+
# @param _allow_empty_response [bool] Should HTTP 204 OkNoContent empty responses be allowed
|
|
16
|
+
|
|
17
|
+
def initialize(_path_to_payload = nil, _allow_empty_response = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(response)
|
|
22
|
+
# If response is empty and the status is 204 (No Content), return nil as the payload
|
|
23
|
+
if response.no_content_response?
|
|
24
|
+
return final(nil) if allow_empty_response?
|
|
25
|
+
|
|
26
|
+
return failure(ResponsePipelineError.new("Ok but empty response not allowed"))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
payload = extract_payload(response)
|
|
30
|
+
if payload.nil?
|
|
31
|
+
failure(ResponsePipelineError.new("Response body did not contain expected payload at #{path_to_payload}"))
|
|
32
|
+
else
|
|
33
|
+
success(payload)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def extract_payload(response)
|
|
38
|
+
if path_to_payload.nil?
|
|
39
|
+
response.data
|
|
40
|
+
else
|
|
41
|
+
normalized_path = path_to_payload.map do |path_element|
|
|
42
|
+
if path_element.is_a?(Integer)
|
|
43
|
+
path_element
|
|
44
|
+
else
|
|
45
|
+
path_element.to_s
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
response.data.dig(*normalized_path)
|
|
49
|
+
end
|
|
50
|
+
# If our path tries to dig into a non hash, we'll get a TypeError. We want to rescue that and ensure
|
|
51
|
+
# our code continues to safely return a result.
|
|
52
|
+
rescue TypeError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def allow_empty_response?
|
|
59
|
+
allow_empty_response
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Response
|
|
5
|
+
class Pipeline
|
|
6
|
+
class Omit < Morpher::Transform
|
|
7
|
+
include Concord::Public.new(:omit)
|
|
8
|
+
private_constant(*constants(false))
|
|
9
|
+
PRIMITIVE = Primitive.new(::Hash)
|
|
10
|
+
|
|
11
|
+
def initialize(omit = nil)
|
|
12
|
+
@omit = omit
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(input)
|
|
17
|
+
if input.is_a?(::Array)
|
|
18
|
+
array.call(input)
|
|
19
|
+
else
|
|
20
|
+
PRIMITIVE
|
|
21
|
+
.call(input)
|
|
22
|
+
.bind { |i| omit_nodes(i) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def omit_nodes(input)
|
|
27
|
+
dup = input.dup
|
|
28
|
+
if omit.present?
|
|
29
|
+
delete_from(dup)
|
|
30
|
+
end
|
|
31
|
+
success(dup)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def delete_from(input)
|
|
35
|
+
omit.each do |path|
|
|
36
|
+
path.walk(input) do |_path, node, remaining_segments|
|
|
37
|
+
if remaining_segments.count == 1
|
|
38
|
+
node.delete(remaining_segments.last)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Response
|
|
5
|
+
class Pipeline
|
|
6
|
+
class Sanitize < Morpher::Transform
|
|
7
|
+
include Anima.new(:safe)
|
|
8
|
+
PRIMITIVE = Primitive.new(::Hash)
|
|
9
|
+
|
|
10
|
+
def initialize(args = { safe: nil })
|
|
11
|
+
@safe = args[:safe]
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(input)
|
|
16
|
+
if input.is_a?(::Array)
|
|
17
|
+
array.call(input)
|
|
18
|
+
else
|
|
19
|
+
PRIMITIVE
|
|
20
|
+
.call(input)
|
|
21
|
+
.bind { |i| sanitize(i) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def sanitize(input)
|
|
28
|
+
filtered = Alchemrest.parameter_filter.filter(input).with_indifferent_access
|
|
29
|
+
if safe.present?
|
|
30
|
+
copy(input, filtered)
|
|
31
|
+
end
|
|
32
|
+
success(filtered.deep_symbolize_keys)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def copy(original, filtered)
|
|
36
|
+
safe.each do |safe_path|
|
|
37
|
+
safe_path.walk(filtered) do |path, filtered_node, remaining_segments|
|
|
38
|
+
if remaining_segments.count == 1 && filtered_node.is_a?(::Hash)
|
|
39
|
+
next unless hash_has_key?(original, path, remaining_segments.last)
|
|
40
|
+
|
|
41
|
+
original_value = original.with_indifferent_access.dig(*(path + remaining_segments))
|
|
42
|
+
filtered_node[remaining_segments.last] = original_value
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def hash_has_key?(hash, path, key)
|
|
49
|
+
if path.empty?
|
|
50
|
+
hash.with_indifferent_access.key?(key)
|
|
51
|
+
else
|
|
52
|
+
hash.with_indifferent_access.dig(*path)
|
|
53
|
+
.key?(key)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|