hearth 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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/VERSION +1 -0
  4. data/lib/hearth/api_error.rb +15 -0
  5. data/lib/hearth/block_io.rb +24 -0
  6. data/lib/hearth/context.rb +34 -0
  7. data/lib/hearth/http/api_error.rb +29 -0
  8. data/lib/hearth/http/client.rb +152 -0
  9. data/lib/hearth/http/error_parser.rb +105 -0
  10. data/lib/hearth/http/headers.rb +70 -0
  11. data/lib/hearth/http/middleware/content_length.rb +29 -0
  12. data/lib/hearth/http/networking_error.rb +20 -0
  13. data/lib/hearth/http/request.rb +132 -0
  14. data/lib/hearth/http/response.rb +29 -0
  15. data/lib/hearth/http.rb +36 -0
  16. data/lib/hearth/json/parse_error.rb +18 -0
  17. data/lib/hearth/json.rb +30 -0
  18. data/lib/hearth/middleware/around_handler.rb +24 -0
  19. data/lib/hearth/middleware/build.rb +26 -0
  20. data/lib/hearth/middleware/host_prefix.rb +48 -0
  21. data/lib/hearth/middleware/parse.rb +42 -0
  22. data/lib/hearth/middleware/request_handler.rb +24 -0
  23. data/lib/hearth/middleware/response_handler.rb +25 -0
  24. data/lib/hearth/middleware/retry.rb +43 -0
  25. data/lib/hearth/middleware/send.rb +62 -0
  26. data/lib/hearth/middleware/validate.rb +29 -0
  27. data/lib/hearth/middleware.rb +16 -0
  28. data/lib/hearth/middleware_builder.rb +246 -0
  29. data/lib/hearth/middleware_stack.rb +73 -0
  30. data/lib/hearth/number_helper.rb +33 -0
  31. data/lib/hearth/output.rb +20 -0
  32. data/lib/hearth/structure.rb +40 -0
  33. data/lib/hearth/stubbing/client_stubs.rb +115 -0
  34. data/lib/hearth/stubbing/stubs.rb +32 -0
  35. data/lib/hearth/time_helper.rb +35 -0
  36. data/lib/hearth/union.rb +10 -0
  37. data/lib/hearth/validator.rb +20 -0
  38. data/lib/hearth/waiters/errors.rb +15 -0
  39. data/lib/hearth/waiters/poller.rb +132 -0
  40. data/lib/hearth/waiters/waiter.rb +79 -0
  41. data/lib/hearth/xml/formatter.rb +68 -0
  42. data/lib/hearth/xml/node.rb +123 -0
  43. data/lib/hearth/xml/node_matcher.rb +24 -0
  44. data/lib/hearth/xml/parse_error.rb +18 -0
  45. data/lib/hearth/xml.rb +58 -0
  46. data/lib/hearth.rb +26 -0
  47. data/sig/lib/seahorse/api_error.rbs +10 -0
  48. data/sig/lib/seahorse/document.rbs +2 -0
  49. data/sig/lib/seahorse/http/api_error.rbs +21 -0
  50. data/sig/lib/seahorse/http/headers.rbs +47 -0
  51. data/sig/lib/seahorse/http/response.rbs +21 -0
  52. data/sig/lib/seahorse/simple_delegator.rbs +3 -0
  53. data/sig/lib/seahorse/structure.rbs +18 -0
  54. data/sig/lib/seahorse/stubbing/client_stubs.rbs +103 -0
  55. data/sig/lib/seahorse/stubbing/stubs.rbs +14 -0
  56. data/sig/lib/seahorse/union.rbs +6 -0
  57. metadata +111 -0
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # @api private
5
+ class MiddlewareStack
6
+ def initialize
7
+ @middleware = []
8
+ end
9
+
10
+ def use(middleware, **middleware_kwargs)
11
+ @middleware.push([middleware, middleware_kwargs])
12
+ end
13
+
14
+ def use_before(before, middleware, **middleware_kwargs)
15
+ new_middleware = []
16
+ @middleware.each do |klass, args|
17
+ new_middleware << [middleware, middleware_kwargs] if before == klass
18
+ new_middleware << [klass, args]
19
+ end
20
+ unless new_middleware.size == @middleware.size + 1
21
+ raise ArgumentError,
22
+ "Failed to insert #{middleware} before #{before}"
23
+ end
24
+
25
+ @middleware = new_middleware
26
+ end
27
+
28
+ def use_after(after, middleware, **middleware_kwargs)
29
+ new_middleware = []
30
+ @middleware.each do |klass, args|
31
+ new_middleware << [klass, args]
32
+ new_middleware << [middleware, middleware_kwargs] if after == klass
33
+ end
34
+ unless new_middleware.size == @middleware.size + 1
35
+ raise ArgumentError,
36
+ "Failed to insert #{middleware} after #{after}"
37
+ end
38
+
39
+ @middleware = new_middleware
40
+ end
41
+
42
+ def remove(remove)
43
+ new_middleware = []
44
+ @middleware.each do |klass, args|
45
+ new_middleware << [klass, args] unless klass == remove
46
+ end
47
+
48
+ unless new_middleware.size == @middleware.size - 1
49
+ raise ArgumentError,
50
+ "Failed to remove #{remove}"
51
+ end
52
+
53
+ @middleware = new_middleware
54
+ end
55
+
56
+ # @param input
57
+ # @param context
58
+ # @return [Output]
59
+ def run(input:, context:)
60
+ stack.call(input, context)
61
+ end
62
+
63
+ private
64
+
65
+ def stack
66
+ app = nil
67
+ @middleware.reverse_each do |(middleware, middleware_args)|
68
+ app = middleware.new(app, **middleware_args)
69
+ end
70
+ app
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # A module that provides helper methods to convert Numeric objects to
5
+ # protocol specific serializable formats.
6
+ # @api private
7
+ module NumberHelper
8
+ class << self
9
+ # @param [Number,String] input
10
+ # @return [String] The serialized number
11
+ def serialize(input)
12
+ if input == ::Float::INFINITY then 'Infinity'
13
+ elsif input == -::Float::INFINITY then '-Infinity'
14
+ elsif input&.nan? then 'NaN'
15
+ else
16
+ input
17
+ end
18
+ end
19
+
20
+ # @param [String] str
21
+ # @return [Number] The input as a number
22
+ def deserialize(str)
23
+ case str
24
+ when 'Infinity' then ::Float::INFINITY
25
+ when '-Infinity' then -::Float::INFINITY
26
+ when 'NaN' then ::Float::NAN
27
+ when nil then nil
28
+ else str.to_f
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # A wrapper class that contains an error or data from the response.
5
+ # @api private
6
+ class Output
7
+ # @param [StandardError] error The error class to be raised.
8
+ # @param [Struct] data The data returned by a client.
9
+ def initialize(error: nil, data: nil)
10
+ @error = error
11
+ @data = data
12
+ end
13
+
14
+ # @return [StandardError, nil]
15
+ attr_accessor :error
16
+
17
+ # @return [Struct, nil]
18
+ attr_accessor :data
19
+ end
20
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # A module mixed into Structs that provides utility methods.
5
+ module Structure
6
+ # Deeply converts the Struct into a hash. Structure members that
7
+ # are `nil` are omitted from the resultant hash.
8
+ #
9
+ # @return [Hash]
10
+ def to_h(obj = self)
11
+ case obj
12
+ when Struct
13
+ _to_h_struct(obj)
14
+ when Hash
15
+ _to_h_hash(obj)
16
+ when Array, Set
17
+ obj.collect { |value| to_hash(value) }
18
+ when Union
19
+ obj.to_h
20
+ else
21
+ obj
22
+ end
23
+ end
24
+ alias to_hash to_h
25
+
26
+ private
27
+
28
+ def _to_h_struct(obj)
29
+ obj.each_pair.with_object({}) do |(member, value), hash|
30
+ hash[member] = to_hash(value) unless value.nil?
31
+ end
32
+ end
33
+
34
+ def _to_h_hash(obj)
35
+ obj.each.with_object({}) do |(key, value), hash|
36
+ hash[key] = to_hash(value)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'stubs'
4
+
5
+ module Hearth
6
+ # This module provides the ability to specify the data and/or errors to
7
+ # return when a client is using stubbed responses.
8
+ # This module should be included in generated service clients.
9
+ #
10
+ # Pass `stub_responses: true` to a client constructor to enable this
11
+ # behavior.
12
+ module ClientStubs
13
+ # Configures what data / errors should be returned from the named operation
14
+ # when response stubbing is enabled.
15
+ #
16
+ # ## Basic usage
17
+ #
18
+ # When you enable response stubbing, the client will generate fake
19
+ # responses and will not make any HTTP requests.
20
+ #
21
+ # client = Service::Client.new(stub_responses: true)
22
+ # client.operation
23
+ # #=> #<struct Service:Types::Operation param1=[], param2=nil>
24
+ #
25
+ # You can specify the stub data using {#stub_responses}
26
+ #
27
+ # client = Service::Client.new(stub_responses: true)
28
+ # client.stub_responses(:operation, {
29
+ # param1: [{ name: 'value1' }]
30
+ # })
31
+ #
32
+ # client.operation.param1.map(&:name)
33
+ # #=> ['value1']
34
+ #
35
+ # ## Stubbing Errors
36
+ #
37
+ # When stubbing is enabled, the SDK will default to generate
38
+ # fake responses with placeholder values. You can override the data
39
+ # returned. You can also specify errors it should raise.
40
+ #
41
+ # # to simulate errors, give the error class, you must
42
+ # # be able to construct an instance with `.new`
43
+ # client.stub_responses(:operation, Timeout::Error)
44
+ # client.operation(param1: 'value')
45
+ # #=> raises new Timeout::Error
46
+ #
47
+ # # or you can give an instance of an error class
48
+ # client.stub_responses(:operation, RuntimeError.new('custom message'))
49
+ # client.operation(param1: 'value')
50
+ # #=> raises the given runtime error object
51
+ #
52
+ # ## Dynamic Stubbing
53
+ #
54
+ # In addition to creating static stubs, it's also possible to generate
55
+ # stubs dynamically based on the parameters with which operations were
56
+ # called, by passing a `Proc` object:
57
+ #
58
+ # client.stub_responses(:operation, -> (context) {
59
+ # if context.params[:param] == 'foo'
60
+ # # return a stub
61
+ # { param1: [{ name: 'value1'}]}
62
+ # else
63
+ # # return an error
64
+ # Services::Errors::NotFound
65
+ # end
66
+ # })
67
+ #
68
+ # ## Stubbing Raw Protocol Responses
69
+ #
70
+ # As an alternative to providing the response data, you can modify the
71
+ # response object provided by the `Proc` object and then
72
+ # return nil.
73
+ #
74
+ # client.stub_responses(:operation, -> (context) {
75
+ # context.response.status = 404 # simulate an error
76
+ # nil
77
+ # })
78
+ #
79
+ # ## Stubbing Multiple Responses
80
+ #
81
+ # Calling an operation multiple times will return similar responses.
82
+ # You can configure multiple stubs and they will be returned in sequence.
83
+ #
84
+ # client.stub_responses(:operation, [
85
+ # Errors::NotFound,
86
+ # { content_length: 150 },
87
+ # ])
88
+ #
89
+ # client.operation(param1: 'value1')
90
+ # #=> raises Errors::NotFound
91
+ #
92
+ # resp = client.operation(param1: 'value2')
93
+ # resp.content_length #=> 150
94
+ #
95
+ # @param [Symbol] operation_name
96
+ #
97
+ # @param [Mixed] stubs One or more responses to return from the named
98
+ # operation.
99
+ #
100
+ # @return [void]
101
+ #
102
+ # @raise [RuntimeError] Raises a runtime error when called
103
+ # on a client that has not enabled response stubbing via
104
+ # `:stub_responses => true`.
105
+ def stub_responses(operation_name, *stubs)
106
+ if @stub_responses
107
+ @stubs.add_stubs(operation_name, stubs.flatten)
108
+ else
109
+ msg = 'Stubbing is not enabled. Enable stubbing in the constructor '\
110
+ 'with `stub_responses: true`'
111
+ raise ArgumentError, msg
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # @api private
5
+ module Stubbing
6
+ # Provides a thread safe data structure for adding and getting stubs
7
+ # per operation.
8
+ class Stubs
9
+ def initialize
10
+ @stubs = {}
11
+ @stub_mutex = Mutex.new
12
+ end
13
+
14
+ def add_stubs(operation_name, stubs)
15
+ @stub_mutex.synchronize do
16
+ @stubs[operation_name.to_sym] = stubs
17
+ end
18
+ end
19
+
20
+ def next(operation_name)
21
+ @stub_mutex.synchronize do
22
+ stubs = @stubs[operation_name] || []
23
+ case stubs.length
24
+ when 0 then nil
25
+ when 1 then stubs.first
26
+ else stubs.shift
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Hearth
6
+ # A module that provides helper methods to convert from Time objects to
7
+ # protocol specific serializable formats.
8
+ # @api private
9
+ module TimeHelper
10
+ class << self
11
+ # @param [Time] time
12
+ # @return [String<Date Time>] The time as an ISO8601 string.
13
+ def to_date_time(time)
14
+ time.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
15
+ end
16
+
17
+ # @param [Time] time
18
+ # @return [Float<Epoch Seconds>] Returns float value of
19
+ # epoch seconds with millisecond precision.
20
+ def to_epoch_seconds(time)
21
+ time = time.utc
22
+ epoch_seconds = time.to_i
23
+ epoch_seconds += (time.nsec / 1_000_000) / 1000.0
24
+ epoch_seconds
25
+ end
26
+
27
+ # @param [Time] time
28
+ # @return [String<Http Date>] Returns the time formatted
29
+ # as an HTTP header date.
30
+ def to_http_date(time)
31
+ time.utc.httpdate
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module Hearth
6
+ # Top level class for all Union types
7
+ class Union < ::SimpleDelegator
8
+ include Hearth::Structure
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # Utility module for working with request parameters.
5
+ #
6
+ # * Validate structure of parameters against the expected type.
7
+ # * Raise errors with context when validation fails.
8
+ # @api private
9
+ module Validator
10
+ # Validate the given values is of the given type(s).
11
+ # @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) }
14
+
15
+ raise ArgumentError,
16
+ "Expected #{context} to be in "\
17
+ "[#{types.map(&:to_s).join(', ')}], got #{value.class}."
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Waiters
5
+ module Errors
6
+ class WaiterFailed < StandardError; end
7
+
8
+ class FailureStateError < StandardError; end
9
+
10
+ class UnexpectedError < StandardError; end
11
+
12
+ class MaxWaitTimeExceeded < StandardError; end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jmespath'
4
+
5
+ module Hearth
6
+ module Waiters
7
+ # Abstract Poller used by generated service Waiters. This class handles
8
+ # sending the request and matching input or output.
9
+ class Poller
10
+ # @api private
11
+ def initialize(options = {})
12
+ @operation_name = options[:operation_name]
13
+ @acceptors = options[:acceptors]
14
+ @input = nil
15
+ end
16
+
17
+ # Makes an API call, returning the resultant state and the response.
18
+ #
19
+ # * `:success` - A success state has been matched.
20
+ # * `:failure` - A terminate failure state has been matched.
21
+ # * `:retry` - The waiter may be retried.
22
+ # * `:error` - The waiter encountered an un-expected error.
23
+ #
24
+ # @example A trival (bad) example of a waiter that polls indefinetly.
25
+ #
26
+ # loop do
27
+ #
28
+ # state, resp = poller.call(client, params, options)
29
+ #
30
+ # case state
31
+ # when :success then return true
32
+ # when :failure then return false
33
+ # when :retry then next
34
+ # when :error then raise 'oops'
35
+ # end
36
+ #
37
+ # end
38
+ #
39
+ # @param [Client] client
40
+ # @param [Hash] params
41
+ # @param [Hash] options
42
+ # @return [Array<Symbol,Response>]
43
+ def call(client, params = {}, options = {})
44
+ begin
45
+ options = options.merge(input_output_middleware)
46
+ response = client.send(@operation_name, params, options)
47
+ rescue Hearth::ApiError => e
48
+ error = e
49
+ end
50
+ resp_or_error = error || response
51
+ @acceptors.each do |acceptor|
52
+ if acceptor_matches?(acceptor[:matcher], response, error)
53
+ return [acceptor[:state].to_sym, resp_or_error]
54
+ end
55
+ end
56
+ [error ? :error : :retry, resp_or_error]
57
+ end
58
+
59
+ private
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) }
66
+ end
67
+
68
+ def acceptor_matches?(matcher, response, error)
69
+ if (m = matcher[:success])
70
+ success_matcher?(m, response, error)
71
+ elsif (m = matcher[:errorType])
72
+ error_type_matcher?(m, error)
73
+ elsif (m = matcher[:inputOutput])
74
+ input_output_matcher?(m, response, error)
75
+ elsif (m = matcher[:output])
76
+ output_matcher?(m, response, error)
77
+ end
78
+ end
79
+
80
+ def success_matcher?(matcher, response, error)
81
+ (matcher == true && response) || (matcher == false && error)
82
+ end
83
+
84
+ def error_type_matcher?(matcher, error)
85
+ # handle shape ID cases
86
+ matcher = matcher.split('#').last.split('$').first
87
+ error.class.to_s.include?(matcher) || error.error_code == matcher
88
+ end
89
+
90
+ def input_output_matcher?(matcher, response, error)
91
+ return false if error
92
+
93
+ data = { input: @input, output: response }
94
+ send(
95
+ "matches_#{matcher[:comparator]}?",
96
+ JMESPath.search(matcher[:path], data),
97
+ matcher[:expected]
98
+ )
99
+ end
100
+
101
+ def output_matcher?(matcher, response, error)
102
+ return false if error
103
+
104
+ send(
105
+ "matches_#{matcher[:comparator]}?",
106
+ JMESPath.search(matcher[:path], response),
107
+ matcher[:expected]
108
+ )
109
+ end
110
+
111
+ # rubocop:disable Naming/MethodName
112
+ def matches_stringEquals?(value, expected)
113
+ value == expected
114
+ end
115
+
116
+ def matches_booleanEquals?(value, expected)
117
+ value.to_s == expected
118
+ end
119
+
120
+ def matches_allStringEquals?(values, expected)
121
+ values.is_a?(Array) && !values.empty? &&
122
+ values.all? { |v| v == expected }
123
+ end
124
+
125
+ def matches_anyStringEquals?(values, expected)
126
+ values.is_a?(Array) && !values.empty? &&
127
+ values.any? { |v| v == expected }
128
+ end
129
+ # rubocop:enable Naming/MethodName
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Waiters
5
+ # Abstract waiter class with high level logic for polling and waiting.
6
+ class Waiter
7
+ # @api private
8
+ def initialize(options = {})
9
+ unless options[:max_wait_time].is_a?(Integer)
10
+ raise ArgumentError,
11
+ 'Waiter must be initialized with `:max_wait_time`'
12
+ end
13
+
14
+ @max_wait_time = options[:max_wait_time]
15
+ @min_delay = options[:min_delay]
16
+ @max_delay = options[:max_delay]
17
+ @poller = options[:poller]
18
+
19
+ @remaining_time = @max_wait_time
20
+ @one_more_retry = false
21
+ end
22
+
23
+ attr_reader :max_wait_time, :min_delay, :max_delay
24
+
25
+ # @param [Client] client The client to poll with.
26
+ # @param [Hash] params The params for the operation.
27
+ # @param [Hash] options Any operation options.
28
+ def wait(client, params = {}, options = {})
29
+ poll(client, params, options)
30
+ true
31
+ end
32
+
33
+ private
34
+
35
+ # https://awslabs.github.io/smithy/1.0/spec/waiters.html#waiter-workflow
36
+ def poll(client, params, options)
37
+ n = 0
38
+ loop do
39
+ state, resp_or_error = @poller.call(client, params, options)
40
+ n += 1
41
+
42
+ case state
43
+ when :retry then nil
44
+ when :success then return
45
+ when :failure then raise Errors::FailureStateError, resp_or_error
46
+ when :error then raise Errors::UnexpectedError, resp_or_error
47
+ end
48
+
49
+ raise Errors::MaxWaitTimeExceeded if @one_more_retry
50
+
51
+ delay = delay(n)
52
+ @remaining_time -= delay
53
+ Kernel.sleep(delay)
54
+ end
55
+ end
56
+
57
+ def delay(attempt)
58
+ delay = if attempt > attempt_ceiling
59
+ max_delay
60
+ else
61
+ min_delay * (2**(attempt - 1))
62
+ end
63
+
64
+ delay = Kernel.rand(min_delay..delay)
65
+
66
+ if @remaining_time - delay <= min_delay
67
+ delay = @remaining_time - min_delay
68
+ @one_more_retry = true
69
+ end
70
+
71
+ delay
72
+ end
73
+
74
+ def attempt_ceiling
75
+ (Math.log(max_delay.to_f / min_delay) / Math.log(2)) + 1
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module Hearth
6
+ module XML
7
+ # A class used for formatting XML strings.
8
+ # @api private
9
+ class Formatter
10
+ NEGATIVE_INDENT = 'indent must be greater than or equal to zero'
11
+
12
+ # @param [String] indent
13
+ # When `indent` is non-empty whitespace, then the XML will
14
+ # be pretty-formatted with each level of nodes indented by
15
+ # `indent`.
16
+ # @raise [ArgumentError] when indent is not a String.
17
+ def initialize(indent: '')
18
+ unless indent.is_a?(String)
19
+ raise ArgumentError, "expected a String, got #{indent.class}"
20
+ end
21
+
22
+ @indent = indent
23
+ @eol = indent.empty? ? '' : "\n"
24
+ end
25
+
26
+ # @param [Node] node
27
+ # @return [String<XML>]
28
+ def format(node)
29
+ buffer = StringIO.new
30
+ serialize(buffer, node, '')
31
+ buffer.string
32
+ end
33
+
34
+ private
35
+
36
+ def serialize(buffer, node, pad)
37
+ return buffer.write(self_close_node(node, pad)) if node.empty?
38
+ return buffer.write(text_node(node, pad)) if node.text
39
+
40
+ serialize_nested(buffer, node, pad)
41
+ end
42
+
43
+ def self_close_node(node, pad)
44
+ "#{pad}<#{node.name}#{attrs(node)}/>#{@eol}"
45
+ end
46
+
47
+ def text_node(node, pad)
48
+ text = node.text.encode(xml: :text)
49
+ "#{pad}<#{node.name}#{attrs(node)}>#{text}</#{node.name}>#{@eol}"
50
+ end
51
+
52
+ def serialize_nested(buffer, node, pad)
53
+ buffer.write("#{pad}<#{node.name}#{attrs(node)}>#{@eol}")
54
+ nested_pad = "#{pad}#{@indent}"
55
+ node.child_nodes.each do |child_node|
56
+ serialize(buffer, child_node, nested_pad)
57
+ end
58
+ buffer.write("#{pad}</#{node.name}>#{@eol}")
59
+ end
60
+
61
+ def attrs(node)
62
+ node.attributes.map do |key, value|
63
+ " #{key}=#{value.to_s.encode(xml: :attr)}"
64
+ end.join
65
+ end
66
+ end
67
+ end
68
+ end