usps 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +56 -0
  5. data/Rakefile +56 -0
  6. data/VERSION +1 -0
  7. data/lib/usps.rb +45 -0
  8. data/lib/usps/address.rb +72 -0
  9. data/lib/usps/client.rb +56 -0
  10. data/lib/usps/configuration.rb +16 -0
  11. data/lib/usps/errors.rb +46 -0
  12. data/lib/usps/request.rb +13 -0
  13. data/lib/usps/request/address_standardization.rb +44 -0
  14. data/lib/usps/request/base.rb +42 -0
  15. data/lib/usps/request/city_and_state_lookup.rb +32 -0
  16. data/lib/usps/request/delivery_confirmation.rb +88 -0
  17. data/lib/usps/request/delivery_confirmation_certify.rb +10 -0
  18. data/lib/usps/request/tracking_lookup.rb +28 -0
  19. data/lib/usps/request/zip_code_lookup.rb +45 -0
  20. data/lib/usps/response.rb +7 -0
  21. data/lib/usps/response/address_standardization.rb +38 -0
  22. data/lib/usps/response/base.rb +13 -0
  23. data/lib/usps/response/city_and_state_lookup.rb +28 -0
  24. data/lib/usps/response/delivery_confirmation.rb +25 -0
  25. data/lib/usps/response/tracking_lookup.rb +19 -0
  26. data/lib/usps/test.rb +34 -0
  27. data/lib/usps/test/address_verification.rb +31 -0
  28. data/lib/usps/test/city_and_state_lookup.rb +19 -0
  29. data/lib/usps/test/delivery_confirmation.rb +59 -0
  30. data/lib/usps/test/tracking_lookup.rb +29 -0
  31. data/lib/usps/test/zip_code_lookup.rb +37 -0
  32. data/spec/address_spec.rb +65 -0
  33. data/spec/configuration_spec.rb +19 -0
  34. data/spec/data/address_standardization_1.xml +1 -0
  35. data/spec/data/address_standardization_2.xml +1 -0
  36. data/spec/data/city_and_state_lookup_1.xml +1 -0
  37. data/spec/data/city_and_state_lookup_2.xml +1 -0
  38. data/spec/data/delivery_confirmation_1.xml +1 -0
  39. data/spec/data/delivery_confirmation_2.xml +1 -0
  40. data/spec/data/tracking_lookup_1.xml +1 -0
  41. data/spec/data/tracking_lookup_2.xml +1 -0
  42. data/spec/request/address_standardization_spec.rb +45 -0
  43. data/spec/request/base_spec.rb +16 -0
  44. data/spec/request/city_and_state_lookup_spec.rb +32 -0
  45. data/spec/request/delivery_confirmation_certify_spec.rb +11 -0
  46. data/spec/request/delivery_confirmation_spec.rb +57 -0
  47. data/spec/request/signature_confirmation_spec.rb +0 -0
  48. data/spec/request/tracking_spec.rb +21 -0
  49. data/spec/request/zip_code_lookup_spec.rb +42 -0
  50. data/spec/response/address_standardization_spec.rb +54 -0
  51. data/spec/response/base_spec.rb +0 -0
  52. data/spec/response/city_and_state_lookup_spec.rb +27 -0
  53. data/spec/response/delivery_confirmation_spec.rb +43 -0
  54. data/spec/response/signature_confirmation_spec.rb +0 -0
  55. data/spec/response/tracking_lookup_spec.rb +27 -0
  56. data/spec/spec.opts +1 -0
  57. data/spec/spec_helper.rb +25 -0
  58. data/spec/usps_spec.rb +8 -0
  59. data/usps.gemspec +129 -0
  60. metadata +178 -0
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
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.
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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