multi_xml 0.6.0 → 0.9.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: add5ff8df4d736d143b1d83c38c5f1b8b35d21cd
4
- data.tar.gz: ec8e8a3657ff340a9a4abefa2561e44dfee8371d
2
+ SHA256:
3
+ metadata.gz: d8e4560022692cf83853b50994663cbabfc0bdb943e78c4c2288dff38869bd08
4
+ data.tar.gz: d25368ffc55f43108708b46cc228e7d4506f44d61cb4d371cdb549fe36bfea2b
5
5
  SHA512:
6
- metadata.gz: 1cb437e88276aa09e33c5b10a1e32bbf181ecae9543976ab13acc4ca458b00b1315a59f1e3e1cd95fd24e47559a85058585578ba16441ac83c98ffa81c5ee902
7
- data.tar.gz: 1931768faabe059174c01e24d12f9b1c8bcc0c9717891ec9ba9098f55c98535ad2a0efab7dcc4bfd756a569f137bf86e4b45b652a182652e6a468393103de9f5
6
+ metadata.gz: 82885e626a6961ffaa1f34d039ba1a4ac0017c917c46f8232413f020252f9b88873643e44ca7c2b5ad1a7e510c2bccaf3757c246c4c2df801600048ee440f4c1
7
+ data.tar.gz: 96ea0d4b0947cdeb5230f07687d94be756379a80db7f7fd5e0484ff5a2b1cbc3407eee3c2ea3dce6e49d2595b0daa2f5ab404d20fda5d71fc8dd9bc0bbc7a748
data/.mutant.yml ADDED
@@ -0,0 +1,21 @@
1
+ usage: opensource
2
+
3
+ integration:
4
+ name: minitest
5
+
6
+ includes:
7
+ - lib
8
+ - test
9
+
10
+ requires:
11
+ - multi_xml
12
+ - mutant/minitest/coverage
13
+
14
+ matcher:
15
+ subjects:
16
+ - MultiXML*
17
+ ignore:
18
+ # Platform-specific code path: only triggers on TruffleRuby. Mutant
19
+ # runs on MRI, so any mutation that touches the RUBY_ENGINE check
20
+ # produces the same observable behavior as the original.
21
+ - MultiXML::ParserResolution#skip_on_platform?
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --order random
data/.rubocop.yml ADDED
@@ -0,0 +1,65 @@
1
+ require:
2
+ - standard
3
+
4
+ plugins:
5
+ - rubocop-performance
6
+ - rubocop-rake
7
+ - rubocop-minitest
8
+ - standard-performance
9
+
10
+ AllCops:
11
+ NewCops: enable
12
+ TargetRubyVersion: 3.2
13
+
14
+ Layout/ArgumentAlignment:
15
+ EnforcedStyle: with_fixed_indentation
16
+ IndentationWidth: 2
17
+
18
+ Layout/ArrayAlignment:
19
+ EnforcedStyle: with_fixed_indentation
20
+
21
+ Layout/CaseIndentation:
22
+ EnforcedStyle: end
23
+
24
+ Layout/EndAlignment:
25
+ EnforcedStyleAlignWith: start_of_line
26
+
27
+ Layout/LineLength:
28
+ Max: 140
29
+
30
+ Layout/ParameterAlignment:
31
+ EnforcedStyle: with_fixed_indentation
32
+ IndentationWidth: 2
33
+
34
+ Layout/SpaceInsideHashLiteralBraces:
35
+ EnforcedStyle: no_space
36
+
37
+ Metrics/ParameterLists:
38
+ CountKeywordArgs: false
39
+
40
+ Style/Alias:
41
+ EnforcedStyle: prefer_alias_method
42
+
43
+ Style/EmptyMethod:
44
+ EnforcedStyle: expanded
45
+
46
+ Style/FrozenStringLiteralComment:
47
+ EnforcedStyle: never
48
+
49
+ Style/RedundantConstantBase:
50
+ Enabled: false
51
+
52
+ Style/RescueStandardError:
53
+ EnforcedStyle: implicit
54
+
55
+ Style/StringLiterals:
56
+ EnforcedStyle: double_quotes
57
+
58
+ Style/StringLiteralsInInterpolation:
59
+ EnforcedStyle: double_quotes
60
+
61
+ Style/SymbolProc:
62
+ Enabled: false
63
+
64
+ Style/TernaryParentheses:
65
+ EnforcedStyle: require_parentheses_when_complex
data/CHANGELOG.md CHANGED
@@ -1,3 +1,65 @@
1
+ 0.9.1
2
+ -----
3
+ * Fix `MultiXml.method(:load)` resolving to `Kernel#load`. The legacy `MultiXml` constant forwarded calls to `MultiXML` via `method_missing`, but `Module#method` doesn't consult `method_missing`, so `MultiXml.method(:load)` resolved to the inherited `Kernel#load` and crashed with `LoadError` when invoked. Replaces `method_missing` with explicit singleton-method forwarders for every public method on `MultiXML`, mirroring the [analogous fix in MultiJSON](https://github.com/sferik/multi_json/issues/66).
4
+
5
+ 0.9.0
6
+ -----
7
+ * Add `MultiXML.with_parser` for fiber-local scoped parser overrides, matching `MultiJSON.with_adapter`. The override lives in `Fiber[:multi_xml_parser]`, so concurrent fibers and threads each see their own parser without racing on a shared module variable; nested calls save and restore the previous value.
8
+ * Add `MultiXML.parse_options` / `MultiXML.parse_options=` for process-wide default options, matching `MultiJSON.parse_options`. Accepts a `Hash` or a callable (`Proc`/`lambda`); a callable receives the call-site hash as its sole positional argument so defaults can be computed per-call. Defaults merge between `DEFAULT_OPTIONS` and call-site overrides.
9
+ * Introduce `MultiXML::Parser` base module — built-in parsers declare their backend exception class via a `ParseError` constant, matching the `MultiJSON::Adapter` convention. Custom parsers can either extend `MultiXML::Parser` and define `ParseError` or keep defining a `.parse_error` method directly; both styles are accepted.
10
+ * Add `MultiXML::ParserLoadError`, raised when the parser spec is invalid, requiring the parser file raises `LoadError`, or the resolved parser doesn't satisfy the contract (must respond to `.parse` and define either a `ParseError` constant or a `.parse_error` method). Inherits from `ArgumentError` and carries the original exception's class name in its message, matching `MultiJSON::AdapterError`.
11
+ * Rename `MultiXml` constant to `MultiXML` (all caps), matching the style of `MultiJSON`. The old `MultiXml` constant continues to work but emits a one-time deprecation warning on first use and will be removed in v1.0.
12
+ * Add `MultiXML.load` as a deprecated alias for `MultiXML.parse`, matching the style of `MultiJSON.load` → `MultiJSON.parse`. Will be removed in v1.0.
13
+ * Rename the `:symbolize_keys` option to `:symbolize_names`, matching Ruby stdlib's `JSON.parse` and MultiJSON. The old option continues to work but emits a one-time deprecation warning; it will be removed in v1.0.
14
+ * [Add `:namespaces` option to `MultiXML.parse` for consistent namespace handling across parsers](https://github.com/sferik/multi_xml/issues/44) — two modes produce byte-identical output on every backend:
15
+ * `:strip` (default) — drop xmlns declarations and prefixes; keeps today's libxml/nokogiri output so most users see no change
16
+ * `:preserve` — keep source prefixes (e.g. `"atom:rel"`) and surface `xmlns` / `xmlns:*` declarations as attributes
17
+ * Fix REXML keeping attribute prefixes (`"gd:etag"`) while other backends stripped them ([#31](https://github.com/sferik/multi_xml/issues/31))
18
+ * Fix Ox prepending namespace prefixes to element names (`"aws:Item"`) when other backends didn't ([#30](https://github.com/sferik/multi_xml/issues/30))
19
+ * Handle namespaced attribute name collisions consistently across backends. When attributes with different prefixes strip to the same local name (e.g. `foo:id` and `bar:id` both becoming `id`), values are collected in an array in document order, with attribute values ahead of any colliding child elements. The libxml SAX parser falls back to its DOM backend in this case since the SAX callback drops attribute prefixes.
20
+ * Fix Ox mixed-content text aggregation in the SAX parser
21
+ * Raise `ArgumentError` on an unknown `:namespaces` mode
22
+ * `undasherize_keys` now runs only in `:strip` mode so prefixed keys aren't rewritten under `:preserve`
23
+ * Reorder `PARSER_PREFERENCE` so `oga` is tried before `rexml`, matching the throughput ranking in the bundled benchmark suite. Affects auto-detection only when neither `ox`, `libxml-ruby`, nor `nokogiri` is available; explicitly selecting a parser is unchanged.
24
+ * Use a TruffleRuby-specific `PARSER_PREFERENCE` ordering (`rexml`, `libxml`, `oga`, `nokogiri`) since TruffleRuby's JIT favors pure-Ruby parsers and penalizes FFI-bound ones. On other engines the default ordering is unchanged.
25
+ * Add a parser benchmark suite (`rake benchmark`) and document per-engine throughput rankings in the README. CI verifies that `PARSER_PREFERENCE` matches the benchmark ranking on MRI, JRuby, and TruffleRuby.
26
+ * Restore JRuby support (dropped in 0.8.0) and add TruffleRuby (native + JVM) to the CI matrix, matching the test coverage of MultiJSON. TruffleRuby is excluded from Windows runners since the `setup-ruby` action doesn't support it there.
27
+ * Add Ruby 4.0 to the CI matrix
28
+ * Support libxml-ruby 6.0.0 by switching from `require "libxml"` (removed in 6.0) to `require "libxml-ruby"`, which is present in both 5.x and 6.x
29
+ * Drop redundant `::Psych::SyntaxError` declaration from the RBS signature to fix a "Different superclasses are specified" type-checking error under rbs v4
30
+
31
+ 0.8.1
32
+ -----
33
+ * [Fix array unwrapping when elements contain nil](https://github.com/sferik/multi_xml/commit/09a875d832c45e2b567889398f45361ec9e36685)
34
+
35
+ 0.8.0
36
+ -----
37
+ * [Add per-parse :parser option to MultiXml.parse](https://github.com/sferik/multi_xml/commit/eb0c1ccadd9026980ba8b6dd0128d6862dc361c4)
38
+ * [Add SAX parsers for Nokogiri and LibXML](https://github.com/sferik/multi_xml/commit/5d67fe6cae3c1ef2c306f1e83fc91b9accfcb724)
39
+ * [Fix inconsistent whitespace handling across parsers](https://github.com/sferik/multi_xml/commit/55aa23f1c401e66984ad1c7d753c1b4258bf0dfd)
40
+ * [Make parsing errors inspectable with cause and xml accessors](https://github.com/sferik/multi_xml/commit/f676f1b657f3352a80ac171d9b839e41ad52a14d)
41
+ * [Drop support for JRuby](https://github.com/sferik/multi_xml/commit/27895ca3918c681ad7ddaa57c5cae7b8340bd601)
42
+
43
+ 0.7.2
44
+ -----
45
+ * [Drop support for Ruby 3.1](https://github.com/sferik/multi_xml/commit/fab6288edd36c58a2b13e0206d8bed305fcb4a4b)
46
+
47
+ 0.7.1
48
+ -----
49
+ * [Relax required Ruby version constraint to allow installation on Debian stable](https://github.com/sferik/multi_xml/commit/7d18711466a15e158dc71344ca6f6e18838ecc8d)
50
+
51
+ 0.7.0
52
+ -----
53
+ * [Add support for Ruby 3.3](https://github.com/sferik/multi_xml/pull/67)
54
+ * [Drop support for Ruby 3.0](https://github.com/sferik/multi_xml/commit/eec72c56307fede3a93f1a61553587cb278b0c8a) [and](https://github.com/sferik/multi_xml/commit/6a6dec80a36c30774a5525b45f71d346fb561e69) [earlier](https://github.com/sferik/multi_xml/commit/e7dad37a0a0be8383a26ffe515c575b5b4d04588)
55
+ * [Don't mutate strings](https://github.com/sferik/multi_xml/commit/71be3fff4afb0277a7e1c47c5f1f4b6106a8eb45)
56
+
57
+ 0.6.0
58
+ -----
59
+ * [Duplexed Streams](https://github.com/sferik/multi_xml/pull/45)
60
+ * [Support for Oga](https://github.com/sferik/multi_xml/pull/47)
61
+ * [Integer unification for Ruby 2.4](https://github.com/sferik/multi_xml/pull/54)
62
+
1
63
  0.5.5
2
64
  -----
3
65
  * [Fix symbolize_keys function](https://github.com/sferik/multi_xml/commit/a4cae3aeb690999287cd30206399abaa5ce1ae81)
data/Gemfile ADDED
@@ -0,0 +1,26 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "libxml-ruby", require: nil, platforms: :ruby
4
+ gem "nokogiri", require: nil
5
+ gem "oga", ">= 2.3", require: nil
6
+ gem "ox", require: nil, platforms: :ruby
7
+ gem "rexml", require: nil
8
+
9
+ gem "minitest", ">= 6"
10
+ gem "minitest-mock", ">= 5.27"
11
+ gem "mutant-minitest", ">= 0.14.1"
12
+ gem "rake", ">= 13.3.1"
13
+ gem "rbs", ">= 4.0.0", platforms: :ruby
14
+ gem "rdoc", ">= 7.0.2"
15
+ gem "rubocop", ">= 1.81.7"
16
+ gem "rubocop-minitest", ">= 0.36"
17
+ gem "rubocop-performance", ">= 1.26.1"
18
+ gem "rubocop-rake", ">= 0.7.1"
19
+ gem "simplecov", ">= 0.22"
20
+ gem "standard", ">= 1.52"
21
+ gem "standard-performance", ">= 1.9"
22
+ gem "steep", ">= 2", platforms: :ruby
23
+ gem "yard", ">= 0.9.38"
24
+ gem "yardstick", ">= 0.9.9"
25
+
26
+ gemspec
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010-2013 Erik Michaels-Ober
1
+ Copyright (c) 2010-2025 Erik Berlin
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,77 +1,175 @@
1
1
  # MultiXML
2
2
 
3
- [![Gem Version](http://img.shields.io/gem/v/multi_xml.svg)][gem]
4
- [![Build Status](http://img.shields.io/travis/sferik/multi_xml.svg)][travis]
5
- [![Dependency Status](http://img.shields.io/gemnasium/sferik/multi_xml.svg)][gemnasium]
6
- [![Code Climate](http://img.shields.io/codeclimate/github/sferik/multi_xml.svg)][codeclimate]
7
- [![Coverage Status](http://img.shields.io/coveralls/sferik/multi_xml.svg)][coveralls]
3
+ [![Tests](https://github.com/sferik/multi_xml/actions/workflows/tests.yml/badge.svg)][tests]
4
+ [![Linter](https://github.com/sferik/multi_xml/actions/workflows/linter.yml/badge.svg)][linter]
5
+ [![Mutant](https://github.com/sferik/multi_xml/actions/workflows/mutant.yml/badge.svg)][mutant]
6
+ [![Typecheck](https://github.com/sferik/multi_xml/actions/workflows/typecheck.yml/badge.svg)][typecheck]
7
+ [![Docs](https://github.com/sferik/multi_xml/actions/workflows/docs.yml/badge.svg)][docs]
8
+ [![Gem Version](https://badge.fury.io/rb/multi_xml.svg)][gem]
9
+
10
+ Lots of Ruby libraries parse XML and everyone has their favorite XML parser.
11
+ Instead of choosing a single XML parser and forcing users of your library to
12
+ be stuck with it, you can use MultiXML instead, which will simply choose the
13
+ fastest available XML parser. Here's how to use it:
8
14
 
9
- [gem]: https://rubygems.org/gems/multi_xml
10
- [travis]: http://travis-ci.org/sferik/multi_xml
11
- [gemnasium]: https://gemnasium.com/sferik/multi_xml
12
- [codeclimate]: https://codeclimate.com/github/sferik/multi_xml
13
- [coveralls]: https://coveralls.io/r/sferik/multi_xml
15
+ ```ruby
16
+ require "multi_xml"
17
+
18
+ MultiXML.parse("<tag>contents</tag>") #=> {"tag" => "contents"}
19
+ MultiXML.parse("<tag>contents</tag>", symbolize_names: true) #=> {tag: "contents"}
20
+ ```
21
+
22
+ `MultiXML.parse` returns `{}` for empty and whitespace-only inputs instead of
23
+ raising, so a missing or blank payload is observable as an empty hash rather
24
+ than an exception. When parsing invalid XML, MultiXML will throw a
25
+ `MultiXML::ParseError`.
26
+
27
+ ```ruby
28
+ begin
29
+ MultiXML.parse("<open></close>")
30
+ rescue MultiXML::ParseError => exception
31
+ exception.xml #=> "<open></close>"
32
+ exception.cause #=> Nokogiri::XML::SyntaxError: ...
33
+ end
34
+ ```
35
+
36
+ ### Deprecated in 0.9.0
37
+
38
+ The module constant, the primary parse entry point, and the
39
+ symbolize-keys option were renamed to align MultiXML with MultiJSON
40
+ and Ruby stdlib `JSON.parse`. The old names still work in 0.x but
41
+ now emit a one-time deprecation warning; they will be removed in 1.0.
14
42
 
15
- A generic swappable back-end for XML parsing
43
+ | Deprecated | Use instead |
44
+ | ----------------------------- | ------------------------------- |
45
+ | `MultiXml` (constant) | `MultiXML` (all-caps) |
46
+ | `MultiXML.load(xml)` | `MultiXML.parse(xml)` |
47
+ | `symbolize_keys:` option | `symbolize_names:` option |
16
48
 
17
- ## Installation
18
- gem install multi_xml
49
+ The `MultiXml` constant (CamelCase) continues to work as a thin
50
+ delegator; every method call, constant lookup, and rescue clause
51
+ routes through `MultiXML` transparently.
19
52
 
20
- ## Documentation
21
- [http://rdoc.info/gems/multi_xml][documentation]
53
+ `ParseError` instances expose `xml` and `cause` readers. `xml` contains the
54
+ input that caused the problem; `cause` contains the original exception raised
55
+ by the underlying parser.
22
56
 
23
- [documentation]: http://rdoc.info/gems/multi_xml
57
+ ### Writing a custom parser
58
+
59
+ A custom parser is any class (or module) that responds to two class methods:
24
60
 
25
- ## Usage Examples
26
- Lots of Ruby libraries utilize XML parsing in some form, and everyone has their
27
- favorite XML library. In order to best support multiple XML parsers and
28
- libraries, `multi_xml` is a general-purpose swappable XML backend library. You
29
- use it like so:
30
61
  ```ruby
31
- require 'multi_xml'
62
+ class MyParser
63
+ def self.parse(io, namespaces: :strip)
64
+ # parse the IO-like object into a Hash, raising ParseError on failure
65
+ end
32
66
 
33
- MultiXml.parser = :ox
34
- MultiXml.parser = MultiXml::Parsers::Ox # Same as above
35
- MultiXml.parse('<tag>This is the contents</tag>') # Parsed using Ox
67
+ def self.parse_error
68
+ MyParser::ParseError
69
+ end
70
+ end
36
71
 
37
- MultiXml.parser = :libxml
38
- MultiXml.parser = MultiXml::Parsers::Libxml # Same as above
39
- MultiXml.parse('<tag>This is the contents</tag>') # Parsed using LibXML
72
+ MultiXML.parser = MyParser
73
+ ```
40
74
 
41
- MultiXml.parser = :nokogiri
42
- MultiXml.parser = MultiXml::Parsers::Nokogiri # Same as above
43
- MultiXml.parse('<tag>This is the contents</tag>') # Parsed using Nokogiri
75
+ `parse_error` is required: `MultiXML.parse` rescues `MyParser.parse_error`
76
+ to wrap parse failures in `MultiXML::ParseError`. The built-in parsers in
77
+ `lib/multi_xml/parsers/` are working examples.
78
+
79
+ MultiXML tries to have intelligent defaulting. If any supported library is
80
+ already loaded, MultiXML uses it before attempting to load others. When no
81
+ backend is preloaded, MultiXML walks its automatic preference list and uses the first
82
+ one that loads successfully:
83
+
84
+ 1. [`ox`][ox]
85
+ 2. [`libxml-ruby`][libxml-ruby]
86
+ 3. [`nokogiri`][nokogiri]
87
+ 4. [`oga`][oga]
88
+ 5. [`rexml`][rexml]
89
+
90
+ This is the library's built-in default selection order, not a guarantee that
91
+ the list is globally fastest for every workload. Real-world performance depends
92
+ on the document shape and the Ruby implementation, and the benchmark suite
93
+ below also measures SAX backends that are not part of automatic parser
94
+ detection. REXML is a Ruby default gem, so it's always available as a
95
+ last-resort fallback on any supported Ruby. If you have a workload where a
96
+ different backend is faster, set it explicitly with
97
+ `MultiXML.parser = :your_parser`.
98
+
99
+ ## Benchmarking Parsers
100
+
101
+ This repo includes a benchmark suite that compares every available built-in
102
+ backend across multiple XML shapes and sizes instead of relying on a single
103
+ synthetic document. The workloads cover:
104
+
105
+ - shallow and wide XML
106
+ - deeply nested XML
107
+ - record batches with repeated siblings
108
+ - attribute-dense elements
109
+ - mixed-content sections
110
+ - namespace-heavy feeds
111
+ - a large catalog-style document
112
+
113
+ Run the full benchmark with:
114
+
115
+ ```bash
116
+ bundle exec rake benchmark
117
+ ```
44
118
 
45
- MultiXml.parser = :rexml
46
- MultiXml.parser = MultiXml::Parsers::Rexml # Same as above
47
- MultiXml.parse('<tag>This is the contents</tag>') # Parsed using REXML
119
+ You can also run the script directly for shorter runs or Markdown-friendly
120
+ output:
48
121
 
49
- MultiXml.parser = :oga
50
- MultiXml.parser = MultiXml::Parsers::Oga # Same as above
51
- MultiXml.parse('<tag>This is the contents</tag>') # Parsed using Oga
122
+ ```bash
123
+ bundle exec ruby benchmark.rb --quick
124
+ bundle exec ruby benchmark.rb --format=markdown
52
125
  ```
53
- The `parser` setter takes either a symbol or a class (to allow for custom XML
54
- parsers) that responds to `.parse` at the class level.
55
126
 
56
- MultiXML tries to have intelligent defaulting. That is, if you have any of the
57
- supported parsers already loaded, it will utilize them before attempting to
58
- load any. When loading, libraries are ordered by speed: first Ox, then LibXML,
59
- then Nokogiri, and finally REXML.
127
+ The output includes:
128
+
129
+ - a single best-overall parser based on the equal-weight geometric mean of
130
+ per-scenario relative throughput
131
+ - an overall ranking table for every parser
132
+ - a scenario matrix showing which parser won each workload
133
+ - an exclusions table when a parser crashes or produces mismatched output on a
134
+ valid workload
135
+
136
+ Allocation efficiency is reported as a secondary metric using allocated Ruby
137
+ objects per parse so ties on throughput are easier to interpret.
138
+
139
+ `PARSER_PREFERENCE` drives auto-detection (see "Configuration" above) and is
140
+ ordered fastest-first per the benchmark suite. CI re-runs the benchmark on
141
+ each supported runtime and fails if the observed ranking diverges from this
142
+ table:
143
+
144
+ | rank | CRuby/MRI | JRuby | TruffleRuby |
145
+ | ---- | ---------- | ---------- | ----------- |
146
+ | 1 | `ox` | — | — |
147
+ | 2 | `libxml` | — | `rexml` |
148
+ | 3 | `nokogiri` | `nokogiri` | `libxml` |
149
+ | 4 | `oga` | — | `oga` |
150
+ | 5 | `rexml` | `rexml` | `nokogiri` |
151
+
152
+ A dash means the parser isn't usable on that runtime. `ox` has no JRuby
153
+ build and is filtered out of TruffleRuby auto-detection (its SAX callbacks
154
+ miscompile under the JIT after warmup); `libxml-ruby` has no JRuby build;
155
+ `oga` 3.x crashes on JRuby 10 (its precompiled Java backend was built
156
+ against an older JRuby API). TruffleRuby's JIT inverts the FFI-vs-pure-Ruby
157
+ tradeoff for the remaining backends, so `rexml` rises to the top and
158
+ `nokogiri` falls to last.
60
159
 
61
160
  ## Supported Ruby Versions
62
- This library aims to support and is [tested against][travis] the following Ruby
63
- implementations:
64
161
 
65
- * Ruby 1.9.3
66
- * Ruby 2.0.0
67
- * Ruby 2.1
68
- * Ruby 2.2
69
- * Ruby 2.3
70
- * [JRuby 9000][jruby]
162
+ This library aims to support and is [tested against](https://github.com/sferik/multi_xml/actions/workflows/tests.yml) the following Ruby
163
+ implementations:
71
164
 
72
- [jruby]: http://jruby.org/
165
+ - Ruby 3.2
166
+ - Ruby 3.3
167
+ - Ruby 3.4
168
+ - Ruby 4.0
169
+ - [JRuby][jruby] 10.0 (targets Ruby 3.4 compatibility)
170
+ - [TruffleRuby][truffleruby] 33.0 (native and JVM)
73
171
 
74
- If something doesn't work on one of these interpreters, it's a bug.
172
+ If something doesn't work in one of these implementations, it's a bug.
75
173
 
76
174
  This library may inadvertently work (or seem to work) on other Ruby
77
175
  implementations, however support will only be provided for the versions listed
@@ -84,12 +182,38 @@ implementation, you will be responsible for providing patches in a timely
84
182
  fashion. If critical issues for a particular implementation exist at the time
85
183
  of a major release, support for that Ruby version may be dropped.
86
184
 
87
- ## Inspiration
88
- MultiXML was inspired by [MultiJSON][].
185
+ ## Versioning
89
186
 
90
- [multijson]: https://github.com/intridea/multi_json/
187
+ This library aims to adhere to [Semantic Versioning 2.0.0][semver]. Violations
188
+ of this scheme should be reported as bugs. Specifically, if a minor or patch
189
+ version is released that breaks backward compatibility, that version should be
190
+ immediately yanked and/or a new version should be immediately released that
191
+ restores compatibility. Breaking changes to the public API will only be
192
+ introduced with new major versions. As a result of this policy, you can (and
193
+ should) specify a dependency on this gem using the [Pessimistic Version
194
+ Constraint][pvc] with two digits of precision. For example:
195
+
196
+ ```ruby
197
+ spec.add_dependency "multi_xml", "~> 0.9"
198
+ ```
91
199
 
92
200
  ## Copyright
93
- Copyright (c) 2010-2013 Erik Michaels-Ober. See [LICENSE][] for details.
94
201
 
202
+ Copyright (c) 2010-2026 Erik Berlin. See [LICENSE][license] for details.
203
+
204
+ [docs]: https://github.com/sferik/multi_xml/actions/workflows/docs.yml
205
+ [gem]: https://rubygems.org/gems/multi_xml
206
+ [jruby]: http://www.jruby.org/
207
+ [libxml-ruby]: https://github.com/xml4r/libxml-ruby
95
208
  [license]: LICENSE.md
209
+ [linter]: https://github.com/sferik/multi_xml/actions/workflows/linter.yml
210
+ [mutant]: https://github.com/sferik/multi_xml/actions/workflows/mutant.yml
211
+ [nokogiri]: https://nokogiri.org/
212
+ [oga]: https://gitlab.com/yorickpeterse/oga
213
+ [ox]: https://github.com/ohler55/ox
214
+ [pvc]: http://docs.rubygems.org/read/chapter/16#page74
215
+ [rexml]: https://github.com/ruby/rexml
216
+ [semver]: http://semver.org/
217
+ [tests]: https://github.com/sferik/multi_xml/actions/workflows/tests.yml
218
+ [truffleruby]: https://www.graalvm.org/ruby/
219
+ [typecheck]: https://github.com/sferik/multi_xml/actions/workflows/typecheck.yml
data/Rakefile ADDED
@@ -0,0 +1,68 @@
1
+ require "bundler/gem_tasks"
2
+ require "shellwords"
3
+
4
+ # Override release task to skip gem push (handled by GitHub Actions with attestations)
5
+ Rake::Task["release"].clear
6
+ desc "Build gem and create tag (gem push handled by CI)"
7
+ task release: %w[build release:guard_clean release:source_control_push]
8
+
9
+ require "rake/testtask"
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << "test"
12
+ t.pattern = "test/**/*_test.rb"
13
+ end
14
+
15
+ require "standard/rake"
16
+ require "rubocop/rake_task"
17
+ RuboCop::RakeTask.new
18
+
19
+ require "yard"
20
+ YARD::Rake::YardocTask.new do |task|
21
+ task.files = ["lib/**/*.rb", "-", "LICENSE.md"]
22
+ task.options = [
23
+ "--no-private",
24
+ "--protected",
25
+ "--output-dir", "doc/yard",
26
+ "--markup", "markdown"
27
+ ]
28
+ end
29
+
30
+ require "yardstick/rake/measurement"
31
+ Yardstick::Rake::Measurement.new do |measurement|
32
+ measurement.output = "measurement/report.txt"
33
+ end
34
+
35
+ require "yardstick/rake/verify"
36
+ Yardstick::Rake::Verify.new do |verify|
37
+ verify.threshold = 100
38
+ end
39
+
40
+ # Steep requires native extensions not available on JRuby or Windows
41
+ unless RUBY_PLATFORM == "java" || Gem.win_platform?
42
+ require "steep/rake_task"
43
+ Steep::RakeTask.new
44
+ end
45
+
46
+ desc "Run linters"
47
+ task lint: %i[rubocop standard]
48
+
49
+ desc "Benchmark available XML parsers across representative workloads"
50
+ task :benchmark do
51
+ args = ENV["BENCHMARK_ARGS"] ? Shellwords.split(ENV["BENCHMARK_ARGS"]) : []
52
+ sh Gem.ruby, "benchmark.rb", *args
53
+ end
54
+
55
+ # Mutant uses fork() which is not available on Windows or JRuby
56
+ desc "Run mutation testing"
57
+ task :mutant do
58
+ if Gem.win_platform? || RUBY_PLATFORM == "java"
59
+ puts "Skipping mutant on Windows/JRuby (fork not supported)"
60
+ else
61
+ system("bundle", "exec", "mutant", "run") || exit(1)
62
+ end
63
+ end
64
+
65
+ default_tasks = %i[test lint verify_measurements mutant]
66
+ default_tasks << :steep unless RUBY_PLATFORM == "java" || Gem.win_platform?
67
+
68
+ task default: default_tasks
data/Steepfile ADDED
@@ -0,0 +1,29 @@
1
+ D = Steep::Diagnostic
2
+
3
+ target :lib do
4
+ signature "sig"
5
+
6
+ # Check core library files (excluding parser implementations that depend on optional gems)
7
+ check "lib/multi_xml.rb"
8
+ check "lib/multi_xml/constants.rb"
9
+ check "lib/multi_xml/errors.rb"
10
+ check "lib/multi_xml/file_like.rb"
11
+ check "lib/multi_xml/helpers.rb"
12
+ check "lib/multi_xml/version.rb"
13
+
14
+ # Use stdlib types
15
+ library "date"
16
+ library "time"
17
+ library "yaml"
18
+ library "bigdecimal"
19
+ library "stringio"
20
+
21
+ configure_code_diagnostics(D::Ruby.strict) do |hash|
22
+ # The fiber-local Fiber[] reader returns untyped — intentional
23
+ # for with_parser's previous_override save/restore.
24
+ hash[D::Ruby::FallbackAny] = :hint
25
+ # set_backtrace has three overloads and Steep can't pick one when
26
+ # given an `(Array[String] | nil)` union from `cause.backtrace`.
27
+ hash[D::Ruby::UnresolvedOverloading] = :hint
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../benchmark"
4
+
5
+ exit MultiXMLBenchmark.run(ARGV)