unhappymapper 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.md +479 -0
  2. data/TODO +0 -0
  3. data/lib/happymapper/attribute.rb +3 -0
  4. data/lib/happymapper/element.rb +3 -0
  5. data/lib/happymapper/item.rb +250 -0
  6. data/lib/happymapper/text_node.rb +3 -0
  7. data/lib/happymapper.rb +574 -0
  8. data/spec/fixtures/address.xml +8 -0
  9. data/spec/fixtures/ambigous_items.xml +22 -0
  10. data/spec/fixtures/analytics.xml +61 -0
  11. data/spec/fixtures/analytics_profile.xml +127 -0
  12. data/spec/fixtures/commit.xml +52 -0
  13. data/spec/fixtures/current_weather.xml +89 -0
  14. data/spec/fixtures/dictionary.xml +20 -0
  15. data/spec/fixtures/family_tree.xml +21 -0
  16. data/spec/fixtures/inagy.xml +86 -0
  17. data/spec/fixtures/lastfm.xml +355 -0
  18. data/spec/fixtures/multiple_namespaces.xml +170 -0
  19. data/spec/fixtures/multiple_primitives.xml +5 -0
  20. data/spec/fixtures/pita.xml +133 -0
  21. data/spec/fixtures/posts.xml +23 -0
  22. data/spec/fixtures/product_default_namespace.xml +17 -0
  23. data/spec/fixtures/product_no_namespace.xml +10 -0
  24. data/spec/fixtures/product_single_namespace.xml +10 -0
  25. data/spec/fixtures/quarters.xml +19 -0
  26. data/spec/fixtures/radar.xml +21 -0
  27. data/spec/fixtures/statuses.xml +422 -0
  28. data/spec/fixtures/subclass_namespace.xml +50 -0
  29. data/spec/happymapper_attribute_spec.rb +21 -0
  30. data/spec/happymapper_element_spec.rb +21 -0
  31. data/spec/happymapper_item_spec.rb +115 -0
  32. data/spec/happymapper_spec.rb +941 -0
  33. data/spec/happymapper_text_node_spec.rb +21 -0
  34. data/spec/happymapper_to_xml_namespaces_spec.rb +196 -0
  35. data/spec/happymapper_to_xml_spec.rb +196 -0
  36. data/spec/ignay_spec.rb +95 -0
  37. data/spec/spec_helper.rb +7 -0
  38. data/spec/xpath_spec.rb +88 -0
  39. metadata +150 -0
