seahorse 0.1.0 → 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +12 -0
  3. data/VERSION +1 -0
  4. data/lib/seahorse/api_error.rb +15 -0
  5. data/lib/seahorse/block_io.rb +24 -0
  6. data/lib/seahorse/context.rb +34 -0
  7. data/lib/seahorse/http/api_error.rb +29 -0
  8. data/lib/seahorse/http/client.rb +152 -0
  9. data/lib/seahorse/http/error_parser.rb +105 -0
  10. data/lib/seahorse/http/headers.rb +70 -0
  11. data/lib/seahorse/http/middleware/content_length.rb +28 -0
  12. data/lib/seahorse/http/networking_error.rb +20 -0
  13. data/lib/seahorse/http/request.rb +132 -0
  14. data/lib/seahorse/http/response.rb +29 -0
  15. data/lib/seahorse/http.rb +36 -0
  16. data/lib/seahorse/json/parse_error.rb +18 -0
  17. data/lib/seahorse/json.rb +30 -0
  18. data/lib/seahorse/middleware/around_handler.rb +24 -0
  19. data/lib/seahorse/middleware/build.rb +26 -0
  20. data/lib/seahorse/middleware/host_prefix.rb +48 -0
  21. data/lib/seahorse/middleware/parse.rb +42 -0
  22. data/lib/seahorse/middleware/request_handler.rb +24 -0
  23. data/lib/seahorse/middleware/response_handler.rb +25 -0
  24. data/lib/seahorse/middleware/retry.rb +43 -0
  25. data/lib/seahorse/middleware/send.rb +62 -0
  26. data/lib/seahorse/middleware/validate.rb +29 -0
  27. data/lib/seahorse/middleware.rb +16 -0
  28. data/lib/seahorse/middleware_builder.rb +246 -0
  29. data/lib/seahorse/middleware_stack.rb +73 -0
  30. data/lib/seahorse/output.rb +20 -0
  31. data/lib/seahorse/structure.rb +40 -0
  32. data/lib/seahorse/stubbing/client_stubs.rb +115 -0
  33. data/lib/seahorse/stubbing/stubs.rb +32 -0
  34. data/lib/seahorse/time_helper.rb +35 -0
  35. data/lib/seahorse/union.rb +10 -0
  36. data/lib/seahorse/validator.rb +20 -0
  37. data/lib/seahorse/waiters/errors.rb +15 -0
  38. data/lib/seahorse/waiters/poller.rb +132 -0
  39. data/lib/seahorse/waiters/waiter.rb +79 -0
  40. data/lib/seahorse/xml/formatter.rb +68 -0
  41. data/lib/seahorse/xml/node.rb +123 -0
  42. data/lib/seahorse/xml/parse_error.rb +18 -0
  43. data/lib/seahorse/xml.rb +58 -0
  44. data/lib/seahorse.rb +23 -13
  45. data/sig/lib/seahorse/api_error.rbs +10 -0
  46. data/sig/lib/seahorse/document.rbs +2 -0
  47. data/sig/lib/seahorse/http/api_error.rbs +21 -0
  48. data/sig/lib/seahorse/http/headers.rbs +47 -0
  49. data/sig/lib/seahorse/http/response.rbs +21 -0
  50. data/sig/lib/seahorse/simple_delegator.rbs +3 -0
  51. data/sig/lib/seahorse/structure.rbs +18 -0
  52. data/sig/lib/seahorse/stubbing/client_stubs.rbs +103 -0
  53. data/sig/lib/seahorse/stubbing/stubs.rbs +14 -0
  54. data/sig/lib/seahorse/union.rbs +6 -0
  55. metadata +73 -54
  56. data/LICENSE +0 -12
  57. data/README.md +0 -3
  58. data/Rakefile +0 -7
  59. data/lib/seahorse/api_translator/inflector.rb +0 -37
  60. data/lib/seahorse/api_translator/operation.rb +0 -150
  61. data/lib/seahorse/api_translator/shape.rb +0 -235
  62. data/lib/seahorse/controller.rb +0 -87
  63. data/lib/seahorse/model.rb +0 -82
  64. data/lib/seahorse/operation.rb +0 -66
  65. data/lib/seahorse/param_validator.rb +0 -158
  66. data/lib/seahorse/railtie.rb +0 -7
  67. data/lib/seahorse/router.rb +0 -20
  68. data/lib/seahorse/shape_builder.rb +0 -84
  69. data/lib/seahorse/type.rb +0 -220
  70. data/lib/seahorse/version.rb +0 -3
  71. data/lib/tasks/seahorse_tasks.rake +0 -24
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seahorse
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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seahorse
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 Seahorse
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 Seahorse
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 Seahorse
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 Seahorse
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:%S.%LZ')
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 Seahorse
6
+ # Top level class for all Union types
7
+ class Union < ::SimpleDelegator
8
+ include Seahorse::Structure
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seahorse
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 Seahorse
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 Seahorse
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 Seahorse::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 Seahorse
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 Seahorse
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