shipengine_sdk 1.0.3

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