data/README.md ADDED
@@ -0,0 +1,479 @@
1
+ HappyMapper
2
+ ===========
3
+
4
+ Happymapper allows you to parse XML data and convert it quickly and easily into ruby data structures.
5
+
6
+ This project is a grandchild (a fork of a fork) of the great work done first by [jnunemaker](https://github.com/jnunemaker/happymapper) and then by [dam5s](http://github.com/dam5s/happymapper/).
7
+ =======
8
+
9
+ Installation
10
+ ------------
11
+
12
+ *Build the gem yourself:*
13
+
14
+ $ git clone https://github.com/burtlo/happymapper
15
+ $ cd happymapper
16
+ $ git checkout master
17
+ $ gem build nokogiri-happymapper.gemspec
18
+ $ gem install --local happymapper-X.X.X.gem
19
+
20
+ *For you [Bundler's](http://gembundler.com/) out there, you can add it to your Gemfile and then `bundle install`*
21
+
22
+ gem 'happymapper', :git => "git://github.com/burtlo/happymapper.git"
23
+
24
+ Differences
25
+ -----------
26
+
27
+ * [dam5s](http://github.com/dam5s/happymapper/)'s fork added [Nokogiri](http://nokogiri.org/) support
28
+ * `#to_xml` support utilizing the same HappyMapper tags
29
+ * Fixes for [namespaces when using composition of classes](https://github.com/burtlo/happymapper/commit/fd1e898c70f7289d2d2618d629b56f2f6623785c)
30
+ * Fixes for instances of XML where a [namespace is defined but no elements with that namespace are found](https://github.com/burtlo/happymapper/commit/9614221a80ff3bda18ff859aa751dff29cf52fd3).
31
+
32
+
33
+ Examples
34
+ --------
35
+
36
+ ## Element Mapping
37
+
38
+ Let's start with a simple example to get our feet wet. Here we have a simple example of XML that defines some address information:
39
+
40
+ <address>
41
+ <street>Milchstrasse</street>
42
+ <housenumber>23</housenumber>
43
+ <postcode>26131</postcode>
44
+ <city>Oldenburg</city>
45
+ <country code="de">Germany</country>
46
+ </address>
47
+
48
+ Happymapper will let you easily model this information as a class:
49
+
50
+ require 'happymapper'
51
+
52
+ class Address
53
+ include HappyMapper
54
+
55
+ tag 'address'
56
+ element :street, String, :tag => 'street'
57
+ element :postcode, String, :tag => 'postcode'
58
+ element :housenumber, Integer, :tag => 'housenumber'
59
+ element :city, String, :tag => 'city'
60
+ element :country, String, :tag => 'country'
61
+ end
62
+
63
+ 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.
64
+
65
+ * `tag` matches the name of the XML tag name 'address'.
66
+
67
+ * `element` defines accessor methods for the specified symbol (e.g. `:street`,`:housenumber`) that will return the class type (e.g. `String`,`Integer`) of the XML tag specified (e.g. `:tag => 'street'`, `:tag => 'housenumber'`).
68
+
69
+ When you define an element with an accessor with the same name as the tag, this is the case for all the examples above, you can omit the `:tag`. These two element declaration are equivalent to each other:
70
+
71
+ element :street, String, :tag => 'street'
72
+ element :street, String
73
+
74
+ Including the additional tag element is not going to hurt anything and in some cases will make it absolutely clear how these elements map to the XML. However, once you know this rule, it is hard not to want to save yourself the keystrokes.
75
+
76
+ Instead of `element` you may also use `has_one`:
77
+
78
+ element :street, String, :tag => 'street'
79
+ element :street, String
80
+ has_one :street, String
81
+
82
+ These three statements are equivalent to each other.
83
+
84
+ ## Parsing
85
+
86
+ With the mapping of the address XML articulated in our Address class it is time to parse the data:
87
+
88
+ address = Address.parse(ADDRESS_XML_DATA, :single => true)
89
+ puts address.street
90
+
91
+ 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.
92
+
93
+ 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.
94
+
95
+ 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:
96
+
97
+ address = Address.parse(ADDRESS_XML_DATA).first
98
+ address = Address.parse(ADDRESS_XML_DATA, :single => true)
99
+
100
+ The first one returns an array and we return the first instance, the second will do that work for us inside of parse.
101
+
102
+ ## Multiple Elements Mapping
103
+
104
+ What if our address XML was a little different, perhaps we allowed multiple streets:
105
+
106
+ <address>
107
+ <street>Milchstrasse</street>
108
+ <street>Another Street</street>
109
+ <housenumber>23</housenumber>
110
+ <postcode>26131</postcode>
111
+ <city>Oldenburg</city>
112
+ <country code="de">Germany</country>
113
+ </address>
114
+
115
+ Similar to `element` or `has_one`, the declaration for when you have multiple elements you simply use:
116
+
117
+ has_many :streets, String, :tag => 'street'
118
+
119
+ Your resulting `streets` method will now return an array.
120
+
121
+ address = Address.parse(ADDRESS_XML_DATA, :single => true)
122
+ puts address.streets.join('\n')
123
+
124
+ 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.
125
+
126
+ require 'happymapper'
127
+
128
+ class Address
129
+ include HappyMapper
130
+
131
+ tag 'address'
132
+
133
+ has_many :streets, String
134
+
135
+ def streets
136
+ @streets.join('\n')
137
+ end
138
+
139
+ element :postcode, String, :tag => 'postcode'
140
+ element :housenumber, String, :tag => 'housenumber'
141
+ element :city, String, :tag => 'city'
142
+ element :country, String, :tag => 'country'
143
+ end
144
+
145
+ Now when we call the method `streets` we get a single value, but we still have the instance variable `@streets` if we ever need to the values as an array.
146
+
147
+
148
+ ## Attribute Mapping
149
+
150
+ <address location='home'>
151
+ <street>Milchstrasse</street>
152
+ <street>Another Street</street>
153
+ <housenumber>23</housenumber>
154
+ <postcode>26131</postcode>
155
+ <city>Oldenburg</city>
156
+ <country code="de">Germany</country>
157
+ </address>
158
+
159
+ Attributes are absolutely the same as `element` or `has_many`
160
+
161
+ attribute :location, String, :tag => 'location
162
+
163
+ Again, you can omit the tag if the attribute accessor symbol matches the name of the attribute.
164
+
165
+
166
+ ## Class composition (and Text Node)
167
+
168
+ Our address has a country and that country element has a code. Up until this point we neglected it as we declared a `country` as being a `String`.
169
+
170
+ <address location='home'>
171
+ <street>Milchstrasse</street>
172
+ <street>Another Street</street>
173
+ <housenumber>23</housenumber>
174
+ <postcode>26131</postcode>
175
+ <city>Oldenburg</city>
176
+ <country code="de">Germany</country>
177
+ </address>
178
+
179
+ Well if we only going to parse country, on it's own, we would likely create a class mapping for it.
180
+
181
+ class Country
182
+ include HappyMapper
183
+
184
+ tag 'country'
185
+
186
+ attribute :code, String
187
+ text_node :name, String
188
+ end
189
+
190
+ We are utilizing an `attribute` declaration and a new declaration called `text_node`.
191
+
192
+ * `text_node` is used when you want the text contained within the element
193
+
194
+ Awesome, now if we were to redeclare our `Address` class we would use our new `Country` class.
195
+
196
+ class Address
197
+ include HappyMapper
198
+
199
+ tag 'address'
200
+
201
+ has_many :streets, String, :tag => 'street'
202
+
203
+ def streets
204
+ @streets.join('\n')
205
+ end
206
+
207
+ element :postcode, String, :tag => 'postcode'
208
+ element :housenumber, String, :tag => 'housenumber'
209
+ element :city, String, :tag => 'city'
210
+ element :country, Country, :tag => 'country'
211
+ end
212
+
213
+ 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.
214
+
215
+ address = Address.parse(ADDRESS_XML_DATA, :single => true)
216
+ puts address.country.code
217
+
218
+ A quick note, in the above example we used the constant `Country`. We could have used `'Country'`. The nice part of using the latter declaration, enclosed in quotes, is that you do not have to define your class before this class. So Country and Address can live in separate files and as long as both constants are available when it comes time to parse you are golden.
219
+
220
+ ## Custom XPATH
221
+
222
+ ### Has One, Has Many
223
+
224
+ Getting to elements deep down within your XML can be a little more work if you did not have xpath support. Consider the following example:
225
+
226
+ <media>
227
+ <gallery>
228
+ <title href="htttp://fishlovers.org/friends">Friends Who Like Fish</title>
229
+ <picture>
230
+ <name>Burtie Sanchez</name>
231
+ <img>burtie01.png</img>
232
+ </picture>
233
+ </gallery>
234
+ <picture>
235
+ <name>Unsorted Photo</name>
236
+ <img>bestfriends.png</img>
237
+ </picture>
238
+ </media>
239
+
240
+ You may want to map the sub-elements contained buried in the 'gallery' as top level items in the media. Traditionally you could use class composition to accomplish this task, however, using the xpath attribute you have the ability to shortcut some of that work.
241
+
242
+ class Media
243
+ include HappyMapper
244
+
245
+ has_one :title, String, :xpath => 'gallery/title'
246
+ has_one :link, String, :xpath => 'gallery/title/@href'
247
+ end
248
+
249
+
250
+ ### Subclasses
251
+
252
+ ## Inheritance (it doesn't work!)
253
+
254
+ While mapping XML to objects you may arrive at a point where you have two or more very similar structures.
255
+
256
+ class Article
257
+ include HappyMapper
258
+
259
+ has_one :title, String
260
+ has_one :author, String
261
+ has_one :published, Time
262
+
263
+ has_one :entry, String
264
+
265
+ end
266
+
267
+ class Gallery
268
+ include HappyMapper
269
+
270
+ has_one :title, String
271
+ has_one :author, String
272
+ has_one :published, Time
273
+
274
+ has_many :photos, String
275
+
276
+ end
277
+
278
+ In this example there are definitely two similarities between our two pieces of content. So much so that you might be included to create an inheritance structure to save yourself some keystrokes.
279
+
280
+ class Content
281
+ include HappyMapper
282
+
283
+ has_one :title, String
284
+ has_one :author, String
285
+ has_one :published, Time
286
+
287
+ end
288
+
289
+ class Article < Content
290
+ include HappyMapper
291
+
292
+ has_one :entry, String
293
+ end
294
+
295
+ class Gallery < Content
296
+ include HappyMapper
297
+
298
+ has_many :photos, String
299
+ end
300
+
301
+ However, *this does not work*. And the reason is because each one of these element declarations are method calls that are defining elements on the class itself. So it is not passed down through inheritance.
302
+
303
+ You can however, use some module mixin power to save you those keystrokes and impress your friends.
304
+
305
+
306
+ module Content
307
+ def self.included(content)
308
+ content.has_one :title, String
309
+ content.has_one :author, String
310
+ content.has_one :published, Time
311
+ end
312
+
313
+ def published_time
314
+ @published.strftime("%H:%M:%S")
315
+ end
316
+
317
+ end
318
+
319
+ class Article
320
+ include HappyMapper
321
+
322
+ include Content
323
+ has_one :entry, String
324
+ end
325
+
326
+ class Gallery
327
+ include HappyMapper
328
+
329
+ include Content
330
+ has_many :photos, String
331
+ end
332
+
333
+
334
+ 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.
335
+
336
+
337
+ ## Subclasses
338
+ I ran into a case where I wanted to capture all the pictures that were directly under media, but not the ones contained within a gallery.
339
+
340
+ <media>
341
+ <gallery>
342
+ <picture>
343
+ <name>Burtie Sanchez</name>
344
+ <img>burtie01.png</img>
345
+ </picture>
346
+ </gallery>
347
+ <picture>
348
+ <name>Unsorted Photo</name>
349
+ <img>bestfriends.png</img>
350
+ </picture>
351
+ </media>
352
+
353
+ The following `Media` class is where I started:
354
+
355
+ require 'happymapper'
356
+
357
+ class Media
358
+ include HappyMapper
359
+
360
+ has_many :galleries, Gallery, :tag => 'gallery'
361
+ has_many :pictures, Picture, :tag => 'picture'
362
+ end
363
+
364
+ However when I parsed the media xml the number of pictures returned to me was 2, not 1.
365
+
366
+ pictures = Media.parse(MEDIA_XML,:single => true).pictures
367
+ pictures.length.should == 1 # => Failed Expectation
368
+
369
+ I was mistaken and that is because, by default the mappings are assigned XPATH './/' which is requiring all the elements no matter where they can be found. To override this you can specify an XPATH value for your defined elements.
370
+
371
+ has_many :pictures, Picture, :tag => 'picture', :xpath => '/media'
372
+
373
+ `/media` states that we are only interested in pictures that can be found directly under the media element. So when we parse again we will have only our one element.
374
+
375
+
376
+ ## Namespaces
377
+
378
+ Obviously your XML and these trivial examples are easy to map and parse because they lack the treacherous namespaces that befall most XML files.
379
+
380
+ Perhaps our `address` XML is really swarming with namespaces:
381
+
382
+ <prefix:address location='home' xmlns:prefix="http://www.unicornland.com/prefix">
383
+ <prefix:street>Milchstrasse</prefix:street>
384
+ <prefix:street>Another Street</prefix:street>
385
+ <prefix:housenumber>23</prefix:housenumber>
386
+ <prefix:postcode>26131</prefix:postcode>
387
+ <prefix:city>Oldenburg</prefix:city>
388
+ <prefix:country code="de">Germany</prefix:country>
389
+ </prefix:address>
390
+
391
+ Here again is our address example with a made up namespace called `prefix` that comes direct to use from unicornland, a very magical place indeed. Well we are going to have to do some work on our class definition and that simply adding this one liner to the `Address` class:
392
+
393
+ class Address
394
+ include HappyMapper
395
+
396
+ tag 'address'
397
+ namespace 'prefix'
398
+ # ... rest of the code ...
399
+ end
400
+
401
+ Of course, if that is too easy for you, you can append a `:namespace => 'prefix` to every one of the elements that you defined.
402
+
403
+ has_many :street, String, :tag => 'street', :namespace => 'prefix'
404
+ element :postcode, String, :tag => 'postcode', :namespace => 'prefix'
405
+ element :housenumber, String, :tag => 'housenumber', :namespace => 'prefix'
406
+ element :city, String, :tag => 'city', :namespace => 'prefix'
407
+ element :country, Country, :tag => 'country', :namespace => 'prefix'
408
+
409
+ I definitely recommend the former, as it saves you a whole hell of lot of typing. However, there are times when appending a namespace to an element declaration is important and that is when it has a different namespace then `namespsace 'prefix'`.
410
+
411
+ Imagine that our `country` actually belonged to a completely different namespace.
412
+
413
+ <prefix:address location='home' xmlns:prefix="http://www.unicornland.com/prefix"
414
+ xmlns:prefix="http://www.trollcountry.com/different">
415
+ <prefix:street>Milchstrasse</prefix:street>
416
+ <prefix:street>Another Street</prefix:street>
417
+ <prefix:housenumber>23</prefix:housenumber>
418
+ <prefix:postcode>26131</prefix:postcode>
419
+ <prefix:city>Oldenburg</prefix:city>
420
+ <different:country code="de">Germany</different:country>
421
+ </prefix:address>
422
+
423
+ Well we would need to specify that namespace:
424
+
425
+ element :country, Country, :tag => 'country', :namespace => 'different'
426
+
427
+ With that we should be able to parse as we once did.
428
+
429
+ ## Saving to XML
430
+
431
+ Saving a class to XML is as easy as calling `#to_xml`. The end result will be the current state of your object represented as xml. Let's cover some details that are sometimes necessary and features present to make your life easier.
432
+
433
+
434
+ ### :on_save
435
+
436
+ When you are saving data to xml it is often important to change or manipulate data to a particular format. For example, a time object:
437
+
438
+ has_one :published_time, Time, :on_save => lambda {|time| time.strftime("%H:%M:%S") if time }
439
+
440
+ Here we add the options `:on_save` and specify a lambda which will be executed on the method call to `:published_time`.
441
+
442
+ ### :state_when_nil
443
+
444
+ When an element contains a nil value, or perhaps the result of the :on_save lambda correctly results in a nil value you will be happy that the element will not appear in the resulting XML. However, there are time when you will want to see that element and that's when `:state_when_nil` is there for you.
445
+
446
+ has_one :favorite_color, String, :state_when_nil => true
447
+
448
+ The resulting XML will include the 'favorite_color' element even if the favorite color has not been specified.
449
+
450
+ ### :read_only
451
+
452
+ When an element, attribute, or text node is a value that you have no interest in
453
+ saving to XML, you can ensure that takes place by stating that it is `read only`.
454
+
455
+ has_one :modified, Boolean, :read_only => true
456
+ attribute :temporary, Boolean, :read_only => true
457
+
458
+ This is useful if perhaps the incoming XML is different than the out-going XML.
459
+
460
+ ### namespaces
461
+
462
+ While parsing the XML only required you to simply specify the prefix of the namespace you wanted to parse, when you persist to xml you will need to define your namespaces so that they are correctly captured.
463
+
464
+ class Address
465
+ include HappyMapper
466
+
467
+ register_namespace 'prefix', 'http://www.unicornland.com/prefix'
468
+ register_namespace 'different', 'http://www.trollcountry.com/different'
469
+
470
+ tag 'address'
471
+ namespace 'prefix'
472
+
473
+ has_many :street, String
474
+ element :postcode, String
475
+ element :housenumber, String
476
+ element :city, String
477
+ element :country, Country, :tag => 'country', :namespace => 'different'
478
+
479
+ end
data/TODO ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ module HappyMapper
2
+ class Attribute < Item; end
3
+ end
@@ -0,0 +1,3 @@
1
+ module HappyMapper
2
+ class Element < Item; end
3
+ end