shippinglogic 1.2.3

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 (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