shipengine_sdk 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +96 -0
  4. data/lib/faraday/raise_http_exception.rb +77 -0
  5. data/lib/shipengine/configuration.rb +43 -0
  6. data/lib/shipengine/constants/base.rb +22 -0
  7. data/lib/shipengine/constants/countries.rb +16 -0
  8. data/lib/shipengine/constants.rb +4 -0
  9. data/lib/shipengine/domain/addresses/address_validation.rb +118 -0
  10. data/lib/shipengine/domain/addresses.rb +76 -0
  11. data/lib/shipengine/domain/carriers/list_carriers.rb +140 -0
  12. data/lib/shipengine/domain/carriers.rb +93 -0
  13. data/lib/shipengine/domain/labels/create_from_rate.rb +163 -0
  14. data/lib/shipengine/domain/labels/create_from_shipment_details.rb +163 -0
  15. data/lib/shipengine/domain/labels/void_label.rb +18 -0
  16. data/lib/shipengine/domain/labels.rb +297 -0
  17. data/lib/shipengine/domain/rates/get_with_shipment_details.rb +347 -0
  18. data/lib/shipengine/domain/rates.rb +379 -0
  19. data/lib/shipengine/domain/tracking/track_using_carrier_code_and_tracking_number.rb +45 -0
  20. data/lib/shipengine/domain/tracking/track_using_label_id.rb +45 -0
  21. data/lib/shipengine/domain/tracking.rb +103 -0
  22. data/lib/shipengine/domain.rb +7 -0
  23. data/lib/shipengine/exceptions/error_code.rb +254 -0
  24. data/lib/shipengine/exceptions/error_type.rb +49 -0
  25. data/lib/shipengine/exceptions.rb +132 -0
  26. data/lib/shipengine/internal_client.rb +91 -0
  27. data/lib/shipengine/utils/base58.rb +109 -0
  28. data/lib/shipengine/utils/pretty_print.rb +29 -0
  29. data/lib/shipengine/utils/request_id.rb +16 -0
  30. data/lib/shipengine/utils/user_agent.rb +24 -0
  31. data/lib/shipengine/utils/validate.rb +106 -0
  32. data/lib/shipengine/version.rb +5 -0
  33. data/lib/shipengine.rb +164 -0
  34. metadata +117 -0
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShipEngine
4
+ module Exceptions
5
+ ##
6
+ ## This class has the ability to return a specific error code.
7
+ ##
8
+ ## @see https://www.shipengine.com/docs/errors/codes/#error-code
9
+ # #/
10
+ class ErrorCode
11
+ # @param [Symbol] key
12
+ # @return [Symbol] error code
13
+ def self.get(key)
14
+ @codes[key]
15
+ end
16
+
17
+ # @param [String] str_key
18
+ # @return [Symbol] error code
19
+ def self.get_by_str(str_key)
20
+ get(str_key.upcase.to_sym)
21
+ end
22
+
23
+ @codes = {
24
+
25
+ ###############################
26
+
27
+ MINIMUM_POSTAL_CODE_VERIFICATION_FAILED: 'minimum_postal_code_verification_failed',
28
+
29
+ ##
30
+ ## Only certain carriers support pre-paid balances. So you can only add funds
31
+ ## to those carriers. If you attempt to add funds to a carrier that doesn't
32
+ ## support it then you'll get this error code.
33
+ # #/
34
+ AUTO_FUND_NOT_SUPPORTED: 'auto_fund_not_supported',
35
+
36
+ ##
37
+ ## Once a batch has started processing it cannot be modified. Attempting to
38
+ ## modify it will cause this error.
39
+ # #/
40
+ BATCH_CANNOT_BE_MODIFIED: 'batch_cannot_be_modified',
41
+
42
+ ##
43
+ ## You attempted to perform an operation on multiple shipments from different
44
+ ## carriers. Try performing separate operations for each carrier instead.
45
+ # #/
46
+
47
+ ##
48
+ ## This error means that you're trying to use a carrier that hasn't been setup
49
+ ## yet. You can setup carriers from your ShipEngine dashboard or via the API.
50
+ # #/
51
+ CARRIER_NOT_CONNECTED: 'carrier_not_connected',
52
+
53
+ ##
54
+ ## The operation you are performing isn't supported by the specified carrier.
55
+ # #/
56
+ CARRIER_NOT_SUPPORTED: 'carrier_not_supported',
57
+
58
+ ##
59
+ ## Some forms of delivery confirmation aren't supported by some carriers.
60
+ ## This error means that the combination of carrier and delivery confirmation
61
+ ## are not supported.
62
+ # #/
63
+ CONFIRMATION_NOT_SUPPORTED: 'confirmation_not_supported',
64
+
65
+ ##
66
+ ## This error means that two or more fields in your API request are mutually
67
+ ## exclusive or contain conflicting values. The error will include a fields
68
+ ## array that lists the conflicting fields.
69
+ # #/
70
+ FIELD_CONFLICT: 'field_conflict',
71
+
72
+ ##
73
+ ## A required field is missing or empty. The field_name property indicates
74
+ ## which field is missing. Note that some fields are conditionally required
75
+ ## based on the values of other fields or the type of operation being performed.
76
+ # #/
77
+ FIELD_VALUE_REQUIRED: 'field_value_required',
78
+
79
+ ##
80
+ ## You attempted to perform an operation that you don't have permissions to do.
81
+ ## Check your API key to ensure that you're using the correct one. Or contact
82
+ ## our support team to ensure that your account has the necessary permissions.
83
+ # #/
84
+ FORBIDDEN: 'forbidden',
85
+
86
+ ##
87
+ ## A few parts of the ShipEngine API allow you to provide your own ID for resources.
88
+ ## These IDs must be unique; otherwise you'll get this error code.
89
+ # #/
90
+ IDENTIFIER_CONFLICT: 'identifier_conflict',
91
+
92
+ ##
93
+ ## When updating a resource (such as a shipment or warehouse) the ID in the URL
94
+ ## and in the request body must match.
95
+ # #/
96
+ IDENTIFIERS_MUST_MATCH: 'identifiers_must_match',
97
+
98
+ ##
99
+ ## When creating a return label you can optionally pair it to an outbound_label_id.
100
+ ## The outbound label must be from the same carrier as the return label.
101
+ # #/
102
+ INCOMPATIBLE_PAIRED_LABELS: 'incompatible_paired_labels',
103
+
104
+ ##
105
+ ## The mailing address that you provided is invalid. Try using our address
106
+ ## validation API to verify addresses before using them.
107
+ # #/
108
+ INVALID_ADDRESS: 'invalid_address',
109
+
110
+ ##
111
+ ## You attempted to perform an operation that isn't allowed for your billing plan.
112
+ ## Contact our sales team for assistance.
113
+ # #/
114
+ INVALID_BILLING_PLAN: 'invalid_billing_plan',
115
+
116
+ ##
117
+ ## When creating a label or creating a return label if you set the charge_event
118
+ ## field to a value that isn't offered by the carrier then you will receive this
119
+ ## error. You can leave the charge_event field unset or set it to carrier_default
120
+ ## instead.
121
+ # #/
122
+ INVALID_CHARGE_EVENT: 'invalid_charge_event',
123
+
124
+ ##
125
+ ## One of the fields in your API request has an invalid value. The field_name
126
+ ## property indicates which field is invalid.
127
+ # #/
128
+ INVALID_FIELD_VALUE: 'invalid_field_value',
129
+
130
+ ##
131
+ ## This error is similar to invalid_field_value but is specifically for ID
132
+ ## fields such as label_id shipment_id carrier_id etc. The field_name
133
+ ## property indicates which field is invalid.
134
+ # #/
135
+ INVALID_IDENTIFIER: 'invalid_identifier',
136
+
137
+ ##
138
+ ## The operation you're attempting to perform is not allowed because the resource
139
+ ## is in the wrong status. For example if a label's status is "voided" then
140
+ ## it cannot be included in a manifest.
141
+ # #/
142
+ INVALID_STATUS: 'invalid_status',
143
+
144
+ ##
145
+ ## A string field in your API request is either too short or too long. The
146
+ ## field_name property indicates which field is invalid and the min_length
147
+ ## and max_length properties indicate the allowed length.
148
+ # #/
149
+ INVALID_STRING_LENGTH: 'invalid_string_length',
150
+
151
+ ##
152
+ ## Not all carriers allow you to add custom images to labels. You can only set
153
+ ## the label_image_id for supported carriers
154
+ # #/
155
+ LABEL_IMAGES_NOT_SUPPORTED: 'label_images_not_supported',
156
+
157
+ ##
158
+ ## This error indicates a problem with your FedEx account. Please contact
159
+ ## FedEx to resolve the issue.
160
+ # #/
161
+ METER_FAILURE: 'meter_failure',
162
+
163
+ ##
164
+ ## The ShipEngine API endpoint that was requested does not exist.
165
+ # #/
166
+ NOT_FOUND: 'not_found',
167
+
168
+ ##
169
+ ## You have exceeded a rate limit. Check the the error_source field to determine
170
+ ## whether the rate limit was imposed by ShipEngine or by a third-party such
171
+ ## as a carrier. If the rate limit is from ShipEngine then consider using bulk
172
+ ## operations to reduce the nuber of API calls or contact our support team
173
+ ## about increasing your rate limit.
174
+ # #/
175
+ RATE_LIMIT_EXCEEDED: 'rate_limit_exceeded',
176
+
177
+ ##
178
+ ## The API call requires a JSON request body. See the corresponding documentation
179
+ ## page for details about the request structure.
180
+ # #/
181
+ REQUEST_BODY_REQUIRED: 'request_body_required',
182
+
183
+ ##
184
+ ## You may receive this error if you attempt to schedule a pickup for a return
185
+ ## label.
186
+ # #/
187
+ RETURN_LABEL_NOT_SUPPORTED: 'return_label_not_supported',
188
+
189
+ ##
190
+ ## You may receive this error if you attempt to perform an operation that
191
+ ## requires a subscription. Please contact our sales department to discuss a
192
+ ## ShipEngine enterprise contract.
193
+ # #/
194
+ SUBSCRIPTION_INACTIVE: 'subscription_inactive',
195
+
196
+ ##
197
+ ## Some carriers require you to accept their terms and conditions before you
198
+ ## can use them via ShipEngine. If you get this error then please login to
199
+ ## the ShipEngine dashboard to read and accept the carrier's terms.
200
+ # #/
201
+ TERMS_NOT_ACCEPTED: 'terms_not_accepted',
202
+
203
+ ##
204
+ ## An API call timed out because ShipEngine did not respond within the allowed
205
+ ## timeframe.
206
+ # #/
207
+ TIMEOUT: 'timeout',
208
+
209
+ ##
210
+ ## This error will occur if you attempt to track a package for a carrier that
211
+ ## doesn't offer that service.
212
+ # #/
213
+ TRACKING_NOT_SUPPORTED: 'tracking_not_supported',
214
+
215
+ ##
216
+ ## You may receive this error if your free trial period has expired and you
217
+ ## have not upgraded your account or added billing information.
218
+ # #/
219
+ TRIAL_EXPIRED: 'trial_expired',
220
+
221
+ ##
222
+ ## Your API key is incorrect expired or missing. Check our authentication
223
+ ## guide to learn more about authentication with ShipEngine.
224
+ # #/
225
+ UNAUTHORIZED: 'unauthorized',
226
+
227
+ ##
228
+ ## This error has not yet been assigned a code. See the notes above about how
229
+ ## to handle these.
230
+ # #/
231
+ UNSPECIFIED: 'unspecified',
232
+
233
+ ##
234
+ ## When verifying your account (by email SMS phone call etc.) this error
235
+ ## indicates that the verification code is incorrect. Please re-start the
236
+ ## verification process to get a new code.
237
+ # #/
238
+ VERIFICATION_FAILURE: 'verification_failure',
239
+
240
+ ##
241
+ ## You attempted to perform an operation on multiple shipments from different
242
+ ## warehouses. Try performing separate operations for each warehouse instead.
243
+ # #/
244
+ WAREHOUSE_CONFLICT: 'warehouse_conflict',
245
+
246
+ ##
247
+ ## ShipEngine only allows you to have one webhook of each type. If you would
248
+ ## like to replace a webhook with a new one please delete the old one fir.
249
+ # #/
250
+ WEBHOOK_EVENT_TYPE_CONFLICT: 'webhook_event_type_conflict'
251
+ }.freeze
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShipEngine
4
+ module Exceptions
5
+ ##
6
+ ## This class has the ability to return a specific error type.
7
+ # #/
8
+ class ErrorType
9
+ # @param [Symbol] key
10
+ # @return [Symbol] error type
11
+ def self.get(key)
12
+ @types[key]
13
+ end
14
+
15
+ # @param [String] str_key
16
+ # @return [Symbol] error type
17
+ def self.get_by_str(str_key)
18
+ get(str_key.upcase.to_sym)
19
+ end
20
+
21
+ @types = {
22
+ # There is a problem with your account. This may be your ShipEngine account
23
+ # or a third-party account. See the the error source to determine which
24
+ # account needs your attention.
25
+ ACCOUNT_STATUS: 'account_status',
26
+ # A security error will occur if your API key is invalid or expired, or if
27
+ # you attempt to perform an operation that is not permitted for your account.
28
+ SECURITY: 'security',
29
+ # Something is wrong with the input provided, such as missing a required field,
30
+ # or an illegal value or combinatio of values. This error type always means
31
+ # that some change needs to be made to the input before retrying.
32
+ VALIDATION: 'validation',
33
+ # There was a business rule violation. Business rules are requirements or
34
+ # limitations of a system. If the error source is ShipEngine, then please
35
+ # read the relevant documentation to find out what limitations or requirements
36
+ # apply. Or contact our support for help. If the error source is the carrier
37
+ # or order source, then ShipEngine support may still be able to help clarify
38
+ # the problem or propose a solution, or you may need to contact the third-party
39
+ # for assistance.
40
+ BUSINESS_RULES: 'business_rules',
41
+ # An unknown or unexpected error occurred in our system. Or an error occurred
42
+ # that has not yet been assigned a specific error_type. If you receive
43
+ # persistent system errors, then please contact our support or check our API
44
+ # status page to see if there's a known issue.
45
+ SYSTEM: 'system'
46
+ }.freeze
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'exceptions/error_code'
4
+ require_relative 'exceptions/error_type'
5
+
6
+ module ShipEngine
7
+ module Exceptions
8
+ DEFAULT_SOURCE = 'shipengine'
9
+ # 400 error, or other "user exceptions"
10
+ class ShipEngineError < StandardError
11
+ # message is inherited
12
+ attr_reader :request_id, :source, :type, :code, :url
13
+
14
+ def initialize(message:, source:, type:, code:, request_id:, url: nil) # rubocop:todo Metrics/ParameterLists
15
+ code = Exceptions::ErrorCode.get_by_str(code) if code.is_a?(String)
16
+ super(message)
17
+ @request_id = request_id
18
+ @source = source || DEFAULT_SOURCE
19
+ @type = type
20
+ @code = code
21
+ @url = url
22
+ end
23
+ end
24
+
25
+ # 400 error, or other "user exceptions"
26
+ class ValidationError < ShipEngineError
27
+ def initialize(message:, code:, request_id: nil, source: nil)
28
+ super(message:, source:, type: Exceptions::ErrorType.get(:VALIDATION), code:, request_id:)
29
+ end
30
+ end
31
+
32
+ # only create custom errors for error "types" (which encompass codes). Prefer to use generic ShipEngine errors.
33
+ def self.create_invalid_field_value_error(message, request_id = nil, source = nil)
34
+ ValidationError.new(message:, code: Exceptions::ErrorCode.get(:INVALID_FIELD_VALUE), request_id:, source:)
35
+ end
36
+
37
+ def self.create_required_error(field_name, request_id = nil, source = nil)
38
+ ValidationError.new(
39
+ message: "#{field_name} must be specified.",
40
+ code: Exceptions::ErrorCode.get(:FIELD_VALUE_REQUIRED),
41
+ request_id:,
42
+ source:
43
+ )
44
+ end
45
+
46
+ def self.create_invariant_error(message, request_id = nil)
47
+ SystemError.new(
48
+ message: "INVARIANT ERROR: #{message}",
49
+ code: Exceptions::ErrorCode.get(:UNSPECIFIED),
50
+ request_id:
51
+ )
52
+ end
53
+
54
+ class BusinessRulesError < ShipEngineError
55
+ def initialize(message:, code:, request_id: nil, source: nil)
56
+ super(message:, source:, type: Exceptions::ErrorType.get(:BUSINESS_RULES), code:, request_id:)
57
+ end
58
+ end
59
+
60
+ class AccountStatusError < ShipEngineError
61
+ def initialize(message:, code:, request_id: nil, source: nil)
62
+ super(message:, source:, type: Exceptions::ErrorType.get(:ACCOUNT_STATUS), code:, request_id:)
63
+ end
64
+ end
65
+
66
+ class SecurityError < ShipEngineError
67
+ def initialize(message:, code:, request_id: nil, source: nil)
68
+ super(message:, source:, type: Exceptions::ErrorType.get(:SECURITY), code:, request_id:)
69
+ end
70
+ end
71
+
72
+ class SystemError < ShipEngineError
73
+ def initialize(message:, code:, request_id: nil, source: nil, url: nil)
74
+ super(message:, source:, type: Exceptions::ErrorType.get(:SYSTEM), code:, request_id:, url:)
75
+ end
76
+ end
77
+
78
+ class TimeoutError < SystemError
79
+ def initialize(message:, source: nil, request_id: nil)
80
+ super(
81
+ message:,
82
+ url: URI('https://www.shipengine.com/docs/rate-limits'),
83
+ code: ErrorCode.get(:TIMEOUT),
84
+ request_id:,
85
+ source: source || DEFAULT_SOURCE
86
+ )
87
+ end
88
+ end
89
+
90
+ class RateLimitError < SystemError
91
+ attr_reader :retries
92
+
93
+ def initialize(retries: nil, message: 'You have exceeded the rate limit.', source: nil, request_id: nil)
94
+ super(
95
+ message:,
96
+ code: ErrorCode.get(:RATE_LIMIT_EXCEEDED),
97
+ request_id:,
98
+ source:,
99
+ url: URI('https://www.shipengine.com/docs/rate-limits'),
100
+ )
101
+ @retries = retries
102
+ end
103
+ end
104
+
105
+ def self.create_error_instance(type:, message:, code:, request_id: nil, source: nil, config: nil) # rubocop:todo Metrics/ParameterLists
106
+ case type
107
+ when Exceptions::ErrorType.get(:BUSINESS_RULES)
108
+ BusinessRulesError.new(message:, code:, request_id:, source:)
109
+ when Exceptions::ErrorType.get(:VALIDATION)
110
+ ValidationError.new(message:, code:, request_id:, source:)
111
+ when Exceptions::ErrorType.get(:ACCOUNT_STATUS)
112
+ AccountStatusError.new(message:, code:, request_id:, source:)
113
+ when Exceptions::ErrorType.get(:SECURITY)
114
+ SecurityError.new(message:, code:, request_id:, source:)
115
+ when Exceptions::ErrorType.get(:SYSTEM)
116
+ case code
117
+ when ErrorCode.get(:RATE_LIMIT_EXCEEDED)
118
+ RateLimitError.new(message:, request_id:, source:, retries: config.retries)
119
+ when ErrorCode.get(:TIMEOUT)
120
+ TimeoutError.new(message:, request_id:, source:)
121
+ else
122
+ SystemError.new(message:, code:, request_id:, source:)
123
+ end
124
+ else
125
+ ShipEngineError.new(message:, code:, request_id:, source:)
126
+ end
127
+ end
128
+
129
+ # @param error_type [String] e.g "validation"
130
+ # @return [BusinessRulesError, AccountStatusError, SecurityError, SystemError, ValidationError]
131
+ end
132
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.expand_path('../faraday/*.rb', __dir__)].each { |f| require f }
4
+ require 'shipengine/utils/request_id'
5
+ require 'shipengine/utils/user_agent'
6
+ require 'faraday_middleware'
7
+ require 'json'
8
+
9
+ # frozen_string_literal: true
10
+ module ShipEngine
11
+ class InternalClient
12
+ attr_reader :configuration
13
+
14
+ # @param [::ShipEngine::Configuration] configuration
15
+ def initialize(configuration)
16
+ @configuration = configuration
17
+ end
18
+
19
+ # Perform an HTTP GET request
20
+ def get(path, options = {}, config = {})
21
+ request(:get, path, options, config)
22
+ end
23
+
24
+ # Perform an HTTP POST request
25
+ def post(path, options = {}, config = {})
26
+ request(:post, path, options, config)
27
+ end
28
+
29
+ # Perform an HTTP PUT request
30
+ def put(path, options = {}, config = {})
31
+ request(:put, path, options, config)
32
+ end
33
+
34
+ # Perform an HTTP DELETE request
35
+ def delete(path, options = {}, config = {})
36
+ request(:delete, path, options, config)
37
+ end
38
+
39
+ private
40
+
41
+ # @param config [::ShipEngine::Configuration]
42
+ # @return [::Faraday::Connection]
43
+ def create_connection(config)
44
+ retries = config.retries
45
+ base_url = config.base_url
46
+ api_key = config.api_key
47
+ timeout = config.timeout
48
+
49
+ Faraday.new(url: base_url) do |conn|
50
+ conn.headers = {
51
+ 'API-Key' => api_key,
52
+ 'Content-Type' => 'application/json',
53
+ 'Accept' => 'application/json',
54
+ 'User-Agent' => Utils::UserAgent.new.to_s
55
+ }
56
+
57
+ conn.options.timeout = timeout / 1000
58
+ conn.request(:json) # auto-coerce bodies to json
59
+ conn.request(:retry, {
60
+ max: retries,
61
+ retry_statuses: [429], # even though this seems self-evident, this field is neccessary for Retry-After to be respected.
62
+ methods: Faraday::Request::Retry::IDEMPOTENT_METHODS + [:post], # :post is not a "retry_attempt-able request by default"
63
+ exceptions: [ShipEngine::Exceptions::RateLimitError],
64
+ retry_block: proc { |env, _opts, _retries, _exception|
65
+ env.request_headers['Retries'] = config.retries.to_s
66
+ }
67
+ })
68
+
69
+ conn.use(FaradayMiddleware::RaiseHttpException)
70
+ # conn.request(:retry_after_header) # should go after :retry_attempt
71
+ # conn.request(:request_sent, config)
72
+ conn.response(:json)
73
+ end
74
+ end
75
+
76
+ # Perform an HTTP request
77
+ def request(method, path, options, config)
78
+ config_with_overrides = @configuration.merge(config)
79
+
80
+ create_connection(config_with_overrides).send(method) do |request|
81
+ case method
82
+ when :get, :delete
83
+ request.url(path, options)
84
+ when :post, :put
85
+ request.path = path
86
+ request.body = options unless options.empty?
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2009 - 2018 Douglas F Shearer
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ # Base58
24
+ # Copyright (c) 2009 - 2018 Douglas F Shearer.
25
+ # http://douglasfshearer.com
26
+ # Distributed under the MIT license as included with this plugin.
27
+
28
+ # rubocop:disable
29
+ class Base58
30
+ # See https://en.wikipedia.org/wiki/Base58
31
+ ALPHABETS = {
32
+ flickr: '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', # This is the default
33
+ bitcoin: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', # Also used for IPFS
34
+ ripple: 'rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz'
35
+ }.freeze
36
+
37
+ # NOTE: If adding new alphabets of non-standard length, this should become a method.
38
+ BASE = ALPHABETS[:flickr].length
39
+
40
+ # Converts a base58 string to a base10 integer.
41
+ def self.base58_to_int(base58_val, alphabet = :flickr)
42
+ raise ArgumentError, 'Invalid alphabet selection.' unless ALPHABETS.include?(alphabet)
43
+
44
+ int_val = 0
45
+ base58_val.reverse.chars.each_with_index do |char, index|
46
+ raise ArgumentError, 'Value passed not a valid Base58 String.' if (char_index = ALPHABETS[alphabet].index(char)).nil?
47
+
48
+ int_val += char_index * (BASE**index)
49
+ end
50
+ int_val
51
+ end
52
+
53
+ # Converts a base10 integer to a base58 string.
54
+ def self.int_to_base58(int_val, alphabet = :flickr)
55
+ raise ArgumentError, 'Value passed is not an Integer.' unless int_val.is_a?(Integer)
56
+ raise ArgumentError, 'Invalid alphabet selection.' unless ALPHABETS.include?(alphabet)
57
+
58
+ base58_val = ''
59
+ while int_val >= BASE
60
+ mod = int_val % BASE
61
+ base58_val = ALPHABETS[alphabet][mod, 1] + base58_val
62
+ int_val = (int_val - mod) / BASE
63
+ end
64
+ ALPHABETS[alphabet][int_val, 1] + base58_val
65
+ end
66
+
67
+ # Converts a ASCII-8BIT (binary) encoded string to a base58 string.
68
+ def self.binary_to_base58(binary_val, alphabet = :flickr, include_leading_zeroes = true)
69
+ raise ArgumentError, 'Value passed is not a String.' unless binary_val.is_a?(String)
70
+ raise ArgumentError, 'Value passed is not binary.' unless binary_val.encoding == Encoding::BINARY
71
+ raise ArgumentError, 'Invalid alphabet selection.' unless ALPHABETS.include?(alphabet)
72
+ return int_to_base58(0, alphabet) if binary_val.empty?
73
+
74
+ if include_leading_zeroes
75
+ nzeroes = binary_val.bytes.find_index { |b| b != 0 } || binary_val.length - 1
76
+ prefix = ALPHABETS[alphabet][0] * nzeroes
77
+ else
78
+ prefix = ''
79
+ end
80
+
81
+ prefix + int_to_base58(binary_val.unpack1('H*').to_i(16), alphabet)
82
+ end
83
+
84
+ # Converts a base58 string to an ASCII-8BIT (binary) encoded string.
85
+ # All leading zeroes in the base58 input are preserved and converted to
86
+ # "\x00" in the output.
87
+ def self.base58_to_binary(base58_val, alphabet = :flickr)
88
+ raise ArgumentError, 'Invalid alphabet selection.' unless ALPHABETS.include?(alphabet)
89
+
90
+ nzeroes = base58_val.chars.find_index { |c| c != ALPHABETS[alphabet][0] } || base58_val.length - 1
91
+ prefix = nzeroes.negative? ? '' : '00' * nzeroes
92
+ [prefix + Private.int_to_hex(base58_to_int(base58_val, alphabet))].pack('H*')
93
+ end
94
+
95
+ module Private
96
+ def self.int_to_hex(int)
97
+ hex = int.to_s(16)
98
+ # The hex string must always consist of an even number of characters,
99
+ # otherwise the pack() parsing will be misaligned.
100
+ hex.length.even? ? hex : "0#{hex}"
101
+ end
102
+ end
103
+
104
+ class << self
105
+ alias encode int_to_base58
106
+ alias decode base58_to_int
107
+ end
108
+ end
109
+ # rubocop:enable
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module ShipEngine
6
+ module Utils
7
+ module PrettyPrint
8
+ # This will be used to add a *to_s* method override
9
+ # to each class that *includes* the *PrettyPrint* module.
10
+ # This method returns a *JSON String* so one can easily inspect
11
+ # the contents of a given object.
12
+ def to_s
13
+ JSON.pretty_generate(to_hash)
14
+ end
15
+
16
+ # This will be used to add a *to_hash* method override
17
+ # to each class that *includes* the *PrettyPrint* module.
18
+ # This will return the class attributes and their values in
19
+ # a *Hash*.
20
+ def to_hash
21
+ hash = {}
22
+ instance_variables.each do |n|
23
+ hash[n.to_s.delete('@')] = instance_variable_get(n)
24
+ end
25
+ hash
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative 'base58'
5
+
6
+ module ShipEngine
7
+ module Utils
8
+ class RequestId
9
+ # @return [String] req_abcd123456789
10
+ def self.create
11
+ base58_encoded_uuid = Base58.binary_to_base58(SecureRandom.uuid.force_encoding('BINARY'))
12
+ "req_#{base58_encoded_uuid}"
13
+ end
14
+ end
15
+ end
16
+ end