erp_integration 0.54.0 → 0.56.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: 015dee8d29c0a8bcd763966f25c24b77efb9cd2e7ac4a1555aa104e9a708c2ec
4
- data.tar.gz: 92f5a27785677904bd7b0cd60fc5a033195ed99d64d9c16941fbb4d942db8b31
3
+ metadata.gz: 55974d4e2c7b76ca461070fe996522e0b7fa06391760549f8959000ed07b2a9e
4
+ data.tar.gz: 43683efd68525709be0fcba1537051830f52d7c0663779a7ab5b4c24b0a6e2e9
5
5
  SHA512:
6
- metadata.gz: 526e86fb610c85d0b5c69f36e2cf1b1c396ff36e6f74c2db6d9764eae9eb1f076d9ed7771b5347ac3060bbdbd047777c1901c427351a280bcb26bf49b6edb269
7
- data.tar.gz: f876d9874077923090353f7e2c9d6766cae725fe1558345ba98f4d5cfc07495bb272f451205f04c6a957402fdf10bc756d69ad8792a55405c91a07ce856d5e4e
6
+ metadata.gz: f0943cc3b1047ef17259a1eeb039fdd65ee74cb3ebc6911ff431162f60058b89d8e26e3f742028b261c2979c304aab4b1a0fd155e5d72fd0f0063a3a3afa351f
7
+ data.tar.gz: 4a06481fb232e9ad506c0d038af4114169546d622eecec0e6d5b03877e6f85648777c278728f87f6cae52c3e695f382ec34e4c1a2adcfadf0d9af23fc354374f
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.0.2
1
+ 3.2.2
data/README.md CHANGED
@@ -44,10 +44,13 @@ end
44
44
  To set up API key rotation, configure multiple keys.
45
45
  Every time a Fulfil client receives a `404` or `429`, it will rotate an API key.
46
46
 
47
+ You can set the `api_key_rotation_threshold` option to automatically change the API key when requests are running low. If you don't want to use the `x-ratelimit-remaining` response header for rotation, set the `api_key_rotation_threshold` to 0 or ignore it. The minimum threshold is 1, meaning the key will rotate when there are 0 remaining requests.
48
+
47
49
  ```erb
48
50
  # config/initializers/erp_integration.rb
49
51
  ErpIntegration.configure do |config|
50
52
  config.fulfil_api_keys = ['<your-api-key1>', '<your-api-key2>']
53
+ config.api_key_rotation_threshold = 1
51
54
  ...
52
55
  end
53
56
  ```
@@ -61,6 +64,47 @@ ErpIntegration::SalesOrder.api_keys_pool = '<your-api-key>'
61
64
  ErpIntegration::SalesOrder.api_keys_pool = ['<your-api-key1>', '<your-api-key2>']
