multi_xml 0.8.1 → 0.9.0

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.
data/lib/multi_xml.rb CHANGED
@@ -3,27 +3,63 @@ require "date"
3
3
  require "stringio"
4
4
  require "time"
5
5
  require "yaml"
6
+ require_relative "multi_xml/concurrency"
6
7
  require_relative "multi_xml/constants"
7
8
  require_relative "multi_xml/errors"
8
9
  require_relative "multi_xml/file_like"
9
10
  require_relative "multi_xml/helpers"
11
+ require_relative "multi_xml/options"
12
+ require_relative "multi_xml/options_normalization"
13
+ require_relative "multi_xml/parser"
14
+ require_relative "multi_xml/parser_resolution"
15
+ require_relative "multi_xml/parse_support"
10
16
 
11
17
  # A generic swappable back-end for parsing XML
12
18
  #
13
- # MultiXml provides a unified interface for XML parsing across different
19
+ # MultiXML provides a unified interface for XML parsing across different
14
20
  # parser libraries. It automatically selects the best available parser
15
21
  # (Ox, LibXML, Nokogiri, Oga, or REXML) and converts XML to Ruby hashes.
16
22
  #
17
23
  # @api public
18
24
  # @example Parse XML
19
- # MultiXml.parse('<root><name>John</name></root>')
25
+ # MultiXML.parse('<root><name>John</name></root>')
20
26
  # #=> {"root"=>{"name"=>"John"}}
21
27
  #
22
28
  # @example Set the parser
23
- # MultiXml.parser = :nokogiri
24
- module MultiXml
29
+ # MultiXML.parser = :nokogiri
30
+ module MultiXML
31
+ extend Options
32
+
33
+ # Tracks which deprecation warnings have already been emitted so each
34
+ # one fires at most once per process. Stored as a Set rather than a
35
+ # Hash so presence checks have unambiguous semantics for mutation tests.
36
+ DEPRECATION_WARNINGS_SHOWN = Set.new
37
+ private_constant :DEPRECATION_WARNINGS_SHOWN
38
+
39
+ # Emit a deprecation warning at most once per process for the given key
40
+ #
41
+ # The warning is tagged with the :deprecated category so callers can
42
+ # silence the whole set with Warning[:deprecated] = false or surface
43
+ # it via ruby -W:deprecated — the standard Ruby idiom for library
44
+ # deprecations since 2.7.
45
+ #
46
+ # @api private
47
+ # @param key [Symbol] identifier for the deprecation (typically the method name)
48
+ # @param message [String] warning message to emit on first call
49
+ # @return [void]
50
+ def self.warn_deprecation_once(key, message)
51
+ Concurrency.synchronize(:deprecation_warnings) do
52
+ return if DEPRECATION_WARNINGS_SHOWN.include?(key)
53
+
54
+ Kernel.warn(message, category: :deprecated)
55
+ DEPRECATION_WARNINGS_SHOWN.add(key)
56
+ end
57
+ end
58
+
25
59
  class << self
26
60
  include Helpers
61
+ include ParserResolution
62
+ include ParseSupport
27
63
 
28
64
  # Get the current XML parser module
29
65
  #
@@ -32,9 +68,16 @@ module MultiXml
32
68
  #
33
69
  # @api public
34
70
  # @return [Module] the current parser module
71
+ # Honors a fiber-local override set by {.with_parser} so concurrent
72
+ # blocks observe their own parser without clobbering the process-wide
73
+ # default. Falls back to the process default when no override is set.
74
+ #
35
75
  # @example Get current parser
36
- # MultiXml.parser #=> MultiXml::Parsers::Ox
76
+ # MultiXML.parser #=> MultiXML::Parsers::Ox
37
77
  def parser
78
+ override = Fiber[:multi_xml_parser]
79
+ return override if override
80
+
38
81
  @parser ||= resolve_parser(detect_parser)
39
82
  end
40
83
 
@@ -43,12 +86,13 @@ module MultiXml
43
86
  # @api public
44
87
  # @param new_parser [Symbol, String, Module] Parser specification
45
88
  # - Symbol/String: :libxml, :nokogiri, :ox, :rexml, :oga
46
- # - Module: Custom parser implementing parse(io) and parse_error
89
+ # - Module: Custom parser implementing parse(io) or
90
+ # parse(io, namespaces: ...) and parse_error
47
91
  # @return [Module] the newly configured parser module
