hearth 1.0.0.pre1 → 1.0.0.pre2

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 (157) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -4
  3. data/VERSION +1 -1
  4. data/lib/hearth/api_error.rb +15 -1
  5. data/lib/hearth/auth_option.rb +21 -0
  6. data/lib/hearth/auth_schemes/anonymous.rb +21 -0
  7. data/lib/hearth/auth_schemes/http_api_key.rb +16 -0
  8. data/lib/hearth/auth_schemes/http_basic.rb +16 -0
  9. data/lib/hearth/auth_schemes/http_bearer.rb +16 -0
  10. data/lib/hearth/auth_schemes/http_digest.rb +16 -0
  11. data/lib/hearth/auth_schemes.rb +32 -0
  12. data/lib/hearth/checksums.rb +31 -0
  13. data/lib/hearth/client_stubs.rb +130 -0
  14. data/lib/hearth/config/env_provider.rb +53 -0
  15. data/lib/hearth/config/resolver.rb +52 -0
  16. data/lib/hearth/configuration.rb +15 -0
  17. data/lib/hearth/connection_pool.rb +77 -0
  18. data/lib/hearth/context.rb +28 -4
  19. data/lib/hearth/dns/host_address.rb +23 -0
  20. data/lib/hearth/dns/host_resolver.rb +92 -0
  21. data/lib/hearth/dns.rb +48 -0
  22. data/lib/hearth/http/api_error.rb +4 -8
  23. data/lib/hearth/http/client.rb +208 -59
  24. data/lib/hearth/http/error_inspector.rb +85 -0
  25. data/lib/hearth/http/error_parser.rb +18 -20
  26. data/lib/hearth/http/field.rb +64 -0
  27. data/lib/hearth/http/fields.rb +117 -0
  28. data/lib/hearth/http/middleware/content_length.rb +5 -2
  29. data/lib/hearth/http/middleware/content_md5.rb +31 -0
  30. data/lib/hearth/http/middleware/request_compression.rb +157 -0
  31. data/lib/hearth/http/middleware.rb +12 -0
  32. data/lib/hearth/http/networking_error.rb +1 -14
  33. data/lib/hearth/http/request.rb +83 -56
  34. data/lib/hearth/http/response.rb +42 -13
  35. data/lib/hearth/http.rb +14 -5
  36. data/lib/hearth/identities/anonymous.rb +8 -0
  37. data/lib/hearth/identities/http_api_key.rb +16 -0
  38. data/lib/hearth/identities/http_bearer.rb +16 -0
  39. data/lib/hearth/identities/http_login.rb +20 -0
  40. data/lib/hearth/identities.rb +21 -0
  41. data/lib/hearth/identity_resolver.rb +17 -0
  42. data/lib/hearth/interceptor.rb +506 -0
  43. data/lib/hearth/interceptor_context.rb +36 -0
  44. data/lib/hearth/interceptor_list.rb +48 -0
  45. data/lib/hearth/interceptors.rb +75 -0
  46. data/lib/hearth/middleware/auth.rb +100 -0
  47. data/lib/hearth/middleware/build.rb +32 -0
  48. data/lib/hearth/middleware/host_prefix.rb +10 -6
  49. data/lib/hearth/middleware/initialize.rb +58 -0
  50. data/lib/hearth/middleware/parse.rb +45 -6
  51. data/lib/hearth/middleware/retry.rb +97 -23
  52. data/lib/hearth/middleware/send.rb +137 -25
  53. data/lib/hearth/middleware/sign.rb +65 -0
  54. data/lib/hearth/middleware/validate.rb +11 -1
  55. data/lib/hearth/middleware.rb +19 -8
  56. data/lib/hearth/middleware_stack.rb +1 -43
  57. data/lib/hearth/networking_error.rb +18 -0
  58. data/lib/hearth/number_helper.rb +2 -2
  59. data/lib/hearth/output.rb +8 -4
  60. data/lib/hearth/plugin_list.rb +53 -0
  61. data/lib/hearth/query/param.rb +52 -0
  62. data/lib/hearth/query/param_list.rb +54 -0
  63. data/lib/hearth/query/param_matcher.rb +32 -0
  64. data/lib/hearth/refreshing_identity_resolver.rb +63 -0
  65. data/lib/hearth/request.rb +22 -0
  66. data/lib/hearth/response.rb +33 -0
  67. data/lib/hearth/retry/adaptive.rb +60 -0
  68. data/lib/hearth/retry/capacity_not_available_error.rb +9 -0
  69. data/lib/hearth/retry/client_rate_limiter.rb +143 -0
  70. data/lib/hearth/retry/exponential_backoff.rb +15 -0
  71. data/lib/hearth/retry/retry_quota.rb +56 -0
  72. data/lib/hearth/retry/standard.rb +46 -0
  73. data/lib/hearth/retry/strategy.rb +20 -0
  74. data/lib/hearth/retry.rb +16 -0
  75. data/lib/hearth/signers/anonymous.rb +16 -0
  76. data/lib/hearth/signers/http_api_key.rb +29 -0
  77. data/lib/hearth/signers/http_basic.rb +23 -0
  78. data/lib/hearth/signers/http_bearer.rb +19 -0
  79. data/lib/hearth/signers/http_digest.rb +19 -0
  80. data/lib/hearth/signers.rb +23 -0
  81. data/lib/hearth/stubs.rb +30 -0
  82. data/lib/hearth/time_helper.rb +5 -3
  83. data/lib/hearth/validator.rb +44 -5
  84. data/lib/hearth/waiters/poller.rb +6 -7
  85. data/lib/hearth/waiters/waiter.rb +17 -4
  86. data/lib/hearth/xml/formatter.rb +11 -2
  87. data/lib/hearth/xml/node.rb +2 -2
  88. data/lib/hearth.rb +32 -5
  89. data/sig/lib/hearth/aliases.rbs +4 -0
  90. data/sig/lib/hearth/api_error.rbs +13 -0
  91. data/sig/lib/hearth/auth_option.rbs +11 -0
  92. data/sig/lib/hearth/auth_schemes/anonymous.rbs +7 -0
  93. data/sig/lib/hearth/auth_schemes/http_api_key.rbs +7 -0
  94. data/sig/lib/hearth/auth_schemes/http_basic.rbs +7 -0
  95. data/sig/lib/hearth/auth_schemes/http_bearer.rbs +7 -0
  96. data/sig/lib/hearth/auth_schemes/http_digest.rbs +7 -0
  97. data/sig/lib/hearth/auth_schemes.rbs +13 -0
  98. data/sig/lib/hearth/block_io.rbs +9 -0
  99. data/sig/lib/hearth/client_stubs.rbs +5 -0
  100. data/sig/lib/hearth/configuration.rbs +7 -0
  101. data/sig/lib/hearth/dns/host_address.rbs +13 -0
  102. data/sig/lib/hearth/dns/host_resolver.rbs +19 -0
  103. data/sig/lib/hearth/http/api_error.rbs +13 -0
  104. data/sig/lib/hearth/http/client.rbs +9 -0
  105. data/sig/lib/hearth/http/field.rbs +19 -0
  106. data/sig/lib/hearth/http/fields.rbs +43 -0
  107. data/sig/lib/hearth/http/request.rbs +25 -0
  108. data/sig/lib/hearth/http/response.rbs +21 -0
  109. data/sig/lib/hearth/identities/anonymous.rbs +6 -0
  110. data/sig/lib/hearth/identities/http_api_key.rbs +9 -0
  111. data/sig/lib/hearth/identities/http_bearer.rbs +9 -0
  112. data/sig/lib/hearth/identities/http_login.rbs +11 -0
  113. data/sig/lib/hearth/identities.rbs +9 -0
  114. data/sig/lib/hearth/identity_resolver.rbs +7 -0
  115. data/sig/lib/hearth/interceptor.rbs +9 -0
  116. data/sig/lib/hearth/interceptor_context.rbs +15 -0
  117. data/sig/lib/hearth/interceptor_list.rbs +16 -0
  118. data/sig/lib/hearth/interfaces.rbs +65 -0
  119. data/sig/lib/hearth/output.rbs +11 -0
  120. data/sig/lib/hearth/plugin_list.rbs +15 -0
  121. data/sig/lib/hearth/query/param.rbs +17 -0
  122. data/sig/lib/hearth/query/param_list.rbs +25 -0
  123. data/sig/lib/hearth/request.rbs +9 -0
  124. data/sig/lib/hearth/response.rbs +11 -0
  125. data/sig/lib/hearth/retry/adaptive.rbs +13 -0
  126. data/sig/lib/hearth/retry/exponential_backoff.rbs +7 -0
  127. data/sig/lib/hearth/retry/standard.rbs +13 -0
  128. data/sig/lib/hearth/retry/strategy.rbs +11 -0
  129. data/sig/lib/hearth/retry.rbs +9 -0
  130. data/sig/lib/hearth/signers/anonymous.rbs +9 -0
  131. data/sig/lib/hearth/signers/http_api_key.rbs +9 -0
  132. data/sig/lib/hearth/signers/http_basic.rbs +9 -0
  133. data/sig/lib/hearth/signers/http_bearer.rbs +9 -0
  134. data/sig/lib/hearth/signers/http_digest.rbs +9 -0
  135. data/sig/lib/hearth/signers.rbs +9 -0
  136. data/sig/lib/hearth/structure.rbs +7 -0
  137. data/sig/lib/hearth/union.rbs +5 -0
  138. data/sig/lib/hearth/waiters/waiter.rbs +17 -0
  139. metadata +132 -22
  140. data/lib/hearth/http/headers.rb +0 -70
  141. data/lib/hearth/middleware/around_handler.rb +0 -24
  142. data/lib/hearth/middleware/request_handler.rb +0 -24
  143. data/lib/hearth/middleware/response_handler.rb +0 -25
  144. data/lib/hearth/middleware_builder.rb +0 -246
  145. data/lib/hearth/stubbing/client_stubs.rb +0 -115
  146. data/lib/hearth/stubbing/stubs.rb +0 -32
  147. data/lib/hearth/waiters/errors.rb +0 -15
  148. data/sig/lib/seahorse/api_error.rbs +0 -10
  149. data/sig/lib/seahorse/document.rbs +0 -2
  150. data/sig/lib/seahorse/http/api_error.rbs +0 -21
  151. data/sig/lib/seahorse/http/headers.rbs +0 -47
  152. data/sig/lib/seahorse/http/response.rbs +0 -21
  153. data/sig/lib/seahorse/simple_delegator.rbs +0 -3
  154. data/sig/lib/seahorse/structure.rbs +0 -18
  155. data/sig/lib/seahorse/stubbing/client_stubs.rbs +0 -103
  156. data/sig/lib/seahorse/stubbing/stubs.rbs +0 -14
  157. data/sig/lib/seahorse/union.rbs +0 -6
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Retry
5
+ # @api private
6
+ # Used only in 'adaptive' retry mode
7
+ class ClientRateLimiter
8
+ MIN_CAPACITY = 1
9
+ MIN_FILL_RATE = 0.5
10
+ SMOOTH = 0.8
11
+ # How much to scale back after a throttling response
12
+ BETA = 0.7
13
+ # Controls how aggressively we scale up after being throttled
14
+ SCALE_CONSTANT = 0.4
15
+
16
+ def initialize
17
+ @mutex = Mutex.new
18
+ @fill_rate = nil
19
+ @max_capacity = nil
20
+ @current_capacity = 0
21
+ @last_timestamp = nil
22
+ @enabled = false
23
+ @measured_tx_rate = 0
24
+ @last_tx_rate_bucket = monotonic_seconds
25
+ @request_count = 0
26
+ @last_max_rate = 0
27
+ @last_throttle_time = monotonic_seconds
28
+ @calculated_rate = nil
29
+ end
30
+
31
+ def token_bucket_acquire(amount, wait_to_fill: true)
32
+ # Client side throttling is not enabled until we see a
33
+ # throttling error
34
+ return unless @enabled
35
+
36
+ @mutex.synchronize do
37
+ token_bucket_refill
38
+
39
+ # Next see if we have enough capacity for the requested amount
40
+ while @current_capacity < amount
41
+ raise CapacityNotAvailableError unless wait_to_fill
42
+
43
+ @mutex.sleep((amount - @current_capacity) / @fill_rate)
44
+ token_bucket_refill
45
+ end
46
+ @current_capacity -= amount
47
+ end
48
+ end
49
+
50
+ def update_sending_rate(is_throttling_error)
51
+ @mutex.synchronize do
52
+ update_measured_rate
53
+
54
+ if is_throttling_error
55
+ rate_to_use = if @enabled
56
+ [@measured_tx_rate, @fill_rate].min
57
+ else
58
+ @measured_tx_rate
59
+ end
60
+
61
+ # The fill_rate is from the token bucket
62
+ @last_max_rate = rate_to_use
63
+ calculate_time_window
64
+ @last_throttle_time = monotonic_seconds
65
+ @calculated_rate = cubic_throttle(rate_to_use)
66
+ enable_token_bucket
67
+ else
68
+ calculate_time_window
69
+ @calculated_rate = cubic_success(monotonic_seconds)
70
+ end
71
+
72
+ new_rate = [@calculated_rate, 2 * @measured_tx_rate].min
73
+ token_bucket_update_rate(new_rate)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def monotonic_seconds
80
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
81
+ end
82
+
83
+ def token_bucket_refill
84
+ timestamp = monotonic_seconds
85
+ unless @last_timestamp
86
+ @last_timestamp = timestamp
87
+ return
88
+ end
89
+
90
+ fill_amount = (timestamp - @last_timestamp) * @fill_rate
91
+ @current_capacity = [
92
+ @max_capacity, @current_capacity + fill_amount
93
+ ].min
94
+
95
+ @last_timestamp = timestamp
96
+ end
97
+
98
+ def token_bucket_update_rate(new_rps)
99
+ # Refill based on our current rate before we update to the
100
+ # new fill rate
101
+ token_bucket_refill
102
+ @fill_rate = [new_rps, MIN_FILL_RATE].max
103
+ @max_capacity = [new_rps, MIN_CAPACITY].max
104
+ # When we scale down we can't have a current capacity that exceeds our
105
+ # max_capacity.
106
+ @current_capacity = [@current_capacity, @max_capacity].min
107
+ end
108
+
109
+ def enable_token_bucket
110
+ @enabled = true
111
+ end
112
+
113
+ def update_measured_rate
114
+ t = monotonic_seconds
115
+ time_bucket = (t * 2).floor / 2.0
116
+ @request_count += 1
117
+ return unless time_bucket > @last_tx_rate_bucket
118
+
119
+ current_rate = @request_count / (time_bucket - @last_tx_rate_bucket)
120
+ @measured_tx_rate = (current_rate * SMOOTH) +
121
+ (@measured_tx_rate * (1 - SMOOTH))
122
+ @request_count = 0
123
+ @last_tx_rate_bucket = time_bucket
124
+ end
125
+
126
+ def calculate_time_window
127
+ # This is broken out into a separate calculation because it only
128
+ # gets updated when @last_max_rate changes so it can be cached.
129
+ base = ((@last_max_rate * (1 - BETA)) / SCALE_CONSTANT)
130
+ @time_window = base**(1.0 / 3)
131
+ end
132
+
133
+ def cubic_success(timestamp)
134
+ dt = timestamp - @last_throttle_time
135
+ (SCALE_CONSTANT * ((dt - @time_window)**3)) + @last_max_rate
136
+ end
137
+
138
+ def cubic_throttle(rate_to_use)
139
+ rate_to_use * BETA
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Retry
5
+ # Computes an exponential backoff delay for a retry attempt.
6
+ class ExponentialBackoff
7
+ # Max backoff (in seconds)
8
+ MAX_BACKOFF = 20
9
+
10
+ def call(attempts)
11
+ [Kernel.rand * (2**attempts), MAX_BACKOFF].min
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Retry
5
+ # @api private
6
+ # Used in 'standard' and 'adaptive' retry modes.
7
+ class RetryQuota
8
+ INITIAL_RETRY_TOKENS = 500
9
+ RETRY_COST = 5
10
+ NO_RETRY_INCREMENT = 1
11
+ TIMEOUT_RETRY_COST = 10
12
+
13
+ def initialize
14
+ @mutex = Mutex.new
15
+ @max_capacity = INITIAL_RETRY_TOKENS
16
+ @available_capacity = @max_capacity
17
+ end
18
+
19
+ # Check if there is sufficient capacity to retry and return it.
20
+ # If there is insufficient capacity, return 0
21
+ # @return [Integer] The amount of capacity checked out
22
+ def checkout_capacity(error_info)
23
+ @mutex.synchronize do
24
+ capacity_amount =
25
+ if error_info.error_type == 'Transient'
26
+ TIMEOUT_RETRY_COST
27
+ else
28
+ RETRY_COST
29
+ end
30
+
31
+ # unable to acquire capacity
32
+ return 0 if capacity_amount > @available_capacity
33
+
34
+ @available_capacity -= capacity_amount
35
+ capacity_amount
36
+ end
37
+ end
38
+
39
+ # capacity_amount refers to the amount of capacity requested from
40
+ # the last retry. It can either be RETRY_COST, TIMEOUT_RETRY_COST,
41
+ # or unset.
42
+ def release(capacity_amount)
43
+ # Implementation note: The release() method is called for
44
+ # every API call. In the common case where the request is
45
+ # successful and we're at full capacity, we can avoid locking.
46
+ # We can't exceed max capacity so there's no work we have to do.
47
+ return if @available_capacity == @max_capacity
48
+
49
+ @mutex.synchronize do
50
+ @available_capacity += capacity_amount || NO_RETRY_INCREMENT
51
+ @available_capacity = [@available_capacity, @max_capacity].min
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Retry
5
+ # Standard retry strategy for retrying requests.
6
+ class Standard < Strategy
7
+ # @param [#call] backoff (ExponentialBackoff) A callable object that
8
+ # calculates a backoff delay for a retry attempt.
9
+ # @param [Integer] max_attempts (3) The maximum number of attempts that
10
+ # will be made for a single request, including the initial attempt.
11
+ def initialize(backoff: ExponentialBackoff.new, max_attempts: 3)
12
+ super()
13
+ @backoff = backoff
14
+ @max_attempts = max_attempts
15
+
16
+ # instance state
17
+ @retry_quota = RetryQuota.new
18
+ @capacity_amount = nil
19
+ end
20
+
21
+ def acquire_initial_retry_token(_token_scope = nil)
22
+ Token.new(retry_count: 0)
23
+ end
24
+
25
+ def refresh_retry_token(retry_token, error_info)
26
+ return unless error_info.retryable?
27
+
28
+ return if retry_token.retry_count >= @max_attempts - 1
29
+
30
+ @capacity_amount = @retry_quota.checkout_capacity(error_info)
31
+ return unless @capacity_amount.positive?
32
+
33
+ delay = error_info.hints[:retry_after]
34
+ delay ||= @backoff.call(retry_token.retry_count)
35
+ retry_token.retry_count += 1
36
+ retry_token.retry_delay = delay
37
+ retry_token
38
+ end
39
+
40
+ def record_success(retry_token)
41
+ @retry_quota.release(@capacity_amount)
42
+ retry_token
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Retry
5
+ # Interface for retry strategies.
6
+ class Strategy
7
+ def acquire_initial_retry_token(_token_scope = nil)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def refresh_retry_token(_retry_token, _error_info)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def record_success(_retry_token)
16
+ raise NotImplementedError
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'retry/strategy'
4
+
5
+ require_relative 'retry/adaptive'
6
+ require_relative 'retry/capacity_not_available_error'
7
+ require_relative 'retry/client_rate_limiter'
8
+ require_relative 'retry/exponential_backoff'
9
+ require_relative 'retry/retry_quota'
10
+ require_relative 'retry/standard'
11
+
12
+ module Hearth
13
+ module Retry
14
+ Token = Struct.new(:retry_count, :retry_delay, keyword_init: true)
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Signers
5
+ # A signer that does not sign requests.
6
+ class Anonymous < Signers::Base
7
+ def sign(request:, identity:, properties:)
8
+ # Do nothing.
9
+ end
10
+
11
+ def reset(request:, properties:)
12
+ # Do nothing.
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Signers
5
+ # A signer that signs requests using the HTTP API Key Auth scheme.
6
+ class HTTPApiKey < Signers::Base
7
+ def sign(request:, identity:, properties:)
8
+ case properties[:in]
9
+ when 'header'
10
+ value = "#{properties[:scheme]} #{identity.key}".strip
11
+ request.headers[properties[:name]] = value
12
+ when 'query'
13
+ name = properties[:name]
14
+ request.append_query_param(name, identity.key)
15
+ end
16
+ end
17
+
18
+ def reset(request:, properties:)
19
+ case properties[:in]
20
+ when 'header'
21
+ request.headers.delete(properties[:name])
22
+ when 'query'
23
+ name = properties[:name]
24
+ request.remove_query_param(name)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Hearth
6
+ module Signers
7
+ # A signer that signs requests using the HTTP Basic Auth scheme.
8
+ class HTTPBasic
9
+ # rubocop:disable Lint/UnusedMethodArgument
10
+ def sign(request:, identity:, properties:)
11
+ # TODO: does not handle realm or other properties
12
+ identity_string = "#{identity.username}:#{identity.password}"
13
+ encoded = Base64.strict_encode64(identity_string)
14
+ request.headers['Authorization'] = "Basic #{encoded}"
15
+ end
16
+
17
+ def reset(request:, properties:)
18
+ request.headers.delete('Authorization')
19
+ end
20
+ # rubocop:enable Lint/UnusedMethodArgument
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Signers
5
+ # A signer that signs requests using the HTTP Bearer Auth scheme.
6
+ class HTTPBearer < Signers::Base
7
+ # rubocop:disable Lint/UnusedMethodArgument
8
+ def sign(request:, identity:, properties:)
9
+ # TODO: does not handle realm or other properties
10
+ request.headers['Authorization'] = "Bearer #{identity.token}"
11
+ end
12
+
13
+ def reset(request:, properties:)
14
+ request.headers.delete('Authorization')
15
+ end
16
+ # rubocop:enable Lint/UnusedMethodArgument
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Signers
5
+ # A signer that signs requests using the HTTP Digest Auth scheme.
6
+ class HTTPDigest < Signers::Base
7
+ def sign(request:, identity:, properties:)
8
+ # TODO: requires a nonce from the server - this cannot
9
+ # be implemented unless we rescue from a 401 and retry
10
+ # with the nonce
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def reset(request:, properties:)
15
+ raise NotImplementedError
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # Namespace for all Signer classes.
5
+ module Signers
6
+ # Base class for all Signer classes.
7
+ class Base
8
+ def sign(request:, identity:, properties:)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def reset(request:, properties:)
13
+ raise NotImplementedError
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ require_relative 'signers/anonymous'
20
+ require_relative 'signers/http_api_key'
21
+ require_relative 'signers/http_basic'
22
+ require_relative 'signers/http_bearer'
23
+ require_relative 'signers/http_digest'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # Provides a thread safe data structure for adding and getting stubs
5
+ # per operation.
6
+ # @api private
7
+ class Stubs
8
+ def initialize
9
+ @stubs = {}
10
+ @stub_mutex = Mutex.new
11
+ end
12
+
13
+ def add_stubs(operation_name, stubs)
14
+ @stub_mutex.synchronize do
15
+ @stubs[operation_name.to_sym] = stubs
16
+ end
17
+ end
18
+
19
+ def next(operation_name)
20
+ @stub_mutex.synchronize do
21
+ stubs = @stubs[operation_name] || []
22
+ case stubs.length
23
+ when 0 then nil
24
+ when 1 then stubs.first
25
+ else stubs.shift
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -11,11 +11,12 @@ module Hearth
11
11
  # @param [Time] time
