shippo 1.0.4 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.atom-build.json +22 -0
- data/.codeclimate.yml +30 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +50 -0
- data/Gemfile +5 -0
- data/Guardfile +39 -0
- data/README.md +288 -0
- data/Rakefile +24 -0
- data/bin/example +114 -0
- data/lib/shippo.rb +23 -97
- data/lib/shippo/api.rb +52 -0
- data/lib/shippo/api/api_object.rb +133 -0
- data/lib/shippo/api/category.rb +49 -0
- data/lib/shippo/api/category/base.rb +144 -0
- data/lib/shippo/api/category/purpose.rb +13 -0
- data/lib/shippo/api/category/source.rb +16 -0
- data/lib/shippo/api/category/state.rb +13 -0
- data/lib/shippo/api/category/status.rb +17 -0
- data/lib/shippo/api/extend/operation.rb +21 -0
- data/lib/shippo/api/extend/transformers.rb +12 -0
- data/lib/shippo/api/extend/url.rb +26 -0
- data/lib/shippo/api/operations.rb +8 -0
- data/lib/shippo/api/operations/create.rb +33 -0
- data/lib/shippo/api/operations/list.rb +22 -0
- data/lib/shippo/api/operations/rates.rb +16 -0
- data/lib/shippo/api/operations/update.rb +12 -0
- data/lib/shippo/api/operations/validate.rb +12 -0
- data/lib/shippo/api/request.rb +159 -0
- data/lib/shippo/api/resource.rb +104 -0
- data/lib/shippo/api/transformers/list.rb +73 -0
- data/lib/shippo/api/version.rb +5 -0
- data/lib/shippo/exceptions.rb +7 -0
- data/lib/shippo/exceptions/api_error.rb +20 -0
- data/lib/shippo/exceptions/error.rb +22 -0
- data/lib/shippo/model/address.rb +5 -0
- data/lib/shippo/model/carrieraccount.rb +5 -0
- data/lib/shippo/model/customs_declaration.rb +6 -0
- data/lib/shippo/model/customs_item.rb +5 -0
- data/lib/shippo/model/manifest.rb +5 -0
- data/lib/shippo/model/parcel.rb +5 -0
- data/lib/shippo/model/rate.rb +5 -0
- data/lib/shippo/model/refund.rb +5 -0
- data/lib/shippo/model/shipment.rb +5 -0
- data/lib/shippo/model/transaction.rb +5 -0
- data/lib/shippo/tasks/shippo.rb +22 -0
- data/shippo.gemspec +34 -0
- metadata +226 -40
- data/example.rb +0 -71
- data/lib/shippo/address.rb +0 -10
- data/lib/shippo/api_object.rb +0 -89
- data/lib/shippo/carrieraccount.rb +0 -8
- data/lib/shippo/container_object.rb +0 -28
- data/lib/shippo/create.rb +0 -18
- data/lib/shippo/customs_declaration.rb +0 -7
- data/lib/shippo/customs_item.rb +0 -7
- data/lib/shippo/error.rb +0 -18
- data/lib/shippo/list.rb +0 -23
- data/lib/shippo/manifest.rb +0 -6
- data/lib/shippo/parcel.rb +0 -6
- data/lib/shippo/rate.rb +0 -5
- data/lib/shippo/refund.rb +0 -6
- data/lib/shippo/resource.rb +0 -29
- data/lib/shippo/shipment.rb +0 -14
- data/lib/shippo/transaction.rb +0 -6
- data/lib/shippo/update.rb +0 -15
- data/test/test.rb +0 -26
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
module Shippo
|
3
|
+
module API
|
4
|
+
module Category
|
5
|
+
# Indicates whether a shipment can be used to purchase Labels or only to obtain quote Rates.
|
6
|
+
# Note that if at least one quote Address is passed in the original request,
|
7
|
+
# the Shipment will be eligible for quotes only.
|
8
|
+
class Purpose < Base
|
9
|
+
allowed_values :purchase, :quote
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
module Shippo
|
3
|
+
module API
|
4
|
+
module Category
|
5
|
+
#
|
6
|
+
# +Source+ category describes the origin of an address in the following way:
|
7
|
+
# * +:fully_entered+ addresses only contain user-given values and are eligible for purchasing a label.
|
8
|
+
# * +:partially_entered+ addresses lack some required information and only qualify for requesting rates.
|
9
|
+
# * +:validator+ addresses have been created by the address validation service.
|
10
|
+
#
|
11
|
+
class Source < Base
|
12
|
+
allowed_values :fully_entered, :partially_entered, :validator
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
module Shippo
|
3
|
+
module API
|
4
|
+
module Category
|
5
|
+
# "VALID" shipments contain all required values and can be used to get rates and purchase labels.
|
6
|
+
# "INCOMPLETE" shipments lack required values but can be used for getting rates.
|
7
|
+
# "INVALID" shipments can't be used for getting rates or labels.
|
8
|
+
class State < Base
|
9
|
+
allowed_values :valid, :invalid, :incomplete
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
module Shippo
|
3
|
+
module API
|
4
|
+
module Category
|
5
|
+
# +Status+ is a category class with the following possible values:
|
6
|
+
#
|
7
|
+
# * "Waiting" shipments have been successfully submitted but not yet been processed.
|
8
|
+
# * "Queued" shipments are currently being processed.
|
9
|
+
# * "Success" shipments have been processed successfully,
|
10
|
+
# meaning that rate generation has concluded.
|
11
|
+
# * "Error" does not occur currently and is reserved for future use.
|
12
|
+
class Status < Base
|
13
|
+
allowed_values :waiting, :queued, :success, :error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Shippo
|
2
|
+
module API
|
3
|
+
module Extend
|
4
|
+
module Operation
|
5
|
+
def self.included(klass)
|
6
|
+
klass.instance_eval do
|
7
|
+
class << self
|
8
|
+
def operations(*ops)
|
9
|
+
ops.each do |operation|
|
10
|
+
module_name = "Shippo::API::Operations::#{operation.to_s.capitalize}"
|
11
|
+
# noinspection RubyResolve
|
12
|
+
self.extend(module_name.constantize)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Shippo
|
2
|
+
module API
|
3
|
+
module Extend
|
4
|
+
module Url
|
5
|
+
def self.included(klass)
|
6
|
+
klass.instance_eval do
|
7
|
+
@url = nil
|
8
|
+
class << self
|
9
|
+
# It's a getter and a class-level setter
|
10
|
+
def url(value = nil)
|
11
|
+
return @url if @url
|
12
|
+
@url ||= value if value
|
13
|
+
@url ||= class_to_url
|
14
|
+
end
|
15
|
+
|
16
|
+
def class_to_url
|
17
|
+
words = self.short_name.underscore.split(/_/)
|
18
|
+
words.map { |w| "/#{w == words.last ? w.pluralize : w}" }.join
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'colored2'
|
2
|
+
require 'awesome_print'
|
3
|
+
module Shippo
|
4
|
+
module API
|
5
|
+
module Operations
|
6
|
+
module Create
|
7
|
+
# Creates an item in the database
|
8
|
+
# @param [Hash] params tacked onto the URL as URI parameters
|
9
|
+
def create(params={})
|
10
|
+
api_params = params.dup
|
11
|
+
Hashie::Extensions::StringifyKeys.stringify_keys!(api_params)
|
12
|
+
|
13
|
+
api_params.dup.each { |k, v| api_params[k] = v.id if v.is_a?(::Shippo::API::Resource) && v.id }
|
14
|
+
|
15
|
+
response = Shippo::API.post("#{url}/", api_params)
|
16
|
+
instance = self.from(response)
|
17
|
+
|
18
|
+
debug_log!(api_params, response, instance) if Shippo::API.debug?
|
19
|
+
instance
|
20
|
+
end
|
21
|
+
|
22
|
+
def debug_log!(api_params, response, instance)
|
23
|
+
puts "#{self.name}->create / request : \n".bold.green.underlined
|
24
|
+
ap(api_params)
|
25
|
+
puts "#{self.name}->create / response: \n".bold.yellow.underlined
|
26
|
+
ap(response)
|
27
|
+
puts "#{self.name}->create / from: \n".bold.blue.underlined
|
28
|
+
ap(instance)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Shippo
|
2
|
+
module API
|
3
|
+
module Operations
|
4
|
+
module List
|
5
|
+
# Return all items
|
6
|
+
# @param [Hash] params of additional URI parameters tacked onto the query URL
|
7
|
+
def all(params={})
|
8
|
+
response = Shippo::API.get("#{url}/", params)
|
9
|
+
self.from(response)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Retrieve a concrete item by it's ID
|
13
|
+
# @param [Fixnum] id database ID of the item to be retrieved
|
14
|
+
# @param [Hash] params of additional URI parameters tacked onto the query URL
|
15
|
+
def get(id, params={})
|
16
|
+
response = Shippo::API.get("#{url}/#{CGI.escape(id)}/", params)
|
17
|
+
self.from(response)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Shippo
|
2
|
+
module API
|
3
|
+
module Operations
|
4
|
+
module Rates
|
5
|
+
def rates(currency = nil, params = {})
|
6
|
+
if !currency.nil?
|
7
|
+
response = Shippo::API.get("#{url}/rates/#{currency}/", params)
|
8
|
+
else
|
9
|
+
response = Shippo::API.get("#{url}/rates/", params)
|
10
|
+
end
|
11
|
+
Shippo::Rate.from(response[:results])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
require 'socket'
|
3
|
+
require 'json'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
require 'shippo/exceptions'
|
7
|
+
|
8
|
+
module Shippo
|
9
|
+
module API
|
10
|
+
#
|
11
|
+
# This class is the primary *internal* Interface to the Shippo API.
|
12
|
+
#
|
13
|
+
# Public consumers should use Model API, and perform actions on models
|
14
|
+
# rather than submit requests directly using this class.
|
15
|
+
#
|
16
|
+
# +Request+ instance is created with the intention to execute a single
|
17
|
+
# API call and once executed, it stores +response+ object. Used
|
18
|
+
# requests can not be re-executed.
|
19
|
+
#
|
20
|
+
# == Example
|
21
|
+
#
|
22
|
+
# @request = Shippo::API::Request.new(
|
23
|
+
# method: :get,
|
24
|
+
# uri: '/address,
|
25
|
+
# params: { object_id: 1 },
|
26
|
+
# headers: { 'Last-Modified' => '1213145' }
|
27
|
+
# begin
|
28
|
+
# @response = @request.execute
|
29
|
+
# Shippo::Address.from(@response)
|
30
|
+
# # =>
|
31
|
+
#
|
32
|
+
class Request
|
33
|
+
attr_accessor :username, :password
|
34
|
+
attr_accessor :method, :url, :params, :headers
|
35
|
+
|
36
|
+
# Result of the execute method is stored in #response and #parsed_response
|
37
|
+
attr_accessor :response, :parsed_response
|
38
|
+
|
39
|
+
# @param [symbol] method :get or any other method such as :put, :post, etc.
|
40
|
+
# @param [String] uri URI component appended to the base URL
|
41
|
+
# @param [Hash] params parameters to append to the URL
|
42
|
+
# @param [Hash] headers headers hash sent to the server
|
43
|
+
def initialize(method:, uri:, params: {}, headers: {})
|
44
|
+
self.method = method
|
45
|
+
self.params = params
|
46
|
+
self.headers = headers
|
47
|
+
self.url = api_url(uri)
|
48
|
+
self.response = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def execute
|
52
|
+
raise ArgumentError.new('Response is already defined, create another Request object.') if self.response
|
53
|
+
validate!
|
54
|
+
begin
|
55
|
+
self.response = shippo_phone_home
|
56
|
+
self.parsed_response = JSON::parse(response.body, { symbolize_names: true })
|
57
|
+
|
58
|
+
rescue ::RestClient::Unauthorized => e
|
59
|
+
raise Shippo::Exceptions::AuthenticationError.new(e.message)
|
60
|
+
|
61
|
+
rescue ::JSON::JSONError, ::JSON::ParserError, ::RestClient::BadRequest
|
62
|
+
raise Shippo::Exceptions::APIServerError.new('Unable to read data received back from the server', self)
|
63
|
+
|
64
|
+
rescue ::RestClient::Exception => e
|
65
|
+
raise Shippo::Exceptions::ConnectionError.new(connection_error_message(url, e))
|
66
|
+
|
67
|
+
rescue StandardError => e
|
68
|
+
raise Shippo::Exceptions::ConnectionError.new(connection_error_message(url, e)) if e.message =~ /TCP|connection|getaddrinfo/
|
69
|
+
|
70
|
+
STDERR.puts "#{self.class.name}: Internal error occurred while connecting to #{url}: #{e.message}".bold.red
|
71
|
+
STDERR.puts 'Stack Trace'.bold.yellow.underlined
|
72
|
+
STDERR.puts e.backtrace.join("\n").yellow
|
73
|
+
raise Shippo::Exceptions::Error.new(e)
|
74
|
+
end
|
75
|
+
self.parsed_response
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def shippo_phone_home
|
81
|
+
payload = {}
|
82
|
+
request_url = url
|
83
|
+
(method == :get) ? request_url = params_to_url(params, url) : payload = params.to_json
|
84
|
+
setup_headers!(headers)
|
85
|
+
opts = make_opts!(headers, method, payload, request_url)
|
86
|
+
make_request!(opts)
|
87
|
+
end
|
88
|
+
|
89
|
+
def make_request!(opts)
|
90
|
+
RestClient::Request.execute(opts) { |response, request, result, &block|
|
91
|
+
if [301, 302, 307].include? response.code
|
92
|
+
response.follow_redirection(request, result, &block)
|
93
|
+
else
|
94
|
+
response.return!(request, result, &block)
|
95
|
+
end
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def make_opts!(headers, method, payload, url)
|
100
|
+
{ :headers => headers,
|
101
|
+
:method => method,
|
102
|
+
:payload => payload,
|
103
|
+
:url => url,
|
104
|
+
:open_timeout => 15,
|
105
|
+
:timeout => 30,
|
106
|
+
:user => username,
|
107
|
+
:password => password,
|
108
|
+
:user_agent => 'Shippo/v2.0 RubyBindings'
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def setup_headers!(headers)
|
113
|
+
headers.merge!(
|
114
|
+
:accept => :json,
|
115
|
+
:content_type => :json,
|
116
|
+
:Authorization => "ShippoToken #{token}"
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
def base
|
121
|
+
::Shippo::API.base
|
122
|
+
end
|
123
|
+
|
124
|
+
def token
|
125
|
+
::Shippo::API.token
|
126
|
+
end
|
127
|
+
|
128
|
+
def connection_error_message(url, error)
|
129
|
+
%Q[Could not connect to the Shippo API, via URL
|
130
|
+
#{url}.
|
131
|
+
|
132
|
+
Please check your Internet connection, try again, if the problem
|
133
|
+
persists please contact Shippo Customer Support.
|
134
|
+
|
135
|
+
Error Description:
|
136
|
+
#{error.class.name} ⇨ #{error.message}].gsub(/^\s*/, '')
|
137
|
+
end
|
138
|
+
|
139
|
+
def api_url(uri_component = '')
|
140
|
+
base + uri_component
|
141
|
+
end
|
142
|
+
|
143
|
+
def params_to_url(params, url)
|
144
|
+
pairs = []
|
145
|
+
params.each { |k, v|
|
146
|
+
pairs.push "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
|
147
|
+
}
|
148
|
+
url += "?#{pairs.join('&')}" unless pairs.empty?
|
149
|
+
url
|
150
|
+
end
|
151
|
+
|
152
|
+
def validate!
|
153
|
+
raise Shippo::Exceptions::AuthenticationError.new(
|
154
|
+
'API credentials seems to be missing, perhaps you forgot to set Shippo::API.token?') \
|
155
|
+
unless token
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
require 'hashie/mash'
|
4
|
+
require 'active_support/inflector'
|
5
|
+
|
6
|
+
require 'shippo/exceptions'
|
7
|
+
|
8
|
+
require_relative 'api_object'
|
9
|
+
require_relative 'category/status'
|
10
|
+
require_relative 'transformers/list'
|
11
|
+
require_relative 'extend/operation'
|
12
|
+
require_relative 'extend/transformers'
|
13
|
+
require_relative 'extend/url'
|
14
|
+
|
15
|
+
module Shippo
|
16
|
+
module API
|
17
|
+
class Resource < Hashie::Mash
|
18
|
+
include Hashie::Extensions::StringifyKeys
|
19
|
+
include Enumerable
|
20
|
+
extend Forwardable
|
21
|
+
|
22
|
+
def self.object_properties
|
23
|
+
Shippo::API::ApiObject::PROPS
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_accessor :object
|
27
|
+
def_delegators :@object, *object_properties
|
28
|
+
|
29
|
+
# Creates a possibly recursive chain (map of lists, etc) of Resource
|
30
|
+
# instances based on whether each value is a scalar, array or a hash.
|
31
|
+
def self.from(values)
|
32
|
+
# recursive on arrays
|
33
|
+
if values.is_a?(Array)
|
34
|
+
values.map { |list| from(list) }
|
35
|
+
elsif values.respond_to?(:keys) # a Hash or a Hash derivative
|
36
|
+
new(values)
|
37
|
+
else
|
38
|
+
values
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.short_name(name = self.name)
|
43
|
+
name.split('::')[-1]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Generate object_ accessors.
|
47
|
+
object_properties.each do |property|
|
48
|
+
method_name = ApiObject.field_name(property)
|
49
|
+
define_method method_name do
|
50
|
+
STDOUT.puts "#{method_name} style accessors are deprecated in favor of #resource.object.#{property}" if Shippo::API.warnings
|
51
|
+
self.object.send(property)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# allows resources to use default or a custom url
|
56
|
+
include Shippo::API::Extend::Url
|
57
|
+
# allows resources to set supported operations
|
58
|
+
include Shippo::API::Extend::Operation
|
59
|
+
# adds #transformers method that enumerates available transformers
|
60
|
+
# of the hashes into other types.
|
61
|
+
include Shippo::API::Extend::Transformers
|
62
|
+
|
63
|
+
# As a Hashie::Mash subclass, Resource can initialize from another hash
|
64
|
+
def initialize(*args)
|
65
|
+
if args.first.is_a?(Fixnum) or
|
66
|
+
(args.first.is_a?(String) && args.first =~ /^[0-9A-Fa-f]+$/)
|
67
|
+
self.id = args.first
|
68
|
+
elsif args.first.respond_to?(:keys)
|
69
|
+
h = Hashie::Mash.new(args.first)
|
70
|
+
self.deep_merge!(h)
|
71
|
+
self.object = ApiObject.create_object(self)
|
72
|
+
transformers.each do |transformer|
|
73
|
+
transformer.new(self).transform
|
74
|
+
end
|
75
|
+
else
|
76
|
+
super(*args)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
"#<#{self.class.short_name}:0x#{self.object_id.to_s(16)}#{id.nil? ? '' : "[id=#{id}]"}#{to_hash.inspect}->#{object.inspect}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_s
|
85
|
+
self.class.short_name + self.to_hash.to_s + '->' + self.object.to_s
|
86
|
+
end
|
87
|
+
|
88
|
+
def url
|
89
|
+
raise Shippo::Exceptions::MissingDataError.new("#{self.class} has no object_id") unless id
|
90
|
+
"#{self.class.url}/#{CGI.escape(id)}"
|
91
|
+
end
|
92
|
+
|
93
|
+
def refresh
|
94
|
+
response = Shippo::API.get(url)
|
95
|
+
self.from(response)
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
def success?
|
100
|
+
self.object && self.object.status && self.object.status.eql?(Shippo::API::Category::Status::SUCCESS)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|