binarylogic-shippinglogic 0.9.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.
- data/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +128 -0
- data/Rakefile +45 -0
- data/VERSION.yml +4 -0
- data/lib/shippinglogic.rb +3 -0
- data/lib/shippinglogic/fedex.rb +64 -0
- data/lib/shippinglogic/fedex/attributes.rb +118 -0
- data/lib/shippinglogic/fedex/error.rb +47 -0
- data/lib/shippinglogic/fedex/rates.rb +199 -0
- data/lib/shippinglogic/fedex/request.rb +47 -0
- data/lib/shippinglogic/fedex/response.rb +68 -0
- data/lib/shippinglogic/fedex/service.rb +40 -0
- data/lib/shippinglogic/fedex/track.rb +66 -0
- data/lib/shippinglogic/fedex/validation.rb +32 -0
- data/spec/fedex/attributes_spec.rb +41 -0
- data/spec/fedex/error_spec.rb +26 -0
- data/spec/fedex/rates_spec.rb +22 -0
- data/spec/fedex/service_spec.rb +14 -0
- data/spec/fedex/track_spec.rb +20 -0
- data/spec/fedex/validation_spec.rb +10 -0
- data/spec/fedex_credentials.example.yaml +4 -0
- data/spec/fedex_spec_no.rb +11 -0
- data/spec/lib/interceptor.rb +13 -0
- data/spec/responses/basic_rate.xml +7 -0
- data/spec/responses/basic_track.xml +7 -0
- data/spec/responses/blank.xml +0 -0
- data/spec/responses/failed_authentication.xml +7 -0
- data/spec/responses/malformed.xml +8 -0
- data/spec/responses/unexpected.xml +1 -0
- data/spec/spec_helper.rb +84 -0
- metadata +113 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Ben Johnson of Binary Logic
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
= Shippinglogic
|
2
|
+
|
3
|
+
The goal of this library is to provide simple and straight forward interfaces to the various shipping web services: FedEx, UPS, USPS, etc. (Only FedEx is supported at this time)
|
4
|
+
|
5
|
+
== Helpful links
|
6
|
+
|
7
|
+
* <b>Documentation:</b> http://rdoc.info/projects/binarylogic/shippinglogic
|
8
|
+
* <b>Repository:</b> http://github.com/binarylogic/shippinglogic/tree/master
|
9
|
+
* <b>Bugs / feature suggestions:</b> http://binarylogic.lighthouseapp.com/projects/16601-searchlogic
|
10
|
+
* <b>Google group:</b> http://groups.google.com/group/shippinglogic
|
11
|
+
|
12
|
+
<b>Before contacting me directly, please read:</b>
|
13
|
+
|
14
|
+
If you find a bug or a problem please post it on lighthouse. If you need help with something, please use google groups. I check both regularly and get emails when anything happens, so that is the best place to get help. This also benefits other people in the future with the same questions / problems. Thank you.
|
15
|
+
|
16
|
+
== Install & use
|
17
|
+
|
18
|
+
Install the gem from rubyforge:
|
19
|
+
|
20
|
+
sudo gem install shippinglogic
|
21
|
+
|
22
|
+
Or from github:
|
23
|
+
|
24
|
+
sudo gem install binarylogic-shippinglogic
|
25
|
+
|
26
|
+
Now just include it in your project and you are ready to go.
|
27
|
+
|
28
|
+
You can also install this as a plugin:
|
29
|
+
|
30
|
+
script/plugin install git://github.com/binarylogic/shippinglogic.git
|
31
|
+
|
32
|
+
See below for usage examples.
|
33
|
+
|
34
|
+
== Usage
|
35
|
+
|
36
|
+
What's unique about this library is it's usage / syntax. Let me show you...
|
37
|
+
|
38
|
+
=== Calls to the web services are lazy
|
39
|
+
|
40
|
+
A simple example where we want to track a FedEx package:
|
41
|
+
|
42
|
+
fedex = Shippinglogic::FedEx.new(key, password, account, meter) # all credentials FedEx requires
|
43
|
+
tracking = fedex.track(:tracking_number => "077973360403984")
|
44
|
+
|
45
|
+
Now here is why this is cool. Nothing has happened yet. No call has been made to the FedEx servers, etc. We do this because we don't need it yet. You don't need it until you start using the object. Lets continue with our example:
|
46
|
+
|
47
|
+
tracking.first # call is made to FedEx and returns the first tracking result in the array
|
48
|
+
tracking.size # cache is used to determine the size of the array
|
49
|
+
|
50
|
+
This is similar to how ActiveRecord's association proxies work. When you call user.orders no database activity occurs until you actually use the object.
|
51
|
+
|
52
|
+
=== Flexibility
|
53
|
+
|
54
|
+
You will notice above we assign the result of the 'track' method to a variable called 'tracking'. That object has more to it:
|
55
|
+
|
56
|
+
# Initializing
|
57
|
+
tracking = fedex.track(:tracking_number => "XXXXXXXXXXXXX")
|
58
|
+
tracking.tracking_number
|
59
|
+
# => "XXXXXXXXXXXXX"
|
60
|
+
|
61
|
+
# Attribute accessors
|
62
|
+
tracking.tracking_number = "YYYYYYYYYYYYYYY"
|
63
|
+
tracking.tracking_number
|
64
|
+
# => "YYYYYYYYYYYYYYY"
|
65
|
+
|
66
|
+
# Mass attribute setting
|
67
|
+
tracking.attributes = {:tracking_number => "ZZZZZZZZZZZZZZZZ"}
|
68
|
+
tracking.tracking_number
|
69
|
+
# => "ZZZZZZZZZZZZZZZZ"
|
70
|
+
|
71
|
+
# Mass attribute reading
|
72
|
+
tracking.attributes
|
73
|
+
# => {:tracking_number => "ZZZZZZZZZZZZZZZZ"}
|
74
|
+
|
75
|
+
== Available services and their features
|
76
|
+
|
77
|
+
This library is still very new, as a result only FedEx is support at this time. More will come.
|
78
|
+
|
79
|
+
=== FedEx
|
80
|
+
|
81
|
+
1. <b>Tracking</b> - See Shippinglogic::Fedex::Track
|
82
|
+
1. <b>Getting service rates</b> - See Shippinglogic::Fedex::Rates
|
83
|
+
|
84
|
+
=== Add your own services
|
85
|
+
|
86
|
+
Simply fork the project and make your changes. If you want to add support for a new service it should be fairly straight forward. Checkout the code in Shippinglogic::Fedex::Track, it very simple and easy to follow. It's a great place to start because its the simplest of services.
|
87
|
+
|
88
|
+
== Interface integration
|
89
|
+
|
90
|
+
What's nice about having an object is that you can pass it around. Let's say you wanted to add simple FedEx tracking functionality to your app:
|
91
|
+
|
92
|
+
class TrackingController < ApplicationController
|
93
|
+
def new
|
94
|
+
@tracking = fedex.track(params[:tracking])
|
95
|
+
end
|
96
|
+
|
97
|
+
def create
|
98
|
+
@tracking = fedex.track(params[:tracking])
|
99
|
+
render :action => :new if !@tracking.successful?
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
def fedex
|
104
|
+
@fedex ||= Shippinglogic::FedEx.new(key, password, account, meter)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
That's pretty simple. Now check out your form:
|
109
|
+
|
110
|
+
# new.html.haml
|
111
|
+
- form_for @tracking do |f|
|
112
|
+
= f.error_messages
|
113
|
+
= f.text_field :tracking_number
|
114
|
+
= f.submit "Track"
|
115
|
+
|
116
|
+
Then your results:
|
117
|
+
|
118
|
+
# create.html.haml
|
119
|
+
- @tracking.each do |event|
|
120
|
+
.event
|
121
|
+
.name= event.name
|
122
|
+
.occured_at= event.occured_at.to_s(:long)
|
123
|
+
.location== #{event.city}, #{event.state} #{event.postal_code}, #{event.country}
|
124
|
+
.residential= event.residential ? "Yes" : "No"
|
125
|
+
|
126
|
+
== Copyright
|
127
|
+
|
128
|
+
Copyright (c) 2009 {Ben Johnson of Binary Logic}[http://www.binarylogic.com], released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "shippinglogic"
|
8
|
+
gem.summary = "A simple and clean library to interface with shipping carriers"
|
9
|
+
gem.description = "A simple and clean library to interface with shipping carriers"
|
10
|
+
gem.email = "bjohnson@binarylogic.com"
|
11
|
+
gem.homepage = "http://github.com/binarylogic/shippinglogic"
|
12
|
+
gem.authors = ["Ben Johnson of Binary Logic"]
|
13
|
+
gem.rubyforge_project = "shippinglogic"
|
14
|
+
gem.add_dependency "activesupport", ">= 2.2.0"
|
15
|
+
gem.add_dependency "httparty", ">= 0.4.4"
|
16
|
+
end
|
17
|
+
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'spec/rake/spectask'
|
23
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
24
|
+
spec.libs << 'lib' << 'spec'
|
25
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
26
|
+
end
|
27
|
+
|
28
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
31
|
+
spec.rcov = true
|
32
|
+
end
|
33
|
+
|
34
|
+
task :default => :spec
|
35
|
+
|
36
|
+
begin
|
37
|
+
require 'rake/contrib/sshpublisher'
|
38
|
+
namespace :rubyforge do
|
39
|
+
desc "Release gem to RubyForge"
|
40
|
+
task :release => ["rubyforge:release:gem"]
|
41
|
+
end
|
42
|
+
rescue LoadError
|
43
|
+
puts "Rake SshDirPublisher is unavailable or your rubyforge environment is not configured."
|
44
|
+
end
|
45
|
+
|
data/VERSION.yml
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require "shippinglogic/fedex/error"
|
2
|
+
require "shippinglogic/fedex/service"
|
3
|
+
require "shippinglogic/fedex/rates"
|
4
|
+
require "shippinglogic/fedex/track"
|
5
|
+
|
6
|
+
module Shippinglogic
|
7
|
+
class FedEx
|
8
|
+
# A hash representing default the options. If you are using this in a Rails app the best place
|
9
|
+
# to modify or change these options is either in an initializer or your specific environment file. Keep
|
10
|
+
# in mind that these options can be modified on the instance level when creating an object. See #initialize
|
11
|
+
# for more details.
|
12
|
+
#
|
13
|
+
# === Options
|
14
|
+
#
|
15
|
+
# * <tt>:test</tt> - this basically tells us which url to use. If set to true we will use the FedEx test URL, if false we
|
16
|
+
# will use the production URL. If you are using this in a rails app, unless you are in your production environment, this
|
17
|
+
# will default to true automatically.
|
18
|
+
# * <tt>:test_url</tt> - the test URL for FedEx's webservices. (default: https://gatewaybeta.fedex.com:443/xml)
|
19
|
+
# * <tt>:production_url</tt> - the production URL for FedEx's webservices. (default: https://gateway.fedex.com:443/xml)
|
20
|
+
def self.options
|
21
|
+
@options ||= {
|
22
|
+
:test => defined?(Rails) && !Rails.env.production?,
|
23
|
+
:production_url => "https://gateway.fedex.com:443/xml",
|
24
|
+
:test_url => "https://gatewaybeta.fedex.com:443/xml"
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_accessor :key, :password, :account, :meter, :options
|
29
|
+
|
30
|
+
# Before you can use the FedEx web services you need to provide 4 credentials:
|
31
|
+
#
|
32
|
+
# 1. Your fedex web service key
|
33
|
+
# 2. Your fedex password
|
34
|
+
# 3. Your fedex account number
|
35
|
+
# 4. Your fedex meter number
|
36
|
+
#
|
37
|
+
# You can easily get these things by logging into your fedex account and going to:
|
38
|
+
#
|
39
|
+
# https://www.fedex.com/wpor/wpor/editConsult.do
|
40
|
+
#
|
41
|
+
# If for some reason this link no longer works because FedEx changed it, just go to the
|
42
|
+
# developer resources area and then navigate to the FedEx web services for shipping area. Once
|
43
|
+
# there you should see a link to apply for a develop test key.
|
44
|
+
#
|
45
|
+
# The last parameter allows you to modify the class options on an instance level. It accepts the
|
46
|
+
# same options that the class level method #options accepts. If you don't want to change any of
|
47
|
+
# them, don't supply this parameter.
|
48
|
+
def initialize(key, password, account, meter, options = {})
|
49
|
+
self.key = key
|
50
|
+
self.password = password
|
51
|
+
self.account = account
|
52
|
+
self.meter = meter
|
53
|
+
self.options = self.class.options.merge(options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def rates(attributes = {})
|
57
|
+
@rates ||= Rates.new(self, attributes)
|
58
|
+
end
|
59
|
+
|
60
|
+
def track(attributes = {})
|
61
|
+
@track ||= Track.new(self, attributes)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Shippinglogic
|
2
|
+
class FedEx
|
3
|
+
# Adds in all of the reading / writing for the various serivce options.
|
4
|
+
module Attributes
|
5
|
+
def self.included(klass)
|
6
|
+
klass.class_eval do
|
7
|
+
extend ClassMethods
|
8
|
+
include InstanceMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Define an attribute for a class, makes adding options / attributes to the class
|
14
|
+
# much cleaner. See the Rates class for an example.
|
15
|
+
def attribute(name, type, options = {})
|
16
|
+
name = name.to_sym
|
17
|
+
options[:type] = type.to_sym
|
18
|
+
attributes[name] = options
|
19
|
+
|
20
|
+
define_method(name) { read_attribute(name) }
|
21
|
+
define_method("#{name}=") { |value| write_attribute(name, value) }
|
22
|
+
end
|
23
|
+
|
24
|
+
# A hash of all the attributes and their options
|
25
|
+
def attributes
|
26
|
+
@attributes ||= {}
|
27
|
+
end
|
28
|
+
|
29
|
+
# An array of the attribute names
|
30
|
+
def attribute_names
|
31
|
+
attributes.keys
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the options specified when defining a specific attribute
|
35
|
+
def attribute_options(name)
|
36
|
+
attributes[name.to_sym]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module InstanceMethods
|
41
|
+
# A convenience so that you can set attributes while initializing an object
|
42
|
+
def initialize(base, attributes = {})
|
43
|
+
@attributes = {}
|
44
|
+
self.attributes = attributes
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a hash of the various attribute values
|
48
|
+
def attributes
|
49
|
+
attributes = {}
|
50
|
+
attribute_names.each do |name|
|
51
|
+
attributes[name] = send(name)
|
52
|
+
end
|
53
|
+
attributes
|
54
|
+
end
|
55
|
+
|
56
|
+
# Accepts a hash of attribute values and sets each attribute to those values
|
57
|
+
def attributes=(value)
|
58
|
+
return if value.blank?
|
59
|
+
value.each do |key, value|
|
60
|
+
next if !attribute_names.include?(key.to_sym)
|
61
|
+
send("#{key}=", value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def attribute_names
|
67
|
+
self.class.attribute_names
|
68
|
+
end
|
69
|
+
|
70
|
+
def attribute_options(name)
|
71
|
+
self.class.attribute_options(name)
|
72
|
+
end
|
73
|
+
|
74
|
+
def attribute_type(name)
|
75
|
+
attribute_options(name)[:type]
|
76
|
+
end
|
77
|
+
|
78
|
+
def attribute_default(name)
|
79
|
+
default = attribute_options(name)[:default]
|
80
|
+
case default
|
81
|
+
when Proc
|
82
|
+
default.call
|
83
|
+
else
|
84
|
+
default
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def write_attribute(name, value)
|
89
|
+
@attributes[name.to_sym] = value
|
90
|
+
end
|
91
|
+
|
92
|
+
def read_attribute(name)
|
93
|
+
name = name.to_sym
|
94
|
+
value = @attributes[name].nil? ? attribute_default(name) : @attributes[name]
|
95
|
+
type = attribute_type(name)
|
96
|
+
return nil if value.nil? && type != :array
|
97
|
+
|
98
|
+
case type
|
99
|
+
when :array
|
100
|
+
value.is_a?(Array) ? value : [value].compact
|
101
|
+
when :integer
|
102
|
+
value.to_i
|
103
|
+
when :float
|
104
|
+
value.to_f
|
105
|
+
when :big_decimal
|
106
|
+
BigDecimal.new(value)
|
107
|
+
when :boolean
|
108
|
+
["true", "1"].include?(value.to_s)
|
109
|
+
when :string, :text
|
110
|
+
value.to_s
|
111
|
+
else
|
112
|
+
value
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -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 respose catch the error object and call the 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.response
|
16
|
+
# # to get the raw response from fedex
|
17
|
+
# end
|
18
|
+
class Error < StandardError
|
19
|
+
attr_accessor :code, :message, :response
|
20
|
+
|
21
|
+
def initialize(response)
|
22
|
+
self.response = response
|
23
|
+
|
24
|
+
if response.blank?
|
25
|
+
self.message = "The response from FedEx was blank."
|
26
|
+
elsif !response.is_a?(Hash)
|
27
|
+
self.message = "The response from FedEx was malformed and was not in a valid XML format."
|
28
|
+
elsif notifications = response[:notifications]
|
29
|
+
self.code = notifications[:code]
|
30
|
+
self.message = notifications[:message]
|
31
|
+
elsif response[:"soapenv:fault"] && detail = response[:"soapenv:fault"][:detail][:"con:fault"]
|
32
|
+
self.code = detail[:"con:error_code"]
|
33
|
+
self.message = detail[:"con:reason"]
|
34
|
+
else
|
35
|
+
self.message = "There was a problem with your fedex request, and we couldn't locate a specific error message. This means your response " +
|
36
|
+
"was in an unexpected format. You might try glancing at the raw response by using the 'response' method on this error object."
|
37
|
+
end
|
38
|
+
|
39
|
+
error_message = ""
|
40
|
+
error_message += "(Error: #{code}) " if code
|
41
|
+
error_message = message
|
42
|
+
|
43
|
+
super(error_message)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|