pax_global_header 0000666 0000000 0000000 00000000064 15127740662 0014524 g ustar 00root root 0000000 0000000 52 comment=3ad93f88b66ca3f8666ea92e3ae09007c2c2961c
multi_xml-0.8.1/ 0000775 0000000 0000000 00000000000 15127740662 0013544 5 ustar 00root root 0000000 0000000 multi_xml-0.8.1/.github/ 0000775 0000000 0000000 00000000000 15127740662 0015104 5 ustar 00root root 0000000 0000000 multi_xml-0.8.1/.github/FUNDING.yml 0000664 0000000 0000000 00000000021 15127740662 0016712 0 ustar 00root root 0000000 0000000 github: [sferik]
multi_xml-0.8.1/.github/workflows/ 0000775 0000000 0000000 00000000000 15127740662 0017141 5 ustar 00root root 0000000 0000000 multi_xml-0.8.1/.github/workflows/docs.yml 0000664 0000000 0000000 00000000551 15127740662 0020615 0 ustar 00root root 0000000 0000000 name: docs
on: [push, pull_request]
jobs:
yard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
bundler-cache: true
- name: Run YARD
run: bundle exec rake yard
- name: Verify Yardstick coverage
run: bundle exec rake verify_measurements
multi_xml-0.8.1/.github/workflows/linter.yml 0000664 0000000 0000000 00000000377 15127740662 0021170 0 ustar 00root root 0000000 0000000 name: linter
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
bundler-cache: true
- run: bundle exec rake lint
multi_xml-0.8.1/.github/workflows/mutant.yml 0000664 0000000 0000000 00000000762 15127740662 0021201 0 ustar 00root root 0000000 0000000 name: mutation tests
on:
push:
branches: [main]
paths-ignore:
- "**/*.md"
pull_request:
branches: [main]
paths-ignore:
- "**/*.md"
workflow_dispatch:
jobs:
mutant:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
bundler-cache: true
- name: Run mutation testing
run: bundle exec rake mutant
env:
MUTANT: "true"
multi_xml-0.8.1/.github/workflows/push.yml 0000664 0000000 0000000 00000001720 15127740662 0020643 0 ustar 00root root 0000000 0000000 name: Push gem to RubyGems
on:
push:
tags:
- "v*"
permissions:
contents: read
jobs:
push:
if: github.repository == 'sferik/multi_xml'
runs-on: ubuntu-latest
environment:
name: rubygems.org
url: https://rubygems.org/gems/multi_xml
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ruby
bundler-cache: true
- uses: rubygems/configure-rubygems-credentials@v1.0.0
- name: Update RubyGems
run: gem update --system
- name: Build gem
run: bundle exec rake build
- name: Sign gem with Sigstore
run: gem exec sigstore-cli sign pkg/*.gem --bundle pkg/multi_xml.gem.sigstore.json
- name: Push gem
run: gem push pkg/*.gem --attestation pkg/multi_xml.gem.sigstore.json
- name: Wait for release
run: gem exec rubygems-await pkg/*.gem
multi_xml-0.8.1/.github/workflows/tests.yml 0000664 0000000 0000000 00000001332 15127740662 0021025 0 ustar 00root root 0000000 0000000 name: tests
on:
push:
branches: [main]
paths-ignore:
- "**/*.md"
pull_request:
branches: [main]
paths-ignore:
- "**/*.md"
workflow_dispatch:
jobs:
test:
strategy:
matrix:
ruby-version: ["3.2", "3.3", "3.4"]
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: false # Do not bundle install yet, need recent versions for Windows
- name: Run tests
run: |
gem install bundler
bundle install
bundle exec rake test
multi_xml-0.8.1/.github/workflows/typecheck.yml 0000664 0000000 0000000 00000000403 15127740662 0021640 0 ustar 00root root 0000000 0000000 name: typecheck
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
bundler-cache: true
- run: bundle exec rake steep
multi_xml-0.8.1/.gitignore 0000664 0000000 0000000 00000000130 15127740662 0015526 0 ustar 00root root 0000000 0000000 *.gem
*~
.bundle
.rvmrc
.yardoc
Gemfile.lock
coverage/*
doc/*
log/*
measurement/*
pkg/*
multi_xml-0.8.1/.mutant.yml 0000664 0000000 0000000 00000000251 15127740662 0015653 0 ustar 00root root 0000000 0000000 usage: opensource
integration:
name: minitest
includes:
- lib
- test
requires:
- multi_xml
- mutant/minitest/coverage
matcher:
subjects:
- MultiXml*
multi_xml-0.8.1/.rspec 0000664 0000000 0000000 00000000027 15127740662 0014660 0 ustar 00root root 0000000 0000000 --color
--order random
multi_xml-0.8.1/.rubocop.yml 0000664 0000000 0000000 00000002211 15127740662 0016012 0 ustar 00root root 0000000 0000000 require:
- standard
plugins:
- rubocop-performance
- rubocop-rake
- rubocop-minitest
- standard-performance
AllCops:
NewCops: enable
TargetRubyVersion: 3.2
Layout/ArgumentAlignment:
EnforcedStyle: with_fixed_indentation
IndentationWidth: 2
Layout/ArrayAlignment:
EnforcedStyle: with_fixed_indentation
Layout/CaseIndentation:
EnforcedStyle: end
Layout/EndAlignment:
EnforcedStyleAlignWith: start_of_line
Layout/LineLength:
Max: 140
Layout/ParameterAlignment:
EnforcedStyle: with_fixed_indentation
IndentationWidth: 2
Layout/SpaceInsideHashLiteralBraces:
EnforcedStyle: no_space
Metrics/ParameterLists:
CountKeywordArgs: false
Style/Alias:
EnforcedStyle: prefer_alias_method
Style/EmptyMethod:
EnforcedStyle: expanded
Style/FrozenStringLiteralComment:
EnforcedStyle: never
Style/RedundantConstantBase:
Enabled: false
Style/RescueStandardError:
EnforcedStyle: implicit
Style/StringLiterals:
EnforcedStyle: double_quotes
Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes
Style/SymbolProc:
Enabled: false
Style/TernaryParentheses:
EnforcedStyle: require_parentheses_when_complex
multi_xml-0.8.1/.yardopts 0000664 0000000 0000000 00000000137 15127740662 0015413 0 ustar 00root root 0000000 0000000 --no-private
--protected
--markup markdown
-
CHANGELOG.md
CONTRIBUTING.md
LICENSE.md
README.md
multi_xml-0.8.1/CHANGELOG.md 0000664 0000000 0000000 00000013625 15127740662 0015364 0 ustar 00root root 0000000 0000000 0.8.1
-----
* [Fix array unwrapping when elements contain nil](https://github.com/sferik/multi_xml/commit/09a875d832c45e2b567889398f45361ec9e36685)
0.8.0
-----
* [Add per-parse :parser option to MultiXml.parse](https://github.com/sferik/multi_xml/commit/eb0c1ccadd9026980ba8b6dd0128d6862dc361c4)
* [Add SAX parsers for Nokogiri and LibXML](https://github.com/sferik/multi_xml/commit/5d67fe6cae3c1ef2c306f1e83fc91b9accfcb724)
* [Fix inconsistent whitespace handling across parsers](https://github.com/sferik/multi_xml/commit/55aa23f1c401e66984ad1c7d753c1b4258bf0dfd)
* [Make parsing errors inspectable with cause and xml accessors](https://github.com/sferik/multi_xml/commit/f676f1b657f3352a80ac171d9b839e41ad52a14d)
* [Drop support for JRuby](https://github.com/sferik/multi_xml/commit/27895ca3918c681ad7ddaa57c5cae7b8340bd601)
0.7.2
-----
* [Drop support for Ruby 3.1](https://github.com/sferik/multi_xml/commit/fab6288edd36c58a2b13e0206d8bed305fcb4a4b)
0.7.1
-----
* [Relax required Ruby version constraint to allow installation on Debian stable](https://github.com/sferik/multi_xml/commit/7d18711466a15e158dc71344ca6f6e18838ecc8d)
0.7.0
-----
* [Add support for Ruby 3.3](https://github.com/sferik/multi_xml/pull/67)
* [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)
* [Don't mutate strings](https://github.com/sferik/multi_xml/commit/71be3fff4afb0277a7e1c47c5f1f4b6106a8eb45)
0.6.0
-----
* [Duplexed Streams](https://github.com/sferik/multi_xml/pull/45)
* [Support for Oga](https://github.com/sferik/multi_xml/pull/47)
* [Integer unification for Ruby 2.4](https://github.com/sferik/multi_xml/pull/54)
0.5.5
-----
* [Fix symbolize_keys function](https://github.com/sferik/multi_xml/commit/a4cae3aeb690999287cd30206399abaa5ce1ae81)
* [Fix Nokogiri parser for the same attr and inner element name](https://github.com/sferik/multi_xml/commit/a28ed86e2d7826b2edeed98552736b4c7ca52726)
0.5.4
-----
* [Add option to not cast parsed values](https://github.com/sferik/multi_xml/commit/44fc05fbcfd60cc8b555b75212471fab29fa8cd0)
* [Use message instead of to_s](https://github.com/sferik/multi_xml/commit/b06f0114434ffe1957dd7bc2712cb5b76c1b45fe)
0.5.3
-----
* [Add cryptographic signature](https://github.com/sferik/multi_xml/commit/f39f0c74308090737816c622dbb7d7aa28c646c0)
0.5.2
-----
* [Remove ability to parse symbols and YAML](https://github.com/sferik/multi_xml/pull/34)
0.5.1
-----
* [Revert "Reset @@parser in between specs"](https://github.com/sferik/multi_xml/issues/28)
0.5.0
-----
* [Reset @@parser in between specs](https://github.com/sferik/multi_xml/commit/b562bed265918b43ac1c4c638ae3a7ffe95ecd83)
* [Add attributes being passed through on content nodes](https://github.com/sferik/multi_xml/commit/631a8bb3c2253db0024f77f47c16d5a53b8128fd)
0.4.4
-----
* [Fix regression in MultiXml.parse](https://github.com/sferik/multi_xml/commit/45ae597d9a35cbd89cc7f5518c85bac30199fc06)
0.4.3
-----
* [Make parser a class variable](https://github.com/sferik/multi_xml/commit/6804ffc8680ed6466c66f2472f5e016c412c2c24)
* [Add TYPE_NAMES constant](https://github.com/sferik/multi_xml/commit/72a21f2e86c8e3ac9689cee5f3a62102cfb98028)
0.4.2
-----
* [Fix bug in dealing with xml element attributes for both REXML and Ox](https://github.com/sferik/multi_xml/commit/ba3c1ac427ff0268abaf8186fb4bd81100c99559)
* [Make Ox the preferred XML parser](https://github.com/sferik/multi_xml/commit/0a718d740c30fba426f300a929cda9ee8250d238)
0.4.1
-----
* [Use the SAX like parser with Ox](https://github.com/sferik/multi_xml/commit/d289d42817a32e48483c00d5361c76fbea62a166)
0.4.0
-----
* [Add support for Ox](https://github.com/sferik/multi_xml/pull/14)
0.3.0
-----
* [Remove core class monkeypatches](https://github.com/sferik/multi_xml/commit/f7cc3ce4d2924c0e0adc6935d1fba5ec79282938)
* [Sort out some class / singleton class issues](https://github.com/sferik/multi_xml/commit/a5dac06bcf658facaaf7afa295f1291c7be15a44)
* [Have parsers refer to toplevel CONTENT_ROOT instead of defining it](https://github.com/sferik/multi_xml/commit/94e6fa49e69b2a2467a0e6d3558f7d9815cae47e)
* [Move redundant input sanitizing to top-level](https://github.com/sferik/multi_xml/commit/4874148214dbbd2e5a4b877734e2519af42d6132)
* [Refactor libxml and nokogiri parsers to inherit from a common ancestor](https://github.com/sferik/multi_xml/commit/e0fdffcbfe641b6aaa3952ffa0570a893de325c2)
0.2.2
-----
* [Respect the global load path](https://github.com/sferik/multi_xml/commit/68eb3011b37f0e0222bb842abd2a78e1285a97c1)
0.2.1
-----
* [Add BlueCloth gem as development dependency for Markdown formatting](https://github.com/sferik/multi_xml/commit/18195cd1789176709f68f0d7f8df7fc944fe4d24)
* [Replace BlueCloth with Maruku for JRuby compatibility](https://github.com/sferik/multi_xml/commit/bad5516a5ec5e7ef7fc5a35c411721522357fa19)
0.2.0
-----
* [Do not automatically load all library files](https://github.com/sferik/multi_xml/commit/dbd0447e062e8930118573c5453150e9371e5955)
0.1.4
-----
* [Preserve backtrace when catching/throwing exceptions](https://github.com/sferik/multi_xml/commit/7475ee90201c2701fddd524082832d16ca62552d)
0.1.3
-----
* [Common error handling for all parsers](https://github.com/sferik/multi_xml/commit/5357c28eddc14e921fd1be1f445db602a8dddaf2)
0.1.2
-----
* [Make wrap an Array class method](https://github.com/sferik/multi_xml/commit/28307b69bd1d9460353c861466e425c2afadcf56)
0.1.1
-----
* [Fix parsing for strings that contain newlines](https://github.com/sferik/multi_xml/commit/68087a4ce50b5d63cfa60d6f1fcbc2f6d689e43f)
0.1.0
-----
* [Add support for LibXML and Nokogiri](https://github.com/sferik/multi_xml/commit/856bb17fce66601e0b3d3eb3b64dbeb25aed3bca)
0.0.1
-----
* [REXML support](https://github.com/sferik/multi_xml/commit/2a848384a7b90fb3e26b5a8d4dc3fa3e3f2db5fc)
multi_xml-0.8.1/CONTRIBUTING.md 0000664 0000000 0000000 00000003734 15127740662 0016004 0 ustar 00root root 0000000 0000000 ## Contributing
In the spirit of [free software][free-sw] , **everyone** is encouraged to help
improve this project.
[free-sw]: http://www.fsf.org/licensing/essays/free-sw.html
Here are some ways *you* can contribute:
* by using alpha, beta, and prerelease versions
* by reporting bugs
* by suggesting new features
* by writing or editing documentation
* by writing specifications
* by writing code (**no patch is too small**: fix typos, add comments, clean up
inconsistent whitespace)
* by refactoring code
* by resolving [issues][]
* by reviewing patches
* [financially][gittip]
[issues]: https://github.com/sferik/multi_xml/issues
[gittip]: https://www.gittip.com/sferik/
## Submitting an Issue
We use the [GitHub issue tracker][issues] to track bugs and features. Before
submitting a bug report or feature request, check to make sure it hasn't
already been submitted. When submitting a bug report, please include a [Gist][]
that includes a stack trace and any details that may be necessary to reproduce
the bug, including your gem version, Ruby version, and operating system.
Ideally, a bug report should include a pull request with failing specs.
[gist]: https://gist.github.com/
## Submitting a Pull Request
1. [Fork the repository.][fork]
2. [Create a topic branch.][branch]
3. Add specs for your unimplemented feature or bug fix.
4. Run `bundle exec rake spec`. If your specs pass, return to step 3.
5. Implement your feature or bug fix.
6. Run `bundle exec rake`. If your specs fail, return to step 5.
7. Run `open coverage/index.html`. If your changes are not completely covered
by your tests, return to step 3.
8. Add documentation for your feature or bug fix.
9. Run `bundle exec rake verify_measurements`. If your changes are not 100%
documented, go back to step 8.
10. Add, commit, and push your changes.
11. [Submit a pull request.][pr]
[fork]: http://help.github.com/fork-a-repo/
[branch]: http://learn.github.com/p/branching.html
[pr]: http://help.github.com/send-pull-requests/
multi_xml-0.8.1/Gemfile 0000664 0000000 0000000 00000001237 15127740662 0015042 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gem "libxml-ruby", require: nil, platforms: :ruby
gem "nokogiri", require: nil
gem "oga", ">= 2.3", require: nil
gem "ox", require: nil, platforms: :ruby
gem "rexml", require: nil
gem "minitest", ">= 6"
gem "minitest-mock", ">= 5.27"
gem "mutant-minitest", ">= 0.14.1"
gem "rake", ">= 13.3.1"
gem "rdoc", ">= 7.0.2"
gem "rubocop", ">= 1.81.7"
gem "rubocop-minitest", ">= 0.36"
gem "rubocop-performance", ">= 1.26.1"
gem "rubocop-rake", ">= 0.7.1"
gem "simplecov", ">= 0.22"
gem "standard", ">= 1.52"
gem "standard-performance", ">= 1.9"
gem "steep", ">= 1.10", platforms: :ruby
gem "yard", ">= 0.9.38"
gem "yardstick", ">= 0.9.9"
gemspec
multi_xml-0.8.1/LICENSE.md 0000664 0000000 0000000 00000002044 15127740662 0015150 0 ustar 00root root 0000000 0000000 Copyright (c) 2010-2025 Erik Berlin
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
multi_xml-0.8.1/README.md 0000664 0000000 0000000 00000004633 15127740662 0015031 0 ustar 00root root 0000000 0000000 # MultiXML
A generic swappable back-end for XML parsing
## Installation
gem install multi_xml
## Documentation
[http://rdoc.info/gems/multi_xml][documentation]
[documentation]: http://rdoc.info/gems/multi_xml
## Usage Examples
```ruby
require 'multi_xml'
MultiXml.parser = :ox
MultiXml.parser = MultiXml::Parsers::Ox # Same as above
MultiXml.parse('This is the contents') # Parsed using Ox
MultiXml.parser = :libxml
MultiXml.parser = MultiXml::Parsers::Libxml # Same as above
MultiXml.parse('This is the contents') # Parsed using LibXML
MultiXml.parser = :nokogiri
MultiXml.parser = MultiXml::Parsers::Nokogiri # Same as above
MultiXml.parse('This is the contents') # Parsed using Nokogiri
MultiXml.parser = :rexml
MultiXml.parser = MultiXml::Parsers::Rexml # Same as above
MultiXml.parse('This is the contents') # Parsed using REXML
MultiXml.parser = :oga
MultiXml.parser = MultiXml::Parsers::Oga # Same as above
MultiXml.parse('This is the contents') # Parsed using Oga
```
The `parser` setter takes either a symbol or a class (to allow for custom XML
parsers) that responds to `.parse` at the class level.
MultiXML tries to have intelligent defaulting. That is, if you have any of the
supported parsers already loaded, it will use them before attempting to load
a new one. When loading, libraries are ordered by speed: first Ox, then LibXML,
then Nokogiri, and finally REXML.
## Supported Ruby Versions
This library aims to support and is tested against the following Ruby
implementations:
* 3.2
* 3.3
* 3.4
* 4.0
If something doesn't work on one of these versions, it's a bug.
This library may inadvertently work (or seem to work) on other Ruby
implementations, however support will only be provided for the versions listed
above.
If you would like this library to support another Ruby version, you may
volunteer to be a maintainer. Being a maintainer entails making sure all tests
run and pass on that implementation. When something breaks on your
implementation, you will be responsible for providing patches in a timely
fashion. If critical issues for a particular implementation exist at the time
of a major release, support for that Ruby version may be dropped.
## Inspiration
MultiXML was inspired by [MultiJSON][].
[multijson]: https://github.com/intridea/multi_json/
## Copyright
Copyright (c) 2010-2025 Erik Berlin. See [LICENSE][] for details.
[license]: LICENSE.md
multi_xml-0.8.1/Rakefile 0000664 0000000 0000000 00000003126 15127740662 0015213 0 ustar 00root root 0000000 0000000 require "bundler/gem_tasks"
# Override release task to skip gem push (handled by GitHub Actions with attestations)
Rake::Task["release"].clear
desc "Build gem and create tag (gem push handled by CI)"
task release: %w[build release:guard_clean release:source_control_push]
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"
end
require "standard/rake"
require "rubocop/rake_task"
RuboCop::RakeTask.new
require "yard"
YARD::Rake::YardocTask.new do |task|
task.files = ["lib/**/*.rb", "-", "LICENSE.md"]
task.options = [
"--no-private",
"--protected",
"--output-dir", "doc/yard",
"--markup", "markdown"
]
end
require "yardstick/rake/measurement"
Yardstick::Rake::Measurement.new do |measurement|
measurement.output = "measurement/report.txt"
end
require "yardstick/rake/verify"
Yardstick::Rake::Verify.new do |verify|
verify.threshold = 100
end
# Steep requires native extensions not available on JRuby or Windows
unless RUBY_PLATFORM == "java" || Gem.win_platform?
require "steep/rake_task"
Steep::RakeTask.new
end
desc "Run linters"
task lint: %i[rubocop standard]
# Mutant uses fork() which is not available on Windows or JRuby
desc "Run mutation testing"
task :mutant do
if Gem.win_platform? || RUBY_PLATFORM == "java"
puts "Skipping mutant on Windows/JRuby (fork not supported)"
else
system("bundle", "exec", "mutant", "run") || exit(1)
end
end
default_tasks = %i[test lint verify_measurements mutant]
default_tasks << :steep unless RUBY_PLATFORM == "java" || Gem.win_platform?
task default: default_tasks
multi_xml-0.8.1/Steepfile 0000664 0000000 0000000 00000001012 15127740662 0015401 0 ustar 00root root 0000000 0000000 D = Steep::Diagnostic
target :lib do
signature "sig"
# Check core library files (excluding parser implementations that depend on optional gems)
check "lib/multi_xml.rb"
check "lib/multi_xml/constants.rb"
check "lib/multi_xml/errors.rb"
check "lib/multi_xml/file_like.rb"
check "lib/multi_xml/helpers.rb"
check "lib/multi_xml/version.rb"
# Use stdlib types
library "date"
library "time"
library "yaml"
library "bigdecimal"
library "stringio"
configure_code_diagnostics(D::Ruby.strict)
end
multi_xml-0.8.1/bin/ 0000775 0000000 0000000 00000000000 15127740662 0014314 5 ustar 00root root 0000000 0000000 multi_xml-0.8.1/bin/console 0000775 0000000 0000000 00000000375 15127740662 0015711 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby
require "bundler/setup"
require "multi_xml"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
require "irb"
IRB.start(__FILE__)
multi_xml-0.8.1/bin/setup 0000775 0000000 0000000 00000000203 15127740662 0015375 0 ustar 00root root 0000000 0000000 #!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here
multi_xml-0.8.1/lib/ 0000775 0000000 0000000 00000000000 15127740662 0014312 5 ustar 00root root 0000000 0000000 multi_xml-0.8.1/lib/multi_xml.rb 0000664 0000000 0000000 00000015501 15127740662 0016653 0 ustar 00root root 0000000 0000000 require "bigdecimal"
require "date"
require "stringio"
require "time"
require "yaml"
require_relative "multi_xml/constants"
require_relative "multi_xml/errors"
require_relative "multi_xml/file_like"
require_relative "multi_xml/helpers"
# A generic swappable back-end for parsing XML
#
# MultiXml provides a unified interface for XML parsing across different
# parser libraries. It automatically selects the best available parser
# (Ox, LibXML, Nokogiri, Oga, or REXML) and converts XML to Ruby hashes.
#
# @api public
# @example Parse XML
# MultiXml.parse('John')
# #=> {"root"=>{"name"=>"John"}}
#
# @example Set the parser
# MultiXml.parser = :nokogiri
module MultiXml
class << self
include Helpers
# Get the current XML parser module
#
# Returns the currently configured parser, auto-detecting one if not set.
# Parsers are checked in order of performance: Ox, LibXML, Nokogiri, Oga, REXML.
#
# @api public
# @return [Module] the current parser module
# @example Get current parser
# MultiXml.parser #=> MultiXml::Parsers::Ox
def parser
@parser ||= resolve_parser(detect_parser)
end
# Set the XML parser to use
#
# @api public
# @param new_parser [Symbol, String, Module] Parser specification
# - Symbol/String: :libxml, :nokogiri, :ox, :rexml, :oga
# - Module: Custom parser implementing parse(io) and parse_error
# @return [Module] the newly configured parser module
# @example Set parser by symbol
# MultiXml.parser = :nokogiri
# @example Set parser by module
# MultiXml.parser = MyCustomParser
def parser=(new_parser)
@parser = resolve_parser(new_parser)
end
# Parse XML into a Ruby Hash
#
# @api public
# @param xml [String, IO] XML content as a string or IO-like object
# @param options [Hash] Parsing options
# @option options [Symbol, String, Module] :parser Parser to use for this call
# @option options [Boolean] :symbolize_keys Convert keys to symbols (default: false)
# @option options [Array] :disallowed_types Types to reject (default: ['yaml', 'symbol'])
# @option options [Boolean] :typecast_xml_value Apply type conversions (default: true)
# @return [Hash] Parsed XML as nested hash
# @raise [ParseError] if XML is malformed
# @raise [DisallowedTypeError] if XML contains a disallowed type attribute
# @example Parse simple XML
# MultiXml.parse('John')
# #=> {"root"=>{"name"=>"John"}}
# @example Parse with symbolized keys
# MultiXml.parse('John', symbolize_keys: true)
# #=> {root: {name: "John"}}
def parse(xml, options = {})
options = DEFAULT_OPTIONS.merge(options)
xml_parser = options[:parser] ? resolve_parser(options.fetch(:parser)) : parser
io = normalize_input(xml)
return {} if io.eof?
result = parse_with_error_handling(io, xml, xml_parser)
result = typecast_xml_value(result, options.fetch(:disallowed_types)) if options.fetch(:typecast_xml_value)
result = symbolize_keys(result) if options.fetch(:symbolize_keys)
result
end
private
# Resolve a parser specification to a module
#
# @api private
# @param spec [Symbol, String, Class, Module] Parser specification
# @return [Module] Resolved parser module
# @raise [RuntimeError] if spec is invalid
def resolve_parser(spec)
case spec
when String, Symbol then load_parser(spec)
when Module then spec
else raise "Invalid parser specification: expected Symbol, String, or Module"
end
end
# Load a parser by name
#
# @api private
# @param name [Symbol, String] Parser name
# @return [Module] Loaded parser module
def load_parser(name)
name = name.to_s.downcase
require "multi_xml/parsers/#{name}"
Parsers.const_get(camelize(name))
end
# Convert underscored string to CamelCase
#
# @api private
# @param name [String] Underscored string
# @return [String] CamelCased string
def camelize(name)
name.split("_").map(&:capitalize).join
end
# Detect the best available parser
#
# @api private
# @return [Symbol] Parser name
# @raise [NoParserError] if no parser is available
def detect_parser
find_loaded_parser || find_available_parser || raise_no_parser_error
end
# Parser constant names mapped to their symbols, in preference order
#
# @api private
LOADED_PARSER_CHECKS = {
Ox: :ox,
LibXML: :libxml,
Nokogiri: :nokogiri,
Oga: :oga
}.freeze
private_constant :LOADED_PARSER_CHECKS
# Find an already-loaded parser library
#
# @api private
# @return [Symbol, nil] Parser name or nil if none loaded
def find_loaded_parser
LOADED_PARSER_CHECKS.each do |const_name, parser_name|
return parser_name if const_defined?(const_name)
end
nil
end
# Try to load and find an available parser
#
# @api private
# @return [Symbol, nil] Parser name or nil if none available
def find_available_parser
PARSER_PREFERENCE.each do |library, parser_name|
return parser_name if try_require(library)
end
nil
end
# Attempt to require a library
#
# @api private
# @param library [String] Library to require
# @return [Boolean] true if successful, false if LoadError
def try_require(library)
require library
true
rescue LoadError
false
end
# Raise an error indicating no parser is available
#
# @api private
# @return [void]
# @raise [NoParserError] always
def raise_no_parser_error
raise NoParserError, <<~MSG.chomp
No XML parser detected. Install one of: ox, nokogiri, libxml-ruby, or oga.
See https://github.com/sferik/multi_xml for more information.
MSG
end
# Normalize input to an IO-like object
#
# @api private
# @param xml [String, IO] Input to normalize
# @return [IO] IO-like object
def normalize_input(xml)
return xml if xml.respond_to?(:read)
StringIO.new(xml.to_s.strip)
end
# Parse XML with error handling and key normalization
#
# @api private
# @param io [IO] IO-like object containing XML
# @param original_input [String, IO] Original input for error reporting
# @param xml_parser [Module] Parser to use
# @return [Hash] Parsed XML with undasherized keys
# @raise [ParseError] if XML is malformed
def parse_with_error_handling(io, original_input, xml_parser)
undasherize_keys(xml_parser.parse(io) || {})
rescue xml_parser.parse_error => e
xml_string = original_input.respond_to?(:read) ? original_input.tap(&:rewind).read : original_input.to_s
raise(ParseError.new(e, xml: xml_string, cause: e))
end
end
end
multi_xml-0.8.1/lib/multi_xml/ 0000775 0000000 0000000 00000000000 15127740662 0016324 5 ustar 00root root 0000000 0000000 multi_xml-0.8.1/lib/multi_xml/constants.rb 0000664 0000000 0000000 00000010027 15127740662 0020665 0 ustar 00root root 0000000 0000000 module MultiXml
# Hash key for storing text content within element hashes
#
# @api public
# @return [String] the key "__content__" used for text content
# @example Accessing text content
# result = MultiXml.parse('John')
# result["name"] #=> "John" (simplified, but internally uses __content__)
TEXT_CONTENT_KEY = "__content__".freeze
# Maps Ruby class names to XML type attribute values
#
# @api public
# @return [Hash{String => String}] mapping of Ruby class names to XML types
# @example Check XML type for a Ruby class
# RUBY_TYPE_TO_XML["Integer"] #=> "integer"
RUBY_TYPE_TO_XML = {
"Symbol" => "symbol",
"Integer" => "integer",
"BigDecimal" => "decimal",
"Float" => "float",
"TrueClass" => "boolean",
"FalseClass" => "boolean",
"Date" => "date",
"DateTime" => "datetime",
"Time" => "datetime",
"Array" => "array",
"Hash" => "hash"
}.freeze
# XML type attributes disallowed by default for security
#
# These types are blocked to prevent code execution vulnerabilities.
#
# @api public
# @return [Array] list of disallowed type names
# @example Check default disallowed types
# DISALLOWED_TYPES #=> ["symbol", "yaml"]
DISALLOWED_TYPES = %w[symbol yaml].freeze
# Values that represent false in XML boolean attributes
#
# @api public
# @return [Set] values considered false
# @example Check false values
# FALSE_BOOLEAN_VALUES.include?("0") #=> true
FALSE_BOOLEAN_VALUES = Set.new(%w[0 false]).freeze
# Default parsing options
#
# @api public
# @return [Hash] default options for parse method
# @example View defaults
# DEFAULT_OPTIONS[:symbolize_keys] #=> false
DEFAULT_OPTIONS = {
typecast_xml_value: true,
disallowed_types: DISALLOWED_TYPES,
symbolize_keys: false
}.freeze
# Parser libraries in preference order (fastest first)
#
# @api public
# @return [Array] pairs of [require_path, parser_symbol]
# @example View parser order
# PARSER_PREFERENCE.first #=> ["ox", :ox]
PARSER_PREFERENCE = [
["ox", :ox],
["libxml", :libxml],
["nokogiri", :nokogiri],
["rexml/document", :rexml],
["oga", :oga]
].freeze
# Parses datetime strings, trying Time first then DateTime
#
# @api private
# @return [Proc] lambda that parses datetime strings
PARSE_DATETIME = lambda do |string|
Time.parse(string).utc
rescue ArgumentError
DateTime.parse(string).to_time.utc
end
# Creates a file-like StringIO from base64-encoded content
#
# @api private
# @return [Proc] lambda that creates file objects
FILE_CONVERTER = lambda do |content, entity|
StringIO.new(content.unpack1("m")).tap do |io|
io.extend(FileLike)
file_io = io # : FileIO
file_io.original_filename = entity["name"]
file_io.content_type = entity["content_type"]
end
end
# Type converters for XML type attributes
#
# Maps type attribute values to lambdas that convert string content.
# Converters with arity 2 receive the content and the full entity hash.
#
# @api public
# @return [Hash{String => Proc}] mapping of type names to converter procs
# @example Using a converter
# TYPE_CONVERTERS["integer"].call("42") #=> 42
TYPE_CONVERTERS = {
# Primitive types
"symbol" => :to_sym.to_proc,
"string" => :to_s.to_proc,
"integer" => :to_i.to_proc,
"float" => :to_f.to_proc,
"double" => :to_f.to_proc,
"decimal" => ->(s) { BigDecimal(s) },
"boolean" => ->(s) { !FALSE_BOOLEAN_VALUES.include?(s.strip) },
# Date and time types
"date" => Date.method(:parse),
"datetime" => PARSE_DATETIME,
"dateTime" => PARSE_DATETIME,
# Binary types
"base64Binary" => ->(s) { s.unpack1("m") },
"binary" => ->(s, entity) { (entity["encoding"] == "base64") ? s.unpack1("m") : s },
"file" => FILE_CONVERTER,
# Structured types
"yaml" => lambda do |string|
YAML.safe_load(string, permitted_classes: [Symbol, Date, Time])
rescue ArgumentError, Psych::SyntaxError
string
end
}.freeze
end
multi_xml-0.8.1/lib/multi_xml/errors.rb 0000664 0000000 0000000 00000005502 15127740662 0020167 0 ustar 00root root 0000000 0000000 module MultiXml
# Raised when XML parsing fails
#
# Preserves the original XML and underlying cause for debugging.
#
# @api public
# @example Catching a parse error
# begin
# MultiXml.parse('')
# rescue MultiXml::ParseError => e
# puts e.xml # The malformed XML
# puts e.cause # The underlying parser exception
# end
class ParseError < StandardError
# The original XML that failed to parse
#
# @api public
# @return [String, nil] the XML string that caused the error
# @example Access the failing XML
# error.xml #=> ""
attr_reader :xml
# The underlying parser exception
#
# @api public
# @return [Exception, nil] the original exception from the parser
# @example Access the cause
# error.cause #=> #
attr_reader :cause
# Create a new ParseError
#
# @api public
# @param message [String, nil] Error message
# @param xml [String, nil] The original XML that failed to parse
# @param cause [Exception, nil] The underlying parser exception
# @return [ParseError] the new error instance
# @example Create a parse error
# ParseError.new("Invalid XML", xml: "", cause: original_error)
def initialize(message = nil, xml: nil, cause: nil)
@xml = xml
@cause = cause
super(message)
end
end
# Raised when no XML parser library is available
#
# This error is raised when MultiXml cannot find any supported XML parser.
# Install one of: ox, nokogiri, libxml-ruby, or oga.
#
# @api public
# @example Catching the error
# begin
# MultiXml.parse('')
# rescue MultiXml::NoParserError => e
# puts "Please install an XML parser gem"
# end
class NoParserError < StandardError; end
# Raised when an XML type attribute is in the disallowed list
#
# By default, 'yaml' and 'symbol' types are disallowed for security reasons.
#
# @api public
# @example Catching a disallowed type error
# begin
# MultiXml.parse('--- :key')
# rescue MultiXml::DisallowedTypeError => e
# puts e.type #=> "yaml"
# end
class DisallowedTypeError < StandardError
# The disallowed type that was encountered
#
# @api public
# @return [String] the type attribute value that was disallowed
# @example Access the disallowed type
# error.type #=> "yaml"
attr_reader :type
# Create a new DisallowedTypeError
#
# @api public
# @param type [String] The disallowed type attribute value
# @return [DisallowedTypeError] the new error instance
# @example Create a disallowed type error
# DisallowedTypeError.new("yaml")
def initialize(type)
@type = type
super("Disallowed type attribute: #{type.inspect}")
end
end
end
multi_xml-0.8.1/lib/multi_xml/file_like.rb 0000664 0000000 0000000 00000003560 15127740662 0020600 0 ustar 00root root 0000000 0000000 module MultiXml
# Mixin that provides file-like metadata to StringIO objects
#
# Used when parsing base64-encoded file content from XML.
# Adds original_filename and content_type attributes to StringIO.
#
# @api public
# @example Extending a StringIO
# io = StringIO.new("file content")
# io.extend(MultiXml::FileLike)
# io.original_filename = "document.pdf"
# io.content_type = "application/pdf"
module FileLike
# Default filename when none is specified
# @api public
# @return [String] the default filename "untitled"
DEFAULT_FILENAME = "untitled".freeze
# Default content type when none is specified
# @api public
# @return [String] the default MIME type "application/octet-stream"
DEFAULT_CONTENT_TYPE = "application/octet-stream".freeze
# Set the original filename
#
# @api public
# @param value [String] The filename to set
# @return [String] the filename that was set
# @example Set filename
# io.original_filename = "report.pdf"
attr_writer :original_filename
# Set the content type
#
# @api public
# @param value [String] The MIME type to set
# @return [String] the content type that was set
# @example Set content type
# io.content_type = "application/pdf"
attr_writer :content_type
# Get the original filename
#
# @api public
# @return [String] the original filename or "untitled" if not set
# @example Get filename
# io.original_filename #=> "document.pdf"
def original_filename
@original_filename || DEFAULT_FILENAME
end
# Get the content type
#
# @api public
# @return [String] the content type or "application/octet-stream" if not set
# @example Get content type
# io.content_type #=> "application/pdf"
def content_type
@content_type || DEFAULT_CONTENT_TYPE
end
end
end
multi_xml-0.8.1/lib/multi_xml/helpers.rb 0000664 0000000 0000000 00000017464 15127740662 0020327 0 ustar 00root root 0000000 0000000 module MultiXml
# Methods for transforming parsed XML hash structures
#
# These helper methods handle key transformation and type casting
# of parsed XML data structures.
#
# @api public
module Helpers
module_function
# Recursively convert all hash keys to symbols
#
# @api private
# @param data [Hash, Array, Object] Data to transform
# @return [Hash, Array, Object] Transformed data with symbolized keys
# @example Symbolize hash keys
# symbolize_keys({"name" => "John"}) #=> {name: "John"}
def symbolize_keys(data)
transform_keys(data, &:to_sym)
end
# Recursively convert dashes in hash keys to underscores
#
# @api private
# @param data [Hash, Array, Object] Data to transform
# @return [Hash, Array, Object] Transformed data with undasherized keys
# @example Convert dashed keys
# undasherize_keys({"first-name" => "John"}) #=> {"first_name" => "John"}
def undasherize_keys(data)
transform_keys(data) { |key| key.tr("-", "_") }
end
# Recursively typecast XML values based on type attributes
#
# @api private
# @param value [Hash, Array, Object] Value to typecast
# @param disallowed_types [Array] Types to reject
# @return [Object] Typecasted value
# @raise [DisallowedTypeError] if a disallowed type is encountered
# @example Typecast integer value
# typecast_xml_value({"__content__" => "42", "type" => "integer"})
# #=> 42
def typecast_xml_value(value, disallowed_types = DISALLOWED_TYPES)
case value
when Hash then typecast_hash(value, disallowed_types)
when Array then typecast_array(value, disallowed_types)
else value
end
end
# Typecast array elements and unwrap single-element arrays
#
# @api private
# @param array [Array] Array to typecast
# @param disallowed_types [Array] Types to reject
# @return [Object, Array] Typecasted array or single element
def typecast_array(array, disallowed_types)
array.map! { |item| typecast_xml_value(item, disallowed_types) }
(array.size == 1) ? array.first : array
end
# Typecast a hash based on its type attribute
#
# @api private
# @param hash [Hash] Hash to typecast
# @param disallowed_types [Array] Types to reject
# @return [Object] Typecasted value
# @raise [DisallowedTypeError] if type is disallowed
def typecast_hash(hash, disallowed_types)
type = hash["type"]
raise DisallowedTypeError, type if disallowed_type?(type, disallowed_types)
convert_hash(hash, type, disallowed_types)
end
# Check if a type is in the disallowed list
#
# @api private
# @param type [String, nil] Type to check
# @param disallowed_types [Array] Disallowed type list
# @return [Boolean] true if type is disallowed
def disallowed_type?(type, disallowed_types)
type && !type.is_a?(Hash) && disallowed_types.include?(type)
end
# Convert a hash based on its type and content
#
# @api private
# @param hash [Hash] Hash to convert
# @param type [String, nil] Type attribute value
# @param disallowed_types [Array] Types to reject
# @return [Object] Converted value
def convert_hash(hash, type, disallowed_types)
return extract_array_entries(hash, disallowed_types) if type == "array"
return convert_text_content(hash) if hash.key?(TEXT_CONTENT_KEY)
return "" if type == "string" && !hash["nil"].eql?("true")
return nil if empty_value?(hash, type)
typecast_children(hash, disallowed_types)
end
# Typecast all child values in a hash
#
# @api private
# @param hash [Hash] Hash with children to typecast
# @param disallowed_types [Array] Types to reject
# @return [Hash, StringIO] Typecasted hash or unwrapped file
def typecast_children(hash, disallowed_types)
result = hash.transform_values { |v| typecast_xml_value(v, disallowed_types) }
unwrap_file_if_present(result)
end
# Extract array entries from element with type="array"
#
# @api private
# @param hash [Hash] Hash containing array entries
# @param disallowed_types [Array] Types to reject
# @return [Array] Extracted and typecasted entries
# @see https://github.com/jnunemaker/httparty/issues/102
def extract_array_entries(hash, disallowed_types)
entries = find_array_entries(hash)
return [] unless entries
wrap_and_typecast(entries, disallowed_types)
end
# Find array or hash entries in a hash, excluding the type key
#
# @api private
# @param hash [Hash] Hash to search
# @return [Array, Hash, nil] Found entries or nil
def find_array_entries(hash)
hash.each do |key, value|
return value if !key.eql?("type") && (value.is_a?(Array) || value.is_a?(Hash))
end
nil
end
# Wrap hash in array if needed and typecast all entries
#
# @api private
# @param entries [Array, Hash] Entries to process
# @param disallowed_types [Array] Types to reject
# @return [Array] Typecasted entries
def wrap_and_typecast(entries, disallowed_types)
entries = [entries] if entries.is_a?(Hash)
entries.map { |entry| typecast_xml_value(entry, disallowed_types) }
end
# Convert text content using type converters
#
# @api private
# @param hash [Hash] Hash containing text content and type
# @return [Object] Converted value
def convert_text_content(hash)
content = hash.fetch(TEXT_CONTENT_KEY)
converter = TYPE_CONVERTERS[hash["type"]]
return unwrap_if_simple(hash, content) unless converter
apply_converter(hash, content, converter)
end
# Unwrap value if hash has no other significant keys
#
# @api private
# @param hash [Hash] Original hash
# @param value [Object] Converted value
# @return [Object, Hash] Value or hash with merged content
def unwrap_if_simple(hash, value)
(hash.size > 1) ? hash.merge(TEXT_CONTENT_KEY => value) : value
end
# Check if a hash represents an empty value
#
# @api private
# @param hash [Hash] Hash to check
# @param type [String, nil] Type attribute value
# @return [Boolean] true if value should be nil
def empty_value?(hash, type)
hash.empty? ||
hash["nil"] == "true" ||
(type && hash.size == 1 && !type.is_a?(Hash))
end
private
# Recursively transform hash keys using a block
#
# @api private
# @param data [Hash, Array, Object] Data to transform
# @return [Hash, Array, Object] Transformed data
def transform_keys(data, &block)
case data
when Hash then data.each_with_object(
{} #: Hash[Symbol, MultiXml::xmlValue] # rubocop:disable Layout/LeadingCommentSpace
) { |(key, value), acc| acc[yield(key)] = transform_keys(value, &block) }
when Array then data.map { |item| transform_keys(item, &block) }
else data
end
end
# Unwrap a file object from the result hash if present
#
# @api private
# @param result [Hash] Hash that may contain a file
# @return [Hash, StringIO] The file if present, otherwise the hash
def unwrap_file_if_present(result)
file = result["file"]
file.is_a?(StringIO) ? file : result
end
# Apply a type converter to content
#
# @api private
# @param hash [Hash] Original hash with type info
# @param content [String] Content to convert
# @param converter [Proc] Converter to apply
# @return [Object] Converted value
def apply_converter(hash, content, converter)
# Binary converters need access to entity attributes (e.g., encoding, name)
return converter.call(content, hash) if converter.arity == 2
hash.delete("type")
unwrap_if_simple(hash, converter.call(content))
end
end
end
multi_xml-0.8.1/lib/multi_xml/parsers/ 0000775 0000000 0000000 00000000000 15127740662 0020003 5 ustar 00root root 0000000 0000000 multi_xml-0.8.1/lib/multi_xml/parsers/dom_parser.rb 0000664 0000000 0000000 00000005572 15127740662 0022474 0 ustar 00root root 0000000 0000000 module MultiXml
module Parsers
# Shared DOM traversal logic for converting XML nodes to hashes
#
# Used by Nokogiri, LibXML, and Oga parsers.
# Including modules must implement:
# - each_child(node) { |child| ... }
# - each_attr(node) { |attr| ... }
# - node_name(node) -> String
#
# @api private
module DomParser
# Convert an XML node to a hash representation
#
# @api private
# @param node [Object] XML node to convert
# @param hash [Hash] Accumulator hash for results
# @return [Hash] Hash representation of the node
def node_to_hash(node, hash = {})
node_hash = {TEXT_CONTENT_KEY => +""}
add_value(hash, node_name(node), node_hash)
collect_children(node, node_hash)
collect_attributes(node, node_hash)
strip_whitespace_content(node_hash)
hash
end
private
# Add a value to a hash, converting to array on duplicates
#
# @api private
# @param hash [Hash] Target hash
# @param key [String] Key to add
# @param value [Object] Value to add
# @return [void]
def add_value(hash, key, value)
existing = hash[key]
hash[key] = case existing
when Array then existing << value
when Hash then [existing, value]
else value
end
end
# Collect all child nodes into a hash
#
# @api private
# @param node [Object] Parent node
# @param node_hash [Hash] Hash to populate
# @return [void]
def collect_children(node, node_hash)
each_child(node) do |child|
if child.element?
node_to_hash(child, node_hash)
elsif text_or_cdata?(child)
node_hash[TEXT_CONTENT_KEY] << child.content
end
end
end
# Check if a node is text or CDATA
#
# @api private
# @param node [Object] Node to check
# @return [Boolean] true if text or CDATA
def text_or_cdata?(node)
node.text? || node.cdata?
end
# Collect all attributes from a node
#
# @api private
# @param node [Object] Node with attributes
# @param node_hash [Hash] Hash to populate
# @return [void]
def collect_attributes(node, node_hash)
each_attr(node) do |attr|
name = node_name(attr)
existing = node_hash[name]
node_hash[name] = existing ? [attr.value, existing] : attr.value
end
end
# Remove empty or whitespace-only text content
#
# @api private
# @param node_hash [Hash] Hash to clean up
# @return [void]
def strip_whitespace_content(node_hash)
content = node_hash[TEXT_CONTENT_KEY]
should_remove = content.empty? || (node_hash.size > 1 && content.strip.empty?)
node_hash.delete(TEXT_CONTENT_KEY) if should_remove
end
end
end
end
multi_xml-0.8.1/lib/multi_xml/parsers/libxml.rb 0000664 0000000 0000000 00000002335 15127740662 0021622 0 ustar 00root root 0000000 0000000 require "libxml"
require_relative "dom_parser"
module MultiXml
module Parsers
# XML parser using the LibXML library
#
# @api private
module Libxml
include DomParser
extend self
# Get the parse error class for this parser
#
# @api private
# @return [Class] LibXML::XML::Error
def parse_error = ::LibXML::XML::Error
# Parse XML from an IO object
#
# @api private
# @param io [IO] IO-like object containing XML
# @return [Hash] Parsed XML as a hash
# @raise [LibXML::XML::Error] if XML is malformed
def parse(io)
node_to_hash(LibXML::XML::Parser.io(io).parse.root)
end
private
# Iterate over child nodes
#
# @param node [LibXML::XML::Node] Parent node
# @return [void]
def each_child(node, &) = node.each_child(&)
# Iterate over attribute nodes
#
# @param node [LibXML::XML::Node] Element node
# @return [void]
def each_attr(node, &) = node.each_attr(&)
# Get the name of a node or attribute
#
# @param node [LibXML::XML::Node] Node to get name from
# @return [String] Node name
def node_name(node) = node.name
end
end
end
multi_xml-0.8.1/lib/multi_xml/parsers/libxml_sax.rb 0000664 0000000 0000000 00000005116 15127740662 0022475 0 ustar 00root root 0000000 0000000 require "libxml"
require "stringio"
require_relative "sax_handler"
module MultiXml
module Parsers
# SAX-based parser using LibXML (faster for large documents)
#
# @api private
module LibxmlSax
module_function
# Get the parse error class for this parser
#
# @api private
# @return [Class] LibXML::XML::Error
def parse_error = ::LibXML::XML::Error
# Parse XML from a string or IO object
#
# @api private
# @param xml [String, IO] XML content
# @return [Hash] Parsed XML as a hash
# @raise [LibXML::XML::Error] if XML is malformed
def parse(xml)
io = xml.respond_to?(:read) ? xml : StringIO.new(xml)
return {} if io.eof?
LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER)
handler = Handler.new
parser = ::LibXML::XML::SaxParser.io(io)
parser.callbacks = handler
parser.parse
handler.result
end
# LibXML SAX handler that builds a hash tree while parsing
#
# @api private
class Handler
include ::LibXML::XML::SaxParser::Callbacks
include SaxHandler
# Create a new SAX handler
#
# @api private
# @return [Handler] new handler instance
def initialize
initialize_handler
end
# Handle start of document (no-op)
#
# @api private
# @return [void]
def on_start_document
end
# Handle end of document (no-op)
#
# @api private
# @return [void]
def on_end_document
end
# Handle parse errors (no-op, LibXML raises directly)
#
# @api private
# @param _error [String] Error message (unused)
# @return [void]
def on_error(_error)
end
# Handle start of an element
#
# @api private
# @param name [String] Element name
# @param attrs [Hash] Element attributes
# @return [void]
def on_start_element(name, attrs = {})
handle_start_element(name, attrs)
end
# Handle end of an element
#
# @api private
# @param _name [String] Element name (unused)
# @return [void]
def on_end_element(_name)
handle_end_element
end
# Handle character data
#
# @api private
# @param text [String] Text content
# @return [void]
def on_characters(text) = append_text(text)
alias_method :on_cdata_block, :on_characters
end
end
end
end
multi_xml-0.8.1/lib/multi_xml/parsers/nokogiri.rb 0000664 0000000 0000000 00000002522 15127740662 0022152 0 ustar 00root root 0000000 0000000 require "nokogiri"
require_relative "dom_parser"
module MultiXml
module Parsers
# XML parser using the Nokogiri library
#
# @api private
module Nokogiri
include DomParser
extend self
# Get the parse error class for this parser
#
# @api private
# @return [Class] Nokogiri::XML::SyntaxError
def parse_error = ::Nokogiri::XML::SyntaxError
# Parse XML from an IO object
#
# @api private
# @param io [IO] IO-like object containing XML
# @return [Hash] Parsed XML as a hash
# @raise [Nokogiri::XML::SyntaxError] if XML is malformed
def parse(io)
doc = ::Nokogiri::XML(io)
raise doc.errors.first unless doc.errors.empty?
node_to_hash(doc.root)
end
private
# Iterate over child nodes
#
# @param node [Nokogiri::XML::Node] Parent node
# @return [void]
def each_child(node, &) = node.children.each(&)
# Iterate over attribute nodes
#
# @param node [Nokogiri::XML::Node] Element node
# @return [void]
def each_attr(node, &) = node.attribute_nodes.each(&)
# Get the name of a node or attribute
#
# @param node [Nokogiri::XML::Node] Node to get name from
# @return [String] Node name
def node_name(node) = node.node_name
end
end
end
multi_xml-0.8.1/lib/multi_xml/parsers/nokogiri_sax.rb 0000664 0000000 0000000 00000005051 15127740662 0023025 0 ustar 00root root 0000000 0000000 require "nokogiri"
require "stringio"
require_relative "sax_handler"
module MultiXml
module Parsers
# SAX-based parser using Nokogiri (faster for large documents)
#
# @api private
module NokogiriSax
module_function
# Get the parse error class for this parser
#
# @api private
# @return [Class] Nokogiri::XML::SyntaxError
def parse_error = ::Nokogiri::XML::SyntaxError
# Parse XML from a string or IO object
#
# @api private
# @param xml [String, IO] XML content
# @return [Hash] Parsed XML as a hash
# @raise [Nokogiri::XML::SyntaxError] if XML is malformed
def parse(xml)
io = xml.respond_to?(:read) ? xml : StringIO.new(xml)
return {} if io.eof?
handler = Handler.new
::Nokogiri::XML::SAX::Parser.new(handler).parse(io)
handler.result
end
# Nokogiri SAX handler that builds a hash tree while parsing
#
# @api private
class Handler < ::Nokogiri::XML::SAX::Document
include SaxHandler
# Create a new SAX handler
#
# @api private
# @return [Handler] new handler instance
def initialize
super
initialize_handler
end
# Handle start of document (no-op)
#
# @api private
# @return [void]
def start_document
end
# Handle end of document (no-op)
#
# @api private
# @return [void]
def end_document
end
# Handle parse errors
#
# @api private
# @param message [String] Error message
# @return [void]
# @raise [Nokogiri::XML::SyntaxError] always
def error(message)
raise ::Nokogiri::XML::SyntaxError, message
end
# Handle start of an element
#
# @api private
# @param name [String] Element name
# @param attrs [Array] Element attributes as pairs
# @return [void]
def start_element(name, attrs = [])
handle_start_element(name, attrs)
end
# Handle end of an element
#
# @api private
# @param _name [String] Element name (unused)
# @return [void]
def end_element(_name)
handle_end_element
end
# Handle character data
#
# @api private
# @param text [String] Text content
# @return [void]
def characters(text) = append_text(text)
alias_method :cdata_block, :characters
end
end
end
end
multi_xml-0.8.1/lib/multi_xml/parsers/oga.rb 0000664 0000000 0000000 00000003464 15127740662 0021105 0 ustar 00root root 0000000 0000000 require "oga"
require_relative "dom_parser"
module MultiXml
module Parsers
# XML parser using the Oga library
#
# @api private
module Oga
include DomParser
extend self
# Get the parse error class for this parser
#
# @api private
# @return [Class] LL::ParserError
def parse_error = LL::ParserError
# Parse XML from an IO object
#
# @api private
# @param io [IO] IO-like object containing XML
# @return [Hash] Parsed XML as a hash
# @raise [LL::ParserError] if XML is malformed
def parse(io)
doc = ::Oga.parse_xml(io)
node_to_hash(doc.children.first)
end
# Collect child nodes into a hash (Oga-specific implementation)
#
# Oga uses different node types than Nokogiri/LibXML.
#
# @api private
# @param node [Oga::XML::Element] Parent node
# @param node_hash [Hash] Hash to populate
# @return [void]
def collect_children(node, node_hash)
each_child(node) do |child|
case child
when ::Oga::XML::Element then node_to_hash(child, node_hash)
when ::Oga::XML::Text, ::Oga::XML::Cdata then node_hash[TEXT_CONTENT_KEY] << child.text
end
end
end
private
# Iterate over child nodes
#
# @param node [Oga::XML::Element] Parent node
# @return [void]
def each_child(node, &) = node.children.each(&)
# Iterate over attribute nodes
#
# @param node [Oga::XML::Element] Element node
# @return [void]
def each_attr(node, &) = node.attributes.each(&)
# Get the name of a node or attribute
#
# @param node [Oga::XML::Node] Node to get name from
# @return [String] Node name
def node_name(node) = node.name
end
end
end
multi_xml-0.8.1/lib/multi_xml/parsers/ox.rb 0000664 0000000 0000000 00000007330 15127740662 0020761 0 ustar 00root root 0000000 0000000 require "ox"
module MultiXml
module Parsers
# XML parser using the Ox library (fastest pure-Ruby parser)
#
# @api private
module Ox
module_function
# Get the parse error class for this parser
#
# @api private
# @return [Class] Ox::ParseError
def parse_error = ::Ox::ParseError
# Parse XML from an IO object
#
# @api private
# @param io [IO] IO-like object containing XML
# @return [Hash] Parsed XML as a hash
def parse(io)
handler = Handler.new
::Ox.sax_parse(handler, io, convert_special: true, skip: :skip_return)
handler.result
end
# SAX event handler that builds a hash tree while parsing
#
# @api private
class Handler
# Create a new SAX handler
#
# @return [Handler] new handler instance
def initialize
@stack = []
end
# Get the parsed result
#
# @return [Hash, nil] the root hash or nil if empty
def result = @stack.first
# Handle start of an element
#
# @param name [Symbol] Element name
# @return [void]
def start_element(name)
@stack << {} if @stack.empty?
child = {}
add_value(name.to_s, child)
@stack << child
end
# Handle end of an element
#
# @param _name [Symbol] Element name (unused)
# @return [void]
def end_element(_name)
strip_whitespace_content if current.key?(TEXT_CONTENT_KEY)
@stack.pop
end
# Handle an attribute
#
# @param name [Symbol] Attribute name
# @param value [String] Attribute value
# @return [void]
def attr(name, value)
add_value(name.to_s, value) unless @stack.empty?
end
# Handle text content
#
# @param value [String] Text content
# @return [void]
def text(value) = add_value(TEXT_CONTENT_KEY, value)
# Handle CDATA content
#
# @param value [String] CDATA content
# @return [void]
def cdata(value) = add_value(TEXT_CONTENT_KEY, value)
# Handle parse errors
#
# @param message [String] Error message
# @param line [Integer] Line number
# @param column [Integer] Column number
# @return [void]
# @raise [Ox::ParseError] always
def error(message, line, column)
raise ::Ox::ParseError, "#{message} at #{line}:#{column}"
end
private
# Get the current element hash
#
# @return [Hash] current hash being built
def current = @stack.last
# Add a value to the current hash, merging with existing if needed
#
# @param key [String] Key to add
# @param value [Object] Value to add
# @return [void]
def add_value(key, value)
existing = current[key]
current[key] = existing ? merge_values(existing, value) : value
end
# Merge a value with an existing value, creating array if needed
#
# @param existing [Object] Existing value
# @param value [Object] Value to append
# @return [Array] array with both values
def merge_values(existing, value)
existing.is_a?(Array) ? existing << value : [existing, value]
end
# Remove empty or whitespace-only text content
#
# @return [void]
def strip_whitespace_content
content = current[TEXT_CONTENT_KEY]
should_remove = content.empty? || (current.size > 1 && content.strip.empty?)
current.delete(TEXT_CONTENT_KEY) if should_remove
end
end
end
end
end
multi_xml-0.8.1/lib/multi_xml/parsers/rexml.rb 0000664 0000000 0000000 00000006742 15127740662 0021470 0 ustar 00root root 0000000 0000000 require "rexml/document"
module MultiXml
module Parsers
# XML parser using Ruby's built-in REXML library
#
# @api private
module Rexml
extend self
# Get the parse error class for this parser
#
# @api private
# @return [Class] REXML::ParseException
def parse_error = ::REXML::ParseException
# Parse XML from an IO object
#
# @api private
# @param io [IO] IO-like object containing XML
# @return [Hash] Parsed XML as a hash
# @raise [REXML::ParseException] if XML is malformed
def parse(io)
doc = REXML::Document.new(io)
element_to_hash({}, doc.root)
end
private
# Convert an element to hash format
#
# @api private
# @param hash [Hash] Accumulator hash
# @param element [REXML::Element] Element to convert
# @return [Hash] Updated hash
def element_to_hash(hash, element)
add_to_hash(hash, element.name, collapse_element(element))
end
# Collapse an element into a hash with attributes and content
#
# @api private
# @param element [REXML::Element] Element to collapse
# @return [Hash] Hash representation
def collapse_element(element)
node_hash = collect_attributes(element)
if element.has_elements?
collect_child_elements(element, node_hash)
add_text_content(node_hash, element) unless whitespace_only?(element)
elsif node_hash.empty? || !whitespace_only?(element)
add_text_content(node_hash, element)
end
node_hash
end
# Collect all attributes from an element into a hash
#
# @api private
# @param element [REXML::Element] Element with attributes
# @return [Hash] Hash of attribute name-value pairs
def collect_attributes(element)
element.attributes.each_with_object({}) { |(name, value), hash| hash[name] = value }
end
# Collect all child elements into a hash
#
# @api private
# @param element [REXML::Element] Parent element
# @param node_hash [Hash] Hash to populate
# @return [void]
def collect_child_elements(element, node_hash)
element.each_element { |child| element_to_hash(node_hash, child) }
end
# Add text content from an element to a hash
#
# @api private
# @param hash [Hash] Target hash
# @param element [REXML::Element] Element with text
# @return [Hash] Updated hash
def add_text_content(hash, element)
return hash unless element.has_text?
text = element.texts.map(&:value).join
add_to_hash(hash, TEXT_CONTENT_KEY, text)
end
# Add a value to a hash, handling duplicates as arrays
#
# @api private
# @param hash [Hash] Target hash
# @param key [String] Key to add
# @param value [Object] Value to add
# @return [Hash] Updated hash
def add_to_hash(hash, key, value)
existing = hash[key]
hash[key] = if existing
existing.is_a?(Array) ? existing << value : [existing, value]
elsif value.is_a?(Array)
[value]
else
value
end
hash
end
# Check if element contains only whitespace text
#
# @api private
# @param element [REXML::Element] Element to check
# @return [Boolean] true if whitespace only
def whitespace_only?(element)
element.texts.join.strip.empty?
end
end
end
end
multi_xml-0.8.1/lib/multi_xml/parsers/sax_handler.rb 0000664 0000000 0000000 00000006320 15127740662 0022621 0 ustar 00root root 0000000 0000000 require "cgi/escape"
module MultiXml
module Parsers
# Shared SAX handler logic for building hash trees from XML events
#
# This module provides the core stack-based parsing logic used by both
# NokogiriSax and LibxmlSax parsers. Including classes must implement
# the callback methods that their respective SAX libraries expect.
#
# @api private
module SaxHandler
# Initialize the handler state
#
# @api private
# @return [void]
def initialize_handler
@result = {}
@stack = [@result]
@pending_attrs = []
end
# Get the parsed result
#
# @api private
# @return [Hash] the parsed hash
attr_reader :result
private
# Get the current element hash
#
# @api private
# @return [Hash] current hash being built
def current = @stack.last
# Handle start of an element by pushing onto the stack
#
# @api private
# @param name [String] Element name
# @param attrs [Hash, Array] Element attributes
# @return [void]
def handle_start_element(name, attrs)
child = {TEXT_CONTENT_KEY => +""}
add_child_to_current(name, child)
@stack << child
@pending_attrs << normalize_attrs(attrs)
end
# Handle end of an element by applying attributes and popping the stack
#
# @api private
# @return [void]
def handle_end_element
apply_attributes(@pending_attrs.pop)
strip_whitespace_content
@stack.pop
end
# Append text to the current element's content
#
# @api private
# @param text [String] Text to append
# @return [void]
def append_text(text)
current[TEXT_CONTENT_KEY] << text
end
# Add a child hash to the current element
#
# @api private
# @param name [String] Child element name
# @param child [Hash] Child hash to add
# @return [void]
def add_child_to_current(name, child)
existing = current[name]
current[name] = case existing
when Array then existing << child
when Hash then [existing, child]
else child
end
end
# Normalize attributes to a hash
#
# @api private
# @param attrs [Hash, Array] Attributes as hash or array of pairs
# @return [Hash] Normalized attributes hash
def normalize_attrs(attrs)
attrs.is_a?(Hash) ? attrs : attrs.to_h
end
# Apply pending attributes to the current element
#
# @api private
# @param attrs [Hash] Attributes to apply
# @return [void]
def apply_attributes(attrs)
attrs.each do |name, value|
unescaped = CGI.unescapeHTML(value)
existing = current[name]
current[name] = existing ? [unescaped, existing] : unescaped
end
end
# Remove empty or whitespace-only text content
#
# @api private
# @return [void]
def strip_whitespace_content
content = current[TEXT_CONTENT_KEY]
should_remove = content.empty? || (current.size > 1 && content.strip.empty?)
current.delete(TEXT_CONTENT_KEY) if should_remove
end
end
end
end
multi_xml-0.8.1/lib/multi_xml/version.rb 0000664 0000000 0000000 00000000240 15127740662 0020332 0 ustar 00root root 0000000 0000000 module MultiXml
# The current version of MultiXml
#
# @api public
# @return [Gem::Version] the gem version
VERSION = Gem::Version.create("0.8.1")
end
multi_xml-0.8.1/multi_xml.gemspec 0000664 0000000 0000000 00000002705 15127740662 0017127 0 ustar 00root root 0000000 0000000 require_relative "lib/multi_xml/version"
Gem::Specification.new do |spec|
spec.name = "multi_xml"
spec.version = MultiXml::VERSION
spec.authors = ["Erik Berlin"]
spec.email = ["sferik@gmail.com"]
spec.summary = "Provides swappable XML backends utilizing LibXML, Nokogiri, Ox, or REXML."
spec.homepage = "https://github.com/sferik/multi_xml"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.2"
spec.metadata = {
"allowed_push_host" => "https://rubygems.org",
"bug_tracker_uri" => "https://github.com/sferik/multi_xml/issues",
"changelog_uri" => "https://github.com/sferik/multi_xml/blob/master/CHANGELOG.md",
"documentation_uri" => "https://rubydoc.info/gems/multi_xml/",
"funding_uri" => "https://github.com/sponsors/sferik",
"homepage_uri" => spec.homepage,
"rubygems_mfa_required" => "true",
"source_code_uri" => "https://github.com/sferik/multi_xml"
}
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(__dir__) do
`git ls-files -z`.split("\x0").reject do |f|
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
end
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.add_dependency("bigdecimal", ">= 3.1", "< 5")
end
multi_xml-0.8.1/sig/ 0000775 0000000 0000000 00000000000 15127740662 0014326 5 ustar 00root root 0000000 0000000 multi_xml-0.8.1/sig/multi_xml.rbs 0000664 0000000 0000000 00000017537 15127740662 0017065 0 ustar 00root root 0000000 0000000 # Type signatures for MultiXml
# Recursive type alias for parsed XML values
# XML parsing produces nested structures of hashes, arrays, and primitive values
type MultiXml::xmlValue = String
| Integer
| Float
| bool
| Symbol
| Time
| Date
| BigDecimal
| StringIO
| nil
| Array[MultiXml::xmlValue]
| Hash[String, MultiXml::xmlValue]
| Hash[Symbol, MultiXml::xmlValue]
# Type for hash with string keys used internally during parsing
type MultiXml::xmlHash = Hash[String, MultiXml::xmlValue]
# Interface for parser modules
interface MultiXml::_Parser
def parse: (StringIO io) -> MultiXml::xmlHash?
def parse_error: () -> singleton(Exception)
end
module MultiXml
VERSION: Gem::Version
TEXT_CONTENT_KEY: String
RUBY_TYPE_TO_XML: Hash[String, String]
DISALLOWED_TYPES: Array[String]
FALSE_BOOLEAN_VALUES: Set[String]
DEFAULT_OPTIONS: Hash[Symbol, bool | Array[String]]
# Array of [library_name, parser_symbol] pairs
PARSER_PREFERENCE: Array[Array[String | Symbol]]
PARSE_DATETIME: ^(String) -> Time
# Lambda for creating file-like StringIO from base64 content
# Uses untyped for content because unpack1 returns various types
# Uses untyped for entity because hash values are xmlValue but we access specific String keys
FILE_CONVERTER: ^(untyped, untyped) -> StringIO
# Type converters keyed by XML type attribute string
# Uses untyped key because hash["type"] returns xmlValue, and Hash#[] with non-String returns nil
TYPE_CONVERTERS: Hash[untyped, Proc | Method]
LOADED_PARSER_CHECKS: Hash[Symbol, Symbol]
self.@parser: Module
extend Helpers
# Public API: Get the current XML parser module
def self.parser: () -> Module
# Public API: Set the XML parser to use
def self.parser=: (Symbol | String | Module new_parser) -> Module
# Public API: Parse XML into a Ruby Hash
# Uses untyped for options because values vary by key (:parser, :symbolize_keys, :disallowed_types, :typecast_xml_value)
def self.parse: (String | StringIO xml, ?Hash[Symbol, untyped] options) -> xmlHash
private
# Resolve a parser specification (Symbol, String, or Module) to a parser
def self.resolve_parser: (Symbol | String | Module spec) -> Module
# Load a parser module by name
def self.load_parser: (Symbol | String name) -> Module
# Convert snake_case to CamelCase
def self.camelize: (String name) -> String
# Detect the best available parser
def self.detect_parser: () -> (Symbol | String)
# Find an already-loaded parser library
def self.find_loaded_parser: () -> Symbol?
# Try to find an available parser by requiring libraries
def self.find_available_parser: () -> (String | Symbol | nil)
# Attempt to require a library, returning success/failure
# Kernel#require accepts String; library may be Symbol from PARSER_PREFERENCE (coerced at runtime)
def self.try_require: (untyped library) -> bool
# Raise NoParserError - never returns
def self.raise_no_parser_error: () -> bot
# Convert String to StringIO, pass through IO-like objects
# Uses respond_to?(:read) duck typing - returns input unchanged if IO-like
def self.normalize_input: (String | StringIO xml) -> untyped
# Parse with error handling and key normalization
# xml_parser implements _Parser interface; original_input uses respond_to? duck typing
def self.parse_with_error_handling: (StringIO io, untyped original_input, untyped xml_parser) -> xmlHash
module Helpers
# Recursively convert all hash keys to symbols
# Uses case/when type dispatch - Steep can't track flow narrowing
def self?.symbolize_keys: (untyped data) -> untyped
# Recursively convert dashes in hash keys to underscores
# Uses case/when type dispatch - Steep can't track flow narrowing
def self?.undasherize_keys: (untyped data) -> untyped
# Recursively typecast XML values based on type attributes
# Uses case/when type dispatch - Steep can't track flow narrowing
def self?.typecast_xml_value: (untyped value, ?Array[String] disallowed_types) -> xmlValue
# Typecast array elements and unwrap single-element arrays
def self?.typecast_array: (Array[xmlValue] array, Array[String] disallowed_types) -> xmlValue
# Typecast a hash based on its type attribute
def self?.typecast_hash: (xmlHash hash, Array[String] disallowed_types) -> xmlValue
# Check if a type is in the disallowed list
# Uses is_a?(Hash) guard then include? - Steep can't narrow xmlValue to String
def self?.disallowed_type?: (untyped type, Array[String] disallowed_types) -> boolish
# Convert a hash based on its type and content
def self?.convert_hash: (xmlHash hash, xmlValue type, Array[String] disallowed_types) -> xmlValue
# Typecast all child values in a hash
def self?.typecast_children: (xmlHash hash, Array[String] disallowed_types) -> (xmlHash | StringIO)
# Extract array entries from element with type="array"
def self?.extract_array_entries: (xmlHash hash, Array[String] disallowed_types) -> Array[xmlValue]
# Find array or hash entries in a hash, excluding the type key
# Returns xmlValue subset (Array or Hash) - uses is_a? that Steep can't narrow
def self?.find_array_entries: (xmlHash hash) -> untyped
# Wrap hash in array if needed and typecast all entries
def self?.wrap_and_typecast: (Array[xmlValue] | xmlHash entries, Array[String] disallowed_types) -> Array[xmlValue]
# Convert text content using type converters
# hash["type"] is xmlValue, used as Hash key - Steep requires String
def self?.convert_text_content: (xmlHash hash) -> xmlValue
# Unwrap value if hash has no other significant keys
def self?.unwrap_if_simple: (xmlHash hash, xmlValue value) -> (xmlValue | xmlHash)
# Check if a hash represents an empty value
def self?.empty_value?: (xmlHash hash, xmlValue type) -> bool
private
# Recursively transform hash keys using a block
# Block receives key (String) and returns transformed key
# Uses untyped because &:to_sym Proc type narrowing not supported by Steep
def self?.transform_keys: (untyped data) { (untyped) -> untyped } -> untyped
# Unwrap a file object from the result hash if present
def self?.unwrap_file_if_present: (xmlHash result) -> (xmlHash | StringIO)
# Apply a type converter to content
# Content is xmlValue (from hash.fetch) but typically String in practice
def self?.apply_converter: (xmlHash hash, untyped content, Proc | Method converter) -> xmlValue
end
module FileLike
DEFAULT_FILENAME: String
DEFAULT_CONTENT_TYPE: String
@original_filename: String?
@content_type: String?
attr_writer original_filename: String?
attr_writer content_type: String?
def original_filename: () -> String
def content_type: () -> String
end
# Represents a StringIO that has been extended with FileLike
# Used for file type conversions in XML parsing
class FileIO < StringIO
include FileLike
end
class ParseError < StandardError
@xml: String?
@cause: Exception?
attr_reader xml: String?
attr_reader cause: Exception?
# Message can be String (normal) or Exception (from parser errors), or nil for default
def initialize: (?(String | Exception | nil) message, ?xml: String?, ?cause: Exception?) -> void
end
class NoParserError < StandardError
end
class DisallowedTypeError < StandardError
@type: String
attr_reader type: String
def initialize: (String type) -> void
end
# Parsers module - parser implementations depend on optional external gems
module Parsers
end
end
# Stub for Psych::SyntaxError which is part of the yaml library
module Psych
class SyntaxError < ::StandardError
end
end
multi_xml-0.8.1/test/ 0000775 0000000 0000000 00000000000 15127740662 0014523 5 ustar 00root root 0000000 0000000 multi_xml-0.8.1/test/attribute_tests.rb 0000664 0000000 0000000 00000001503 15127740662 0020274 0 ustar 00root root 0000000 0000000 # Tests parsing XML attributes and handling conflicts between attributes and child elements
module ParserAttributeTests
def test_element_with_same_inner_element_and_attribute_name_returns_array
assert_equal %w[John Smith], MultiXml.parse("Smith")["user"]["name"]
end
def test_content_returns_correct_value
assert_equal "Erik Berlin", MultiXml.parse("Erik Berlin")["user"]
end
def test_attribute_returns_correct_value
assert_equal "Erik Berlin", MultiXml.parse('')["user"]["name"]
end
def test_multiple_attributes_return_correct_values
result = MultiXml.parse('')["user"]
assert_equal "Erik Berlin", result["name"]
assert_equal "sferik", result["screen_name"]
end
end
multi_xml-0.8.1/test/basic_tests.rb 0000664 0000000 0000000 00000002757 15127740662 0017366 0 ustar 00root root 0000000 0000000 # Tests fundamental parsing: empty input, valid XML, and CDATA handling
module ParserBasicTests
def test_blank_string_returns_empty_hash
assert_empty(MultiXml.parse(""))
end
def test_whitespace_string_returns_empty_hash
assert_empty(MultiXml.parse(" "))
end
def test_frozen_whitespace_string_returns_empty_hash
assert_empty(MultiXml.parse(" ".freeze))
end
def test_valid_xml_parses_correctly
assert_equal({"user" => nil}, MultiXml.parse(""))
end
def test_cdata_returns_correct_content
assert_equal "Erik Berlin", MultiXml.parse("")["user"]
end
def test_xml_with_comment_ignores_comment_nodes
assert_equal({"root" => "content"}, MultiXml.parse("content"))
end
def test_xml_with_processing_instruction
assert_equal({"root" => "content"}, MultiXml.parse('content'))
end
end
# Tests for parsers that properly raise errors on invalid XML (excludes Oga)
module ParserStrictErrorTests
def test_invalid_xml_raises_parse_error
assert_raises(MultiXml::ParseError) { MultiXml.parse("") }
end
def test_invalid_xml_includes_original_xml_in_exception
xml = ""
MultiXml.parse(xml)
rescue MultiXml::ParseError => e
assert_equal xml, e.xml
end
def test_invalid_xml_includes_underlying_cause_in_exception
MultiXml.parse("")
rescue MultiXml::ParseError => e
refute_nil e.cause
end
end
multi_xml-0.8.1/test/children_tests.rb 0000664 0000000 0000000 00000004103 15127740662 0020060 0 ustar 00root root 0000000 0000000 # Tests nested element parsing, sibling arrays, and whitespace handling in hierarchies
module ParserChildrenTests
def test_children_with_attributes_return_correct_values
assert_equal "Erik Berlin", MultiXml.parse('')["users"]["user"]["name"]
end
def test_children_with_text_return_correct_values
assert_equal "Erik Berlin", MultiXml.parse("Erik Berlin")["user"]["name"]
end
def test_children_with_unrecognized_type_attribute_passes_through
assert_equal "admin", MultiXml.parse('Erik Berlin')["user"]["type"]
end
def test_children_with_non_type_attribute_tags_on_content_nodes
xml = "1230.123"
values = MultiXml.parse(xml)["options"]["value"]
assert_equal "123", values[0]["__content__"]
assert_equal "USD", values[0]["currency"]
assert_equal "0.123", values[1]["__content__"]
assert_equal "percent", values[1]["number"]
end
def test_children_with_newlines_and_whitespace_parse_correctly
assert_equal({"user" => {"name" => "Erik Berlin"}}, MultiXml.parse("\n Erik Berlin\n"))
end
def test_nested_children_parse_correctly
xml = ''
expected = {"users" => {"user" => {"name" => "Erik Berlin", "status" => {"text" => "Hello"}}}}
assert_equal expected, MultiXml.parse(xml)
end
def test_sibling_children_return_array
assert_kind_of Array, MultiXml.parse("Erik BerlinWynn Netherland")["users"]["user"]
end
def test_sibling_children_parse_correctly
xml = "Erik BerlinWynn Netherland"
assert_equal({"users" => {"user" => ["Erik Berlin", "Wynn Netherland"]}}, MultiXml.parse(xml))
end
def test_element_with_children_and_mixed_text
result = MultiXml.parse("inner text ")
assert result.dig("root", "child")
end
end
multi_xml-0.8.1/test/convert_hash_string_type_test.rb 0000664 0000000 0000000 00000002521 15127740662 0023221 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests convert_hash string type behavior
class ConvertHashStringTypeTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_returns_empty_string_for_string_type_without_nil_true
assert_equal "", convert_hash({"type" => "string"}, "string", [])
end
def test_returns_nil_for_string_type_with_nil_true
assert_nil convert_hash({"type" => "string", "nil" => "true"}, "string", [])
end
def test_string_type_returns_empty_when_nil_absent
hash = {"type" => "string"}
result = convert_hash(hash, "string", [])
assert_equal "", result
end
def test_string_type_returns_nil_when_nil_true
hash = {"type" => "string", "nil" => "true"}
result = convert_hash(hash, "string", [])
assert_nil result
end
def test_string_type_returns_empty_when_nil_not_true
hash = {"type" => "string", "nil" => "false"}
result = convert_hash(hash, "string", [])
assert_equal "", result
end
def test_nil_check_uses_string_comparison
hash = {"type" => "string", "nil" => "true"}
result = convert_hash(hash, "string", [])
assert_nil result
end
def test_non_string_type_does_not_return_empty
hash = {"type" => "boolean", "nil" => "false"}
result = convert_hash(hash, "boolean", [])
refute_equal "", result
assert_kind_of Hash, result
end
end
multi_xml-0.8.1/test/convert_hash_test.rb 0000664 0000000 0000000 00000005363 15127740662 0020601 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests convert_hash behavior for various type attributes
class ConvertHashTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_with_array_type
hash = {"type" => "array", "item" => %w[a b]}
result = convert_hash(hash, "array", [])
assert_equal %w[a b], result
end
def test_with_text_content
hash = {"type" => "string", "__content__" => "hello"}
result = convert_hash(hash, "string", [])
assert_equal "hello", result
end
def test_returns_nil_for_empty_hash
result = convert_hash({}, nil, [])
assert_nil result
end
def test_typecasts_children_otherwise
hash = {"child" => {"type" => "integer", "__content__" => "123"}}
result = convert_hash(hash, nil, [])
assert_equal({"child" => 123}, result)
end
def test_passes_disallowed_types_to_typecast_children
hash = {"child" => {"type" => "yaml", "__content__" => "test"}}
assert_raises(MultiXml::DisallowedTypeError) do
convert_hash(hash, nil, ["yaml"])
end
end
def test_passes_disallowed_types_to_extract_array
hash = {"type" => "array", "item" => [{"type" => "yaml", "__content__" => "test"}]}
assert_raises(MultiXml::DisallowedTypeError) do
convert_hash(hash, "array", ["yaml"])
end
end
def test_with_text_content_key_uses_convert_text_content
hash = {MultiXml::TEXT_CONTENT_KEY => "42", "type" => "integer"}
result = convert_hash(hash, "integer", [])
assert_equal 42, result
end
def test_without_text_content_key_falls_through
hash = {"type" => "string", "nil" => "false"}
result = convert_hash(hash, "string", [])
assert_equal "", result
end
def test_processes_non_array_non_text_content
hash = {"child" => "value"}
result = convert_hash(hash, nil, [])
assert_equal({"child" => "value"}, result)
end
def test_integer_type_returns_nil_not_empty_string
hash = {"type" => "integer"}
result = convert_hash(hash, "integer", [])
assert_nil result
end
def test_empty_value_receives_type
hash = {"type" => "integer"}
result = convert_hash(hash, "integer", [])
assert_nil result
end
def test_empty_value_type_matters
hash = {"key" => "value"}
result_with_nil_type = convert_hash(hash, nil, [])
assert_equal({"key" => "value"}, result_with_nil_type)
end
def test_calls_typecast_children_last
hash = {"name" => {"type" => "integer", "__content__" => "123"}}
result = convert_hash(hash, nil, [])
assert_equal({"name" => 123}, result)
end
def test_passes_disallowed_types_through_all_paths
hash = {"nested" => {"type" => "symbol", "__content__" => "test"}}
assert_raises(MultiXml::DisallowedTypeError) do
convert_hash(hash, nil, ["symbol"])
end
end
end
multi_xml-0.8.1/test/convert_text_content_test.rb 0000664 0000000 0000000 00000001716 15127740662 0022372 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests convert_text_content with type converters
class ConvertTextContentTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_requires_content_key
hash = {MultiXml::TEXT_CONTENT_KEY => "test value", "type" => "string"}
result = convert_text_content(hash)
assert_equal "test value", result
end
def test_accesses_content_key
hash = {MultiXml::TEXT_CONTENT_KEY => "value", "type" => "string"}
result = convert_text_content(hash)
assert_equal "value", result
end
def test_with_unknown_type
hash = {MultiXml::TEXT_CONTENT_KEY => "test value", "type" => "unknown_type"}
result = convert_text_content(hash)
assert_equal({"__content__" => "test value", "type" => "unknown_type"}, result)
end
def test_without_type_returns_content
hash = {MultiXml::TEXT_CONTENT_KEY => "test value"}
result = convert_text_content(hash)
assert_equal "test value", result
end
end
multi_xml-0.8.1/test/datetime_fallback_test.rb 0000664 0000000 0000000 00000000564 15127740662 0021527 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests DateTime parsing fallback behavior
class DateTimeFallbackTest < Minitest::Test
cover "MultiXml*"
def test_parse_datetime_falls_back_to_datetime_for_iso_week_format
converter = MultiXml::PARSE_DATETIME
result = converter.call("2020-W01")
assert_kind_of Time, result
assert_equal Time.utc(2019, 12, 30), result
end
end
multi_xml-0.8.1/test/disallowed_type_error_test.rb 0000664 0000000 0000000 00000001334 15127740662 0022511 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests for DisallowedTypeErrorTest
class DisallowedTypeErrorTest < Minitest::Test
cover "MultiXml*"
def test_stores_type
error = MultiXml::DisallowedTypeError.new("yaml")
assert_equal "yaml", error.type
end
def test_message_includes_type_inspect
error = MultiXml::DisallowedTypeError.new("yaml")
assert_equal 'Disallowed type attribute: "yaml"', error.message
end
def test_message_with_symbol_type
error = MultiXml::DisallowedTypeError.new(:symbol)
assert_equal "Disallowed type attribute: :symbol", error.message
end
def test_inherits_from_standard_error
error = MultiXml::DisallowedTypeError.new("yaml")
assert_kind_of StandardError, error
end
end
multi_xml-0.8.1/test/disallowed_type_test.rb 0000664 0000000 0000000 00000002450 15127740662 0021300 0 ustar 00root root 0000000 0000000 require "test_helper"
require_relative "test_subclasses"
# Tests disallowed_type? checking
class DisallowedTypeTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_returns_true_for_disallowed
assert disallowed_type?("yaml", %w[yaml symbol])
end
def test_returns_false_for_allowed
refute disallowed_type?("string", %w[yaml symbol])
end
def test_returns_false_for_nil_type
refute disallowed_type?(nil, %w[yaml symbol])
end
def test_returns_false_when_type_is_hash
refute disallowed_type?({"nested" => "value"}, ["yaml"])
end
def test_nil_type_returns_false
refute disallowed_type?(nil, [nil])
end
def test_hash_type_not_checked
hash_type = {"yaml" => true}
refute disallowed_type?(hash_type, [hash_type])
end
def test_both_conditions_needed
assert disallowed_type?("yaml", ["yaml"])
refute disallowed_type?(nil, ["yaml"])
refute disallowed_type?({"yaml" => true}, ["yaml"])
end
def test_with_hash_subclass
subclass = HashSubclass.new
subclass["yaml"] = true
refute disallowed_type?(subclass, [subclass])
end
def test_checks_first_condition_type_truthiness
refute disallowed_type?(false, ["false"])
end
def test_with_symbol_in_list
assert disallowed_type?(:symbol, [:symbol])
end
end
multi_xml-0.8.1/test/empty_type_tests.rb 0000664 0000000 0000000 00000002675 15127740662 0020503 0 ustar 00root root 0000000 0000000 # Tests type coercion of empty/self-closing elements (integer, boolean, date, etc.)
module ParserEmptyTypeTests
def test_empty_integer_returns_nil
assert_nil MultiXml.parse('')["tag"]
end
def test_empty_boolean_returns_nil
assert_nil MultiXml.parse('')["tag"]
end
def test_empty_date_returns_nil
assert_nil MultiXml.parse('')["tag"]
end
def test_empty_datetime_returns_nil
assert_nil MultiXml.parse('')["tag"]
end
def test_empty_file_returns_nil
assert_nil MultiXml.parse('')["tag"]
end
def test_empty_yaml_raises_disallowed_type_error
assert_raises(MultiXml::DisallowedTypeError) { MultiXml.parse('')["tag"] }
end
def test_empty_yaml_returns_nil_when_allowed
assert_nil MultiXml.parse('', disallowed_types: [])["tag"]
end
def test_empty_symbol_raises_disallowed_type_error
assert_raises(MultiXml::DisallowedTypeError) { MultiXml.parse('')["tag"] }
end
def test_empty_symbol_returns_nil_when_allowed
assert_nil MultiXml.parse('', disallowed_types: [])["tag"]
end
def test_empty_array_returns_empty_array
assert_empty MultiXml.parse('')["tag"]
end
def test_empty_array_with_whitespace_returns_empty_array
assert_empty MultiXml.parse(' ')["tag"]
end
end
multi_xml-0.8.1/test/empty_value_test.rb 0000664 0000000 0000000 00000002607 15127740662 0020446 0 ustar 00root root 0000000 0000000 require "test_helper"
require_relative "test_subclasses"
# Tests empty_value? checking for nil/empty values
class EmptyValueTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_true_for_empty_hash
assert empty_value?({}, nil)
end
def test_true_when_nil_equals_true
assert empty_value?({"nil" => "true"}, nil)
end
def test_true_when_only_type_present
assert empty_value?({"type" => "integer"}, "integer")
end
def test_false_when_nil_not_true
refute empty_value?({"nil" => "false", "other" => "value"}, nil)
end
def test_false_when_type_is_hash
refute empty_value?({"type" => {"nested" => "value"}}, {"nested" => "value"})
end
def test_with_hash_type_returns_false
hash_type = {"nested" => "data"}
result = empty_value?({"type" => hash_type}, hash_type)
refute result
end
def test_with_string_type_and_size_one
result = empty_value?({"type" => "integer"}, "integer")
assert result
end
def test_with_string_type_and_size_two
result = empty_value?({"type" => "integer", "other" => "key"}, "integer")
refute result
end
def test_with_hash_subclass_as_type
type = HashSubclass.new
type["nested"] = "value"
hash = {"type" => type}
refute empty_value?(hash, type)
end
def test_false_when_type_nil_and_hash_has_content
refute empty_value?({"content" => "value"}, nil)
end
end
multi_xml-0.8.1/test/entity_tests.rb 0000664 0000000 0000000 00000001562 15127740662 0017612 0 ustar 00root root 0000000 0000000 # Tests XML entity decoding (<, >, etc.) and dash-to-underscore key conversion
module ParserEntityTests
def test_xml_entities_in_content_are_unescaped
{"<" => "<", ">" => ">", '"' => """, "'" => "'", "&" => "&"}.each do |char, entity|
assert_equal char, MultiXml.parse("#{entity}")["tag"]
end
end
def test_xml_entities_in_attribute_are_unescaped
{"<" => "<", ">" => ">", '"' => """, "'" => "'", "&" => "&"}.each do |char, entity|
assert_equal char, MultiXml.parse("")["tag"]["attribute"]
end
end
def test_dasherized_tag_is_undasherized
assert_includes MultiXml.parse("").keys, "tag_1"
end
def test_dasherized_attribute_is_undasherized
assert_includes MultiXml.parse('')["tag"].keys, "attribute_1"
end
end
multi_xml-0.8.1/test/extract_array_entries_disallowed_types_test.rb 0000664 0000000 0000000 00000002537 15127740662 0026152 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests extract_array_entries disallowed type propagation
class ExtractArrayEntriesDisallowedTypesTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_passes_disallowed_types_to_array_elements
hash = {"type" => "array", "item" => [{"type" => "yaml", "__content__" => "test"}]}
assert_raises(MultiXml::DisallowedTypeError) do
extract_array_entries(hash, ["yaml"])
end
end
def test_passes_disallowed_types_to_hash_element
hash = {"type" => "array", "item" => {"type" => "yaml", "__content__" => "test"}}
assert_raises(MultiXml::DisallowedTypeError) do
extract_array_entries(hash, ["yaml"])
end
end
def test_hash_branch_uses_custom_disallowed_types_not_default
# Use "integer" which is NOT in DISALLOWED_TYPES default, so if the mutant
# removes the disallowed_types argument, it will use the default and NOT raise
hash = {"type" => "array", "item" => {"type" => "integer", "__content__" => "42"}}
assert_raises(MultiXml::DisallowedTypeError) do
extract_array_entries(hash, ["integer"])
end
end
def test_with_custom_disallowed_type_raises
hash = {"type" => "array", "item" => [{"type" => "integer", "__content__" => "42"}]}
assert_raises(MultiXml::DisallowedTypeError) do
extract_array_entries(hash, ["integer"])
end
end
end
multi_xml-0.8.1/test/extract_array_entries_test.rb 0000664 0000000 0000000 00000006042 15127740662 0022512 0 ustar 00root root 0000000 0000000 require "test_helper"
require_relative "test_subclasses"
# Tests extract_array_entries for type="array" handling
class ExtractArrayEntriesTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_with_array_value
assert_equal %w[Alice Bob], extract_array_entries({"type" => "array", "user" => %w[Alice Bob]}, [])
end
def test_with_hash_value
assert_equal [{"name" => "Alice"}], extract_array_entries({"type" => "array", "user" => {"name" => "Alice"}}, [])
end
def test_with_no_entries
assert_empty extract_array_entries({"type" => "array"}, [])
end
def test_ignores_type_key
assert_equal %w[a b], extract_array_entries({"type" => "array", "item" => %w[a b]}, [])
end
def test_type_key_is_array_should_be_skipped
hash = {"type" => %w[array nested], "item" => %w[a b]}
result = extract_array_entries(hash, [])
assert_equal %w[a b], result
end
def test_type_key_is_only_array
hash = {"type" => %w[array stuff]}
result = extract_array_entries(hash, [])
assert_empty result
end
def test_type_key_is_hash_should_be_skipped
hash = {"type" => {"nested" => "value"}, "item" => %w[x y]}
result = extract_array_entries(hash, [])
assert_equal %w[x y], result
end
def test_skips_type_even_if_hash_value
hash = {"type" => {"complex" => "type"}, "data" => {"key" => "val"}}
result = extract_array_entries(hash, [])
assert_equal [{"key" => "val"}], result
end
def test_with_array_subclass
subclass_array = ArraySubclass.new
subclass_array.push("a", "b")
hash = {"type" => "array", "items" => subclass_array}
result = extract_array_entries(hash, [])
assert_equal %w[a b], result
end
def test_with_hash_subclass_value
subclass_hash = HashSubclass.new
subclass_hash["key"] = "val"
hash = {"type" => "array", "item" => subclass_hash}
result = extract_array_entries(hash, [])
assert_equal [{"key" => "val"}], result
end
def test_with_integer_value_returns_empty
hash = {"type" => "array", "count" => 42}
result = extract_array_entries(hash, [])
assert_empty result
end
def test_with_nil_value_returns_empty
hash = {"type" => "array", "items" => nil}
result = extract_array_entries(hash, [])
assert_empty result
end
def test_maps_entries_with_typecast
hash = {"type" => "array", "item" => [{"type" => "integer", "__content__" => "42"}]}
result = extract_array_entries(hash, [])
assert_equal [42], result
end
def test_requires_array_or_hash_value
hash = {"type" => "array", "name" => "string_value", "items" => %w[a b]}
result = extract_array_entries(hash, [])
assert_equal %w[a b], result
end
def test_with_frozen_type_key
hash = {"type" => "array", "items" => %w[a b]}
result = extract_array_entries(hash, [])
assert_equal %w[a b], result
end
def test_skips_type_key_with_interned_string
type_key = +"type"
hash = {type_key => "array", "data" => %w[x y]}
result = extract_array_entries(hash, [])
assert_equal %w[x y], result
end
end
multi_xml-0.8.1/test/file_array_tests.rb 0000664 0000000 0000000 00000003106 15127740662 0020407 0 ustar 00root root 0000000 0000000 # Tests file type (base64 to StringIO) and array type coercion
module ParserFileArrayTests
def test_file_type_returns_stringio
xml = 'ZGF0YQ=='
result = MultiXml.parse(xml)["tag"]
assert_kind_of StringIO, result
assert_equal "data", result.string
assert_equal "data.txt", result.original_filename
assert_equal "text/plain", result.content_type
end
def test_file_type_with_missing_name_and_content_type
xml = 'ZGF0YQ=='
result = MultiXml.parse(xml)["tag"]
assert_kind_of StringIO, result
assert_equal "data", result.string
assert_equal "untitled", result.original_filename
assert_equal "application/octet-stream", result.content_type
end
def test_array_type_returns_array
xml = 'Erik BerlinWynn Netherland'
result = MultiXml.parse(xml)["users"]
assert_kind_of Array, result
assert_equal ["Erik Berlin", "Wynn Netherland"], result
end
def test_array_type_with_other_attributes_returns_array
xml = 'Erik BerlinWynn Netherland'
result = MultiXml.parse(xml)["users"]
assert_kind_of Array, result
assert_equal ["Erik Berlin", "Wynn Netherland"], result
end
def test_array_type_with_single_item_returns_array
xml = 'Erik Berlin'
result = MultiXml.parse(xml)["users"]
assert_kind_of Array, result
assert_equal ["Erik Berlin"], result
end
end
multi_xml-0.8.1/test/file_like_test.rb 0000664 0000000 0000000 00000011653 15127740662 0020040 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests for FileLikeOriginalFilenameTest
class FileLikeOriginalFilenameTest < Minitest::Test
cover "MultiXml*"
def test_original_filename_returns_custom_value_when_set
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
io.original_filename = "custom.txt"
assert_equal "custom.txt", io.original_filename
end
def test_original_filename_returns_default_when_not_set
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
assert_equal "untitled", io.original_filename
assert_equal MultiXml::FileLike::DEFAULT_FILENAME, io.original_filename
end
def test_original_filename_returns_default_when_set_to_nil
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
io.original_filename = nil
assert_equal "untitled", io.original_filename
end
def test_original_filename_uses_instance_variable_not_method_call
# The mutant would cause infinite recursion
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
io.original_filename = "test.xml"
# Should not cause stack overflow
assert_equal "test.xml", io.original_filename
end
def test_original_filename_returns_set_value_not_always_default
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
io.original_filename = "my_file.pdf"
refute_equal MultiXml::FileLike::DEFAULT_FILENAME, io.original_filename
assert_equal "my_file.pdf", io.original_filename
end
def test_original_filename_with_falsy_but_present_value
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
# Not setting original_filename leaves @original_filename as nil
refute_nil io.original_filename
assert_equal "untitled", io.original_filename
end
def test_original_filename_does_not_raise
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
assert_equal "untitled", io.original_filename
end
def test_original_filename_returns_actual_value_not_nil
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
refute_nil io.original_filename
end
def test_original_filename_prefers_instance_variable_over_default
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
io.original_filename = "specific.doc"
assert_equal "specific.doc", io.original_filename
refute_equal "untitled", io.original_filename
end
def test_original_filename_body_is_needed
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
result = io.original_filename
refute_nil result
assert_kind_of String, result
end
end
# Tests for FileLikeContentTypeTest
class FileLikeContentTypeTest < Minitest::Test
cover "MultiXml*"
def test_content_type_returns_custom_value_when_set
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
io.content_type = "text/plain"
assert_equal "text/plain", io.content_type
end
def test_content_type_returns_default_when_not_set
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
assert_equal "application/octet-stream", io.content_type
assert_equal MultiXml::FileLike::DEFAULT_CONTENT_TYPE, io.content_type
end
def test_content_type_returns_default_when_set_to_nil
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
io.content_type = nil
assert_equal "application/octet-stream", io.content_type
end
def test_content_type_uses_instance_variable_not_method_call
# The mutant would cause infinite recursion
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
io.content_type = "image/png"
# Should not cause stack overflow
assert_equal "image/png", io.content_type
end
def test_content_type_returns_set_value_not_always_default
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
io.content_type = "application/pdf"
refute_equal MultiXml::FileLike::DEFAULT_CONTENT_TYPE, io.content_type
assert_equal "application/pdf", io.content_type
end
def test_content_type_with_falsy_but_present_value
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
# Not setting content_type leaves @content_type as nil
refute_nil io.content_type
assert_equal "application/octet-stream", io.content_type
end
def test_content_type_does_not_raise
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
assert_equal "application/octet-stream", io.content_type
end
def test_content_type_returns_actual_value_not_nil
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
refute_nil io.content_type
end
def test_content_type_prefers_instance_variable_over_default
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
io.content_type = "text/html"
assert_equal "text/html", io.content_type
refute_equal "application/octet-stream", io.content_type
end
def test_content_type_body_is_needed
io = StringIO.new("test")
io.extend(MultiXml::FileLike)
result = io.content_type
refute_nil result
assert_kind_of String, result
end
end
multi_xml-0.8.1/test/key_transform_test.rb 0000664 0000000 0000000 00000001431 15127740662 0020771 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests key transformation helpers
class KeyTransformTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_undasherize_keys_with_array
input = [{"first-name" => "John"}, {"last-name" => "Doe"}]
assert_equal [{"first_name" => "John"}, {"last_name" => "Doe"}], undasherize_keys(input)
end
def test_undasherize_keys_with_nested_array
assert_equal({"users" => [{"first_name" => "John"}]}, undasherize_keys({"users" => [{"first-name" => "John"}]}))
end
def test_undasherize_keys_with_plain_value
assert_equal "plain string", undasherize_keys("plain string")
end
def test_symbolize_keys_with_array
assert_equal [{name: "John"}, {name: "Jane"}], symbolize_keys([{"name" => "John"}, {"name" => "Jane"}])
end
end
multi_xml-0.8.1/test/load_parser_test.rb 0000664 0000000 0000000 00000007001 15127740662 0020400 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests for LoadParserTest
class LoadParserTest < Minitest::Test
cover "MultiXml*"
def test_load_parser_with_symbol
result = MultiXml.send(:load_parser, :nokogiri)
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_load_parser_with_string
result = MultiXml.send(:load_parser, "nokogiri")
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_load_parser_converts_to_string_and_downcases
result = MultiXml.send(:load_parser, :NOKOGIRI)
assert_equal MultiXml::Parsers::Nokogiri, result
end
end
# Tests for LoadParserDetailedTest
class LoadParserDetailedTest < Minitest::Test
cover "MultiXml*"
def test_load_parser_downcases_symbol
result = MultiXml.send(:load_parser, :NOKOGIRI)
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_load_parser_converts_to_string
result = MultiXml.send(:load_parser, "nokogiri")
assert_equal MultiXml::Parsers::Nokogiri, result
end
end
# Tests for LoadParserCamelizeTest
class LoadParserCamelizeTest < Minitest::Test
cover "MultiXml*"
def test_load_parser_converts_to_camelcase
result = MultiXml.send(:load_parser, :NOKOGIRI)
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_load_parser_handles_underscore_names
skip "libxml not available on Windows/JRuby" if windows? || jruby?
# libxml_sax should become LibxmlSax
result = MultiXml.send(:load_parser, :libxml_sax)
assert_equal MultiXml::Parsers::LibxmlSax, result
end
def test_load_parser_handles_underscore_names_nokogiri_sax
# nokogiri_sax should become NokogiriSax
result = MultiXml.send(:load_parser, :nokogiri_sax)
assert_equal MultiXml::Parsers::NokogiriSax, result
end
end
# Tests load_parser string conversion
class LoadParserStringConversionTest < Minitest::Test
cover "MultiXml*"
def test_load_parser_calls_to_s_on_symbol
# Symbols don't have downcase method directly in older Ruby
result = MultiXml.send(:load_parser, :NOKOGIRI)
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_load_parser_calls_downcase
# Without downcase, "NOKOGIRI" wouldn't match "nokogiri" file
result = MultiXml.send(:load_parser, "NOKOGIRI")
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_load_parser_with_mixed_case_string
# Ensure downcase is called on the string
result = MultiXml.send(:load_parser, "Nokogiri")
assert_equal MultiXml::Parsers::Nokogiri, result
end
end
# Tests that verify downcase is actually called on the require path
class LoadParserRequirePathTest < Minitest::Test
cover "MultiXml*"
def test_load_parser_requires_lowercase_path
required_paths = []
stub_require(required_paths) { MultiXml.send(:load_parser, "REXML") }
assert_includes required_paths, "multi_xml/parsers/rexml"
end
private
def stub_require(required_paths)
original_require = Kernel.instance_method(:require)
suppress_warnings { define_require(required_paths, original_require) }
yield
ensure
suppress_warnings { restore_require(original_require) }
end
def define_require(required_paths, original_require)
Kernel.define_method(:require) do |path|
required_paths << path
original_require.bind_call(self, path)
end
end
def restore_require(original_require)
Kernel.define_method(:require) { |path| original_require.bind_call(self, path) }
end
def suppress_warnings
old_verbose = $VERBOSE
$VERBOSE = nil
yield
ensure
$VERBOSE = old_verbose
end
end
multi_xml-0.8.1/test/mixed_attribute_tests.rb 0000664 0000000 0000000 00000005451 15127740662 0021470 0 ustar 00root root 0000000 0000000 # Tests elements with both type attributes and other attributes, including unrecognized types
module ParserMixedAttributeTests
def test_children_with_unrecognized_type_attribute_tags_on_content_nodes_first_value
xml = "1230.123123"
values = MultiXml.parse(xml)["options"]["value"]
assert_equal "123", values[0]["__content__"]
assert_equal "USD", values[0]["type"]
end
def test_children_with_unrecognized_type_attribute_tags_on_content_nodes_second_value
xml = "1230.123123"
values = MultiXml.parse(xml)["options"]["value"]
assert_equal "0.123", values[1]["__content__"]
assert_equal "percent", values[1]["type"]
end
def test_children_with_unrecognized_type_attribute_tags_on_content_nodes_third_value
xml = "1230.123123"
values = MultiXml.parse(xml)["options"]["value"]
assert_equal "123", values[2]["__content__"]
assert_equal "USD", values[2]["currency"]
end
def test_children_mixing_attributes_and_non_attributes_first_value
xml = "1230.123123"
assert_equal "123", MultiXml.parse(xml)["options"]["value"][0]["__content__"]
assert_equal "USD", MultiXml.parse(xml)["options"]["value"][0]["type"]
end
def test_children_mixing_attributes_and_non_attributes_second_value
xml = "1230.123123"
assert_equal "0.123", MultiXml.parse(xml)["options"]["value"][1]["__content__"]
assert_equal "percent", MultiXml.parse(xml)["options"]["value"][1]["type"]
end
def test_children_mixing_attributes_and_non_attributes_third_value
xml = "1230.123123"
assert_equal "123", MultiXml.parse(xml)["options"]["value"][2]
end
def test_children_mixing_recognized_type_attribute_and_non_type_attributes
xml = "123"
result = MultiXml.parse(xml)["options"]["value"]
assert_equal 123, result["__content__"]
assert_equal "USD", result["number"]
end
def test_children_mixing_unrecognized_type_attribute_and_non_type_attributes
xml = "123"
result = MultiXml.parse(xml)["options"]["value"]
assert_equal "123", result["__content__"]
assert_equal "USD", result["number"]
assert_equal "currency", result["type"]
end
end
multi_xml-0.8.1/test/multi_xml_test.rb 0000664 0000000 0000000 00000021062 15127740662 0020122 0 ustar 00root root 0000000 0000000 require "test_helper"
require "support/mock_decoder"
# Tests setting and retrieving the global XML parser backend
class MultiXmlParserConfigTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
if @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
elsif MultiXml.instance_variable_defined?(:@parser)
MultiXml.send(:remove_instance_variable, :@parser)
end
end
def test_picks_a_default_parser
parser = MultiXml.parser
assert_kind_of Module, parser
assert_respond_to parser, :parse
end
def test_defaults_to_the_best_available_gem
MultiXml.send(:remove_instance_variable, :@parser) if MultiXml.instance_variable_defined?(:@parser)
expected = (windows? || jruby?) ? "MultiXml::Parsers::Nokogiri" : "MultiXml::Parsers::Ox"
assert_equal expected, MultiXml.parser.name
end
def test_is_settable_via_a_symbol
MultiXml.parser = :rexml
assert_equal "MultiXml::Parsers::Rexml", MultiXml.parser.name
end
def test_is_settable_via_a_class
MultiXml.parser = MockDecoder
assert_equal "MockDecoder", MultiXml.parser.name
end
end
# Tests overriding the parser on a per-call basis via the :parser option
class MultiXmlPerParseParserTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
if @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
elsif MultiXml.instance_variable_defined?(:@parser)
MultiXml.send(:remove_instance_variable, :@parser)
end
end
def test_allows_per_parse_parser_via_symbol
MultiXml.parser = :rexml
assert_equal({"user" => "Erik"}, MultiXml.parse("Erik", parser: :nokogiri))
end
def test_allows_per_parse_parser_via_string
MultiXml.parser = :rexml
assert_equal({"user" => "Erik"}, MultiXml.parse("Erik", parser: "nokogiri"))
end
def test_allows_per_parse_parser_via_class
MultiXml.parser = :rexml
require "multi_xml/parsers/nokogiri"
assert_equal({"user" => "Erik"}, MultiXml.parse("Erik", parser: MultiXml::Parsers::Nokogiri))
end
def test_does_not_change_class_level_parser_when_using_per_parse_parser
MultiXml.parser = :rexml
MultiXml.parse("Erik", parser: :nokogiri)
assert_equal "MultiXml::Parsers::Rexml", MultiXml.parser.name
end
def test_uses_class_level_parser_when_parser_option_is_not_provided
MultiXml.parser = :nokogiri
result = MultiXml.parse("Erik")
assert_equal({"user" => "Erik"}, result)
end
def test_raises_error_for_invalid_per_parse_parser
error = assert_raises(RuntimeError) { MultiXml.parse("", parser: 123) }
assert_match(/Invalid parser specification/, error.message)
end
def test_wraps_parser_errors_correctly_with_per_parse_parser
assert_raises(MultiXml::ParseError) { MultiXml.parse("", parser: :nokogiri) }
end
def test_options_parser_key_is_truthy_when_present
result = MultiXml.parse("test", parser: :nokogiri)
assert_equal({"root" => "test"}, result)
end
def test_options_without_parser_uses_default
MultiXml.parser = :rexml
result = MultiXml.parse("test")
assert_equal({"root" => "test"}, result)
end
end
# Tests automatic type conversion based on XML type attributes (float, binary, datetime, etc.)
class MultiXmlTypecastTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
if @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
elsif MultiXml.instance_variable_defined?(:@parser)
MultiXml.send(:remove_instance_variable, :@parser)
end
end
def test_float_type_returns_float
MultiXml.parser = best_available_parser
result = MultiXml.parse('3.14')["tag"]
assert_kind_of Float, result
assert_in_delta(3.14, result)
end
def test_string_type_with_content_returns_string
MultiXml.parser = best_available_parser
result = MultiXml.parse('hello')["tag"]
assert_kind_of String, result
assert_equal "hello", result
end
def test_binary_type_with_base64_encoding_decodes_content
MultiXml.parser = best_available_parser
result = MultiXml.parse('ZGF0YQ==')["tag"]
assert_equal "data", result
end
def test_binary_type_without_encoding_returns_raw_content
MultiXml.parser = best_available_parser
result = MultiXml.parse('raw data')["tag"]
assert_equal "raw data", result
end
def test_datetime_fallback_to_datetime_class
MultiXml.parser = best_available_parser
result = MultiXml.parse('1970-01-01T00:00:00+00:00')["tag"]
assert_kind_of Time, result
end
def test_invalid_yaml_returns_original_string
MultiXml.parser = best_available_parser
xml = '{ invalid yaml content'
result = MultiXml.parse(xml, disallowed_types: [])["tag"]
assert_equal "{ invalid yaml content", result
end
def test_three_sibling_elements_creates_array
MultiXml.parser = best_available_parser
xml = "ABC"
result = MultiXml.parse(xml)["users"]["user"]
assert_kind_of Array, result
assert_equal %w[A B C], result
end
end
# Tests for empty input handling
class MultiXmlEmptyInputTest < Minitest::Test
cover "MultiXml*"
def test_parse_empty_string_returns_empty_hash
result = MultiXml.parse("")
assert_empty(result)
end
def test_parse_empty_xml_returns_empty_hash_not_nil
result = MultiXml.parse(" ")
assert_empty(result)
refute_nil result
end
def test_parse_empty_input_early_returns
result = MultiXml.parse("")
assert_empty(result)
end
end
# Tests conversion of dashed XML element names to underscored Ruby hash keys
class MultiXmlKeyTransformTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
MultiXml.parser = best_available_parser
end
def teardown
if @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
elsif MultiXml.instance_variable_defined?(:@parser)
MultiXml.send(:remove_instance_variable, :@parser)
end
end
def test_parse_with_error_handling_undasherizes_keys
result = MultiXml.parse("value")
assert_equal({"root" => {"my_key" => "value"}}, result)
refute result["root"].key?("my-key")
assert result["root"].key?("my_key")
end
end
# Tests that malformed XML raises ParseError with a meaningful message
class MultiXmlParseErrorTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
if @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
elsif MultiXml.instance_variable_defined?(:@parser)
MultiXml.send(:remove_instance_variable, :@parser)
end
end
def test_parse_error_message_is_string
MultiXml.parser = :nokogiri
error = assert_raises(MultiXml::ParseError) do
MultiXml.parse("")
end
assert_kind_of String, error.message
refute_match(/REXML::ParseException/, error.message) if error.message.is_a?(String)
end
end
# Tests for parser loading
class MultiXmlParserLoadingTest < Minitest::Test
cover "MultiXml*"
def test_load_parser_with_mixed_case_name
parser = MultiXml.send(:load_parser, "Nokogiri")
assert_equal "MultiXml::Parsers::Nokogiri", parser.name
end
def test_load_parser_with_symbol
parser = MultiXml.send(:load_parser, :NOKOGIRI)
assert_equal "MultiXml::Parsers::Nokogiri", parser.name
end
def test_find_loaded_parser_uses_object_const_defined
result = MultiXml.send(:find_loaded_parser)
assert_find_loaded_parser_result(result)
end
def test_resolve_parser_with_class
require "multi_xml/parsers/nokogiri"
parser = MultiXml.send(:resolve_parser, MultiXml::Parsers::Nokogiri)
assert_equal MultiXml::Parsers::Nokogiri, parser
end
private
def assert_find_loaded_parser_result(result)
expected = expected_loaded_parser
expected ? assert_equal(expected, result) : assert_nil(result)
end
def expected_loaded_parser
return :ox if defined?(Ox)
return :libxml if defined?(LibXML)
return :nokogiri if defined?(Nokogiri)
:oga if defined?(Oga)
end
end
multi_xml-0.8.1/test/normalize_input_test.rb 0000664 0000000 0000000 00000001516 15127740662 0021331 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests for NormalizeInputTest
class NormalizeInputTest < Minitest::Test
cover "MultiXml*"
def test_normalize_input_returns_io_unchanged
io = StringIO.new("")
result = MultiXml.send(:normalize_input, io)
assert_same io, result
end
def test_normalize_input_converts_string_to_stringio
result = MultiXml.send(:normalize_input, "")
assert_kind_of StringIO, result
assert_equal "", result.read
end
def test_normalize_input_strips_whitespace
result = MultiXml.send(:normalize_input, " ")
assert_equal "", result.read
end
def test_normalize_input_calls_to_s_on_non_string
obj = Object.new
def obj.to_s
""
end
result = MultiXml.send(:normalize_input, obj)
assert_equal "", result.read
end
end
multi_xml-0.8.1/test/parse_error_test.rb 0000664 0000000 0000000 00000002572 15127740662 0020440 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests for ParseErrorTest
class ParseErrorTest < Minitest::Test
cover "MultiXml*"
def test_parse_error_stores_message
error = MultiXml::ParseError.new("Test message")
assert_equal "Test message", error.message
end
def test_parse_error_with_nil_message_has_default_message
error = MultiXml::ParseError.new
assert_equal "MultiXml::ParseError", error.message
end
def test_parse_error_stores_xml
error = MultiXml::ParseError.new("msg", xml: "")
assert_equal "", error.xml
end
def test_parse_error_stores_cause
cause = StandardError.new("original")
error = MultiXml::ParseError.new("msg", cause: cause)
assert_equal cause, error.cause
end
def test_parse_error_xml_defaults_to_nil
error = MultiXml::ParseError.new("msg")
assert_nil error.xml
end
def test_parse_error_cause_defaults_to_nil
error = MultiXml::ParseError.new("msg")
assert_nil error.cause
end
def test_parse_error_with_all_parameters
cause = StandardError.new("original")
error = MultiXml::ParseError.new("Test", xml: "", cause: cause)
assert_equal "Test", error.message
assert_equal "", error.xml
assert_equal cause, error.cause
end
def test_parse_error_inherits_from_standard_error
error = MultiXml::ParseError.new("test")
assert_kind_of StandardError, error
end
end
multi_xml-0.8.1/test/parse_method_test.rb 0000664 0000000 0000000 00000003241 15127740662 0020561 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests MultiXml.parse with options
class ParseMethodTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
MultiXml.parser = best_available_parser
end
def teardown
if @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
elsif MultiXml.instance_variable_defined?(:@parser)
MultiXml.send(:remove_instance_variable, :@parser)
end
end
def test_options_merge_preserves_parser
MultiXml.parser = :rexml
result = MultiXml.parse("a", parser: :nokogiri)
assert_equal({"r" => "a"}, result)
end
def test_options_merge_uses_defaults
MultiXml.parser = best_available_parser
result = MultiXml.parse('1')
assert_equal 1, result["r"]
end
def test_with_options_hash_merges_defaults
result = MultiXml.parse("", {})
assert_equal({"root" => nil}, result)
end
def test_applies_typecast_option
result = MultiXml.parse('5', typecast_xml_value: true)
assert_equal 5, result["n"]
end
def test_skips_typecast_when_disabled
result = MultiXml.parse('5', typecast_xml_value: false)
assert_equal({"type" => "integer", "__content__" => "5"}, result["n"])
end
def test_applies_symbolize_keys
result = MultiXml.parse("test", symbolize_keys: true)
assert_equal({root: {name: "test"}}, result)
end
def test_respects_disallowed_types_option
assert_raises(MultiXml::DisallowedTypeError) do
MultiXml.parse('test', disallowed_types: ["yaml"])
end
end
end
multi_xml-0.8.1/test/parse_options_test.rb 0000664 0000000 0000000 00000012423 15127740662 0020776 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests for ParseOptionsTest
class ParseOptionsTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
MultiXml.parser = best_available_parser
end
def teardown
return unless @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
end
def test_parse_with_typecast_xml_value_true
result = MultiXml.parse('42', typecast_xml_value: true)
assert_equal 42, result["tag"]
end
def test_parse_with_typecast_xml_value_false
result = MultiXml.parse('42', typecast_xml_value: false)
assert_equal({"type" => "integer", "__content__" => "42"}, result["tag"])
end
def test_parse_with_symbolize_keys_true
result = MultiXml.parse("John", symbolize_keys: true)
assert_equal({root: {name: "John"}}, result)
end
def test_parse_with_symbolize_keys_false
result = MultiXml.parse("John", symbolize_keys: false)
assert_equal({"root" => {"name" => "John"}}, result)
end
def test_parse_with_disallowed_types_empty_allows_yaml
result = MultiXml.parse('--- test', disallowed_types: [])
assert_equal "test", result["tag"]
end
def test_parse_with_custom_disallowed_types
assert_raises(MultiXml::DisallowedTypeError) do
MultiXml.parse('42', disallowed_types: ["integer"])
end
end
def test_parse_uses_parser_option_when_provided
MultiXml.parser = :rexml
result = MultiXml.parse("test", parser: :nokogiri)
assert_equal({"root" => "test"}, result)
end
def test_parse_uses_class_parser_when_parser_option_nil
MultiXml.parser = best_available_parser
# When options[:parser] is nil (falsy), should use class-level parser
result = MultiXml.parse("test", parser: nil)
assert_equal({"root" => "test"}, result)
end
end
# Tests for ParseWithParserOptionTest
class ParseWithParserOptionTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
MultiXml.parser = :rexml
end
def teardown
return unless @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
end
def test_parse_uses_parser_option_when_truthy
result = MultiXml.parse("test", parser: :nokogiri)
assert_equal({"root" => "test"}, result)
end
def test_parse_uses_class_parser_when_parser_option_nil
result = MultiXml.parse("test", parser: nil)
# Should use REXML (the class parser we set)
assert_equal({"root" => "test"}, result)
end
def test_parse_uses_class_parser_when_parser_option_false
result = MultiXml.parse("test", parser: false)
# Should use REXML (the class parser we set)
assert_equal({"root" => "test"}, result)
end
def test_parse_with_explicit_parser_option
MultiXml.parser = :rexml
result = MultiXml.parse("value", parser: :nokogiri)
# Should use Nokogiri, not REXML
assert_equal({"root" => "value"}, result)
end
end
# Tests parse option access behavior
class ParseOptionsAccessTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
return unless @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
end
def test_parse_accesses_parser_option_with_bracket
# With fetch, missing key raises, with [] returns nil
MultiXml.parser = best_available_parser
# options without :parser key should work (use class-level parser)
result = MultiXml.parse("value", symbolize_keys: false)
assert_equal({"test" => "value"}, result)
end
def test_parse_uses_truthy_check_for_parser_option
MultiXml.parser = :rexml
# nil parser option should fall back to class parser
result = MultiXml.parse("v", parser: nil)
assert_equal({"r" => "v"}, result)
end
def test_parse_uses_provided_parser_when_truthy
MultiXml.parser = :rexml
# Truthy parser option should be used
result = MultiXml.parse("v", parser: :nokogiri)
assert_equal({"r" => "v"}, result)
end
def test_parse_accesses_typecast_option_correctly
MultiXml.parser = best_available_parser
result_with = MultiXml.parse('42', typecast_xml_value: true)
result_without = MultiXml.parse('42', typecast_xml_value: false)
assert_equal 42, result_with["n"]
assert_equal({"type" => "integer", "__content__" => "42"}, result_without["n"])
end
def test_parse_accesses_symbolize_keys_option_correctly
MultiXml.parser = best_available_parser
result_with = MultiXml.parse("v", symbolize_keys: true)
result_without = MultiXml.parse("v", symbolize_keys: false)
assert_equal({root: {name: "v"}}, result_with)
assert_equal({"root" => {"name" => "v"}}, result_without)
end
def test_parse_accesses_disallowed_types_option_correctly
MultiXml.parser = best_available_parser
assert_raises(MultiXml::DisallowedTypeError) do
MultiXml.parse('test', disallowed_types: ["yaml"])
end
end
end
multi_xml-0.8.1/test/parse_with_error_handling_test.rb 0000664 0000000 0000000 00000017550 15127740662 0023341 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests for ParseWithErrorHandlingTest
class ParseWithErrorHandlingTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
if @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
elsif MultiXml.instance_variable_defined?(:@parser)
MultiXml.send(:remove_instance_variable, :@parser)
end
end
def test_parse_with_io_input_captures_xml_in_error
MultiXml.parser = :nokogiri
io = StringIO.new("")
begin
MultiXml.parse(io)
rescue MultiXml::ParseError => e
assert_equal "", e.xml
end
end
def test_parse_error_message_from_parser
MultiXml.parser = :nokogiri
begin
MultiXml.parse("")
rescue MultiXml::ParseError => e
refute_nil e.message
refute_empty e.message
end
end
def test_parse_error_cause_is_parser_error
MultiXml.parser = :nokogiri
begin
MultiXml.parse("")
rescue MultiXml::ParseError => e
assert_kind_of Exception, e.cause
end
end
def test_parse_returns_empty_hash_when_parser_returns_nil
MultiXml.parser = best_available_parser
result = MultiXml.parse("")
assert_kind_of Hash, result
end
end
# Tests for ParseWithErrorHandlingDetailedTest
class ParseWithErrorHandlingDetailedTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
MultiXml.parser = :nokogiri
end
def teardown
return unless @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
end
def test_parse_wraps_parser_error_with_xml
io = StringIO.new("")
begin
MultiXml.parse(io)
flunk "Expected ParseError"
rescue MultiXml::ParseError => e
assert_equal "", e.xml
end
end
def test_parse_wraps_parser_error_with_message
MultiXml.parse("")
flunk "Expected ParseError"
rescue MultiXml::ParseError => e
refute_nil e.message
end
def test_parse_wraps_parser_error_with_cause
MultiXml.parse("")
flunk "Expected ParseError"
rescue MultiXml::ParseError => e
refute_nil e.cause
end
end
# Tests for ParseWithErrorHandlingNilTest
class ParseWithErrorHandlingNilTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
return unless @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
end
def test_parse_with_error_handling_handles_nil_parser_result
# When parser returns nil, should get empty hash not nil
MultiXml.parser = best_available_parser
result = MultiXml.parse("")
# Result should be a hash, not nil
assert_kind_of Hash, result
end
def test_parse_error_with_io_uses_rewind
MultiXml.parser = :nokogiri
io = StringIO.new("")
begin
MultiXml.parse(io)
flunk "Expected ParseError"
rescue MultiXml::ParseError => e
# io should have been rewound and read
assert_equal "", e.xml
end
end
end
# Create a mock parser that returns nil
class NilReturningParser
def self.parse(_io)
nil
end
def self.parse_error
StandardError
end
end
# Create a mock parser that always fails
class FailingParser
class ParseFailed < StandardError; end
def self.parse(_io)
raise ParseFailed, "Parse failed"
end
def self.parse_error
ParseFailed
end
end
# Tests for ParseWithErrorHandlingNilReturnTest
class ParseWithErrorHandlingNilReturnTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
return unless @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
end
def test_parse_returns_empty_hash_when_parser_returns_nil
# When parser returns nil, without || {} we'd get nil passed to undasherize_keys
MultiXml.parser = NilReturningParser
# Disable typecast to see raw result from parse_with_error_handling
result = MultiXml.parse("", typecast_xml_value: false)
# Must be empty hash, not nil
assert_empty(result)
end
def test_parse_returns_empty_hash_not_nil_when_parser_returns_nil
MultiXml.parser = NilReturningParser
# Disable typecast to see raw result
result = MultiXml.parse("", typecast_xml_value: false)
# Specifically test it's {} not nil
refute_nil result
assert_empty result
end
def test_parse_error_uses_to_s_on_string_input
# Strings respond to both, but we're testing the to_s path
MultiXml.parser = FailingParser
begin
MultiXml.parse("")
flunk "Expected ParseError"
rescue MultiXml::ParseError => e
assert_equal "", e.xml
end
end
def test_parse_error_with_io_that_responds_to_read
MultiXml.parser = FailingParser
io = StringIO.new("")
begin
MultiXml.parse(io)
flunk "Expected ParseError"
rescue MultiXml::ParseError => e
assert_equal "", e.xml
end
end
def test_parse_error_rewinds_io_before_reading
# The FailingParser raises an error during parse, so original_input
# still needs to be readable for error message
MultiXml.parser = :nokogiri
io = StringIO.new("")
begin
MultiXml.parse(io)
flunk "Expected ParseError"
rescue MultiXml::ParseError => e
# Should have rewound and read full content
assert_equal "", e.xml
end
end
end
# Tests parse error message handling
class ParseWithErrorHandlingMessageTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
return unless @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
end
def test_parse_error_message_is_original_exception_message
MultiXml.parser = FailingParser
begin
MultiXml.parse("")
flunk "Expected ParseError"
rescue MultiXml::ParseError => e
# Message must be the string "Parse failed", not nil or the exception object
assert_equal "Parse failed", e.message
assert_instance_of String, e.message
end
end
def test_parse_error_message_is_not_exception_object_to_s
# If e is passed instead of e.message, the message would be e.to_s
# which includes class name like "#")
flunk "Expected ParseError"
rescue MultiXml::ParseError => e
# Should be exactly "Parse failed", not the exception's inspect/to_s
refute_match(/FailingParser/, e.message)
refute_match(/#, e.message)
end
end
end
# Tests parse error to_s behavior
class ParseWithErrorHandlingToSTest < Minitest::Test
cover "MultiXml*"
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
return unless @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
end
def test_parse_error_xml_uses_to_s_not_to_str
obj = obj_with_to_s("", to_str: "")
assert_equal "", parse_error_xml(obj)
end
def test_parse_error_xml_uses_to_s_not_raw_input
obj = obj_with_to_s("")
assert_equal "", parse_error_xml(obj)
assert_instance_of String, parse_error_xml(obj)
end
private
def obj_with_to_s(to_s_val, to_str: nil)
obj = Object.new
obj.define_singleton_method(:to_s) { to_s_val }
obj.define_singleton_method(:to_str) { to_str } if to_str
obj
end
def parse_error_xml(input)
MultiXml.parser = FailingParser
MultiXml.parse(input)
flunk "Expected ParseError"
rescue MultiXml::ParseError => e
e.xml
end
end
multi_xml-0.8.1/test/parser_detection_test.rb 0000664 0000000 0000000 00000011002 15127740662 0021433 0 ustar 00root root 0000000 0000000 require "test_helper"
# Shared setup/teardown for tests that modify MultiXml parser state
module ParserStateReset
def setup
@original_parser = MultiXml.instance_variable_get(:@parser)
end
def teardown
if @original_parser
MultiXml.instance_variable_set(:@parser, @original_parser)
elsif MultiXml.instance_variable_defined?(:@parser)
MultiXml.send(:remove_instance_variable, :@parser)
end
end
end
# Tests for find_loaded_parser method
class FindLoadedParserTest < Minitest::Test
cover "MultiXml*"
include ParserStateReset
def test_returns_best_available_parser_when_defined
assert_equal best_available_parser, MultiXml.send(:find_loaded_parser)
end
def test_returns_nokogiri_when_ox_and_libxml_not_defined
with_hidden_consts(:Ox, :LibXML) { assert_equal :nokogiri, MultiXml.send(:find_loaded_parser) }
end
def test_returns_oga_when_only_oga_defined
with_hidden_consts(:Ox, :LibXML, :Nokogiri) { assert_equal :oga, MultiXml.send(:find_loaded_parser) }
end
def test_returns_nil_when_no_parsers_defined
with_hidden_consts(:Ox, :LibXML, :Nokogiri, :Oga) { assert_nil MultiXml.send(:find_loaded_parser) }
end
private
def with_hidden_consts(*const_names)
# Only hide constants that are actually defined
existing = const_names.select { |name| Object.const_defined?(name) }
saved = existing.to_h { |name| [name, Object.send(:remove_const, name)] }
MultiXml.send(:remove_instance_variable, :@parser) if MultiXml.instance_variable_defined?(:@parser)
yield
ensure
saved&.each { |name, value| Object.const_set(name, value) }
end
end
# Tests for find_available_parser method
class FindAvailableParserTest < Minitest::Test
cover "MultiXml*"
def test_returns_symbol
assert_kind_of Symbol, MultiXml.send(:find_available_parser)
end
def test_returns_first_loadable_parser
assert_equal best_available_parser, MultiXml.send(:find_available_parser)
end
def test_returns_nil_when_no_parsers_available
with_parser_preference([["nonexistent_1", :fake1], ["nonexistent_2", :fake2]]) do
assert_nil MultiXml.send(:find_available_parser)
end
end
def test_continues_after_load_error
# Use nokogiri as fallback since it's available on all platforms
with_parser_preference([["nonexistent_parser_gem", :nonexistent], ["nokogiri", :nokogiri]]) do
assert_equal :nokogiri, MultiXml.send(:find_available_parser)
end
end
private
def with_parser_preference(preference)
original = MultiXml::PARSER_PREFERENCE.dup
MultiXml.send(:remove_const, :PARSER_PREFERENCE)
MultiXml.const_set(:PARSER_PREFERENCE, preference)
yield
ensure
MultiXml.send(:remove_const, :PARSER_PREFERENCE)
MultiXml.const_set(:PARSER_PREFERENCE, original.freeze)
end
end
# Tests for detect_parser method
class DetectParserTest < Minitest::Test
cover "MultiXml*"
include ParserStateReset
def test_returns_loaded_parser_when_available
assert_equal best_available_parser, MultiXml.send(:detect_parser)
end
def test_falls_back_to_find_available_when_loaded_returns_nil
MultiXml.stub(:find_loaded_parser, nil) do
assert_equal best_available_parser, MultiXml.send(:detect_parser)
end
end
def test_uses_find_loaded_result_not_find_available
MultiXml.stub(:find_available_parser, :rexml) do
assert_equal best_available_parser, MultiXml.send(:detect_parser)
end
end
def test_raises_when_no_parser_available
with_no_parsers_available do
assert_raises(MultiXml::NoParserError) { MultiXml.send(:detect_parser) }
end
end
private
def with_no_parsers_available
# Only remove constants that are actually defined
existing = %i[Ox LibXML Nokogiri Oga].select { |n| Object.const_defined?(n) }
saved_consts = existing.to_h { |n| [n, Object.send(:remove_const, n)] }
saved_pref = MultiXml::PARSER_PREFERENCE
MultiXml.send(:remove_const, :PARSER_PREFERENCE)
MultiXml.const_set(:PARSER_PREFERENCE, [["nonexistent", :fake]])
yield
ensure
saved_consts.each { |name, value| Object.const_set(name, value) }
MultiXml.send(:remove_const, :PARSER_PREFERENCE)
MultiXml.const_set(:PARSER_PREFERENCE, saved_pref)
end
end
# Tests for raise_no_parser_error method
class RaiseNoParserErrorTest < Minitest::Test
cover "MultiXml*"
def test_raises_no_parser_error_with_helpful_message
error = assert_raises(MultiXml::NoParserError) { MultiXml.send(:raise_no_parser_error) }
assert_match(/No XML parser detected/, error.message)
assert_match(/ox/, error.message)
end
end
multi_xml-0.8.1/test/parser_integration_test.rb 0000664 0000000 0000000 00000002160 15127740662 0022005 0 ustar 00root root 0000000 0000000 require "test_helper"
require "parser_tests"
# Parser configurations: [require_name, class_name, test_module]
DOM_PARSERS = {
"LibXML" => ["libxml", "LibXML", DomParserTests],
"REXML" => ["rexml/document", "REXML", DomParserTests],
"Nokogiri" => ["nokogiri", "Nokogiri", DomParserTests],
"Ox" => ["ox", "Ox", DomParserTests],
"Oga" => ["oga", "Oga", LenientDomParserTests]
}.freeze
SAX_PARSERS = {
"libxml_sax" => ["libxml", "LibxmlSax", SaxParserFullTests],
"nokogiri_sax" => ["nokogiri", "NokogiriSax", SaxParserFullTests]
}.freeze
# Generate test classes for each parser
DOM_PARSERS.merge(SAX_PARSERS).each do |parser_name, (require_name, class_name, test_module)|
# Suppress parse-time warnings from oga gem
if require_name == "oga"
original_verbose = $VERBOSE
$VERBOSE = nil
end
require require_name
$VERBOSE = original_verbose if require_name == "oga"
klass = Class.new(Minitest::Test) do
include test_module
const_set(:PARSER, parser_name)
end
Object.const_set("#{class_name}ParserTest", klass)
rescue LoadError
puts "Tests not run for #{parser_name} due to a LoadError"
end
multi_xml-0.8.1/test/parser_tests.rb 0000664 0000000 0000000 00000002174 15127740662 0017572 0 ustar 00root root 0000000 0000000 require "test_setup"
require "basic_tests"
require "whitespace_tests"
require "attribute_tests"
require "typecast_tests"
require "yaml_symbol_tests"
require "file_array_tests"
require "empty_type_tests"
require "entity_tests"
require "children_tests"
require "mixed_attribute_tests"
require "stream_tests"
# Common tests that run on all parsers
module ParserCommonTests
include ParserTestSetup
include ParserBasicTests
include ParserWhitespaceTests
include ParserAttributeTests
include ParserTypecastTests
include ParserYamlSymbolTests
include ParserFileArrayTests
include ParserEmptyTypeTests
include ParserEntityTests
include ParserChildrenTests
include ParserMixedAttributeTests
include ParserStreamTests
end
# Tests for DOM parsers (all parsers except SAX variants)
module DomParserTests
include ParserCommonTests
include ParserStrictErrorTests
end
# Tests for DOM parsers that don't raise on invalid XML (Oga)
module LenientDomParserTests
include ParserCommonTests
end
# Tests for SAX parsers
module SaxParserFullTests
include ParserCommonTests
include ParserStrictErrorTests
include SaxParserTests
end
multi_xml-0.8.1/test/raise_no_parser_error_test.rb 0000664 0000000 0000000 00000001552 15127740662 0022476 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests raise_no_parser_error message formatting
class RaiseNoParserErrorTest < Minitest::Test
cover "MultiXml*"
def test_raises_no_parser_error_with_message
error = assert_raises(MultiXml::NoParserError) do
MultiXml.send(:raise_no_parser_error)
end
assert_includes error.message, "No XML parser detected"
end
def test_raises_no_parser_error_mentions_parser_options
error = assert_raises(MultiXml::NoParserError) do
MultiXml.send(:raise_no_parser_error)
end
assert_includes error.message, "ox"
assert_includes error.message, "nokogiri"
end
def test_no_parser_error_message_has_no_trailing_newline
error = assert_raises(MultiXml::NoParserError) do
MultiXml.send(:raise_no_parser_error)
end
refute error.message.end_with?("\n"), "Message should not end with newline"
end
end
multi_xml-0.8.1/test/resolve_parser_test.rb 0000664 0000000 0000000 00000004474 15127740662 0021153 0 ustar 00root root 0000000 0000000 require "test_helper"
require "support/mock_decoder"
# Tests for ResolveParserTest
class ResolveParserTest < Minitest::Test
cover "MultiXml*"
def test_resolve_parser_with_module
require "multi_xml/parsers/nokogiri"
result = MultiXml.send(:resolve_parser, MultiXml::Parsers::Nokogiri)
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_resolve_parser_with_class
result = MultiXml.send(:resolve_parser, MockDecoder)
assert_equal MockDecoder, result
end
def test_resolve_parser_raises_for_invalid_spec
error = assert_raises(RuntimeError) do
MultiXml.send(:resolve_parser, 123)
end
assert_match(/Invalid parser specification/, error.message)
end
end
# Tests for ResolveParserDetailedTest
class ResolveParserDetailedTest < Minitest::Test
cover "MultiXml*"
def test_resolve_parser_accepts_module
require "multi_xml/parsers/nokogiri"
result = MultiXml.send(:resolve_parser, MultiXml::Parsers::Nokogiri)
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_resolve_parser_accepts_class
result = MultiXml.send(:resolve_parser, MockDecoder)
assert_equal MockDecoder, result
end
def test_resolve_parser_raises_for_integer
error = assert_raises(RuntimeError) do
MultiXml.send(:resolve_parser, 123)
end
assert_match(/Invalid parser/, error.message)
end
def test_resolve_parser_raises_for_nil
error = assert_raises(RuntimeError) do
MultiXml.send(:resolve_parser, nil)
end
assert_match(/Invalid parser/, error.message)
end
end
# Tests resolve_parser case statement branches
class ResolveParserCaseTest < Minitest::Test
cover "MultiXml*"
def test_resolve_parser_handles_string
result = MultiXml.send(:resolve_parser, "nokogiri")
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_resolve_parser_handles_symbol
result = MultiXml.send(:resolve_parser, :nokogiri)
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_resolve_parser_handles_module
require "multi_xml/parsers/nokogiri"
result = MultiXml.send(:resolve_parser, MultiXml::Parsers::Nokogiri)
assert_equal MultiXml::Parsers::Nokogiri, result
end
def test_resolve_parser_handles_class
result = MultiXml.send(:resolve_parser, MockDecoder)
assert_equal MockDecoder, result
end
end
multi_xml-0.8.1/test/rexml_array_branch_test.rb 0000664 0000000 0000000 00000000617 15127740662 0021755 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests REXML parser add_to_hash behavior
class RexmlArrayBranchTest < Minitest::Test
cover "MultiXml*"
def test_add_to_hash_wraps_array_value_in_array
require "multi_xml/parsers/rexml"
hash = {}
value = %w[item1 item2]
result = MultiXml::Parsers::Rexml.send(:add_to_hash, hash, "key", value)
assert_equal [%w[item1 item2]], result["key"]
end
end
multi_xml-0.8.1/test/sax_handler_test.rb 0000664 0000000 0000000 00000001730 15127740662 0020400 0 ustar 00root root 0000000 0000000 require "test_helper"
require "multi_xml/parsers/sax_handler"
# Test harness that includes SaxHandler for testing
class SaxHandlerTestHarness
include MultiXml::Parsers::SaxHandler
def initialize
initialize_handler
end
# Expose private method for testing
def test_normalize_attrs(attrs)
normalize_attrs(attrs)
end
end
# Tests for SaxHandler normalize_attrs method
class SaxHandlerNormalizeAttrsTest < Minitest::Test
cover "MultiXml*"
def setup
@handler = SaxHandlerTestHarness.new
end
def test_normalize_attrs_returns_hash_when_given_hash
attrs = {"class" => "foo", "id" => "bar"}
result = @handler.test_normalize_attrs(attrs)
assert_equal attrs, result
assert_same attrs, result # Should return the same object
end
def test_normalize_attrs_converts_array_to_hash
attrs = [%w[class foo], %w[id bar]]
result = @handler.test_normalize_attrs(attrs)
assert_equal({"class" => "foo", "id" => "bar"}, result)
end
end
multi_xml-0.8.1/test/stream_tests.rb 0000664 0000000 0000000 00000001610 15127740662 0017563 0 ustar 00root root 0000000 0000000 # Tests parsing from IO streams like pipes (not just strings)
module ParserStreamTests
def test_duplexed_stream_parses_correctly
rd, wr = IO.pipe
Thread.new do
"".each_char { |chunk| wr << chunk }
wr.close
end
assert_equal({"user" => nil}, MultiXml.parse(rd))
end
end
# Tests specific to SAX parsers that accept both String and IO input directly
module SaxParserTests
def test_sax_parser_direct_string_input
result = MultiXml.parser.parse("content")
assert_equal({"root" => {"__content__" => "content"}}, result)
end
def test_sax_parser_direct_io_input
result = MultiXml.parser.parse(StringIO.new("content"))
assert_equal({"root" => {"__content__" => "content"}}, result)
end
def test_sax_parser_direct_empty_io_input
result = MultiXml.parser.parse(StringIO.new(""))
assert_empty(result)
end
end
multi_xml-0.8.1/test/support/ 0000775 0000000 0000000 00000000000 15127740662 0016237 5 ustar 00root root 0000000 0000000 multi_xml-0.8.1/test/support/mock_decoder.rb 0000664 0000000 0000000 00000000055 15127740662 0021202 0 ustar 00root root 0000000 0000000 class MockDecoder
def self.parse
end
end
multi_xml-0.8.1/test/test_helper.rb 0000664 0000000 0000000 00000001313 15127740662 0017364 0 ustar 00root root 0000000 0000000 def jruby?
RUBY_PLATFORM == "java"
end
def windows?
Gem.win_platform?
end
# Returns the best available parser for the current platform
# ox and libxml are not available on Windows or JRuby
def best_available_parser
if windows? || jruby?
:nokogiri
else
:ox
end
end
# Returns an array of parser constants that are actually loaded
def loaded_parser_consts
%i[Ox LibXML Nokogiri Oga].select { |name| Object.const_defined?(name) }
end
require "simplecov"
SimpleCov.start do
add_filter "/test"
enable_coverage :branch
minimum_coverage line: 100, branch: 100 unless ENV["MUTANT"]
end
require "multi_xml"
require "minitest/autorun"
require "minitest/mock"
require "mutant/minitest/coverage"
multi_xml-0.8.1/test/test_setup.rb 0000664 0000000 0000000 00000000715 15127740662 0017252 0 ustar 00root root 0000000 0000000 # Shared setup that configures the parser backend before each test
module ParserTestSetup
def self.included(base)
base.extend(Mutant::Minitest::Coverage)
base.cover("MultiXml*")
end
def setup
MultiXml.parser = self.class::PARSER
LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER) if %w[LibXML libxml_sax].include?(self.class::PARSER)
rescue LoadError
skip "Parser #{self.class::PARSER} couldn't be loaded"
end
end
multi_xml-0.8.1/test/test_subclasses.rb 0000664 0000000 0000000 00000000167 15127740662 0020262 0 ustar 00root root 0000000 0000000 # Subclasses for testing is_a? vs instance_of? behavior
class HashSubclass < Hash
end
class ArraySubclass < Array
end
multi_xml-0.8.1/test/typecast_array_test.rb 0000664 0000000 0000000 00000004265 15127740662 0021150 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests typecast_array behavior including unwrapping single elements
class TypecastArrayTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_with_single_element_returns_first
assert_equal({"key" => "value"}, typecast_array([{"key" => "value"}], []))
end
def test_with_multiple_elements_returns_array
assert_equal [{"a" => 1}, {"b" => 2}], typecast_array([{"a" => 1}, {"b" => 2}], [])
end
def test_with_empty_array
assert_empty typecast_array([], [])
end
def test_recursively_typecasts
assert_equal 42, typecast_array([{"type" => "integer", "__content__" => "42"}], [])
end
def test_mutates_original
input = [{"type" => "integer", "__content__" => "42"}]
original_first = input.first
typecast_array(input, [])
assert_equal 42, input.first
refute_same original_first, input.first
end
def test_one_returns_false_for_empty
result = typecast_array([], [])
assert_empty result
end
def test_one_returns_true_for_single
result = typecast_array(["only"], [])
assert_equal "only", result
end
def test_one_returns_false_for_multiple
result = typecast_array(%w[one two], [])
assert_equal %w[one two], result
end
def test_passes_disallowed_types_to_nested_calls
input = [{"type" => "yaml", "__content__" => "test: value"}]
result = typecast_array(input, [])
assert_equal({"test" => "value"}, result)
end
def test_with_custom_disallowed_types
input = [{"type" => "integer", "__content__" => "42"}]
assert_raises(MultiXml::DisallowedTypeError) do
typecast_array(input, ["integer"])
end
end
def test_preserves_array_with_nil_and_value
input = [{}, {"__content__" => "value"}]
result = typecast_array(input, [])
assert_equal [nil, "value"], result
end
def test_preserves_array_with_multiple_nils
input = [{}, {}]
result = typecast_array(input, [])
assert_equal [nil, nil], result
end
def test_preserves_array_with_false_and_value
input = [{"type" => "boolean", "__content__" => "false"}, {"__content__" => "value"}]
result = typecast_array(input, [])
assert_equal [false, "value"], result
end
end
multi_xml-0.8.1/test/typecast_children_test.rb 0000664 0000000 0000000 00000003667 15127740662 0021627 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests typecast_children behavior with StringIO file handling
class TypecastChildrenTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_unwraps_stringio_file
file = StringIO.new("content")
assert_same file, typecast_children({"file" => file, "other" => "data"}, [])
end
def test_returns_hash_when_file_not_stringio
result = typecast_children({"file" => "not a stringio", "other" => "data"}, [])
assert_kind_of Hash, result
assert_equal "not a stringio", result["file"]
end
def test_returns_hash_when_no_file_key
result = typecast_children({"name" => "value", "other" => "data"}, [])
assert_kind_of Hash, result
assert_equal "value", result["name"]
end
def test_returns_hash_when_file_is_nil
result = typecast_children({"file" => nil, "other" => "data"}, [])
assert_kind_of Hash, result
assert_nil result["file"]
end
def test_stringio_subclass_is_unwrapped
klass = Class.new(StringIO)
file = klass.new("data")
hash = {"file" => file, "other" => "stuff"}
result = typecast_children(hash, [])
assert_equal file, result
assert_kind_of StringIO, result
end
def test_exact_stringio_is_unwrapped
file = StringIO.new("data")
hash = {"file" => file}
result = typecast_children(hash, [])
assert_equal file, result
end
def test_with_file_key_containing_integer
hash = {"file" => 42, "name" => "test"}
result = typecast_children(hash, [])
assert_kind_of Hash, result
assert_equal 42, result["file"]
end
def test_uses_bracket_access_for_file
hash = {"name" => "test", "data" => "value"}
result = typecast_children(hash, [])
assert_kind_of Hash, result
assert_equal "test", result["name"]
end
def test_returns_file_not_fetches
file = StringIO.new("content")
hash = {"file" => file}
result = typecast_children(hash, [])
assert_same file, result
end
end
multi_xml-0.8.1/test/typecast_hash_test.rb 0000664 0000000 0000000 00000001276 15127740662 0020754 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests typecast_hash type attribute handling
class TypecastHashTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_raises_disallowed_type_error_with_type
error = assert_raises(MultiXml::DisallowedTypeError) do
typecast_hash({"type" => "yaml"}, ["yaml"])
end
assert_equal "yaml", error.type
end
def test_passes_type_to_convert_hash
hash = {"type" => "array", "item" => %w[a b c]}
result = typecast_hash(hash, [])
assert_equal %w[a b c], result
end
def test_type_affects_conversion
hash = {"type" => "string", "nil" => "false"}
result = typecast_hash(hash, [])
assert_equal "", result
end
end
multi_xml-0.8.1/test/typecast_tests.rb 0000664 0000000 0000000 00000006447 15127740662 0020141 0 ustar 00root root 0000000 0000000 # Tests type attribute coercion (boolean, integer, date, datetime, decimal, base64)
module ParserTypecastTests
def test_typecast_xml_value_true_typecasts_string_type
xml = "Settings" \
'Test'
assert_equal "", MultiXml.parse(xml)["global_settings"]["group"]["setting"]
end
def test_typecast_xml_value_false_preserves_type_attribute
xml = "Settings" \
'Test'
setting = MultiXml.parse(xml, typecast_xml_value: false)["global_settings"]["group"]["setting"]
assert_equal({"type" => "string", "description" => {"__content__" => "Test"}}, setting)
end
def test_symbolize_keys_option
xml = 'Wynn Netherland'
expected = {users: {user: [{name: "Erik Berlin"}, {name: "Wynn Netherland"}]}}
assert_equal expected, MultiXml.parse(xml, symbolize_keys: true)
end
def test_boolean_true_returns_true
assert MultiXml.parse('true')["tag"]
end
def test_boolean_false_returns_false
refute MultiXml.parse('false')["tag"]
end
def test_boolean_1_returns_true
assert MultiXml.parse('1')["tag"]
end
def test_boolean_0_returns_false
refute MultiXml.parse('0')["tag"]
end
def test_integer_returns_positive_integer
result = MultiXml.parse('1')["tag"]
assert_kind_of Integer, result
assert_equal 1, result
end
def test_integer_returns_negative_integer
result = MultiXml.parse('-1')["tag"]
assert_kind_of Integer, result
assert_equal(-1, result)
end
def test_string_type_returns_string
result = MultiXml.parse('')["tag"]
assert_kind_of String, result
assert_equal "", result
end
def test_date_type_returns_date
result = MultiXml.parse('1970-01-01')["tag"]
assert_kind_of Date, result
assert_equal Date.parse("1970-01-01"), result
end
def test_datetime_type_returns_time
result = MultiXml.parse('1970-01-01 00:00')["tag"]
assert_kind_of Time, result
assert_equal Time.parse("1970-01-01 00:00"), result
end
def test_date_time_type_returns_time
result = MultiXml.parse('1970-01-01 00:00')["tag"]
assert_kind_of Time, result
assert_equal Time.parse("1970-01-01 00:00"), result
end
def test_double_type_returns_float
result = MultiXml.parse('3.14159265358979')["tag"]
assert_kind_of Float, result
assert_in_delta(3.14159265358979, result)
end
def test_decimal_type_returns_bigdecimal
result = MultiXml.parse('3.14159265358979')["tag"]
assert_kind_of BigDecimal, result
assert_in_delta(3.14159265358979, result)
end
def test_base64binary_type_returns_decoded_string
result = MultiXml.parse('aW1hZ2UucG5n')["tag"]
assert_kind_of String, result
assert_equal "image.png", result
end
end
multi_xml-0.8.1/test/typecast_xml_value_test.rb 0000664 0000000 0000000 00000001720 15127740662 0022017 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests typecast_xml_value with default and custom disallowed types
class TypecastXmlValueTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_uses_default_disallowed_types
assert_raises(MultiXml::DisallowedTypeError) do
typecast_xml_value({"type" => "yaml", "__content__" => "test"})
end
end
def test_with_explicit_empty_disallowed_types
result = typecast_xml_value({"type" => "yaml", "__content__" => "test"}, [])
assert_equal "test", result
end
def test_passes_disallowed_types_to_array
value = [{"type" => "yaml", "__content__" => "key: value"}, "second"]
result = typecast_xml_value(value, [])
assert_equal [{"key" => "value"}, "second"], result
end
def test_custom_disallowed_blocks_in_array
value = [{"type" => "integer", "__content__" => "42"}]
assert_raises(MultiXml::DisallowedTypeError) do
typecast_xml_value(value, ["integer"])
end
end
end
multi_xml-0.8.1/test/unwrap_if_simple_test.rb 0000664 0000000 0000000 00000001742 15127740662 0021456 0 ustar 00root root 0000000 0000000 require "test_helper"
# Tests unwrap_if_simple value merging behavior
class UnwrapIfSimpleTest < Minitest::Test
cover "MultiXml*"
include MultiXml::Helpers
def test_merges_value_when_multiple_keys
hash = {"attr1" => "val1", "attr2" => "val2"}
value = "converted"
result = unwrap_if_simple(hash, value)
assert_equal({"attr1" => "val1", "attr2" => "val2", MultiXml::TEXT_CONTENT_KEY => "converted"}, result)
assert_equal "converted", result[MultiXml::TEXT_CONTENT_KEY]
end
def test_returns_value_when_single_key
hash = {"only_key" => "val"}
value = "the_value"
result = unwrap_if_simple(hash, value)
assert_equal "the_value", result
end
def test_value_must_be_in_result
hash = {"type" => "string", "other" => "data"}
value = "important_content"
result = unwrap_if_simple(hash, value)
assert_includes result.keys, MultiXml::TEXT_CONTENT_KEY
assert_equal "important_content", result[MultiXml::TEXT_CONTENT_KEY]
end
end
multi_xml-0.8.1/test/whitespace_tests.rb 0000664 0000000 0000000 00000001711 15127740662 0020426 0 ustar 00root root 0000000 0000000 # Tests whitespace preservation in text content vs. stripping around child elements
module ParserWhitespaceTests
def test_preserves_whitespace_when_no_children_or_attributes
assert_equal " ", MultiXml.parse(" ")["tag"]
end
def test_preserves_multiple_spaces_when_no_children_or_attributes
assert_equal " ", MultiXml.parse(" ")["tag"]
end
def test_preserves_newlines_and_tabs_when_no_children_or_attributes
assert_equal "\n\t\n", MultiXml.parse("\n\t\n")["tag"]
end
def test_strips_whitespace_when_there_are_child_elements
assert_equal({"child" => nil}, MultiXml.parse(" ")["tag"])
end
def test_strips_whitespace_when_there_are_attributes
assert_equal({"attr" => "val"}, MultiXml.parse(' ')["tag"])
end
def test_preserves_content_with_surrounding_whitespace
assert_equal " hello ", MultiXml.parse(" hello ")["tag"]
end
end
multi_xml-0.8.1/test/yaml_symbol_tests.rb 0000664 0000000 0000000 00000002305 15127740662 0020621 0 ustar 00root root 0000000 0000000 # Tests YAML and Symbol type handling, including disallowed_types security option
module ParserYamlSymbolTests
def test_yaml_type_raises_disallowed_type_error_by_default
xml = "--- \n1: returns an integer\n:message: Have a nice day\n" \
"array: \n- has-dashes: true\n has_underscores: true\n"
assert_raises(MultiXml::DisallowedTypeError) { MultiXml.parse(xml)["tag"] }
end
def test_yaml_type_returns_parsed_yaml_when_allowed
xml = "--- \n1: returns an integer\n:message: Have a nice day\n" \
"array: \n- has-dashes: true\n has_underscores: true\n"
expected = {:message => "Have a nice day", 1 => "returns an integer",
"array" => [{"has-dashes" => true, "has_underscores" => true}]}
assert_equal expected, MultiXml.parse(xml, disallowed_types: [])["tag"]
end
def test_symbol_type_raises_disallowed_type_error
assert_raises(MultiXml::DisallowedTypeError) { MultiXml.parse('my_symbol')["tag"] }
end
def test_symbol_type_returns_symbol_when_allowed
assert_equal :my_symbol, MultiXml.parse('my_symbol', disallowed_types: [])["tag"]
end
end