easypost 5.0.1 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be1297ea7c20b21f3f959cb281001d7435902ede623ca47ac981dba2b19309c6
4
- data.tar.gz: 73e65fb3a6dc6a00635d0ae1f4351edcdaf427d5d29c7cf8254f43a770208033
3
+ metadata.gz: 74cc993ead7c401dc7dbd719667872b1317a83e31c93bf85e3489cdc5b322a51
4
+ data.tar.gz: 17095956ef150f649b12b9b974db4098b495bbfe7710fe368d594e7e6410e6ea
5
5
  SHA512:
6
- metadata.gz: 99d4ded392e0e2b5a3a30c3eb58c9b9dff59997d479ae0174fa59a6b0d8dec1260f3861fd3d3a965545fc910c48cd137ccd1b24704db0f56d0287e15890c4f94
7
- data.tar.gz: c31978176d4f25898021b9306180171e86826e48409de7a007cd0aeff371e528dfe8b690963c9cc97df3288ba61db27658b8780add0e29943db13a99d095f6df
6
+ metadata.gz: 8708b3ed757fdcec975e84a585bbbdc5e428de0f32b34b047b3d1860fb663db7b48748a49a5841de405adb00bc3102c3bc7ffea56d7d4ff0bf7ff6a74e81015d
7
+ data.tar.gz: 4fb43956853af9c440f7b84a804250e5e817ea3261e9900f821236bf2ae4fc623d1a68a36a899628b655ca6b1833e9095b3b943d2224224a95af24fb6c732bca
@@ -48,8 +48,6 @@ jobs:
48
48
  run: make install-styleguide
49
49
  - name: Lint Project
50
50
  run: make lint
51
- - name: Run security analysis
52
- run: make scan
53
51
  docs:
54
52
  if: github.ref == 'refs/heads/master'
55
53
  runs-on: ubuntu-latest
data/.gitignore CHANGED
@@ -25,4 +25,15 @@ test/version_tmp
25
25
  tmp
26
26
  vendor/
27
27
  /easycop.yml
28
- /.rubocop.yml
28
+ /.rubocop.yml
29
+ [._]*.s[a-v][a-z]
30
+ [._]*.sw[a-p]
31
+ [._]s[a-rt-v][a-z]
32
+ [._]ss[a-gi-z]
33
+ [._]sw[a-p]
34
+ Session.vim
35
+ Sessionx.vim
36
+ .netrwhist
37
+ *~
38
+ tags
39
+ [._]*.un~
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v5.1.0 (2023-07-28)
4
+
5
+ - Adds hooks to introspect the request and response of API calls (see `HTTP Hooks` section in the README for more details)
6
+ - Maps 400 status code responses to the new `BadRequestError` class
7
+
3
8
  ## v5.0.1 (2023-06-20)
4
9
 
5
10
  - Fixes a bug where the params of a `customs_info` on create weren't wrapped properly which led to an empty set of `customs_items`
data/Makefile CHANGED
@@ -20,10 +20,6 @@ coverage:
20
20
  docs:
21
21
  bundle exec rdoc lib -o docs --title "EasyPost Ruby Docs"
22
22
 
23
- ## format - Fix Rubocop errors
24
- format:
25
- bundle exec rubocop -a
26
-
27
23
  ## install-styleguide - Import the style guides (Unix only)
28
24
  install-styleguide: | update-examples-submodule
29
25
  sh examples/symlink_directory_files.sh examples/style_guides/ruby .
@@ -32,14 +28,11 @@ install-styleguide: | update-examples-submodule
32
28
  install: | update-examples-submodule
33
29
  bundle install
34
30
 
35
- ## update-examples-submodule - Update the examples submodule
36
- update-examples-submodule:
37
- git submodule init
38
- git submodule update --remote
39
-
40
31
  ## lint - Lint the project
41
- lint:
42
- bundle exec rubocop
32
+ lint: rubocop scan
33
+
34
+ ## lint-fix - Fix Rubocop errors
35
+ lint-fix: rubocop-fix
43
36
 
