shippinglogic 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/.document +5 -0
  2. data/CHANGELOG.rdoc +55 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +175 -0
  5. data/Rakefile +37 -0
  6. data/VERSION.yml +5 -0
  7. data/init.rb +1 -0
  8. data/lib/shippinglogic.rb +3 -0
  9. data/lib/shippinglogic/attributes.rb +121 -0
  10. data/lib/shippinglogic/error.rb +22 -0
  11. data/lib/shippinglogic/fedex.rb +84 -0
  12. data/lib/shippinglogic/fedex/cancel.rb +47 -0
  13. data/lib/shippinglogic/fedex/enumerations.rb +348 -0
  14. data/lib/shippinglogic/fedex/error.rb +47 -0
  15. data/lib/shippinglogic/fedex/rate.rb +229 -0
  16. data/lib/shippinglogic/fedex/request.rb +134 -0
  17. data/lib/shippinglogic/fedex/response.rb +72 -0
  18. data/lib/shippinglogic/fedex/service.rb +11 -0
  19. data/lib/shippinglogic/fedex/ship.rb +238 -0
  20. data/lib/shippinglogic/fedex/signature.rb +68 -0
  21. data/lib/shippinglogic/fedex/track.rb +124 -0
  22. data/lib/shippinglogic/proxy.rb +23 -0
  23. data/lib/shippinglogic/service.rb +42 -0
  24. data/lib/shippinglogic/ups.rb +83 -0
  25. data/lib/shippinglogic/ups/cancel.rb +52 -0
  26. data/lib/shippinglogic/ups/enumerations.rb +56 -0
  27. data/lib/shippinglogic/ups/error.rb +42 -0
  28. data/lib/shippinglogic/ups/label.rb +50 -0
  29. data/lib/shippinglogic/ups/rate.rb +228 -0
  30. data/lib/shippinglogic/ups/request.rb +49 -0
  31. data/lib/shippinglogic/ups/response.rb +58 -0
  32. data/lib/shippinglogic/ups/service.rb +11 -0
  33. data/lib/shippinglogic/ups/ship_accept.rb +53 -0
  34. data/lib/shippinglogic/ups/ship_confirm.rb +170 -0
  35. data/lib/shippinglogic/ups/track.rb +118 -0
  36. data/lib/shippinglogic/validation.rb +32 -0
  37. data/shippinglogic.gemspec +120 -0
  38. data/spec/attributes_spec.rb +67 -0
  39. data/spec/config/fedex_credentials.example.yml +4 -0
  40. data/spec/config/ups_credentials.example.yml +3 -0
  41. data/spec/error_spec.rb +43 -0
  42. data/spec/fedex/cancel_spec.rb +10 -0
  43. data/spec/fedex/error_spec.rb +26 -0
  44. data/spec/fedex/rate_spec.rb +87 -0
  45. data/spec/fedex/request_spec.rb +15 -0
  46. data/spec/fedex/responses/blank.xml +0 -0
  47. data/spec/fedex/responses/cancel_not_found.xml +7 -0
  48. data/spec/fedex/responses/failed_authentication.xml +7 -0
  49. data/spec/fedex/responses/malformed.xml +8 -0
  50. data/spec/fedex/responses/rate_defaults.xml +7 -0
  51. data/spec/fedex/responses/rate_insurance.xml +9 -0
  52. data/spec/fedex/responses/rate_no_services.xml +7 -0
  53. data/spec/fedex/responses/rate_non_custom_packaging.xml +7 -0
  54. data/spec/fedex/responses/ship_defaults.xml +7 -0
  55. data/spec/fedex/responses/ship_with_no_signature.xml +7 -0
  56. data/spec/fedex/responses/signature_defaults.xml +7 -0
  57. data/spec/fedex/responses/track_defaults.xml +7 -0
  58. data/spec/fedex/responses/unexpected.xml +1 -0
  59. data/spec/fedex/service_spec.rb +19 -0
  60. data/spec/fedex/ship_spec.rb +37 -0
  61. data/spec/fedex/signature_spec.rb +11 -0
  62. data/spec/fedex/spec_helper.rb +84 -0
  63. data/spec/fedex/track_spec.rb +37 -0
  64. data/spec/fedex_spec.rb +16 -0
  65. data/spec/lib/interceptor.rb +17 -0
  66. data/spec/proxy_spec.rb +42 -0
  67. data/spec/service_spec.rb +23 -0
  68. data/spec/spec_helper.rb +12 -0
  69. data/spec/ups/responses/blank.xml +0 -0
  70. data/spec/ups/responses/track_defaults.xml +2 -0
  71. data/spec/ups/spec_helper.rb +43 -0
  72. data/spec/ups_spec.rb +16 -0
  73. data/spec/validation_spec.rb +49 -0
  74. metadata +163 -0
