xmlmapper 0.5.9 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -4
  3. data/README.md +35 -35
  4. data/lib/xmlmapper.rb +776 -1
  5. data/lib/{happymapper → xmlmapper}/anonymous_mapper.rb +23 -23
  6. data/lib/{happymapper → xmlmapper}/attribute.rb +1 -1
  7. data/lib/{happymapper → xmlmapper}/element.rb +1 -1
  8. data/lib/{happymapper → xmlmapper}/item.rb +1 -1
  9. data/lib/{happymapper → xmlmapper}/supported_types.rb +2 -2
  10. data/lib/{happymapper → xmlmapper}/text_node.rb +1 -1
  11. data/lib/xmlmapper/version.rb +3 -0
  12. data/spec/attribute_default_value_spec.rb +1 -1
  13. data/spec/attributes_spec.rb +2 -2
  14. data/spec/fixtures/unformatted_address.xml +1 -0
  15. data/spec/has_many_empty_array_spec.rb +2 -2
  16. data/spec/ignay_spec.rb +5 -5
  17. data/spec/inheritance_spec.rb +6 -6
  18. data/spec/mixed_namespaces_spec.rb +2 -2
  19. data/spec/parse_with_object_to_update_spec.rb +4 -4
  20. data/spec/spec_helper.rb +1 -1
  21. data/spec/to_xml_spec.rb +5 -5
  22. data/spec/to_xml_with_namespaces_spec.rb +6 -6
  23. data/spec/wilcard_tag_name_spec.rb +8 -8
  24. data/spec/wrap_spec.rb +5 -5
  25. data/spec/{happymapper → xmlmapper}/attribute_spec.rb +1 -1
  26. data/spec/{happymapper → xmlmapper}/element_spec.rb +2 -2
  27. data/spec/{happymapper → xmlmapper}/item_spec.rb +16 -16
  28. data/spec/xmlmapper/text_node_spec.rb +9 -0
  29. data/spec/{happymapper_parse_spec.rb → xmlmapper_parse_spec.rb} +3 -3
  30. data/spec/{happymapper_spec.rb → xmlmapper_spec.rb} +87 -66
  31. data/spec/xpath_spec.rb +5 -5
  32. metadata +22 -21
  33. data/lib/happymapper.rb +0 -776
  34. data/lib/happymapper/version.rb +0 -3
  35. data/spec/happymapper/text_node_spec.rb +0 -9
data/spec/xpath_spec.rb CHANGED
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe "Specifying elements and attributes with an xpath" do
4
4
 
5
5
  class Item
6
- include HappyMapper
6
+ include XmlMapper
7
7
 
8
8
  tag 'item'
9
9
  namespace 'amazing'
@@ -21,7 +21,7 @@ describe "Specifying elements and attributes with an xpath" do
21
21
  end
22
22
 
23
23
  class Baby
24
- include HappyMapper
24
+ include XmlMapper
25
25
 
26
26
  has_one :name, String
27
27
  end
@@ -65,19 +65,19 @@ describe "Specifying elements and attributes with an xpath" do
65
65
  end
66
66
 
67
67
  it "should find the subitems based on the xpath" do
68
- expect(subject.more_details_text).to have(2).items
68
+ expect(subject.more_details_text.size).to eq 2
69
69
  expect(subject.more_details_text.first).to eq "more 1"
70
70
  expect(subject.more_details_text.last).to eq "more 2"
71
71
  end
72
72
 
73
73
  it "should find the subitems based on the xpath" do
74
- expect(subject.more_details).to have(2).items
74
+ expect(subject.more_details.size).to eq 2
75
75
  expect(subject.more_details.first).to eq "this one"
76
76
  expect(subject.more_details.last).to eq "another one"
77
77
  end
78
78
 
79
79
  it "should find the subitems based on the xpath" do
80
- expect(subject.more_details_alternative).to have(2).items
80
+ expect(subject.more_details_alternative.size).to eq 2
81
81
  expect(subject.more_details_alternative.first).to eq "this one"
82
82
  expect(subject.more_details_alternative.last).to eq "another one"
83
83
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xmlmapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.9
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Le Berrigaud
@@ -54,15 +54,14 @@ extra_rdoc_files:
54
54
  files:
55
55
  - CHANGELOG.md
56
56
  - README.md
