hearth 1.0.0.pre2 → 1.0.0.pre3

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/VERSION +1 -1
  4. data/lib/hearth/anonymous_auth_resolver.rb +11 -0
  5. data/lib/hearth/auth_schemes/anonymous.rb +3 -3
  6. data/lib/hearth/auth_schemes.rb +3 -3
  7. data/lib/hearth/client.rb +66 -0
  8. data/lib/hearth/client_stubs.rb +1 -3
  9. data/lib/hearth/config/resolver.rb +6 -5
  10. data/lib/hearth/context.rb +1 -0
  11. data/lib/hearth/dns/host_address.rb +20 -16
  12. data/lib/hearth/endpoint_rules.rb +154 -0
  13. data/lib/hearth/http/client.rb +5 -7
  14. data/lib/hearth/http/error_inspector.rb +2 -2
  15. data/lib/hearth/http/field.rb +4 -19
  16. data/lib/hearth/http/header_list_builder.rb +42 -0
  17. data/lib/hearth/http/header_list_parser.rb +92 -0
  18. data/lib/hearth/http/middleware/content_length.rb +3 -3
  19. data/lib/hearth/http/middleware/content_md5.rb +0 -1
  20. data/lib/hearth/http/middleware/request_compression.rb +7 -10
  21. data/lib/hearth/http.rb +2 -0
  22. data/lib/hearth/{identity_resolver.rb → identity_provider.rb} +1 -1
  23. data/lib/hearth/interceptor_context.rb +8 -4
  24. data/lib/hearth/interceptors.rb +2 -1
  25. data/lib/hearth/json.rb +4 -4
  26. data/lib/hearth/middleware/auth.rb +9 -6
  27. data/lib/hearth/middleware/build.rb +0 -1
  28. data/lib/hearth/middleware/endpoint.rb +79 -0
  29. data/lib/hearth/middleware/host_prefix.rb +1 -2
  30. data/lib/hearth/middleware/initialize.rb +0 -1
  31. data/lib/hearth/middleware/parse.rb +0 -1
  32. data/lib/hearth/middleware/retry.rb +9 -2
  33. data/lib/hearth/middleware/send.rb +0 -1
  34. data/lib/hearth/middleware.rb +1 -0
  35. data/lib/hearth/middleware_stack.rb +1 -1
  36. data/lib/hearth/number_helper.rb +1 -1
  37. data/lib/hearth/query/param.rb +7 -3
  38. data/lib/hearth/query/param_matcher.rb +5 -6
  39. data/lib/hearth/{refreshing_identity_resolver.rb → refreshing_identity_provider.rb} +2 -2
  40. data/lib/hearth/request.rb +2 -2
  41. data/lib/hearth/response.rb +5 -2
  42. data/lib/hearth/retry/adaptive.rb +2 -2
  43. data/lib/hearth/retry/client_rate_limiter.rb +8 -6
  44. data/lib/hearth/retry/exponential_backoff.rb +1 -1
  45. data/lib/hearth/retry/standard.rb +2 -2
  46. data/lib/hearth/retry.rb +16 -3
  47. data/lib/hearth/structure.rb +7 -3
  48. data/lib/hearth/stubs.rb +12 -4
  49. data/lib/hearth/time_helper.rb +1 -2
  50. data/lib/hearth/validator.rb +37 -21
  51. data/lib/hearth/waiters/poller.rb +4 -2
  52. data/lib/hearth/waiters/waiter.rb +6 -5
  53. data/lib/hearth/xml/node.rb +0 -1
  54. data/lib/hearth/xml/node_matcher.rb +0 -1
  55. data/lib/hearth.rb +8 -4
  56. data/sig/lib/hearth/aliases.rbs +5 -3
  57. data/sig/lib/hearth/anonymous_auth_resolver.rbs +5 -0
  58. data/sig/lib/hearth/auth_schemes.rbs +1 -1
  59. data/sig/lib/hearth/client.rbs +9 -0
  60. data/sig/lib/hearth/configuration.rbs +2 -2
  61. data/sig/lib/hearth/dns/host_address.rbs +1 -3
  62. data/sig/lib/hearth/dns/host_resolver.rbs +3 -3
  63. data/sig/lib/hearth/endpoint_rules.rbs +17 -0
  64. data/sig/lib/hearth/http/field.rbs +1 -1
  65. data/sig/lib/hearth/http/fields.rbs +1 -1
  66. data/sig/lib/hearth/http/header_list_builder.rbs +15 -0
  67. data/sig/lib/hearth/http/header_list_parser.rbs +19 -0
  68. data/sig/lib/hearth/http/networking_error.rbs +6 -0
  69. data/sig/lib/hearth/http/response.rbs +1 -1
  70. data/sig/lib/hearth/identities.rbs +1 -1
  71. data/sig/lib/hearth/{identity_resolver.rbs → identity_provider.rbs} +1 -1
  72. data/sig/lib/hearth/interceptor_context.rbs +4 -2
  73. data/sig/lib/hearth/interfaces.rbs +52 -30
  74. data/sig/lib/hearth/json/parse_error.rbs +9 -0
  75. data/sig/lib/hearth/networking_error.rbs +7 -0
  76. data/sig/lib/hearth/output.rbs +4 -4
  77. data/sig/lib/hearth/plugin_list.rbs +5 -7
  78. data/sig/lib/hearth/query/param.rbs +2 -2
  79. data/sig/lib/hearth/refreshing_identity_provider.rbs +10 -0
  80. data/sig/lib/hearth/request.rbs +2 -2
  81. data/sig/lib/hearth/response.rbs +2 -2
  82. data/sig/lib/hearth/retry/exponential_backoff.rbs +1 -1
  83. data/sig/lib/hearth/retry.rbs +1 -1
  84. data/sig/lib/hearth/structure.rbs +1 -2
  85. data/sig/lib/hearth/stubs.rbs +9 -0
  86. data/sig/lib/hearth/union.rbs +1 -1
  87. data/sig/lib/hearth/xml/parse_error.rbs +9 -0
  88. metadata +26 -10
  89. data/lib/hearth/retry/strategy.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bace852980aa4785929fce82aeb1a0b683fd6cf1f77d83d1dee39315d8177361
