multi_xml 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.mutant.yml +16 -0
- data/.rspec +2 -0
- data/.rubocop.yml +65 -0
- data/CHANGELOG.md +28 -0
- data/Gemfile +24 -0
- data/LICENSE.md +1 -1
- data/README.md +9 -29
- data/Rakefile +61 -0
- data/Steepfile +22 -0
- data/lib/multi_xml/constants.rb +134 -0
- data/lib/multi_xml/errors.rb +93 -0
- data/lib/multi_xml/file_like.rb +62 -0
- data/lib/multi_xml/helpers.rb +228 -0
- data/lib/multi_xml/parsers/dom_parser.rb +97 -0
- data/lib/multi_xml/parsers/libxml.rb +36 -19
- data/lib/multi_xml/parsers/libxml_sax.rb +103 -0
- data/lib/multi_xml/parsers/nokogiri.rb +38 -20
- data/lib/multi_xml/parsers/nokogiri_sax.rb +102 -0
- data/lib/multi_xml/parsers/oga.rb +52 -57
- data/lib/multi_xml/parsers/ox.rb +105 -63
- data/lib/multi_xml/parsers/rexml.rb +85 -79
- data/lib/multi_xml/parsers/sax_handler.rb +117 -0
- data/lib/multi_xml/version.rb +5 -43
- data/lib/multi_xml.rb +179 -269
- data/sig/multi_xml.rbs +227 -0
- metadata +46 -23
- data/lib/multi_xml/parsers/libxml2_parser.rb +0 -72
- data/multi_xml.gemspec +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a6a703a4614209e07bba561caf80be62e407c2168d7234ce8323b1cf10314a1a
|
|
4
|
+
data.tar.gz: f93e6d26aa75c0d357968bca0577b4584285941569a1f418eb843c70ede3aee2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7ae3d51af07c83c76b976caaff6db7a0a2a9e305e82a4f66234aeb9f4566d2654c167d5578bebe8ba44bdeaccf343c60b49c52407728896c28a1ebd33c5cf06
|
|
7
|
+
data.tar.gz: ef1fee85543fa75f734322cf3d66483d3029d2811fdd42db83537e2e9cca331ed69771caa4b164665e37f42b97ac70918a238ddc0ed0c119d4680ab2fee9c41c
|
data/.mutant.yml
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require:
|
|
2
|
+
- standard
|
|
3
|
+
|
|
4
|
+
plugins:
|
|
5
|
+
- rubocop-performance
|
|
6
|
+
- rubocop-rake
|
|
7
|
+
- rubocop-minitest
|
|
8
|
+
- standard-performance
|
|
9
|
+
|
|
10
|
+
AllCops:
|
|
11
|
+
NewCops: enable
|
|
12
|
+
TargetRubyVersion: 3.2
|
|
13
|
+
|
|
14
|
+
Layout/ArgumentAlignment:
|
|
15
|
+
EnforcedStyle: with_fixed_indentation
|
|
16
|
+
IndentationWidth: 2
|
|
17
|
+
|
|
18
|
+
Layout/ArrayAlignment:
|
|
19
|
+
EnforcedStyle: with_fixed_indentation
|
|
20
|
+
|
|
21
|
+
Layout/CaseIndentation:
|
|
22
|
+
EnforcedStyle: end
|
|
23
|
+
|
|
24
|
+
Layout/EndAlignment:
|
|
25
|
+
EnforcedStyleAlignWith: start_of_line
|
|
26
|
+
|
|
27
|
+
Layout/LineLength:
|
|
28
|
+
Max: 140
|
|
29
|
+
|
|
30
|
+
Layout/ParameterAlignment:
|
|
31
|
+
EnforcedStyle: with_fixed_indentation
|
|
32
|
+
IndentationWidth: 2
|
|
33
|
+
|
|
34
|
+
Layout/SpaceInsideHashLiteralBraces:
|
|
35
|
+
EnforcedStyle: no_space
|
|
36
|
+
|
|
37
|
+
Metrics/ParameterLists:
|
|
38
|
+
CountKeywordArgs: false
|
|
39
|
+
|
|
40
|
+
Style/Alias:
|
|
41
|
+
EnforcedStyle: prefer_alias_method
|
|
42
|
+
|
|
43
|
+
Style/EmptyMethod:
|
|
44
|
+
EnforcedStyle: expanded
|
|
45
|
+
|
|
46
|
+
Style/FrozenStringLiteralComment:
|
|
47
|
+
EnforcedStyle: never
|
|
48
|
+
|
|
49
|
+
Style/RedundantConstantBase:
|
|
50
|
+
Enabled: false
|
|
51
|
+
|
|
52
|
+
Style/RescueStandardError:
|
|
53
|
+
EnforcedStyle: implicit
|
|
54
|
+
|
|
55
|
+
Style/StringLiterals:
|
|
56
|
+
EnforcedStyle: double_quotes
|
|
57
|
+
|
|
58
|
+
Style/StringLiteralsInInterpolation:
|
|
59
|
+
EnforcedStyle: double_quotes
|
|
60
|
+
|
|
61
|
+
Style/SymbolProc:
|
|
62
|
+
Enabled: false
|
|
63
|
+
|
|
64
|
+
Style/TernaryParentheses:
|
|
65
|
+
EnforcedStyle: require_parentheses_when_complex
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
0.8.0
|
|
2
|
+
-----
|
|
3
|
+
* [Add per-parse :parser option to MultiXml.parse](https://github.com/sferik/multi_xml/commit/eb0c1ccadd9026980ba8b6dd0128d6862dc361c4)
|
|
4
|
+
* [Add SAX parsers for Nokogiri and LibXML](https://github.com/sferik/multi_xml/commit/5d67fe6cae3c1ef2c306f1e83fc91b9accfcb724)
|
|
5
|
+
* [Fix inconsistent whitespace handling across parsers](https://github.com/sferik/multi_xml/commit/55aa23f1c401e66984ad1c7d753c1b4258bf0dfd)
|
|
6
|
+
* [Make parsing errors inspectable with cause and xml accessors](https://github.com/sferik/multi_xml/commit/f676f1b657f3352a80ac171d9b839e41ad52a14d)
|
|
7
|
+
* [Drop support for JRuby](https://github.com/sferik/multi_xml/commit/27895ca3918c681ad7ddaa57c5cae7b8340bd601)
|
|
8
|
+
|
|
9
|
+
0.7.2
|
|
10
|
+
-----
|
|
11
|
+
* [Drop support for Ruby 3.1](https://github.com/sferik/multi_xml/commit/fab6288edd36c58a2b13e0206d8bed305fcb4a4b)
|
|
12
|
+
|
|
13
|
+
0.7.1
|
|
14
|
+
-----
|
|
15
|
+
* [Relax required Ruby version constraint to allow installation on Debian stable](https://github.com/sferik/multi_xml/commit/7d18711466a15e158dc71344ca6f6e18838ecc8d)
|
|
16
|
+
|
|
17
|
+
0.7.0
|
|
18
|
+
-----
|
|
19
|
+
* [Add support for Ruby 3.3](https://github.com/sferik/multi_xml/pull/67)
|
|
20
|
+
* [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)
|
|
21
|
+
* [Don't mutate strings](https://github.com/sferik/multi_xml/commit/71be3fff4afb0277a7e1c47c5f1f4b6106a8eb45)
|
|
22
|
+
|
|
23
|
+
0.6.0
|
|
24
|
+
-----
|
|
25
|
+
* [Duplexed Streams](https://github.com/sferik/multi_xml/pull/45)
|
|
26
|
+
* [Support for Oga](https://github.com/sferik/multi_xml/pull/47)
|
|
27
|
+
* [Integer unification for Ruby 2.4](https://github.com/sferik/multi_xml/pull/54)
|
|
28
|
+
|
|
1
29
|
0.5.5
|
|
2
30
|
-----
|
|
3
31
|
* [Fix symbolize_keys function](https://github.com/sferik/multi_xml/commit/a4cae3aeb690999287cd30206399abaa5ce1ae81)
|
data/Gemfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
source "https://rubygems.org"
|
|
2
|
+
|
|
3
|
+
gem "libxml-ruby", require: nil, platforms: :ruby
|
|
4
|
+
gem "nokogiri", require: nil
|
|
5
|
+
gem "oga", ">= 2.3", require: nil
|
|
6
|
+
gem "ox", require: nil, platforms: :ruby
|
|
7
|
+
gem "rexml", require: nil
|
|
8
|
+
|
|
9
|
+
gem "minitest", ">= 5.27"
|
|
10
|
+
gem "mutant-minitest", ">= 0.13.5"
|
|
11
|
+
gem "rake", ">= 13.3.1"
|
|
12
|
+
gem "rdoc", ">= 7.0.2"
|
|
13
|
+
gem "rubocop", ">= 1.81.7"
|
|
14
|
+
gem "rubocop-minitest", ">= 0.36"
|
|
15
|
+
gem "rubocop-performance", ">= 1.26.1"
|
|
16
|
+
gem "rubocop-rake", ">= 0.7.1"
|
|
17
|
+
gem "simplecov", ">= 0.22"
|
|
18
|
+
gem "standard", ">= 1.52"
|
|
19
|
+
gem "standard-performance", ">= 1.9"
|
|
20
|
+
gem "steep", ">= 1.10", platforms: :ruby
|
|
21
|
+
gem "yard", ">= 0.9.38"
|
|
22
|
+
gem "yardstick", ">= 0.9.9"
|
|
23
|
+
|
|
24
|
+
gemspec
|
data/LICENSE.md
CHANGED
data/README.md
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
# MultiXML
|
|
2
2
|
|
|
3
|
-
[][gem]
|
|
4
|
-
[][travis]
|
|
5
|
-
[][gemnasium]
|
|
6
|
-
[][codeclimate]
|
|
7
|
-
[][coveralls]
|
|
8
|
-
|
|
9
|
-
[gem]: https://rubygems.org/gems/multi_xml
|
|
10
|
-
[travis]: http://travis-ci.org/sferik/multi_xml
|
|
11
|
-
[gemnasium]: https://gemnasium.com/sferik/multi_xml
|
|
12
|
-
[codeclimate]: https://codeclimate.com/github/sferik/multi_xml
|
|
13
|
-
[coveralls]: https://coveralls.io/r/sferik/multi_xml
|
|
14
|
-
|
|
15
3
|
A generic swappable back-end for XML parsing
|
|
16
4
|
|
|
17
5
|
## Installation
|
|
@@ -23,10 +11,6 @@ A generic swappable back-end for XML parsing
|
|
|
23
11
|
[documentation]: http://rdoc.info/gems/multi_xml
|
|
24
12
|
|
|
25
13
|
## Usage Examples
|
|
26
|
-
Lots of Ruby libraries utilize XML parsing in some form, and everyone has their
|
|
27
|
-
favorite XML library. In order to best support multiple XML parsers and
|
|
28
|
-
libraries, `multi_xml` is a general-purpose swappable XML backend library. You
|
|
29
|
-
use it like so:
|
|
30
14
|
```ruby
|
|
31
15
|
require 'multi_xml'
|
|
32
16
|
|
|
@@ -54,24 +38,20 @@ The `parser` setter takes either a symbol or a class (to allow for custom XML
|
|
|
54
38
|
parsers) that responds to `.parse` at the class level.
|
|
55
39
|
|
|
56
40
|
MultiXML tries to have intelligent defaulting. That is, if you have any of the
|
|
57
|
-
supported parsers already loaded, it will
|
|
58
|
-
|
|
41
|
+
supported parsers already loaded, it will use them before attempting to load
|
|
42
|
+
a new one. When loading, libraries are ordered by speed: first Ox, then LibXML,
|
|
59
43
|
then Nokogiri, and finally REXML.
|
|
60
44
|
|
|
61
45
|
## Supported Ruby Versions
|
|
62
|
-
This library aims to support and is
|
|
46
|
+
This library aims to support and is tested against the following Ruby
|
|
63
47
|
implementations:
|
|
64
48
|
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* Ruby 2.3
|
|
70
|
-
* [JRuby 9000][jruby]
|
|
71
|
-
|
|
72
|
-
[jruby]: http://jruby.org/
|
|
49
|
+
* 3.2
|
|
50
|
+
* 3.3
|
|
51
|
+
* 3.4
|
|
52
|
+
* 4.0
|
|
73
53
|
|
|
74
|
-
If something doesn't work on one of these
|
|
54
|
+
If something doesn't work on one of these versions, it's a bug.
|
|
75
55
|
|
|
76
56
|
This library may inadvertently work (or seem to work) on other Ruby
|
|
77
57
|
implementations, however support will only be provided for the versions listed
|
|
@@ -90,6 +70,6 @@ MultiXML was inspired by [MultiJSON][].
|
|
|
90
70
|
[multijson]: https://github.com/intridea/multi_json/
|
|
91
71
|
|
|
92
72
|
## Copyright
|
|
93
|
-
Copyright (c) 2010-
|
|
73
|
+
Copyright (c) 2010-2025 Erik Berlin. See [LICENSE][] for details.
|
|
94
74
|
|
|
95
75
|
[license]: LICENSE.md
|
data/Rakefile
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
|
|
3
|
+
# Override release task to skip gem push (handled by GitHub Actions with attestations)
|
|
4
|
+
Rake::Task["release"].clear
|
|
5
|
+
desc "Build gem and create tag (gem push handled by CI)"
|
|
6
|
+
task release: %w[build release:guard_clean release:source_control_push]
|
|
7
|
+
|
|
8
|
+
require "rake/testtask"
|
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
|
10
|
+
t.libs << "test"
|
|
11
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require "standard/rake"
|
|
15
|
+
require "rubocop/rake_task"
|
|
16
|
+
RuboCop::RakeTask.new
|
|
17
|
+
|
|
18
|
+
require "yard"
|
|
19
|
+
YARD::Rake::YardocTask.new do |task|
|
|
20
|
+
task.files = ["lib/**/*.rb", "-", "LICENSE.md"]
|
|
21
|
+
task.options = [
|
|
22
|
+
"--no-private",
|
|
23
|
+
"--protected",
|
|
24
|
+
"--output-dir", "doc/yard",
|
|
25
|
+
"--markup", "markdown"
|
|
26
|
+
]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
require "yardstick/rake/measurement"
|
|
30
|
+
Yardstick::Rake::Measurement.new do |measurement|
|
|
31
|
+
measurement.output = "measurement/report.txt"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
require "yardstick/rake/verify"
|
|
35
|
+
Yardstick::Rake::Verify.new do |verify|
|
|
36
|
+
verify.threshold = 100
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Steep requires native extensions not available on JRuby or Windows
|
|
40
|
+
unless RUBY_PLATFORM == "java" || Gem.win_platform?
|
|
41
|
+
require "steep/rake_task"
|
|
42
|
+
Steep::RakeTask.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "Run linters"
|
|
46
|
+
task lint: %i[rubocop standard]
|
|
47
|
+
|
|
48
|
+
# Mutant uses fork() which is not available on Windows or JRuby
|
|
49
|
+
desc "Run mutation testing"
|
|
50
|
+
task :mutant do
|
|
51
|
+
if Gem.win_platform? || RUBY_PLATFORM == "java"
|
|
52
|
+
puts "Skipping mutant on Windows/JRuby (fork not supported)"
|
|
53
|
+
else
|
|
54
|
+
system("bundle", "exec", "mutant", "run") || exit(1)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
default_tasks = %i[test lint verify_measurements mutant]
|
|
59
|
+
default_tasks << :steep unless RUBY_PLATFORM == "java" || Gem.win_platform?
|
|
60
|
+
|
|
61
|
+
task default: default_tasks
|
data/Steepfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
D = Steep::Diagnostic
|
|
2
|
+
|
|
3
|
+
target :lib do
|
|
4
|
+
signature "sig"
|
|
5
|
+
|
|
6
|
+
# Check core library files (excluding parser implementations that depend on optional gems)
|
|
7
|
+
check "lib/multi_xml.rb"
|
|
8
|
+
check "lib/multi_xml/constants.rb"
|
|
9
|
+
check "lib/multi_xml/errors.rb"
|
|
10
|
+
check "lib/multi_xml/file_like.rb"
|
|
11
|
+
check "lib/multi_xml/helpers.rb"
|
|
12
|
+
check "lib/multi_xml/version.rb"
|
|
13
|
+
|
|
14
|
+
# Use stdlib types
|
|
15
|
+
library "date"
|
|
16
|
+
library "time"
|
|
17
|
+
library "yaml"
|
|
18
|
+
library "bigdecimal"
|
|
19
|
+
library "stringio"
|
|
20
|
+
|
|
21
|
+
configure_code_diagnostics(D::Ruby.strict)
|
|
22
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module MultiXml
|
|
2
|
+
# Hash key for storing text content within element hashes
|
|
3
|
+
#
|
|
4
|
+
# @api public
|
|
5
|
+
# @return [String] the key "__content__" used for text content
|
|
6
|
+
# @example Accessing text content
|
|
7
|
+
# result = MultiXml.parse('<name>John</name>')
|
|
8
|
+
# result["name"] #=> "John" (simplified, but internally uses __content__)
|
|
9
|
+
TEXT_CONTENT_KEY = "__content__".freeze
|
|
10
|
+
|
|
11
|
+
# Maps Ruby class names to XML type attribute values
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
# @return [Hash{String => String}] mapping of Ruby class names to XML types
|
|
15
|
+
# @example Check XML type for a Ruby class
|
|
16
|
+
# RUBY_TYPE_TO_XML["Integer"] #=> "integer"
|
|
17
|
+
RUBY_TYPE_TO_XML = {
|
|
18
|
+
"Symbol" => "symbol",
|
|
19
|
+
"Integer" => "integer",
|
|
20
|
+
"BigDecimal" => "decimal",
|
|
21
|
+
"Float" => "float",
|
|
22
|
+
"TrueClass" => "boolean",
|
|
23
|
+
"FalseClass" => "boolean",
|
|
24
|
+
"Date" => "date",
|
|
25
|
+
"DateTime" => "datetime",
|
|
26
|
+
"Time" => "datetime",
|
|
27
|
+
"Array" => "array",
|
|
28
|
+
"Hash" => "hash"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# XML type attributes disallowed by default for security
|
|
32
|
+
#
|
|
33
|
+
# These types are blocked to prevent code execution vulnerabilities.
|
|
34
|
+
#
|
|
35
|
+
# @api public
|
|
36
|
+
# @return [Array<String>] list of disallowed type names
|
|
37
|
+
# @example Check default disallowed types
|
|
38
|
+
# DISALLOWED_TYPES #=> ["symbol", "yaml"]
|
|
39
|
+
DISALLOWED_TYPES = %w[symbol yaml].freeze
|
|
40
|
+
|
|
41
|
+
# Values that represent false in XML boolean attributes
|
|
42
|
+
#
|
|
43
|
+
# @api public
|
|
44
|
+
# @return [Set<String>] values considered false
|
|
45
|
+
# @example Check false values
|
|
46
|
+
# FALSE_BOOLEAN_VALUES.include?("0") #=> true
|
|
47
|
+
FALSE_BOOLEAN_VALUES = Set.new(%w[0 false]).freeze
|
|
48
|
+
|
|
49
|
+
# Default parsing options
|
|
50
|
+
#
|
|
51
|
+
# @api public
|
|
52
|
+
# @return [Hash] default options for parse method
|
|
53
|
+
# @example View defaults
|
|
54
|
+
# DEFAULT_OPTIONS[:symbolize_keys] #=> false
|
|
55
|
+
DEFAULT_OPTIONS = {
|
|
56
|
+
typecast_xml_value: true,
|
|
57
|
+
disallowed_types: DISALLOWED_TYPES,
|
|
58
|
+
symbolize_keys: false
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# Parser libraries in preference order (fastest first)
|
|
62
|
+
#
|
|
63
|
+
# @api public
|
|
64
|
+
# @return [Array<Array>] pairs of [require_path, parser_symbol]
|
|
65
|
+
# @example View parser order
|
|
66
|
+
# PARSER_PREFERENCE.first #=> ["ox", :ox]
|
|
67
|
+
PARSER_PREFERENCE = [
|
|
68
|
+
["ox", :ox],
|
|
69
|
+
["libxml", :libxml],
|
|
70
|
+
["nokogiri", :nokogiri],
|
|
71
|
+
["rexml/document", :rexml],
|
|
72
|
+
["oga", :oga]
|
|
73
|
+
].freeze
|
|
74
|
+
|
|
75
|
+
# Parses datetime strings, trying Time first then DateTime
|
|
76
|
+
#
|
|
77
|
+
# @api private
|
|
78
|
+
# @return [Proc] lambda that parses datetime strings
|
|
79
|
+
PARSE_DATETIME = lambda do |string|
|
|
80
|
+
Time.parse(string).utc
|
|
81
|
+
rescue ArgumentError
|
|
82
|
+
DateTime.parse(string).to_time.utc
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Creates a file-like StringIO from base64-encoded content
|
|
86
|
+
#
|
|
87
|
+
# @api private
|
|
88
|
+
# @return [Proc] lambda that creates file objects
|
|
89
|
+
FILE_CONVERTER = lambda do |content, entity|
|
|
90
|
+
StringIO.new(content.unpack1("m")).tap do |io|
|
|
91
|
+
io.extend(FileLike)
|
|
92
|
+
file_io = io # : FileIO
|
|
93
|
+
file_io.original_filename = entity["name"]
|
|
94
|
+
file_io.content_type = entity["content_type"]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Type converters for XML type attributes
|
|
99
|
+
#
|
|
100
|
+
# Maps type attribute values to lambdas that convert string content.
|
|
101
|
+
# Converters with arity 2 receive the content and the full entity hash.
|
|
102
|
+
#
|
|
103
|
+
# @api public
|
|
104
|
+
# @return [Hash{String => Proc}] mapping of type names to converter procs
|
|
105
|
+
# @example Using a converter
|
|
106
|
+
# TYPE_CONVERTERS["integer"].call("42") #=> 42
|
|
107
|
+
TYPE_CONVERTERS = {
|
|
108
|
+
# Primitive types
|
|
109
|
+
"symbol" => :to_sym.to_proc,
|
|
110
|
+
"string" => :to_s.to_proc,
|
|
111
|
+
"integer" => :to_i.to_proc,
|
|
112
|
+
"float" => :to_f.to_proc,
|
|
113
|
+
"double" => :to_f.to_proc,
|
|
114
|
+
"decimal" => ->(s) { BigDecimal(s) },
|
|
115
|
+
"boolean" => ->(s) { !FALSE_BOOLEAN_VALUES.include?(s.strip) },
|
|
116
|
+
|
|
117
|
+
# Date and time types
|
|
118
|
+
"date" => Date.method(:parse),
|
|
119
|
+
"datetime" => PARSE_DATETIME,
|
|
120
|
+
"dateTime" => PARSE_DATETIME,
|
|
121
|
+
|
|
122
|
+
# Binary types
|
|
123
|
+
"base64Binary" => ->(s) { s.unpack1("m") },
|
|
124
|
+
"binary" => ->(s, entity) { (entity["encoding"] == "base64") ? s.unpack1("m") : s },
|
|
125
|
+
"file" => FILE_CONVERTER,
|
|
126
|
+
|
|
127
|
+
# Structured types
|
|
128
|
+
"yaml" => lambda do |string|
|
|
129
|
+
YAML.safe_load(string, permitted_classes: [Symbol, Date, Time])
|
|
130
|
+
rescue ArgumentError, Psych::SyntaxError
|
|
131
|
+
string
|
|
132
|
+
end
|
|
133
|
+
}.freeze
|
|
134
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
module MultiXml
|
|
2
|
+
# Raised when XML parsing fails
|
|
3
|
+
#
|
|
4
|
+
# Preserves the original XML and underlying cause for debugging.
|
|
5
|
+
#
|
|
6
|
+
# @api public
|
|
7
|
+
# @example Catching a parse error
|
|
8
|
+
# begin
|
|
9
|
+
# MultiXml.parse('<invalid>')
|
|
10
|
+
# rescue MultiXml::ParseError => e
|
|
11
|
+
# puts e.xml # The malformed XML
|
|
12
|
+
# puts e.cause # The underlying parser exception
|
|
13
|
+
# end
|
|
14
|
+
class ParseError < StandardError
|
|
15
|
+
# The original XML that failed to parse
|
|
16
|
+
#
|
|
17
|
+
# @api public
|
|
18
|
+
# @return [String, nil] the XML string that caused the error
|
|
19
|
+
# @example Access the failing XML
|
|
20
|
+
# error.xml #=> "<invalid>"
|
|
21
|
+
attr_reader :xml
|
|
22
|
+
|
|
23
|
+
# The underlying parser exception
|
|
24
|
+
#
|
|
25
|
+
# @api public
|
|
26
|
+
# @return [Exception, nil] the original exception from the parser
|
|
27
|
+
# @example Access the cause
|
|
28
|
+
# error.cause #=> #<Nokogiri::XML::SyntaxError: ...>
|
|
29
|
+
attr_reader :cause
|
|
30
|
+
|
|
31
|
+
# Create a new ParseError
|
|
32
|
+
#
|
|
33
|
+
# @api public
|
|
34
|
+
# @param message [String, nil] Error message
|
|
35
|
+
# @param xml [String, nil] The original XML that failed to parse
|
|
36
|
+
# @param cause [Exception, nil] The underlying parser exception
|
|
37
|
+
# @return [ParseError] the new error instance
|
|
38
|
+
# @example Create a parse error
|
|
39
|
+
# ParseError.new("Invalid XML", xml: "<bad>", cause: original_error)
|
|
40
|
+
def initialize(message = nil, xml: nil, cause: nil)
|
|
41
|
+
@xml = xml
|
|
42
|
+
@cause = cause
|
|
43
|
+
super(message)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raised when no XML parser library is available
|
|
48
|
+
#
|
|
49
|
+
# This error is raised when MultiXml cannot find any supported XML parser.
|
|
50
|
+
# Install one of: ox, nokogiri, libxml-ruby, or oga.
|
|
51
|
+
#
|
|
52
|
+
# @api public
|
|
53
|
+
# @example Catching the error
|
|
54
|
+
# begin
|
|
55
|
+
# MultiXml.parse('<root/>')
|
|
56
|
+
# rescue MultiXml::NoParserError => e
|
|
57
|
+
# puts "Please install an XML parser gem"
|
|
58
|
+
# end
|
|
59
|
+
class NoParserError < StandardError; end
|
|
60
|
+
|
|
61
|
+
# Raised when an XML type attribute is in the disallowed list
|
|
62
|
+
#
|
|
63
|
+
# By default, 'yaml' and 'symbol' types are disallowed for security reasons.
|
|
64
|
+
#
|
|
65
|
+
# @api public
|
|
66
|
+
# @example Catching a disallowed type error
|
|
67
|
+
# begin
|
|
68
|
+
# MultiXml.parse('<data type="yaml">--- :key</data>')
|
|
69
|
+
# rescue MultiXml::DisallowedTypeError => e
|
|
70
|
+
# puts e.type #=> "yaml"
|
|
71
|
+
# end
|
|
72
|
+
class DisallowedTypeError < StandardError
|
|
73
|
+
# The disallowed type that was encountered
|
|
74
|
+
#
|
|
75
|
+
# @api public
|
|
76
|
+
# @return [String] the type attribute value that was disallowed
|
|
77
|
+
# @example Access the disallowed type
|
|
78
|
+
# error.type #=> "yaml"
|
|
79
|
+
attr_reader :type
|
|
80
|
+
|
|
81
|
+
# Create a new DisallowedTypeError
|
|
82
|
+
#
|
|
83
|
+
# @api public
|
|
84
|
+
# @param type [String] The disallowed type attribute value
|
|
85
|
+
# @return [DisallowedTypeError] the new error instance
|
|
86
|
+
# @example Create a disallowed type error
|
|
87
|
+
# DisallowedTypeError.new("yaml")
|
|
88
|
+
def initialize(type)
|
|
89
|
+
@type = type
|
|
90
|
+
super("Disallowed type attribute: #{type.inspect}")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module MultiXml
|
|
2
|
+
# Mixin that provides file-like metadata to StringIO objects
|
|
3
|
+
#
|
|
4
|
+
# Used when parsing base64-encoded file content from XML.
|
|
5
|
+
# Adds original_filename and content_type attributes to StringIO.
|
|
6
|
+
#
|
|
7
|
+
# @api public
|
|
8
|
+
# @example Extending a StringIO
|
|
9
|
+
# io = StringIO.new("file content")
|
|
10
|
+
# io.extend(MultiXml::FileLike)
|
|
11
|
+
# io.original_filename = "document.pdf"
|
|
12
|
+
# io.content_type = "application/pdf"
|
|
13
|
+
module FileLike
|
|
14
|
+
# Default filename when none is specified
|
|
15
|
+
# @api public
|
|
16
|
+
# @return [String] the default filename "untitled"
|
|
17
|
+
DEFAULT_FILENAME = "untitled".freeze
|
|
18
|
+
|
|
19
|
+
# Default content type when none is specified
|
|
20
|
+
# @api public
|
|
21
|
+
# @return [String] the default MIME type "application/octet-stream"
|
|
22
|
+
DEFAULT_CONTENT_TYPE = "application/octet-stream".freeze
|
|
23
|
+
|
|
24
|
+
# Set the original filename
|
|
25
|
+
#
|
|
26
|
+
# @api public
|
|
27
|
+
# @param value [String] The filename to set
|
|
28
|
+
# @return [String] the filename that was set
|
|
29
|
+
# @example Set filename
|
|
30
|
+
# io.original_filename = "report.pdf"
|
|
31
|
+
attr_writer :original_filename
|
|
32
|
+
|
|
33
|
+
# Set the content type
|
|
34
|
+
#
|
|
35
|
+
# @api public
|
|
36
|
+
# @param value [String] The MIME type to set
|
|
37
|
+
# @return [String] the content type that was set
|
|
38
|
+
# @example Set content type
|
|
39
|
+
# io.content_type = "application/pdf"
|
|
40
|
+
attr_writer :content_type
|
|
41
|
+
|
|
42
|
+
# Get the original filename
|
|
43
|
+
#
|
|
44
|
+
# @api public
|
|
45
|
+
# @return [String] the original filename or "untitled" if not set
|
|
46
|
+
# @example Get filename
|
|
47
|
+
# io.original_filename #=> "document.pdf"
|
|
48
|
+
def original_filename
|
|
49
|
+
@original_filename || DEFAULT_FILENAME
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get the content type
|
|
53
|
+
#
|
|
54
|
+
# @api public
|
|
55
|
+
# @return [String] the content type or "application/octet-stream" if not set
|
|
56
|
+
# @example Get content type
|
|
57
|
+
# io.content_type #=> "application/pdf"
|
|
58
|
+
def content_type
|
|
59
|
+
@content_type || DEFAULT_CONTENT_TYPE
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|