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