google4r 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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