4
- data.tar.gz: bff4b301d6940ccb485973b728b761e0d773489068cb093bf1789fcfe8cd1c35
3
+ metadata.gz: 5a6aa5be0acf82ba6ba9b2d399c702fd0da402764dd4769cc3d72a632074dbb4
4
+ data.tar.gz: f7b14b1778860d004cb9fcea31c1fb0d230760c330447136414342f37cbd3a52
5
5
  SHA512:
6
- metadata.gz: 27e9e5a1c32430314c3df34e843efef873231e5691d4d7df262615e4a9b65fb1f29dbdf8c0b4037e2a86d13d7597e2ff29b45d8eea8db87b2ed1559740b4c53c
7
- data.tar.gz: ae6db7148e7cc6b98d8a2d0cc0bab09327ff10774c78e5b3fd76ef8a219ac07ebff636fd7636c7c17ed53f3fcbf964ce111a8e3d11d090120d59f95d2eafb227
6
+ metadata.gz: dd6f92a2e98ab0942ddd68a6edb86cd4e274389c4d1595ec2ff360a6c6a7a9887a5534a58271009eef0697018bf8327b5c144cc47a541e102d57a0f3fe198be2
7
+ data.tar.gz: 7ae01e2405d9d7301ea2b9f6b8805fcd438de16d498b9e394f7e0ba5452a3acc6e681b5a32e7371b3cbe9fced1984ded25e4b16bb3512d1bf282f19079490614
data/CHANGELOG.md CHANGED
@@ -1,6 +1,11 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
3
 
4
+ 1.0.0.pre3 (2024-05-01)
5
+ ------------------
6
+
7
+ * Feature - Third initial public pre-release for Smithy Ruby SDKs.
8
+
4
9
  1.0.0.pre2 (2023-12-19)
5
10
  ------------------
6
11
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0.pre2
1
+ 1.0.0.pre3
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # Always returns the Anonymous/noAuth auth scheme.
5
+ # Can be used to effectively disable/skip auth.
6
+ class AnonymousAuthResolver
7
+ def resolve(_params)
8
+ [Hearth::AuthOption.new(scheme_id: 'smithy.api#noAuth')]
9
+ end
10
+ end
11
+ end
@@ -12,9 +12,9 @@ module Hearth
12
12
  )
