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,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Data
|
|
5
|
+
# This class has two purposes. First it provides the DSL that allows developers to mark fields on a
|
|
6
|
+
# Alchemrest::Data class as safe or omitted. Second, it walks the entire object graph of nested
|
|
7
|
+
# objects to build up full path definitions for all the safe an omitted fields defined on child
|
|
8
|
+
# objects as well.
|
|
9
|
+
#
|
|
10
|
+
# @example Using `Alchemrest::Data::CaptureConfiguration` to delete define some safe and omitted fields
|
|
11
|
+
# CaptureConfiguration.new(BankApi::Data::User) do
|
|
12
|
+
# safe :name
|
|
13
|
+
# omitted :age
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# The attribute `path_to_payload` allows us to deal with situations where the domain data defined
|
|
17
|
+
# by the host class is nested inside a larger payload. For example `{ data: { user: { user data } } }`
|
|
18
|
+
class CaptureConfiguration
|
|
19
|
+
attr_reader :safe_keys, :omitted_keys, :path_to_payload
|
|
20
|
+
|
|
21
|
+
def initialize(host_class:, path_to_payload: nil, omitted_keys: [], safe_keys: [], &block)
|
|
22
|
+
raise ArgumentError, "Must be a sub class of Alchemrest::Data" unless host_class < Alchemrest::Data
|
|
23
|
+
|
|
24
|
+
@host = host_class
|
|
25
|
+
@path_to_payload = path_to_payload
|
|
26
|
+
@safe_keys = safe_keys
|
|
27
|
+
@omitted_keys = omitted_keys
|
|
28
|
+
instance_eval(&block) unless block.nil?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def with_path_to_payload(path_to_payload)
|
|
32
|
+
self.class.new(
|
|
33
|
+
host_class: @host,
|
|
34
|
+
path_to_payload:,
|
|
35
|
+
safe_keys:,
|
|
36
|
+
omitted_keys:,
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def safe_paths
|
|
41
|
+
paths = @safe_keys.dup
|
|
42
|
+
|
|
43
|
+
paths.concat(get_child_paths(:safe_paths))
|
|
44
|
+
paths.map { |path| prefix_with_path_to_payload_nodes(path) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def omitted_paths
|
|
48
|
+
paths = @omitted_keys.dup
|
|
49
|
+
|
|
50
|
+
paths.concat(get_child_paths(:omitted_paths))
|
|
51
|
+
paths.map { |path| prefix_with_path_to_payload_nodes(path) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def safe(*keys)
|
|
55
|
+
@safe_keys = keys
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def omitted(*keys)
|
|
59
|
+
@omitted_keys = keys
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def get_child_paths(type)
|
|
65
|
+
paths = []
|
|
66
|
+
@host.graph.sub_graphs.each do |key, child|
|
|
67
|
+
paths << { key => child.type.capture_configuration.public_send(type) }
|
|
68
|
+
end
|
|
69
|
+
paths
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def prefix_with_path_to_payload_nodes(path)
|
|
73
|
+
Array(path_to_payload).reverse.reduce(path) { |child, key| { key => child } }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Data
|
|
5
|
+
class Field
|
|
6
|
+
include Anima.new(:transform, :name, :required)
|
|
7
|
+
|
|
8
|
+
def initialize(transform:, name:, required: false)
|
|
9
|
+
unless transform.is_a?(Morpher::Transform) || transform.is_a?(Data)
|
|
10
|
+
raise ArgumentError, "transform must be an instance of Morpher::Transform or Alchemrest::Data, not #{transform.class}"
|
|
11
|
+
end
|
|
12
|
+
raise ArgumentError, "must provide name" unless name
|
|
13
|
+
raise ArgumentError, "must provide non-empty name" if name.to_s.strip.empty?
|
|
14
|
+
|
|
15
|
+
@transform = transform
|
|
16
|
+
@name = name.to_s
|
|
17
|
+
@required = required && !transform.instance_of?(Morpher::Transform::Maybe)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def output_type
|
|
21
|
+
base_type = transform.output_type if transform.respond_to?(:output_type)
|
|
22
|
+
return unless base_type
|
|
23
|
+
|
|
24
|
+
if required
|
|
25
|
+
base_type
|
|
26
|
+
else
|
|
27
|
+
base_type.with(sorbet_type: T.nilable(base_type.sorbet_type))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def constraints
|
|
32
|
+
output_type&.constraints || []
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Data
|
|
5
|
+
# A simple data structure to hold a graph of all the objects in a schema.
|
|
6
|
+
class Graph
|
|
7
|
+
include Anima.new(:type, :sub_graphs, :fields)
|
|
8
|
+
|
|
9
|
+
def initialize(type:, sub_graphs: nil, fields: nil)
|
|
10
|
+
unless type < Data
|
|
11
|
+
raise ArgumentError, "Graph types must be branching Alchemrest::Data class"
|
|
12
|
+
end
|
|
13
|
+
unless sub_graphs.nil? || sub_graph_hash_has_correct_types?(sub_graphs:)
|
|
14
|
+
raise ArgumentError, "sub_graphs must be a hash of graphs"
|
|
15
|
+
end
|
|
16
|
+
unless fields.nil? || fields_hash_has_correct_types?(fields:)
|
|
17
|
+
raise ArgumentError, "fields must be a hash of fields"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@type = type
|
|
21
|
+
@fields = fields
|
|
22
|
+
@sub_graphs = sub_graphs
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def children
|
|
26
|
+
sub_graphs
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def sub_graph_hash_has_correct_types?(sub_graphs:)
|
|
32
|
+
sub_graphs.instance_of?(Hash) && sub_graphs.values.all? { |graph| graph.instance_of?(Graph) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def fields_hash_has_correct_types?(fields:)
|
|
36
|
+
fields.instance_of?(Hash) && fields.values.all? { |graph| graph.instance_of?(Field) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Data
|
|
5
|
+
class Record < Module
|
|
6
|
+
DEFAULTS = {
|
|
7
|
+
required: Morpher::EMPTY_HASH,
|
|
8
|
+
optional: Morpher::EMPTY_HASH,
|
|
9
|
+
allow_additional_properties: true,
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
include Anima.new(:required, :optional, :allow_additional_properties)
|
|
13
|
+
|
|
14
|
+
def self.new(**attributes)
|
|
15
|
+
super(**DEFAULTS.merge(attributes))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# rubocop:disable Metrics/AbcSize
|
|
19
|
+
def included(host)
|
|
20
|
+
optional = optional()
|
|
21
|
+
optional_transform = transform(optional)
|
|
22
|
+
required = required()
|
|
23
|
+
required_transform = transform(required)
|
|
24
|
+
additional_properties_allowed = allow_additional_properties
|
|
25
|
+
|
|
26
|
+
host.class_eval do
|
|
27
|
+
include Anima.new(*(required.keys + optional.keys))
|
|
28
|
+
|
|
29
|
+
const_set(
|
|
30
|
+
:TRANSFORM,
|
|
31
|
+
Transforms::Typed.new(
|
|
32
|
+
transform: Morpher::Transform::Sequence.new(
|
|
33
|
+
[
|
|
34
|
+
Morpher::Transform::Primitive.new(Hash),
|
|
35
|
+
Morpher::Transform::Hash::Symbolize.new,
|
|
36
|
+
Transforms::LooseHash.new(
|
|
37
|
+
required: required_transform,
|
|
38
|
+
optional: optional_transform,
|
|
39
|
+
allow_additional_properties: additional_properties_allowed,
|
|
40
|
+
),
|
|
41
|
+
Morpher::Transform::Success.new(public_method(:new)),
|
|
42
|
+
],
|
|
43
|
+
),
|
|
44
|
+
output_type: Transforms::OutputType.simple(host),
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
# rubocop:enable Metrics/AbcSize
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def transform(attributes)
|
|
54
|
+
attributes.map do |name, transform|
|
|
55
|
+
Morpher::Transform::Hash::Key.new(name, transform)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Data
|
|
5
|
+
class Schema < Alchemrest::Data::Record
|
|
6
|
+
def included(host)
|
|
7
|
+
super
|
|
8
|
+
|
|
9
|
+
host.extend(ClassMethods)
|
|
10
|
+
|
|
11
|
+
host.const_set(
|
|
12
|
+
:Collection,
|
|
13
|
+
Class.new(host) do
|
|
14
|
+
const_set :TRANSFORM, superclass::TRANSFORM.array
|
|
15
|
+
|
|
16
|
+
def self.capture_configuration
|
|
17
|
+
superclass.capture_configuration
|
|
18
|
+
end
|
|
19
|
+
end,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Store the graph as a constant for easy access from other classes
|
|
23
|
+
host.const_set(:GRAPH, extract_graph(host))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def extract_graph(host)
|
|
29
|
+
Graph.new(
|
|
30
|
+
type: host,
|
|
31
|
+
sub_graphs: extract_sub_graphs,
|
|
32
|
+
fields: extract_fields,
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def extract_sub_graphs
|
|
37
|
+
required.merge(optional)
|
|
38
|
+
.filter { |_k, v| v.respond_to?(:output_type) && v.output_type.graph }
|
|
39
|
+
.transform_values { |v| v.output_type.graph }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def extract_fields
|
|
43
|
+
required_fields = required.map { |k, v| [k, Field.new(transform: v, name: k, required: true)] }
|
|
44
|
+
optional_fields = optional.map { |k, v| [k, Field.new(transform: v, name: k)] }
|
|
45
|
+
(required_fields + optional_fields).to_h
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module ClassMethods
|
|
49
|
+
def capture_configuration
|
|
50
|
+
@capture_configuration ||= CaptureConfiguration.new(host_class: self)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def response_transformer
|
|
54
|
+
Alchemrest::ResponseTransformers::Morpher.new(self::TRANSFORM)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def from_hash(hash)
|
|
58
|
+
transform_prelude = self::TRANSFORM.call(hash.deep_stringify_keys)
|
|
59
|
+
if transform_prelude.right?
|
|
60
|
+
transform_prelude.from_right
|
|
61
|
+
else
|
|
62
|
+
raise MorpherTransformError, transform_prelude.from_left
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def []
|
|
67
|
+
self::Collection
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def graph
|
|
71
|
+
self::GRAPH
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def configure_response_capture(&)
|
|
75
|
+
@capture_configuration = CaptureConfiguration.new(host_class: self, &)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class EndpointDefinition
|
|
5
|
+
VALID_HTTP_METHODS = IceNine.deep_freeze(%w(get post put delete head patch options trace).to_set)
|
|
6
|
+
private_constant(:VALID_HTTP_METHODS)
|
|
7
|
+
|
|
8
|
+
include Anima.new(:template, :http_method, :builder_block)
|
|
9
|
+
|
|
10
|
+
def initialize(template:, http_method:, builder_block:)
|
|
11
|
+
super
|
|
12
|
+
raise ArgumentError, "template must be string" unless template.instance_of?(String)
|
|
13
|
+
|
|
14
|
+
raise ArgumentError, "missing template" if template.empty?
|
|
15
|
+
|
|
16
|
+
downcased_http_method = http_method.downcase
|
|
17
|
+
raise ArgumentError, "must provide a valid HTTP method" unless VALID_HTTP_METHODS.include?(downcased_http_method)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def url_for(context)
|
|
21
|
+
build_for(context).url
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def params_for(context)
|
|
25
|
+
builder = build_for(context)
|
|
26
|
+
if builder.params
|
|
27
|
+
builder.params
|
|
28
|
+
elsif builder.values || builder.query
|
|
29
|
+
(builder.values || {}).merge(builder.query || {}).transform_values { |v| CGI.escape(v.to_s) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_for(context)
|
|
36
|
+
options = UrlBuilder::Options.new
|
|
37
|
+
if builder_block&.arity&.zero?
|
|
38
|
+
Alchemrest.deprecator.warn HASH_RETURNING_BLOCK_MESSAGE
|
|
39
|
+
options.params = context.instance_exec(&builder_block)
|
|
40
|
+
elsif builder_block
|
|
41
|
+
context.instance_exec(options, &builder_block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
options.create_builder(template:)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
def deconstruct
|
|
6
|
+
[to_s]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def deconstruct_keys(_keys)
|
|
10
|
+
{ error: to_s }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class ResponseError < Error
|
|
15
|
+
attr_reader :response
|
|
16
|
+
|
|
17
|
+
def initialize(response)
|
|
18
|
+
super
|
|
19
|
+
@response = response
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
method = response.env.method
|
|
24
|
+
url = response.env.url
|
|
25
|
+
[
|
|
26
|
+
"HTTP #{response.status} for #{method.upcase} #{url}",
|
|
27
|
+
response.error_details.to_s,
|
|
28
|
+
].compact.join(" - ")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def deconstruct
|
|
32
|
+
[response.status, response.error_details]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def deconstruct_keys(_keys)
|
|
36
|
+
{ status: response.status, error: response.error_details }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class ServerError < ResponseError
|
|
41
|
+
attr_reader :response, :cause
|
|
42
|
+
|
|
43
|
+
def initialize(response, cause = nil)
|
|
44
|
+
@response = response
|
|
45
|
+
@cause = cause
|
|
46
|
+
super(response)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
if response.circuit_open?
|
|
51
|
+
"CIRCUIT OPEN"
|
|
52
|
+
elsif response.timeout?
|
|
53
|
+
"#{response.status} timeout"
|
|
54
|
+
else
|
|
55
|
+
error_message
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def error_message
|
|
62
|
+
method = response.env.method
|
|
63
|
+
url = response.env.url
|
|
64
|
+
[
|
|
65
|
+
"HTTP #{response.status} for #{method.upcase} #{url}",
|
|
66
|
+
response.error_details.to_s,
|
|
67
|
+
cause,
|
|
68
|
+
].compact.join(" - ")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class ClientError < ResponseError; end
|
|
73
|
+
|
|
74
|
+
class AuthError < ClientError; end
|
|
75
|
+
|
|
76
|
+
class NotFoundError < ClientError; end
|
|
77
|
+
|
|
78
|
+
class UndefinedClientError < Error; end
|
|
79
|
+
|
|
80
|
+
class RequestFailedError < Error; end
|
|
81
|
+
|
|
82
|
+
class TimeoutError < RequestFailedError; end
|
|
83
|
+
|
|
84
|
+
class TransformError < Error; end
|
|
85
|
+
|
|
86
|
+
class ResultRescued < Error; end
|
|
87
|
+
|
|
88
|
+
class CircuitOpenError < Error; end
|
|
89
|
+
|
|
90
|
+
class MorpherTransformError < Error
|
|
91
|
+
attr_reader :error
|
|
92
|
+
|
|
93
|
+
def initialize(error)
|
|
94
|
+
@error = error
|
|
95
|
+
super()
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def to_s
|
|
99
|
+
"Response does not match expected schema - #{error.compact_message}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class ResponsePipelineError < Error; end
|
|
104
|
+
|
|
105
|
+
class InvalidConfigurationError < Error; end
|
|
106
|
+
|
|
107
|
+
class ConfigurationNotReadyError < Error; end
|
|
108
|
+
|
|
109
|
+
class KillSwitchEnabledError < Error; end
|
|
110
|
+
|
|
111
|
+
class NoRegisteredTransformError < Error
|
|
112
|
+
def initialize(from:, to:)
|
|
113
|
+
super("No registered transform for #{from.output_type_name} -> #{to}")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class NoTransformOptionForNameError < Error
|
|
118
|
+
def initialize(from:, to:, name:, options:)
|
|
119
|
+
super("No transform named #{name} for #{from.output_type_name} -> #{to}. Available options: #{options.keys.join(', ')}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
module FactoryBot
|
|
5
|
+
def self.enable!
|
|
6
|
+
::FactoryBot.register_strategy(:alchemrest_record_for, Alchemrest::FactoryBot::AlchemrestStrategy)
|
|
7
|
+
::FactoryBot.register_strategy(:alchemrest_hash_for, Alchemrest::FactoryBot::HashStrategy)
|
|
8
|
+
|
|
9
|
+
::FactoryBot::Syntax::Default::DSL.class_eval do
|
|
10
|
+
include Alchemrest::FactoryBot::Mixins
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class OmitKey
|
|
15
|
+
include Singleton
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module Mixins
|
|
19
|
+
def alchemrest_factory(name, options = {}, &block)
|
|
20
|
+
raise ArgumentError, "You must specify a class" unless options[:class]
|
|
21
|
+
raise ArgumentError, "You must specify a block" unless block
|
|
22
|
+
|
|
23
|
+
factory(name, options) do
|
|
24
|
+
skip_create
|
|
25
|
+
|
|
26
|
+
initialize_with do
|
|
27
|
+
provided_attributes = attributes.reject { |_, v| v.is_a?(Alchemrest::FactoryBot::OmitKey) }
|
|
28
|
+
options[:class].constantize.from_hash(provided_attributes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
instance_eval(&block)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class AlchemrestStrategy
|
|
37
|
+
def association(runner)
|
|
38
|
+
runner.run(:alchemrest_hash_for)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def result(evaluation)
|
|
42
|
+
evaluation.object
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_sym
|
|
46
|
+
:alchemrest_record_for
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class HashStrategy
|
|
51
|
+
def association(runner)
|
|
52
|
+
runner.run(:alchemrest_hash_for)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def result(evaluation)
|
|
56
|
+
evaluation.hash.reject { |_, v| v.is_a?(Alchemrest::FactoryBot::OmitKey) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_sym
|
|
60
|
+
:alchemrest_hash_for
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
module FaradayMiddleware
|
|
5
|
+
class ExternalApiInstrumentation < Faraday::Middleware
|
|
6
|
+
attr_reader :external_service
|
|
7
|
+
|
|
8
|
+
def initialize(app, options = {})
|
|
9
|
+
super(app)
|
|
10
|
+
@external_service = options.fetch(:external_service)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
::ActiveSupport::Notifications.instrument(name, env: env, external_service: external_service) do
|
|
15
|
+
@app.call(env)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def name
|
|
20
|
+
"#{external_service}_api_request"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
module FaradayMiddleware
|
|
5
|
+
class JsonParser < Faraday::Middleware
|
|
6
|
+
DEFAULT_RESPONSE = '{}'
|
|
7
|
+
|
|
8
|
+
def on_complete(env)
|
|
9
|
+
# stash the body for debugging
|
|
10
|
+
env[:raw_body] = env.body
|
|
11
|
+
|
|
12
|
+
env.body = if env.parse_body?
|
|
13
|
+
parse_body(env.body)
|
|
14
|
+
else
|
|
15
|
+
{}
|
|
16
|
+
end
|
|
17
|
+
rescue MultiJson::ParseError
|
|
18
|
+
env.body = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def parse_body(raw_body)
|
|
22
|
+
MultiJson.load(parseable_body(raw_body))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parseable_body(raw_body)
|
|
26
|
+
raw_body.presence || DEFAULT_RESPONSE
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
module FaradayMiddleware
|
|
5
|
+
class KillSwitch < Faraday::Middleware
|
|
6
|
+
attr_reader :service_name
|
|
7
|
+
|
|
8
|
+
def initialize(app, options = {})
|
|
9
|
+
@service_name = options.fetch(:service_name)
|
|
10
|
+
super(app)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
kill_switch = Alchemrest::KillSwitch.new(service_name:)
|
|
15
|
+
|
|
16
|
+
raise KillSwitchEnabledError if kill_switch.active?
|
|
17
|
+
|
|
18
|
+
app.call(env)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemrest
|
|
4
|
+
module FaradayMiddleware
|
|
5
|
+
class UnderScoreResponse < Faraday::Middleware
|
|
6
|
+
def on_complete(env)
|
|
7
|
+
env.body = deep_transform_keys!(env.body)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def deep_transform_keys!(object)
|
|
13
|
+
case object
|
|
14
|
+
when Array
|
|
15
|
+
object.each { |item| deep_transform_keys!(item) }
|
|
16
|
+
when Hash
|
|
17
|
+
object.deep_transform_keys! { |k| k.to_s.underscore }
|
|
18
|
+
else
|
|
19
|
+
object
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|