xml-mapping_extensions 0.3.1 → 0.3.2
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 +4 -4
- data/CHANGES.md +4 -0
- data/README.md +70 -8
- data/lib/xml/mapping_extensions/date_node.rb +6 -8
- data/lib/xml/mapping_extensions/typesafe_enum_node.rb +10 -0
- data/lib/xml/mapping_extensions/version.rb +1 -1
- data/lib/xml/mapping_extensions.rb +53 -0
- data/spec/unit/xml/mapping_extensions/array_node_spec.rb +51 -0
- data/spec/unit/xml/mapping_extensions/mapping_extensions_spec.rb +92 -0
- data/spec/unit/xml/mapping_extensions/node_base_spec.rb +19 -2
- data/spec/unit/xml/mapping_extensions/typesafe_enum_node_spec.rb +10 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f68c4d7b4c7476a3c92353cc0f19762e847f560d
|
4
|
+
data.tar.gz: 1d1c1252d4dafe2ea164768aa63c7345ed16a126
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0bbef3436e6917c064244eede751febb5fe568c9537429c2a108549c04b9386f34ec48179a5b6a68cac863344c0883e12845358b08f83ba62014dba94c14de34
|
7
|
+
data.tar.gz: ab29250ee7e8396e0ee6232080a422117192d3447206fd6a2c52e51afa3c0ad00bd4ec4ffdc4a4e28a3970563b9180ea88f970ce8908b8f5466edb9011a10d18
|
data/CHANGES.md
CHANGED
data/README.md
CHANGED
@@ -1,17 +1,64 @@
|
|
1
1
|
# XML::MappingExtensions
|
2
2
|
|
3
|
-
[](https://travis-ci.org/dmolesUC3/xml-mapping_extensions)
|
4
|
+
[](https://codeclimate.com/github/dmolesUC3/xml-mapping_extensions)
|
5
|
+
[](http://inch-ci.org/github/dmolesUC3/xml-mapping_extensions)
|
6
6
|
[](https://github.com/dmolesUC3/xml-mapping_extensions/releases)
|
7
7
|
|
8
|
-
Additional mapping nodes and other utility
|
8
|
+
Additional mapping nodes and other utility code for working with
|
9
9
|
[XML::Mapping](http://multi-io.github.io/xml-mapping/).
|
10
10
|
|
11
|
-
##
|
11
|
+
## Extension methods
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
This gem adds two methods, `write_xml` and `parse_xml`, to XML mapping instances and classes respectively, to reduce
|
14
|
+
boilerplate.
|
15
|
+
|
16
|
+
The `write_xml` method supplements [XML::Mapping#save_to_xml] by writing the object out as a `String` rather than as an `REXML::Element`.
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
elem = MyElement.new
|
20
|
+
elem.attribute = 123
|
21
|
+
elem.text = 'element text'
|
22
|
+
elem.children = ['child 1', 'child 2']
|
23
|
+
puts elem.write_xml
|
24
|
+
```
|
25
|
+
|
26
|
+
outputs
|
27
|
+
|
28
|
+
```xml
|
29
|
+
<my-element attribute='123'>
|
30
|
+
element text
|
31
|
+
<child>child 1</child>
|
32
|
+
<child>child 2</child>
|
33
|
+
</my-element>
|
34
|
+
```
|
35
|
+
|
36
|
+
The `parse_xml` method supplements
|
37
|
+
[XML::Mapping::ClassMethods#load_from_xml](http://multi-io.github.io/xml-mapping/XML/Mapping/ClassMethods.html#method-i-load_from_xml)
|
38
|
+
by abstracting away the difference between strings, XML documents, XML elements,
|
39
|
+
files, and IO-like objects.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
my_xml_path = 'my_xml.xml'
|
43
|
+
my_xml_file = File.new(my_xml_path)
|
44
|
+
my_xml_string = File.read(my_xml_path)
|
45
|
+
my_xml_io = StringIO.new(my_xml_string)
|
46
|
+
my_xml_document = REXML::Document.new(my_xml_string)
|
47
|
+
my_xml_element = my_xml_document.root
|
48
|
+
|
49
|
+
# Standard XML::Mapping load_from_xml method
|
50
|
+
elem = MyXMLClass.load_from_xml(my_xml_element)
|
51
|
+
|
52
|
+
# parse_xml equivalent
|
53
|
+
[my_xml_file, my_xml_string, my_xml_io, my_xml_document, my_xml_element].each do |xml_source|
|
54
|
+
expect(MyXMLClass.parse_xml(xml_source)).to eq(elem) # assuming MyXMLClass implements ==
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
## Custom nodes
|
59
|
+
|
60
|
+
To create a custom node type, require `xml/mapping_extensions` and extend one of
|
61
|
+
the abstract node classes, or use one of the provided implementations.
|
15
62
|
|
16
63
|
### Abstract nodes
|
17
64
|
|
@@ -21,6 +68,21 @@ classes, or use one of the provided implementations.
|
|
21
68
|
Note that you must call `::XML::Mapping.add_node_class` for your new node class
|
22
69
|
to be registered with the XML mapping engine.
|
23
70
|
|
71
|
+
#### Example
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
class LaTeXRationalNode < XML::MappingExtensions::NodeBase
|
75
|
+
def to_value(xml_text)
|
76
|
+
match_data = /\\frac\{([0-9.]+)\}\{([0-9.]+)\}/.match(xml_text)
|
77
|
+
Rational("#{match_data[1]}/#{match_data[2]}")
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_xml_text(value)
|
81
|
+
"\\frac{#{value.numerator}}{#{value.denominator}}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
24
86
|
### Provided implementations
|
25
87
|
|
26
88
|
- `DateNode`: maps XML Schema dates to `Date` objects
|
@@ -29,7 +91,7 @@ to be registered with the XML mapping engine.
|
|
29
91
|
- `MimeTypeNode`: maps MIME type strings to `MIME::Type` objects
|
30
92
|
- `TypesafeEnumNode`: maps XML strings to [typesafe_enum](https://github.com/dmolesUC3/typesafe_enum) values
|
31
93
|
|
32
|
-
|
94
|
+
#### Example
|
33
95
|
|
34
96
|
```ruby
|
35
97
|
require 'xml/mapping_extensions'
|
@@ -5,13 +5,9 @@ require 'time'
|
|
5
5
|
module XML
|
6
6
|
module MappingExtensions
|
7
7
|
# XML mapping for XML Schema dates.
|
8
|
-
# Known limitation: loses time zone info
|
8
|
+
# Known limitation: loses time zone info (Ruby `Date` doesn't
|
9
|
+
# support it)
|
9
10
|
class DateNode < NodeBase
|
10
|
-
|
11
|
-
def initialize(*args)
|
12
|
-
super
|
13
|
-
end
|
14
|
-
|
15
11
|
# Whether date should be output with UTC "Zulu" time
|
16
12
|
# designator ("Z")
|
17
13
|
# @return [Boolean, nil] True if date should be output
|
@@ -20,15 +16,17 @@ module XML
|
|
20
16
|
@options[:zulu]
|
21
17
|
end
|
22
18
|
|
23
|
-
#
|
19
|
+
# Converts an XML Schema date string to a `Date` object
|
20
|
+
# @param xml_text [String] an XML Schema date
|
24
21
|
# @return [Date] the value as a `Date`
|
25
22
|
def to_value(xml_text)
|
26
23
|
Date.xmlschema(xml_text)
|
27
24
|
end
|
28
25
|
|
26
|
+
# Converts a `Date` object to an XML schema string
|
29
27
|
# @param value [Date] the value as a `Date`
|
30
28
|
# @return [String] the value as an XML Schema date string, without
|
31
|
-
# time zone information
|
29
|
+
# time zone information unless {#zulu} is set
|
32
30
|
def to_xml_text(value)
|
33
31
|
text = value.xmlschema
|
34
32
|
zulu ? "#{text}Z" : text
|
@@ -11,17 +11,27 @@ module XML
|
|
11
11
|
# # for node class MyEnum
|
12
12
|
# typesafe_enum_node :my_enum, '@my_enum', default_value: nil, class: MyEnum
|
13
13
|
class TypesafeEnumNode < NodeBase
|
14
|
+
|
15
|
+
# Creates a new {TypesafeEnumNode}
|
16
|
+
# @param :class the enum class
|
14
17
|
def initialize(*args)
|
15
18
|
super
|
16
19
|
@enum_class = @options[:class]
|
20
|
+
fail ArgumentError, "No :class found for TypesafeEnumNode #{@attrname} of #{@owner}" unless @enum_class
|
17
21
|
end
|
18
22
|
|
23
|
+
# Converts an enum value or value string to an enum instance
|
24
|
+
# @param xml_text the enum value or value string
|
25
|
+
# @return [TypesafeEnum::Base] an instance of the enum class declared in the initializer
|
19
26
|
def to_value(xml_text)
|
20
27
|
enum_instance = @enum_class.find_by_value(xml_text)
|
21
28
|
enum_instance = @enum_class.find_by_value_str(xml_text) unless enum_instance
|
22
29
|
enum_instance
|
23
30
|
end
|
24
31
|
|
32
|
+
# Converts an enum value or value string to an enum instance
|
33
|
+
# @param enum_instance [TypesafeEnum::Base] an instance of the enum class declared in the initializer
|
34
|
+
# @return [String] the enum value as a string
|
25
35
|
def to_xml_text(enum_instance)
|
26
36
|
enum_instance.value.to_s if enum_instance
|
27
37
|
end
|
@@ -1,7 +1,60 @@
|
|
1
|
+
require 'xml/mapping'
|
2
|
+
|
1
3
|
module XML
|
2
4
|
# Additional mapping nodes and other utility classes for working with
|
3
5
|
# [XML::Mapping](http://multi-io.github.io/xml-mapping/)
|
4
6
|
module MappingExtensions
|
5
7
|
Dir.glob(File.expand_path('../mapping_extensions/*.rb', __FILE__), &method(:require))
|
6
8
|
end
|
9
|
+
|
10
|
+
module Mapping
|
11
|
+
|
12
|
+
# Writes this mapped object as an XML string.
|
13
|
+
#
|
14
|
+
# @param options [Hash] the options to be passed to
|
15
|
+
# [XML::Mapping#save_to_xml](http://multi-io.github.io/xml-mapping/XML/Mapping.html#method-i-save_to_xml)
|
16
|
+
# @return [String] the XML form of the object, as a compact, pretty-printed string.
|
17
|
+
def write_xml(options = { mapping: :_default })
|
18
|
+
xml = save_to_xml(options)
|
19
|
+
formatter = REXML::Formatters::Pretty.new
|
20
|
+
formatter.compact = true
|
21
|
+
io = StringIO.new
|
22
|
+
formatter.write(xml, io)
|
23
|
+
io.string
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
# Create a new instance of this class from the XML contained in
|
28
|
+
# `xml`, which can be a string, REXML document, or REXML element
|
29
|
+
# @param xml [String, REXML::Document, REXML::Element]
|
30
|
+
# @return [Object] an instance of this class.
|
31
|
+
def parse_xml(xml)
|
32
|
+
element = case xml
|
33
|
+
when REXML::Document
|
34
|
+
xml.root
|
35
|
+
when REXML::Element
|
36
|
+
xml
|
37
|
+
else
|
38
|
+
fail ArgumentError, "Unexpected argument type; expected XML document, String, or IO source, was #{xml.class}" unless can_parse(xml)
|
39
|
+
REXML::Document.new(xml).root
|
40
|
+
end
|
41
|
+
load_from_xml(element)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Whether the argument can be parsed as an `REXML::Document`, i.e. whether
|
47
|
+
# it is a `String` or something `IO`-like
|
48
|
+
# @param arg [Object] a possibly-parsable object
|
49
|
+
# @return [Boolean] true if `REXML::Document.new()` should be able to parse
|
50
|
+
# the argument, false otherwise
|
51
|
+
def can_parse(arg)
|
52
|
+
arg.is_a?(String) ||
|
53
|
+
(arg.respond_to?(:read) &&
|
54
|
+
arg.respond_to?(:readline) &&
|
55
|
+
arg.respond_to?(:nil?) &&
|
56
|
+
arg.respond_to?(:eof?))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
7
60
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module XML
|
4
|
+
module Mapping
|
5
|
+
|
6
|
+
class ArrayNodeSpecElem
|
7
|
+
include ::XML::Mapping
|
8
|
+
|
9
|
+
root_element_name 'array_node_spec_elem'
|
10
|
+
|
11
|
+
array_node :child_nodes, 'child_nodes', 'child_node', class: String
|
12
|
+
end
|
13
|
+
|
14
|
+
describe ArrayNode do
|
15
|
+
it 'reads nested nodes' do
|
16
|
+
xml_str = '<array_node_spec_elem>
|
17
|
+
<child_nodes>
|
18
|
+
<child_node>foo</child_node>
|
19
|
+
<child_node>bar</child_node>
|
20
|
+
</child_nodes>
|
21
|
+
</array_node_spec_elem>'
|
22
|
+
xml = REXML::Document.new(xml_str).root
|
23
|
+
elem = ArrayNodeSpecElem.load_from_xml(xml)
|
24
|
+
expect(elem.child_nodes).to eq(%w(foo bar))
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'writes nested nodes' do
|
28
|
+
elem = ArrayNodeSpecElem.new
|
29
|
+
elem.child_nodes = %w(foo bar)
|
30
|
+
xml = elem.save_to_xml
|
31
|
+
expected_xml = '<array_node_spec_elem>
|
32
|
+
<child_nodes>
|
33
|
+
<child_node>foo</child_node>
|
34
|
+
<child_node>bar</child_node>
|
35
|
+
</child_nodes>
|
36
|
+
</array_node_spec_elem>'
|
37
|
+
expect(xml).to be_xml(expected_xml)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'writes empty intermediate nodes for empty arrays' do
|
41
|
+
elem = ArrayNodeSpecElem.new
|
42
|
+
elem.child_nodes = []
|
43
|
+
xml = elem.save_to_xml
|
44
|
+
expected_xml = '<array_node_spec_elem>
|
45
|
+
<child_nodes/>
|
46
|
+
</array_node_spec_elem>'
|
47
|
+
expect(xml).to be_xml(expected_xml)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module XML
|
5
|
+
module Mapping
|
6
|
+
class ParseXMLSpecElement
|
7
|
+
include ::XML::Mapping
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
root_element_name 'element'
|
11
|
+
|
12
|
+
numeric_node :attribute, '@attribute'
|
13
|
+
text_node :text, 'text()'
|
14
|
+
array_node :children, 'child', class: String
|
15
|
+
|
16
|
+
def <=>(other)
|
17
|
+
return nil unless self.class == other.class
|
18
|
+
[:attribute, :text, :children].each do |p|
|
19
|
+
order = send(p) <=> other.send(p)
|
20
|
+
return order if order != 0
|
21
|
+
end
|
22
|
+
0
|
23
|
+
end
|
24
|
+
|
25
|
+
def hash
|
26
|
+
[:attribute, :text, :children].hash
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#write_xml' do
|
31
|
+
it 'writes an XML string' do
|
32
|
+
elem = ParseXMLSpecElement.new
|
33
|
+
elem.attribute = 123
|
34
|
+
elem.text = 'element text'
|
35
|
+
elem.children = ['child 1', 'child 2']
|
36
|
+
expected_xml = elem.save_to_xml
|
37
|
+
xml_string = elem.write_xml
|
38
|
+
expect(xml_string).to be_a(String)
|
39
|
+
expect(xml_string).to be_xml(expected_xml)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module ClassMethods
|
44
|
+
describe '#parse_xml' do
|
45
|
+
|
46
|
+
before(:each) do
|
47
|
+
@xml_string = '<element attribute="123">
|
48
|
+
element text
|
49
|
+
<child>child 1</child>
|
50
|
+
<child>child 2</child>
|
51
|
+
</element>'
|
52
|
+
@xml_document = REXML::Document.new(@xml_string)
|
53
|
+
@xml_element = @xml_document.root
|
54
|
+
@expected_element = ParseXMLSpecElement.load_from_xml(@xml_element)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'parses a String' do
|
58
|
+
elem = ParseXMLSpecElement.parse_xml(@xml_string)
|
59
|
+
expect(elem).to eq(@expected_element)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'parses a REXML::Document' do
|
63
|
+
elem = ParseXMLSpecElement.parse_xml(@xml_document)
|
64
|
+
expect(elem).to eq(@expected_element)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'parses a REXML::Element' do
|
68
|
+
elem = ParseXMLSpecElement.parse_xml(@xml_element)
|
69
|
+
expect(elem).to eq(@expected_element)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'parses an IO' do
|
73
|
+
xml_io = StringIO.new(@xml_string)
|
74
|
+
elem = ParseXMLSpecElement.parse_xml(xml_io)
|
75
|
+
expect(elem).to eq(@expected_element)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'parses a file' do
|
79
|
+
xml_file = Tempfile.new(%w(parse_xml_spec .xml))
|
80
|
+
begin
|
81
|
+
xml_file.write(@xml_string)
|
82
|
+
xml_file.rewind
|
83
|
+
elem = ParseXMLSpecElement.parse_xml(xml_file)
|
84
|
+
expect(elem).to eq(@expected_element)
|
85
|
+
ensure
|
86
|
+
xml_file.close(true)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -7,17 +7,34 @@ module XML
|
|
7
7
|
include ::XML::Mapping
|
8
8
|
end
|
9
9
|
|
10
|
+
class LaTeXRationalNode < NodeBase
|
11
|
+
def to_value(xml_text)
|
12
|
+
match_data = /\\frac\{([0-9.]+)\}\{([0-9.]+)\}/.match(xml_text)
|
13
|
+
Rational("#{match_data[1]}/#{match_data[2]}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_xml_text(value)
|
17
|
+
"\\frac{#{value.numerator}}{#{value.denominator}}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
10
21
|
describe NodeBase do
|
11
22
|
before :each do
|
12
23
|
@node = NodeBase.new(SomeMappingClass, :attr_name, 'attr_name')
|
13
24
|
end
|
14
25
|
|
15
26
|
describe '#extract_attr_value' do
|
16
|
-
it 'forwards to #to_value'
|
27
|
+
it 'forwards to #to_value' do
|
28
|
+
node = LaTeXRationalNode.new(SomeMappingClass, :attr_name, 'attr_name')
|
29
|
+
expect(node.to_value('\frac{1}{2}')).to eq(Rational('1/2'))
|
30
|
+
end
|
17
31
|
end
|
18
32
|
|
19
33
|
describe '#set_attr_value' do
|
20
|
-
it 'forwards to #to_xml_text'
|
34
|
+
it 'forwards to #to_xml_text' do
|
35
|
+
node = LaTeXRationalNode.new(SomeMappingClass, :attr_name, 'attr_name')
|
36
|
+
expect(node.to_xml_text(Rational('1/2'))).to eq('\frac{1}{2}')
|
37
|
+
end
|
21
38
|
end
|
22
39
|
|
23
40
|
describe '#xml_text' do
|
@@ -90,6 +90,16 @@ module XML
|
|
90
90
|
elem = TypesafeEnumNodeSpecElem.load_from_xml(doc.root)
|
91
91
|
expect(elem.save_to_xml).to be_xml(xml_string)
|
92
92
|
end
|
93
|
+
|
94
|
+
it 'requires a class' do
|
95
|
+
expect do
|
96
|
+
class TypesafeEnumNodeSpecElem
|
97
|
+
include ::XML::Mapping
|
98
|
+
|
99
|
+
typesafe_enum_node :my_bad_enum, '@my_bad_enum'
|
100
|
+
end
|
101
|
+
end.to raise_error(ArgumentError)
|
102
|
+
end
|
93
103
|
end
|
94
104
|
end
|
95
105
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xml-mapping_extensions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Moles
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-12-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mime-types
|
@@ -198,7 +198,9 @@ files:
|
|
198
198
|
- spec/.rubocop.yml
|
199
199
|
- spec/rspec_custom_matchers.rb
|
200
200
|
- spec/spec_helper.rb
|
201
|
+
- spec/unit/xml/mapping_extensions/array_node_spec.rb
|
201
202
|
- spec/unit/xml/mapping_extensions/date_node_spec.rb
|
203
|
+
- spec/unit/xml/mapping_extensions/mapping_extensions_spec.rb
|
202
204
|
- spec/unit/xml/mapping_extensions/mime_type_node_spec.rb
|
203
205
|
- spec/unit/xml/mapping_extensions/node_base_spec.rb
|
204
206
|
- spec/unit/xml/mapping_extensions/time_node_spec.rb
|
@@ -233,7 +235,9 @@ test_files:
|
|
233
235
|
- spec/.rubocop.yml
|
234
236
|
- spec/rspec_custom_matchers.rb
|
235
237
|
- spec/spec_helper.rb
|
238
|
+
- spec/unit/xml/mapping_extensions/array_node_spec.rb
|
236
239
|
- spec/unit/xml/mapping_extensions/date_node_spec.rb
|
240
|
+
- spec/unit/xml/mapping_extensions/mapping_extensions_spec.rb
|
237
241
|
- spec/unit/xml/mapping_extensions/mime_type_node_spec.rb
|
238
242
|
- spec/unit/xml/mapping_extensions/node_base_spec.rb
|
239
243
|
- spec/unit/xml/mapping_extensions/time_node_spec.rb
|