google4r 0.0.1

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.
Files changed (39) hide show
  1. data/CHANGES +5 -0
  2. data/LICENSE +22 -0
  3. data/README +75 -0
  4. data/lib/google4r/checkout.rb +36 -0
  5. data/lib/google4r/checkout/commands.rb +267 -0
  6. data/lib/google4r/checkout/frontend.rb +100 -0
  7. data/lib/google4r/checkout/notifications.rb +533 -0
  8. data/lib/google4r/checkout/shared.rb +501 -0
  9. data/lib/google4r/checkout/xml_generation.rb +271 -0
  10. data/lib/google4r/maps.rb +174 -0
  11. data/test/checkout/integration/checkout_command_test.rb +103 -0
  12. data/test/checkout/unit/address_test.rb +131 -0
  13. data/test/checkout/unit/area_test.rb +41 -0
  14. data/test/checkout/unit/checkout_command_test.rb +112 -0
  15. data/test/checkout/unit/checkout_command_xml_generator_test.rb +187 -0
  16. data/test/checkout/unit/command_test.rb +126 -0
  17. data/test/checkout/unit/flat_rate_shipping_test.rb +114 -0
  18. data/test/checkout/unit/frontend_test.rb +63 -0
  19. data/test/checkout/unit/item_test.rb +159 -0
  20. data/test/checkout/unit/marketing_preferences_test.rb +65 -0
  21. data/test/checkout/unit/merchant_code_test.rb +122 -0
  22. data/test/checkout/unit/new_order_notification_test.rb +115 -0
  23. data/test/checkout/unit/notification_acknowledgement_test.rb +43 -0
  24. data/test/checkout/unit/notification_handler_test.rb +93 -0
  25. data/test/checkout/unit/order_adjustment_test.rb +95 -0
  26. data/test/checkout/unit/order_state_change_notification_test.rb +159 -0
  27. data/test/checkout/unit/pickup_shipping_test.rb +70 -0
  28. data/test/checkout/unit/private_data_parser_test.rb +68 -0
  29. data/test/checkout/unit/shipping_adjustment_test.rb +100 -0
  30. data/test/checkout/unit/shipping_method_test.rb +41 -0
  31. data/test/checkout/unit/shopping_cart_test.rb +146 -0
  32. data/test/checkout/unit/tax_rule_test.rb +65 -0
  33. data/test/checkout/unit/tax_table_test.rb +82 -0
  34. data/test/checkout/unit/us_country_area_test.rb +76 -0
  35. data/test/checkout/unit/us_state_area_test.rb +70 -0
  36. data/test/checkout/unit/us_zip_area_test.rb +66 -0
  37. data/test/maps/geocoder_test.rb +143 -0
  38. data/var/cacert.pem +7815 -0
  39. metadata +100 -0