12
12
  # @return [String<Date Time>] The time as an ISO8601 string.
13
13
  def to_date_time(time)
14
- time.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
14
+ optional_ms_digits = time.subsec.zero? ? nil : 3
15
+ time.utc.iso8601(optional_ms_digits)
15
16
  end
16
17
 
17
18
  # @param [Time] time
18
- # @return [Float<Epoch Seconds>] Returns float value of
19
+ # @return [Float<Epoch Seconds>] Returns the float value of
19
20
  # epoch seconds with millisecond precision.
20
21
  def to_epoch_seconds(time)
21
22
  time = time.utc
@@ -28,7 +29,8 @@ module Hearth
28
29
  # @return [String<Http Date>] Returns the time formatted
29
30
  # as an HTTP header date.
30
31
  def to_http_date(time)
31
- time.utc.httpdate
32
+ fractional = '.%L' unless time.subsec.zero?
33
+ time.utc.strftime("%a, %d %b %Y %H:%M:%S#{fractional} GMT")
32
34
  end
33
35
  end
34
36
  end
@@ -3,18 +3,57 @@
3
3
  module Hearth
4
4
  # Utility module for working with request parameters.
5
5
  #
6
- # * Validate structure of parameters against the expected type.
6
+ # * Validate structure of parameters against the expected input.
7
7
  # * Raise errors with context when validation fails.
