nokogiri-happymapper 0.6.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +64 -5
- data/README.md +296 -192
- data/lib/happymapper/anonymous_mapper.rb +46 -43
- data/lib/happymapper/attribute.rb +7 -5
- data/lib/happymapper/element.rb +19 -22
- data/lib/happymapper/item.rb +19 -21
- data/lib/happymapper/supported_types.rb +20 -28
- data/lib/happymapper/text_node.rb +4 -3
- data/lib/happymapper/version.rb +3 -1
- data/lib/happymapper.rb +336 -362
- data/lib/nokogiri-happymapper.rb +4 -0
- metadata +124 -105
- data/spec/attribute_default_value_spec.rb +0 -50
- data/spec/attributes_spec.rb +0 -36
- data/spec/fixtures/address.xml +0 -9
- data/spec/fixtures/ambigous_items.xml +0 -22
- data/spec/fixtures/analytics.xml +0 -61
- data/spec/fixtures/analytics_profile.xml +0 -127
- data/spec/fixtures/atom.xml +0 -19
- data/spec/fixtures/commit.xml +0 -52
- data/spec/fixtures/current_weather.xml +0 -89
- data/spec/fixtures/current_weather_missing_elements.xml +0 -18
- data/spec/fixtures/default_namespace_combi.xml +0 -6
- data/spec/fixtures/dictionary.xml +0 -20
- data/spec/fixtures/family_tree.xml +0 -21
- data/spec/fixtures/inagy.xml +0 -85
- data/spec/fixtures/lastfm.xml +0 -355
- data/spec/fixtures/multiple_namespaces.xml +0 -170
- data/spec/fixtures/multiple_primitives.xml +0 -5
- data/spec/fixtures/optional_attributes.xml +0 -6
- data/spec/fixtures/pita.xml +0 -133
- data/spec/fixtures/posts.xml +0 -23
- data/spec/fixtures/product_default_namespace.xml +0 -18
- data/spec/fixtures/product_no_namespace.xml +0 -10
- data/spec/fixtures/product_single_namespace.xml +0 -10
- data/spec/fixtures/quarters.xml +0 -19
- data/spec/fixtures/radar.xml +0 -21
- data/spec/fixtures/set_config_options.xml +0 -3
- data/spec/fixtures/statuses.xml +0 -422
- data/spec/fixtures/subclass_namespace.xml +0 -50
- data/spec/fixtures/unformatted_address.xml +0 -1
- data/spec/fixtures/wrapper.xml +0 -11
- data/spec/happymapper/attribute_spec.rb +0 -12
- data/spec/happymapper/element_spec.rb +0 -9
- data/spec/happymapper/item_spec.rb +0 -136
- data/spec/happymapper/text_node_spec.rb +0 -9
- data/spec/happymapper_parse_spec.rb +0 -113
- data/spec/happymapper_spec.rb +0 -1129
- data/spec/has_many_empty_array_spec.rb +0 -43
- data/spec/ignay_spec.rb +0 -95
- data/spec/inheritance_spec.rb +0 -107
- data/spec/mixed_namespaces_spec.rb +0 -61
- data/spec/parse_with_object_to_update_spec.rb +0 -111
- data/spec/spec_helper.rb +0 -7
- data/spec/to_xml_spec.rb +0 -201
- data/spec/to_xml_with_namespaces_spec.rb +0 -232
- data/spec/wilcard_tag_name_spec.rb +0 -96
- data/spec/wrap_spec.rb +0 -82
- data/spec/xpath_spec.rb +0 -89
data/README.md
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
-
HappyMapper
|
2
|
-
===========
|
1
|
+
# HappyMapper
|
3
2
|
|
4
|
-
Happymapper allows you to parse XML data and convert it quickly and easily into
|
3
|
+
Happymapper allows you to parse XML data and convert it quickly and easily into
|
4
|
+
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).
|
8
8
|
|
9
|
+
[![Gem Version](https://badge.fury.io/rb/nokogiri-happymapper.svg)](https://badge.fury.io/rb/nokogiri-happymapper)
|
10
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/491015f82bd2a45fd9d3/maintainability)](https://codeclimate.com/github/mvz/happymapper/maintainability)
|
11
|
+
|
9
12
|
## Major Differences
|
10
13
|
|
11
14
|
* [Nokogiri](http://nokogiri.org/) support
|
@@ -13,40 +16,49 @@ This project is a fork of the great work done first by
|
|
13
16
|
* Raw XML content parsing
|
14
17
|
* `#to_xml` support utilizing the same HappyMapper tags
|
15
18
|
* Numerous fixes for namespaces when using composition of classes
|
16
|
-
* Fixes for instances of XML where a namespace is defined but no elements
|
19
|
+
* Fixes for instances of XML where a namespace is defined but no elements
|
20
|
+
with that namespace are found
|
17
21
|
|
18
22
|
## Installation
|
19
23
|
|
20
|
-
|
24
|
+
Install via rubygems:
|
21
25
|
|
22
26
|
$ gem install nokogiri-happymapper
|
23
27
|
|
24
|
-
|
25
|
-
|
28
|
+
Or add the `nokogiri-happymapper` gem to your project's `Gemfile`.
|
29
|
+
|
30
|
+
gem 'nokogiri-happymapper', require: 'happymapper'
|
31
|
+
|
32
|
+
You can now also require `nokogiri-happymapper` directly.
|
26
33
|
|
27
|
-
gem 'nokogiri-happymapper'
|
34
|
+
gem 'nokogiri-happymapper'
|
28
35
|
|
29
|
-
Run
|
36
|
+
Run Bundler to install the gem:
|
30
37
|
|
31
38
|
$ bundle install
|
32
39
|
|
33
|
-
|
40
|
+
## Examples
|
34
41
|
|
35
|
-
Let's start with a simple example to get our feet wet. Here we have a simple
|
42
|
+
Let's start with a simple example to get our feet wet. Here we have a simple
|
43
|
+
example of XML that defines some address information:
|
36
44
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
45
|
+
```xml
|
46
|
+
<address>
|
47
|
+
<street>Milchstrasse</street>
|
48
|
+
<housenumber>23</housenumber>
|
49
|
+
<postcode>26131</postcode>
|
50
|
+
<city>Oldenburg</city>
|
51
|
+
<country code="de">Germany</country>
|
52
|
+
</address>
|
53
|
+
```
|
44
54
|
|
45
|
-
Happymapper provides support for simple, zero configuration parsing as well as
|
55
|
+
Happymapper provides support for simple, zero configuration parsing as well as
|
56
|
+
the ability to model the XML content in classes.
|
46
57
|
|
47
|
-
|
58
|
+
### HappyMapper.parse(XML)
|
48
59
|
|
49
|
-
With no classes or configuration you can parse the example XML with little
|
60
|
+
With no classes or configuration you can parse the example XML with little
|
61
|
+
effort:
|
50
62
|
|
51
63
|
```ruby
|
52
64
|
address = HappyMapper.parse(ADDRESS_XML_DATA)
|
@@ -60,11 +72,14 @@ address.country.content # => Germany
|
|
60
72
|
|
61
73
|
It is important to be aware that this no configuration parsing is limited in capacity:
|
62
74
|
|
63
|
-
* All element names are converted to accessor methods with
|
75
|
+
* All element names are converted to accessor methods with
|
76
|
+
[underscorized](http://rubydoc.info/gems/activesupport/ActiveSupport/Inflector:underscore)
|
77
|
+
names
|
64
78
|
* All value fields are left as String types
|
65
|
-
* Determining if there is just one or multiple child elements is hard, so it
|
79
|
+
* Determining if there is just one or multiple child elements is hard, so it
|
80
|
+
assumes it is one until it finds another with the same name.
|
66
81
|
|
67
|
-
|
82
|
+
### Address.parse(XML)
|
68
83
|
|
69
84
|
Happymapper will let you easily model this information as a class:
|
70
85
|
|
@@ -75,88 +90,115 @@ class Address
|
|
75
90
|
include HappyMapper
|
76
91
|
|
77
92
|
tag 'address'
|
78
|
-
element :street, String, :
|
79
|
-
element :postcode, String, :
|
80
|
-
element :housenumber, Integer, :
|
81
|
-
element :city, String, :
|
82
|
-
element :country, String, :
|
93
|
+
element :street, String, tag: 'street'
|
94
|
+
element :postcode, String, tag: 'postcode'
|
95
|
+
element :housenumber, Integer, tag: 'housenumber'
|
96
|
+
element :city, String, tag: 'city'
|
97
|
+
element :country, String, tag: 'country'
|
83
98
|
end
|
84
99
|
```
|
85
100
|
|
86
|
-
To make a class HappyMapper compatible you simply `include HappyMapper` within
|
101
|
+
To make a class HappyMapper compatible you simply `include HappyMapper` within
|
102
|
+
the class definition. This takes care of all the work of defining all the
|
103
|
+
speciality methods and magic you need to get running. As you can see we
|
104
|
+
immediately start using these methods.
|
87
105
|
|
88
106
|
* `tag` matches the name of the XML tag name 'address'.
|
89
107
|
|
90
|
-
* `element` defines accessor methods for the specified symbol
|
108
|
+
* `element` defines accessor methods for the specified symbol
|
109
|
+
(e.g. `:street`,`:housenumber`) that will return the class type
|
110
|
+
(e.g. `String`,`Integer`) of the XML tag specified
|
111
|
+
(e.g. `tag: 'street'`, `tag: 'housenumber'`).
|
91
112
|
|
92
|
-
When you define an element with an accessor with the same name as the tag, this
|
113
|
+
When you define an element with an accessor with the same name as the tag, this
|
114
|
+
is the case for all the examples above, you can omit the `:tag`. These two
|
115
|
+
element declaration are equivalent to each other:
|
93
116
|
|
94
117
|
```ruby
|
95
|
-
element :street, String, :
|
118
|
+
element :street, String, tag: 'street'
|
96
119
|
element :street, String
|
97
120
|
```
|
98
121
|
|
99
|
-
Including the additional tag element is not going to hurt anything and in some
|
122
|
+
Including the additional tag element is not going to hurt anything and in some
|
123
|
+
cases will make it absolutely clear how these elements map to the XML. However,
|
124
|
+
once you know this rule, it is hard not to want to save yourself the
|
125
|
+
keystrokes.
|
100
126
|
|
101
127
|
Instead of `element` you may also use `has_one`:
|
102
128
|
|
103
129
|
```ruby
|
104
|
-
element :street, String, :
|
130
|
+
element :street, String, tag: 'street'
|
105
131
|
element :street, String
|
106
132
|
has_one :street, String
|
107
133
|
```
|
108
134
|
|
109
135
|
These three statements are equivalent to each other.
|
110
136
|
|
111
|
-
|
137
|
+
### Parsing
|
112
138
|
|
113
|
-
With the mapping of the address XML articulated in our Address class it is time
|
139
|
+
With the mapping of the address XML articulated in our Address class it is time
|
140
|
+
to parse the data:
|
114
141
|
|
115
142
|
```ruby
|
116
|
-
address = Address.parse(ADDRESS_XML_DATA, :
|
143
|
+
address = Address.parse(ADDRESS_XML_DATA, single: true)
|
117
144
|
puts address.street
|
118
145
|
```
|
119
146
|
|
120
|
-
Assuming that the constant `ADDRESS_XML_DATA` contains a string representation
|
147
|
+
Assuming that the constant `ADDRESS_XML_DATA` contains a string representation
|
148
|
+
of the address XML data this is fairly straight-forward save for the `parse`
|
149
|
+
method.
|
121
150
|
|
122
|
-
The `parse` method, like `tag` and `element` are all added when you included
|
151
|
+
The `parse` method, like `tag` and `element` are all added when you included
|
152
|
+
HappyMapper in the class. Parse is a wonderful, magical place that converts all
|
153
|
+
these declarations that you have made into the data structure you are about to
|
154
|
+
know and love.
|
123
155
|
|
124
|
-
But what about the
|
156
|
+
But what about the `single: true`? Right, that is because by default when
|
157
|
+
your object is all done parsing it will be an array. In this case an array with
|
158
|
+
one element, but an array none the less. So the following are equivalent to
|
159
|
+
each other:
|
125
160
|
|
126
161
|
```ruby
|
127
162
|
address = Address.parse(ADDRESS_XML_DATA).first
|
128
|
-
address = Address.parse(ADDRESS_XML_DATA, :
|
163
|
+
address = Address.parse(ADDRESS_XML_DATA, single: true)
|
129
164
|
```
|
130
165
|
|
131
|
-
The first one returns an array and we return the first instance, the second
|
166
|
+
The first one returns an array and we return the first instance, the second
|
167
|
+
will do that work for us inside of parse.
|
132
168
|
|
133
|
-
|
169
|
+
### Multiple Elements Mapping
|
134
170
|
|
135
|
-
What if our address XML was a little different, perhaps we allowed multiple
|
171
|
+
What if our address XML was a little different, perhaps we allowed multiple
|
172
|
+
streets:
|
136
173
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
174
|
+
```xml
|
175
|
+
<address>
|
176
|
+
<street>Milchstrasse</street>
|
177
|
+
<street>Another Street</street>
|
178
|
+
<housenumber>23</housenumber>
|
179
|
+
<postcode>26131</postcode>
|
180
|
+
<city>Oldenburg</city>
|
181
|
+
<country code="de">Germany</country>
|
182
|
+
</address>
|
183
|
+
```
|
145
184
|
|
146
|
-
Similar to `element` or `has_one`, the declaration for when you have multiple
|
185
|
+
Similar to `element` or `has_one`, the declaration for when you have multiple
|
186
|
+
elements you simply use:
|
147
187
|
|
148
188
|
```ruby
|
149
|
-
has_many :streets, String, :
|
189
|
+
has_many :streets, String, tag: 'street'
|
150
190
|
```
|
151
191
|
|
152
192
|
Your resulting `streets` method will now return an array.
|
153
193
|
|
154
194
|
```ruby
|
155
|
-
address = Address.parse(ADDRESS_XML_DATA, :
|
195
|
+
address = Address.parse(ADDRESS_XML_DATA, single: true)
|
156
196
|
puts address.streets.join('\n')
|
157
197
|
```
|
158
198
|
|
159
|
-
Imagine that you have to write `streets.join('\n')` for the rest of eternity
|
199
|
+
Imagine that you have to write `streets.join('\n')` for the rest of eternity
|
200
|
+
throughout your code. It would be a nightmare and one that you could avoid by
|
201
|
+
creating your own convenience method.
|
160
202
|
|
161
203
|
```ruby
|
162
204
|
require 'happymapper'
|
@@ -172,52 +214,60 @@ class Address
|
|
172
214
|
@streets.join('\n')
|
173
215
|
end
|
174
216
|
|
175
|
-
element :postcode, String, :
|
176
|
-
element :housenumber, String, :
|
177
|
-
element :city, String, :
|
178
|
-
element :country, String, :
|
217
|
+
element :postcode, String, tag: 'postcode'
|
218
|
+
element :housenumber, String, tag: 'housenumber'
|
219
|
+
element :city, String, tag: 'city'
|
220
|
+
element :country, String, tag: 'country'
|
179
221
|
end
|
180
222
|
```
|
181
223
|
|
182
|
-
Now when we call the method `streets` we get a single value, but we still have
|
224
|
+
Now when we call the method `streets` we get a single value, but we still have
|
225
|
+
the instance variable `@streets` if we ever need to the values as an array.
|
183
226
|
|
184
227
|
|
185
|
-
|
228
|
+
### Attribute Mapping
|
186
229
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
230
|
+
```xml
|
231
|
+
<address location='home'>
|
232
|
+
<street>Milchstrasse</street>
|
233
|
+
<street>Another Street</street>
|
234
|
+
<housenumber>23</housenumber>
|
235
|
+
<postcode>26131</postcode>
|
236
|
+
<city>Oldenburg</city>
|
237
|
+
<country code="de">Germany</country>
|
238
|
+
</address>
|
239
|
+
```
|
195
240
|
|
196
241
|
Attributes are absolutely the same as `element` or `has_many`
|
197
242
|
|
198
243
|
```ruby
|
199
|
-
attribute :location, String, :
|
244
|
+
attribute :location, String, tag: 'location
|
200
245
|
```
|
201
246
|
|
202
|
-
Again, you can omit the tag if the attribute accessor symbol matches the name
|
247
|
+
Again, you can omit the tag if the attribute accessor symbol matches the name
|
248
|
+
of the attribute.
|
203
249
|
|
250
|
+
#### Attributes On Empty Child Elements
|
204
251
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
252
|
+
```xml
|
253
|
+
<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom">
|
254
|
+
<id>tag:all-the-episodes.heroku.com,2005:/tv_shows</id>
|
255
|
+
<link rel="alternate" type="text/html" href="http://all-the-episodes.heroku.com"/>
|
256
|
+
<link rel="self" type="application/atom+xml"
|
257
|
+
href="http://all-the-episodes.heroku.com/tv_shows.atom"/>
|
258
|
+
<title>TV Shows</title>
|
259
|
+
<updated>2011-07-10T06:52:27Z</updated>
|
260
|
+
</feed>
|
261
|
+
```
|
214
262
|
|
215
|
-
In this case you would need to map an element to a new `Link` class just to
|
263
|
+
In this case you would need to map an element to a new `Link` class just to
|
264
|
+
access `<link>`s attributes, except that there is an alternate syntax. Instead
|
265
|
+
of
|
216
266
|
|
217
267
|
```ruby
|
218
268
|
class Feed
|
219
269
|
# ....
|
220
|
-
has_many :links, Link, :
|
270
|
+
has_many :links, Link, tag: 'link', xpath: '.'
|
221
271
|
end
|
222
272
|
|
223
273
|
class Link
|
@@ -232,17 +282,25 @@ end
|
|
232
282
|
You can drop the `Link` class and simply replace the `has_many` on `Feed` with
|
233
283
|
|
234
284
|
```ruby
|
235
|
-
element :link, String, :
|
285
|
+
element :link, String, single: false, attributes: { rel: String, type: String, href: String }
|
236
286
|
```
|
237
287
|
|
238
|
-
As there is no content, the type given for `:link` (`String` above) is
|
288
|
+
As there is no content, the type given for `:link` (`String` above) is
|
289
|
+
irrelevant, but `nil` won't work and other types may try to perform typecasting
|
290
|
+
and fail. You can omit the single: false for elements that only occur once
|
291
|
+
within their parent.
|
239
292
|
|
240
|
-
This syntax is most appropriate for elements that (a) have attributes but no
|
293
|
+
This syntax is most appropriate for elements that (a) have attributes but no
|
294
|
+
content and (b) only occur at only one level of the heirarchy. If `<feed>`
|
295
|
+
contained another element that also contained a `<link>` (as atom feeds
|
296
|
+
generally do) it would be DRY-er to use the first syntax, i.e. with a separate
|
297
|
+
`Link` class.
|
241
298
|
|
242
299
|
|
243
|
-
|
300
|
+
### Class composition (and Text Node)
|
244
301
|
|
245
|
-
Our address has a country and that country element has a code. Up until this
|
302
|
+
Our address has a country and that country element has a code. Up until this
|
303
|
+
point we neglected it as we declared a `country` as being a `String`.
|
246
304
|
|
247
305
|
<address location='home'>
|
248
306
|
<street>Milchstrasse</street>
|
@@ -253,7 +311,8 @@ Our address has a country and that country element has a code. Up until this poi
|
|
253
311
|
<country code="de">Germany</country>
|
254
312
|
</address>
|
255
313
|
|
256
|
-
Well if we only going to parse country, on it's own, we would likely create a
|
314
|
+
Well if we only going to parse country, on it's own, we would likely create a
|
315
|
+
class mapping for it.
|
257
316
|
|
258
317
|
```ruby
|
259
318
|
class Country
|
@@ -270,7 +329,8 @@ We are utilizing an `attribute` declaration and a new declaration called `conten
|
|
270
329
|
|
271
330
|
* `content` is used when you want the text contained within the element
|
272
331
|
|
273
|
-
Awesome, now if we were to redeclare our `Address` class we would use our new
|
332
|
+
Awesome, now if we were to redeclare our `Address` class we would use our new
|
333
|
+
`Country` class.
|
274
334
|
|
275
335
|
```ruby
|
276
336
|
class Address
|
@@ -278,64 +338,77 @@ class Address
|
|
278
338
|
|
279
339
|
tag 'address'
|
280
340
|
|
281
|
-
has_many :streets, String, :
|
341
|
+
has_many :streets, String, tag: 'street'
|
282
342
|
|
283
343
|
def streets
|
284
344
|
@streets.join('\n')
|
285
345
|
end
|
286
346
|
|
287
|
-
element :postcode, String, :
|
288
|
-
element :housenumber, String, :
|
289
|
-
element :city, String, :
|
290
|
-
element :country, Country, :
|
347
|
+
element :postcode, String, tag: 'postcode'
|
348
|
+
element :housenumber, String, tag: 'housenumber'
|
349
|
+
element :city, String, tag: 'city'
|
350
|
+
element :country, Country, tag: 'country'
|
291
351
|
end
|
292
352
|
```
|
293
353
|
|
294
|
-
Instead of `String`, `Boolean`, or `Integer` we say that it is a `Country` and
|
354
|
+
Instead of `String`, `Boolean`, or `Integer` we say that it is a `Country` and
|
355
|
+
HappyMapper takes care of the details of continuing the XML mapping through the
|
356
|
+
country element.
|
295
357
|
|
296
358
|
```ruby
|
297
|
-
address = Address.parse(ADDRESS_XML_DATA, :
|
359
|
+
address = Address.parse(ADDRESS_XML_DATA, single: true)
|
298
360
|
puts address.country.code
|
299
361
|
```
|
300
362
|
|
301
|
-
A quick note, in the above example we used the constant `Country`. We could
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
</
|
321
|
-
</
|
363
|
+
A quick note, in the above example we used the constant `Country`. We could
|
364
|
+
have used `'Country'`. The nice part of using the latter declaration, enclosed
|
365
|
+
in quotes, is that you do not have to define your class before this class. So
|
366
|
+
Country and Address can live in separate files and as long as both constants
|
367
|
+
are available when it comes time to parse you are golden.
|
368
|
+
|
369
|
+
### Custom XPATH
|
370
|
+
|
371
|
+
#### Has One, Has Many
|
372
|
+
|
373
|
+
Getting to elements deep down within your XML can be a little more work if you
|
374
|
+
did not have xpath support. Consider the following example:
|
375
|
+
|
376
|
+
```xml
|
377
|
+
<media>
|
378
|
+
<gallery>
|
379
|
+
<title href="htttp://fishlovers.org/friends">Friends Who Like Fish</title>
|
380
|
+
<picture>
|
381
|
+
<name>Burtie Sanchez</name>
|
382
|
+
<img>burtie01.png</img>
|
383
|
+
</picture>
|
384
|
+
</gallery>
|
385
|
+
<picture>
|
386
|
+
<name>Unsorted Photo</name>
|
387
|
+
<img>bestfriends.png</img>
|
388
|
+
</picture>
|
389
|
+
</media>
|
390
|
+
```
|
322
391
|
|
323
|
-
You may want to map the sub-elements contained buried in the 'gallery' as top
|
392
|
+
You may want to map the sub-elements contained buried in the 'gallery' as top
|
393
|
+
level items in the media. Traditionally you could use class composition to
|
394
|
+
accomplish this task, however, using the xpath attribute you have the ability
|
395
|
+
to shortcut some of that work.
|
324
396
|
|
325
397
|
```ruby
|
326
398
|
class Media
|
327
399
|
include HappyMapper
|
328
400
|
|
329
|
-
has_one :title, String, :
|
330
|
-
has_one :link, String, :
|
401
|
+
has_one :title, String, xpath: 'gallery/title'
|
402
|
+
has_one :link, String, xpath: 'gallery/title/@href'
|
331
403
|
end
|
332
404
|
```
|
333
405
|
|
334
|
-
|
406
|
+
### Shared Functionality
|
335
407
|
|
336
|
-
|
408
|
+
#### Inheritance Approach
|
337
409
|
|
338
|
-
While mapping XML to objects you may arrive at a point where you have two or
|
410
|
+
While mapping XML to objects you may arrive at a point where you have two or
|
411
|
+
more very similar structures.
|
339
412
|
|
340
413
|
```ruby
|
341
414
|
class Article
|
@@ -361,7 +434,9 @@ class Gallery
|
|
361
434
|
end
|
362
435
|
```
|
363
436
|
|
364
|
-
In this example there are definitely two similarities between our two pieces of
|
437
|
+
In this example there are definitely two similarities between our two pieces of
|
438
|
+
content. So much so that you might be included to create an inheritance
|
439
|
+
structure to save yourself some keystrokes.
|
365
440
|
|
366
441
|
```ruby
|
367
442
|
class Content
|
@@ -385,7 +460,7 @@ class Gallery < Content
|
|
385
460
|
end
|
386
461
|
```
|
387
462
|
|
388
|
-
|
463
|
+
#### Module Mixins Approach
|
389
464
|
|
390
465
|
You can also solve the above problem through mixins.
|
391
466
|
|
@@ -417,25 +492,31 @@ class Gallery
|
|
417
492
|
end
|
418
493
|
```
|
419
494
|
|
420
|
-
Here, when we include `Content` in both of these classes the module method
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
495
|
+
Here, when we include `Content` in both of these classes the module method
|
496
|
+
`#included` is called and our class is given as a parameter. So we take that
|
497
|
+
opportunity to do some surgery and define our happymapper elements as well as
|
498
|
+
any other methods that may rely on those instance variables that come along in
|
499
|
+
the package.
|
500
|
+
|
501
|
+
### Filtering with XPATH (non-greedy)
|
502
|
+
|
503
|
+
I ran into a case where I wanted to capture all the pictures that were directly
|
504
|
+
under media, but not the ones contained within a gallery.
|
505
|
+
|
506
|
+
```xml
|
507
|
+
<media>
|
508
|
+
<gallery>
|
509
|
+
<picture>
|
510
|
+
<name>Burtie Sanchez</name>
|
511
|
+
<img>burtie01.png</img>
|
512
|
+
</picture>
|
513
|
+
</gallery>
|
514
|
+
<picture>
|
515
|
+
<name>Unsorted Photo</name>
|
516
|
+
<img>bestfriends.png</img>
|
517
|
+
</picture>
|
518
|
+
</media>
|
519
|
+
```
|
439
520
|
|
440
521
|
The following `Media` class is where I started:
|
441
522
|
|
@@ -445,15 +526,15 @@ require 'happymapper'
|
|
445
526
|
class Media
|
446
527
|
include HappyMapper
|
447
528
|
|
448
|
-
has_many :galleries, Gallery, :
|
449
|
-
has_many :pictures, Picture, :
|
529
|
+
has_many :galleries, Gallery, tag: 'gallery'
|
530
|
+
has_many :pictures, Picture, tag: 'picture'
|
450
531
|
end
|
451
532
|
```
|
452
533
|
|
453
534
|
However when I parsed the media xml the number of pictures returned to me was 2, not 1.
|
454
535
|
|
455
536
|
```ruby
|
456
|
-
pictures = Media.parse(MEDIA_XML
|
537
|
+
pictures = Media.parse(MEDIA_XML,single: true).pictures
|
457
538
|
pictures.length.should == 1 # => Failed Expectation
|
458
539
|
```
|
459
540
|
|
@@ -466,16 +547,16 @@ To limit an element from being greedy and only finding elements at the
|
|
466
547
|
level of the current node you can specify an XPATH.
|
467
548
|
|
468
549
|
```ruby
|
469
|
-
has_many :pictures, Picture, :
|
550
|
+
has_many :pictures, Picture, tag: 'picture', xpath: '.'
|
470
551
|
```
|
471
552
|
|
472
553
|
`.` states that we are only interested in pictures that can be found directly
|
473
554
|
under the current node. So when we parse again we will have only our one element.
|
474
555
|
|
556
|
+
### Namespaces
|
475
557
|
|
476
|
-
|
477
|
-
|
478
|
-
Obviously your XML and these trivial examples are easy to map and parse because they lack the treacherous namespaces that befall most XML files.
|
558
|
+
Obviously your XML and these trivial examples are easy to map and parse because
|
559
|
+
they lack the treacherous namespaces that befall most XML files.
|
479
560
|
|
480
561
|
Perhaps our `address` XML is really swarming with namespaces:
|
481
562
|
|
@@ -488,7 +569,10 @@ Perhaps our `address` XML is really swarming with namespaces:
|
|
488
569
|
<prefix:country code="de">Germany</prefix:country>
|
489
570
|
</prefix:address>
|
490
571
|
|
491
|
-
Here again is our address example with a made up namespace called `prefix` that
|
572
|
+
Here again is our address example with a made up namespace called `prefix` that
|
573
|
+
comes direct to use from unicornland, a very magical place indeed. Well we are
|
574
|
+
going to have to do some work on our class definition and that simply adding
|
575
|
+
this one liner to the `Address` class:
|
492
576
|
|
493
577
|
```ruby
|
494
578
|
class Address
|
@@ -500,90 +584,110 @@ class Address
|
|
500
584
|
end
|
501
585
|
```
|
502
586
|
|
503
|
-
Of course, if that is too easy for you, you can append a
|
587
|
+
Of course, if that is too easy for you, you can append a `namespace: 'prefix`
|
588
|
+
to every one of the elements that you defined.
|
504
589
|
|
505
590
|
```ruby
|
506
|
-
has_many :street, String, :
|
507
|
-
element :postcode, String, :
|
508
|
-
element :housenumber, String, :
|
509
|
-
element :city, String, :
|
510
|
-
element :country, Country, :
|
591
|
+
has_many :street, String, tag: 'street', namespace: 'prefix'
|
592
|
+
element :postcode, String, tag: 'postcode', namespace: 'prefix'
|
593
|
+
element :housenumber, String, tag: 'housenumber', namespace: 'prefix'
|
594
|
+
element :city, String, tag: 'city', namespace: 'prefix'
|
595
|
+
element :country, Country, tag: 'country', namespace: 'prefix'
|
511
596
|
```
|
512
597
|
|
513
|
-
I definitely recommend the former, as it saves you a whole hell of lot of
|
598
|
+
I definitely recommend the former, as it saves you a whole hell of lot of
|
599
|
+
typing. However, there are times when appending a namespace to an element
|
600
|
+
declaration is important and that is when it has a different namespace than
|
601
|
+
`namespace 'prefix'`.
|
514
602
|
|
515
603
|
Imagine that our `country` actually belonged to a completely different namespace.
|
516
604
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
605
|
+
```xml
|
606
|
+
<prefix:address location='home'
|
607
|
+
xmlns:prefix="http://www.unicornland.com/prefix"
|
608
|
+
xmlns:different="http://www.trollcountry.com/different">
|
609
|
+
<prefix:street>Milchstrasse</prefix:street>
|
610
|
+
<prefix:street>Another Street</prefix:street>
|
611
|
+
<prefix:housenumber>23</prefix:housenumber>
|
612
|
+
<prefix:postcode>26131</prefix:postcode>
|
613
|
+
<prefix:city>Oldenburg</prefix:city>
|
614
|
+
<different:country code="de">Germany</different:country>
|
615
|
+
</prefix:address>
|
616
|
+
```
|
526
617
|
|
527
618
|
Well we would need to specify that namespace:
|
528
619
|
|
529
620
|
```ruby
|
530
|
-
element :country, Country, :
|
621
|
+
element :country, Country, tag: 'country', namespace: 'different'
|
531
622
|
```
|
532
623
|
|
533
624
|
With that we should be able to parse as we once did.
|
534
625
|
|
535
|
-
|
626
|
+
### Large Datasets (`:in_groups_of`)
|
536
627
|
|
537
|
-
When dealing with large sets of XML that simply cannot or should not be placed
|
628
|
+
When dealing with large sets of XML that simply cannot or should not be placed
|
629
|
+
into memory the objects can be handled in groups through the `:in_groups_of`
|
630
|
+
parameter.
|
538
631
|
|
539
632
|
```ruby
|
540
|
-
Address.parse(LARGE_ADDRESS_XML_DATA
|
633
|
+
Address.parse(LARGE_ADDRESS_XML_DATA,in_groups_of: 5) do |group|
|
541
634
|
puts address.streets
|
542
635
|
end
|
543
636
|
```
|
544
637
|
|
545
|
-
This trivial block will parse the large set of XML data and in groups of 5
|
638
|
+
This trivial block will parse the large set of XML data and in groups of 5
|
639
|
+
addresses at a time display the streets.
|
546
640
|
|
547
|
-
|
641
|
+
### Saving to XML
|
548
642
|
|
549
|
-
Saving a class to XML is as easy as calling `#to_xml`. The end result will be
|
643
|
+
Saving a class to XML is as easy as calling `#to_xml`. The end result will be
|
644
|
+
the current state of your object represented as xml. Let's cover some details
|
645
|
+
that are sometimes necessary and features present to make your life easier.
|
550
646
|
|
551
647
|
|
552
|
-
|
648
|
+
#### :on_save
|
553
649
|
|
554
|
-
When you are saving data to xml it is often important to change or manipulate
|
650
|
+
When you are saving data to xml it is often important to change or manipulate
|
651
|
+
data to a particular format. For example, a time object:
|
555
652
|
|
556
653
|
```ruby
|
557
|
-
has_one :published_time, Time, :
|
654
|
+
has_one :published_time, Time, on_save: lambda {|time| time.strftime("%H:%M:%S") if time }
|
558
655
|
```
|
559
656
|
|
560
|
-
Here we add the options `:on_save` and specify a lambda which will be executed
|
657
|
+
Here we add the options `:on_save` and specify a lambda which will be executed
|
658
|
+
on the method call to `:published_time`.
|
561
659
|
|
562
|
-
|
660
|
+
#### :state_when_nil
|
563
661
|
|
564
|
-
When an element contains a nil value, or perhaps the result of the :on_save
|
662
|
+
When an element contains a nil value, or perhaps the result of the :on_save
|
663
|
+
lambda correctly results in a nil value you will be happy that the element will
|
664
|
+
not appear in the resulting XML. However, there are time when you will want to
|
665
|
+
see that element and that's when `:state_when_nil` is there for you.
|
565
666
|
|
566
667
|
```ruby
|
567
|
-
has_one :favorite_color, String, :
|
668
|
+
has_one :favorite_color, String, state_when_nil: true
|
568
669
|
```
|
569
670
|
|
570
|
-
The resulting XML will include the 'favorite_color' element even if the
|
671
|
+
The resulting XML will include the 'favorite_color' element even if the
|
672
|
+
favorite color has not been specified.
|
571
673
|
|
572
|
-
|
674
|
+
#### :read_only
|
573
675
|
|
574
676
|
When an element, attribute, or text node is a value that you have no interest in
|
575
677
|
saving to XML, you can ensure that takes place by stating that it is `read only`.
|
576
678
|
|
577
679
|
```ruby
|
578
|
-
has_one :modified, Boolean, :
|
579
|
-
attribute :temporary, Boolean, :
|
680
|
+
has_one :modified, Boolean, read_only: true
|
681
|
+
attribute :temporary, Boolean, read_only: true
|
580
682
|
```
|
581
683
|
|
582
684
|
This is useful if perhaps the incoming XML is different than the out-going XML.
|
583
685
|
|
584
|
-
|
686
|
+
#### namespaces
|
585
687
|
|
586
|
-
Parsing the XML to objects only required you to simply specify the prefix of
|
688
|
+
Parsing the XML to objects only required you to simply specify the prefix of
|
689
|
+
the namespace you wanted to parse, when you persist to xml you will need to
|
690
|
+
define your namespaces so that they are correctly captured.
|
587
691
|
|
588
692
|
```ruby
|
589
693
|
class Address
|
@@ -599,7 +703,7 @@ class Address
|
|
599
703
|
element :postcode, String
|
600
704
|
element :housenumber, String
|
601
705
|
element :city, String
|
602
|
-
element :country, Country, :
|
706
|
+
element :country, Country, tag: 'country', namespace: 'different'
|
603
707
|
|
604
708
|
end
|
605
|
-
```
|
709
|
+
```
|