48
92
  # @example Set parser by symbol
49
- # MultiXml.parser = :nokogiri
93
+ # MultiXML.parser = :nokogiri
50
94
  # @example Set parser by module
51
- # MultiXml.parser = MyCustomParser
95
+ # MultiXML.parser = MyCustomParser
52
96
  def parser=(new_parser)
53
97
  @parser = resolve_parser(new_parser)
54
98
  end
@@ -59,157 +103,116 @@ module MultiXml
59
103
  # @param xml [String, IO] XML content as a string or IO-like object
60
104
  # @param options [Hash] Parsing options
61
105
  # @option options [Symbol, String, Module] :parser Parser to use for this call
62
- # @option options [Boolean] :symbolize_keys Convert keys to symbols (default: false)
106
+ # @option options [Boolean] :symbolize_names Convert keys to symbols (default: false)
63
107
  # @option options [Array<String>] :disallowed_types Types to reject (default: ['yaml', 'symbol'])
64
108
  # @option options [Boolean] :typecast_xml_value Apply type conversions (default: true)
109
+ # @option options [Symbol] :namespaces Namespace handling mode (:strip or :preserve)
65
110
  # @return [Hash] Parsed XML as nested hash
66
111
  # @raise [ParseError] if XML is malformed
67
112
  # @raise [DisallowedTypeError] if XML contains a disallowed type attribute
68
113
  # @example Parse simple XML
69
- # MultiXml.parse('<root><name>John</name></root>')
114
+ # MultiXML.parse('<root><name>John</name></root>')
70
115
  # #=> {"root"=>{"name"=>"John"}}
71
- # @example Parse with symbolized keys
72
- # MultiXml.parse('<root><name>John</name></root>', symbolize_keys: true)
116
+ # @example Parse with symbolized names
117
+ # MultiXML.parse('<root><name>John</name></root>', symbolize_names: true)
73
118
  # #=> {root: {name: "John"}}
74
119
  def parse(xml, options = {})
75
- options = DEFAULT_OPTIONS.merge(options)
76
- xml_parser = options[:parser] ? resolve_parser(options.fetch(:parser)) : parser
77
-
120
+ call_site = OptionsNormalization.normalize_symbolize_option(options)
121
+ global = OptionsNormalization.normalize_symbolize_option(parse_options(call_site))
122
+ options = DEFAULT_OPTIONS.merge(global, call_site)
123
+ namespaces = validate_namespaces_mode(options.fetch(:namespaces))
78
124
  io = normalize_input(xml)
79
125
  return {} if io.eof?
80
126
 
81
- result = parse_with_error_handling(io, xml, xml_parser)
82
- result = typecast_xml_value(result, options.fetch(:disallowed_types)) if options.fetch(:typecast_xml_value)
83
- result = symbolize_keys(result) if options.fetch(:symbolize_keys)
84
- result
85
- end
86
-
87
- private
88
-
89
- # Resolve a parser specification to a module
90
- #
91
- # @api private
92
- # @param spec [Symbol, String, Class, Module] Parser specification
93
- # @return [Module] Resolved parser module
94
- # @raise [RuntimeError] if spec is invalid
95
- def resolve_parser(spec)
96
- case spec
97
- when String, Symbol then load_parser(spec)
98
- when Module then spec
99
- else raise "Invalid parser specification: expected Symbol, String, or Module"
100
- end
101
- end
102
-
103
- # Load a parser by name
104
- #
105
- # @api private
106
- # @param name [Symbol, String] Parser name
107
- # @return [Module] Loaded parser module
108
- def load_parser(name)
109
- name = name.to_s.downcase
110
- require "multi_xml/parsers/#{name}"
111
- Parsers.const_get(camelize(name))
112
- end
113
-
114
- # Convert underscored string to CamelCase
115
- #
116
- # @api private
117
- # @param name [String] Underscored string
118
- # @return [String] CamelCased string
119
- def camelize(name)
120
- name.split("_").map(&:capitalize).join
121
- end
122
-
123
- # Detect the best available parser
124
- #
125
- # @api private
126
- # @return [Symbol] Parser name
127
- # @raise [NoParserError] if no parser is available
128
- def detect_parser
129
- find_loaded_parser || find_available_parser || raise_no_parser_error
127
+ result = parse_with_error_handling(io, xml, resolve_parse_parser(options), namespaces)
128
+ apply_postprocessing(result, options)
130
129
  end
