usps 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +56 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/lib/usps.rb +45 -0
- data/lib/usps/address.rb +72 -0
- data/lib/usps/client.rb +56 -0
- data/lib/usps/configuration.rb +16 -0
- data/lib/usps/errors.rb +46 -0
- data/lib/usps/request.rb +13 -0
- data/lib/usps/request/address_standardization.rb +44 -0
- data/lib/usps/request/base.rb +42 -0
- data/lib/usps/request/city_and_state_lookup.rb +32 -0
- data/lib/usps/request/delivery_confirmation.rb +88 -0
- data/lib/usps/request/delivery_confirmation_certify.rb +10 -0
- data/lib/usps/request/tracking_lookup.rb +28 -0
- data/lib/usps/request/zip_code_lookup.rb +45 -0
- data/lib/usps/response.rb +7 -0
- data/lib/usps/response/address_standardization.rb +38 -0
- data/lib/usps/response/base.rb +13 -0
- data/lib/usps/response/city_and_state_lookup.rb +28 -0
- data/lib/usps/response/delivery_confirmation.rb +25 -0
- data/lib/usps/response/tracking_lookup.rb +19 -0
- data/lib/usps/test.rb +34 -0
- data/lib/usps/test/address_verification.rb +31 -0
- data/lib/usps/test/city_and_state_lookup.rb +19 -0
- data/lib/usps/test/delivery_confirmation.rb +59 -0
- data/lib/usps/test/tracking_lookup.rb +29 -0
- data/lib/usps/test/zip_code_lookup.rb +37 -0
- data/spec/address_spec.rb +65 -0
- data/spec/configuration_spec.rb +19 -0
- data/spec/data/address_standardization_1.xml +1 -0
- data/spec/data/address_standardization_2.xml +1 -0
- data/spec/data/city_and_state_lookup_1.xml +1 -0
- data/spec/data/city_and_state_lookup_2.xml +1 -0
- data/spec/data/delivery_confirmation_1.xml +1 -0
- data/spec/data/delivery_confirmation_2.xml +1 -0
- data/spec/data/tracking_lookup_1.xml +1 -0
- data/spec/data/tracking_lookup_2.xml +1 -0
- data/spec/request/address_standardization_spec.rb +45 -0
- data/spec/request/base_spec.rb +16 -0
- data/spec/request/city_and_state_lookup_spec.rb +32 -0
- data/spec/request/delivery_confirmation_certify_spec.rb +11 -0
- data/spec/request/delivery_confirmation_spec.rb +57 -0
- data/spec/request/signature_confirmation_spec.rb +0 -0
- data/spec/request/tracking_spec.rb +21 -0
- data/spec/request/zip_code_lookup_spec.rb +42 -0
- data/spec/response/address_standardization_spec.rb +54 -0
- data/spec/response/base_spec.rb +0 -0
- data/spec/response/city_and_state_lookup_spec.rb +27 -0
- data/spec/response/delivery_confirmation_spec.rb +43 -0
- data/spec/response/signature_confirmation_spec.rb +0 -0
- data/spec/response/tracking_lookup_spec.rb +27 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/usps_spec.rb +8 -0
- data/usps.gemspec +129 -0
- metadata +178 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Chris Gaffney
|
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,56 @@
|
|
1
|
+
= usps
|
2
|
+
|
3
|
+
Ruby API for accessing the USPS WebTools API found here: http://www.usps.com/webtools/technical.htm
|
4
|
+
|
5
|
+
Usage of this library assumes you already have a USPS API account and that all priviledges have been granted.
|
6
|
+
|
7
|
+
== Exposed API Calls
|
8
|
+
|
9
|
+
The following USPS API calls are currently exposed through this library:
|
10
|
+
|
11
|
+
<AddressValidateRequest> -- USPS::Request::AddressStandardization
|
12
|
+
<CityStateLookupRequest> -- USPS::Request::CityAndStateLookup
|
13
|
+
<ZipCodeLookupRequest> -- USPS::Request::ZipCodeLookup
|
14
|
+
<TrackRequest> -- USPS::Request::TrackingLookup
|
15
|
+
|
16
|
+
<DeliveryConfirmationV3.0Request> -- USPS::Request::DeliveryConfirmation (for production)
|
17
|
+
<DeliveryConfirmCertifyV3.0Request> -- USPS::Request::DeliveryConfirmationCertify (for testing)
|
18
|
+
|
19
|
+
== How to use?
|
20
|
+
|
21
|
+
Using the library is as simple as building a new USPS::Request::[type] object, calling #send! and using the response.
|
22
|
+
For example, to send a tracking request you'd do the following:
|
23
|
+
|
24
|
+
request = USPS::Request::TrackingLookup.new(tracking_number)
|
25
|
+
response = request.send!
|
26
|
+
|
27
|
+
response.summary
|
28
|
+
response.details
|
29
|
+
|
30
|
+
The library assumes that either ENV['USPS_USER'] is set, or that you set USPS.username to your USPS API username.
|
31
|
+
|
32
|
+
See the individual USPS::Request classes for details on how to use them.
|
33
|
+
|
34
|
+
== USPS API Certification
|
35
|
+
|
36
|
+
Part of the process of setting up an account with the USPS API is to run certain tests against the USPS API.
|
37
|
+
This library has all the requisite tests built in, runnable with a simple
|
38
|
+
|
39
|
+
USPS_USER="[username]" rake certify
|
40
|
+
|
41
|
+
If any of the tests fail, you don't have access to that API and may need to work with USPS to fix.
|
42
|
+
|
43
|
+
== Note on Patches/Pull Requests
|
44
|
+
|
45
|
+
* Fork the project.
|
46
|
+
* Make your feature addition or bug fix.
|
47
|
+
* Add tests for it. This is important so I don't break it in a
|
48
|
+
future version unintentionally.
|
49
|
+
* Commit, do not mess with rakefile, version, or history.
|
50
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
51
|
+
* Send me a pull request. Bonus points for topic branches.
|
52
|
+
|
53
|
+
|
54
|
+
== Copyright
|
55
|
+
|
56
|
+
Copyright (c) 2010 Chris Gaffney. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
|
7
|
+
Jeweler::Tasks.new do |gem|
|
8
|
+
gem.name = "usps"
|
9
|
+
gem.summary = "USPS Webtools API for Ruby"
|
10
|
+
gem.description = "USPS Webtools API for Ruby."
|
11
|
+
gem.email = "gaffneyc@gmail.com"
|
12
|
+
gem.homepage = "http://github.com/gaffneyc/usps"
|
13
|
+
gem.authors = ["Chris Gaffney"]
|
14
|
+
|
15
|
+
gem.add_dependency 'builder', '>= 2.1.2'
|
16
|
+
gem.add_dependency 'nokogiri', '>= 1.4.1'
|
17
|
+
gem.add_dependency 'typhoeus', '>= 0.1.18'
|
18
|
+
|
19
|
+
gem.add_development_dependency "mocha", ">= 0.9.8"
|
20
|
+
gem.add_development_dependency "rspec", ">= 1.3.0"
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'spec/rake/spectask'
|
28
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
31
|
+
end
|
32
|
+
|
33
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
34
|
+
spec.libs << 'lib' << 'spec'
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :spec => :check_dependencies
|
40
|
+
|
41
|
+
task :default => :spec
|
42
|
+
|
43
|
+
require 'rake/rdoctask'
|
44
|
+
Rake::RDocTask.new do |rdoc|
|
45
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
46
|
+
|
47
|
+
rdoc.rdoc_dir = 'rdoc'
|
48
|
+
rdoc.title = "usps #{version}"
|
49
|
+
rdoc.rdoc_files.include('README*')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "Run the certification tests against USPS's API. Requires ENV['USPS_USER'] to be set or passed in."
|
54
|
+
task :certify do
|
55
|
+
ruby "-rubygems -Ilib -rusps lib/usps/test.rb"
|
56
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/usps.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'builder'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module USPS
|
5
|
+
require 'usps/errors'
|
6
|
+
require 'usps/configuration'
|
7
|
+
|
8
|
+
autoload :Client, 'usps/client'
|
9
|
+
autoload :Address, 'usps/address'
|
10
|
+
autoload :Request, 'usps/request'
|
11
|
+
autoload :Response, 'usps/response'
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_writer :config
|
15
|
+
|
16
|
+
def client
|
17
|
+
@client ||= Client.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def testing=(val)
|
21
|
+
config.testing = val
|
22
|
+
end
|
23
|
+
|
24
|
+
def config
|
25
|
+
@config ||= Configuration.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def configure(&block)
|
29
|
+
block.call(self.config)
|
30
|
+
end
|
31
|
+
|
32
|
+
# These two methods are currently here for backwards compatability
|
33
|
+
def username=(user)
|
34
|
+
config.username = user
|
35
|
+
end
|
36
|
+
|
37
|
+
def username
|
38
|
+
config.username
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_city_and_state_for_zip(zip)
|
42
|
+
USPS::Request::CityAndStateLookup.new(zip).send!.get(zip)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/usps/address.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# TODO: Documentation
|
2
|
+
#
|
3
|
+
# The USPS API uses a standard where Address2 is the street adress and Address1 is
|
4
|
+
# the apartment, suite, etc... I have switched them to match how I see them on an envelope.
|
5
|
+
# Additionally they are refered to address and extra_address though both address1 and address2
|
6
|
+
# work. Just remember they are flip flopped based on the USPS documentation.
|
7
|
+
class USPS::Address < Struct.new(:name, :company, :address1, :address2, :city, :state, :zip5, :zip4)
|
8
|
+
|
9
|
+
# Alias address getters and setters for a slightly more expressive api
|
10
|
+
alias :address :address1
|
11
|
+
alias :address= :address1=
|
12
|
+
alias :extra_address :address2
|
13
|
+
alias :extra_address= :address2=
|
14
|
+
|
15
|
+
# The USPS always refers to company as firm
|
16
|
+
alias :firm :company
|
17
|
+
alias :firm= :company=
|
18
|
+
|
19
|
+
attr_reader :error
|
20
|
+
|
21
|
+
def initialize(options = {}, &block)
|
22
|
+
options.each_pair do |k, v|
|
23
|
+
self.send("#{k}=", v)
|
24
|
+
end
|
25
|
+
|
26
|
+
block.call(self) if block
|
27
|
+
end
|
28
|
+
|
29
|
+
def zip
|
30
|
+
zip4 ? "#{zip5}-#{zip4}" : zip5.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
# Sets zip5 and zip4 if given a zip code in the format "99881" or "99881-1234"
|
34
|
+
def zip=(val)
|
35
|
+
self.zip5, self.zip4 = val.to_s.split('-')
|
36
|
+
end
|
37
|
+
|
38
|
+
# Check with the USPS if this address can be verified and will in missing
|
39
|
+
# fields (such as zip code) if they are available.
|
40
|
+
def valid?
|
41
|
+
@error = nil
|
42
|
+
standardize
|
43
|
+
true
|
44
|
+
rescue USPS::Error => e
|
45
|
+
@error = e
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def standardize
|
50
|
+
response = USPS::Request::AddressStandardization.new(self).send!
|
51
|
+
response[self]
|
52
|
+
end
|
53
|
+
|
54
|
+
def standardize!
|
55
|
+
replace(self.standardize)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Similar to Hash#replace, overwrite the values of this object with the other.
|
59
|
+
# It will not replace a provided key on the original object that does not exist
|
60
|
+
# on the replacing object (such as name with verification requests).
|
61
|
+
def replace(other)
|
62
|
+
raise ArgumentError unless other.is_a?(USPS::Address)
|
63
|
+
|
64
|
+
other.each_pair do |key, val|
|
65
|
+
# Do not overwrite values that may exist on the original but not on
|
66
|
+
# the replacement.
|
67
|
+
self[key] = val unless val.nil?
|
68
|
+
end
|
69
|
+
|
70
|
+
self
|
71
|
+
end
|
72
|
+
end
|
data/lib/usps/client.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'typhoeus'
|
2
|
+
|
3
|
+
module USPS
|
4
|
+
class Client
|
5
|
+
def request(request, &block)
|
6
|
+
server = server(request)
|
7
|
+
|
8
|
+
# The USPS documentation shows all requests as being GET requests, but
|
9
|
+
# the servers also appear to support POST. We're using POST here so we
|
10
|
+
# don't need to escape the request and to keep from running into any
|
11
|
+
# concerns with data length.
|
12
|
+
response = Typhoeus::Request.post(server, {
|
13
|
+
:timeout => USPS.config.timeout,
|
14
|
+
:params => {
|
15
|
+
"API" => request.api,
|
16
|
+
"XML" => request.build
|
17
|
+
}
|
18
|
+
})
|
19
|
+
|
20
|
+
# Parse the request
|
21
|
+
xml = Nokogiri::XML.parse(response.body)
|
22
|
+
|
23
|
+
# Process and raise errors
|
24
|
+
if((error = xml.search('Error')).any?)
|
25
|
+
# This is where things get a little tricky. There are a bunch of errors
|
26
|
+
# that the USPS can send back.
|
27
|
+
why = error.search('Description').text
|
28
|
+
code = error.search('Number').text
|
29
|
+
source = error.search('Source').text
|
30
|
+
|
31
|
+
raise Error.for_code(code).new(why, code, source)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Initialize the proper response object and parse the message
|
35
|
+
request.response_for(xml)
|
36
|
+
end
|
37
|
+
|
38
|
+
def testing?
|
39
|
+
USPS.config.testing
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def server(request)
|
44
|
+
dll = testing? ? "ShippingAPITest.dll" : "ShippingAPI.dll"
|
45
|
+
|
46
|
+
case
|
47
|
+
when request.secure?
|
48
|
+
"https://secure.shippingapis.com/#{dll}"
|
49
|
+
when testing?
|
50
|
+
"http://testing.shippingapis.com/#{dll}"
|
51
|
+
else
|
52
|
+
"http://production.shippingapis.com/#{dll}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module USPS
|
2
|
+
# Configuration options:
|
3
|
+
# username: Provided by the USPS during registration
|
4
|
+
# timeout: Connection timeout in milliseconds, nil to disable
|
5
|
+
# testing: Should requests be done against the testing service or not
|
6
|
+
# (only specific requests are supported).
|
7
|
+
class Configuration < Struct.new(:username, :timeout, :testing)
|
8
|
+
def initialize
|
9
|
+
self.timeout = 5000
|
10
|
+
self.testing = false
|
11
|
+
self.username = ENV['USPS_USER']
|
12
|
+
end
|
13
|
+
|
14
|
+
alias :testing? :testing
|
15
|
+
end
|
16
|
+
end
|
data/lib/usps/errors.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module USPS
|
2
|
+
# Error Hierarchy
|
3
|
+
#
|
4
|
+
# StandardError
|
5
|
+
# USPS::Error
|
6
|
+
# USPS::AuthorizationError
|
7
|
+
# USPS::ValidationError
|
8
|
+
# USPS::InvalidCityError
|
9
|
+
# USPS::InvalidStateError
|
10
|
+
# USPS::AddressNotFoundError
|
11
|
+
# USPS::MultipleAddressError
|
12
|
+
# USPS::InvalidImageTypeError
|
13
|
+
class Error < StandardError
|
14
|
+
attr_reader :number, :source
|
15
|
+
|
16
|
+
def initialize(message, number, source)
|
17
|
+
super(message)
|
18
|
+
|
19
|
+
@number = number
|
20
|
+
@source = source
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def for_code(code)
|
25
|
+
case code
|
26
|
+
when '80040b1a' ; AuthorizationError
|
27
|
+
when '-2147219400'; InvalidCityError
|
28
|
+
when '-2147219401'; AddressNotFoundError
|
29
|
+
when '-2147219402'; InvalidStateError
|
30
|
+
when '-2147219403'; MultipleAddressError
|
31
|
+
when '-2147218900'; InvalidImageTypeError
|
32
|
+
else ; Error
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class AuthorizationError < Error; end
|
39
|
+
|
40
|
+
class ValidationError < Error; end
|
41
|
+
class InvalidCityError < ValidationError; end
|
42
|
+
class InvalidStateError < ValidationError; end
|
43
|
+
class AddressNotFoundError < ValidationError; end
|
44
|
+
class MultipleAddressError < ValidationError; end
|
45
|
+
class InvalidImageTypeError < ValidationError; end
|
46
|
+
end
|
data/lib/usps/request.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module USPS::Request
|
2
|
+
autoload :Base, 'usps/request/base'
|
3
|
+
autoload :ZipCodeLookup, 'usps/request/zip_code_lookup'
|
4
|
+
autoload :CityAndStateLookup, 'usps/request/city_and_state_lookup'
|
5
|
+
autoload :AddressStandardization, 'usps/request/address_standardization'
|
6
|
+
|
7
|
+
# Delivery and Signature confirmation.
|
8
|
+
# DeliveryConfirmationCertify and SignatureConfirmationCertify should be used for testing
|
9
|
+
autoload :DeliveryConfirmation, 'usps/request/delivery_confirmation'
|
10
|
+
autoload :DeliveryConfirmationCertify, 'usps/request/delivery_confirmation_certify'
|
11
|
+
|
12
|
+
autoload :TrackingLookup, 'usps/request/tracking_lookup'
|
13
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module USPS::Request
|
2
|
+
# TODO: #send! could be made smarter to send lookup batches
|
3
|
+
class AddressStandardization < Base
|
4
|
+
config(
|
5
|
+
:api => 'Verify',
|
6
|
+
:tag => 'AddressValidateRequest',
|
7
|
+
:secure => false,
|
8
|
+
:response => USPS::Response::AddressStandardization
|
9
|
+
)
|
10
|
+
|
11
|
+
# At most 5 addresses can be verified
|
12
|
+
def initialize(*addresses)
|
13
|
+
@addresses = addresses.flatten
|
14
|
+
|
15
|
+
if @addresses.size > 5
|
16
|
+
raise ArgumentError, 'at most 5 addresses can be verified at a time'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def response_for(xml)
|
21
|
+
self.class.response.new(@addresses, xml)
|
22
|
+
end
|
23
|
+
|
24
|
+
def build
|
25
|
+
super do |builder|
|
26
|
+
@addresses.each_with_index do |addy, i|
|
27
|
+
builder.tag!('Address', :ID => i) do
|
28
|
+
builder.tag!('FirmName', addy.firm)
|
29
|
+
|
30
|
+
# Address fields are swapped in the USPS API
|
31
|
+
builder.tag!('Address1', addy.extra_address)
|
32
|
+
builder.tag!('Address2', addy.address)
|
33
|
+
|
34
|
+
builder.tag!('City', addy.city)
|
35
|
+
builder.tag!('State', addy.state)
|
36
|
+
|
37
|
+
builder.tag!('Zip5', addy.zip5)
|
38
|
+
builder.tag!('Zip4', addy.zip4)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|