8
8
  # @api private
9
9
  module Validator
10
- # Validate the given values is of the given type(s).
10
+ # Validate the given value is of the given type(s).
11
+ # @param value [Object] The value to validate.
12
+ # @param types [Array<Class>] The types to validate against.
13
+ # @param context [String] The context of the value being validated.
11
14
  # @raise [ArgumentError] Raises when the value is not one of given type(s).
12
- def self.validate!(value, *types, context:)
13
- return if !value || types.any? { |type| value.is_a?(type) }
15
+ def self.validate_types!(value, *types, context:)
16
+ return if value.nil? || types.any? { |type| value.is_a?(type) }
14
17
 
15
18
  raise ArgumentError,
16
- "Expected #{context} to be in "\
19
+ "Expected #{context} to be in " \
17
20
  "[#{types.map(&:to_s).join(', ')}], got #{value.class}."
18
21
  end
22
+
23
+ # Validate a value is present and not nil.
24
+ # @param value [Object] The value to validate.
25
+ # @param context [String] The context of the value being validated.
26
+ # @raise [ArgumentError] Raises when the value is nil.
27
+ def self.validate_required!(value, context:)
28
+ raise ArgumentError, "Expected #{context} to be set." if value.nil?
29
+ end
30
+
31
+ # Validate unknown parameters are not present for a given Struct.
32
+ # @param struct [Struct] The Struct to validate against.
33
+ # @param params [Hash] The parameters to validate.
34
+ # @param context [String] The context of the value being validated.
35
+ # @raise [ArgumentError] Raises when unknown parameters are present.
36
+ def self.validate_unknown!(struct, params, context:)
37
+ unknown = params.keys - struct.members
38
+ return if unknown.empty?
39
+
40
+ unknown = unknown.map { |key| "#{context}[:#{key}]" }
41
+ raise ArgumentError,
42
+ "Unexpected members: [#{unknown.join(', ')}]"
43
+ end
44
+
45
+ # Validate the given value is within the expected range (inclusive).
46
+ # @param value [Object] The value to validate.
47
+ # @param min [Numeric] The minimum that the given value should be.
48
+ # @param max [Numeric] The maximum that the given value should be.
49
+ # @param context [String] The context of the value being validated.
50
+ # @raise [ArgumentError] Raises when the value is not within expected range.
51
+ def self.validate_range!(value, min:, max:, context:)
52
+ return if value.nil? || value.between?(min, max)
53
+
54
+ raise ArgumentError,
55
+ "Expected #{context} to be between " \
56
+ "#{min} to #{max}, got #{value}."
57
+ end
19
58
  end