130
+ end
131
131
 
132
- # Parser constant names mapped to their symbols, in preference order
133
- #
134
- # @api private
135
- LOADED_PARSER_CHECKS = {
136
- Ox: :ox,
137
- LibXML: :libxml,
138
- Nokogiri: :nokogiri,
139
- Oga: :oga
140
- }.freeze
141
- private_constant :LOADED_PARSER_CHECKS
132
+ # Execute a block with a temporarily-swapped parser
133
+ #
134
+ # The override is stored in fiber-local storage so concurrent fibers
135
+ # and threads each see their own parser without racing on a shared
136
+ # module variable; nested calls save and restore the previous
137
+ # fiber-local value. Matches {MultiJSON.with_adapter}.
138
+ #
139
+ # @api public
140
+ # @param new_parser [Symbol, String, Module] parser to use
141
+ # @yield block to execute with the temporary parser
142
+ # @return [Object] result of the block
143
+ # @example
144
+ # MultiXML.with_parser(:rexml) { MultiXML.parse("<a>1</a>") }
145
+ def self.with_parser(new_parser)
146
+ previous_override = Fiber[:multi_xml_parser]
147
+ Fiber[:multi_xml_parser] = resolve_parser(new_parser)
148
+ yield
149
+ ensure
150
+ Fiber[:multi_xml_parser] = previous_override
151
+ end
152
+ end
142
153
 
143
- # Find an already-loaded parser library
144
- #
145
- # @api private
146
- # @return [Symbol, nil] Parser name or nil if none loaded
147
- def find_loaded_parser
148
- LOADED_PARSER_CHECKS.each do |const_name, parser_name|
149
- return parser_name if const_defined?(const_name)
150
- end
151
- nil
152
- end
154
+ require_relative "multi_xml/deprecated"
153
155
 
154
- # Try to load and find an available parser
156
+ # Backward-compatible alias for the legacy MultiXml constant name
157
+ #
158
+ # Downstream code that still writes MultiXml.parse(...) or
159
+ # rescue MultiXml::ParseError continues to work, but emits a one-time
160
+ # deprecation warning pointing at MultiXML. The module forwards every
161
+ # method call to {MultiXML} via {.method_missing} and resolves constant
162
+ # access via {.const_missing}, so both dotted calls and :: constant
163
+ # lookups (including rescue clauses) route through the canonical module.
164
+ #
165
+ # @api public
166
+ # @deprecated Use {MultiXML} (all-caps) instead. Will be removed in v1.0.
167
+ module MultiXml
168
+ class << self
169
+ # Forward method calls to {MultiXML}, emitting a one-time warning
155
170
  #
156
- # @api private
157
- # @return [Symbol, nil] Parser name or nil if none available
158
- def find_available_parser
159
- PARSER_PREFERENCE.each do |library, parser_name|
160
- return parser_name if try_require(library)
171
+ # @api public
172
+ # @return [Object] the delegated call's return value
173
+ # @example
174
+ # MultiXml.parse("<a>1</a>") # delegates to MultiXML.parse
175
+ # rubocop:disable Naming/BlockForwarding, Style/ArgumentsForwarding
176
+ def method_missing(name, *args, **kwargs, &block)
177
+ ::MultiXML.warn_deprecation_once(:multi_xml_constant,
178
+ "The MultiXml constant is deprecated and will be removed in v1.0. Use MultiXML instead.")
179
+ if ::MultiXML.respond_to?(name)
180
+ ::MultiXML.public_send(name, *args, **kwargs, &block)
181
+ else
182
+ super
161
183
  end
162
- nil
163
184
  end
185
+ # rubocop:enable Naming/BlockForwarding, Style/ArgumentsForwarding
164
186
 
165
- # Attempt to require a library
187
+ # Respond to any method {MultiXML} responds to
166
188
  #
