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
@@ -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