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
@@ -0,0 +1,20 @@
|
|
1
|
+
module Postmaster
|
2
|
+
class PostmasterError < StandardError
|
3
|
+
attr_reader :message
|
4
|
+
attr_reader :http_status
|
5
|
+
attr_reader :http_body
|
6
|
+
attr_reader :json_body
|
7
|
+
|
8
|
+
def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil)
|
9
|
+
@message = message
|
10
|
+
@http_status = http_status
|
11
|
+
@http_body = http_body
|
12
|
+
@json_body = json_body
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
|
17
|
+
"#{status_string}#{@message}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Postmaster
|
2
|
+
module JSON
|
3
|
+
if MultiJson.respond_to?(:dump)
|
4
|
+
def self.dump(*args)
|
5
|
+
MultiJson.dump(*args)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.load(*args)
|
9
|
+
MultiJson.load(*args)
|
10
|
+
end
|
11
|
+
else
|
12
|
+
def self.dump(*args)
|
13
|
+
MultiJson.encode(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.load(*args)
|
17
|
+
MultiJson.decode(*args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Postmaster
|
2
|
+
class PostmasterObject
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
# The default :id method is deprecated and isn't useful to us
|
6
|
+
if method_defined?(:id)
|
7
|
+
undef :id
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(id=nil)
|
11
|
+
@values = {}
|
12
|
+
# This really belongs in APIResource, but not putting it there allows us
|
13
|
+
# to have a unified inspect method
|
14
|
+
@unsaved_values = Set.new
|
15
|
+
@transient_values = Set.new
|
16
|
+
self.id = id if id
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.construct_from(values)
|
20
|
+
obj = self.new(values[:id])
|
21
|
+
obj.refresh_from(values)
|
22
|
+
obj
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s(*args)
|
26
|
+
Postmaster::JSON.dump(@values, :pretty => true)
|
27
|
+
end
|
28
|
+
|
29
|
+
def inspect()
|
30
|
+
id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
|
31
|
+
"#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> JSON: " + Postmaster::JSON.dump(@values, :pretty => true)
|
32
|
+
end
|
33
|
+
|
34
|
+
def refresh_from(values)
|
35
|
+
# which keys should be converted to Postmaster_Objects
|
36
|
+
obj_keys = {
|
37
|
+
'Postmaster::Shipment.to' => Postmaster::Address,
|
38
|
+
'Postmaster::Shipment.from_' => Postmaster::Address,
|
39
|
+
'Postmaster::Shipment.package' => Postmaster::Package,
|
40
|
+
'Postmaster::AddressProposal.address' => Postmaster::Address,
|
41
|
+
#'Postmaster::Tracking.last_update' => 'DateTime',
|
42
|
+
#'Postmaster::TrackingHistory.timestamp' => 'DateTime'
|
43
|
+
}
|
44
|
+
|
45
|
+
# which keys should be converted to list of Postmaster_Objects
|
46
|
+
obj_list_keys = {
|
47
|
+
'Postmaster::AddressValidation.addresses' => Postmaster::Address,
|
48
|
+
'Postmaster::Shipment.packages' => Postmaster::Package,
|
49
|
+
'Postmaster::Tracking.history' => Postmaster::TrackingHistory
|
50
|
+
}
|
51
|
+
|
52
|
+
removed = Set.new(@values.keys - values.keys)
|
53
|
+
added = Set.new(values.keys - @values.keys)
|
54
|
+
# Wipe old state before setting new. This is useful for e.g. updating a
|
55
|
+
# customer, where there is no persistent card parameter. Mark those values
|
56
|
+
# which don't persist as transient
|
57
|
+
|
58
|
+
instance_eval do
|
59
|
+
remove_accessors(removed)
|
60
|
+
add_accessors(added)
|
61
|
+
end
|
62
|
+
removed.each do |k|
|
63
|
+
@values.delete(k)
|
64
|
+
end
|
65
|
+
values.each do |k, v|
|
66
|
+
full_key = self.class.name + "." + k.to_s
|
67
|
+
if obj_keys.has_key?(full_key)
|
68
|
+
klass = obj_keys[full_key]
|
69
|
+
@values[k] = klass.construct_from(v)
|
70
|
+
elsif obj_list_keys.has_key?(full_key)
|
71
|
+
klass = obj_list_keys[full_key]
|
72
|
+
@values[k] = []
|
73
|
+
v.each do |i|
|
74
|
+
@values[k].push(klass.construct_from(i))
|
75
|
+
end
|
76
|
+
else
|
77
|
+
@values[k] = v
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def [](k)
|
83
|
+
k = k.to_sym if k.kind_of?(String)
|
84
|
+
@values[k]
|
85
|
+
end
|
86
|
+
|
87
|
+
def []=(k, v)
|
88
|
+
k = k.to_sym if k.kind_of?(String)
|
89
|
+
send(:"#{k}=", v)
|
90
|
+
end
|
91
|
+
|
92
|
+
def has_key?(k)
|
93
|
+
k = k.to_sym if k.kind_of?(String)
|
94
|
+
@values.has_key?(k)
|
95
|
+
end
|
96
|
+
|
97
|
+
def delete(k)
|
98
|
+
k = k.to_sym if k.kind_of?(String)
|
99
|
+
@values.delete(k)
|
100
|
+
remove_accessors([k])
|
101
|
+
end
|
102
|
+
|
103
|
+
def keys
|
104
|
+
@values.keys
|
105
|
+
end
|
106
|
+
|
107
|
+
def values
|
108
|
+
@values.values
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_json(*a)
|
112
|
+
Postmaster::JSON.dump(@values)
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_hash
|
116
|
+
result = @values.clone
|
117
|
+
result.each do |k, v|
|
118
|
+
if v.kind_of? Postmaster::PostmasterObject
|
119
|
+
result[k] = v.to_hash
|
120
|
+
elsif v.kind_of? Array and v[0].kind_of? Postmaster::PostmasterObject
|
121
|
+
result[k] = v.map { |i| i.to_hash }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
result
|
125
|
+
end
|
126
|
+
|
127
|
+
def each(&blk)
|
128
|
+
@values.each(&blk)
|
129
|
+
end
|
130
|
+
|
131
|
+
protected
|
132
|
+
|
133
|
+
def metaclass
|
134
|
+
class << self; self; end
|
135
|
+
end
|
136
|
+
|
137
|
+
def remove_accessors(keys)
|
138
|
+
metaclass.instance_eval do
|
139
|
+
keys.each do |k|
|
140
|
+
k_eq = :"#{k}="
|
141
|
+
remove_method(k) if method_defined?(k)
|
142
|
+
remove_method(k_eq) if method_defined?(k_eq)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def add_accessors(keys)
|
148
|
+
metaclass.instance_eval do
|
149
|
+
keys.each do |k|
|
150
|
+
k_eq = :"#{k}="
|
151
|
+
define_method(k) { @values[k] }
|
152
|
+
define_method(k_eq) do |v|
|
153
|
+
@values[k] = v
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def method_missing(name, *args)
|
160
|
+
if name.to_s.end_with?('=')
|
161
|
+
attr = name.to_s[0...-1].to_sym
|
162
|
+
@values[attr] = args[0]
|
163
|
+
add_accessors([attr])
|
164
|
+
return
|
165
|
+
else
|
166
|
+
return @values[name] if @values.has_key?(name)
|
167
|
+
end
|
168
|
+
|
169
|
+
super
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Postmaster
|
2
|
+
|
3
|
+
class Shipment < APIResource
|
4
|
+
include Postmaster::APIOperations::Create
|
5
|
+
include Postmaster::APIOperations::List
|
6
|
+
|
7
|
+
def track
|
8
|
+
response = Postmaster.request(:get, url('track'))
|
9
|
+
if response[:results].nil?
|
10
|
+
return nil
|
11
|
+
end
|
12
|
+
response[:results].map { |i| Postmaster::Tracking.construct_from(i) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def void
|
16
|
+
response = Postmaster.request(:post, url('void'))
|
17
|
+
refresh_from({})
|
18
|
+
response[:message] == 'OK'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Postmaster
|
2
|
+
module Util
|
3
|
+
|
4
|
+
def self.objects_to_ids(h)
|
5
|
+
case h
|
6
|
+
when APIResource
|
7
|
+
h.id
|
8
|
+
when Hash
|
9
|
+
res = {}
|
10
|
+
h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
|
11
|
+
res
|
12
|
+
when Array
|
13
|
+
h.map { |v| objects_to_ids(v) }
|
14
|
+
else
|
15
|
+
h
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.symbolize_names(object)
|
20
|
+
case object
|
21
|
+
when Hash
|
22
|
+
new = {}
|
23
|
+
object.each do |key, value|
|
24
|
+
key = (key.to_sym rescue key) || key
|
25
|
+
new[key] = symbolize_names(value)
|
26
|
+
end
|
27
|
+
new
|
28
|
+
when Array
|
29
|
+
object.map { |value| symbolize_names(value) }
|
30
|
+
else
|
31
|
+
object
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.url_encode(key)
|
36
|
+
URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.flatten_params(params, parent_key=nil)
|
40
|
+
result = []
|
41
|
+
params.each do |key, value|
|
42
|
+
calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
|
43
|
+
if value.is_a?(Hash)
|
44
|
+
result += flatten_params(value, calculated_key)
|
45
|
+
elsif value.is_a?(Array)
|
46
|
+
result += flatten_params_array(value, calculated_key)
|
47
|
+
else
|
48
|
+
result << [calculated_key, value]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.flatten_params_array(value, calculated_key)
|
55
|
+
result = []
|
56
|
+
value.each do |elem|
|
57
|
+
if elem.is_a?(Hash)
|
58
|
+
result += flatten_params(elem, calculated_key)
|
59
|
+
elsif elem.is_a?(Array)
|
60
|
+
result += flatten_params_array(elem, calculated_key)
|
61
|
+
else
|
62
|
+
result << ["#{calculated_key}[]", elem]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
result
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/postmaster.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
2
|
+
|
3
|
+
require 'postmaster/version'
|
4
|
+
|
5
|
+
spec = Gem::Specification.new do |s|
|
6
|
+
s.name = 'postmaster'
|
7
|
+
s.version = Postmaster::VERSION
|
8
|
+
s.summary = 'Library for postmaster.io service'
|
9
|
+
s.description = 'Postmaster takes the pain out of sending shipments via UPS, Fedex, and USPS. Save money before you ship, while you ship, and after you ship. See https://postmaster.io for details.'
|
10
|
+
s.authors = ['Postmaster']
|
11
|
+
s.email = ['support@postmaster.io']
|
12
|
+
s.homepage = 'https://postmaster.io'
|
13
|
+
s.require_paths = %w{lib}
|
14
|
+
|
15
|
+
s.add_dependency('rest-client', '~> 1.4')
|
16
|
+
s.add_dependency('multi_json', '>= 1.0.4', '< 2')
|
17
|
+
|
18
|
+
s.add_development_dependency('mocha')
|
19
|
+
s.add_development_dependency('shoulda')
|
20
|
+
s.add_development_dependency('test-unit')
|
21
|
+
s.add_development_dependency('rake')
|
22
|
+
|
23
|
+
s.files = `git ls-files`.split("\n")
|
24
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
25
|
+
s.require_paths = ['lib']
|
26
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'test/unit'
|
3
|
+
require 'postmaster'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'shoulda'
|
6
|
+
require 'mocha'
|
7
|
+
require 'rest-client'
|
8
|
+
require 'cgi'
|
9
|
+
require 'uri'
|
10
|
+
|
11
|
+
class TestAddressRuby < Test::Unit::TestCase
|
12
|
+
include Mocha
|
13
|
+
|
14
|
+
sample_address =
|
15
|
+
|
16
|
+
context "Address" do
|
17
|
+
|
18
|
+
should "be valid" do
|
19
|
+
result = Postmaster::AddressValidation.validate({
|
20
|
+
:company => "Asls",
|
21
|
+
:contact => "Joe Smith",
|
22
|
+
:line1 => "1110 Algarita Ave",
|
23
|
+
:city => "Austin",
|
24
|
+
:state => "TX",
|
25
|
+
:zip_code => "78704",
|
26
|
+
:country => "US",
|
27
|
+
})
|
28
|
+
|
29
|
+
assert_instance_of(Postmaster::AddressValidation, result);
|
30
|
+
assert(result.keys.include?(:status))
|
31
|
+
assert_kind_of(Array, result[:addresses])
|
32
|
+
assert(!result[:addresses].empty?)
|
33
|
+
|
34
|
+
address = result[:addresses][0]
|
35
|
+
assert_instance_of(Postmaster::Address, address);
|
36
|
+
assert(address.keys.include?(:zip_code))
|
37
|
+
assert_equal("78704", address[:zip_code])
|
38
|
+
end
|
39
|
+
|
40
|
+
should "be invalid" do
|
41
|
+
result = Postmaster::AddressValidation.validate({
|
42
|
+
:company => "Asls",
|
43
|
+
:contact => "Joe Smith",
|
44
|
+
:line1 => "007 Nowhere Ave",
|
45
|
+
:city => "Austin",
|
46
|
+
:state => "TX",
|
47
|
+
:zip_code => "00001",
|
48
|
+
:country => "US",
|
49
|
+
})
|
50
|
+
|
51
|
+
assert_instance_of(Postmaster::AddressValidation, result);
|
52
|
+
assert(result.keys.include?(:status))
|
53
|
+
assert_equal("WRONG_ADDRESS", result[:status])
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'postmaster'
|
4
|
+
require 'mocha'
|
5
|
+
include Mocha
|
6
|
+
|
7
|
+
#monkeypatch request methods
|
8
|
+
module Postmaster
|
9
|
+
@mock_rest_client = nil
|
10
|
+
|
11
|
+
def self.mock_rest_client=(mock_client)
|
12
|
+
@mock_rest_client = mock_client
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.api_key
|
16
|
+
ENV['PM_API_KEY'] || @@api_key
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.api_base
|
20
|
+
ENV['PM_API_HOST'] || @@api_base
|
21
|
+
end
|
22
|
+
|
23
|
+
# remeber original execute before monkey-patching it
|
24
|
+
class << Postmaster
|
25
|
+
alias_method :original_execute_request, :execute_request
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.execute_request(opts)
|
29
|
+
# send calls to mock if it's defined otherwise call real service
|
30
|
+
if @mock_rest_client
|
31
|
+
get_params = (opts[:headers] || {})[:params]
|
32
|
+
post_params = opts[:payload]
|
33
|
+
case opts[:method]
|
34
|
+
when :get then @mock_rest_client.get opts[:url], get_params, post_params
|
35
|
+
when :post then @mock_rest_client.post opts[:url], get_params, post_params
|
36
|
+
when :delete then @mock_rest_client.delete opts[:url], get_params, post_params
|
37
|
+
end
|
38
|
+
else
|
39
|
+
original_execute_request(opts)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_response(body, code=200)
|
45
|
+
# When an exception is raised, restclient clobbers method_missing. Hence we
|
46
|
+
# can't just use the stubs interface.
|
47
|
+
body = MultiJson.dump(body) if !(body.kind_of? String)
|
48
|
+
m = mock
|
49
|
+
m.instance_variable_set('@postmaster_values', { :body => body, :code => code })
|
50
|
+
def m.body; @postmaster_values[:body]; end
|
51
|
+
def m.code; @postmaster_values[:code]; end
|
52
|
+
m
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def test_invalid_api_key_error
|
57
|
+
{
|
58
|
+
"error" => {
|
59
|
+
"type" => "invalid_request_error",
|
60
|
+
"message" => "Invalid API Key provided: invalid"
|
61
|
+
}
|
62
|
+
}
|
63
|
+
end
|