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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eecda6c9e7f04c691c79c526d0fefbf4b2b46c9e
4
- data.tar.gz: bd9701c0b3bad3324f7218d94dc47b0ac92105f4
3
+ metadata.gz: 01d484e73f2c9225912fdd49addedaa71ff18f4a
4
+ data.tar.gz: 274e89894d6e53f0abb8fa269e4878c2f4b3f771
5
5
  SHA512:
6
- metadata.gz: 54a736eda4f3af41abfc0161a2ede5e8be5a28da6e081c288648b3f46083052a31d8aac1ce093f6404f469cf520176e5c33c1c325b1eef011ee19a1c76e96017
7
- data.tar.gz: bae38c19dbf4c2ab34469f4cd2181d0699c20728d939a6f860ca5f1e9bdc84442fe48b1e86324f02c9c53fa6eab72111415b873eda278acc081380a57d140818
6
+ metadata.gz: 5b44469677e336a95c4f8abb685f535079f1f0deb57aec5d5fd7c77fce2142a8c19538986e1cdfff6107663d40d5ad2b41168c2b82278d92a760218fbbdf54b2
7
+ data.tar.gz: 38cad28864fd8dbc93c3f208ba15a428c4fbff82e0dccaead78a0b5b0a3c5f407dd48bfa9dc85551a94bb3c1e31f6be5900ee8d1c21f256c453111dd5c186985
data/CHANGELOG.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  * Allow child elements to remove their parent's namespacing (dcarneiro)
8
8
  * has_many elements were returning nil because the tag name was being ignored (haarts)
9
- * Subclassed happymapper classes are allowed to override elements (benoist)
9
+ * Subclassed xmlmapper classes are allowed to override elements (benoist)
10
10
  * Attributes on elements with dashes will properly created methods (alex-klepa)
11
11
  * 'Embedded' attributes break parsing when parent element is not present (geoffwa)
12
12
 
@@ -26,10 +26,10 @@
26
26
  within the xml. This implementation addresses issues with calling #to_xml
27
27
  with content that was parsed from an xpath. (zrob)
28
28
 
29
- * Parent HappyMapper classes may dictate the name of the tag for the child
30
- HappyMapper instances. (zrob)
29
+ * Parent XmlMapper classes may dictate the name of the tag for the child
30
+ XmlMapper instances. (zrob)
31
31
 
32
32
  ## 0.5.3/ 2012-09-23
33
33
 
34
34
  * String is the default type for parsed fields. (crv)
35
- * Update the attributes of an existing HappyMapper instance with new XML (benoist)
35
+ * Update the attributes of an existing XmlMapper instance with new XML (benoist)
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
- HappyMapper
1
+ Xmlmapper
2
2
  ===========
3
3
 
4
- Happymapper allows you to parse XML data and convert it quickly and easily into ruby data structures.
4
+ Xmlmapper allows you to parse XML data and convert it quickly and easily into ruby data structures.
5
5
 
6
6
  This project is a fork of the great work done first by
