shippo 1.0.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.atom-build.json +22 -0
  3. data/.codeclimate.yml +30 -0
  4. data/.gitignore +22 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +1156 -0
  7. data/.travis.yml +16 -0
  8. data/CHANGELOG.md +50 -0
  9. data/Gemfile +5 -0
  10. data/Guardfile +39 -0
  11. data/README.md +288 -0
  12. data/Rakefile +24 -0
  13. data/bin/example +114 -0
  14. data/lib/shippo.rb +23 -97
  15. data/lib/shippo/api.rb +52 -0
  16. data/lib/shippo/api/api_object.rb +133 -0
  17. data/lib/shippo/api/category.rb +49 -0
  18. data/lib/shippo/api/category/base.rb +144 -0
  19. data/lib/shippo/api/category/purpose.rb +13 -0
  20. data/lib/shippo/api/category/source.rb +16 -0
  21. data/lib/shippo/api/category/state.rb +13 -0
  22. data/lib/shippo/api/category/status.rb +17 -0
  23. data/lib/shippo/api/extend/operation.rb +21 -0
  24. data/lib/shippo/api/extend/transformers.rb +12 -0
  25. data/lib/shippo/api/extend/url.rb +26 -0
  26. data/lib/shippo/api/operations.rb +8 -0
  27. data/lib/shippo/api/operations/create.rb +33 -0
  28. data/lib/shippo/api/operations/list.rb +22 -0
  29. data/lib/shippo/api/operations/rates.rb +16 -0
  30. data/lib/shippo/api/operations/update.rb +12 -0
  31. data/lib/shippo/api/operations/validate.rb +12 -0
  32. data/lib/shippo/api/request.rb +159 -0
  33. data/lib/shippo/api/resource.rb +104 -0
  34. data/lib/shippo/api/transformers/list.rb +73 -0
  35. data/lib/shippo/api/version.rb +5 -0
  36. data/lib/shippo/exceptions.rb +7 -0
  37. data/lib/shippo/exceptions/api_error.rb +20 -0
  38. data/lib/shippo/exceptions/error.rb +22 -0
  39. data/lib/shippo/model/address.rb +5 -0
  40. data/lib/shippo/model/carrieraccount.rb +5 -0
  41. data/lib/shippo/model/customs_declaration.rb +6 -0
  42. data/lib/shippo/model/customs_item.rb +5 -0
  43. data/lib/shippo/model/manifest.rb +5 -0
  44. data/lib/shippo/model/parcel.rb +5 -0
  45. data/lib/shippo/model/rate.rb +5 -0
  46. data/lib/shippo/model/refund.rb +5 -0
  47. data/lib/shippo/model/shipment.rb +5 -0
  48. data/lib/shippo/model/transaction.rb +5 -0
  49. data/lib/shippo/tasks/shippo.rb +22 -0
  50. data/shippo.gemspec +34 -0
  51. metadata +226 -40
  52. data/example.rb +0 -71
  53. data/lib/shippo/address.rb +0 -10
  54. data/lib/shippo/api_object.rb +0 -89
  55. data/lib/shippo/carrieraccount.rb +0 -8
  56. data/lib/shippo/container_object.rb +0 -28
  57. data/lib/shippo/create.rb +0 -18
  58. data/lib/shippo/customs_declaration.rb +0 -7
  59. data/lib/shippo/customs_item.rb +0 -7
  60. data/lib/shippo/error.rb +0 -18
  61. data/lib/shippo/list.rb +0 -23
  62. data/lib/shippo/manifest.rb +0 -6
  63. data/lib/shippo/parcel.rb +0 -6
  64. data/lib/shippo/rate.rb +0 -5
  65. data/lib/shippo/refund.rb +0 -6
  66. data/lib/shippo/resource.rb +0 -29
  67. data/lib/shippo/shipment.rb +0 -14
  68. data/lib/shippo/transaction.rb +0 -6
  69. data/lib/shippo/update.rb +0 -15
  70. 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,12 @@
1
+ require_relative '../transformers/list.rb'
2
+ module Shippo
3
+ module API
4
+ module Extend
5
+ module Transformers
6
+ def transformers
7
+ @transformers ||= [ Shippo::API::Transformers::List ].freeze
8
+ end
9
+ end
10
+ end
11
+ end
12
+ 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,8 @@
1
+ module Shippo
2
+ module API
3
+ module Operations
4
+ end
5
+ end
6
+ end
7
+
8
+ Shippo.dir('shippo/api/operations')
@@ -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,12 @@
1
+ module Shippo
2
+ module API
3
+ module Operations
4
+ module Update
5
+ def update(object_id, params={})
6
+ response = Shippo::API.put("#{url}/#{CGI.escape(object_id)}/", params)
7
+ self.from(response)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Shippo
2
+ module API
3
+ module Operations
4
+ module Validate
5
+ def validate(params={})
6
+ response = Shippo::API.get("#{url}/validate/", params)
7
+ Shippo::Address.from(response)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ 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