13
13
  end
14
14
 
15
- # @return [IdentityResolver, nil]
16
- def identity_resolver(_identity_resolvers = {})
17
- Hearth::IdentityResolver.new(proc { Identities::Anonymous.new })
15
+ # @return [IdentityProvider, nil]
16
+ def identity_provider(_identity_providers = {})
17
+ Hearth::IdentityProvider.new(proc { Identities::Anonymous.new })
18
18
  end
19
19
  end
20
20
  end
@@ -14,9 +14,9 @@ module Hearth
14
14
  # @return [String]
15
15
  attr_reader :scheme_id
16
16
 
17
- # @return [IdentityResolver, nil]
18
- def identity_resolver(identity_resolvers = {})
19
- identity_resolvers[@identity_type]
17
+ # @return [IdentityProvider, nil]
18
+ def identity_provider(identity_provider = {})
19
+ identity_provider[@identity_type]
20
20
  end
21
21
 
22
22
  # @return [Signers::Base]
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'client_stubs'
4
+
5
+ module Hearth
6
+ # Base Client class for all generated SDK clients.
7
+ class Client
8
+ include ClientStubs
9
+
10
+ # Plugins applied to all instances of this client.
11
+ # @return [Hearth::PluginList]
12
+ def self.plugins
13
+ @plugins ||= PluginList.new
14
+ end
15
+
16
+ # @param [Hash] options
17
+ # Options used to construct an instance of {Config}
18
+ # @param [Class] config_class
19
+ # The configuration class to use.
20
+ def initialize(options, config_class)
21
+ @config = initialize_config(options, config_class)
22
+ end
23
+
24
+ # @return [Configuration]
25
+ attr_reader :config
26
+
27
+ private
28
+
29
+ def initialize_config(options, config_class)
30
+ client_interceptors = options.delete(:interceptors) || []
31
+ config = config_class.new(**options)
32
+ config.validate!
33
+ self.class.plugins.each { |p| p.call(config) }
34
+ config.plugins.each { |p| p.call(config) }
35
+ config.interceptors.concat(client_interceptors)
36
+ config.validate!
37
+ config.freeze
38
+ end
39
+
40
+ def operation_config(options)
41
+ return @config if options.empty?
42
+
43
+ if options.include?(:stub_responses) || options.include?(:stubs)
44
+ msg = 'Overriding stubs or stub_responses on ' \
45
+ 'operations is not allowed'
46
+ raise ArgumentError, msg
47
+ end
48
+
49
+ operation_plugins = options.delete(:plugins)
50
+ operation_interceptors = options.delete(:interceptors) || []
51
+ config = @config.merge(options)
52
+ config.validate!
53
+ operation_plugins&.each { |p| p.call(config) }
54
+ config.interceptors.concat(operation_interceptors)
55
+ config.validate!
56
+ config.freeze
57
+ end
58
+
59
+ def output_stream(options = {}, &block)
60
+ return options.delete(:output_stream) if options[:output_stream]
61
+ return Hearth::BlockIO.new(block) if block
62
+
63
+ ::StringIO.new
64
+ end
65
+ end
66
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'stubs'
4
-
5
3
  module Hearth
6
4
  # This module provides the ability to specify the data and/or errors to
7
5
  # return when a client is using stubbed responses.
@@ -119,7 +117,7 @@ module Hearth
119
117
  # `:stub_responses => true`.
120
118
  def stub_responses(operation_name, *stubs)
121
119
  if @config.stub_responses
122
- @stubs.add_stubs(operation_name, stubs.flatten)
120
+ @config.stubs.set_stubs(operation_name, stubs.flatten)
123
121
  else
124
122
  msg = 'Stubbing is not enabled. Enable stubbing in Config ' \
125
123
  'with `stub_responses: true`'
@@ -38,11 +38,12 @@ module Hearth
38
38
 
39
39
  def resolve_default(key)
40
40
  @defaults[key]&.each do |default|