167
- # @api private
168
- # @param library [String] Library to require
169
- # @return [Boolean] true if successful, false if LoadError
170
- def try_require(library)
171
- require library
172
- true
173
- rescue LoadError
174
- false
175
- end
176
-
177
- # Raise an error indicating no parser is available
178
- #
179
- # @api private
180
- # @return [void]
181
- # @raise [NoParserError] always
182
- def raise_no_parser_error
183
- raise NoParserError, <<~MSG.chomp
184
- No XML parser detected. Install one of: ox, nokogiri, libxml-ruby, or oga.
185
- See https://github.com/sferik/multi_xml for more information.
186
- MSG
189
+ # @api public
190
+ # @param name [Symbol] method name
191
+ # @param include_private [Boolean] include private methods
192
+ # @return [Boolean] true if {MultiXML} responds to the method
193
+ # @example
194
+ # MultiXml.respond_to?(:parse) #=> true
195
+ def respond_to_missing?(name, include_private)
196
+ ::MultiXML.respond_to?(name, include_private)
187
197
  end
188
198
 
189
- # Normalize input to an IO-like object
199
+ # Resolve missing constants to their {MultiXML} counterparts
190
200
  #
191
- # @api private
192
- # @param xml [String, IO] Input to normalize
193
- # @return [IO] IO-like object
194
- def normalize_input(xml)
195
- return xml if xml.respond_to?(:read)
196
-
197
- StringIO.new(xml.to_s.strip)
198
- end
199
-
200
- # Parse XML with error handling and key normalization
201
+ # The lookup is performed with ``inherit: false`` so a stray
202
+ # top-level ``::ParseError`` constant in the host process (Racc
203
+ # defines one when Nokogiri is loaded) is correctly ignored. Enables
204
+ # rescue MultiXml::ParseError and MultiXml::Parsers::Ox to keep
205
+ # working during the deprecation cycle.
201
206
  #
202
- # @api private
203
- # @param io [IO] IO-like object containing XML
204
- # @param original_input [String, IO] Original input for error reporting
205
- # @param xml_parser [Module] Parser to use
206
- # @return [Hash] Parsed XML with undasherized keys
207
- # @raise [ParseError] if XML is malformed
208
- def parse_with_error_handling(io, original_input, xml_parser)
209
- undasherize_keys(xml_parser.parse(io) || {})
210
- rescue xml_parser.parse_error => e
211
- xml_string = original_input.respond_to?(:read) ? original_input.tap(&:rewind).read : original_input.to_s
212
- raise(ParseError.new(e, xml: xml_string, cause: e))
207
+ # @api public
208
+ # @param name [Symbol] constant name
209
+ # @return [Object] the resolved constant from {MultiXML}
210
+ # @example
211
+ # MultiXml::Parsers::Ox # returns MultiXML::Parsers::Ox
212
+ def const_missing(name)
213
+ ::MultiXML.warn_deprecation_once(:multi_xml_constant,
214
+ "The MultiXml constant is deprecated and will be removed in v1.0. Use MultiXML instead.")
215
+ ::MultiXML.const_get(name, false)
213
216
  end
214
217
  end
215
218
  end
data/sig/multi_xml.rbs CHANGED
@@ -1,8 +1,8 @@
1
- # Type signatures for MultiXml
1
+ # Type signatures for MultiXML
2
2
 
3
3
  # Recursive type alias for parsed XML values
4
4
  # XML parsing produces nested structures of hashes, arrays, and primitive values
5
- type MultiXml::xmlValue = String
5
+ type MultiXML::xmlValue = String
6
6
  | Integer
7
7
  | Float
8
8
  | bool
@@ -12,20 +12,25 @@ type MultiXml::xmlValue = String
12
12
  | BigDecimal
13
13
  | StringIO
14
14
  | nil
15
- | Array[MultiXml::xmlValue]
16
- | Hash[String, MultiXml::xmlValue]
17
- | Hash[Symbol, MultiXml::xmlValue]
15
+ | Array[MultiXML::xmlValue]
16
+ | Hash[String, MultiXML::xmlValue]
17
+ | Hash[Symbol, MultiXML::xmlValue]
18
18
 
19
19
  # Type for hash with string keys used internally during parsing
20
- type MultiXml::xmlHash = Hash[String, MultiXml::xmlValue]
20
+ type MultiXML::xmlHash = Hash[String, MultiXML::xmlValue]
21
21
 
22
22
  # Interface for parser modules
23
- interface MultiXml::_Parser
24
- def parse: (StringIO io) -> MultiXml::xmlHash?
23
+ interface MultiXML::_Parser
24
+ def parse: ((IO | StringIO) io) -> MultiXML::xmlHash?
25
+ | ((IO | StringIO) io, ?namespaces: Symbol) -> MultiXML::xmlHash?
25
26
  def parse_error: () -> singleton(Exception)