20
59
  end
@@ -6,8 +6,8 @@ module Hearth
6
6
  module Waiters
7
7
  # Abstract Poller used by generated service Waiters. This class handles
8
8
  # sending the request and matching input or output.
9
+ # @api private
9
10
  class Poller
10
- # @api private
11
11
  def initialize(options = {})
12
12
  @operation_name = options[:operation_name]
13
13
  @acceptors = options[:acceptors]
@@ -42,7 +42,7 @@ module Hearth
42
42
  # @return [Array<Symbol,Response>]
43
43
  def call(client, params = {}, options = {})
44
44
  begin
45
- options = options.merge(input_output_middleware)
45
+ options = options.merge(input_output_interceptor)
46
46
  response = client.send(@operation_name, params, options)
47
47
  rescue Hearth::ApiError => e
48
48
  error = e
@@ -58,11 +58,10 @@ module Hearth
58
58
 
59
59
  private
60
60
 
61
- def input_output_middleware
62
- middleware = lambda do |input, _context|
63
- @input = input # get internal details of middleware
64
- end
65
- { middleware: MiddlewareBuilder.before_send(middleware) }
61
+ def input_output_interceptor
62
+ # get internal details - capture input before send
63
+ interceptor = proc { |context| @input = context.input }
64
+ { interceptors: [Interceptor.new(read_before_transmit: interceptor)] }
66
65
  end