41
- value = if default.respond_to?(:call)
42
- default.call(self)
43
- else
44
- default
45
- end
41
+ value =
42
+ if default.respond_to?(:call)
43
+ default.call(self)
44
+ else
45
+ default
46
+ end
46
47
  return value unless value.nil?
47
48
  end
48
49
  nil
@@ -14,6 +14,7 @@ module Hearth
14
14
  @response = options[:response]
15
15
  @logger = options[:logger]
16
16
  @interceptors = options[:interceptors] || InterceptorList.new
17
+ @auth = options[:auth]
17
18
  @metadata = options[:metadata] || {}
18
19
  end
19
20
 
@@ -3,21 +3,25 @@
3
3
  module Hearth
4
4
  module DNS
5
5
  # Address results from a DNS lookup in {HostResolver}.
6
- class HostAddress
7
- def initialize(address_type:, address:, hostname:)
8
- @address_type = address_type
9
- @address = address
10
- @hostname = hostname
11
- end
12
-
13
- # @return [Symbol]
14
- attr_reader :address_type
15
-
16
- # @return [String]
17
- attr_reader :address
18
-
19
- # @return [String]
20
- attr_reader :hostname
21
- end
6
+ # @!method initialize(*args)
7
+ # @option args [Symbol] :address_type The type of address. For example,
8
+ # :A or :AAAA.
9
+ # @option args [String] :address The IP address.
10
+ # @option args [String] :hostname The hostname that was resolved.
11
+ # @!attribute address_type
12
+ # The type of address. For example, :A or :AAAA.
13
+ # @return [Symbol]
14
+ # @!attribute address
15
+ # The IP address.
16
+ # @return [String]
17
+ # @!attribute hostname
18
+ # The hostname that was resolved.
19
+ # @return [String]
20
+ HostAddress = Struct.new(
21
+ :address_type,
22
+ :address,
23
+ :hostname,
24
+ keyword_init: true
25
+ )
22
26
  end
