pax_global_header00006660000000000000000000000064150044660570014521gustar00rootroot0000000000000052 comment=35bff56b82185983620fe60a095752b3d9a7c10f multi_xml-0.7.2/000077500000000000000000000000001500446605700135415ustar00rootroot00000000000000multi_xml-0.7.2/.github/000077500000000000000000000000001500446605700151015ustar00rootroot00000000000000multi_xml-0.7.2/.github/FUNDING.yml000066400000000000000000000000211500446605700167070ustar00rootroot00000000000000github: [sferik] multi_xml-0.7.2/.github/workflows/000077500000000000000000000000001500446605700171365ustar00rootroot00000000000000multi_xml-0.7.2/.github/workflows/ci.yml000066400000000000000000000005241500446605700202550ustar00rootroot00000000000000name: ci on: [push, pull_request] jobs: build: strategy: matrix: ruby: ["3.2", "3.3", "3.4", "jruby-10"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake test multi_xml-0.7.2/.gitignore000066400000000000000000000001301500446605700155230ustar00rootroot00000000000000*.gem *~ .bundle .rvmrc .yardoc Gemfile.lock coverage/* doc/* log/* measurement/* pkg/* multi_xml-0.7.2/.rspec000066400000000000000000000000271500446605700146550ustar00rootroot00000000000000--color --order random multi_xml-0.7.2/.rubocop.yml000066400000000000000000000017201500446605700160130ustar00rootroot00000000000000require: - standard plugins: - rubocop-performance - rubocop-rake - rubocop-rspec - standard-performance AllCops: NewCops: enable TargetRubyVersion: 3.2 Layout/ArgumentAlignment: EnforcedStyle: with_fixed_indentation IndentationWidth: 2 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/Documentation: Enabled: false Style/FrozenStringLiteralComment: EnforcedStyle: never Style/OpenStructUse: Enabled: false Style/StringLiterals: EnforcedStyle: double_quotes Style/StringLiteralsInInterpolation: EnforcedStyle: double_quotes Style/TernaryParentheses: EnforcedStyle: require_parentheses multi_xml-0.7.2/.yardopts000066400000000000000000000001371500446605700154100ustar00rootroot00000000000000--no-private --protected --markup markdown - CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md multi_xml-0.7.2/CHANGELOG.md000066400000000000000000000121271500446605700153550ustar00rootroot000000000000000.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.7.2/CONTRIBUTING.md000066400000000000000000000037341500446605700160010ustar00rootroot00000000000000## 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.7.2/Gemfile000066400000000000000000000010341500446605700150320ustar00rootroot00000000000000source "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 "rake", ">= 13.2.1" gem "rspec", ">= 3.12" gem "rubocop", ">= 1.62.1" gem "rubocop-performance", ">= 1.20.2" gem "rubocop-rake", ">= 0.6" gem "rubocop-rspec", ">= 2.24" gem "simplecov", ">= 0.22" gem "standard", ">= 1.35.1" gem "standard-performance", ">= 1.3.1" gem "yard", ">= 0.9.36" gem "yardstick", ">= 0.9.9" gemspec multi_xml-0.7.2/LICENSE.md000066400000000000000000000020441500446605700151450ustar00rootroot00000000000000Copyright (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.7.2/README.md000066400000000000000000000046401500446605700150240ustar00rootroot00000000000000# 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 * JRuby 10 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.7.2/Rakefile000066400000000000000000000013041500446605700152040ustar00rootroot00000000000000require "bundler" Bundler::GemHelper.install_tasks require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) task test: :spec 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 = 48.8 end task default: %i[spec rubocop verify_measurements] multi_xml-0.7.2/bin/000077500000000000000000000000001500446605700143115ustar00rootroot00000000000000multi_xml-0.7.2/bin/console000077500000000000000000000003751500446605700157060ustar00rootroot00000000000000#!/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.7.2/bin/setup000077500000000000000000000002031500446605700153720ustar00rootroot00000000000000#!/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.7.2/lib/000077500000000000000000000000001500446605700143075ustar00rootroot00000000000000multi_xml-0.7.2/lib/multi_xml.rb000066400000000000000000000233351500446605700166540ustar00rootroot00000000000000require "bigdecimal" require "date" require "stringio" require "time" require "yaml" module MultiXml # rubocop:disable Metrics/ModuleLength class ParseError < StandardError; end class NoParserError < StandardError; end class DisallowedTypeError < StandardError def initialize(type) super("Disallowed type attribute: #{type.inspect}") end end unless defined?(REQUIREMENT_MAP) REQUIREMENT_MAP = [ ["ox", :ox], ["libxml", :libxml], ["nokogiri", :nokogiri], ["rexml/document", :rexml], ["oga", :oga] ].freeze end CONTENT_ROOT = "__content__".freeze unless defined?(CONTENT_ROOT) unless defined?(PARSING) float_proc = proc { |float| float.to_f } datetime_proc = proc { |time| Time.parse(time).utc rescue DateTime.parse(time).utc } # rubocop:disable Style/RescueModifier PARSING = { "symbol" => proc { |symbol| symbol.to_sym }, "date" => proc { |date| Date.parse(date) }, "datetime" => datetime_proc, "dateTime" => datetime_proc, "integer" => proc { |integer| integer.to_i }, "float" => float_proc, "double" => float_proc, "decimal" => proc { |number| BigDecimal(number) }, "boolean" => proc { |boolean| !%w[0 false].include?(boolean.strip) }, "string" => proc { |string| string.to_s }, "yaml" => proc { |yaml| YAML.load(yaml) rescue yaml }, # rubocop:disable Style/RescueModifier "base64Binary" => proc { |binary| base64_decode(binary) }, "binary" => proc { |binary, entity| parse_binary(binary, entity) }, "file" => proc { |file, entity| parse_file(file, entity) } }.freeze end unless defined?(TYPE_NAMES) TYPE_NAMES = { "Symbol" => "symbol", "Integer" => "integer", "BigDecimal" => "decimal", "Float" => "float", "TrueClass" => "boolean", "FalseClass" => "boolean", "Date" => "date", "DateTime" => "datetime", "Time" => "datetime", "Array" => "array", "Hash" => "hash" }.freeze end DISALLOWED_XML_TYPES = %w[symbol yaml].freeze DEFAULT_OPTIONS = { typecast_xml_value: true, disallowed_types: DISALLOWED_XML_TYPES, symbolize_keys: false }.freeze class << self # Get the current parser class. def parser return @parser if defined?(@parser) self.parser = default_parser @parser end # The default parser based on what you currently # have loaded and installed. First checks to see # if any parsers are already loaded, then checks # to see which are installed if none are loaded. def default_parser return :ox if defined?(::Ox) return :libxml if defined?(::LibXML) return :nokogiri if defined?(::Nokogiri) return :oga if defined?(::Oga) REQUIREMENT_MAP.each do |library, parser| require library return parser rescue LoadError next end raise(NoParserError, "No XML parser detected. If you're using Rubinius and Bundler, try adding an XML parser to your Gemfile (e.g. libxml-ruby, nokogiri, or rubysl-rexml). For more information, see https://github.com/sferik/multi_xml/issues/42.") end # Set the XML parser utilizing a symbol, string, or class. # Supported by default are: # # * :libxml # * :nokogiri # * :ox # * :rexml # * :oga def parser=(new_parser) case new_parser when String, Symbol require "multi_xml/parsers/#{new_parser.to_s.downcase}" @parser = MultiXml::Parsers.const_get(new_parser.to_s.split("_").collect(&:capitalize).join.to_s) when Class, Module @parser = new_parser else raise("Did not recognize your parser specification. Please specify either a symbol or a class.") end end # Parse an XML string or IO into Ruby. # # Options # # :symbolize_keys :: If true, will use symbols instead of strings for the keys. # # :disallowed_types :: Types to disallow from being typecasted. Defaults to `['yaml', 'symbol']`. Use `[]` to allow all types. # # :typecast_xml_value :: If true, won't typecast values for parsed document def parse(xml, options = {}) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity xml ||= "" options = DEFAULT_OPTIONS.merge(options) xml = xml.strip if xml.respond_to?(:strip) begin xml = StringIO.new(xml) unless xml.respond_to?(:read) char = xml.getc return {} if char.nil? xml.ungetc(char) hash = undasherize_keys(parser.parse(xml) || {}) hash = typecast_xml_value(hash, options[:disallowed_types]) if options[:typecast_xml_value] rescue DisallowedTypeError raise rescue parser.parse_error => e raise(ParseError, e.message, e.backtrace) end hash = symbolize_keys(hash) if options[:symbolize_keys] hash end # This module decorates files with the original_filename # and content_type methods. module FileLike # :nodoc: attr_writer :original_filename, :content_type def original_filename @original_filename || "untitled" end def content_type @content_type || "application/octet-stream" end end private # TODO: Add support for other encodings def parse_binary(binary, entity) # :nodoc: case entity["encoding"] when "base64" base64_decode(binary) else binary end end def parse_file(file, entity) f = StringIO.new(base64_decode(file)) f.extend(FileLike) f.original_filename = entity["name"] f.content_type = entity["content_type"] f end def base64_decode(input) input.unpack1("m") end def symbolize_keys(params) case params when Hash params.inject({}) do |result, (key, value)| result.merge(key.to_sym => symbolize_keys(value)) end when Array params.collect { |value| symbolize_keys(value) } else params end end def undasherize_keys(params) case params when Hash params.each_with_object({}) do |(key, value), hash| hash[key.to_s.tr("-", "_")] = undasherize_keys(value) hash end when Array params.collect { |value| undasherize_keys(value) } else params end end def typecast_xml_value(value, disallowed_types = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity disallowed_types ||= DISALLOWED_XML_TYPES case value when Hash if value.include?("type") && !value["type"].is_a?(Hash) && disallowed_types.include?(value["type"]) raise(DisallowedTypeError, value["type"]) end if value["type"] == "array" # this commented-out suggestion helps to avoid the multiple attribute # problem, but it breaks when there is only one item in the array. # # from: https://github.com/jnunemaker/httparty/issues/102 # # _, entries = value.detect { |k, v| k != 'type' && v.is_a?(Array) } # This attempt fails to consider the order that the detect method # retrieves the entries. # _, entries = value.detect {|key, _| key != 'type'} # This approach ignores attribute entries that are not convertable # to an Array which allows attributes to be ignored. _, entries = value.detect { |k, v| k != "type" && (v.is_a?(Array) || v.is_a?(Hash)) } case entries when NilClass [] when String [] if entries.strip.empty? when Array entries.collect { |entry| typecast_xml_value(entry, disallowed_types) } when Hash [typecast_xml_value(entries, disallowed_types)] else raise("can't typecast #{entries.class.name}: #{entries.inspect}") end elsif value.key?(CONTENT_ROOT) content = value[CONTENT_ROOT] block = PARSING[value["type"]] if block if block.arity == 1 value.delete("type") if PARSING[value["type"]] if value.keys.size > 1 value[CONTENT_ROOT] = block.call(content) value else block.call(content) end else block.call(content, value) end else (value.keys.size > 1) ? value : content end elsif value["type"] == "string" && value["nil"] != "true" "" # blank or nil parsed values are represented by nil elsif value.empty? || value["nil"] == "true" nil # If the type is the only element which makes it then # this still makes the value nil, except if type is # a XML node(where type['value'] is a Hash) elsif value["type"] && value.size == 1 && !value["type"].is_a?(Hash) nil else xml_value = value.each_with_object({}) do |(k, v), hash| hash[k] = typecast_xml_value(v, disallowed_types) hash end # Turn {:files => {:file => #} into {:files => #} so it is compatible with # how multipart uploaded files from HTML appear (xml_value["file"].is_a?(StringIO)) ? xml_value["file"] : xml_value end when Array value.map! { |i| typecast_xml_value(i, disallowed_types) } (value.length > 1) ? value : value.first when String value else raise("can't typecast #{value.class.name}: #{value.inspect}") end end end end multi_xml-0.7.2/lib/multi_xml/000077500000000000000000000000001500446605700163215ustar00rootroot00000000000000multi_xml-0.7.2/lib/multi_xml/parsers/000077500000000000000000000000001500446605700200005ustar00rootroot00000000000000multi_xml-0.7.2/lib/multi_xml/parsers/libxml.rb000066400000000000000000000010711500446605700216130ustar00rootroot00000000000000require "libxml" unless defined?(LibXML) require "multi_xml/parsers/libxml2_parser" module MultiXml module Parsers module Libxml # :nodoc: include Libxml2Parser extend self def parse_error ::LibXML::XML::Error end def parse(xml) node_to_hash(LibXML::XML::Parser.io(xml).parse.root) end private def each_child(node, &) node.each_child(&) end def each_attr(node, &) node.each_attr(&) end def node_name(node) node.name end end end end multi_xml-0.7.2/lib/multi_xml/parsers/libxml2_parser.rb000066400000000000000000000035631500446605700232610ustar00rootroot00000000000000module MultiXml module Parsers module Libxml2Parser # :nodoc: # Convert XML document to hash # # node:: # The XML node object to convert to a hash. # # hash:: # Hash to merge the converted element into. def node_to_hash(node, hash = {}) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength node_hash = {MultiXml::CONTENT_ROOT => ""} name = node_name(node) # Insert node hash into parent hash correctly. case hash[name] when Array hash[name] << node_hash when Hash hash[name] = [hash[name], node_hash] when NilClass hash[name] = node_hash end # Handle child elements each_child(node) do |c| if c.element? node_to_hash(c, node_hash) elsif c.text? || c.cdata? node_hash[MultiXml::CONTENT_ROOT] += c.content end end # Remove content node if it is empty node_hash.delete(MultiXml::CONTENT_ROOT) if node_hash[MultiXml::CONTENT_ROOT].strip.empty? # Handle attributes each_attr(node) do |a| key = node_name(a) v = node_hash[key] node_hash[key] = ((v) ? [a.value, v] : a.value) end hash end # Parse an XML Document IO into a simple hash. # xml:: # XML Document IO to parse def parse(_) raise(NotImplementedError, "inheritor should define #{__method__}") end private def each_child(*) raise(NotImplementedError, "inheritor should define #{__method__}") end def each_attr(*) raise(NotImplementedError, "inheritor should define #{__method__}") end def node_name(*) raise(NotImplementedError, "inheritor should define #{__method__}") end end end end multi_xml-0.7.2/lib/multi_xml/parsers/nokogiri.rb000066400000000000000000000012311500446605700221430ustar00rootroot00000000000000require "nokogiri" unless defined?(Nokogiri) require "multi_xml/parsers/libxml2_parser" module MultiXml module Parsers module Nokogiri # :nodoc: include Libxml2Parser extend self def parse_error ::Nokogiri::XML::SyntaxError end def parse(xml) doc = ::Nokogiri::XML(xml) raise(doc.errors.first) unless doc.errors.empty? node_to_hash(doc.root) end private def each_child(node, &) node.children.each(&) end def each_attr(node, &) node.attribute_nodes.each(&) end def node_name(node) node.node_name end end end end multi_xml-0.7.2/lib/multi_xml/parsers/oga.rb000066400000000000000000000032641500446605700211000ustar00rootroot00000000000000require "oga" unless defined?(Oga) require "multi_xml/parsers/libxml2_parser" module MultiXml module Parsers module Oga # :nodoc: include Libxml2Parser extend self def parse_error LL::ParserError end def parse(io) document = ::Oga.parse_xml(io) node_to_hash(document.children[0]) end def node_to_hash(node, hash = {}) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength node_hash = {MultiXml::CONTENT_ROOT => ""} name = node_name(node) # Insert node hash into parent hash correctly. case hash[name] when Array hash[name] << node_hash when Hash hash[name] = [hash[name], node_hash] when NilClass hash[name] = node_hash end # Handle child elements each_child(node) do |c| if c.is_a?(::Oga::XML::Element) node_to_hash(c, node_hash) elsif c.is_a?(::Oga::XML::Text) || c.is_a?(::Oga::XML::Cdata) node_hash[MultiXml::CONTENT_ROOT] += c.text end end # Remove content node if it is empty node_hash.delete(MultiXml::CONTENT_ROOT) if node_hash[MultiXml::CONTENT_ROOT].strip.empty? # Handle attributes each_attr(node) do |a| key = node_name(a) v = node_hash[key] node_hash[key] = ((v) ? [a.value, v] : a.value) end hash end private def each_child(node, &) node.children.each(&) end def each_attr(node, &) node.attributes.each(&) end def node_name(node) node.name end end end end multi_xml-0.7.2/lib/multi_xml/parsers/ox.rb000066400000000000000000000041441500446605700207560ustar00rootroot00000000000000require "ox" unless defined?(Ox) # Each MultiXml parser is expected to parse an XML document into a Hash. The # conversion rules are: # # - Each document starts out as an empty Hash. # # - Reading an element created an entry in the parent Hash that has a key of # the element name and a value of a Hash with attributes as key value # pairs. Children are added as described by this rule. # # - Text and CDATE is stored in the parent element Hash with a key of # MultiXml::CONTENT_ROOT and a value of the text itself. # # - If a key already exists in the Hash then the value associated with the key # is converted to an Array with the old and new value in it. # # - Other elements such as the xml prolog, doctype, and comments are ignored. # module MultiXml module Parsers module Ox # :nodoc: module_function def parse_error Exception end def parse(io) handler = Handler.new ::Ox.sax_parse(handler, io, convert_special: true, skip: :skip_return) handler.doc end class Handler attr_accessor :stack def initialize @stack = [] end def doc @stack[0] end def attr(name, value) append(name, value) unless @stack.empty? end def text(value) append(MultiXml::CONTENT_ROOT, value) end def cdata(value) append(MultiXml::CONTENT_ROOT, value) end def start_element(name) @stack.push({}) if @stack.empty? h = {} append(name, h) @stack.push(h) end def end_element(_) @stack.pop end def error(message, line, column) raise(StandardError, "#{message} at #{line}:#{column}") end def append(key, value) key = key.to_s h = @stack.last if h.key?(key) v = h[key] if v.is_a?(Array) v << value else h[key] = [v, value] end else h[key] = value end end end end end end multi_xml-0.7.2/lib/multi_xml/parsers/rexml.rb000066400000000000000000000061401500446605700214550ustar00rootroot00000000000000require "rexml/document" unless defined?(REXML::Document) module MultiXml module Parsers module Rexml # :nodoc: extend self def parse_error ::REXML::ParseException end # Parse an XML Document IO into a simple hash using REXML # # xml:: # XML Document IO to parse def parse(xml) doc = REXML::Document.new(xml) raise(REXML::ParseException, "The document #{doc.to_s.inspect} does not have a valid root") unless doc.root merge_element!({}, doc.root) end private # Convert an XML element and merge into the hash # # hash:: # Hash to merge the converted element into. # element:: # XML element to merge into hash def merge_element!(hash, element) merge!(hash, element.name, collapse(element)) end # Actually converts an XML document element into a data structure. # # element:: # The document element to be collapsed. def collapse(element) hash = get_attributes(element) if element.has_elements? element.each_element { |child| merge_element!(hash, child) } merge_texts!(hash, element) unless empty_content?(element) hash else merge_texts!(hash, element) end end # Merge all the texts of an element into the hash # # hash:: # Hash to add the converted element to. # element:: # XML element whose texts are to me merged into the hash def merge_texts!(hash, element) if element.has_text? # must use value to prevent double-escaping texts = element.texts.map(&:value).join merge!(hash, MultiXml::CONTENT_ROOT, texts) else hash end end # Adds a new key/value pair to an existing Hash. If the key to be added # already exists and the existing value associated with key is not # an Array, it will be wrapped in an Array. Then the new value is # appended to that Array. # # hash:: # Hash to add key/value pair to. # key:: # Key to be added. # value:: # Value to be associated with key. def merge!(hash, key, value) if hash.key?(key) if hash[key].instance_of?(Array) hash[key] << value else hash[key] = [hash[key], value] end elsif value.instance_of?(Array) hash[key] = [value] else hash[key] = value end hash end # Converts the attributes array of an XML element into a hash. # Returns an empty Hash if node has no attributes. # # element:: # XML element to extract attributes from. def get_attributes(element) attributes = {} element.attributes.each { |n, v| attributes[n] = v } attributes end # Determines if a document element has text content # # element:: # XML element to be checked. def empty_content?(element) element.texts.join.strip.empty? end end end end multi_xml-0.7.2/lib/multi_xml/version.rb000066400000000000000000000000751500446605700203350ustar00rootroot00000000000000module MultiXml VERSION = Gem::Version.create("0.7.2") end multi_xml-0.7.2/multi_xml.gemspec000066400000000000000000000032661500446605700171270ustar00rootroot00000000000000require_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" 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"] # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html spec.metadata["rubygems_mfa_required"] = "true" spec.add_dependency("bigdecimal", "~> 3.1") end multi_xml-0.7.2/spec/000077500000000000000000000000001500446605700144735ustar00rootroot00000000000000multi_xml-0.7.2/spec/helper.rb000066400000000000000000000004621500446605700163010ustar00rootroot00000000000000def jruby? RUBY_PLATFORM == "java" end require "simplecov" SimpleCov.start do add_filter "/spec" percent = (jruby?) ? 91.38 : 93.16 minimum_coverage(percent) end require "multi_xml" require "rspec" RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = :expect end end multi_xml-0.7.2/spec/multi_xml_spec.rb000066400000000000000000000026051500446605700200470ustar00rootroot00000000000000require "helper" require "parser_shared_example" class MockDecoder def self.parse; end end describe "MultiXml" do context "Parsers" do it "picks a default parser" do expect(MultiXml.parser).to be_a(Module) expect(MultiXml.parser).to respond_to(:parse) end it "defaults to the best available gem" do # Clear cache variable possibly set by previous tests MultiXml.send(:remove_instance_variable, :@parser) if MultiXml.instance_variable_defined?(:@parser) if jruby? # Ox and Libxml are not not currently available on JRuby, so Nokogiri is the best available gem expect(MultiXml.parser.name).to eq("MultiXml::Parsers::Nokogiri") else expect(MultiXml.parser.name).to eq("MultiXml::Parsers::Ox") end end it "is settable via a symbol" do MultiXml.parser = :rexml expect(MultiXml.parser.name).to eq("MultiXml::Parsers::Rexml") end it "is settable via a class" do MultiXml.parser = MockDecoder expect(MultiXml.parser.name).to eq("MockDecoder") end end [%w[LibXML libxml], %w[REXML rexml/document], %w[Nokogiri nokogiri], %w[Ox ox], %w[Oga oga]].each do |parser| require parser.last context "#{parser.first} parser" do it_behaves_like "a parser", parser.first end rescue LoadError puts "Tests not run for #{parser.first} due to a LoadError" end end multi_xml-0.7.2/spec/parser_shared_example.rb000066400000000000000000000526401500446605700213640ustar00rootroot00000000000000shared_examples_for "a parser" do |parser| before do MultiXml.parser = parser LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER) if parser == "LibXML" rescue LoadError pending "Parser #{parser} couldn't be loaded" end describe ".parse" do context "a blank string" do before do @xml = "" end it "returns an empty Hash" do expect(MultiXml.parse(@xml)).to eq({}) end end context "a whitespace string" do before do @xml = " " end it "returns an empty Hash" do expect(MultiXml.parse(@xml)).to eq({}) end end context "a frozen string" do before do @xml = " " end it "returns an empty Hash" do expect(MultiXml.parse(@xml)).to eq({}) end end unless parser == "Oga" context "an invalid XML document" do before do @xml = "" end it "raises MultiXml::ParseError" do expect { MultiXml.parse(@xml) }.to raise_error(MultiXml::ParseError) end end end context "a valid XML document" do before do @xml = "" end it "parses correctly" do expect(MultiXml.parse(@xml)).to eq("user" => nil) end context "with CDATA" do before do @xml = "" end it "returns the correct CDATA" do expect(MultiXml.parse(@xml)["user"]).to eq("Erik Berlin") end end context "element with the same inner element and attribute name" do before do @xml = "Smith" end it "returns names as Array" do expect(MultiXml.parse(@xml)["user"]["name"]).to eq %w[John Smith] end end context "with content" do before do @xml = "Erik Berlin" end it "returns the correct content" do expect(MultiXml.parse(@xml)["user"]).to eq("Erik Berlin") end end context "with an attribute" do before do @xml = '' end it "returns the correct attribute" do expect(MultiXml.parse(@xml)["user"]["name"]).to eq("Erik Berlin") end end context "with multiple attributes" do before do @xml = '' end it "returns the correct attributes" do expect(MultiXml.parse(@xml)["user"]["name"]).to eq("Erik Berlin") expect(MultiXml.parse(@xml)["user"]["screen_name"]).to eq("sferik") end end context "typecast management" do before do @xml = %( Settings Test ) end context "with :typecast_xml_value => true" do before do @setting = MultiXml.parse(@xml)["global_settings"]["group"]["setting"] end it { expect(@setting).to eq "" } end context "with :typecast_xml_value => false" do before do @setting = MultiXml.parse(@xml, typecast_xml_value: false)["global_settings"]["group"]["setting"] end it { expect(@setting).to eq("type" => "string", "description" => {"__content__" => "Test"}) } end end context "with :symbolize_keys => true" do before do @xml = 'Wynn Netherland' end it "symbolizes keys" do expect(MultiXml.parse(@xml, symbolize_keys: true)).to eq(users: {user: [{name: "Erik Berlin"}, {name: "Wynn Netherland"}]}) end end context 'with an attribute type="boolean"' do %w[true false].each do |boolean| context "when #{boolean}" do it "returns #{boolean}" do xml = "#{boolean}" expect(MultiXml.parse(xml)["tag"]).to be instance_eval(boolean) end end end context "when 1" do before do @xml = '1' end it "returns true" do expect(MultiXml.parse(@xml)["tag"]).to be true end end context "when 0" do before do @xml = '0' end it "returns false" do expect(MultiXml.parse(@xml)["tag"]).to be false end end end context 'with an attribute type="integer"' do context "with a positive integer" do before do @xml = '1' end it "returns a Integer" do expect(MultiXml.parse(@xml)["tag"]).to be_a(Integer) end it "returns a positive number" do expect(MultiXml.parse(@xml)["tag"]).to be > 0 end it "returns the correct number" do expect(MultiXml.parse(@xml)["tag"]).to eq(1) end end context "with a negative integer" do before do @xml = '-1' end it "returns a Integer" do expect(MultiXml.parse(@xml)["tag"]).to be_a(Integer) end it "returns a negative number" do expect(MultiXml.parse(@xml)["tag"]).to be < 0 end it "returns the correct number" do expect(MultiXml.parse(@xml)["tag"]).to eq(-1) end end end context 'with an attribute type="string"' do before do @xml = '' end it "returns a String" do expect(MultiXml.parse(@xml)["tag"]).to be_a(String) end it "returns the correct string" do expect(MultiXml.parse(@xml)["tag"]).to eq("") end end context 'with an attribute type="date"' do before do @xml = '1970-01-01' end it "returns a Date" do expect(MultiXml.parse(@xml)["tag"]).to be_a(Date) end it "returns the correct date" do expect(MultiXml.parse(@xml)["tag"]).to eq(Date.parse("1970-01-01")) end end context 'with an attribute type="datetime"' do before do @xml = '1970-01-01 00:00' end it "returns a Time" do expect(MultiXml.parse(@xml)["tag"]).to be_a(Time) end it "returns the correct time" do expect(MultiXml.parse(@xml)["tag"]).to eq(Time.parse("1970-01-01 00:00")) end end context 'with an attribute type="dateTime"' do before do @xml = '1970-01-01 00:00' end it "returns a Time" do expect(MultiXml.parse(@xml)["tag"]).to be_a(Time) end it "returns the correct time" do expect(MultiXml.parse(@xml)["tag"]).to eq(Time.parse("1970-01-01 00:00")) end end context 'with an attribute type="double"' do before do @xml = '3.14159265358979' end it "returns a Float" do expect(MultiXml.parse(@xml)["tag"]).to be_a(Float) end it "returns the correct number" do expect(MultiXml.parse(@xml)["tag"]).to eq(3.14159265358979) end end context 'with an attribute type="decimal"' do before do @xml = '3.14159265358979' end it "returns a BigDecimal" do expect(MultiXml.parse(@xml)["tag"]).to be_a(BigDecimal) end it "returns the correct number" do expect(MultiXml.parse(@xml)["tag"]).to eq(3.14159265358979) end end context 'with an attribute type="base64Binary"' do before do @xml = 'aW1hZ2UucG5n' end it "returns a String" do expect(MultiXml.parse(@xml)["tag"]).to be_a(String) end it "returns the correct string" do expect(MultiXml.parse(@xml)["tag"]).to eq("image.png") end end context 'with an attribute type="yaml"' do before do @xml = "--- \n1: returns an integer\n:message: Have a nice day\narray: \n- has-dashes: true\n has_underscores: true\n" end it "raises MultiXML::DisallowedTypeError by default" do expect { MultiXml.parse(@xml)["tag"] }.to raise_error(MultiXml::DisallowedTypeError) end it "returns the correctly parsed YAML when the type is allowed" do expect(MultiXml.parse(@xml, disallowed_types: [])["tag"]).to eq(:message => "Have a nice day", 1 => "returns an integer", "array" => [{"has-dashes" => true, "has_underscores" => true}]) end end context 'with an attribute type="symbol"' do before do @xml = 'my_symbol' end it "raises MultiXML::DisallowedTypeError" do expect { MultiXml.parse(@xml)["tag"] }.to raise_error(MultiXml::DisallowedTypeError) end it "returns the correctly parsed Symbol when the type is allowed" do expect(MultiXml.parse(@xml, disallowed_types: [])["tag"]).to eq(:my_symbol) end end context 'with an attribute type="file"' do before do @xml = 'ZGF0YQ==' end it "returns a StringIO" do expect(MultiXml.parse(@xml)["tag"]).to be_a(StringIO) end it "is decoded correctly" do expect(MultiXml.parse(@xml)["tag"].string).to eq("data") end it "has the correct file name" do expect(MultiXml.parse(@xml)["tag"].original_filename).to eq("data.txt") end it "has the correct content type" do expect(MultiXml.parse(@xml)["tag"].content_type).to eq("text/plain") end context "with missing name and content type" do before do @xml = 'ZGF0YQ==' end it "returns a StringIO" do expect(MultiXml.parse(@xml)["tag"]).to be_a(StringIO) end it "is decoded correctly" do expect(MultiXml.parse(@xml)["tag"].string).to eq("data") end it "has the default file name" do expect(MultiXml.parse(@xml)["tag"].original_filename).to eq("untitled") end it "has the default content type" do expect(MultiXml.parse(@xml)["tag"].content_type).to eq("application/octet-stream") end end end context 'with an attribute type="array"' do before do @xml = 'Erik BerlinWynn Netherland' end it "returns an Array" do expect(MultiXml.parse(@xml)["users"]).to be_a(Array) end it "returns the correct array" do expect(MultiXml.parse(@xml)["users"]).to eq(["Erik Berlin", "Wynn Netherland"]) end end context 'with an attribute type="array" in addition to other attributes' do before do @xml = 'Erik BerlinWynn Netherland' end it "returns an Array" do expect(MultiXml.parse(@xml)["users"]).to be_a(Array) end it "returns the correct array" do expect(MultiXml.parse(@xml)["users"]).to eq(["Erik Berlin", "Wynn Netherland"]) end end context 'with an attribute type="array" containing only one item' do before do @xml = 'Erik Berlin' end it "returns an Array" do expect(MultiXml.parse(@xml)["users"]).to be_a(Array) end it "returns the correct array" do expect(MultiXml.parse(@xml)["users"]).to eq(["Erik Berlin"]) end end %w[integer boolean date datetime file].each do |type| context "with an empty attribute type=\"#{type}\"" do before do @xml = "" end it "returns nil" do expect(MultiXml.parse(@xml)["tag"]).to be_nil end end end %w[yaml symbol].each do |type| context "with an empty attribute type=\"#{type}\"" do before do @xml = "" end it "raises MultiXml::DisallowedTypeError by default" do expect { MultiXml.parse(@xml)["tag"] }.to raise_error(MultiXml::DisallowedTypeError) end it "returns nil when the type is allowed" do expect(MultiXml.parse(@xml, disallowed_types: [])["tag"]).to be_nil end end end context 'with an empty attribute type="array"' do before do @xml = '' end it "returns an empty Array" do expect(MultiXml.parse(@xml)["tag"]).to eq([]) end context "with whitespace" do before do @xml = ' ' end it "returns an empty Array" do expect(MultiXml.parse(@xml)["tag"]).to eq([]) end end end context "with XML entities" do before do @xml_entities = { "<" => "<", ">" => ">", '"' => """, "'" => "'", "&" => "&" } end context "in content" do it "returns unescaped XML entities" do @xml_entities.each do |key, value| xml = "#{value}" expect(MultiXml.parse(xml)["tag"]).to eq(key) end end end context "in attribute" do it "returns unescaped XML entities" do @xml_entities.each do |key, value| xml = "" expect(MultiXml.parse(xml)["tag"]["attribute"]).to eq(key) end end end end context "with dasherized tag" do before do @xml = "" end it "returns undasherize tag" do expect(MultiXml.parse(@xml).keys).to include("tag_1") end end context "with dasherized attribute" do before do @xml = '' end it "returns undasherize attribute" do expect(MultiXml.parse(@xml)["tag"].keys).to include("attribute_1") end end context "with children" do context "with attributes" do before do @xml = '' end it "returns the correct attributes" do expect(MultiXml.parse(@xml)["users"]["user"]["name"]).to eq("Erik Berlin") end end context "with text" do before do @xml = "Erik Berlin" end it "returns the correct text" do expect(MultiXml.parse(@xml)["user"]["name"]).to eq("Erik Berlin") end end context "with an unrecognized attribute type" do before do @xml = 'Erik Berlin' end it "passes through the type" do expect(MultiXml.parse(@xml)["user"]["type"]).to eq("admin") end end context "with attribute tags on content nodes" do context "non 'type' attributes" do before do @xml = <<-XML 123 0.123 XML @parsed_xml = MultiXml.parse(@xml) end it "adds the attributes to the value hash" do expect(@parsed_xml["options"]["value"][0]["__content__"]).to eq("123") expect(@parsed_xml["options"]["value"][0]["currency"]).to eq("USD") expect(@parsed_xml["options"]["value"][1]["__content__"]).to eq("0.123") expect(@parsed_xml["options"]["value"][1]["number"]).to eq("percent") end end context "unrecognized type attributes" do before do @xml = <<-XML 123 0.123 123 XML @parsed_xml = MultiXml.parse(@xml) end it "adds the attributes to the value hash passing through the type" do expect(@parsed_xml["options"]["value"][0]["__content__"]).to eq("123") expect(@parsed_xml["options"]["value"][0]["type"]).to eq("USD") expect(@parsed_xml["options"]["value"][1]["__content__"]).to eq("0.123") expect(@parsed_xml["options"]["value"][1]["type"]).to eq("percent") expect(@parsed_xml["options"]["value"][2]["__content__"]).to eq("123") expect(@parsed_xml["options"]["value"][2]["currency"]).to eq("USD") end end context "mixing attributes and non-attributes content nodes type attributes" do before do @xml = <<-XML 123 0.123 123 XML @parsed_xml = MultiXml.parse(@xml) end it "adds the attributes to the value hash passing through the type" do expect(@parsed_xml["options"]["value"][0]["__content__"]).to eq("123") expect(@parsed_xml["options"]["value"][0]["type"]).to eq("USD") expect(@parsed_xml["options"]["value"][1]["__content__"]).to eq("0.123") expect(@parsed_xml["options"]["value"][1]["type"]).to eq("percent") expect(@parsed_xml["options"]["value"][2]).to eq("123") end end context "mixing recognized type attribute and non-type attributes on content nodes" do before do @xml = <<-XML 123 XML @parsed_xml = MultiXml.parse(@xml) end it "adds the the non-type attribute and remove the recognized type attribute and do the typecast" do expect(@parsed_xml["options"]["value"]["__content__"]).to eq(123) expect(@parsed_xml["options"]["value"]["number"]).to eq("USD") end end context "mixing unrecognized type attribute and non-type attributes on content nodes" do before do @xml = <<-XML 123 XML @parsed_xml = MultiXml.parse(@xml) end it "adds the the non-type attributes and type attribute to the value hash" do expect(@parsed_xml["options"]["value"]["__content__"]).to eq("123") expect(@parsed_xml["options"]["value"]["number"]).to eq("USD") expect(@parsed_xml["options"]["value"]["type"]).to eq("currency") end end end context "with newlines and whitespace" do before do @xml = <<-XML Erik Berlin XML end it "parses correctly" do expect(MultiXml.parse(@xml)).to eq("user" => {"name" => "Erik Berlin"}) end end # Babies having babies context "with children" do before do @xml = '' end it "parses correctly" do expect(MultiXml.parse(@xml)).to eq("users" => {"user" => {"name" => "Erik Berlin", "status" => {"text" => "Hello"}}}) end end end context "with sibling children" do before do @xml = "Erik BerlinWynn Netherland" end it "returns an Array" do expect(MultiXml.parse(@xml)["users"]["user"]).to be_a(Array) end it "parses correctly" do expect(MultiXml.parse(@xml)).to eq("users" => {"user" => ["Erik Berlin", "Wynn Netherland"]}) end end end context "a duplexed stream" do before do @xml, wr = IO.pipe Thread.new do "".each_char do |chunk| wr << chunk end wr.close end end it "parses correctly" do expect(MultiXml.parse(@xml)).to eq("user" => nil) end end end end multi_xml-0.7.2/spec/speed.rb000077500000000000000000000024301500446605700161220ustar00rootroot00000000000000#!/usr/bin/env ruby -wW1 $LOAD_PATH << "." $LOAD_PATH << "../lib" if __FILE__ == $PROGRAM_NAME while (i = ARGV.index("-I")) _, path = ARGV.slice!(i, 2) $LOAD_PATH << path end end require "optparse" require "stringio" require "multi_xml" %w[libxml nokogiri ox].each do |library| require library rescue LoadError next end $verbose = 0 $parsers = [] $iterations = 10 opts = OptionParser.new opts.on("-v", "increase verbosity") { $verbose += 1 } opts.on("-p", "--parser [String]", String, "parser to test") { |parsers| $parsers = [parsers] } opts.on("-i", "--iterations [Int]", Integer, "iterations") { |iterations| $iterations = iterations } opts.on("-h", "--help", "Show this display") do puts opts Process.exit!(0) end files = opts.parse(ARGV) if $parsers.empty? $parsers << "libxml" if defined?(LibXML) $parsers << "nokogiri" if defined?(Nokogiri) $parsers << "ox" if defined?(Ox) end files.each do |filename| times = {} xml = File.read(filename) $parsers.each do |p| MultiXml.parser = p start = Time.now $iterations.times do io = StringIO.new(xml) MultiXml.parse(io) end times[p] = Time.now - start end times.each do |p, t| puts format("%8s took %0.3f seconds to parse %s %d times.", p, t, filename, $iterations) end end