hearth 1.0.0.pre2 → 1.0.0.pre3

Sign up to get free protection for your applications and to get access to all the features.
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