23
27
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+ require 'ipaddr'
5
+ require 'uri'
6
+
7
+ module Hearth
8
+ # Functions in the Smithy rules engine are named routines that
9
+ # operate on a finite set of specified inputs, returning an output.
10
+ # The rules engine has a set of included functions that can be
11
+ # invoked without additional dependencies, called the standard library.
12
+ module EndpointRules
13
+ # An Authentication Scheme supported by an Endpoint
14
+ # @!attribute scheme_id
15
+ # The identifier of the authentication scheme.
16
+ # @return [String]
17
+ # @!attribute properties
18
+ # Additional properties of the authentication scheme.
19
+ # @return [Hash]
20
+ AuthScheme = Struct.new(
21
+ :scheme_id,
22
+ :properties,
23
+ keyword_init: true
24
+ ) do
25
+ # @option args [String] :scheme_id
26
+ # @option args [Hash] :properties ({})
27
+ def initialize(*args)
28
+ super
29
+ self.properties ||= {}
30
+ end
31
+ end
32
+
33
+ # An Endpoint resolved by an EndpointProvider
34
+ # @!attribute uri
35
+ # The URI of the endpoint.
36
+ # @return [String]
37
+ # @!attribute auth_schemes
38
+ # The authentication schemes supported by the endpoint.
39
+ # @return [Array<AuthScheme>]
40
+ # @!attribute headers
41
+ # The headers to include in requests to the endpoint.
42
+ # @return [Hash]
43
+ Endpoint = Struct.new(
44
+ :uri,
45
+ :auth_schemes,
46
+ :headers,
47
+ keyword_init: true
48
+ ) do
49
+ # @option args [String] :uri
50
+ # @option args [Array<AuthScheme>] :auth_schemes ([])
51
+ # @option args [Hash] :headers ({})
52
+ def initialize(*args)
53
+ super
54
+ self.auth_schemes ||= []
55
+ self.headers ||= {}
56
+ end
57
+ end
58
+
59
+ # Evaluates whether the input string is a compliant RFC 1123 host segment.
60
+ # When allowSubDomains is true, evaluates whether the input string is
61
+ # composed of values that are each compliant RFC 1123 host segments
62
+ # joined by dot (.) characters.
63
+ # @api private
64
+ # rubocop:disable Style/OptionalBooleanParameter
65
+ def self.valid_host_label?(value, allow_sub_domains = false)
66
+ return false if value.empty?
67
+
68
+ if allow_sub_domains
69
+ labels = value.split('.', -1)
70
+ return labels.all? { |l| valid_host_label?(l, false) }
71
+ end
72
+
73
+ !!(value =~ /\A(?!-)[a-zA-Z0-9-]{1,63}(?<!-)\z/)
74
+ end
75
+ # rubocop:enable Style/OptionalBooleanParameter
76
+
77
+ # Computes a URL structure given an input string.
78
+ # @api private
79
+ def self.parse_url(value)
80
+ URL.new(value).as_json
81
+ rescue ArgumentError, URI::InvalidURIError
82
+ nil
83
+ end
84
+
85
+ # Computes a portion of a given string based on
86
+ # the provided start and end indices.
87
+ # @api private
88
+ def self.substring(input, start, stop, reverse)
89
+ return nil if start >= stop || input.size < stop
90
+
91
+ return nil if input.chars.any? { |c| c.ord > 127 }
92
+
93
+ return input[start...stop] unless reverse
94
+
95
+ r_start = input.size - stop
96
+ r_stop = input.size - start
97
+ input[r_start...r_stop]
98
+ end
99
+
100
+ # Performs RFC 3986#section-2.1 defined percent-encoding on the input value.
101
+ # @api private
102
+ def self.uri_encode(value)
103
+ CGI.escape(value.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~')
104
+ end
105
+
106
+ # @api private
107
+ class URL
108
+ def initialize(url)
109
+ uri = URI(url)
110
+ @scheme = uri.scheme
111
+ # only support http and https schemes
112
+ raise ArgumentError unless %w[https http].include?(@scheme)
113
+
114
+ # do not support query
115
+ raise ArgumentError if uri.query
116
+
117
+ @authority = _authority(url, uri)
118
+ @path = uri.path
119
+ @normalized_path = uri.path + (uri.path[-1] == '/' ? '' : '/')
120
+ @is_ip = _is_ip(uri.host)
121
+ end
122
+
123
+ attr_reader :scheme, :authority, :path, :normalized_path, :is_ip
124
+
125
+ def as_json(_options = {})
126
+ {
127
+ 'scheme' => scheme,
128
+ 'authority' => authority,
129
+ 'path' => path,
130
+ 'normalizedPath' => normalized_path,
131
+ 'isIp' => is_ip
132
+ }
133
+ end
134
+
135
+ private
136
+
137
+ def _authority(url, uri)
138
+ # don't include port if it's default and not parsed originally
139
+ if uri.default_port == uri.port && !url.include?(":#{uri.port}")
140
+ uri.host
141
+ else
142
+ "#{uri.host}:#{uri.port}"
143
+ end
144
+ end
145
+
146
+ def _is_ip(authority)
147
+ IPAddr.new(authority)
148
+ true
149
+ rescue IPAddr::InvalidAddressError
150
+ false
151
+ end
152
+ end
153
+ end
154
+ end
@@ -11,7 +11,7 @@ module Hearth
11
11
  class Client
12
12
  # @api private
13
13
  OPTIONS = {
14
- logger: Logger.new($stdout),
14
+ logger: nil,
15
15
  debug_output: nil,
16
16
  proxy: nil,
17
17
  open_timeout: 15,
@@ -31,12 +31,11 @@ module Hearth
31
31
  #
32
32
  # @param [Hash] options The options for this HTTP Client
33
33
  #
34
- # @option options [Logger] :logger (Logger.new($stdout)) A logger
35
- # used to log Net::HTTP requests and responses when `:debug_output`
36
- # is enabled.
34
+ # @option options [Logger] :logger (nil) A logger used to log Net::HTTP
35
+ # requests and responses when `:debug_output` is enabled.
37
36
  #
38
37
  # @option options [Boolean] :debug_output (false) When `true`,
39
- # sets an output stream to the configured Logger for debugging.
38
+ # sets an output stream to the configured Logger (if any) for debugging.
40
39
  #
41
40
  # @option options [String, URI] :proxy A proxy to send
42
41
  # requests through. Formatted like 'http://proxy.com:123'.
@@ -131,11 +130,10 @@ module Hearth
131
130
  end
132
131
 
133
132
  # Starts and returns a new HTTP connection.
134
- # @param [URI] endpoint
135
133
  # @return [Net::HTTP]
136
134
  def new_connection(endpoint, logger)
137
135
  http = create_http(endpoint)
138
- http.set_debug_output(logger || @logger) if @debug_output
136
+ http.set_debug_output(@logger || logger) if @debug_output
139
137
  configure_timeouts(http)
140
138
 
141
139
  if endpoint.scheme == 'https'
@@ -5,7 +5,6 @@ require 'time'
5
5
  module Hearth
6
6
  module HTTP
7
7
  # An HTTP error inspector, using hints from status code and headers.
8
- # @api private
9
8
  class ErrorInspector
10
9
  def initialize(error, http_response)
11
10
  @error = error
@@ -17,6 +16,7 @@ module Hearth
17
16
  throttling? ||
18
17
  transient? ||
19
18
  server?) &&
19
+ # IO does not respond to #truncate and is not rewindable
20
20
  @http_response.body.respond_to?(:truncate)
21
21
  end
22
22
 
@@ -77,7 +77,7 @@ module Hearth
77
77
  rescue ArgumentError # empty string, somehow
78
78
  nil
79
79
  end
80
- rescue TypeError # header is not prseent
80
+ rescue TypeError # header is not present
81
81
  nil
82
82
  end
83
83
  end
@@ -5,9 +5,8 @@ module Hearth
5
5
  # Represents an HTTP field.
6
6
  class Field
7
7
  # @param [String] name The name of the field.
8
- # @param [Array|#to_s] value (nil) The values for the field. It can be any
9
- # object that responds to `#to_s` or an Array of objects that respond to
10
- # `#to_s`.
8
+ # @param [#to_s] value (nil) The value for the field. It can be any
9
+ # object that responds to `#to_s`.
11
10
  # @param [Symbol] kind The kind of field, either :header or :trailer.
12
11
  def initialize(name, value = nil, kind: :header)
13
12
  if name.nil? || name.empty?
@@ -25,17 +24,10 @@ module Hearth
25
24
  # @return [Symbol]
26
25
  attr_reader :kind
27
26
 
28
- # Returns an escaped string representation of the field.
27
+ # Returns a string representation of the field.
29
28
  # @return [String]
30
29
  def value(encoding = nil)
31
- value =
32
- if @value.is_a?(Array)
33
- @value.compact.map { |v| escape_value(v.to_s) }.join(', ')
34
- else
35
- @value.to_s
36
- end
37
- value = value.encode(encoding) if encoding
38
- value
30
+ encoding ? @value.to_s.encode(encoding) : @value.to_s
39
31
  end
40
32
 
41
33
  # @return [Boolean]
@@ -52,13 +44,6 @@ module Hearth
52
44
  def to_h
53
45
  { @name => value }
54
46
  end
55
-
56
- private
57
-
58
- def escape_value(str)
59
- s = str
60
- s.include?('"') || s.include?(',') ? "\"#{s.gsub('"', '\"')}\"" : s
61
- end
62
47
  end
63
48
  end
64
49
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module HTTP
5
+ # @api private
6
+ module HeaderListBuilder
7
+ class << self
8
+ def build_list(value)
9
+ value.compact.join(', ')
10
+ end
11
+
12
+ # builds a string from a list of possibly quoted values
13
+ # ensures that quoted values are escaped
14
+ def build_string_list(value)
15
+ value.compact.map { |s| escape_value(s) }.join(', ')
16
+ end
17
+
18
+ def build_http_date_list(value)
19
+ value.compact.map { |t| Hearth::TimeHelper.to_http_date(t) }
20
+ .join(', ')
21
+ end
22
+
23
+ def build_date_time_list(value)
24
+ value.compact.map { |t| Hearth::TimeHelper.to_date_time(t) }
25
+ .join(', ')
26
+ end
27
+
28
+ def build_epoch_seconds_list(value)
29
+ value.compact.map { |t| Hearth::TimeHelper.to_epoch_seconds(t) }
30
+ .join(', ')
31
+ end
32
+
33
+ private
34
+
35
+ def escape_value(str)
36
+ s = str
37
+ s.include?('"') || s.include?(',') ? "\"#{s.gsub('"', '\"')}\"" : s
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+
5
+ module Hearth
6
+ module HTTP
7
+ # @api private
8
+ module HeaderListParser
9
+ class << self
10
+ def parse_boolean_list(value)
11
+ value.split(', ').map { |s| s == 'true' }
12
+ end
13
+
14
+ def parse_integer_list(value)
15
+ value.split(', ').map(&:to_i)
16
+ end
17
+
18
+ def parse_float_list(value)
19
+ value.split(', ').map(&:to_f)
20
+ end
21
+
22
+ # parse a list of possibly quoted and escaped string values
23
+ # Follows:
24
+ # # [RFC-7230's specification of header values](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6).
25
+ def parse_string_list(value)
26
+ buffer = StringScanner.new(value)
27
+ parsed = []
28
+
29
+ parsed << read_value(buffer) until buffer.eos?
30
+
31
+ parsed
32
+ end
33
+
34
+ # rfc822/http-date has a comma after day but is NOT escaped.
35
+ # # eg: Mon, 16 Dec 2019 23:48:18 GMT, Mon, 16 Dec 2019 23:48:18 GMT
36
+ def parse_http_date_list(value)
37
+ value.split(',').each_slice(2).map { |v| Time.parse(v[0] + v[1]) }
38
+ end
39
+
40
+ def parse_date_time_list(value)
41
+ value.split(',').map { |v| Time.parse(v) }
42
+ end
43
+
44
+ def parse_epoch_seconds_list(value)
45
+ value.split(',').map { |v| Time.at(v.to_i) }
46
+ end
47
+
48
+ private
49
+
50
+ def read_value(buffer)
51
+ until buffer.eos?
52
+ case buffer.peek(1)
53
+ when ' ', "\t"
54
+ # drop leading whitespace
55
+ buffer.getch
56
+ next
57
+ when '"'
58
+ buffer.getch # drop the quote and advance
59
+ return read_quoted_value(buffer)
60
+ else
61
+ return read_unquoted_value(buffer)
62
+ end
63
+ end
64
+ # buffer is only whitespace
65
+ nil
66
+ end
67
+
68
+ def read_unquoted_value(buffer)
69
+ # there cannot be any escaped values
70
+ value = buffer.scan_until(/,|$/)
71
+ # drop the comma if we matched it
72
+ buffer.matched == ',' ? value.chop : value
73
+ end
74
+
75
+ def read_quoted_value(buffer)
76
+ # scan until we have an unescaped double quote
77
+ value = buffer.scan_until(/[^\\]"/)
78
+ unless value
79
+ raise ArgumentError,
80
+ 'Invalid String list: No closing quote found'
81
+ end
82
+
83
+ # drop any remaining whitespace/commas
84
+ buffer.scan_until(/[\s,]*/)
85
+ # the last character will always be the closing quote.
86
+ # Add a starting quote and then unescape (undump)
87
+ "\"#{value}".undump
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -4,7 +4,6 @@ module Hearth
4
4
  module HTTP
5
5
  module Middleware
6
6
  # A middleware that sets Content-Length for any body that has a size.
7
- # @api private
8
7
  class ContentLength
9
8
  include Hearth::Middleware::Logging
10
9
 
@@ -17,9 +16,10 @@ module Hearth
17
16
  # @return [Output]
18
17
  def call(input, context)
19
18
  request = context.request
19
+ body = request.body
20
20
  if !request.headers.key?('Content-Length') &&
21
- request.body.respond_to?(:size)
22
- length = request.body.size
21
+ (body.respond_to?(:size) && body.size.positive?)
22
+ length = body.size
23
23
  request.headers['Content-Length'] = length
24
24
  log_debug(context, "Set Content-Length to #{length}")
25
25
  end
@@ -4,7 +4,6 @@ module Hearth
4
4
  module HTTP
5
5
  module Middleware
6
6
  # A middleware that sets Content-MD5 for any body.
7
- # @api private
8
7
  class ContentMD5
9
8
  include Hearth::Middleware::Logging
10
9