nokogiri-happymapper 0.6.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|
+
[](https://badge.fury.io/rb/nokogiri-happymapper)
|
10
|
+
[](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
|
+
```
|