xml-mapping_extensions 0.4.3 → 0.4.4

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: 8846f750f369604e10e815fa58a11019c8dd8b3d
4
- data.tar.gz: 3b05e612551a8d455108af3d7f351f5b6566b2e0
3
+ metadata.gz: 36f81c099c7ffafcad6ffa1af533b3a5f9ced684
4
+ data.tar.gz: 09a3a094c1855d14b2e3f89d72ba210bc0a7ede1
5
5
  SHA512:
6
- metadata.gz: 89e7edf4af982cb52e5222f385d136911e4ca48e5719bff5c6576c691e151f152dc2ec6f91a6d6f997b1f1a6ae146677c208ae6c29f5375592498912e505bc07
7
- data.tar.gz: 44b088113a9a0bc8f1335e1cc447977ca94ac9e516823c5bf436cb78060e7a2e73336a8b6208e6611ffe2ed080d2bca55eb4fea84a5334edaef1049c77033365
6
+ metadata.gz: b94a791a3a778e2e457e177a04c424011b012a422532aebfc9257a97ec08a3866b991a87b13df49f6ef8d65960e22fee4149426897437c5f74131e5b9ef8a14e
7
+ data.tar.gz: 247e5a8e25895cdbad0f3bddf62aafdde2dae6f89e77770e86fa22bd05d89741b0d36b342779851e2812cc55c7247e8d424639ce6c8b028fae5dac92e7e9a3b7
data/CHANGES.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.4.4 (12 July 2016)
2
+
3
+ - Add '#fallback_mapping'
4
+
1
5
  ## 0.4.3 (11 July 2016)
2
6
 
3
7
  - Allow `write_xml` and `parse_xml` to take an `options` hash, to be passed on to `save_to_xml` or `load_from_xml`,
