smithy-client 1.0.0.pre0 → 1.0.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/VERSION +1 -1
  4. data/lib/smithy-client/anonymous_provider.rb +12 -0
  5. data/lib/smithy-client/auth_option.rb +23 -0
  6. data/lib/smithy-client/auth_scheme.rb +25 -0
  7. data/lib/smithy-client/auth_schemes/anonymous.rb +18 -0
  8. data/lib/smithy-client/auth_schemes/http_api_key.rb +18 -0
  9. data/lib/smithy-client/auth_schemes/http_basic.rb +18 -0
  10. data/lib/smithy-client/auth_schemes/http_bearer.rb +18 -0
  11. data/lib/smithy-client/auth_schemes/http_digest.rb +18 -0
  12. data/lib/smithy-client/base.rb +200 -0
  13. data/lib/smithy-client/block_io.rb +36 -0
  14. data/lib/smithy-client/configuration.rb +222 -0
  15. data/lib/smithy-client/default_params.rb +91 -0
  16. data/lib/smithy-client/dynamic_errors.rb +82 -0
  17. data/lib/smithy-client/endpoint_rules.rb +186 -0
  18. data/lib/smithy-client/handler.rb +29 -0
  19. data/lib/smithy-client/handler_builder.rb +33 -0
  20. data/lib/smithy-client/handler_context.rb +67 -0
  21. data/lib/smithy-client/handler_list.rb +197 -0
  22. data/lib/smithy-client/handler_list_entry.rb +102 -0
  23. data/lib/smithy-client/http/error_inspector.rb +87 -0
  24. data/lib/smithy-client/http/headers.rb +122 -0
  25. data/lib/smithy-client/http/request.rb +57 -0
  26. data/lib/smithy-client/http/response.rb +178 -0
  27. data/lib/smithy-client/http_api_key_provider.rb +18 -0
  28. data/lib/smithy-client/http_bearer_provider.rb +18 -0
  29. data/lib/smithy-client/http_login_provider.rb +19 -0
  30. data/lib/smithy-client/identities/anonymous.rb +10 -0
  31. data/lib/smithy-client/identities/http_api_key.rb +18 -0
  32. data/lib/smithy-client/identities/http_bearer.rb +18 -0
  33. data/lib/smithy-client/identities/http_login.rb +22 -0
  34. data/lib/smithy-client/identity.rb +15 -0
  35. data/lib/smithy-client/log_formatter.rb +215 -0
  36. data/lib/smithy-client/log_param_filter.rb +88 -0
  37. data/lib/smithy-client/log_param_formatter.rb +65 -0
  38. data/lib/smithy-client/managed_file.rb +14 -0
  39. data/lib/smithy-client/net_http/connection_pool.rb +297 -0
  40. data/lib/smithy-client/net_http/handler.rb +160 -0
  41. data/lib/smithy-client/net_http/patches.rb +28 -0
  42. data/lib/smithy-client/networking_error.rb +16 -0
  43. data/lib/smithy-client/pageable_response.rb +138 -0
  44. data/lib/smithy-client/param_converter.rb +243 -0
  45. data/lib/smithy-client/param_validator.rb +213 -0
  46. data/lib/smithy-client/plugin.rb +144 -0
  47. data/lib/smithy-client/plugin_list.rb +141 -0
  48. data/lib/smithy-client/plugins/anonymous_auth.rb +23 -0
  49. data/lib/smithy-client/plugins/checksum_required.rb +51 -0
  50. data/lib/smithy-client/plugins/content_length.rb +26 -0
  51. data/lib/smithy-client/plugins/default_params.rb +22 -0
  52. data/lib/smithy-client/plugins/host_prefix.rb +69 -0
  53. data/lib/smithy-client/plugins/http_api_key_auth.rb +37 -0
  54. data/lib/smithy-client/plugins/http_basic_auth.rb +47 -0
  55. data/lib/smithy-client/plugins/http_bearer_auth.rb +37 -0
  56. data/lib/smithy-client/plugins/http_digest_auth.rb +60 -0
  57. data/lib/smithy-client/plugins/idempotency_token.rb +34 -0
  58. data/lib/smithy-client/plugins/logging.rb +56 -0
  59. data/lib/smithy-client/plugins/net_http.rb +163 -0
  60. data/lib/smithy-client/plugins/pageable_response.rb +37 -0
  61. data/lib/smithy-client/plugins/param_converter.rb +32 -0
  62. data/lib/smithy-client/plugins/param_validator.rb +30 -0
  63. data/lib/smithy-client/plugins/protocol.rb +66 -0
  64. data/lib/smithy-client/plugins/raise_response_errors.rb +33 -0
  65. data/lib/smithy-client/plugins/request_compression.rb +200 -0
  66. data/lib/smithy-client/plugins/response_target.rb +71 -0
  67. data/lib/smithy-client/plugins/retry_errors.rb +125 -0
  68. data/lib/smithy-client/plugins/sign_requests.rb +24 -0
  69. data/lib/smithy-client/plugins/stub_responses.rb +102 -0
  70. data/lib/smithy-client/protocol_spec_matcher.rb +60 -0
  71. data/lib/smithy-client/refreshing_identity_provider.rb +65 -0
  72. data/lib/smithy-client/request.rb +76 -0
  73. data/lib/smithy-client/response.rb +48 -0
  74. data/lib/smithy-client/retry/adaptive.rb +66 -0
  75. data/lib/smithy-client/retry/client_rate_limiter.rb +142 -0
  76. data/lib/smithy-client/retry/quota.rb +58 -0
  77. data/lib/smithy-client/retry/standard.rb +52 -0
  78. data/lib/smithy-client/retry.rb +36 -0
  79. data/lib/smithy-client/rpc_v2_cbor/protocol.rb +38 -0
  80. data/lib/smithy-client/rpc_v2_cbor/request_builder.rb +76 -0
  81. data/lib/smithy-client/rpc_v2_cbor/response_parser.rb +86 -0
  82. data/lib/smithy-client/rpc_v2_cbor/response_stubber.rb +34 -0
  83. data/lib/smithy-client/service_error.rb +57 -0
  84. data/lib/smithy-client/signer.rb +16 -0
  85. data/lib/smithy-client/signers/anonymous.rb +13 -0
  86. data/lib/smithy-client/signers/http_api_key.rb +52 -0
  87. data/lib/smithy-client/signers/http_basic.rb +23 -0
  88. data/lib/smithy-client/signers/http_bearer.rb +19 -0
  89. data/lib/smithy-client/signers/http_digest.rb +21 -0
  90. data/lib/smithy-client/stubbing/data_applicator.rb +61 -0
  91. data/lib/smithy-client/stubbing/empty_stub.rb +69 -0
  92. data/lib/smithy-client/stubbing/endpoint_provider.rb +22 -0
  93. data/lib/smithy-client/stubbing/protocol.rb +29 -0
  94. data/lib/smithy-client/stubbing/stub_data.rb +25 -0
  95. data/lib/smithy-client/stubbing.rb +14 -0
  96. data/lib/smithy-client/stubs.rb +212 -0
  97. data/lib/smithy-client/util.rb +15 -0
  98. data/lib/smithy-client/waiters/poller.rb +93 -0
  99. data/lib/smithy-client/waiters/waiter.rb +113 -0
  100. data/lib/smithy-client.rb +66 -1
  101. metadata +163 -9
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'time'
5
+
6
+ module Smithy
7
+ module Client
8
+ # @api private
9
+ class DefaultParams
10
+ include Schema::Shapes
11
+
12
+ def initialize(ref)
13
+ @ref = ref
14
+ end
15
+
16
+ # @param [Hash] params
17
+ # @return [Hash]
18
+ def apply(params)
19
+ structure(@ref, params)
20
+ end
21
+
22
+ private
23
+
24
+ def shape(ref, value)
25
+ case ref.shape
26
+ when ListShape then list(ref, value)
27
+ when MapShape then map(ref, value)
28
+ when StructureShape then structure(ref, value)
29
+ else value
30
+ end
31
+ end
32
+
33
+ def list(ref, values)
34
+ return if values.nil?
35
+
36
+ shape = ref.shape
37
+ values.each do |value|
38
+ shape(shape.member, value)
39
+ end
40
+ values
41
+ end
42
+
43
+ def map(ref, values)
44
+ return if values.nil?
45
+
46
+ shape = ref.shape
47
+ values.each_pair do |_key, value|
48
+ shape(shape.value, value)
49
+ end
50
+ values
51
+ end
52
+
53
+ def structure(ref, values)
54
+ return if values.nil?
55
+
56
+ ref.shape.members.each do |member_name, member_ref|
57
+ value = values[member_name]
58
+ value ||= default(member_ref) if default?(ref, member_ref.traits)
59
+ next if value.nil? && !default?(ref, member_ref.traits) # default can have nil values
60
+
61
+ values[member_name] = shape(member_ref, value)
62
+ end
63
+ values
64
+ end
65
+
66
+ def default?(ref, traits)
67
+ # skip defaults for top level members
68
+ return false if ref == @ref
69
+
70
+ traits.include?('smithy.api#default') && !traits.include?('smithy.api#clientOptional')
71
+ end
72
+
73
+ def default(ref)
74
+ default = ref.traits['smithy.api#default']
75
+ case ref.shape
76
+ when BlobShape then Base64.strict_decode64(default)
77
+ when TimestampShape then timestamp_default(default)
78
+ else default
79
+ end
80
+ end
81
+
82
+ def timestamp_default(default)
83
+ case default
84
+ when String then Time.parse(default)
85
+ when Integer then Time.at(default)
86
+ else raise ArgumentError, "Invalid default value for Timestamp: #{default.inspect}"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # This module is mixed into generated Errors modules, providing dynamic
6
+ # error classes. Error classes all inherit from {ServiceError}.
7
+ #
8
+ # # Creates and returns the class
9
+ # Weather::Errors::MyNewErrorClass
10
+ #
11
+ # Since the complete list of possible errors returned by services may
12
+ # not be known, this allows us to create them as needed. This also
13
+ # allows users to rescue errors by class without them being concrete
14
+ # classes beforehand.
15
+ #
16
+ # @api private
17
+ module DynamicErrors
18
+ def self.extended(submodule)
19
+ submodule.instance_variable_set('@const_set_mutex', Mutex.new)
20
+ submodule.const_set(:ServiceError, Class.new(ServiceError))
21
+ end
22
+
23
+ def const_missing(constant)
24
+ set_error_constant(constant)
25
+ end
26
+
27
+ # Given the name of a service and an error code, this method
28
+ # returns an error class that extends {ServiceError}.
29
+ #
30
+ # Weather::Errors.error_class('NoSuchCity').new
31
+ # #=> #<Weather::Errors::NoSuchCity>
32
+ #
33
+ # @api private
34
+ def error_class(error_code)
35
+ constant = error_class_constant(error_code)
36
+ if error_const_set?(constant)
37
+ err_class = const_get(constant)
38
+ err_class.code = constant.to_s
39
+ err_class
40
+ else
41
+ set_error_constant(constant)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Convert an error code to an error class name/constant.
48
+ # This requires filtering non-safe characters from the constant
49
+ # name and ensuring it begins with an uppercase letter.
50
+ #
51
+ # @param [String] error_code
52
+ # @return [Symbol] Returns a symbolized constant name for the given `error_code`.
53
+ def error_class_constant(error_code)
54
+ constant = error_code.to_s
55
+ constant = constant.gsub(/https?:.*$/, '')
56
+ constant = constant.gsub(/[^a-zA-Z0-9]/, '')
57
+ constant = "Error#{constant}" unless constant.match(/^[a-z]/i)
58
+ constant = constant[0].upcase + constant[1..]
59
+ constant.to_sym
60
+ end
61
+
62
+ def set_error_constant(constant) # rubocop:disable Naming/AccessorMethodName
63
+ @const_set_mutex.synchronize do
64
+ # Ensure the const was not defined while blocked by the mutex
65
+ if error_const_set?(constant)
66
+ const_get(constant)
67
+ else
68
+ error_class = Class.new(const_get(:ServiceError))
69
+ error_class.code = constant.to_s
70
+ const_set(constant, error_class)
71
+ end
72
+ end
73
+ end
74
+
75
+ def error_const_set?(constant)
76
+ # Purposefully not using #const_defined? as that method returns true
77
+ # for constants not defined directly in the current module.
78
+ constants.include?(constant.to_sym)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+ require 'ipaddr'
5
+ require 'uri'
6
+
7
+ module Smithy
8
+ module Client
9
+ # Functions in the Smithy rules engine are named routines that
10
+ # operate on a finite set of specified inputs, returning an output.
11
+ # The rules engine has a set of included functions that can be
12
+ # invoked without additional dependencies, called the standard library.
13
+ module EndpointRules
14
+ # Regex that extracts anything in square brackets
15
+ BRACKET_REGEX = /\[(.*?)\]/
16
+
17
+ # An Endpoint resolved by an EndpointProvider
18
+ class Endpoint
19
+ # @param [String] uri
20
+ # @param [Hash] properties ({})
21
+ # @param [Hash] headers ({})
22
+ def initialize(uri:, properties: {}, headers: {})
23
+ @uri = uri
24
+ @properties = properties
25
+ @headers = headers
26
+ end
27
+
28
+ # The URI of the endpoint.
29
+ # @return [String]
30
+ attr_accessor :uri
31
+
32
+ # The authentication schemes supported by the endpoint.
33
+ # @return [Hash]
34
+ attr_accessor :properties
35
+
36
+ # The headers to include in requests to the endpoint.
37
+ # @return [Hash]
38
+ attr_accessor :headers
39
+ end
40
+
41
+ # Evaluates whether the input string is a compliant RFC 1123 host segment.
42
+ # When allowSubDomains is true, evaluates whether the input string is
43
+ # composed of values that are each compliant RFC 1123 host segments
44
+ # joined by dot (.) characters.
45
+ # @api private
46
+ def self.valid_host_label?(value, allow_sub_domains)
47
+ return false if value.empty?
48
+
49
+ if allow_sub_domains
50
+ labels = value.split('.', -1)
51
+ return labels.all? { |l| valid_host_label?(l, false) }
52
+ end
53
+
54
+ !!(value =~ /\A(?!-)[a-zA-Z0-9-]{1,63}(?<!-)\z/)
55
+ end
56
+
57
+ # Computes a URL structure given an input string.
58
+ # @api private
59
+ def self.parse_url(value)
60
+ URL.new(value).as_json
61
+ rescue ArgumentError, URI::InvalidURIError
62
+ nil
63
+ end
64
+
65
+ # Computes a portion of a given string based on
66
+ # the provided start and end indices.
67
+ # @api private
68
+ def self.substring(input, start, stop, reverse)
69
+ return nil if start >= stop || input.size < stop
70
+
71
+ return nil if input.chars.any? { |c| c.ord > 127 }
72
+
73
+ return input[start...stop] unless reverse
74
+
75
+ r_start = input.size - stop
76
+ r_stop = input.size - start
77
+ input[r_start...r_stop]
78
+ end
79
+
80
+ # Performs RFC 3986#section-2.1 defined percent-encoding on the input value.
81
+ # @api private
82
+ def self.uri_encode(value)
83
+ CGI.escape(value.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~')
84
+ end
85
+
86
+ # isSet(value: Option<T>) bool
87
+ # @api private
88
+ def self.set?(value)
89
+ !value.nil?
90
+ end
91
+
92
+ # not(value: bool) bool
93
+ # @api private
94
+ def self.not?(bool)
95
+ !bool
96
+ end
97
+
98
+ # getAttr(value: Object | Array, path: string) Document
99
+ # @api private
100
+ def self.attr(value, path)
101
+ parts = path.split('.')
102
+
103
+ val = attr_brackets(parts, value)
104
+
105
+ if parts.size == 1
106
+ val
107
+ else
108
+ attr(val, parts.slice(1..-1).join('.')) # rubocop:disable Style/Attr
109
+ end
110
+ end
111
+
112
+ # stringEquals(value1: string, value2: string) bool
113
+ # @api private
114
+ def self.string_equals?(value1, value2)
115
+ value1 == value2
116
+ end
117
+
118
+ # booleanEquals(value1: bool, value2: bool) bool
119
+ # @api private
120
+ def self.boolean_equals?(value1, value2)
121
+ value1 == value2
122
+ end
123
+
124
+ # @api private
125
+ class URL
126
+ def initialize(url)
127
+ uri = URI(url)
128
+ @scheme = uri.scheme
129
+ # only support http and https schemes
130
+ raise ArgumentError unless %w[https http].include?(@scheme)
131
+
132
+ # do not support query
133
+ raise ArgumentError if uri.query
134
+
135
+ @authority = extract_authority(url, uri)
136
+ @path = uri.path
137
+ @normalized_path = uri.path + (uri.path[-1] == '/' ? '' : '/')
138
+ @is_ip = ip?(uri.host)
139
+ end
140
+
141
+ attr_reader :scheme, :authority, :path, :normalized_path, :is_ip
142
+
143
+ def as_json(*)
144
+ {
145
+ 'scheme' => scheme,
146
+ 'authority' => authority,
147
+ 'path' => path,
148
+ 'normalizedPath' => normalized_path,
149
+ 'isIp' => is_ip
150
+ }
151
+ end
152
+
153
+ private
154
+
155
+ def extract_authority(url, uri)
156
+ # don't include port if it's default and not parsed originally
157
+ if uri.default_port == uri.port && !url.include?(":#{uri.port}")
158
+ uri.host
159
+ else
160
+ "#{uri.host}:#{uri.port}"
161
+ end
162
+ end
163
+
164
+ def ip?(authority)
165
+ IPAddr.new(authority)
166
+ true
167
+ rescue IPAddr::InvalidAddressError
168
+ false
169
+ end
170
+ end
171
+
172
+ def self.attr_brackets(parts, value)
173
+ if (index = parts.first[BRACKET_REGEX, 1])
174
+ # remove brackets and index from part before indexing
175
+ if (base = parts.first.gsub(BRACKET_REGEX, '')) && !base.empty?
176
+ value[base][index.to_i]
177
+ else
178
+ value[index.to_i]
179
+ end
180
+ else
181
+ value[parts.first]
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # Base class for handlers in the client stack.
6
+ class Handler
7
+ # @param [Handler] handler (nil) The next handler in the stack that
8
+ # should be called from within the {#call} method. This value
9
+ # must only be nil for send handlers.
10
+ def initialize(handler = nil)
11
+ @handler = handler
12
+ end
13
+
14
+ # @return [Handler, nil]
15
+ attr_accessor :handler
16
+
17
+ # @param [Smithy::Client::HandlerContext] context
18
+ # @return [Smithy::Client::Response]
19
+ def call(context)
20
+ @handler.call(context)
21
+ end
22
+
23
+ # @api private
24
+ def inspect
25
+ "#<#{self.class.name || 'UnknownHandler'} @handler=#{@handler.inspect}>"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # This module provides the ability to add handlers to a class or
6
+ # module. The including class or extending module must respond to
7
+ # `#handlers`, returning a {HandlerList}.
8
+ module HandlerBuilder
9
+ def handle(*args, &block)
10
+ options = args.last.is_a?(Hash) ? args.pop : {}
11
+ handler_class = block ? handler_for(*args, &block) : args.first
12
+ handlers.add(handler_class, options)
13
+ end
14
+ alias handler handle
15
+
16
+ private
17
+
18
+ def handler_for(name = nil, &block)
19
+ if name
20
+ const_set(name, new_handler(block))
21
+ else
22
+ new_handler(block)
23
+ end
24
+ end
25
+
26
+ def new_handler(block)
27
+ Class.new(Handler) do
28
+ define_method(:call, &block)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # Context that is passed to handlers during execution.
6
+ class HandlerContext
7
+ # @option options [Symbol] :operation_name (nil)
8
+ # @option options [OperationShape] :operation (nil)
9
+ # @option options [Base] :client (nil)
10
+ # @option options [Hash] :params ({})
11
+ # @option options [Configuration] :config (nil)
12
+ # @option options [HTTP::Request] :http_request (HTTP::Request.new)
13
+ # @option options [HTTP::Response] :http_response (HTTP::Response.new)
14
+ # @option options [Hash] :metadata ({})
15
+ def initialize(options = {})
16
+ @operation_name = options[:operation_name]
17
+ @operation = options[:operation]
18
+ @client = options[:client]
19
+ @params = options[:params] || {}
20
+ @config = options[:config]
21
+ @http_request = options[:http_request] || HTTP::Request.new
22
+ @http_response = options[:http_response] || HTTP::Response.new
23
+ @retries = 0
24
+ @metadata = {}
25
+ end
26
+
27
+ # @return [Symbol] Name of the API operation called.
28
+ attr_accessor :operation_name
29
+
30
+ # @return [OperationShape] Shape of the Operation called.
31
+ attr_accessor :operation
32
+
33
+ # @return [Base]
34
+ attr_accessor :client
35
+
36
+ # @return [Hash] The request parameters as a Hash.
37
+ attr_accessor :params
38
+
39
+ # @return [Struct] The client configuration.
40
+ attr_accessor :config
41
+
42
+ # @return [HTTP::Request]
43
+ attr_accessor :http_request
44
+
45
+ # @return [HTTP::Response]
46
+ attr_accessor :http_response
47
+
48
+ # @return [Integer]
49
+ attr_accessor :retries
50
+
51
+ # @return [Hash]
52
+ attr_reader :metadata
53
+
54
+ # @param [Symbol] key
55
+ # @return [Object]
56
+ def [](key)
57
+ @metadata[key]
58
+ end
59
+
60
+ # @param [Symbol] key
61
+ # @param [Object] value
62
+ def []=(key, value)
63
+ @metadata[key] = value
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # A list of handlers that are used to build a handler stack.
6
+ class HandlerList
7
+ include Enumerable
8
+
9
+ # @api private
10
+ def initialize(options = {})
11
+ @index = options[:index] || 0
12
+ @entries = {}
13
+ @mutex = Mutex.new
14
+ entries = options[:entries] || []
15
+ add_entries(entries) unless entries.empty?
16
+ end
17
+
18
+ # Yields the handlers in stack order, which is reverse priority.
19
+ # @yieldparam [Class<Handler>] handler_class
20
+ def each
21
+ entries.sort.each do |entry|
22
+ yield(entry.handler_class) if entry.operations.nil?
23
+ end
24
+ end
25
+
26
+ # @return [Array<HandlerListEntry>]
27
+ def entries
28
+ @mutex.synchronize do
29
+ @entries.values
30
+ end
31
+ end
32
+
33
+ # Registers a handler. Handlers are used to build a handler stack.
34
+ # Handlers default to the `:build` step with default priority of 50.
35
+ # The step and priority determine where in the stack a handler
36
+ # will be.
37
+ #
38
+ # ## Handler Stack Ordering
39
+ #
40
+ # A handler stack is built from the inside-out. The stack is
41
+ # seeded with the send handler. Handlers are constructed recursively
42
+ # in reverse step and priority order so that the highest priority
43
+ # handler is on the outside.
44
+ #
45
+ # By constructing the stack from the inside-out, this ensures
46
+ # that the validate handlers will be called first and the sign handlers
47
+ # will be called just before the final and only send handler is called.
48
+ #
49
+ # ## Steps
50
+ #
51
+ # Handlers are ordered first by step. These steps represent the
52
+ # life-cycle of a request. Valid steps are:
53
+ #
54
+ # * `:initialize`
55
+ # * `:validate`
56
+ # * `:build`
57
+ # * `:retry`
58
+ # * `:parse`
59
+ # * `:sign`
60
+ # * `:send`
61
+ #
62
+ # Many handlers can be added to the same step, except for `:send`.
63
+ # There can be only one `:send` handler. Adding an additional
64
+ # `:send` handler replaces the previous one.
65
+ #
66
+ # ## Priorities
67
+ #
68
+ # Handlers within a single step are executed in priority order. The
69
+ # higher the priority, the earlier in the stack the handler will
70
+ # be called.
71
+ #
72
+ # * Handler priority is an integer between 0 and 99, inclusively.
73
+ # * Handler priority defaults to 50.
74
+ # * When multiple handlers are added to the same step with the same
75
+ # priority, the last one added will have the highest priority and
76
+ # the first one added will have the lowest priority.
77
+ #
78
+ # @param [Class<Handler>] handler_class This should be a subclass
79
+ # of {Handler}.
80
+ #
81
+ # @option options [Symbol] :step (:build) The request life-cycle
82
+ # step the handler should run in. Defaults to `:build`. The
83
+ # list of possible steps, in high-to-low priority order are:
84
+ #
85
+ # * `:initialize`
86
+ # * `:validate`
87
+ # * `:build`
88
+ # * `:retry`
89
+ # * `:parse`
90
+ # * `:sign`
91
+ # * `:send`
92
+ #
93
+ # @option options [Integer] :priority (50) The priority of this
94
+ # handler within a step. The priority must be between 0 and 99
95
+ # inclusively. It defaults to 50. When two handlers have the
96
+ # same `:step` and `:priority`, the handler registered last has
97
+ # the highest priority.
98
+ #
99
+ # @option options [Array<Symbol>] :operations A list of
100
+ # operations names the handler should be applied to. When
101
+ # `:operations` is omitted, the handler is applied to all
102
+ # operations for the client.
103
+ #
104
+ # @raise ArgumentError if the priority is not between 0 and 99 or
105
+ # if the step is not a valid step.
106
+ # @return [Class<Handler>] Returns the handler class that was added.
107
+ #
108
+ def add(handler_class, options = {})
109
+ @mutex.synchronize do
110
+ add_entry(
111
+ HandlerListEntry.new(
112
+ options.merge(
113
+ handler_class: handler_class,
114
+ inserted: next_index
115
+ )
116
+ )
117
+ )
118
+ end
119
+ handler_class
120
+ end
121
+
122
+ # @param [Class<Handler>] handler_class
123
+ def remove(handler_class)
124
+ @mutex.synchronize do
125
+ @entries.each do |key, entry|
126
+ remove_entry(key, entry, handler_class)
127
+ end
128
+ end
129
+ end
130
+
131
+ # Copies handlers from the `source_list` onto the current handler list.
132
+ # If a block is given, only the entries that return a `true` value
133
+ # from the block will be copied.
134
+ # @param [HandlerList] source_list
135
+ # @return [void]
136
+ def copy_from(source_list)
137
+ entries = []
138
+ source_list.entries.each do |entry|
139
+ if block_given?
140
+ entries << entry.copy(inserted: next_index) if yield(entry)
141
+ else
142
+ entries << entry.copy(inserted: next_index)
143
+ end
144
+ end
145
+ add_entries(entries)
146
+ end
147
+
148
+ # Returns a handler list for the given operation. The returned
149
+ # will have the operation specific handlers merged with the common
150
+ # handlers.
151
+ # @param [Symbol] operation The name of an operation.
152
+ # @return [HandlerList]
153
+ def for(operation)
154
+ HandlerList.new(index: @index, entries: filter(operation))
155
+ end
156
+
157
+ # Constructs the handlers recursively, building a handler stack.
158
+ # The `:send` handler will be at the top of the stack and the
159
+ # `:validate` handlers will be at the bottom.
160
+ # @return [Handler, nil]
161
+ def to_stack
162
+ inject(nil) { |stack, handler| handler.new(stack) }
163
+ end
164
+
165
+ private
166
+
167
+ def add_entries(entries)
168
+ @mutex.synchronize do
169
+ entries.each { |entry| add_entry(entry) }
170
+ end
171
+ end
172
+
173
+ def add_entry(entry)
174
+ key = entry.step == :send ? :send : entry.object_id
175
+ @entries[key] = entry
176
+ end
177
+
178
+ def remove_entry(key, entry, handler_class)
179
+ @entries.delete(key) if entry.handler_class == handler_class
180
+ end
181
+
182
+ def filter(operation)
183
+ entries.each_with_object([]) do |entry, filtered|
184
+ if entry.operations.nil?
185
+ filtered << entry.copy
186
+ elsif entry.operations.include?(operation)
187
+ filtered << entry.copy(operations: nil)
188
+ end
189
+ end
190
+ end
191
+
192
+ def next_index
193
+ @index += 1
194
+ end
195
+ end
196
+ end
197
+ end