xml-mapping_extensions 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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