@@ -0,0 +1,533 @@
1
+ #--
2
+ # Project: google4r
3
+ # File: lib/google4r/checkout/notifications.rb
4
+ # Author: Manuel Holtgrewe <purestorm at ggnore dot net>
5
+ # Copyright: (c) 2007 by Manuel Holtgrewe
6
+ # License: MIT License as follows:
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person obtaining
9
+ # a copy of this software and associated documentation files (the
10
+ # "Software"), to deal in the Software without restriction, including
11
+ # without limitation the rights to use, copy, modify, merge, publish,
12
+ # distribute, sublicense, and/or sell copies of the Software, and to permit
13
+ # persons to whom the Software is furnished to do so, subject to the
14
+ # following conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be included
17
+ # in all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
22
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
23
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
24
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
25
+ # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+ #++
27
+ # This file contains the classes and modules that are used in the notification
28
+ # handling code.
29
+
30
+ require 'rexml/document'
31
+
32
+ module Google4R #:nodoc:
33
+ module Checkout #:nodoc:
34
+ # Thrown by Notification on unimplemented and unknown notification from Google.
35
+ class UnknownNotificationType < Exception
36
+ end
37
+
38
+ # Use this objects of this class to tell Google that everything went fine.
39
+ #
40
+ # === Example
41
+ #
42
+ # render :text => NotificationAcknowledgement.to_xml
43
+ class NotificationAcknowledgement
44
+ def to_xml
45
+ %q{<?xml version="1.0" encoding="UTF-8"?><notification-acknowledgment xmlns="http://checkout.google.com/schema/2"/>}
46
+ end
47
+ end
48
+
49
+ # This class expects the message sent by Google. It parses the XMl document and returns
50
+ # the appropriate Notification. If the notification sent by Google is invalid then a
51
+ # UnknownNotificationType is raised that you should catch and then send a 404 to Google
52
+ # to indicate that the notification handler has not been implemented yet.
53
+ #
54
+ # See http://code.google.com/apis/checkout/developer/index.html#notification_api for
55
+ # details.
56
+ #--
57
+ # TODO
58
+ #
59
+ # * Check HTTP Auth Basic - in Rails?
60
+ #++
61
+ class NotificationHandler
62
+ # The Frontend object that created this NotificationHandler
63
+ attr_accessor :frontend
64
+
65
+ # Create a new NotificationHandler and assign value of the parameter frontend to
66
+ # the frontend attribute.
67
+ def initialize(frontend)
68
+ @frontend = frontend
69
+ end
70
+
71
+ # Parses the given xml_str and returns the appropriate *Notification class. At the
72
+ # moment, only NewOrderNotification and OrderStateChangeNotification objects can be
73
+ # returned.
74
+ def handle(xml_str)
75
+ root = REXML::Document.new(xml_str).root
76
+
77
+ case root.name
78
+ when 'new-order-notification' then
79
+ NewOrderNotification.create_from_element(root, frontend)
80
+ when 'order-state-change-notification' then
81
+ OrderStateChangeNotification.create_from_element(root, frontend)
82
+ else
83
+ raise UnknownNotificationType, "Unknown notification type: #{root.name}"
84
+ end
85
+ end
86
+ end
87
+
88
+ # Google Checkout sends <new-order-notification> messages to the web service when a new
89
+ # order has been successfully filed with Google Checkout. These messages will be parsed
90
+ # into NewOrderNotification instances.
91
+ class NewOrderNotification
92
+ # The frontend this notification belongs to.
93
+ attr_accessor :frontend
94
+
95
+ # The serial number of the new order notification (String).
96
+ attr_accessor :serial_number
97
+
98
+ # The Google order number the new order notification belongs to (String).
99
+ attr_accessor :google_order_number
100
+
101
+ # The buyer's billing address (Address).
102
+ attr_accessor :buyer_billing_address
103
+
104
+ # The buyer's shipping adress (Address).
105
+ attr_accessor :buyer_shipping_address
106
+
107
+ # The buyer's ID from Google Checkout (String).
108
+ attr_accessor :buyer_id
109
+
110
+ # The buyer's marketing preferences (MarketingPreferences).
111
+ attr_accessor :marketing_preferences
112
+
113
+ # The order's financial order state (String, one of FinancialOrderState::*).
114
+ attr_accessor :financial_order_state
115
+
116
+ # The order's fulfillment state (String, one of FulfillmentOrderState::*).
117
+ attr_accessor :fulfillment_order_state
118
+
119
+ # The order's number at Google Checkout (String).
120
+ attr_accessor :google_order_number
121
+
122
+ # The order's total adjustment (OrderAdjustment).
123
+ attr_accessor :order_adjustment
124
+
125
+ # The order's total amount (Money).
126
+ attr_accessor :order_total
127
+
128
+ # The order's shopping cart (ShoppingCart)
129
+ attr_accessor :shopping_cart
130
+
131
+ # The order's timestamp (Time), also see #timestamp=
132
+ attr_accessor :timestamp
133
+
134
+ # Set the order's timestamp (Time). When the timestamp is set then the tax tables valid
135
+ # at the given point of time are set into the attribute tax tables from the frontend's
136
+ # tax_table_factory.
137
+ def timestamp=(time)
138
+ @timestamp = time
139
+ @tax_tables = frontend.tax_table_factory.effective_tax_tables_at(time)
140
+ end
141
+
142
+ # The tax tables for the items in the order notification.
143
+ attr_reader :tax_tables
144
+
145
+ # Sets the frontend attribute to the value of the frontend parameter.
146
+ def initialize(frontend)
147
+ @frontend = frontend
148
+ end
149
+
150
+ # Factory method to create a new CheckoutNotification object from the REXML:Element object
151
+ #
152
+ # Raises NoMethodError and RuntimeError exceptions if the given element misses required
153
+ # elements.
154
+ #
155
+ # You have to pass in the Frontend class this notification belongs to.
156
+ def self.create_from_element(element, frontend)
157
+ result = NewOrderNotification.new(frontend)
158
+
159
+ result.timestamp = Time.parse(element.elements['timestamp'].text)
160
+ result.serial_number = element.elements['@serial-number'].value
161
+ result.google_order_number = element.elements['google-order-number'].text
162
+ result.buyer_billing_address = Address.create_from_element(element.elements['buyer-billing-address'])
163
+ result.buyer_shipping_address = Address.create_from_element(element.elements['buyer-shipping-address'])
164
+ result.buyer_id = element.elements['buyer-id'].text
165
+ result.marketing_preferences = MarketingPreferences.create_from_element(element.elements['buyer-marketing-preferences'])
166
+ result.financial_order_state = element.elements['financial-order-state'].text
167
+ result.fulfillment_order_state = element.elements['fulfillment-order-state'].text
168
+ result.order_adjustment = OrderAdjustment.create_from_element(element.elements['order-adjustment'])
169
+ result.shopping_cart = ShoppingCart.create_from_element(element.elements['shopping-cart'], result)
170
+
171
+ amount = (element.elements['order-total'].text.to_f * 100).to_i rescue nil # TODO: this will break for currencies where 100c != 1d
172
+ currency = element.elements['order-total/@currency'].value rescue nil
173
+ result.order_total = Money.new(amount, currency)
174
+
175
+ return result
176
+ end
177
+ end
178
+
179
+ # GoogleCheckout sends <order-change-notification> messages to the web service when the
180
+ # order's state changes. They will get parsed into OrderStateChangeNotification objects.
181
+ class OrderStateChangeNotification
182
+ # The frontend this notification belongs to.
183
+ attr_accessor :frontend
184
+
185
+ # The serial number of the notification (String).
186
+ attr_accessor :serial_number
187
+
188
+ # The order number in Google's database (String).
189
+ attr_accessor :google_order_number
190
+
191
+ # The previous financial state of the order (String, one of FinancialOrderState::*).
192
+ attr_accessor :previous_financial_order_state
193
+
194
+ # The new financial state of the order (String, one of FinancialOrderState::*).
195
+ attr_accessor :new_financial_order_state
196
+
197
+ # The previous fulfillment state of the order (String, one of FulfillmentOrderState::*).
198
+ attr_accessor :previous_fulfillment_order_state
199
+
200
+ # The new fulfillment state of the order (String, one of FulfillmentOrderState::*).
201
+ attr_accessor :new_fulfillment_order_state
202
+
203
+ # The reason for the change (String, can be nil).
204
+ attr_accessor :reason
205
+
206
+ # The timestamp of the notification. Also see #timestamp=
207
+ attr_accessor :timestamp
208
+
209
+ # Set the order's timestamp (Time). When the timestamp is set then the tax tables valid
210
+ # at the given point of time are set into the attribute tax tables from the frontend's
211
+ # tax_table_factory.
212
+ def timestamp=(time)
213
+ @timestamp = time
214
+ @tax_tables = frontend.tax_table_factory.effective_tax_tables_at(time)
215
+ end
216
+
217
+ # The tax tables for the items in the order notification.
218
+ attr_reader :tax_tables
219
+
220
+ # Sets the frontend attribute to the value of the frontend parameter.
221
+ def initialize(frontend)
222
+ @frontend = frontend
223
+ end
224
+
225
+ # Factory methdo that creates a new OrderStateChangeNotification from an REXML::Element instance.
226
+ # Use this to create instances of OrderStateChangeNotification.
227
+ #
228
+ # Raises NoMethodError and RuntimeError exceptions if the given element misses required
229
+ # elements.
230
+ def self.create_from_element(element, frontend)
231
+ result = OrderStateChangeNotification.new(frontend)
232
+
233
+ result.timestamp = Time.parse(element.elements['timestamp'].text)
234
+
235
+ result.serial_number = element.elements['@serial-number'].value
236
+ result.google_order_number = element.elements['google-order-number'].text
237
+ result.new_financial_order_state = element.elements['new-financial-order-state'].text
238
+ result.previous_financial_order_state = element.elements['previous-financial-order-state'].text
239
+ result.new_fulfillment_order_state = element.elements['new-fulfillment-order-state'].text
240
+ result.previous_fulfillment_order_state = element.elements['previous-fulfillment-order-state'].text
241
+ result.reason = element.elements['reason'].text rescue nil
242
+
243
+ return result
244
+ end
245
+ end
246
+
247
+ # Container for the valid financial order states as defined in the
248
+ # Google Checkout API.
249
+ module FinancialOrderState
250
+ REVIEWING = "REVIEWING".freeze
251
+ CHARGEABLE = "CHARGEABLE".freeze
252
+ CHARGING = "CHARGING".freeze
253
+ CHARGED = "CHARGED".freeze
254
+ PAYMENT_DECLINED = "PAYMENT_DECLINED".freeze
255
+ CANCELLED = "CANCELLED".freeze
256
+ CANCELLED_BY_GOOGLE = "CANCELLED_BY_GOOGLE".freeze
257
+ end
258
+
259
+ # Container for the valid fulfillment order states as defined in the
260
+ # Google Checkout API.
261
+ module FulfillmentOrderState
262
+ NEW = "NEW".freeze
263
+ PROCESSING = "PROCESSING".freeze
264
+ DELIVERED = "DELIVERED".freeze
265
+ WILL_NOT_DELIVER = "WILL_NOT_DELIVER".freeze
266
+ end
267
+
268
+ # Address instances are used in NewOrderNotification objects for the buyer's billing
269
+ # and buyer's shipping address.
270
+ class Address
271
+ # Contact name (String, optional).
272
+ attr_accessor :contact_name
273
+
274
+ # Second Address line (String).
275
+ attr_accessor :address1
276
+
277
+ # Second Address line (String optional).
278
+ attr_accessor :address2
279
+
280
+ # The buyer's city name (String).
281
+ attr_accessor :city
282
+
283
+ # The buyer's company name (String; optional).
284
+ attr_accessor :company_name
285
+
286
+ # The buyer's country code (String, 2 chars, ISO 3166).
287
+ attr_accessor :country_code
288
+
289
+ # The buyer's email address (String; optional).
290
+ attr_accessor :email
291
+
292
+ # The buyer's phone number (String; optional).
293
+ attr_accessor :fax
294
+
295
+ # The buyer's phone number (String; Optional, can be enforced in CheckoutCommand).)
296
+ attr_accessor :phone
297
+
298
+ # The buyers postal/zip code (String).
299
+ attr_accessor :postal_code
300
+
301
+ # The buyer's geographical region (String).
302
+ attr_accessor :region
303
+
304
+ # Creates a new Address from the given REXML::Element instance.
305
+ def self.create_from_element(element)
306
+ result = Address.new
307
+
308
+ result.address1 = element.elements['address1'].text
309
+ result.address2 = element.elements['address2'].text rescue nil
310
+ result.city = element.elements['city'].text
311
+ result.company_name = element.elements['company-name'].text rescue nil
312
+ result.contact_name = element.elements['contact-name'].text rescue nil
313
+ result.country_code = element.elements['country-code'].text
314
+ result.email = element.elements['email'].text rescue nil
315
+ result.fax = element.elements['fax'].text rescue nil
316
+ result.phone = element.elements['phone'].text rescue nil
317
+ result.postal_code = element.elements['postal-code'].text
318
+ result.region = element.elements['region'].text
319
+
320
+ return result
321
+ end
322
+ end
323
+
324
+ # The marketing preferences of a customer.
325
+ class MarketingPreferences
326
+ # Boolean, true iff the customer wants to receive emails.
327
+ attr_accessor :email_allowed
328
+
329
+ # Creates a new MarketingPreferences object from a given REXML::Element instance.
330
+ def self.create_from_element(element)
331
+ result = MarketingPreferences.new
332
+
333
+ result.email_allowed = (element.elements['email-allowed'].text.downcase == 'true')
334
+
335
+ return result
336
+ end
337
+ end
338
+
339
+ # MerchantCodes represent gift certificates or coupons that have been used in an order.
340
+ #
341
+ # Only used with Merchant Calculations.
342
+ class MerchantCode
343
+ GIFT_CERTIFICATE = "GIFT_CERTIFICATE".freeze
344
+ COUPON = "COUPON".freeze
345
+
346
+ # The type of the adjustment. Can be one of GIFT_CERTIFICATE and COUPON.
347
+ attr_accessor :type
348
+
349
+ # The adjustment's code (String).
350
+ attr_accessor :code
351
+
352
+ # The amount of money that has been calculated as the adjustment's worth (Money, optional).
353
+ attr_accessor :calculated_amount
354
+
355
+ # The amount of the adjustment that has been applied to the cart's total (Money).
356
+ attr_accessor :applied_amount
357
+
358
+ # The message associated with the direct adjustment (String, optional).
359
+ attr_accessor :message
360
+
361
+ # Creates the MerchantCode from the given REXML::Element instance. The Element's
362
+ # name must be "gift-certificate-adjustment" or "coupon-adjustment".
363
+ def self.create_from_element(element)
364
+ result = MerchantCode.new
365
+
366
+ hash = Hash.new
367
+ hash[:type] =
368
+ case element.name
369
+ when 'gift-certificate-adjustment' then
370
+ GIFT_CERTIFICATE
371
+ when 'coupon-adjustment' then
372
+ COUPON
373
+ else
374
+ raise "Invalid tag name: #{element.name} in \n—-\n#{element.to_s}\n—-."
375
+ end
376
+ hash[:code] = element.elements['code'].text
377
+ hash[:calculated_amount] = (element.elements['calculated-amount'].text.to_f * 100).to_i rescue nil # TODO: this will break for currencies where 100c != 1d
378
+ hash[:calculated_amount_currency] = element.elements['calculated-amount/@currency'].value rescue nil
379
+ hash[:applied_amount] = (element.elements['applied-amount'].text.to_f * 100).to_i # TODO: this will break for currencies where 100c != 1d
380
+ hash[:applied_amount_currency] = element.elements['applied-amount/@currency'].value
381
+ hash[:message] = element.elements['message'].text rescue nil
382
+
383
+ result.type = hash[:type]
384
+ result.code = hash[:code]
385
+ if not hash[:calculated_amount].nil? then
386
+ result.calculated_amount = Money.new(hash[:calculated_amount], hash[:calculated_amount_currency])
387
+ end
388
+ result.applied_amount = Money.new(hash[:applied_amount], hash[:applied_amount_currency])
389
+ result.message = hash[:message] unless hash[:message].nil?
390
+
391
+ return result
392
+ end
393
+ end
394
+
395
+ # ShippingAdjustments represent the chosen shipping method.
396
+ class ShippingAdjustment
397
+ MERCHANT_CALCULATED = "MERCHANT_CALCULATED".freeze
398
+ FLAT_RATE = "FLAT_RATE".freeze
399
+ PICKUP = "PICKUP".freeze
400
+
401
+ # The type of the shipping adjustment, one of MERCHANT_CALCULATED, FLAT_RATE
402
+ # PICKUP.
403
+ attr_accessor :type
404
+
405
+ # The name of the shipping adjustment.
406
+ attr_accessor :name
407
+
408
+ # The cost of the selected shipping (Money).
409
+ attr_accessor :cost
410
+
411
+ # Creates a new ShippingAdjustment object from a REXML::Element object.
412
+ #
413
+ # Can raise a RuntimeException if the given Element is invalid.
414
+ def self.create_from_element(element)
415
+ result = ShippingAdjustment.new
416
+
417
+ hash = Hash.new
418
+ hash[:type] =
419
+ case element.name
420
+ when 'flat-rate-shipping-adjustment' then
421
+ FLAT_RATE
422
+ when 'pickup-shipping-adjustment' then
423
+ PICKUP
424
+ when 'merchant-calculated-shipping-adjustment' then
425
+ MERCHANT_CALCULATED
426
+ else
427
+ raise "Unexpected shipping adjustment '#{element.name}'"
428
+ end
429
+ hash[:name] = element.elements['shipping-name'].text
430
+ hash[:cost] = (element.elements['shipping-cost'].text.to_f * 100).to_i # TODO: this will break for currencies where 100c != 1d
431
+ hash[:cost_currency] = element.elements['shipping-cost/@currency'].value
432
+
433
+ result.type = hash[:type]
434
+ result.name = hash[:name]
435
+ result.cost = Money.new(hash[:cost], hash[:cost_currency])
436
+
437
+ return result
438
+ end
439
+ end
440
+
441
+ # OrderAdjustment objects contain the adjustments (i.e. the entities in the cart that
442
+ # represent positive and negative amounts (at the moment Google Checkout support coupons,
443
+ # gift certificates and shipping)).
444
+ class OrderAdjustment
445
+ # The <adjustment-total> tag contains the total adjustment to an order total based
446
+ # on tax, shipping, gift certificates and coupon codes (optional).
447
+ attr_accessor :adjustment_total
448
+
449
+ # Boolean, true iff the merchant calculations have been successful (optional).
450
+ attr_accessor :merchant_calculation_successful
451
+
452
+ # Array of MerchantCode objects.
453
+ attr_accessor :merchant_codes
454
+
455
+ # The chosen ShippingAdjustment object for this order.
456
+ attr_accessor :shipping
457
+
458
+ # The total amount of tax that has been paid for this order (Money, optional).
459
+ attr_accessor :total_tax
460
+
461
+ # Creates a new OrderAdjustment from a given REXML::Element object.
462
+ def self.create_from_element(element)
463
+ result = OrderAdjustment.new
464
+
465
+ hash = Hash.new
466
+ hash[:total_tax] = (element.elements['total-tax'].text.to_f * 100).to_i rescue nil # TODO: this will break for currencies where 100c != 1d
467
+ hash[:total_tax_currency] = element.elements['total-tax/@currency'].value rescue nil
468
+
469
+ shipping_element = element.elements["shipping/*"]
470
+ hash[:shipping] = ShippingAdjustment.create_from_element(shipping_element)
471
+
472
+ hash[:merchant_codes] = Array.new
473
+ element.elements.each(%q{merchant-codes/*}) do |code_elem|
474
+ hash[:merchant_codes] << MerchantCode.create_from_element(code_elem)
475
+ end
476
+
477
+ hash[:merchant_calculation_successful] = (element.elements['merchant-calculation-successful'].text.downcase == 'true') rescue nil
478
+
479
+ hash[:adjustment_total] = (element.elements['adjustment-total'].text.to_f * 100).to_i rescue nil # TODO: this will break for currencies where 100c != 1d
480
+ hash[:adjustment_total_currency] = element.elements['adjustment-total/@currency'].value rescue nil
481
+
482
+ result.total_tax = Money.new(hash[:total_tax], hash[:total_tax_currency]) unless hash[:total_tax].nil?
483
+ result.adjustment_total = Money.new(hash[:adjustment_total], hash[:adjustment_total_currency]) unless hash[:adjustment_total].nil?
484
+ result.merchant_codes = hash[:merchant_codes] # no unless since the Array is initialized
485
+ result.merchant_calculation_successful = hash[:merchant_calculation_successful]
486
+ result.shipping = hash[:shipping]
487
+
488
+ return result
489
+ end
490
+ end
491
+
492
+ # Class with static methods to parse the <merchant-private(-item)-data> tags.
493
+ class PrivateDataParser
494
+ # Returns a Hash with the representation of the structure in
495
+ # item/merchant-private-item-data.
496
+ def self.element_to_value(element)
497
+ # The return value; We will iterate over all children below. When we see a REXML::Element
498
+ # child then we set result to a Hash if it is nil. After the result is a Hash, we will
499
+ # ignore all REXML::Text children.
500
+ # Otherwise, result will be set to the REXML::Text node's value if it is not whitespace
501
+ # only.
502
+ result = nil
503
+
504
+ element.each_child do |child|
505
+ case child
506
+ when REXML::Element
507
+ result ||= Hash.new
508
+ child_value = self.element_to_value(child)
509
+
510
+ # <foo>bar</foo> becomes 'foo' => 'bar
511
+ # <foo>foo</foo><foo>bar</foo> becomes 'foo' => [ 'foo', 'bar' ]
512
+ if result[child.name].nil? then
513
+ result[child.name] = child_value
514
+ elsif result[child.name].kind_of?(Array) then
515
+ result[child.name] << child_value
516
+ else
517
+ tmp = result[child.name]
518
+ result[child.name] = [ tmp, child_value ]
519
+ end
520
+ when REXML::Text
521
+ next if result.kind_of?(Hash) # ignore text if we already found a tag
522
+ str = child.value.strip
523
+ result = str if str.length > 0
524
+ else
525
+ # ignore
526
+ end
527
+ end
528
+
529
+ return result
530
+ end
531
+ end
532
+ end
533
+ end