57
- - lib/happymapper.rb
58
- - lib/happymapper/anonymous_mapper.rb
59
- - lib/happymapper/attribute.rb
60
- - lib/happymapper/element.rb
61
- - lib/happymapper/item.rb
62
- - lib/happymapper/supported_types.rb
63
- - lib/happymapper/text_node.rb
64
- - lib/happymapper/version.rb
65
57
  - lib/xmlmapper.rb
58
+ - lib/xmlmapper/anonymous_mapper.rb
59
+ - lib/xmlmapper/attribute.rb
60
+ - lib/xmlmapper/element.rb
61
+ - lib/xmlmapper/item.rb
62
+ - lib/xmlmapper/supported_types.rb
63
+ - lib/xmlmapper/text_node.rb
64
+ - lib/xmlmapper/version.rb
66
65
  - spec/attribute_default_value_spec.rb
67
66
  - spec/attributes_spec.rb
68
67
  - spec/fixtures/address.xml
@@ -91,13 +90,8 @@ files:
91
90
  - spec/fixtures/set_config_options.xml
92
91
  - spec/fixtures/statuses.xml
93
92
  - spec/fixtures/subclass_namespace.xml
93
+ - spec/fixtures/unformatted_address.xml
94
94
  - spec/fixtures/wrapper.xml
95
- - spec/happymapper/attribute_spec.rb
96
- - spec/happymapper/element_spec.rb
97
- - spec/happymapper/item_spec.rb
98
- - spec/happymapper/text_node_spec.rb
99
- - spec/happymapper_parse_spec.rb
100
- - spec/happymapper_spec.rb
101
95
  - spec/has_many_empty_array_spec.rb
102
96
  - spec/ignay_spec.rb
103
97
  - spec/inheritance_spec.rb
@@ -108,6 +102,12 @@ files:
108
102
  - spec/to_xml_with_namespaces_spec.rb
109
103
  - spec/wilcard_tag_name_spec.rb
110
104
  - spec/wrap_spec.rb
105
+ - spec/xmlmapper/attribute_spec.rb
106
+ - spec/xmlmapper/element_spec.rb
107
+ - spec/xmlmapper/item_spec.rb
108
+ - spec/xmlmapper/text_node_spec.rb
109
+ - spec/xmlmapper_parse_spec.rb
110
+ - spec/xmlmapper_spec.rb
111
111
  - spec/xpath_spec.rb
112
112
  homepage: http://github.com/digidentity/xmlmapper
113
113
  licenses:
@@ -162,13 +162,8 @@ test_files:
162
162
  - spec/fixtures/set_config_options.xml
163
163
  - spec/fixtures/statuses.xml
164
164
  - spec/fixtures/subclass_namespace.xml
165
+ - spec/fixtures/unformatted_address.xml
165
166
  - spec/fixtures/wrapper.xml
166
- - spec/happymapper/attribute_spec.rb
167
- - spec/happymapper/element_spec.rb
168
- - spec/happymapper/item_spec.rb
169
- - spec/happymapper/text_node_spec.rb
170
- - spec/happymapper_parse_spec.rb
171
- - spec/happymapper_spec.rb
172
167
  - spec/has_many_empty_array_spec.rb
173
168
  - spec/ignay_spec.rb
174
169
  - spec/inheritance_spec.rb
@@ -179,4 +174,10 @@ test_files:
179
174
  - spec/to_xml_with_namespaces_spec.rb
180
175
  - spec/wilcard_tag_name_spec.rb
181
176
  - spec/wrap_spec.rb
177
+ - spec/xmlmapper/attribute_spec.rb
178
+ - spec/xmlmapper/element_spec.rb
179
+ - spec/xmlmapper/item_spec.rb
180
+ - spec/xmlmapper/text_node_spec.rb
181
+ - spec/xmlmapper_parse_spec.rb
182
+ - spec/xmlmapper_spec.rb
182
183
  - spec/xpath_spec.rb
