shippo 1.0.4 → 2.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.
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
@@ -1,104 +1,30 @@
1
- require 'rest_client'
2
- require 'json'
3
- require 'set'
4
-
5
- require_relative 'shippo/error.rb'
6
- require_relative 'shippo/container_object.rb'
7
- require_relative 'shippo/api_object.rb'
8
- require_relative 'shippo/resource.rb'
9
-
10
- # api operations
11
- require_relative 'shippo/create.rb'
12
- require_relative 'shippo/update.rb'
13
- require_relative 'shippo/list.rb'
14
-
15
-
16
- # api objects
17
- require_relative 'shippo/address.rb'
18
- require_relative 'shippo/carrieraccount.rb'
19
- require_relative 'shippo/customs_item.rb'
20
- require_relative 'shippo/customs_declaration.rb'
21
- require_relative 'shippo/manifest.rb'
22
- require_relative 'shippo/parcel.rb'
23
- require_relative 'shippo/rate.rb'
24
- require_relative 'shippo/refund.rb'
25
- require_relative 'shippo/shipment.rb'
26
- require_relative 'shippo/transaction.rb'
1
+ #
2
+ # +Shippo+ is the ruby module enclosing all and any ruby-based
3
+ # functionality developed by Shippo, Inc.
4
+ #
5
+ # This gem providers wrappers for Shippo API in ruby. You are
6
+ # not required to use any particular library to access Shippo
7
+ # API, but it is our hope that this gem helps you bootstrap your
8
+ # Shippo integration.
9
+ #
10
+
11
+ require 'require_dir'
27
12
 
28
13
  module Shippo
29
- @api_base = 'https://api.goshippo.com/v1'
30
- @api_version = 1.0
31
- @api_token = ''
32
-
33
- class << self
34
- attr_accessor :api_base, :api_version, :api_token
35
- end
14
+ extend ::RequireDir
15
+ init_from_source __FILE__
16
+ end
36
17
 
37
- def self.api_url(url='')
38
- @api_base + url
39
- end
18
+ require 'shippo/api'
40
19
 
41
- def self.request(method, url, params = {}, headers = {})
42
- if @api_token.empty?
43
- raise AuthError.new("API credentials missing! Make sure to set Shippo.api_token")
44
- end
45
- begin
46
- payload = {}
47
- url = api_url(url)
48
- headers.merge!(
49
- :accept => :json,
50
- :content_type => :json,
51
- :Authorization => "ShippoToken #{@api_token}"
52
- )
53
- case method
54
- when :get
55
- pairs = []
56
- params.each { |k, v|
57
- pairs.push "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
58
- }
59
- url += "?#{pairs.join('&')}" unless pairs.empty?
60
- else
61
- payload = params.to_json
62
- end
63
- opts = { :headers => headers,
64
- :method => method,
65
- :payload => payload,
66
- :url => url,
67
- :open_timeout => 15,
68
- :timeout => 30,
69
- :user => @api_user,
70
- :password => @api_pass,
71
- :user_agent => "Shippo/v1 RubyBindings"
72
- }
73
- res = make_request(opts)
74
- rescue => e
75
- case e
76
- when RestClient::ServerBrokeConnection, RestClient::RequestTimeout
77
- msg = "Could not connect to the Shippo API at #{@api_base}. " +
78
- "Please proceed to check your connection, try again and " +
79
- "contact Shippo support should the issue persist."
80
- raise ConnectionError.new msg + "\n\n(e.message)"
81
- when SocketError
82
- msg = "Unexpected error connecting to the Shippo API at #{@api_base}."
83
- when RestClient::ExceptionWithResponse
84
- msg = "error: #{e} #{e.http_body}"
85
- else
86
- msg = "error: #{e}"
87
- end
88
- raise APIError.new msg
89
- end
90
- parse(res)
20
+ # Backwards compatibility
21
+ module Shippo
22
+ def self.api_key(value)
23
+ ::Shippo::API.token = value
91
24
  end
92
- def self.parse(response)
93
- JSON::parse(response.body, { :symbolize_names => true })
25
+ class << self
26
+ alias_method :api_key=, :api_key
27
+ alias_method :api_token=, :api_key
94
28
  end
95
29
  end
