xml-mapping_extensions 0.4.3 → 0.4.4

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: 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