@@ -0,0 +1,47 @@
1
+ module Shippinglogic
2
+ class FedEx
3
+ # If FedEx responds with an error, we try our best to pull the pertinent information out of that
4
+ # response and raise it with this object. Any time FedEx says there is a problem an object of this
5
+ # class will be raised.
6
+ #
7
+ # === Tip
8
+ #
9
+ # If you want to see the raw request / respose catch the error object and call the request / response method. Ex:
10
+ #
11
+ # begin
12
+ # # my fedex code
13
+ # rescue Shippinglogic::FedEx::Error => e
14
+ # # do whatever you want here, just do:
15
+ # # e.request
16
+ # # e.response
17
+ # # to get the raw response from fedex
18
+ # end
19
+ class Error < Shippinglogic::Error
20
+ def initialize(request, response)
21
+ super
22
+
23
+ if response.blank?
24
+ add_error("The response from FedEx was blank.")
25
+ elsif !response.is_a?(Hash)
26
+ add_error("The response from FedEx was malformed and was not in a valid XML format.")
27
+ elsif notifications = response[:notifications]
28
+ notifications = notifications.is_a?(Array) ? notifications : [notifications]
29
+ notifications.delete_if { |notification| Response::SUCCESSFUL_SEVERITIES.include?(notification[:severity]) }
30
+ notifications.each { |notification| add_error(notification[:message], notification[:code]) }
31
+ elsif response[:"soapenv:fault"] && detail = response[:"soapenv:fault"][:detail][:"con:fault"]
32
+ add_error(detail[:"con:reason"], detail[:"con:error_code"])
33
+
34
+ if detail[:"con:details"] && detail[:"con:details"][:"con1:validation_failure_detail"] && messages = detail[:"con:details"][:"con1:validation_failure_detail"][:"con1:message"]
35
+ messages = messages.is_a?(Array) ? messages : [messages]
36
+ messages.each { |message| add_error(message) }
37
+ end
38
+ else
39
+ add_error(
40
+ "There was a problem with your fedex request, and we couldn't locate a specific error message. This means your response " +
41
+ "was in an unexpected format. You might try glancing at the raw response by using the 'response' method on this error object."
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,229 @@
1
+ module Shippinglogic
2
+ class FedEx
3
+ # An interface to the rate services provided by FedEx. Allows you to get an array of rates from fedex for a shipment,
4
+ # or a single rate for a specific service.
5
+ #
6
+ # == Options
7
+ # === Shipper options
8
+ #
9
+ # * <tt>shipper_name</tt> - name of the shipper.
10
+ # * <tt>shipper_title</tt> - title of the shipper.
11
+ # * <tt>shipper_company_name</tt> - company name of the shipper.
12
+ # * <tt>shipper_phone_number</tt> - phone number of the shipper.
13
+ # * <tt>shipper_email</tt> - email of the shipper.
14
+ # * <tt>shipper_streets</tt> - street part of the address, separate multiple streets with a new line, dont include blank lines.
15
+ # * <tt>shipper_city</tt> - city part of the address.
16
+ # * <tt>shipper_state_</tt> - state part of the address, use state abreviations.
17
+ # * <tt>shipper_postal_code</tt> - postal code part of the address. Ex: zip for the US.
18
+ # * <tt>shipper_country</tt> - country code part of the address. FedEx expects abbreviations, but Shippinglogic will convert full names to abbreviations for you.
19
+ # * <tt>shipper_residential</tt> - a boolean value representing if the address is redential or not (default: false)
20
+ #
21
+ # === Recipient options
22
+ #
23
+ # * <tt>recipient_name</tt> - name of the recipient.
24
+ # * <tt>recipient_title</tt> - title of the recipient.
25
+ # * <tt>recipient_company_name</tt> - company name of the recipient.
26
+ # * <tt>recipient_phone_number</tt> - phone number of the recipient.
27
+ # * <tt>recipient_email</tt> - email of the recipient.
28
+ # * <tt>recipient_streets</tt> - street part of the address, separate multiple streets with a new line, dont include blank lines.
29
+ # * <tt>recipient_city</tt> - city part of the address.
30
+ # * <tt>recipient_state</tt> - state part of the address, use state abreviations.
31
+ # * <tt>recipient_postal_code</tt> - postal code part of the address. Ex: zip for the US.
32
+ # * <tt>recipient_country</tt> - country code part of the address. FedEx expects abbreviations, but Shippinglogic will convert full names to abbreviations for you.
33
+ # * <tt>recipient_residential</tt> - a boolean value representing if the address is redential or not (default: false)
34
+ #
35
+ # === Packaging options
36
+ #
37
+ # One thing to note is that FedEx does support multiple package shipments. The problem is that all of the packages must be identical.
38
+ # FedEx specifically notes in their documentation that mutiple package specifications are not allowed. So your only option for a
39
+ # multi package shipment is to increase the package_count option and keep the dimensions and weight the same for all packages. Then again,
40
+ # the documentation for the FedEx web services is terrible, so I could be wrong. Any tests I tried resulted in an error though.
41
+ #
42
+ # * <tt>packaging_type</tt> - one of Enumerations::PACKAGE_TYPES. (default: YOUR_PACKAGING)
43
+ # * <tt>package_count</tt> - the number of packages in your shipment. (default: 1)
44
+ # * <tt>package_weight</tt> - a single packages weight.
45
+ # * <tt>package_weight_units</tt> - either LB or KG. (default: LB)
46
+ # * <tt>package_length</tt> - a single packages length, only required if using YOUR_PACKAGING for packaging_type.
47
+ # * <tt>package_width</tt> - a single packages width, only required if using YOUR_PACKAGING for packaging_type.
48
+ # * <tt>package_height</tt> - a single packages height, only required if using YOUR_PACKAGING for packaging_type.
49
+ # * <tt>package_dimension_units</tt> - either IN or CM. (default: IN)
50
+ #
51
+ # === Monetary options
52
+ #
53
+ # * <tt>currency_type</tt> - the type of currency. (default: nil, because FedEx will default to your account preferences)
54
+ # * <tt>insured_value</tt> - the value you want to insure, if any. (default: nil)
55
+ # * <tt>payment_type</tt> - one of Enumerations::PAYMENT_TYPES. (default: SENDER)
56
+ # * <tt>payor_account_number</tt> - if the account paying for this ship is different than the account you specified then
57
+ # you can specify that here. (default: your account number)
58
+ # * <tt>payor_country</tt> - the country code for the account number. (default: US)
59
+ #
60
+ # === Delivery options
61
+ #
62
+ # * <tt>ship_time</tt> - a Time object representing when you want to ship the package. (default: Time.now)
63
+ # * <tt>service_type</tt> - one of Enumerations::SERVICE_TYPES, this is optional, leave this blank if you want a list of all
64
+ # available services. (default: nil)
65
+ # * <tt>delivery_deadline</tt> - whether or not to include estimated transit times. (default: true)
66
+ # * <tt>dropoff_type</tt> - one of Enumerations::DROP_OFF_TYPES. (default: REGULAR_PICKUP)
67
+ # * <tt>special_services_requested</tt> - any exceptions or special services FedEx needs to be aware of, this should be
68
+ # one or more of Enumerations::SPECIAL_SERVICES. (default: nil)
69
+ #
70
+ # === Misc options
71
+ #
72
+ # * <tt>rate_request_types</tt> - one or more of RATE_REQUEST_TYPES. (default: ACCOUNT)
73
+ # * <tt>include_transit_times</tt> - whether or not to include estimated transit times. (default: true)
74
+ #
75
+ # == Simple Example
76
+ #
77
+ # Here is a very simple example. Mix and match the options above to get more accurate rates:
78
+ #
79
+ # fedex = Shippinglogic::FedEx.new(key, password, account, meter)
80
+ # rates = fedex.rate(
81
+ # :shipper_postal_code => "10007",
82
+ # :shipper_country => "US",
83
+ # :recipient_postal_code => "75201",
84
+ # :recipient_country => "US",
85
+ # :package_weight => 24,
86
+ # :package_length => 12,
87
+ # :package_width => 12,
88
+ # :package_height => 12
89
+ # )
90
+ #
91
+ # rates.first
92
+ # #<Shippinglogic::FedEx::Rates::Rate @currency="USD", @name="First Overnight", @cost=#<BigDecimal:19ea290,'0.7001E2',8(8)>,
93
+ # @deadline=Fri Aug 07 08:00:00 -0400 2009, @type="FIRST_OVERNIGHT", @saturday=false>
94
+ #
95
+ # # to show accessor methods
96
+ # rates.first.name
97
+ # # => "First Overnight"
98
+ class Rate < Service
99
+ # Each rate result is an object of this class
100
+ class Service; attr_accessor :name, :type, :saturday, :delivered_by, :speed, :rate, :currency; end
101
+
102
+ VERSION = {:major => 6, :intermediate => 0, :minor => 0}
103
+
104
+ # shipper options
105
+ attribute :shipper_name, :string
106
+ attribute :shipper_title, :string
107
+ attribute :shipper_company_name, :string
108
+ attribute :shipper_phone_number, :string
109
+ attribute :shipper_email, :string
110
+ attribute :shipper_streets, :string
111
+ attribute :shipper_city, :string
112
+ attribute :shipper_state, :string
113
+ attribute :shipper_postal_code, :string
114
+ attribute :shipper_country, :string, :modifier => :country_code
115
+ attribute :shipper_residential, :boolean, :default => false
116
+
117
+ # recipient options
118
+ attribute :recipient_name, :string
119
+ attribute :recipient_title, :string
120
+ attribute :recipient_company_name, :string
121
+ attribute :recipient_phone_number, :string
122
+ attribute :recipient_email, :string
123
+ attribute :recipient_streets, :string
124
+ attribute :recipient_city, :string
125
+ attribute :recipient_state, :string
126
+ attribute :recipient_postal_code, :string
127
+ attribute :recipient_country, :string, :modifier => :country_code
128
+ attribute :recipient_residential, :boolean, :default => false
129
+
130
+ # packaging options
131
+ attribute :packaging_type, :string, :default => "YOUR_PACKAGING"
132
+ attribute :package_count, :integer, :default => 1
133
+ attribute :package_weight, :float
134
+ attribute :package_weight_units, :string, :default => "LB"
135
+ attribute :package_length, :integer
136
+ attribute :package_width, :integer
137
+ attribute :package_height, :integer
138
+ attribute :package_dimension_units, :string, :default => "IN"
139
+
140
+ # monetary options
141
+ attribute :currency_type, :string
142
+ attribute :insured_value, :decimal
143
+ attribute :payment_type, :string, :default => "SENDER"
144
+ attribute :payor_account_number, :string, :default => lambda { |shipment| shipment.base.account }
145
+ attribute :payor_country, :string
146
+
147
+ # delivery options
148
+ attribute :ship_time, :datetime, :default => lambda { |rate| Time.now }
149
+ attribute :service_type, :string
150
+ attribute :delivery_deadline, :datetime
151
+ attribute :dropoff_type, :string, :default => "REGULAR_PICKUP"
152
+ attribute :special_services_requested, :array
153
+
154
+ # misc options
155
+ attribute :rate_request_types, :array, :default => ["ACCOUNT"]
156
+ attribute :include_transit_times, :boolean, :default => true
157
+
158
+ private
159
+ def target
160
+ @target ||= parse_response(request(build_request))
161
+ end
162
+
163
+ def build_request
164
+ b = builder
165
+ xml = b.RateRequest(:xmlns => "http://fedex.com/ws/rate/v#{VERSION[:major]}") do
166
+ build_authentication(b)
167
+ build_version(b, "crs", VERSION[:major], VERSION[:intermediate], VERSION[:minor])
168
+ b.ReturnTransitAndCommit include_transit_times
169
+ b.SpecialServicesRequested special_services_requested.join(",") if special_services_requested.any?
170
+
171
+ b.RequestedShipment do
172
+ b.ShipTimestamp ship_time.xmlschema if ship_time
173
+ b.ServiceType service_type if service_type
174
+ b.DropoffType dropoff_type if dropoff_type
175
+ b.PackagingType packaging_type if packaging_type
176
+ build_insured_value(b)
177
+ b.Shipper { build_address(b, :shipper) }
178
+ b.Recipient { build_address(b, :recipient) }
179
+ b.ShippingChargesPayment do
180
+ b.PaymentType payment_type if payment_type
181
+ b.Payor do
182
+ b.AccountNumber payor_account_number if payor_account_number
183
+ b.CountryCode payor_country if payor_country
184
+ end
185
+ end
186
+ b.RateRequestTypes rate_request_types.join(",") if rate_request_types
187
+ build_package(b)
188
+ end
189
+ end
190
+ end
191
+
192
+ def parse_response(response)
193
+ return [] if !response[:rate_reply_details]
194
+
195
+ response[:rate_reply_details].collect do |details|
196
+ shipment_detail = details[:rated_shipment_details].is_a?(Array) ? details[:rated_shipment_details].first : details[:rated_shipment_details]
197
+ cost = shipment_detail[:shipment_rate_detail][:total_net_charge]
198
+
199
+ delivered_by = details[:delivery_timestamp] && Time.parse(details[:delivery_timestamp])
200
+ speed = case details[:service_type]
201
+ when /overnight/i
202
+ 86400 # 1.day
203
+ when /2_day/i
204
+ 172800 # 2.days
205
+ else
206
+ 259200 # 3.days
207
+ end
208
+
209
+ if meets_deadline?(delivered_by)
210
+ service = Service.new
211
+ service.name = details[:service_type].gsub("_", " ").gsub(/\b(\w)(\w*)/){ $1 + $2.downcase }
212
+ service.type = details[:service_type]
213
+ service.saturday = details[:applied_options] == "SATURDAY_DELIVERY"
214
+ service.delivered_by = delivered_by
215
+ service.speed = speed
216
+ service.rate = BigDecimal.new(cost[:amount])
217
+ service.currency = cost[:currency]
218
+ service
219
+ end
220
+ end.compact
221
+ end
222
+
223
+ def meets_deadline?(delivered_by)
224
+ return true if !delivery_deadline
225
+ delivered_by && delivered_by <= delivery_deadline
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,134 @@
1
+ require "builder"
2
+
3
+ module Shippinglogic
4
+ class FedEx
5
+ # Methods relating to building and sending a request to FedEx's web services.
6
+ module Request
7
+ private
8
+ # Convenience method for sending requests to FedEx
9
+ def request(body)
10
+ real_class.post(base.url, :body => body)
11
+ end
12
+
13
+ # Convenience method to create a builder object so that our builder options are consistent across
14
+ # the various services.
15
+ #
16
+ # Ex: if I want to change the indent level to 3 it should change for all requests built.
17
+ def builder
18
+ b = Builder::XmlMarkup.new(:indent => 2)
19
+ b.instruct!
20
+ b
21
+ end
22
+
23
+ # A convenience method for building the authentication block in your XML request
24
+ def build_authentication(b)
25
+ b.WebAuthenticationDetail do
26
+ b.UserCredential do
27
+ b.Key base.key
28
+ b.Password base.password
29
+ end
30
+ end
31
+
32
+ b.ClientDetail do
33
+ b.AccountNumber base.account
34
+ b.MeterNumber base.meter
35
+ end
36
+ end
37
+
38
+ # A convenience method for building the version block in your XML request
39
+ def build_version(b, service, major, intermediate, minor)
40
+ b.Version do
41
+ b.ServiceId service
42
+ b.Major major
43
+ b.Intermediate intermediate
44
+ b.Minor minor
45
+ end
46
+ end
47
+
48
+ # A convenience method for building the contact block in your XML request
49
+ def build_contact(b, type)
50
+ b.Contact do
51
+ b.PersonName send("#{type}_name") if send("#{type}_name")
52
+ b.Title send("#{type}_title") if send("#{type}_title")
53
+ b.CompanyName send("#{type}_company_name") if send("#{type}_company_name")
54
+ b.PhoneNumber send("#{type}_phone_number") if send("#{type}_phone_number")
55
+ b.EMailAddress send("#{type}_email") if send("#{type}_email")
56
+ end
57
+ end
58
+
59
+ # A convenience method for building the address block in your XML request
60
+ def build_address(b, type)
61
+ b.Address do
62
+ b.StreetLines send("#{type}_streets") if send("#{type}_streets")
63
+ b.City send("#{type}_city") if send("#{type}_city")
64
+ b.StateOrProvinceCode state_code(send("#{type}_state")) if send("#{type}_state")
65
+ b.PostalCode send("#{type}_postal_code") if send("#{type}_postal_code")
66
+ b.CountryCode country_code(send("#{type}_country")) if send("#{type}_country")
67
+ b.Residential send("#{type}_residential")
68
+ end
69
+ end
70
+
71
+ def build_insured_value(b)
72
+ if insured_value
73
+ b.TotalInsuredValue do
74
+ b.Currency currency_type
75
+ b.Amount insured_value
76
+ end
77
+ end
78
+ end
79
+
80
+ # A convenience method for building the package block in your XML request
81
+ def build_package(b)
82
+ b.PackageCount package_count
83
+
84
+ b.RequestedPackages do
85
+ b.SequenceNumber 1
86
+
87
+ b.Weight do
88
+ b.Units package_weight_units
89
+ b.Value package_weight
90
+ end
91
+
92
+ if custom_packaging?
93
+ b.Dimensions do
94
+ b.Length package_length
95
+ b.Width package_width
96
+ b.Height package_height
97
+ b.Units package_dimension_units
98
+ end
99
+ end
100
+
101
+ if respond_to?(:signature) && signature
102
+ self.special_services_requested << "SIGNATURE_OPTION"
103
+ end
104
+
105
+ if (respond_to?(:special_services_requested) && special_services_requested.any?)
106
+ b.SpecialServicesRequested do
107
+ if special_services_requested.any?
108
+ b.SpecialServiceTypes special_services_requested.join(",")
109
+ end
110
+
111
+ if signature
112
+ b.SignatureOptionDetail do
113
+ b.OptionType signature
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ def custom_packaging?
122
+ packaging_type == "YOUR_PACKAGING"
123
+ end
124
+
125
+ def country_code(value)
126
+ Enumerations::FEDEX_COUNTRY_CODES[value.to_s] || Enumerations::RAILS_COUNTRY_CODES[value.to_s] || value.to_s
127
+ end
128
+
129
+ def state_code(value)
130
+ Enumerations::STATE_CODES[value.to_s] || value.to_s
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,72 @@
1
+ require "shippinglogic/fedex/error"
2
+
3
+ module Shippinglogic
4
+ class FedEx
5
+ # Methods relating to receiving a response from FedEx and cleaning it up.
6
+ module Response
7
+ SUCCESSFUL_SEVERITIES = ["SUCCESS", "NOTE", "WARNING"]
8
+
9
+ private
10
+ # Overwriting the request method to clean the response and handle errors.
11
+ def request(body)
12
+ response = clean_response(super)
13
+
14
+ if success?(response)
15
+ response
16
+ else
17
+ raise Error.new(body, response)
18
+ end
19
+ end
20
+
21
+ # Was the response a success?
22
+ def success?(response)
23
+ response.is_a?(Hash) && SUCCESSFUL_SEVERITIES.include?(response[:highest_severity])
24
+ end
25
+
26
+ # Cleans the response and returns it in a more 'user friendly' format that is easier
27
+ # to work with.
28
+ def clean_response(response)
29
+ cut_to_the_chase(sanitize_response_keys(response))
30
+ end
31
+
32
+ # FedEx likes nested XML tags, because they send quite a bit of them back in responses.
33
+ # This method just 'cuts to the chase' and get to the heart of the response.
34
+ def cut_to_the_chase(response)
35
+ if response.is_a?(Hash) && response.keys.first && response.keys.first.to_s =~ /_reply(_details)?$/
36
+ response.values.first
37
+ else
38
+ response
39
+ end
40
+ end
41
+
42
+ # Recursively sanitizes the response object by clenaing up any hash keys.
43
+ def sanitize_response_keys(response)
44
+ if response.is_a?(Hash)
45
+ response.inject({}) do |r, (key, value)|
46
+ r[sanitize_response_key(key)] = sanitize_response_keys(value)
47
+ r
48
+ end
49
+ elsif response.is_a?(Array)
50
+ response.collect { |r| sanitize_response_keys(r) }
51
+ else
52
+ response
53
+ end
54
+ end
55
+
56
+ # FedEx returns a SOAP response. I just want the plain response without all of the SOAP BS.
57
+ # It basically turns this:
58
+ #
59
+ # {"v3:ServiceInfo" => ...}
60
+ #
61
+ # into:
62
+ #
63
+ # {:service_info => ...}
64
+ #
65
+ # I also did not want to use the underscore method provided by ActiveSupport because I am trying
66
+ # to avoid using that as a dependency.
67
+ def sanitize_response_key(key)
68
+ key.to_s.sub(/^(v[0-9]|ns):/, "").gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase.to_sym
69
+ end
70
+ end
71
+ end
72
+ end