postmaster 1.0.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/README.md +57 -0
- data/Rakefile +18 -0
- data/lib/example.rb +42 -0
- data/lib/postmaster.rb +208 -0
- data/lib/postmaster/address.rb +17 -0
- data/lib/postmaster/api_operations/create.rb +16 -0
- data/lib/postmaster/api_operations/list.rb +16 -0
- data/lib/postmaster/api_resource.rb +36 -0
- data/lib/postmaster/errors/api_connection_error.rb +4 -0
- data/lib/postmaster/errors/api_error.rb +4 -0
- data/lib/postmaster/errors/authentication_error.rb +4 -0
- data/lib/postmaster/errors/invalid_request_error.rb +10 -0
- data/lib/postmaster/errors/postmaster_error.rb +20 -0
- data/lib/postmaster/json.rb +21 -0
- data/lib/postmaster/package.rb +6 -0
- data/lib/postmaster/postmaster_object.rb +172 -0
- data/lib/postmaster/shipment.rb +21 -0
- data/lib/postmaster/tracking.rb +9 -0
- data/lib/postmaster/util.rb +68 -0
- data/lib/postmaster/version.rb +3 -0
- data/postmaster.gemspec +26 -0
- data/test/test_address.rb +57 -0
- data/test/test_helper.rb +63 -0
- data/test/test_postmaster.rb +171 -0
- data/test/test_shipment.rb +107 -0
- metadata +185 -0
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Postmaster
|
2
|
+
|
3
|
+
Developer Friendly Shipping
|
4
|
+
|
5
|
+
Postmaster takes the pain out of sending shipments via UPS, Fedex, and USPS.
|
6
|
+
Save money before you ship, while you ship, and after you ship.
|
7
|
+
|
8
|
+
https://www.postmaster.io/
|
9
|
+
|
10
|
+
## Requirements
|
11
|
+
|
12
|
+
- [Ruby](http://www.ruby-lang.org/) 1.8.7 or above.
|
13
|
+
- [rest-client](https://github.com/archiloque/rest-client) 1.4 or above.
|
14
|
+
- [multi_json](https://github.com/intridea/multi_json) 1.0.2 or above.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
You can install using [gem](#gem) or from [source](#source).
|
19
|
+
|
20
|
+
### Gem
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
gem 'postmaster'
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
$ gem install postmaster
|
33
|
+
|
34
|
+
### Source
|
35
|
+
|
36
|
+
Download the postmaster-ruby source:
|
37
|
+
|
38
|
+
$ git clone https://github.com/postmaster/postmaster-ruby
|
39
|
+
|
40
|
+
If you want to build the gem from source:
|
41
|
+
|
42
|
+
$ cd postmaster-ruby
|
43
|
+
$ gem build postmaster.gemspec
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
See https://postmaster.io/docs for tutorials and documentation.
|
48
|
+
|
49
|
+
## Issues
|
50
|
+
|
51
|
+
Please use appropriately tagged github [issues](https://github.com/postmaster/postmaster-api/issues)
|
52
|
+
to request features or report bugs.
|
53
|
+
|
54
|
+
## Testing
|
55
|
+
|
56
|
+
$ cd tests
|
57
|
+
$ PM_API_KEY=your-api-key rake test
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rcov/rcovtask'
|
4
|
+
|
5
|
+
Rake::TestTask.new(:test) do |test|
|
6
|
+
test.libs << 'lib' << 'test'
|
7
|
+
test.pattern = 'test/**/test_*.rb'
|
8
|
+
test.verbose = true
|
9
|
+
end
|
10
|
+
|
11
|
+
Rcov::RcovTask.new do |t|
|
12
|
+
t.test_files = FileList['test/test_*.rb']
|
13
|
+
#t.verbose = true # uncomment to see the executed command
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
task :default => [:test]
|
18
|
+
|
data/lib/example.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "postmaster"
|
2
|
+
|
3
|
+
Postmaster.api_key = "example-api-key"
|
4
|
+
|
5
|
+
result = Postmaster::AddressValidation.validate(
|
6
|
+
:company => "ASLS",
|
7
|
+
:contact => "Joe Smith",
|
8
|
+
:line1 => "1110 Someplace Ave.",
|
9
|
+
:city => "Austin",
|
10
|
+
:state => "TX",
|
11
|
+
:zip => "78704",
|
12
|
+
:country => "US"
|
13
|
+
)
|
14
|
+
|
15
|
+
result = Postmaster::Shipment.create(
|
16
|
+
:to => {
|
17
|
+
:company => "ASLS",
|
18
|
+
:contact => "Joe Smith",
|
19
|
+
:line1 => "1110 Someplace Ave.",
|
20
|
+
:city => "Austin",
|
21
|
+
:state => "TX",
|
22
|
+
:zip_code => "78704",
|
23
|
+
:phone_no => "919-720-7941",
|
24
|
+
:country => "US"
|
25
|
+
},
|
26
|
+
:carrier => "ups",
|
27
|
+
:service => "2DAY",
|
28
|
+
:package => {
|
29
|
+
:value => 55,
|
30
|
+
:weight => 1.5,
|
31
|
+
:length => 10,
|
32
|
+
:width => 6,
|
33
|
+
:height => 8,
|
34
|
+
},
|
35
|
+
:reference => "Order # 54321"
|
36
|
+
)
|
37
|
+
|
38
|
+
shipment = Postmaster::Shipment.retrieve(1)
|
39
|
+
result = shipment.track()
|
40
|
+
|
41
|
+
shipment = Postmaster::Shipment.retrieve(1)
|
42
|
+
result = shipment.void()
|
data/lib/postmaster.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
# Postmaster Ruby bindings
|
2
|
+
require 'pp'
|
3
|
+
require 'cgi'
|
4
|
+
require 'set'
|
5
|
+
require 'rubygems'
|
6
|
+
require 'openssl'
|
7
|
+
|
8
|
+
gem 'rest-client', '~> 1.4'
|
9
|
+
require 'rest_client'
|
10
|
+
require 'multi_json'
|
11
|
+
|
12
|
+
# Version
|
13
|
+
require 'postmaster/version'
|
14
|
+
|
15
|
+
# API operations
|
16
|
+
require 'postmaster/api_operations/create'
|
17
|
+
require 'postmaster/api_operations/list'
|
18
|
+
|
19
|
+
# Resources
|
20
|
+
require 'postmaster/util'
|
21
|
+
require 'postmaster/json'
|
22
|
+
require 'postmaster/postmaster_object'
|
23
|
+
require 'postmaster/api_resource'
|
24
|
+
|
25
|
+
require 'postmaster/address'
|
26
|
+
require 'postmaster/shipment'
|
27
|
+
require 'postmaster/package'
|
28
|
+
require 'postmaster/tracking'
|
29
|
+
|
30
|
+
# Errors
|
31
|
+
require 'postmaster/errors/postmaster_error'
|
32
|
+
require 'postmaster/errors/api_error'
|
33
|
+
require 'postmaster/errors/api_connection_error'
|
34
|
+
require 'postmaster/errors/invalid_request_error'
|
35
|
+
require 'postmaster/errors/authentication_error'
|
36
|
+
|
37
|
+
|
38
|
+
module Postmaster
|
39
|
+
@@api_key = nil
|
40
|
+
@@api_base = 'https://api.postmaster.io'
|
41
|
+
|
42
|
+
def self.api_url(url='')
|
43
|
+
self.api_base + url
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.api_key=(api_key)
|
47
|
+
@@api_key = api_key
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.api_key
|
51
|
+
@@api_key
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.api_base=(api_base)
|
55
|
+
@@api_base = api_base
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.api_base
|
59
|
+
@@api_base
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.request(method, url, params={}, headers={})
|
63
|
+
api_key = self.api_key
|
64
|
+
raise AuthenticationError.new('No API key provided. (HINT: set your API key using "Postmaster.api_key = <API-KEY>".)') unless api_key
|
65
|
+
|
66
|
+
uname = (@@uname ||= RUBY_PLATFORM =~ /linux|darwin/i ? `uname -a 2>/dev/null`.strip : nil)
|
67
|
+
lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
|
68
|
+
ua = {
|
69
|
+
:bindings_version => Postmaster::VERSION,
|
70
|
+
:lang => 'ruby',
|
71
|
+
:lang_version => lang_version,
|
72
|
+
:platform => RUBY_PLATFORM,
|
73
|
+
:publisher => 'postmaster',
|
74
|
+
:uname => uname
|
75
|
+
}
|
76
|
+
|
77
|
+
params = Util.objects_to_ids(params)
|
78
|
+
url = self.api_url(url)
|
79
|
+
case method.to_s.downcase.to_sym
|
80
|
+
when :get, :head, :delete
|
81
|
+
# Make params into GET parameters
|
82
|
+
if params && params.count > 0
|
83
|
+
query_string = Util.flatten_params(params).collect{|key, value| "#{key}=#{Util.url_encode(value)}"}.join('&')
|
84
|
+
url += "?#{query_string}"
|
85
|
+
end
|
86
|
+
payload = nil
|
87
|
+
else
|
88
|
+
payload = Util.flatten_params(params).collect{|(key, value)| "#{key}=#{Util.url_encode(value)}"}.join('&')
|
89
|
+
end
|
90
|
+
|
91
|
+
begin
|
92
|
+
headers = { :x_postmaster_client_user_agent => Postmaster::JSON.dump(ua) }.merge(headers)
|
93
|
+
rescue => e
|
94
|
+
headers = {
|
95
|
+
:x_postmaster_client_raw_user_agent => ua.inspect,
|
96
|
+
:error => "#{e} (#{e.class})"
|
97
|
+
}.merge(headers)
|
98
|
+
end
|
99
|
+
|
100
|
+
headers = {
|
101
|
+
:user_agent => "Postmaster/v1 RubyBindings/#{Postmaster::VERSION}",
|
102
|
+
:content_type => 'application/x-www-form-urlencoded'
|
103
|
+
}.merge(headers)
|
104
|
+
|
105
|
+
opts = {
|
106
|
+
:method => method,
|
107
|
+
:url => url,
|
108
|
+
:headers => headers,
|
109
|
+
:open_timeout => 60,
|
110
|
+
:payload => payload,
|
111
|
+
:timeout => 80,
|
112
|
+
:user => api_key,
|
113
|
+
:password => "",
|
114
|
+
}
|
115
|
+
|
116
|
+
begin
|
117
|
+
response = execute_request(opts)
|
118
|
+
rescue SocketError => e
|
119
|
+
self.handle_restclient_error(e)
|
120
|
+
rescue NoMethodError => e
|
121
|
+
# Work around RestClient bug
|
122
|
+
if e.message =~ /\WRequestFailed\W/
|
123
|
+
e = APIConnectionError.new('Unexpected HTTP response code')
|
124
|
+
self.handle_restclient_error(e)
|
125
|
+
else
|
126
|
+
raise
|
127
|
+
end
|
128
|
+
rescue RestClient::ExceptionWithResponse => e
|
129
|
+
if rcode = e.http_code and rbody = e.http_body
|
130
|
+
self.handle_api_error(rcode, rbody)
|
131
|
+
else
|
132
|
+
self.handle_restclient_error(e)
|
133
|
+
end
|
134
|
+
rescue RestClient::Exception, Errno::ECONNREFUSED => e
|
135
|
+
self.handle_restclient_error(e)
|
136
|
+
end
|
137
|
+
|
138
|
+
rbody = response.body
|
139
|
+
rcode = response.code
|
140
|
+
begin
|
141
|
+
# Would use :symbolize_names => true, but apparently there is
|
142
|
+
# some library out there that makes symbolize_names not work.
|
143
|
+
resp = Postmaster::JSON.load(rbody)
|
144
|
+
rescue MultiJson::DecodeError
|
145
|
+
raise APIError.new("Invalid response object from API: #{rbody.inspect} (HTTP response code was #{rcode})", rcode, rbody)
|
146
|
+
end
|
147
|
+
|
148
|
+
resp = Util.symbolize_names(resp)
|
149
|
+
resp
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def self.execute_request(opts)
|
155
|
+
RestClient::Request.execute(opts)
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.handle_api_error(rcode, rbody)
|
159
|
+
begin
|
160
|
+
error_obj = Postmaster::JSON.load(rbody)
|
161
|
+
error_obj = Util.symbolize_names(error_obj)
|
162
|
+
error = error_obj[:error] or raise PostmasterError.new # escape from parsing
|
163
|
+
rescue MultiJson::DecodeError, PostmasterError
|
164
|
+
raise APIError.new("Invalid response object from API: #{rbody.inspect} (HTTP response code was #{rcode})", rcode, rbody)
|
165
|
+
end
|
166
|
+
|
167
|
+
case rcode
|
168
|
+
when 400, 404 then
|
169
|
+
raise invalid_request_error(error, rcode, rbody, error_obj)
|
170
|
+
when 401
|
171
|
+
raise authentication_error(error, rcode, rbody, error_obj)
|
172
|
+
when 403
|
173
|
+
raise permission_error(error, rcode, rbody, error_obj)
|
174
|
+
else
|
175
|
+
raise api_error(error, rcode, rbody, error_obj)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.invalid_request_error(error, rcode, rbody, error_obj)
|
180
|
+
InvalidRequestError.new(error[:message], error[:param], rcode, rbody, error_obj)
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.authentication_error(error, rcode, rbody, error_obj)
|
184
|
+
AuthenticationError.new(error[:message], rcode, rbody, error_obj)
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.permission_error(error, rcode, rbody, error_obj)
|
188
|
+
PermissionError.new(error[:message], error[:param], error[:code], rcode, rbody, error_obj)
|
189
|
+
end
|
190
|
+
|
191
|
+
def self.api_error(error, rcode, rbody, error_obj)
|
192
|
+
APIError.new(error[:message], rcode, rbody, error_obj)
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.handle_restclient_error(e)
|
196
|
+
case e
|
197
|
+
when RestClient::ServerBrokeConnection, RestClient::RequestTimeout
|
198
|
+
message = "Could not connect to Postmaster ($apiBase). Please check your internet connection and try again. If this problem persists, let us know at support@postmaster.io."
|
199
|
+
when RestClient::SSLCertificateNotVerified
|
200
|
+
message = "Could not verify Postmaster's SSL certificate. If this problem persists, let us know at support@postmaster.io."
|
201
|
+
else
|
202
|
+
message = "Unexpected error communicating with Postmaster. If this problem persists, let us know at support@postmaster.io."
|
203
|
+
end
|
204
|
+
message += "\n\n(Network error: #{e.message})"
|
205
|
+
raise APIConnectionError.new(message)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Postmaster
|
2
|
+
|
3
|
+
class Address < APIResource
|
4
|
+
end
|
5
|
+
|
6
|
+
class AddressProposal < APIResource
|
7
|
+
end
|
8
|
+
|
9
|
+
class AddressValidation < APIResource
|
10
|
+
|
11
|
+
def self.validate(params={})
|
12
|
+
response = Postmaster.request(:post, '/v1/validate', params)
|
13
|
+
self.construct_from(response)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Postmaster
|
2
|
+
module APIOperations
|
3
|
+
module Create
|
4
|
+
module ClassMethods
|
5
|
+
def create(params={})
|
6
|
+
response = Postmaster.request(:post, self.url, params)
|
7
|
+
self.construct_from(response)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Postmaster
|
2
|
+
module APIOperations
|
3
|
+
module List
|
4
|
+
module ClassMethods
|
5
|
+
def all(filters={})
|
6
|
+
response = Postmaster.request(:get, url, filters)
|
7
|
+
response[:results].map { |i| self.construct_from(i) }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Postmaster
|
2
|
+
class APIResource < PostmasterObject
|
3
|
+
def self.class_name
|
4
|
+
self.name.split('::')[-1]
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.url()
|
8
|
+
if self == APIResource
|
9
|
+
raise NotImplementedError.new('APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)')
|
10
|
+
end
|
11
|
+
"/v1/#{CGI.escape(class_name.downcase)}s"
|
12
|
+
end
|
13
|
+
|
14
|
+
def url(action=nil)
|
15
|
+
unless id = self['id']
|
16
|
+
raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id')
|
17
|
+
end
|
18
|
+
if action.nil?
|
19
|
+
return "#{self.class.url}/#{CGI.escape(id.to_s)}"
|
20
|
+
end
|
21
|
+
"#{self.class.url}/#{CGI.escape(id.to_s)}/#{CGI.escape(action)}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def refresh
|
25
|
+
response = Postmaster.request(:get, url)
|
26
|
+
refresh_from(response)
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.retrieve(id)
|
31
|
+
instance = self.new(id)
|
32
|
+
instance.refresh
|
33
|
+
instance
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|