data/README.md CHANGED
@@ -19,7 +19,7 @@ Additional mapping nodes and other utility code for working with
19
19
  - [Reading](#reading)
20
20
  - [Writing](#writing)
21
21
  - [Namespaces](#namespaces)
22
-
22
+ - [Fallback mappings](#fallback-mappings)
23
23
 
24
24
  ## Extension methods
25
25
 
@@ -264,3 +264,63 @@ puts obj.write_xml
264
264
  <px:child>child 2</px:child>
265
265
  </px:element>
266
266
  ```
267
+
268
+ ## Fallback mappings
269
+
270
+ The [XML::Mapping](http://multi-io.github.io/xml-mapping/) library provides an “alternate mapping”
271
+ mechanism allowing
272
+ [multiple XML mappings per class](http://multi-io.github.io/xml-mapping/#label-Multiple+Mappings+per+Class).
273
+ However, it requires each mapping to be exhaustive -- an alternate mapping must redefine all attribute
274
+ mappings defined in the primary, or else ignore those attributes. Sometimes, however, it is
275
+
276
+ ```ruby
277
+ class ValidatingElement
278
+ include ::XML::Mapping
279
+
280
+ root_element_name 'element'
281
+ text_node :name, '@name'
282
+ text_node :value, '@value'
283
+
284
+ use_mapping :strict
285
+ numeric_node :value, '@value', writer: proc { |obj, xml| xml.add_attribute('value', Float(obj.value)) }
286
+ end
287
+
288
+ invalid_string = '<element name="abc" value="efg"/>'
289
+ ValidatingElement.parse_xml(invalid_string, mapping: :strict)
290
+ # ArgumentError: invalid value for Float(): "efg"
291
+
292
+ elem = ValidatingElement.parse_xml(invalid_string) # OK
293
+ # => #<XML::Mapping::ValidatingElement:0x007fa5631ae9c8>
294
+ elem.write_xml
295
+ # => "<element name='abc' value='efg'/>"
296
+
297
+ elem.write_xml(mapping: :strict)
298
+ # ArgumentError: invalid value for Float(): "efg"
299
+ ```
300
+
301
+ So far, so good; but say we set a valid value and try to output with the
302
+ `:strict` mapping?
303
+
304
+ ```ruby
305
+ elem.value = 123
306
+ elem.write_xml(mapping: :strict)
307
+ # => <validating-element value='123'/>
308
+ ```
309
+
310
+ Since the `:strict` mapping doesn't define a root element name, we get the
311
+ default (based on the class name), and since it doesn't define a mapping
312
+ for the `name` attribute, we lose that entirely.
313
+
314
+ But if we add a fallback mapping --
315
+
316
+ ```ruby
317
+ class ValidatingElement
318
+ fallback_mapping :strict, :_default
319
+ end
320
+
321
+ elem.write_xml(mapping: :strict)
322
+ # => <element value='123' name='abc'/>
323
+ ```
324
+
325
+ -- the `:strict` mapping now gets the root element name `element`,
326
+ and the `name` attribute, as defined under the `:_default` mapping.
@@ -1,6 +1,6 @@
1
1
  module XML
2
2
  module MappingExtensions
3
3
  # The version of this gem
4
- VERSION = '0.4.3'
4
+ VERSION = '0.4.4'
5
5
  end
6
6
  end
@@ -48,7 +48,18 @@ module XML
48
48
  io.string
49
49
  end
50
50
 
51
+ # Additional accessors needed for `#fallback_mapping`.
52
+ class Node
53
+ attr_reader :attrname
54
+ attr_accessor :mapping
55
+ end
56
+
51
57
  module ClassMethods
58
+
59
+ # Gets the configured root element names for this object.
60
+ # @return [Hash[Symbol, String]] the root element names for this object, by mapping.
61
+ attr_reader :root_element_names
62
+
52
63
  # Create a new instance of this class from the XML contained in
53
64
  # `xml`, which can be a string, REXML document, or REXML element
54
65
  # @param xml [String, REXML::Document, REXML::Element]
@@ -66,6 +77,26 @@ module XML
66
77
  load_from_xml(element, options)
67
78
  end
68
79
 
80
+ # Configures one mapping as a fallback for another, allowing mappings
81
+ # that differ e.g. from the default only in some small detail. Creates
82
+ # mapping nodes based on the fallback for all attributes for which a
83
+ # mapping node is present in the fallback but not in the primary, and
84
+ # likewise sets the root element name for the primary to the root element
85
+ # name for the fallback if it has not already been set.
86
+ #
87
+ # @param mapping [Symbol] the primary mapping
88
+ # @param fallback [Symbol] the fallback mapping to be used if the primary
89
+ # mapping lacks a mapping node or root element name
90
+ def fallback_mapping(mapping, fallback)
91
+ mapping_nodes = nodes_by_attrname(mapping)
92
+ add_fallback_nodes(mapping_nodes, mapping, fallback)
93
+ xml_mapping_nodes_hash[mapping] = mapping_nodes.values
94
+
95
+ return if root_element_names[mapping]
96
+ fallback_name = root_element_names[fallback]
97
+ root_element_name(fallback_name, mapping: mapping)
98
+ end
99
+
69
100
  private
70
101
 
71
102
  # Whether the argument can be parsed as an `REXML::Document`, i.e. whether
@@ -76,9 +107,34 @@ module XML
76
107
  def can_parse(arg)
77
108
  arg.is_a?(String) ||
78
109
  (arg.respond_to?(:read) &&
79
- arg.respond_to?(:readline) &&
80
- arg.respond_to?(:nil?) &&
81
- arg.respond_to?(:eof?))
110
+ arg.respond_to?(:readline) &&
111
+ arg.respond_to?(:nil?) &&
112
+ arg.respond_to?(:eof?))
113
+ end
114
+
115
+ # Returns the nodes for the specified mapping as a hash accessible by
116
+ # their attribute names.
117
+ # @param mapping [Symbol] the mapping
118
+ # @return [Hash[Symbol, Node]] the nodes
119
+ def nodes_by_attrname(mapping)
120
+ nodes = xml_mapping_nodes(mapping: mapping)
121
+ nodes.map { |node| [node.attrname, node] }.to_h
122
+ end
123
+
124
+ # Creates mapping nodes based on the fallback for all attributes for which a
125
+ # mapping node is present in the fallback but not in the primary.
126
+ # @param mapping_nodes [Hash[Symbol, Node]] The primary nodes
127
+ # @param mapping [Symbol] The primary mapping
128
+ # @param fallback [Symbol] The fallback mapping
129
+ def add_fallback_nodes(mapping_nodes, mapping, fallback)
130
+ xml_mapping_nodes(mapping: fallback).each do |fallback_node|
131
+ attrname = fallback_node.attrname
132
+ next if mapping_nodes.key?(attrname)
133
+
134
+ node = fallback_node.clone
135
+ fallback_node.mapping = mapping
136
+ mapping_nodes[attrname] = node
137
+ end
82
138
  end
83
139
  end
84
140
  end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ module XML
4
+ module Mapping
5
+ class FMSpecObject
6
+ include ::XML::Mapping
7
+
8
+ root_element_name 'element'
9
+
10
+ numeric_node :attribute, '@attribute'
11
+ text_node :text, 'text()'
12
+ array_node :children, 'child', class: String
13
+
14
+ use_mapping :alternate
15
+ text_node :attribute, '@attribute'
16
+ array_node :children, 'children',
17
+ reader: (proc do |obj, xml|
18
+ children_elem = xml.elements['children']
19
+ obj.children = children_elem ? children_elem.text.split(',') : []
20
+ end),
21
+ writer: (proc do |obj, xml|
22
+ children_text = obj.children.join(',') if obj.children && !obj.children.empty?
23
+ xml.add_element('children').text = children_text if children_text
24
+ end)
25
+
26
+ fallback_mapping :alternate, :_default
27
+ end
28
+
29
+ describe '#save_to_xml' do
30
+ it 'writes an XML string' do
31
+ obj = FMSpecObject.new
32
+ obj.attribute = 123
33
+ obj.text = 'element text'
34
+ obj.children = ['child 1', 'child 2']
35
+ saved_xml = obj.save_to_xml
36
+ expected_xml = '<element attribute="123">element text<child>child 1</child><child>child 2</child></element>'
37
+ expect(saved_xml).to be_xml(expected_xml)
38
+ end
39
+
40
+ it 'accepts an alternate mapping' do
41
+ obj = FMSpecObject.new
42
+ obj.attribute = 'elvis'
43
+ obj.text = 'element text'
44
+ obj.children = ['child 1', 'child 2']
45
+ saved_xml = obj.save_to_xml(mapping: :alternate)
46
+ expected_xml = '<element attribute="elvis">element text<children>child 1,child 2</children></element>'
47
+ expect(saved_xml).to be_xml(expected_xml)
48
+ end
49
+ end
50
+
51
+ describe '#parse_xml' do
52
+ it 'parses a String' do
53
+ xml_string = '<element attribute="123">element text<child>child 1</child><child>child 2</child></element>'
54
+ obj = FMSpecObject.parse_xml(xml_string)
55
+ expect(obj.attribute).to eq(123)
56
+ expect(obj.text).to eq('element text')
57
+ expect(obj.children).to eq(['child 1', 'child 2'])
58
+ end
59
+
60
+ it 'accepts an alternate mapping' do
61
+ xml_string = '<element attribute="elvis">element text<children>child 1,child 2</children></element>'
62
+ obj = FMSpecObject.parse_xml(xml_string, mapping: :alternate)
63
+ expect(obj.attribute).to eq('elvis')
64
+ expect(obj.text).to eq('element text')
65
+ expect(obj.children).to eq(['child 1', 'child 2'])
66
+ end
67
+ end
68
+
69
+ class ValidatingElement
70
+ include ::XML::Mapping
71
+
72
+ root_element_name 'element'
73
+ text_node :name, '@name'
74
+ text_node :value, '@value'
75
+
76
+ use_mapping :strict
77
+ numeric_node :value, '@value', writer: proc { |obj, xml| xml.add_attribute('value', Float(obj.value)) }
78
+
79
+ fallback_mapping :strict, :_default
80
+ end
81
+
82
+ describe '#fallback_mapping' do
83
+ it 'round-trips' do
84
+ invalid_string = '<element name="abc" value="efg"/>'
85
+ expect { ValidatingElement.parse_xml(invalid_string, mapping: :strict) }.to raise_error(ArgumentError)
86
+
87
+ elem = ValidatingElement.parse_xml(invalid_string) # OK
88
+ expect(elem.write_xml).to be_xml("<element name='abc' value='efg'/>")
89
+
90
+ expect { elem.write_xml(mapping: :strict) }.to raise_error(ArgumentError)
91
+
92
+ elem.value = 123.4
93
+ expect(elem.write_xml(mapping: :strict)).to be_xml("<element value='123.4' name='abc'/>")
94
+ end
95
+ end
96
+ end
97
+ end
@@ -14,7 +14,15 @@ module XML
14
14
  array_node :children, 'child', class: String
15
15
 
16
16
  use_mapping :alternate
17
- text_node :text_alt, 'text()'
17
+ array_node :children, 'children',
18
+ reader: (proc do |obj, xml|
19
+ children_elem = xml.elements['children']
20
+ obj.children = children_elem ? children_elem.text.split(',') : []
21
+ end),
22
+ writer: (proc do |obj, xml|
23
+ children_text = obj.children.join(',') if obj.children && !obj.children.empty?
24
+ xml.add_element('children').text = children_text if children_text
25
+ end)
18
26
 
19
27
  def <=>(other)
20
28
  return nil unless self.class == other.class
@@ -45,67 +53,71 @@ module XML
45
53
  it 'accepts an alternate mapping' do
46
54
  obj = MXSpecObject.new
47
55
  obj.attribute = 123
48
- obj.text_alt = 'element text'
49
56
  obj.children = ['child 1', 'child 2']
50
- expected_xml = obj.save_to_xml(mapping: :alternate)
57
+ saved_xml = obj.save_to_xml(mapping: :alternate)
51
58
  xml_string = obj.write_xml(mapping: :alternate)
52
59
  expect(xml_string).to be_a(String)
53
- expect(xml_string).to be_xml(expected_xml)
60
+ expect(xml_string).to be_xml(saved_xml)
54
61
  end
55
62
  end
56
63
 
57
- module ClassMethods
58
- describe '#parse_xml' do
64
+ describe '#parse_xml' do
59
65
 
60
- before(:each) do
61
- @xml_string = '<element attribute="123">
66
+ before(:each) do
67
+ @xml_string = '<element attribute="123">
62
68
  element text
63
69
  <child>child 1</child>
64
70
  <child>child 2</child>
65
71
  </element>'
66
- @xml_document = REXML::Document.new(@xml_string)
67
- @xml_element = @xml_document.root
68
- @expected_element = MXSpecObject.load_from_xml(@xml_element)
69
- end
72
+ @xml_document = REXML::Document.new(@xml_string)
73
+ @xml_element = @xml_document.root
74
+ @expected_element = MXSpecObject.load_from_xml(@xml_element)
75
+ end
70
76
 
71
- it 'parses a String' do
72
- obj = MXSpecObject.parse_xml(@xml_string)
73
- expect(obj).to eq(@expected_element)
74
- end
77
+ it 'parses a String' do
78
+ obj = MXSpecObject.parse_xml(@xml_string)
79
+ expect(obj).to eq(@expected_element)
80
+ end
75
81
 
76
- it 'parses a REXML::Document' do
77
- obj = MXSpecObject.parse_xml(@xml_document)
78
- expect(obj).to eq(@expected_element)
79
- end
82
+ it 'parses a REXML::Document' do
83
+ obj = MXSpecObject.parse_xml(@xml_document)
84
+ expect(obj).to eq(@expected_element)
85
+ end
80
86
 
81
- it 'parses a REXML::Element' do
82
- obj = MXSpecObject.parse_xml(@xml_element)
83
- expect(obj).to eq(@expected_element)
84
- end
87
+ it 'parses a REXML::Element' do
88
+ obj = MXSpecObject.parse_xml(@xml_element)
89
+ expect(obj).to eq(@expected_element)
90
+ end
91
+
92
+ it 'parses an IO' do
93
+ xml_io = StringIO.new(@xml_string)
94
+ obj = MXSpecObject.parse_xml(xml_io)
95
+ expect(obj).to eq(@expected_element)
96
+ end
85
97
 
86
- it 'parses an IO' do
87
- xml_io = StringIO.new(@xml_string)
88
- obj = MXSpecObject.parse_xml(xml_io)
98
+ it 'parses a file' do
99
+ xml_file = Tempfile.new(%w(parse_xml_spec.xml))
100
+ begin
101
+ xml_file.write(@xml_string)
102
+ xml_file.rewind
103
+ obj = MXSpecObject.parse_xml(xml_file)
89
104
  expect(obj).to eq(@expected_element)
105
+ ensure
106
+ xml_file.close(true)
90
107
  end
108
+ end
91
109
 
92
- it 'parses a file' do
93
- xml_file = Tempfile.new(%w(parse_xml_spec .xml))
94
- begin
95
- xml_file.write(@xml_string)
96
- xml_file.rewind
97
- obj = MXSpecObject.parse_xml(xml_file)
98
- expect(obj).to eq(@expected_element)
99
- ensure
100
- xml_file.close(true)
101
- end
102
- end
110
+ it 'accepts an alternate mapping' do
111
+ @xml_string = '<element attribute="123">
112
+ element text
113
+ <children>child 1,child 2</children>
114
+ </element>'
115
+ @xml_document = REXML::Document.new(@xml_string)
116
+ @xml_element = @xml_document.root
103
117
 
104
- it 'accepts an alternate mapping' do
105
- @expected_element = MXSpecObject.load_from_xml(@xml_element, mapping: :alternate)
106
- obj = MXSpecObject.parse_xml(@xml_element, mapping: :alternate)
107
- expect(obj).to eq(@expected_element)
108
- end
118
+ @expected_element = MXSpecObject.load_from_xml(@xml_element, mapping: :alternate)
119
+ obj = MXSpecObject.parse_xml(@xml_element, mapping: :alternate)
120
+ expect(obj).to eq(@expected_element)
109
121
  end
110
122
  end
111
123
  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.4.3
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Moles
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-11 00:00:00.000000000 Z
11
+ date: 2016-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mime-types
@@ -202,6 +202,7 @@ files:
202
202
  - spec/spec_helper.rb
203
203
  - spec/unit/xml/mapping_extensions/array_node_spec.rb
204
204
  - spec/unit/xml/mapping_extensions/date_node_spec.rb
205
+ - spec/unit/xml/mapping_extensions/fallback_mapping_spec.rb
205
206
  - spec/unit/xml/mapping_extensions/mapping_extensions_spec.rb
206
207
  - spec/unit/xml/mapping_extensions/mime_type_node_spec.rb
207
208
  - spec/unit/xml/mapping_extensions/namespace_spec.rb
@@ -241,6 +242,7 @@ test_files:
241
242
  - spec/spec_helper.rb
242
243
  - spec/unit/xml/mapping_extensions/array_node_spec.rb
243
244
  - spec/unit/xml/mapping_extensions/date_node_spec.rb
245
+ - spec/unit/xml/mapping_extensions/fallback_mapping_spec.rb
244
246
  - spec/unit/xml/mapping_extensions/mapping_extensions_spec.rb
245
247
  - spec/unit/xml/mapping_extensions/mime_type_node_spec.rb
246
248
  - spec/unit/xml/mapping_extensions/namespace_spec.rb