62
65
  ```
63
66
 
67
+ ### Rate limiting
68
+
69
+ To manage the number of requests made to avoid rate limiting by the API provider, you can configure rate limiters in the ERP Integration gem. Each rate limiter should implement the `api_key_fragment` and `within_limit` methods.
70
+
71
+ ### Setting Up Rate Limiters
72
+ You can configure multiple rate limiters to be used for HTTP operations on the client.
73
+
74
+ ```ruby
75
+ # config/initializers/erp_integration.rb
76
+ ErpIntegration.configure do |config|
77
+ config.rate_limiters = [
78
+ MyRateLimiter.new(api_key_fragment: 'key1'),
79
+ MyRateLimiter.new(api_key_fragment: 'key2')
80
+ ]
81
+ end
82
+ ```
83
+
84
+ Each rate limiter must implement the following methods:
85
+ - `api_key_fragment`: This method should return a string that is used to identify the rate limiter by the API key.
86
+ - `within_limit`: This method should `yield` to the block if the rate limit is not exceeded.
87
+
88
+ ### Example Rate Limiter
89
+ Here is an example implementation of a rate limiter:
90
+
91
+ ```ruby
92
+ class MyRateLimiter
93
+ def initialize(api_key_fragment:)
94
+ @api_key_fragment = api_key_fragment
95
+ end
96
+
97
+ def api_key_fragment
98
+ @api_key_fragment
99
+ end
100
+
101
+ def within_limit
102
+ # Implement your rate limiting logic here
103
+ yield
104
+ end
105
+ end
106
+ ```
107
+
64
108
  ### Supported Query Methods
65
109
 
66
110
  After configuring the gem, one can easily query all the available ERP resources from the connected third-parties. In all cases, the API will return a collection of resources.
@@ -22,6 +22,12 @@ module ErpIntegration
22
22
  # @return [String] The base URL for Fulfil.
23
23
  attr_accessor :fulfil_base_url
24
24
 
25
+ # The `api_key_rotation_threshold` is used by the `ApiKeysRotation` middleware
26
+ # it will rotate the API key when the remaining requests are less than the threshold.
27
+ #
28
+ # Set the `api_key_rotation_threshold` to 0, it you don't want to rotate by the remaining requests.
29
+ attr_reader :api_key_rotation_threshold
30
+
25
31
  # Allows configuring an adapter for the `BillOfMaterial` resource. When
26
32
  # none is configured, it will default to Fulfil.
27
33
  # @return [Symbol] The configured adapter for the bill_of_material.
@@ -172,6 +178,10 @@ module ErpIntegration
172
178
  end
173
179
  end
174
180
 
181
+ def api_key_rotation_threshold=(threshold)
182
+ @api_key_rotation_threshold = threshold || 0
183
+ end
184
+
175
185
  def carrier_adapter
176
186
  @carrier_adapter || :fulfil
177
187
  end
@@ -299,6 +309,18 @@ module ErpIntegration
299
309
  def product_option_adapter
300
310
  @product_option_adapter || :fulfil
301
311
  end
312
+
313
+ # Rate limiters that will be used for HTTP operations on Client
314
+ # to manage the number of requests made to avoid rate limiting by the API provider.
315
+ def rate_limiters
316
+ @rate_limiters ||= []
317
+ end
318
+
319
+ # Sets the rate limiters that will be used
320
+ # @raise [MissingMethodError] if the rate limiter object doesn't respond to the required methods.
321
+ def rate_limiters=(rate_limiters)
322
+ @rate_limiters = (rate_limiters || []).each { |limiter| RateLimiter.validate!(limiter) }
323
+ end
302
324
  end
303
325
 
304
326
  # Returns ERP Integration's configuration.
@@ -33,7 +33,7 @@ module ErpIntegration
33
33
  # Custom error handling for the error response
34
34
  faraday.use ErpIntegration::Middleware::ErrorHandling
35
35
  # Custom middleware for rotating API keys
36
- faraday.use ErpIntegration::Middleware::ApiKeysRotation, api_keys_pool: api_keys_pool
36
+ faraday.use ErpIntegration::Middleware::ApiKeysRotation, rotation_options
37
37
  # Custom middleware for logging requests and responses
38
38
  faraday.use ErpIntegration::Middleware::Logger, @logger if @logger
39
39
 
@@ -48,7 +48,9 @@ module ErpIntegration
48
48
  raise ErpIntegration::Error, 'The Fulfil API key and/or base URL are missing.'
49
49
  end
50
50
 
51
- connection.public_send(action_name, "api/#{version}/#{path}", options).body
51
+ rate_limiter.within_limit do
52
+ connection.public_send(action_name, "api/#{version}/#{path}", options).body
53
+ end
52
54
  end
53
55
  end
54
56
 
@@ -64,12 +66,23 @@ module ErpIntegration
64
66
  api_keys_pool.current_key
65
67
  end
66
68
 
69
+ def rate_limiter
70
+ RateLimiter.find_by_api_key(api_key)
71
+ end
72
+
67
73
  def default_headers
68
74
  {
69
75
  'Accept': 'application/json',
70
76
  'Content-Type': 'application/json'
71
77
  }
72
78
  end
79
+
80
+ def rotation_options
81
+ {
82
+ api_keys_pool: api_keys_pool,
83
+ rotation_theshold: ErpIntegration.config.api_key_rotation_threshold
84
+ }
85
+ end
73
86
  end
74
87
  end
75
88
  end
@@ -20,14 +20,21 @@ module ErpIntegration
20
20
  end
21
21
 
22
22
  def on_request(env)
23
- env[:request_headers]['X-API-KEY'] = options[:api_keys_pool].current_key
23
+ env.request_headers['X-API-KEY'] = options[:api_keys_pool].current_key
24
24
  end
25
25
 
26
26
  def on_complete(env)
27
- return unless HTTP_ROTATE_CODES.include?(env.status)
27
+ return unless rotate?(env)
28
28
 
29
29
  options[:api_keys_pool].rotate_key!
30
30
  end
31
+
32
+ def rotate?(env)
33
+ return true if HTTP_ROTATE_CODES.include?(env.status)
34
+ return false unless env.response_headers['x-ratelimit-remaining'].present?
35
+
36
+ env.response_headers['x-ratelimit-remaining'].to_i < options[:rotation_theshold].to_i
37
+ end
31
38
  end
32
39
  end
33
40
  end
@@ -19,7 +19,7 @@ module ErpIntegration
19
19
  end
20
20
 
21
21
  def call(env)
22
- api_key_fragment = sanitize_api_key(env[:request_headers]['X-API-KEY'])
22
+ api_key_fragment = sanitize_api_key(env.request_headers['X-API-KEY']) || 'none'
23
23
 
24
24
  if @logger.respond_to?(:tagged)
25
25
  @logger.tagged("API key *#{api_key_fragment}") do
@@ -43,6 +43,8 @@ module ErpIntegration
43
43
  @formatter.exception(exc) if @formatter.respond_to?(:exception)
44
44
  end
45
45
 
46
+ # @param [String] api_key
47
+ # @return [String] The last 4 characters of the API key
46
48
  def sanitize_api_key(api_key)
47
49
  api_key[-4..-1] if api_key
48
50
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErpIntegration
4
+ class RateLimiter
5
+ class MissingMethodError < StandardError; end
6
+ # The `api_key_fragment` method should return a string that is used to identify
7
+ # the rate limiter by the API key.
8
+ #
9
+ # The `within_limit` method should yield to the block if the rate limit is not exceeded.
10
+ REQUIRED_METHODS = %i[api_key_fragment within_limit].freeze
11
+
12
+ class << self
13
+ # Validates that the rate limiter object responds to the required methods.
14
+ # It requires the `api_key_fragment` and `within_limit` methods to be present.
15
+ #
16
+ # @raise [MissingMethodError]
17
+ # @param rate_limiter [Object]
18
+ def validate!(rate_limiter)
19
+ REQUIRED_METHODS.each do |method|
20
+ next if rate_limiter.respond_to?(method)
21
+
22
+ raise MissingMethodError, "'#{rate_limiter.class}##{method}' method is required."
23
+ end
24
+ end
25
+
26
+ # Finds the rate limiter by the API key.
27
+ # If the API key is not found, it returns an unlimited rate limiter.
28
+ #
29
+ # @param api_key [String]
30
+ # @return [Object] The rate limiter object.
31
+ def find_by_api_key(api_key)
32
+ rate_limiters[api_key] || unlimited
33
+ end
34
+
35
+ # Returns an unlimited rate limiter.
36
+ #
37
+ # @return [RateLimiter::Unlimited] The unlimited rate limiter object.
38
+ def unlimited
39
+ @unlimited ||= Unlimited.new
40
+ end
41
+
42
+ private
43
+
44
+ # The `rate_limiters` hash stores the rate limiter objects found by the API key.
45
+ #
46
+ # @return [Hash] The rate limiters hash.
47
+ def rate_limiters
48
+ @rate_limiters ||= Hash.new do |h, api_key|
49
+ h[api_key] = ErpIntegration.config.rate_limiters.find do |rate_limiter|
50
+ api_key.end_with?(rate_limiter.api_key_fragment)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # {Unlimited} is a rate limiter that allows all requests.
57
+ # When the rate limiter wasn't found by the API key, it defaults to this class.
58
+ # Or if the `rate_limiters` option is not set in the configuration.
59
+ class Unlimited
60
+ # Executes the block without any restrictions.
61
+ def within_limit
62
+ yield
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ErpIntegration
4
- VERSION = '0.54.0'
4
+ VERSION = '0.56.0'
5
5
  end
@@ -11,6 +11,7 @@ require 'json'
11
11
  require_relative 'erp_integration/version'
12
12
  require_relative 'erp_integration/errors'
13
13
  require_relative 'erp_integration/api_keys_pool'
14
+ require_relative 'erp_integration/rate_limiter'
14
15
  require_relative 'erp_integration/configuration'
15
16
 
16
17
  # Middleware
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: erp_integration
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.54.0
4
+ version: 0.56.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Vermaas
@@ -329,6 +329,7 @@ files:
329
329
  - lib/erp_integration/purchase_order.rb
330
330
  - lib/erp_integration/purchase_order_line.rb
331
331
  - lib/erp_integration/purchase_request.rb
332
+ - lib/erp_integration/rate_limiter.rb
332
333
  - lib/erp_integration/resource.rb
333
334
  - lib/erp_integration/resources/errors.rb
334
335
  - lib/erp_integration/resources/persistence.rb
@@ -368,7 +369,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
368
369
  - !ruby/object:Gem::Version
369
370
  version: '0'
370
371
  requirements: []
371
- rubygems_version: 3.5.11
372
+ rubygems_version: 3.5.6
372
373
  signing_key:
373
374
  specification_version: 4
374
375
  summary: Connects Mejuri with third-party ERP vendors