google4r-checkout 0.1.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/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