44
37
  ## publish - Publishes the built gem to Rubygems
45
38
  publish:
@@ -50,6 +43,14 @@ publish:
50
43
  release:
51
44
  gh release create ${tag} dist/*
52
45
 
46
+ ## rubocop - lints the project with rubocop
47
+ rubocop:
48
+ bundle exec rubocop
49
+
50
+ ## rubocop-fix - fix rubocop errors
51
+ rubocop-fix:
52
+ bundle exec rubocop -a
53
+
53
54
  ## scan - Runs security analysis on the project with Brakeman
54
55
  scan:
55
56
  bundle exec brakeman lib --force
@@ -61,4 +62,9 @@ test:
61
62
  ## update - Updates dependencies
62
63
  update: | update-examples-submodule
63
64
 
64
- .PHONY: help build clean coverage docs format install install-styleguide lint publish release scan test update update-examples-submodule
65
+ ## update-examples-submodule - Update the examples submodule
66
+ update-examples-submodule:
67
+ git submodule init
68
+ git submodule update --remote
69
+
70
+ .PHONY: help build clean coverage docs install install-styleguide lint lint-fix publish release rubocop rubocop-fix scan test update update-examples-submodule
data/README.md CHANGED
@@ -61,6 +61,7 @@ puts bought_shipment
61
61
  ### Custom Connections
62
62
 
63
63
  Pass in a lambda function as `custom_client_exec` when initializing a client that responds to `call(method, uri, headers, open_timeout, read_timeout, body = nil)` where:
64
+
64
65
  - `uri` is the fully-qualified URL of the EasyPost endpoint, including query parameters (`Uri` object)
65
66
  - `method` is the lowercase name of the HTTP method being used for the request (e.g. `:get`, `:post`, `:put`, `:delete`)
66
67
  - `headers` is a hash with all headers needed for the request pre-populated, including authorization (`Hash` object)
@@ -69,7 +70,8 @@ Pass in a lambda function as `custom_client_exec` when initializing a client tha
69
70
  - `body` is a string of the body data to be included in the request, or nil (e.g. GET or DELETE request) (string or `nil`)
70
71
 
71
72
  The lambda function should return an object with `code` and `body` attributes, where:
72
- - `code` is the HTTP response status code (integer)
73
+
74
+ - `code` is the HTTP response status code (integer)
73
75
  - `body` is the response body (string)
74
76
 
75
77
  #### Faraday
@@ -116,6 +118,49 @@ my_client = described_class.new(
116
118
  )
117
119
  ```
118
120
 
121
+ ### HTTP Hooks
122
+
123
+ Users can audit HTTP requests and responses being made by the library by subscribing to request and response events. To do so, pass a block to the `subscribe_request_hook` and `subscribe_response_hook` methods of an instance of `EasyPost::Client`:
124
+
125
+ ```ruby
126
+ require 'easypost'
127
+
128
+ client = EasyPost::Client.new(api_key: ENV['EASYPOST_API_KEY'])
129
+
130
+ # Returns a randomly-generated symbol which you can use to later unsubscribe the request hook
131
+ client.subscribe_request_hook do |request_data|
132
+ # Your code goes here
133
+ end
134
+ # Returns a randomly-generated symbol which you can use to later unsubscribe the response hook
135
+ client.subscribe_response_hook do |response_data|
136
+ # Your code goes here
137
+ end
138
+ ```
139
+
140
+ You can also name your hook subscriptions by providing an optional parameter to the methods above:
141
+
142
+ ```ruby
143
+ require 'easypost'
144
+
145
+ client = EasyPost::Client.new(api_key: ENV['EASYPOST_API_KEY'])
146
+
147
+ request_hook = client.subscribe_request_hook(:my_request_hook) do |request_data|
148
+ # Your code goes here
149
+ end
150
+ response_hook = client.subscribe_response_hook(:my_response_hook) do |response_data|
151
+ # Your code goes here
152
+ end
153
+
154
+ puts request_hook # :my_request_hook
155
+ puts response_hook # :my_response_hook
156
+ ```
157
+
158
+ Keep in mind that subscribing a hook with the same name of an existing hook will replace the existing hook with the new one. A request hook and a response hook can share the same name.
159
+
160
+ #### Custom HTTP Connections with HTTP Hooks
161
+
162
+ If you're using a custom HTTP connection, keep in mind that the `response_data` parameter that a response hook receives *will not be hydrated* with all the response data. You will have to inspect the `client_response_object` property in `response_data` to inspect the response code, response headers and response body.
163
+
119
164
  ## Documentation
120
165
 
121
166
  API documentation can be found at: <https://easypost.com/docs/api>.
@@ -141,9 +186,7 @@ make install-style
141
186
 
142
187
  # Lint project
143
188
  make lint
144
-
145
- # Fix linting errors
146
- make format
189
+ make lint-fix
147
190
 
148
191
  # Run tests (coverage is generated on a successful test suite run)
149
192
  EASYPOST_TEST_API_KEY=123... EASYPOST_PROD_API_KEY=123... make test
@@ -155,8 +198,7 @@ make scan
155
198
  make docs
156
199
 
157
200
  # Update submodules
158
- git submodule init
159
- git submodule update --remote
201
+ make update-examples-submodule
160
202
  ```
161
203
 
162
204
  ### Testing
data/VERSION CHANGED
@@ -1 +1 @@
1
- 5.0.1
1
+ 5.1.0
@@ -4,6 +4,7 @@ require_relative 'services'
4
4
  require_relative 'http_client'
5
5
  require_relative 'internal_utilities'
6
6
  require 'json'
7
+ require 'securerandom'
7
8
 
8
9
  class EasyPost::Client
9
10
  attr_reader :open_timeout, :read_timeout, :api_base
@@ -90,6 +91,54 @@ class EasyPost::Client
90
91
  EasyPost::InternalUtilities::Json.convert_json_to_object(response.body, cls)
91
92
  end
92
93
 
94
+ # Subscribe a request hook
95
+ #
96
+ # @param name [Symbol] the name of the hook. Defaults ot a ranom hexadecimal-based symbol
97
+ # @param block [Block] a code block that will be executed before a request is made
98
+ # @return [Symbol] the name of the request hook
99
+ def subscribe_request_hook(name = SecureRandom.hex.to_sym, &block)
100
+ EasyPost::Hooks.subscribe(:request, name, block)
101
+ end
102
+
103
+ # Unsubscribe a request hook
104
+ #
105
+ # @param name [Symbol] the name of the hook
106
+ # @return [Block] the hook code block
107
+ def unsubscribe_request_hook(name)
108
+ EasyPost::Hooks.unsubscribe(:request, name)
109
+ end
110
+
111
+ # Unsubscribe all request hooks
112
+ #
113
+ # @return [Hash] a hash containing all request hook subscriptions
114
+ def unsubscribe_all_request_hooks
115
+ EasyPost::Hooks.unsubscribe_all(:request)
116
+ end
117
+
118
+ # Subscribe a response hook
119
+ #
120
+ # @param name [Symbol] the name of the hook. Defaults ot a ranom hexadecimal-based symbol
121
+ # @param block [Block] a code block that will be executed upon receiving the response from a request
122
+ # @return [Symbol] the name of the response hook
123
+ def subscribe_response_hook(name = SecureRandom.hex.to_sym, &block)
124
+ EasyPost::Hooks.subscribe(:response, name, block)
125
+ end
126
+
127
+ # Unsubscribe a response hook
128
+ #
129
+ # @param name [Symbol] the name of the hook
130
+ # @return [Block] the hook code block
131
+ def unsubscribe_response_hook(name)
132
+ EasyPost::Hooks.unsubscribe(:response, name)
133
+ end
134
+
135
+ # Unsubscribe all response hooks
136
+ #
137
+ # @return [Hash] a hash containing all response hook subscriptions
138
+ def unsubscribe_all_response_hooks
139
+ EasyPost::Hooks.unsubscribe_all(:response)
140
+ end
141
+
93
142
  private
94
143
 
95
144
  def http_config
@@ -77,6 +77,8 @@ class EasyPost::Errors::ApiError < EasyPost::Errors::EasyPostError
77
77
  EasyPost::Errors::ConnectionError
78
78
  when 300, 301, 302, 303, 304, 305, 306, 307, 308
79
79
  EasyPost::Errors::RedirectError
80
+ when 400
81
+ EasyPost::Errors::BadRequestError
80
82
  when 401
81
83
  EasyPost::Errors::UnauthorizedError
82
84
  when 402
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'api_error'
4
+
5
+ class EasyPost::Errors::BadRequestError < EasyPost::Errors::ApiError
6
+ end
@@ -12,6 +12,7 @@ require_relative 'errors/invalid_parameter_error'
12
12
  require_relative 'errors/missing_parameter_error'
13
13
  require_relative 'errors/signature_verification_error'
14
14
  require_relative 'errors/api/api_error'
15
+ require_relative 'errors/api/bad_request_error'
15
16
  require_relative 'errors/api/connection_error'
16
17
  require_relative 'errors/api/forbidden_error'
17
18
  require_relative 'errors/api/gateway_timeout_error'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EasyPost::Hooks::RequestContext
4
+ attr_reader :method, :path, :headers, :request_body, :request_timestamp, :request_uuid
5
+
6
+ def initialize(method:, path:, headers:, request_body:, request_timestamp:, request_uuid:)
7
+ @method = method
8
+ @path = path
9
+ @headers = headers
10
+ @request_body = request_body
11
+ @request_timestamp = request_timestamp
12
+ @request_uuid = request_uuid
13
+
14
+ freeze
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EasyPost::Hooks::ResponseContext
4
+ attr_reader :http_status, :method, :path, :headers, :response_body,
5
+ :request_timestamp, :response_timestamp, :request_uuid,
6
+ :client_response_object
7
+
8
+ def initialize(http_status:, method:, path:, headers:, response_body:,
9
+ request_timestamp:, response_timestamp:, request_uuid:,
10
+ client_response_object:)
11
+ @http_status = http_status
12
+ @method = method
13
+ @path = path
14
+ @headers = headers
15
+ @response_body = response_body
16
+ @request_timestamp = request_timestamp
17
+ @response_timestamp = response_timestamp
18
+ @request_uuid = request_uuid
19
+ @client_response_object = client_response_object
20
+
21
+ freeze
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyPost::Hooks
4
+ def self.subscribe(type, name, block)
5
+ subscribers[type][name] = block
6
+
7
+ name
8
+ end
9
+
10
+ def self.unsubscribe(type, name)
11
+ subscribers[type].delete(name)
12
+ end
13
+
14
+ def self.unsubscribe_all(type)
15
+ subscribers.delete(type)
16
+ end
17
+
18
+ def self.notify(type, context)
19
+ subscribers[type].each_value { |subscriber| subscriber.call(context) }
20
+ end
21
+
22
+ def self.any_subscribers?(type)
23
+ !subscribers[type].empty?
24
+ end
25
+
26
+ def self.subscribers
27
+ @subscribers ||= Hash.new { |hash, key| hash[key] = {} }
28
+ end
29
+
30
+ private_class_method :subscribers
31
+ end
32
+
33
+ require_relative 'hooks/request_context'
34
+ require_relative 'hooks/response_context'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'securerandom'
4
+
3
5
  class EasyPost::HttpClient
4
6
  def initialize(base_url, config, custom_client_exec = nil)
5
7
  @base_url = base_url
@@ -20,17 +22,67 @@ class EasyPost::HttpClient
20
22
 
21
23
  uri = URI.parse("#{@base_url}/#{api_version}/#{path}")
22
24
  headers = @config[:headers].merge(headers || {})
23
- body = JSON.dump(EasyPost::InternalUtilities.objects_to_ids(body)) if body
25
+ serialized_body = JSON.dump(EasyPost::InternalUtilities.objects_to_ids(body)) if body
24
26
  open_timeout = @config[:open_timeout]
25
27
  read_timeout = @config[:read_timeout]
28
+ request_timestamp = Time.now
29
+ request_uuid = SecureRandom.uuid
30
+
31
+ if EasyPost::Hooks.any_subscribers?(:request)
32
+ request_context = EasyPost::Hooks::RequestContext.new(
33
+ method: method,
34
+ path: uri.to_s,
35
+ headers: headers,
36
+ request_body: body,
37
+ request_timestamp: request_timestamp,
38
+ request_uuid: request_uuid,
39
+ )
40
+ EasyPost::Hooks.notify(:request, request_context)
41
+ end
26
42
 
27
43
  # Execute the request, return the response.
28
44
 
29
- if @custom_client_exec
30
- @custom_client_exec.call(method, uri, headers, open_timeout, read_timeout, body)
31
- else
32
- default_request_execute(method, uri, headers, open_timeout, read_timeout, body)
45
+ response = if @custom_client_exec
46
+ @custom_client_exec.call(method, uri, headers, open_timeout, read_timeout, serialized_body)
47
+ else
48
+ default_request_execute(method, uri, headers, open_timeout, read_timeout, serialized_body)
49
+ end
50
+ response_timestamp = Time.now
51
+
52
+ if EasyPost::Hooks.any_subscribers?(:response)
53
+ response_context = {
54
+ http_status: nil,
55
+ method: method,
56
+ path: uri.to_s,
57
+ headers: nil,
58
+ response_body: nil,
59
+ request_timestamp: request_timestamp,
60
+ response_timestamp: response_timestamp,
61
+ client_response_object: response,
62
+ request_uuid: request_uuid,
63
+ }
64
+
65
+ # If using a custom HTTP client, the user will have to infer these from the raw
66
+ # client_response_object attribute
67
+ if response.is_a?(Net::HTTPResponse)
68
+ response_body = begin
69
+ JSON.parse(response.body)
70
+ rescue JSON::ParseError
71
+ response.body
72
+ end
73
+ response_context.merge!(
74
+ {
75
+ http_status: response.code.to_i,
76
+ headers: response.each_header.to_h,
77
+ response_body: response_body,
78
+ },
79
+ )
80
+ end
81
+
82
+ EasyPost::Hooks.notify(:response, EasyPost::Hooks::ResponseContext.new(**response_context))
33
83
  end
84
+
85
+ response
34
86
  end
35
87
 
36
88
  def default_request_execute(method, uri, headers, open_timeout, read_timeout, body = nil)
data/lib/easypost.rb CHANGED
@@ -24,5 +24,8 @@ require 'easypost/errors'
24
24
  # Internal Utilities
25
25
  require 'easypost/internal_utilities'
26
26
 
27
+ # Hooks
28
+ require 'easypost/hooks'
29
+
27
30
  module EasyPost
28
31
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easypost
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.1
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - EasyPost Developers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-20 00:00:00.000000000 Z
11
+ date: 2023-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: brakeman
@@ -237,6 +237,7 @@ files:
237
237
  - lib/easypost/constants.rb
238
238
  - lib/easypost/errors.rb
239
239
  - lib/easypost/errors/api/api_error.rb
240
+ - lib/easypost/errors/api/bad_request_error.rb
240
241
  - lib/easypost/errors/api/connection_error.rb
241
242
  - lib/easypost/errors/api/external_api_error.rb
242
243
  - lib/easypost/errors/api/forbidden_error.rb
@@ -262,6 +263,9 @@ files:
262
263
  - lib/easypost/errors/invalid_parameter_error.rb
263
264
  - lib/easypost/errors/missing_parameter_error.rb
264
265
  - lib/easypost/errors/signature_verification_error.rb
266
+ - lib/easypost/hooks.rb
267
+ - lib/easypost/hooks/request_context.rb
268
+ - lib/easypost/hooks/response_context.rb
265
269
  - lib/easypost/http_client.rb
266
270
  - lib/easypost/internal_utilities.rb
267
271
  - lib/easypost/models.rb