67
66
 
68
67
  def acceptor_matches?(matcher, response, error)
@@ -2,9 +2,22 @@
2
2
 
3
3
  module Hearth
4
4
  module Waiters
5
+ # A super class error that is raised when a waiter detects a condition
6
+ # where the waiter can never succeed.
7
+ class WaiterFailed < StandardError; end
8
+
9
+ # Raised when the waiter reached an expected failure state.
10
+ class FailureStateError < WaiterFailed; end
11
+
12
+ # Raised when the waiter has reached the maximum waiting time.
13
+ class MaxWaitTimeExceededError < WaiterFailed; end
14
+
15
+ # Raised when the waiter received an unexpected error.
16
+ class UnexpectedError < WaiterFailed; end
17
+
5
18
  # Abstract waiter class with high level logic for polling and waiting.
19
+ # @api private
6
20
  class Waiter
7
- # @api private
8
21
  def initialize(options = {})
9
22
  unless options[:max_wait_time].is_a?(Integer)
10
23
  raise ArgumentError,
@@ -42,11 +55,11 @@ module Hearth
42
55
  case state
43
56
  when :retry then nil
44
57
  when :success then return
45
- when :failure then raise Errors::FailureStateError, resp_or_error
46
- when :error then raise Errors::UnexpectedError, resp_or_error
58
+ when :failure then raise FailureStateError, resp_or_error
59
+ when :error then raise UnexpectedError, resp_or_error
47
60
  end