96
- def make_request(opts)
97
- RestClient::Request.execute(opts){ |response, request, result, &block|
98
- if [301, 302, 307].include? response.code
99
- response.follow_redirection(request, result, &block)
100
- else
101
- response.return!(request, result, &block)
102
- end
103
- }
104
- end
30
+
@@ -0,0 +1,52 @@
1
+ require 'rest_client'
2
+ require 'json'
3
+ require 'set'
4
+
5
+ require_relative '../shippo' unless defined?(Shippo) && Shippo.respond_to?(:dir_r)
6
+
7
+ require 'shippo/exceptions'
8
+ require 'shippo/api/category'
9
+ require 'shippo/api/request'
10
+ require 'shippo/api/resource'
11
+
12
+ module Shippo
13
+ module API
14
+ @base = 'https://api.goshippo.com/v1'
15
+ @version = 1.0
16
+ @token = ''
17
+ @debug = Integer(ENV['SHIPPO_DEBUG'] || 0) > 0 ? true : false
18
+ @warnings = true
19
+
20
+ class << self
21
+ attr_accessor :base, :version, :token, :debug, :warnings
22
+ # @param [Symbol] method One of :get, :put, :post
23
+ # @param [String] uri the URL component after the first slash but before params
24
+ # @param [Hash] params hash of optional parameters to add to the URL
25
+ # @param [Hash] headers optionally added headers
26
+ def request(method, uri, params = {}, headers = {})
27
+ ::Shippo::API::Request.new(method: method,
28
+ uri: uri,
29
+ params: params,
30
+ headers: headers).execute
31
+ end
32
+
33
+ %i[get put post].each do |method|
34
+ define_method method do |*args|
35
+ uri, params, headers = *args
36
+ request(method, uri, params || {}, headers || {})
37
+ end
38
+ end
39
+
40
+ def debug?
41
+ Shippo::API.debug
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ Shippo.dir('shippo/api')
48
+ Shippo.dir('shippo/api/transformers')
49
+ Shippo.dir('shippo/api/extend')
50
+ Shippo.dir('shippo/model')
51
+
52
+
@@ -0,0 +1,133 @@
1
+ require 'hashie/dash'
2
+ require 'hashie/extensions/stringify_keys'
3
+ require 'hashie/extensions/symbolize_keys'
4
+ require 'hashie/extensions/dash/property_translation'
5
+ require 'active_support/inflector'
6
+
7
+ module Shippo
8
+ module API
9
+ # +ApiObject+ is a class that contains only a set of specific fields
10
+ # that can be used in both requests and responses from Shippo API.
11
+ # Upon return with each response, +ApiObject+ instance is automatically
12
+ # created and populated with fields that begin with a prefix `object_`.
13
+ # The prefix is deleted, and the fields are made available through the
14
+ # non-prefixed accessors.
15
+ #
16
+ # This class uses +Hashie::Dash+ under the hood, in order to provide convenient
17
+ # transformations, support required/optional attributes, etc.
18
+ #
19
+ #
20
+ # == Example
21
+ #
22
+ # ```ruby
23
+ # response = {
24
+ # "object_state" => "VALID",
25
+ # "object_purpose" => "PURCHASE",
26
+ # "object_source" => "FULLY_ENTERED",
27
+ # "object_created" => "2014-07-16T23:20:31.089Z",
28
+ # "object_updated" => "2014-07-16T23:20:31.089Z",
29
+ # "object_id" => "747207de2ba64443b645d08388d0309c",
30
+ # "object_owner" => "shippotle@goshippo.com",
31
+ # "name" => "Shawn Ippotle",
32
+ # "company" => "Shippo",
33
+ # "street1" => "215 Clayton St.",
34
+ # "street2" => "",
35
+ # "city" => "San Francisco",
36
+ # "state" => "CA",
37
+ # "zip" => "94117",
38
+ # "country" => "US",
39
+ # "phone" => "+1 555 341 9393",
40
+ # "email" => "shippotle@goshippo.com"
41
+ # }
42
+ #
43
+ # require 'shippo'
44
+ # address = Shippo::Address.from(response)
45
+ # address.name
46
+ # # ⤷ Shawn Ippotle
47
+ # require 'ap'
48
+ # ap address.object
49
+ # # ⤷
50
+ # {
51
+ # :state => #<Shippo::API::Category::State:0x007fd374b4d0d0 @name=:state, @value=:valid>,
52
+ # :purpose => #<Shippo::API::Category::Purpose:0x007fd373df2070 @name=:purpose, @value=:purchase>,
53
+ # :source => #<Shippo::API::Category::Source:0x007fd374b4fbf0 @name=:source, @value=:fully_entered>,
54
+ # :created => 2014-07-16 23:20:31 UTC,
55
+ # :updated => 2014-07-16 23:20:31 UTC,
56
+ # :id => "747207de2ba64443b645d08388d0309c",
57
+ # :owner => "shippotle@goshippo.com"
58
+ # }
59
+ # ```
60
+ class ApiObject < Hashie::Dash
61
+ include Hashie::Extensions::Dash::PropertyTranslation
62
+
63
+ PREFIX = { id: 'resource_', default: 'object_' }.freeze
64
+
65
+ class << self
66
+ def field_name(property)
67
+ "#{PREFIX[property.to_sym] || PREFIX[:default]}#{property}".to_sym
68
+ end
69
+
70
+ def matches_prefix?(value)
71
+ %r[^(#{PREFIX.values.join('|')})].match(value.to_s)
72
+ end
73
+
74
+ def mk_opts(property)
75
+ { with: ->(value) { value }, from: "#{field_name(property)}".to_sym, required: false }
76
+ end
77
+ end
78
+
79
+ # list of allowed properties, of a given type.
80
+ PROPS_ID = %i(id).freeze
81
+ PROPS_CATEG = %i(state purpose source status).freeze
82
+ PROPS_EMAIL = %i(owner).freeze
83
+ PROPS_TIMED = %i(created updated).freeze
84
+
85
+ PROPS = (PROPS_ID + PROPS_EMAIL + PROPS_TIMED + PROPS_CATEG ).flatten.freeze
86
+ PROPS_AS_IS = (PROPS_EMAIL + PROPS_ID).freeze
87
+
88
+ def self.setup_property(prop, custom = {})
89
+ property prop, self.mk_opts(prop).merge(custom)
90
+ end
91
+
92
+ PROPS_AS_IS.each { |prop| setup_property(prop) }
93
+ PROPS_TIMED.each { |prop| setup_property(prop, with: ->(value) { Time.parse(value) } ) }
94
+ PROPS_EMAIL.each { |prop| setup_property(prop, with: ->(value) {
95
+ value && value.strip!
96
+ value = "#{value}@gmail.com" if value and value !~ %r[.*@.*\..*]
97
+ value
98
+ })
99
+ }
100
+ PROPS_CATEG.each { |prop| setup_property(prop, with: ->(value) {
101
+ Shippo::API::Category.for(prop, value)
102
+ })
103
+ }
104
+
105
+ def self.create_object(h)
106
+ object_keys = h.keys.select { |k| matches_prefix?(k) }
107
+ h_object = {}
108
+ object_keys.each { |k| h_object[k.to_s] = h[k] }
109
+ instance = self.new(h_object)
110
+ object_keys.each { |k| h.delete(k) if h.key(k) }
111
+ instance
112
+ end
113
+
114
+ def initialize(*args)
115
+ opts = args.first
116
+ if opts && opts.respond_to?(:keys)
117
+ Hashie::Extensions::SymbolizeKeys.symbolize_keys!(opts)
118
+ if opts[:object_id]
119
+ opts[(PREFIX[:id] + 'id').to_sym] = opts[:object_id]
120
+ opts.delete(:object_id)
121
+ end
122
+ super(opts)
123
+ else
124
+ super(args)
125
+ end
126
+ end
127
+
128
+ def to_s
129
+ Shippo::API::Resource.short_name(self.class.name) + self.to_hash.to_s
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,49 @@
1
+ require 'hashie/dash'
2
+ require 'active_support/inflector'
3
+ require 'shippo/exceptions'
4
+ module Shippo
5
+ module API
6
+ # For enumerations with discrete possible set of values, +Category+ class
7
+ # offers it's subclasses and users tremendous benefits.
8
+ #
9
+ # Categories should be always created via the Facåde
10
+ # +Shippo::API::Category.for(name, value)+. Although it is possible to
11
+ # directly instantiate subclasses, it is not recommended for performance reasons.
12
+ #
13
+ # == Example
14
+ #
15
+ # ```ruby
16
+ # require 'shippo/api'
17
+ # class My::Big::Module::Size < ::Shippo::API::Category::Base
18
+ # allowed_values :small, :medium, :large, :xlarge, :xxlarge
19
+ # end
20
+ # # ⤷ [:small, :medium, :large, :xlarge, :xxlarge]
21
+ #
22
+ # my_size = Shippo::API::Category.for('size', 'xlarge')
23
+ # # ⤷ XLARGE
24
+ # my_size.class.name
25
+ # # ⤷ My::Big::Module::Size
26
+ # ```
27
+
28
+ module Category
29
+ @categories = {}
30
+ class << self
31
+ attr_accessor :categories
32
+ end
33
+
34
+ class DuplicateValueError < ::Shippo::Exceptions::APIError;
35
+ end
36
+
37
+ def self.key(value)
38
+ value.to_s.downcase.to_sym
39
+ end
40
+
41
+ def self.for(name, value)
42
+ cat = self.categories[name.to_s.downcase.to_sym]
43
+ cat ? cat[value.to_s.downcase.to_sym] : nil
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ Shippo.dir('shippo/api/category')
@@ -0,0 +1,144 @@
1
+ module Shippo
2
+ module API
3
+ module Category
4
+ # +Base+ is a convenience abstract class that provides the following functionality
5
+ # to it's subclasses, which are meant to be enumerations with a fixed number of possible
6
+ # values.
7
+ #
8
+ # Base populates a global hash +Shippo::API::Category.categories+ which is keyed by the
9
+ # lower cased and symbolized category name (eg, :status or :purpose), each value is another
10
+ # hash consisting of keys (values of each category, eg :success, :error) and value being the
11
+ # constant created for such a value.
12
+ #
13
+ # __WARNING__: You are not supposed to instantiate these classes, to be honest. The
14
+ # "correct" way is via the Facåde +Shippo::API::Category.for(name, value)+.
15
+ #
16
+ # == Example
17
+ #
18
+ # ```ruby
19
+ # require 'shippo/api'
20
+ # class Size < ::Shippo::API::Category::Base
21
+ # allowed_values :small, :medium, :large, :xlarge, :xxlarge
22
+ # end
23
+ # # ⤷ [:small, :medium, :large, :xlarge, :xxlarge]
24
+ #
25
+ # my_size = Shippo::API::Category.for('size', 'xlarge')
26
+ # # ⤷ size:xlarge
27
+ #
28
+ # my_size.xlarge?
29
+ # # ⤷ true
30
+ # my_size.medium?
31
+ # # ⤷ false
32
+ # my_size.name
33
+ # # ⤷ size
34
+ # my_size.value
35
+ # # ⤷ xlarge
36
+ #
37
+ # medium_1 = Size.new('medium')
38
+ # ⤷ size:medium
39
+ # medium_2 = Size.new('medium')
40
+ # ⤷ size:medium
41
+ # medium_3 = Shippo::API::Category.for(:size, :medium)
42
+ # ⤷ size:medium
43
+ #
44
+ # # But keep in mind these instances are all different objects,
45
+ # # which are +eql?()+ to each other, but not identical.
46
+ # medium_1.object_id
47
+ # # ⤷ 70282669124280
48
+ # medium_2.object_id
49
+ # # ⤷ 70282669580500
50
+ # medium_3.object_id
51
+ # # ⤷ 70282681963740
52
+ # medium_1.eql?(medium_2)
53
+ # # ⤷ true
54
+ # medium_2.eql?(medium_3)
55
+ # # ⤷ true
56
+ # ```
57
+ #
58
+ class Base
59
+
60
+ @categories = Shippo::API::Category.categories
61
+ class << self
62
+ attr_accessor :categories
63
+ end
64
+
65
+ def self.inherited(klazz)
66
+ klazz.instance_eval do
67
+ @allowed_values = Set.new
68
+ class << self
69
+ def categories
70
+ ::Shippo::API::Category::Base.categories
71
+ end
72
+
73
+ def value_transformer(values)
74
+ values.map(&:downcase).map(&:to_sym)
75
+ end
76
+
77
+ def allowed_values(*values)
78
+ return @allowed_values if values.nil? || values.empty? || !@allowed_values.empty?
79
+
80
+ @allowed_values = self.value_transformer(values)
81
+ @allowed_values.each do |allowed_value|
82
+ category_value = Category.key(allowed_value)
83
+ category_const = category_value.to_s.upcase
84
+
85
+ raise ::Shippo::API::Category::DuplicateValueError.new(
86
+ "Constant #{category_const} is already defined in #{self.name}") if self.const_defined?(category_const)
87
+
88
+ new_instance = self.new(category_value)
89
+ self.const_set(category_const, new_instance)
90
+
91
+ define_method "#{category_value}?".to_sym do
92
+ value.eql?(category_value)
93
+ end
94
+
95
+ categories[new_instance.name] ||= {}
96
+ categories[new_instance.name][new_instance.value] = new_instance
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ attr_accessor :name, :value
104
+
105
+ def initialize(value)
106
+ raise ::Shippo::Exceptions::AbstractClassInitError.new('Can not instantiate Base!') if self.class.eql?(::Shippo::API::Category::Base)
107
+ self.name = Category.key(self.class.name.gsub(%r{.*::}, ''))
108
+ self.value = assign_value(value)
109
+ end
110
+
111
+ def eql?(other)
112
+ self.class.eql?(other.class) && self.value.eql?(other.value)
113
+ end
114
+
115
+ def to_s
116
+ "#{self.value.upcase}"
117
+ end
118
+
119
+ private
120
+
121
+ def value_allowed?(value)
122
+ self.class.allowed_values.include?(value)
123
+ end
124
+
125
+ def assign_value(value)
126
+ value = clean(value)
127
+
128
+ if value_allowed?(value)
129
+ @value = value
130
+ else
131
+ raise ::Shippo::Exceptions::InvalidCategoryValueError.new(
132
+ "Value #{value} is not allowed for Category #{self.class.name}, allowed values are: #{self.class.allowed_values})")
133
+ end
134
+ @value
135
+ end
136
+
137
+ def clean(value)
138
+ Category.key(value)
139
+ end
140
+
141
+ end
142
+ end
143
+ end
144
+ end