pax_global_header00006660000000000000000000000064151277406620014524gustar00rootroot0000000000000052 comment=3ad93f88b66ca3f8666ea92e3ae09007c2c2961c multi_xml-0.8.1/000077500000000000000000000000001512774066200135445ustar00rootroot00000000000000multi_xml-0.8.1/.github/000077500000000000000000000000001512774066200151045ustar00rootroot00000000000000multi_xml-0.8.1/.github/FUNDING.yml000066400000000000000000000000211512774066200167120ustar00rootroot00000000000000github: [sferik] multi_xml-0.8.1/.github/workflows/000077500000000000000000000000001512774066200171415ustar00rootroot00000000000000multi_xml-0.8.1/.github/workflows/docs.yml000066400000000000000000000005511512774066200206150ustar00rootroot00000000000000name: 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.yml000066400000000000000000000003771512774066200211700ustar00rootroot00000000000000name: 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.yml000066400000000000000000000007621512774066200212010ustar00rootroot00000000000000name: 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.yml000066400000000000000000000017201512774066200206430ustar00rootroot00000000000000name: 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.yml000066400000000000000000000013321512774066200210250ustar00rootroot00000000000000name: 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.yml000066400000000000000000000004031512774066200216400ustar00rootroot00000000000000name: 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/.gitignore000066400000000000000000000001301512774066200155260ustar00rootroot00000000000000*.gem *~ .bundle .rvmrc .yardoc Gemfile.lock coverage/* doc/* log/* measurement/* pkg/* multi_xml-0.8.1/.mutant.yml000066400000000000000000000002511512774066200156530ustar00rootroot00000000000000usage: opensource integration: name: minitest includes: - lib - test requires: - multi_xml - mutant/minitest/coverage matcher: subjects: - MultiXml* multi_xml-0.8.1/.rspec000066400000000000000000000000271512774066200146600ustar00rootroot00000000000000--color --order random multi_xml-0.8.1/.rubocop.yml000066400000000000000000000022111512774066200160120ustar00rootroot00000000000000require: - 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/.yardopts000066400000000000000000000001371512774066200154130ustar00rootroot00000000000000--no-private --protected --markup markdown - CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md multi_xml-0.8.1/CHANGELOG.md000066400000000000000000000136251512774066200153640ustar00rootroot000000000000000.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.md000066400000000000000000000037341512774066200160040ustar00rootroot00000000000000## 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/Gemfile000066400000000000000000000012371512774066200150420ustar00rootroot00000000000000source "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.md000066400000000000000000000020441512774066200151500ustar00rootroot00000000000000Copyright (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.md000066400000000000000000000046331512774066200150310ustar00rootroot00000000000000# 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/Rakefile000066400000000000000000000031261512774066200152130ustar00rootroot00000000000000require "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/Steepfile000066400000000000000000000010121512774066200154010ustar00rootroot00000000000000D = 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/000077500000000000000000000000001512774066200143145ustar00rootroot00000000000000multi_xml-0.8.1/bin/console000077500000000000000000000003751512774066200157110ustar00rootroot00000000000000#!/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/setup000077500000000000000000000002031512774066200153750ustar00rootroot00000000000000#!/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/000077500000000000000000000000001512774066200143125ustar00rootroot00000000000000multi_xml-0.8.1/lib/multi_xml.rb000066400000000000000000000155011512774066200166530ustar00rootroot00000000000000require "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/000077500000000000000000000000001512774066200163245ustar00rootroot00000000000000multi_xml-0.8.1/lib/multi_xml/constants.rb000066400000000000000000000100271512774066200206650ustar00rootroot00000000000000module 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.rb000066400000000000000000000055021512774066200201670ustar00rootroot00000000000000module 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.rb000066400000000000000000000035601512774066200206000ustar00rootroot00000000000000module 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.rb000066400000000000000000000174641512774066200203270ustar00rootroot00000000000000module 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/000077500000000000000000000000001512774066200200035ustar00rootroot00000000000000multi_xml-0.8.1/lib/multi_xml/parsers/dom_parser.rb000066400000000000000000000055721512774066200224740ustar00rootroot00000000000000module 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.rb000066400000000000000000000023351512774066200216220ustar00rootroot00000000000000require "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.rb000066400000000000000000000051161512774066200224750ustar00rootroot00000000000000require "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.rb000066400000000000000000000025221512774066200221520ustar00rootroot00000000000000require "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.rb000066400000000000000000000050511512774066200230250ustar00rootroot00000000000000require "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.rb000066400000000000000000000034641512774066200211050ustar00rootroot00000000000000require "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.rb000066400000000000000000000073301512774066200207610ustar00rootroot00000000000000require "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.rb000066400000000000000000000067421512774066200214700ustar00rootroot00000000000000require "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.rb000066400000000000000000000063201512774066200226210ustar00rootroot00000000000000require "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.rb000066400000000000000000000002401512774066200203320ustar00rootroot00000000000000module 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.gemspec000066400000000000000000000027051512774066200171270ustar00rootroot00000000000000require_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/000077500000000000000000000000001512774066200143265ustar00rootroot00000000000000multi_xml-0.8.1/sig/multi_xml.rbs000066400000000000000000000175371512774066200170650ustar00rootroot00000000000000# 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/000077500000000000000000000000001512774066200145235ustar00rootroot00000000000000multi_xml-0.8.1/test/attribute_tests.rb000066400000000000000000000015031512774066200202740ustar00rootroot00000000000000# 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.rb000066400000000000000000000027571512774066200173660ustar00rootroot00000000000000# 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.rb000066400000000000000000000041031512774066200200600ustar00rootroot00000000000000# 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.rb000066400000000000000000000025211512774066200232210ustar00rootroot00000000000000require "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.rb000066400000000000000000000053631512774066200206010ustar00rootroot00000000000000require "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.rb000066400000000000000000000017161512774066200223720ustar00rootroot00000000000000require "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.rb000066400000000000000000000005641512774066200215270ustar00rootroot00000000000000require "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.rb000066400000000000000000000013341512774066200225110ustar00rootroot00000000000000require "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.rb000066400000000000000000000024501512774066200213000ustar00rootroot00000000000000require "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.rb000066400000000000000000000026751512774066200205030ustar00rootroot00000000000000# 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.rb000066400000000000000000000026071512774066200204460ustar00rootroot00000000000000require "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.rb000066400000000000000000000015621512774066200176120ustar00rootroot00000000000000# 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.rb000066400000000000000000000025371512774066200261520ustar00rootroot00000000000000require "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.rb000066400000000000000000000060421512774066200225120ustar00rootroot00000000000000require "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.rb000066400000000000000000000031061512774066200204070ustar00rootroot00000000000000# 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.rb000066400000000000000000000116531512774066200200400ustar00rootroot00000000000000require "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.rb000066400000000000000000000014311512774066200207710ustar00rootroot00000000000000require "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.rb000066400000000000000000000070011512774066200204000ustar00rootroot00000000000000require "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.rb000066400000000000000000000054511512774066200214700ustar00rootroot00000000000000# 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.rb000066400000000000000000000210621512774066200201220ustar00rootroot00000000000000require "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.rb000066400000000000000000000015161512774066200213310ustar00rootroot00000000000000require "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.rb000066400000000000000000000025721512774066200204400ustar00rootroot00000000000000require "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.rb000066400000000000000000000032411512774066200205610ustar00rootroot00000000000000require "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.rb000066400000000000000000000124231512774066200207760ustar00rootroot00000000000000require "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.rb000066400000000000000000000175501512774066200233410ustar00rootroot00000000000000require "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(/#", 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.rb000066400000000000000000000110021512774066200214330ustar00rootroot00000000000000require "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.rb000066400000000000000000000021601512774066200220050ustar00rootroot00000000000000require "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.rb000066400000000000000000000021741512774066200175720ustar00rootroot00000000000000require "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.rb000066400000000000000000000015521512774066200224760ustar00rootroot00000000000000require "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.rb000066400000000000000000000044741512774066200211530ustar00rootroot00000000000000require "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.rb000066400000000000000000000006171512774066200217550ustar00rootroot00000000000000require "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.rb000066400000000000000000000017301512774066200204000ustar00rootroot00000000000000require "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.rb000066400000000000000000000016101512774066200175630ustar00rootroot00000000000000# 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/000077500000000000000000000000001512774066200162375ustar00rootroot00000000000000multi_xml-0.8.1/test/support/mock_decoder.rb000066400000000000000000000000551512774066200212020ustar00rootroot00000000000000class MockDecoder def self.parse end end multi_xml-0.8.1/test/test_helper.rb000066400000000000000000000013131512774066200173640ustar00rootroot00000000000000def 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.rb000066400000000000000000000007151512774066200172520ustar00rootroot00000000000000# 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.rb000066400000000000000000000001671512774066200202620ustar00rootroot00000000000000# 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.rb000066400000000000000000000042651512774066200211500ustar00rootroot00000000000000require "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.rb000066400000000000000000000036671512774066200216270ustar00rootroot00000000000000require "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.rb000066400000000000000000000012761512774066200207540ustar00rootroot00000000000000require "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.rb000066400000000000000000000064471512774066200201410ustar00rootroot00000000000000# 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.rb000066400000000000000000000017201512774066200220170ustar00rootroot00000000000000require "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.rb000066400000000000000000000017421512774066200214560ustar00rootroot00000000000000require "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.rb000066400000000000000000000017111512774066200204260ustar00rootroot00000000000000# 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.rb000066400000000000000000000023051512774066200206210ustar00rootroot00000000000000# 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