7
7
  [jnunemaker](https://github.com/jnunemaker/happymapper).
@@ -11,20 +11,20 @@ This project is a fork of the great work done first by
11
11
  * [Nokogiri](http://nokogiri.org/) support
12
12
  * Text nodes parsing
13
13
  * Raw XML content parsing
14
- * `#to_xml` support utilizing the same HappyMapper tags
14
+ * `#to_xml` support utilizing the same XmlMapper tags
15
15
  * Numerous fixes for namespaces when using composition of classes
16
16
  * Fixes for instances of XML where a namespace is defined but no elements with that namespace are found
17
17
 
18
18
  ## Installation
19
19
 
20
- ### [Rubygems](https://rubyygems.org/gems/nokogiri-happymapper)
20
+ ### [Rubygems](https://rubyygems.org/gems/xmlmapper)
21
21
 
22
- $ gem install nokogiri-happymapper
22
+ $ gem install xmlmapper
23
23
 
24
24
  ### [Bundler](http://gembundler.com/)
25
- Add the `nokogiri-happymapper` gem to your project's `Gemfile`.
25
+ Add the `xmlmapper` gem to your project's `Gemfile`.
26
26
 
27
- gem 'nokogiri-happymapper', :require => 'happymapper'
27
+ gem 'xmlmapper'
28
28
 
29
29
  Run the bundler command to install the gem:
30
30
 
@@ -42,14 +42,14 @@ Let's start with a simple example to get our feet wet. Here we have a simple exa
42
42
  <country code="de">Germany</country>
43
43
  </address>
44
44
 
45
- Happymapper provides support for simple, zero configuration parsing as well as the ability to model the XML content in classes.
45
+ XmlMapper provides support for simple, zero configuration parsing as well as the ability to model the XML content in classes.
46
46
 
47
- ## HappyMapper.parse(XML)
47
+ ## XmlMapper.parse(XML)
48
48
 
49
49
  With no classes or configuration you can parse the example XML with little effort:
50
50
 
51
51
  ```ruby
52
- address = HappyMapper.parse(ADDRESS_XML_DATA)
52
+ address = XmlMapper.parse(ADDRESS_XML_DATA)
53
53
  address.street # => Milchstrasse
54
54
  address.housenumber # => 23
55
55
  address.postcode # => 26131
@@ -66,13 +66,13 @@ It is important to be aware that this no configuration parsing is limited in cap
66
66
 
67
67
  ## Address.parse(XML)
68
68
 
69
- Happymapper will let you easily model this information as a class:
69
+ XmlMapper will let you easily model this information as a class:
70
70
 
71
71
  ```ruby
72
- require 'happymapper'
72
+ require 'xmlmapper'
73
73
 
74
74
  class Address
75
- include HappyMapper
75
+ include XmlMapper
76
76
 
77
77
  tag 'address'
78
78
  element :street, String, :tag => 'street'
@@ -83,7 +83,7 @@ class Address
83
83
  end
84
84
  ```
85
85
 
86
- To make a class HappyMapper compatible you simply `include HappyMapper` within the class definition. This takes care of all the work of defining all the speciality methods and magic you need to get running. As you can see we immediately start using these methods.
86
+ To make a class XmlMapper compatible you simply `include XmlMapper` within the class definition. This takes care of all the work of defining all the speciality methods and magic you need to get running. As you can see we immediately start using these methods.
87
87
 
88
88
  * `tag` matches the name of the XML tag name 'address'.
89
89
 
@@ -119,7 +119,7 @@ puts address.street
119
119
 
120
120
  Assuming that the constant `ADDRESS_XML_DATA` contains a string representation of the address XML data this is fairly straight-forward save for the `parse` method.
121
121
 
122
- The `parse` method, like `tag` and `element` are all added when you included HappyMapper in the class. Parse is a wonderful, magical place that converts all these declarations that you have made into the data structure you are about to know and love.
122
+ The `parse` method, like `tag` and `element` are all added when you included XmlMapper in the class. Parse is a wonderful, magical place that converts all these declarations that you have made into the data structure you are about to know and love.
123
123
 
124
124
  But what about the `:single => true`? Right, that is because by default when your object is all done parsing it will be an array. In this case an array with one element, but an array none the less. So the following are equivalent to each other:
125
125
 
@@ -159,10 +159,10 @@ puts address.streets.join('\n')
159
159
  Imagine that you have to write `streets.join('\n')` for the rest of eternity throughout your code. It would be a nightmare and one that you could avoid by creating your own convenience method.
160
160
 
161
161
  ```ruby
162
- require 'happymapper'
162
+ require 'xmlmapper'
163
163
 
164
164
  class Address
165
- include HappyMapper
165
+ include XmlMapper
166
166
 
167
167
  tag 'address'
168
168
 
@@ -221,7 +221,7 @@ class Feed
221
221
  end
222
222
 
223
223
  class Link
224
- include HappyMapper
224
+ include XmlMapper
225
225
 
226
226
  attribute :rel, String
227
227
  attribute :type, String
@@ -257,7 +257,7 @@ Well if we only going to parse country, on it's own, we would likely create a cl
257
257
 
258
258
  ```ruby
259
259
  class Country
260
- include HappyMapper
260
+ include XmlMapper
261
261
 
262
262
  tag 'country'
263
263
 
@@ -274,7 +274,7 @@ Awesome, now if we were to redeclare our `Address` class we would use our new `C
274
274
 
275
275
  ```ruby
276
276
  class Address
277
- include HappyMapper
277
+ include XmlMapper
278
278
 
279
279
  tag 'address'
280
280
 
@@ -291,7 +291,7 @@ class Address
291
291
  end
292
292
  ```
293
293
 
294
- Instead of `String`, `Boolean`, or `Integer` we say that it is a `Country` and HappyMapper takes care of the details of continuing the XML mapping through the country element.
294
+ Instead of `String`, `Boolean`, or `Integer` we say that it is a `Country` and XmlMapper takes care of the details of continuing the XML mapping through the country element.
295
295
 
296
296
  ```ruby
297
297
  address = Address.parse(ADDRESS_XML_DATA, :single => true)
@@ -324,7 +324,7 @@ You may want to map the sub-elements contained buried in the 'gallery' as top le
324
324
 
325
325
  ```ruby
326
326
  class Media
327
- include HappyMapper
327
+ include XmlMapper
328
328
 
329
329
  has_one :title, String, :xpath => 'gallery/title'
330
330
  has_one :link, String, :xpath => 'gallery/title/@href'
@@ -339,7 +339,7 @@ While mapping XML to objects you may arrive at a point where you have two or mor
339
339
 
340
340
  ```ruby
341
341
  class Article
342
- include HappyMapper
342
+ include XmlMapper
343
343
 
344
344
  has_one :title, String
345
345
  has_one :author, String
@@ -350,7 +350,7 @@ class Article
350
350
  end
351
351
 
352
352
  class Gallery
353
- include HappyMapper
353
+ include XmlMapper
354
354
 
355
355
  has_one :title, String
356
356
  has_one :author, String
@@ -365,7 +365,7 @@ In this example there are definitely two similarities between our two pieces of
365
365
 
366
366
  ```ruby
367
367
  class Content
368
- include HappyMapper
368
+ include XmlMapper
369
369
 
370
370
  has_one :title, String
371
371
  has_one :author, String
@@ -373,13 +373,13 @@ class Content
373
373
  end
374
374
 
375
375
  class Article < Content
376
- include HappyMapper
376
+ include XmlMapper
377
377
 
378
378
  has_one :entry, String
379
379
  end
380
380
 
381
381
  class Gallery < Content
382
- include HappyMapper
382
+ include XmlMapper
383
383
 
384
384
  has_many :photos, String
385
385
  end
@@ -403,21 +403,21 @@ module Content
403
403
  end
404
404
 
405
405
  class Article
406
- include HappyMapper
406
+ include XmlMapper
407
407
 
408
408
  include Content
409
409
  has_one :entry, String
410
410
  end
411
411
 
412
412
  class Gallery
413
- include HappyMapper
413
+ include XmlMapper
414
414
 
415
415
  include Content
416
416
  has_many :photos, String
417
417
  end
418
418
  ```
419
419
 
420
- Here, when we include `Content` in both of these classes the module method `#included` is called and our class is given as a parameter. So we take that opportunity to do some surgery and define our happymapper elements as well as any other methods that may rely on those instance variables that come along in the package.
420
+ Here, when we include `Content` in both of these classes the module method `#included` is called and our class is given as a parameter. So we take that opportunity to do some surgery and define our xmlmapper elements as well as any other methods that may rely on those instance variables that come along in the package.
421
421
 
422
422
 
423
423
  ## Filtering with XPATH (non-greedy)
@@ -440,10 +440,10 @@ I ran into a case where I wanted to capture all the pictures that were directly
440
440
  The following `Media` class is where I started:
441
441
 
442
442
  ```ruby
443
- require 'happymapper'
443
+ require 'xmlmapper'
444
444
 
445
445
  class Media
446
- include HappyMapper
446
+ include XmlMapper
447
447
 
448
448
  has_many :galleries, Gallery, :tag => 'gallery'
449
449
  has_many :pictures, Picture, :tag => 'picture'
@@ -492,7 +492,7 @@ Here again is our address example with a made up namespace called `prefix` that
492
492
 
493
493
  ```ruby
494
494
  class Address
495
- include HappyMapper
495
+ include XmlMapper
496
496
 
497
497
  tag 'address'
498
498
  namespace 'prefix'
@@ -587,7 +587,7 @@ Parsing the XML to objects only required you to simply specify the prefix of the
587
587
 
588
588
  ```ruby
589
589
  class Address
590
- include HappyMapper
590
+ include XmlMapper
591
591
 
592
592
  register_namespace 'prefix', 'http://www.unicornland.com/prefix'
593
593
  register_namespace 'different', 'http://www.trollcountry.com/different'
@@ -602,4 +602,4 @@ class Address
602
602
  element :country, Country, :tag => 'country', :namespace => 'different'
603
603
 
604
604
  end
605
- ```
605
+ ```
data/lib/xmlmapper.rb CHANGED
@@ -1 +1,776 @@
1
- require 'happymapper'
1
+ require 'nokogiri'
2
+ require 'date'
3
+ require 'time'
4
+ require 'xmlmapper/anonymous_mapper'
5
+
6
+ module XmlMapper
7
+ class Boolean; end
8
+ class XmlContent; end
9
+
10
+ extend AnonymousMapper
11
+
12
+ DEFAULT_NS = "xmlmapper"
13
+
14
+ def self.included(base)
15
+ if !(base.superclass <= XmlMapper)
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 XmlMapper 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 XmlMapper
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 'xmlmapper'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 XmlMapper 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 XmlMapper 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(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
440
+ end
441
+
442
+ # If the XmlMapper 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(save_with: Nokogiri::XML::Node::SaveOptions::AS_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
+ # XmlMapper elements and attributes. The method is defined in a way
498
+ # that it can be called recursively by classes that are also XmlMapper
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 XmlMapper element.
513
+ #
514
+ # @return [String,Nokogiri::XML::Builder] return XML representation of the
515
+ # XmlMapper 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?(XmlMapper)
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 XmlMappers that are children of this object. New instances will be
750
+ # created for any XmlMapper 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 XmlMappers
760
+ class AnonymousWrapperClassFactory
761
+ def self.get(name, &blk)
762
+ Class.new do
763
+ include XmlMapper
764
+ tag name
765
+ instance_eval &blk
766
+ end
767
+ end
768
+ end
769
+
770
+ end
771
+
772
+ require 'xmlmapper/supported_types'
773
+ require 'xmlmapper/item'
774
+ require 'xmlmapper/attribute'
775
+ require 'xmlmapper/element'
776
+ require 'xmlmapper/text_node'