data/lib/happymapper.rb DELETED
@@ -1,776 +0,0 @@
1
- require 'nokogiri'
2
- require 'date'
3
- require 'time'
4
- require 'happymapper/anonymous_mapper'
5
-
6
- module HappyMapper
7
- class Boolean; end
8
- class XmlContent; end
9
-
10
- extend AnonymousMapper
11
-
12
- DEFAULT_NS = "happymapper"
13
-
14
- def self.included(base)
15
- if !(base.superclass <= HappyMapper)
16
- base.instance_eval do
17
- @attributes = {}
18
- @elements = {}
19
- @registered_namespaces = {}
20
- @wrapper_anonymous_classes = {}
21
- end
22
- else
23
- base.instance_eval do
24
- @attributes =
25
- superclass.instance_variable_get(:@attributes).dup
26
- @elements =
27
- superclass.instance_variable_get(:@elements).dup
28
- @registered_namespaces =
29
- superclass.instance_variable_get(:@registered_namespaces).dup
30
- @wrapper_anonymous_classes =
31
- superclass.instance_variable_get(:@wrapper_anonymous_classes).dup
32
- end
33
- end
34
-
35
- base.extend ClassMethods
36
- end
37
-
38
- module ClassMethods
39
-
40
- #
41
- # The xml has the following attributes defined.
42
- #
43
- # @example
44
- #
45
- # "<country code='de'>Germany</country>"
46
- #
47
- # # definition of the 'code' attribute within the class
48
- # attribute :code, String
49
- #
50
- # @param [Symbol] name the name of the accessor that is created
51
- # @param [String,Class] type the class name of the name of the class whcih
52
- # the object will be converted upon parsing
53
- # @param [Hash] options additional parameters to send to the relationship
54
- #
55
- def attribute(name, type, options={})
56
- attribute = Attribute.new(name, type, options)
57
- @attributes[name] = attribute
58
- attr_accessor attribute.method_name.intern
59
- end
60
-
61
- #
62
- # The elements defined through {#attribute}.
63
- #
64
- # @return [Array<Attribute>] a list of the attributes defined for this class;
65
- # an empty array is returned when there have been no attributes defined.
66
- #
67
- def attributes
68
- @attributes.values
69
- end
70
-
71
- #
72
- # Register a namespace that is used to persist the object namespace back to
73
- # XML.
74
- #
75
- # @example
76
- #
77
- # register_namespace 'prefix', 'http://www.unicornland.com/prefix'
78
- #
79
- # # the output will contain the namespace defined
80
- #
81
- # "<outputXML xmlns:prefix="http://www.unicornland.com/prefix">
82
- # ...
83
- # </outputXML>"
84
- #
85
- # @param [String] namespace the xml prefix
86
- # @param [String] ns url for the xml namespace
87
- #
88
- def register_namespace(namespace, ns)
89
- @registered_namespaces.merge!({namespace => ns})
90
- end
91
-
92
- #
93
- # An element defined in the XML that is parsed.
94
- #
95
- # @example
96
- #
97
- # "<address location='home'>
98
- # <city>Oldenburg</city>
99
- # </address>"
100
- #
101
- # # definition of the 'city' element within the class
102
- #
103
- # element :city, String
104
- #
105
- # @param [Symbol] name the name of the accessor that is created
106
- # @param [String,Class] type the class name of the name of the class whcih
107
- # the object will be converted upon parsing
108
- # @param [Hash] options additional parameters to send to the relationship
109
- #
110
- def element(name, type, options={})
111
- element = Element.new(name, type, options)
112
- @elements[name] = element
113
- attr_accessor element.method_name.intern
114
- end
115
-
116
- #
117
- # The elements defined through {#element}, {#has_one}, and {#has_many}.
118
- #
119
- # @return [Array<Element>] a list of the elements contained defined for this
120
- # class; an empty array is returned when there have been no elements
121
- # defined.
122
- #
123
- def elements
124
- @elements.values
125
- end
126
-
127
- #
128
- # The value stored in the text node of the current element.
129
- #
130
- # @example
131
- #
132
- # "<firstName>Michael Jackson</firstName>"
133
- #
134
- # # definition of the 'firstName' text node within the class
135
- #
136
- # content :first_name, String
137
- #
138
- # @param [Symbol] name the name of the accessor that is created
139
- # @param [String,Class] type the class name of the name of the class whcih
140
- # the object will be converted upon parsing. By Default String class will be taken.
141
- # @param [Hash] options additional parameters to send to the relationship
142
- #
143
- def content(name, type=String, options={})
144
- @content = TextNode.new(name, type, options)
145
- attr_accessor @content.method_name.intern
146
- end
147
-
148
- #
149
- # Sets the object to have xml content, this will assign the XML contents
150
- # that are parsed to the attribute accessor xml_content. The object will
151
- # respond to the method #xml_content and will return the XML data that
152
- # it has parsed.
153
- #
154
- def has_xml_content
155
- attr_accessor :xml_content
156
- end
157
-
158
- #
159
- # The object has one of these elements in the XML. If there are multiple,
160
- # the last one will be set to this value.
161
- #
162
- # @param [Symbol] name the name of the accessor that is created
163
- # @param [String,Class] type the class name of the name of the class whcih
164
- # the object will be converted upon parsing
165
- # @param [Hash] options additional parameters to send to the relationship
166
- #
167
- # @see #element
168
- #
169
- def has_one(name, type, options={})
170
- element name, type, {:single => true}.merge(options)
171
- end
172
-
173
- #
174
- # The object has many of these elements in the XML.
175
- #
176
- # @param [Symbol] name the name of accessor that is created
177
- # @param [String,Class] type the class name or the name of the class which
178
- # the object will be converted upon parsing.
179
- # @param [Hash] options additional parameters to send to the relationship
180
- #
181
- # @see #element
182
- #
183
- def has_many(name, type, options={})
184
- element name, type, {:single => false}.merge(options)
185
- end
186
-
187
- #
188
- # The list of registered after_parse callbacks.
189
- #
190
- def after_parse_callbacks
191
- @after_parse_callbacks ||= []
192
- end
193
-
194
- #
195
- # Register a new after_parse callback, given as a block.
196
- #
197
- # @yield [object] Yields the newly-parsed object to the block after parsing.
198
- # Sub-objects will be already populated.
199
- def after_parse(&block)
200
- after_parse_callbacks.push(block)
201
- end
202
-
203
- #
204
- # Specify a namespace if a node and all its children are all namespaced
205
- # elements. This is simpler than passing the :namespace option to each
206
- # defined element.
207
- #
208
- # @param [String] namespace the namespace to set as default for the class
209
- # element.
210
- #
211
- def namespace(namespace = nil)
212
- @namespace = namespace if namespace
213
- @namespace
214
- end
215
-
216
- #
217
- # @param [String] new_tag_name the name for the tag
218
- #
219
- def tag(new_tag_name)
220
- @tag_name = new_tag_name.to_s unless new_tag_name.nil? || new_tag_name.to_s.empty?
221
- end
222
-
223
- #
224
- # The name of the tag
225
- #
226
- # @return [String] the name of the tag as a string, downcased
227
- #
228
- def tag_name
229
- @tag_name ||= to_s.split('::')[-1].downcase
230
- end
231
-
232
- # There is an XML tag that needs to be known for parsing and should be generated
233
- # during a to_xml. But it doesn't need to be a class and the contained elements should
234
- # be made available on the parent class
235
- #
236
- # @param [String] name the name of the element that is just a place holder
237
- # @param [Proc] blk the element definitions inside the place holder tag
238
- #
239
- def wrap(name, &blk)
240
- # Get an anonymous HappyMapper that has 'name' as its tag and defined
241
- # in '&blk'. Then save that to a class instance variable for later use
242
- wrapper = AnonymousWrapperClassFactory.get(name, &blk)
243
- @wrapper_anonymous_classes[wrapper.inspect] = wrapper
244
-
245
- # Create getter/setter for each element and attribute defined on the anonymous HappyMapper
246
- # onto this class. They get/set the value by passing thru to the anonymous class.
247
- passthrus = wrapper.attributes + wrapper.elements
248
- passthrus.each do |item|
249
- class_eval %{
250
- def #{item.method_name}
251
- @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new
252
- @#{name}.#{item.method_name}
253
- end
254
- def #{item.method_name}=(value)
255
- @#{name} ||= self.class.instance_variable_get('@wrapper_anonymous_classes')['#{wrapper.inspect}'].new
256
- @#{name}.#{item.method_name} = value
257
- end
258
- }
259
- end
260
-
261
- has_one name, wrapper
262
- end
263
-
264
- # The callback defined through {.with_nokogiri_config}.
265
- #
266
- # @return [Proc] the proc to pass to Nokogiri to setup parse options. nil if empty.
267
- #
268
- def nokogiri_config_callback
269
- @nokogiri_config_callback
270
- end
271
-
272
- # Register a config callback according to the block Nokogori expects when calling Nokogiri::XML::Document.parse().
273
- # See http://nokogiri.org/Nokogiri/XML/Document.html#method-c-parse
274
- #
275
- # @param [Proc] the proc to pass to Nokogiri to setup parse options
276
- #
277
- def with_nokogiri_config(&blk)
278
- @nokogiri_config_callback = blk
279
- end
280
-
281
- #
282
- # @param [Nokogiri::XML::Node,Nokogiri:XML::Document,String] xml the XML
283
- # contents to convert into Object.
284
- # @param [Hash] options additional information for parsing. :single => true
285
- # if requesting a single object, otherwise it defaults to retuning an
286
- # array of multiple items. :xpath information where to start the parsing
287
- # :namespace is the namespace to use for additional information.
288
- #
289
- def parse(xml, options = {})
290
-
291
- # create a local copy of the objects namespace value for this parse execution
292
- namespace = @namespace
293
-
294
- # If the XML specified is an Node then we have what we need.
295
- if xml.is_a?(Nokogiri::XML::Node) && !xml.is_a?(Nokogiri::XML::Document)
296
- node = xml
297
- else
298
-
299
- # If xml is an XML document select the root node of the document
300
- if xml.is_a?(Nokogiri::XML::Document)
301
- node = xml.root
302
- else
303
-
304
- # Attempt to parse the xml value with Nokogiri XML as a document
305
- # and select the root element
306
- xml = Nokogiri::XML(
307
- xml, nil, nil,
308
- Nokogiri::XML::ParseOptions::STRICT,
309
- &nokogiri_config_callback
310
- )
311
- node = xml.root
312
- end
313
-
314
- # if the node name is equal to the tag name then the we are parsing the
315
- # root element and that is important to record so that we can apply
316
- # the correct xpath on the elements of this document.
317
-
318
- root = node.name == tag_name
319
- end
320
-
321
- # if any namespaces have been provied then we should capture those and then
322
- # merge them with any namespaces found on the xml node and merge all that
323
- # with any namespaces that have been registered on the object
324
-
325
- namespaces = options[:namespaces] || {}
326
- namespaces = namespaces.merge(xml.collect_namespaces) if xml.respond_to?(:collect_namespaces)
327
- namespaces = namespaces.merge(@registered_namespaces)
328
-
329
- # if a namespace has been provided then set the current namespace to it
330
- # or set the default namespace to the one defined under 'xmlns'
331
- # or set the default namespace to the namespace that matches 'happymapper's
332
-
333
- if options[:namespace]
334
- namespace = options[:namespace]
335
- elsif namespaces.has_key?("xmlns")
336
- namespace ||= DEFAULT_NS
337
- namespaces[DEFAULT_NS] = namespaces.delete("xmlns")
338
- elsif namespaces.has_key?(DEFAULT_NS)
339
- namespace ||= DEFAULT_NS
340
- end
341
-
342
- # from the options grab any nodes present and if none are present then
343
- # perform the following to find the nodes for the given class
344
-
345
- nodes = options.fetch(:nodes) do
346
-
347
- # when at the root use the xpath '/' otherwise use a more gready './/'
348
- # unless an xpath has been specified, which should overwrite default
349
- # and finally attach the current namespace if one has been defined
350
- #
351
-
352
- xpath = (root ? '/' : './/')
353
- xpath = options[:xpath].to_s.sub(/([^\/])$/, '\1/') if options[:xpath]
354
- xpath += "#{namespace}:" if namespace
355
-
356
- nodes = []
357
-
358
- # when finding nodes, do it in this order:
359
- # 1. specified tag if one has been provided
360
- # 2. name of element
361
- # 3. tag_name (derived from class name by default)
362
-
363
- # If a tag has been provided we need to search for it.
364
-
365
- if options.key?(:tag)
366
- begin
367
- nodes = node.xpath(xpath + options[:tag].to_s, namespaces)
368
- rescue
369
- # This exception takes place when the namespace is often not found
370
- # and we should continue on with the empty array of nodes.
371
- end
372
- else
373
-
374
- # This is the default case when no tag value is provided.
375
- # First we use the name of the element `items` in `has_many items`
376
- # Second we use the tag name which is the name of the class cleaned up
377
-
378
- [options[:name], tag_name].compact.each do |xpath_ext|
379
- begin
380
- nodes = node.xpath(xpath + xpath_ext.to_s, namespaces)
381
- rescue
382
- break
383
- # This exception takes place when the namespace is often not found
384
- # and we should continue with the empty array of nodes or keep looking
385
- end
386
- break if nodes && !nodes.empty?
387
- end
388
-
389
- end
390
-
391
- nodes
392
- end
393
-
394
- # Nothing matching found, we can go ahead and return
395
- return ( ( options[:single] || root ) ? nil : [] ) if nodes.size == 0
396
-
397
- # If the :limit option has been specified then we are going to slice
398
- # our node results by that amount to allow us the ability to deal with
399
- # a large result set of data.
400
-
401
- limit = options[:in_groups_of] || nodes.size
402
-
403
- # If the limit of 0 has been specified then the user obviously wants
404
- # none of the nodes that we are serving within this batch of nodes.
405
-
406
- return [] if limit == 0
407
-
408
- collection = []
409
-
410
- nodes.each_slice(limit) do |slice|
411
-
412
- part = slice.map do |n|
413
-
414
- # If an existing HappyMapper object is provided, update it with the
415
- # values from the xml being parsed. Otherwise, create a new object
416
-
417
- obj = options[:update] ? options[:update] : new
418
-
419
- attributes.each do |attr|
420
- value = attr.from_xml_node(n, namespace, namespaces)
421
- value = attr.default if value.nil?
422
- obj.send("#{attr.method_name}=", value)
423
- end
424
-
425
- elements.each do |elem|
426
- obj.send("#{elem.method_name}=",elem.from_xml_node(n, namespace, namespaces))
427
- end
428
-
429
- if @content
430
- obj.send("#{@content.method_name}=",@content.from_xml_node(n, namespace, namespaces))
431
- end
432
-
433
- # If the HappyMapper class has the method #xml_value=,
434
- # attr_writer :xml_value, or attr_accessor :xml_value then we want to
435
- # assign the current xml that we just parsed to the xml_value
436
-
437
- if obj.respond_to?('xml_value=')
438
- n.namespaces.each {|name,path| n[name] = path }
439
- obj.xml_value = n.to_xml
440
- end
441
-
442
- # If the HappyMapper class has the method #xml_content=,
443
- # attr_write :xml_content, or attr_accessor :xml_content then we want to
444
- # assign the child xml that we just parsed to the xml_content
445
-
446
- if obj.respond_to?('xml_content=')
447
- n = n.children if n.respond_to?(:children)
448
- obj.xml_content = n.to_xml
449
- end
450
-
451
- # Call any registered after_parse callbacks for the object's class
452
-
453
- obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }
454
-
455
- # collect the object that we have created
456
-
457
- obj
458
- end
459
-
460
- # If a block has been provided and the user has requested that the objects
461
- # be handled in groups then we should yield the slice of the objects to them
462
- # otherwise continue to lump them together
463
-
464
- if block_given? and options[:in_groups_of]
465
- yield part
466
- else
467
- collection += part
468
- end
469
-
470
- end
471
-
472
- # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
473
- nodes = nil
474
-
475
- # If the :single option has been specified or we are at the root element
476
- # then we are going to return the first item in the collection. Otherwise
477
- # the return response is going to be an entire array of items.
478
-
479
- if options[:single] or root
480
- collection.first
481
- else
482
- collection
483
- end
484
- end
485
- end
486
-
487
- # Set all attributes with a default to their default values
488
- def initialize
489
- super
490
- self.class.attributes.reject {|attr| attr.default.nil?}.each do |attr|
491
- send("#{attr.method_name}=", attr.default)
492
- end
493
- end
494
-
495
- #
496
- # Create an xml representation of the specified class based on defined
497
- # HappyMapper elements and attributes. The method is defined in a way
498
- # that it can be called recursively by classes that are also HappyMapper
499
- # classes, allowg for the composition of classes.
500
- #
501
- # @param [Nokogiri::XML::Builder] builder an instance of the XML builder which
502
- # is being used when called recursively.
503
- # @param [String] default_namespace The name of the namespace which is the
504
- # default for the xml being produced; this is the namespace of the
505
- # parent
506
- # @param [String] namespace_override The namespace specified with the element
507
- # declaration in the parent. Overrides the namespace declaration in the
508
- # element class itself when calling #to_xml recursively.
509
- # @param [String] tag_from_parent The xml tag to use on the element when being
510
- # called recursively. This lets the parent doc define its own structure.
511
- # Otherwise the element uses the tag it has defined for itself. Should only
512
- # apply when calling a child HappyMapper element.
513
- #
514
- # @return [String,Nokogiri::XML::Builder] return XML representation of the
515
- # HappyMapper object; when called recursively this is going to return
516
- # and Nokogiri::XML::Builder object.
517
- #
518
- def to_xml(builder = nil, default_namespace = nil, namespace_override = nil,
519
- tag_from_parent = nil)
520
-
521
- #
522
- # If to_xml has been called without a passed in builder instance that
523
- # means we are going to return xml output. When it has been called with
524
- # a builder instance that means we most likely being called recursively
525
- # and will return the end product as a builder instance.
526
- #
527
- unless builder
528
- write_out_to_xml = true
529
- builder = Nokogiri::XML::Builder.new
530
- end
531
-
532
- #
533
- # Find the attributes for the class and collect them into an array
534
- # that will be placed into a Hash structure
535
- #
536
- attributes = self.class.attributes.collect do |attribute|
537
-
538
- #
539
- # If an attribute is marked as read_only then we want to ignore the attribute
540
- # when it comes to saving the xml document; so we wiill not go into any of
541
- # the below process
542
- #
543
- unless attribute.options[:read_only]
544
-
545
- value = send(attribute.method_name)
546
- value = nil if value == attribute.default
547
-
548
- #
549
- # If the attribute defines an on_save lambda/proc or value that maps to
550
- # a method that the class has defined, then call it with the value as a
551
- # parameter.
552
- #
553
- if on_save_action = attribute.options[:on_save]
554
- if on_save_action.is_a?(Proc)
555
- value = on_save_action.call(value)
556
- elsif respond_to?(on_save_action)
557
- value = send(on_save_action,value)
558
- end
559
- end
560
-
561
- #
562
- # Attributes that have a nil value should be ignored unless they explicitly
563
- # state that they should be expressed in the output.
564
- #
565
- if not value.nil? || attribute.options[:state_when_nil]
566
- attribute_namespace = attribute.options[:namespace]
567
- [ "#{attribute_namespace ? "#{attribute_namespace}:" : ""}#{attribute.tag}", value ]
568
- else
569
- []
570
- end
571
-
572
- else
573
- []
574
- end
575
-
576
- end.flatten
577
-
578
- attributes = Hash[ *attributes ]
579
-
580
- #
581
- # Create a tag in the builder that matches the class's tag name unless a tag was passed
582
- # in a recursive call from the parent doc. Then append
583
- # any attributes to the element that were defined above.
584
- #
585
- builder.send("#{tag_from_parent || self.class.tag_name}_",attributes) do |xml|
586
-
587
- #
588
- # Add all the registered namespaces to the root element.
589
- # When this is called recurisvely by composed classes the namespaces
590
- # are still added to the root element
591
- #
592
- # However, we do not want to add the namespace if the namespace is 'xmlns'
593
- # which means that it is the default namesapce of the code.
594
- #
595
- if self.class.instance_variable_get('@registered_namespaces') && builder.doc.root
596
- self.class.instance_variable_get('@registered_namespaces').each_pair do |name,href|
597
- name = nil if name == "xmlns"
598
- builder.doc.root.add_namespace(name,href)
599
- end
600
- end
601
-
602
- #
603
- # If the object we are serializing has a namespace declaration we will want
604
- # to use that namespace or we will use the default namespace.
605
- # When neither are specifed we are simply using whatever is default to the
606
- # builder
607
- #
608
- namespace_for_parent = namespace_override
609
- if self.class.respond_to?(:namespace) && self.class.namespace
610
- namespace_for_parent ||= self.class.namespace
611
- end
612
- namespace_for_parent ||= default_namespace
613
-
614
- xml.parent.namespace =
615
- builder.doc.root.namespace_definitions.find { |x| x.prefix == namespace_for_parent }
616
-
617
-
618
- #
619
- # When a content has been defined we add the resulting value
620
- # the output xml
621
- #
622
- if content = self.class.instance_variable_get('@content')
623
-
624
- unless content.options[:read_only]
625
- text_accessor = content.tag || content.name
626
- value = send(text_accessor)
627
-
628
- if on_save_action = content.options[:on_save]
629
- if on_save_action.is_a?(Proc)
630
- value = on_save_action.call(value)
631
- elsif respond_to?(on_save_action)
632
- value = send(on_save_action,value)
633
- end
634
- end
635
-
636
- builder.text(value)
637
- end
638
-
639
- end
640
-
641
- #
642
- # for every define element (i.e. has_one, has_many, element) we are
643
- # going to persist each one
644
- #
645
- self.class.elements.each do |element|
646
-
647
- #
648
- # If an element is marked as read only do not consider at all when
649
- # saving to XML.
650
- #
651
- unless element.options[:read_only]
652
-
653
- tag = element.tag || element.name
654
-
655
- #
656
- # The value to store is the result of the method call to the element,
657
- # by default this is simply utilizing the attr_accessor defined. However,
658
- # this allows for this method to be overridden
659
- #
660
- value = send(element.name)
661
-
662
- #
663
- # If the element defines an on_save lambda/proc then we will call that
664
- # operation on the specified value. This allows for operations to be
665
- # performed to convert the value to a specific value to be saved to the xml.
666
- #
667
- if on_save_action = element.options[:on_save]
668
- if on_save_action.is_a?(Proc)
669
- value = on_save_action.call(value)
670
- elsif respond_to?(on_save_action)
671
- value = send(on_save_action,value)
672
- end
673
- end
674
-
675
- #
676
- # Normally a nil value would be ignored, however if specified then
677
- # an empty element will be written to the xml
678
- #
679
- if value.nil? && element.options[:single] && element.options[:state_when_nil]
680
- xml.send("#{tag}_","")
681
- end
682
-
683
- #
684
- # To allow for us to treat both groups of items and singular items
685
- # equally we wrap the value and treat it as an array.
686
- #
687
- if value.nil?
688
- values = []
689
- elsif value.respond_to?(:to_ary) && !element.options[:single]
690
- values = value.to_ary
691
- else
692
- values = [value]
693
- end
694
-
695
- values.each do |item|
696
-
697
- if item.is_a?(HappyMapper)
698
-
699
- #
700
- # Other items are convertable to xml through the xml builder
701
- # process should have their contents retrieved and attached
702
- # to the builder structure
703
- #
704
- item.to_xml(xml, self.class.namespace || default_namespace,
705
- element.options[:namespace],
706
- element.options[:tag] || nil)
707
-
708
- elsif !item.nil?
709
-
710
- item_namespace = element.options[:namespace] || self.class.namespace || default_namespace
711
-
712
- #
713
- # When a value exists we should append the value for the tag
714
- #
715
- if item_namespace
716
- xml[item_namespace].send("#{tag}_",item.to_s)
717
- else
718
- xml.send("#{tag}_",item.to_s)
719
- end
720
-
721
- else
722
-
723
- #
724
- # Normally a nil value would be ignored, however if specified then
725
- # an empty element will be written to the xml
726
- #
727
- xml.send("#{tag}_","") if element.options[:state_when_nil]
728
-
729
- end
730
-
731
- end
732
-
733
- end
734
- end
735
-
736
- end
737
-
738
- # Write out to XML, this value was set above, based on whether or not an XML
739
- # builder object was passed to it as a parameter. When there was no parameter
740
- # we assume we are at the root level of the #to_xml call and want the actual
741
- # xml generated from the object. If an XML builder instance was specified
742
- # then we assume that has been called recursively to generate a larger
743
- # XML document.
744
- write_out_to_xml ? builder.to_xml : builder
745
-
746
- end
747
-
748
- # Parse the xml and update this instance. This does not update instances
749
- # of HappyMappers that are children of this object. New instances will be
750
- # created for any HappyMapper children of this object.
751
- #
752
- # Params and return are the same as the class parse() method above.
753
- def parse(xml, options = {})
754
- self.class.parse(xml, options.merge!(:update => self))
755
- end
756
-
757
- private
758
-
759
- # Factory for creating anonmyous HappyMappers
760
- class AnonymousWrapperClassFactory
761
- def self.get(name, &blk)
762
- Class.new do
763
- include HappyMapper
764
- tag name
765
- instance_eval &blk
766
- end
767
- end
768
- end
769
-
770
- end
771
-
772
- require 'happymapper/supported_types'
773
- require 'happymapper/item'
774
- require 'happymapper/attribute'
775
- require 'happymapper/element'
776
- require 'happymapper/text_node'