26
27
  end
27
28
 
28
- module MultiXml
29
+ module MultiXML
30
+ extend Options
31
+
32
+ type parse_options_input = Hash[Symbol, untyped] | ^() -> Hash[Symbol, untyped] | ^(Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
33
+
29
34
  VERSION: Gem::Version
30
35
 
31
36
  TEXT_CONTENT_KEY: String
@@ -36,13 +41,17 @@ module MultiXml
36
41
 
37
42
  FALSE_BOOLEAN_VALUES: Set[String]
38
43
 
39
- DEFAULT_OPTIONS: Hash[Symbol, bool | Array[String]]
44
+ NAMESPACE_MODES: Array[Symbol]
45
+
46
+ DEFAULT_OPTIONS: Hash[Symbol, bool | Array[String] | Symbol]
40
47
 
41
48
  # Array of [library_name, parser_symbol] pairs
42
49
  PARSER_PREFERENCE: Array[Array[String | Symbol]]
43
50
 
44
51
  PARSE_DATETIME: ^(String) -> Time
45
52
 
53
+ ISO_WEEK_DATE: Regexp
54
+
46
55
  # Lambda for creating file-like StringIO from base64 content
47
56
  # Uses untyped for content because unpack1 returns various types
48
57
  # Uses untyped for entity because hash values are xmlValue but we access specific String keys
@@ -52,12 +61,40 @@ module MultiXml
52
61
  # Uses untyped key because hash["type"] returns xmlValue, and Hash#[] with non-String returns nil
53
62
  TYPE_CONVERTERS: Hash[untyped, Proc | Method]
54
63
 
55
- LOADED_PARSER_CHECKS: Hash[Symbol, Symbol]
64
+ DEPRECATION_WARNINGS_SHOWN: Set[Symbol]
56
65
 
57
66
  self.@parser: Module
58
67
 
59
68
  extend Helpers
60
69
 
70
+ module Concurrency
71
+ MUTEXES: Hash[Symbol, Thread::Mutex]
72
+
73
+ def self.synchronize: [T] (Symbol name) { () -> T } -> T
74
+ end
75
+
76
+ module Options
77
+ EMPTY_OPTIONS: Hash[Symbol, untyped]
78
+
79
+ def parse_options=: (parse_options_input options) -> parse_options_input
80
+
81
+ def parse_options: (*untyped args) -> Hash[Symbol, untyped]
82
+ end
83
+
84
+ module Parser
85
+ def parse_error: () -> Class
86
+ end
87
+
88
+ module ParserResolution
89
+ LOADED_PARSER_CHECKS: Hash[Symbol, Symbol]
90
+ end
91
+
92
+ module ParseSupport
93
+ end
94
+
95
+ # Emit a deprecation warning at most once per process
96
+ def self.warn_deprecation_once: (Symbol key, String message) -> void
97
+
61
98
  # Public API: Get the current XML parser module
62
99
  def self.parser: () -> Module
63
100
 
@@ -68,11 +105,19 @@ module MultiXml
68
105
  # Uses untyped for options because values vary by key (:parser, :symbolize_keys, :disallowed_types, :typecast_xml_value)
69
106
  def self.parse: (String | StringIO xml, ?Hash[Symbol, untyped] options) -> xmlHash
70
107
 
108
+ def self.parse_iso_week_datetime: (String string) -> Time
109
+ # Public API: Execute a block with a temporarily-swapped parser
110
+ def self.with_parser: [T] (Symbol | String | Module new_parser) { () -> T } -> T
111
+ # Deprecated alias for `parse`
112
+ def self.load: (String | StringIO xml, ?Hash[Symbol, untyped] options) -> xmlHash
71
113
  private
72
114
 
73
115
  # Resolve a parser specification (Symbol, String, or Module) to a parser
74
116
  def self.resolve_parser: (Symbol | String | Module spec) -> Module
75
117
 
118
+ # Validate that a parser satisfies the contract
119
+ def self.validate_parser!: (Module parser) -> Module
120
+
76
121
  # Load a parser module by name
77
122
  def self.load_parser: (Symbol | String name) -> Module
78
123
 
@@ -101,7 +146,30 @@ module MultiXml
101
146
 
102
147
  # Parse with error handling and key normalization
103
148
  # xml_parser implements _Parser interface; original_input uses respond_to? duck typing
104
- def self.parse_with_error_handling: (StringIO io, untyped original_input, untyped xml_parser) -> xmlHash
149
+ def self.parse_with_error_handling: ((IO | StringIO) io, untyped original_input, untyped xml_parser, Symbol namespaces) -> xmlHash
150
+
151
+ # Dispatch to parsers that may or may not accept `namespaces:`
152
+ def self.parse_with_namespaces_compatibility: ((IO | StringIO) io, untyped xml_parser, Symbol namespaces) -> xmlHash?
153
+
154
+ # Validate the :namespaces option value
155
+ def self.validate_namespaces_mode: (untyped mode) -> Symbol
156
+
157
+ # Resolve which parser this parse call should use, honoring the :parser option
158
+ def self.resolve_parse_parser: (Hash[Symbol, untyped] options) -> Module
159
+
160
+ # Check whether a parser accepts the `namespaces:` keyword
161
+ def self.parser_supports_namespaces_keyword?: (untyped xml_parser) -> bool
162
+
163
+ # Apply typecasting and key-symbolization as configured
164
+ def self.apply_postprocessing: (xmlHash result, Hash[Symbol, untyped] options) -> xmlHash
165
+
166
+ module OptionsNormalization
167
+ # Translate the deprecated :symbolize_keys option to :symbolize_names
168
+ def self.normalize_symbolize_option: (Hash[Symbol, untyped] options) -> Hash[Symbol, untyped]
169
+ end
170
+
171
+ # Define a deprecated alias that delegates to a new method name
172
+ def self.deprecate_alias: (Symbol name, Symbol replacement) -> Symbol
105
173
 
106
174
  module Helpers
107
175
  # Recursively convert all hash keys to symbols
@@ -207,6 +275,12 @@ module MultiXml
207
275
  class NoParserError < StandardError
208
276
  end
209
277
 
278
+ class ParserLoadError < ArgumentError
279
+ def initialize: (?(String | nil) message, ?cause: Exception?) -> void
280
+
281
+ def self.build: (Exception original_exception) -> ParserLoadError
282
+ end
283
+
210
284
  class DisallowedTypeError < StandardError
211
285
  @type: String
212
286
 
@@ -220,8 +294,11 @@ module MultiXml
220
294
  end
221
295
  end
222
296
 
223
- # Stub for Psych::SyntaxError which is part of the yaml library
224
- module Psych
225
- class SyntaxError < ::StandardError
226
- end
297
+ # Backward-compatible alias for the legacy MultiXml constant name
298
+ module MultiXml
299
+ def self.method_missing: (Symbol name, *untyped args, **untyped kwargs) ?{ (*untyped) -> untyped } -> untyped
300
+
301
+ def self.respond_to_missing?: ((Symbol | String) name, bool include_private) -> bool
302
+
303
+ def self.const_missing: (Symbol name) -> untyped
227
304
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multi_xml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
@@ -46,11 +46,20 @@ files:
46
46
  - README.md
47
47
  - Rakefile
48
48
  - Steepfile
49
+ - benchmark.rb
50
+ - benchmark/overall_parser_benchmark.rb
49
51
  - lib/multi_xml.rb
52
+ - lib/multi_xml/concurrency.rb
50
53
  - lib/multi_xml/constants.rb
54
+ - lib/multi_xml/deprecated.rb
51
55
  - lib/multi_xml/errors.rb
52
56
  - lib/multi_xml/file_like.rb
53
57
  - lib/multi_xml/helpers.rb
58
+ - lib/multi_xml/options.rb
59
+ - lib/multi_xml/options_normalization.rb
60
+ - lib/multi_xml/parse_support.rb
61
+ - lib/multi_xml/parser.rb
62
+ - lib/multi_xml/parser_resolution.rb
54
63
  - lib/multi_xml/parsers/dom_parser.rb
55
64
  - lib/multi_xml/parsers/libxml.rb
56
65
  - lib/multi_xml/parsers/libxml_sax.rb
@@ -88,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
97
  - !ruby/object:Gem::Version
89
98
  version: '0'
90
99
  requirements: []
91
- rubygems_version: 4.0.3
100
+ rubygems_version: 4.0.11
92
101
  specification_version: 4
93
102
  summary: Provides swappable XML backends utilizing LibXML, Nokogiri, Ox, or REXML.
94
103
  test_files: []