48
61
 
49
- raise Errors::MaxWaitTimeExceeded if @one_more_retry
62
+ raise MaxWaitTimeExceededError if @one_more_retry
50
63
 
51
64
  delay = delay(n)
52
65
  @remaining_time -= delay
@@ -45,7 +45,7 @@ module Hearth
45
45
  end
46
46
 
47
47
  def text_node(node, pad)
48
- text = node.text.encode(xml: :text)
48
+ text = escape(node.text, :text)
49
49
  "#{pad}<#{node.name}#{attrs(node)}>#{text}</#{node.name}>#{@eol}"
50
50
  end
51
51
 
@@ -60,9 +60,18 @@ module Hearth
60
60
 
61
61
  def attrs(node)
62
62
  node.attributes.map do |key, value|
63
- " #{key}=#{value.to_s.encode(xml: :attr)}"
63
+ " #{key}=#{escape(value, :attr)}"
64
64
  end.join
65
65
  end
66
+
67
+ def escape(string, text_or_attr)
68
+ string.to_s
69
+ .encode(xml: text_or_attr)
70
+ .gsub("\u{000D}", '&#xD;') # Carriage Return
71
+ .gsub("\u{000A}", '&#xA;') # Line Feed
72
+ .gsub("\u{0085}", '&#x85;') # Next Line
73
+ .gsub("\u{2028}", '&#x2028;') # Line Separator
74
+ end
66
75
  end
67
76
  end
68
77
  end
@@ -9,9 +9,9 @@ module Hearth
9
9
  BOTH_TYPES = 'Nodes may not have both text and child nodes'
10
10
 
11
11
  # @param [String] name
12
- def initialize(name, *children)
12
+ def initialize(name, *children, attributes: {})
13
13
  @name = name
14
- @attributes = {}
14
+ @attributes = attributes
15
15
  @child_nodes = []
16
16
  @child_node_map = {}
17
17
  @text = []