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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: df4aa8abcf1f0411d135fc696128f7d31f58aa70
4
- data.tar.gz: 654c655782c2eeb1d0916960e5f3c3a294bb5926
3
+ metadata.gz: f68c4d7b4c7476a3c92353cc0f19762e847f560d
4
+ data.tar.gz: 1d1c1252d4dafe2ea164768aa63c7345ed16a126
5
5
  SHA512:
6
- metadata.gz: c653214e5cf34f8a7f8a5cc2cb4ba9fb8a745873095aa405eb3a50eb14cc083f8a2071671b5e4e413fbbd8bf29f6fa8f8c51845df47242f7c86ec2662136c74f
7
- data.tar.gz: 7f7e5cfaa402d787cd09c1b66b7c9bdf72f6e64a9b6f6c3a944f2153f8f88430bef6b6cede5acc2e847ff33daf206c5f8f6e184c879f276b19087cfe4e84f25b
6
+ metadata.gz: 0bbef3436e6917c064244eede751febb5fe568c9537429c2a108549c04b9386f34ec48179a5b6a68cac863344c0883e12845358b08f83ba62014dba94c14de34
7
+ data.tar.gz: ab29250ee7e8396e0ee6232080a422117192d3447206fd6a2c52e51afa3c0ad00bd4ec4ffdc4a4e28a3970563b9180ea88f970ce8908b8f5466edb9011a10d18
data/CHANGES.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.3.2
2
+
3
+ - Add `XML::Mapping#write_xml` and `XML::Mapping::ClassMethods#parse_xml`
4
+
1
5
  ## 0.3.1 (19 November 2015)
2
6
 
3
7
  - Update to [typesafe_enum](https://github.com/dmolesUC3/typesafe_enum) 0.1.2.
data/README.md CHANGED
@@ -1,17 +1,64 @@
1
1
  # XML::MappingExtensions
2
2
 
3
- [![Build Status](https://travis-ci.org/dmolesUC3/xml-mapping_extensions.png?branch=master)](https://travis-ci.org/dmolesUC3/xml-mapping_extensions)
4
- [![Code Climate](https://codeclimate.com/github/dmolesUC3/xml-mapping_extensions.png)](https://codeclimate.com/github/dmolesUC3/xml-mapping_extensions)
5
- [![Inline docs](http://inch-ci.org/github/dmolesUC3/xml-mapping_extensions.png)](http://inch-ci.org/github/dmolesUC3/xml-mapping_extensions)
3
+ [![Build Status](https://travis-ci.org/dmolesUC3/xml-mapping_extensions.svg?branch=master)](https://travis-ci.org/dmolesUC3/xml-mapping_extensions)
4
+ [![Code Climate](https://codeclimate.com/github/dmolesUC3/xml-mapping_extensions.svg)](https://codeclimate.com/github/dmolesUC3/xml-mapping_extensions)
5
+ [![Inline docs](http://inch-ci.org/github/dmolesUC3/xml-mapping_extensions.svg)](http://inch-ci.org/github/dmolesUC3/xml-mapping_extensions)
6
6
  [![Gem Version](https://img.shields.io/gem/v/xml-mapping_extensions.svg)](https://github.com/dmolesUC3/xml-mapping_extensions/releases)
7
7
 
8
- Additional mapping nodes and other utility classes for working with
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
- ## Usage
11
+ ## Extension methods
12
12
 
13
- Require `xml/mapping_extensions` and extend one of the abstract node
14
- classes, or use one of the provided implementations.
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
- ### Example
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
- # param xml_text [String] an XML Schema date
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,6 +1,6 @@
1
1
  module XML
2
2
  module MappingExtensions
3
3
  # The version of this gem
4
- VERSION = '0.3.1'
4
+ VERSION = '0.3.2'
5
5
  end
6
6
  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.1
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-20 00:00:00.000000000 Z
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