vindi-rails 0.3.0 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d47de92a057d08eecb40383f35a7083241a21c2ab5d1cf4a8570e9e785745c2
4
- data.tar.gz: 63e9778169289f179efb040ccdcae656b50bff4ac180d28d2682cd8376965a30
3
+ metadata.gz: 6f34e79050cac8b00e8adb6757a19bf53c5f8dc6d36e73a9dbd00a9e9d0225eb
4
+ data.tar.gz: 22e96970c0466b000dfa0335f9b375dd6cd947983796bb726bb4012c2951b704
5
5
  SHA512:
6
- metadata.gz: 4e541c8084c6ccd3ed0f6c2a35992771cb28e3d32255f4627de72a6a495b1cf10fbb9689c9f9cf16ef6b0c58d16844274a04f3766f78a53a3f15ae2bf6c3cc16
7
- data.tar.gz: 8b9df8873fbf4da7ff52d843942727bc358cb3c9e9361760d91c185487d5c93e169c17a7baf40ae9a932000ab75c25b37a2b6a65d15cfb463bbfefa7e50828dd
6
+ metadata.gz: 6a59dcae84e89f32fc84944528ae9e91ec154f85211c66201e88c8742af6129cf5c2d7b883407479cfdbac2112c7541478f43af4c2c662e059aa9ff7b3cec11d
7
+ data.tar.gz: 7327ed4fc6ededab8b4cb9a3da5baae263818d9567f4133c83faa3f6ec3575f723b586377d2b1a7695bbedda0b8584c79fa8562c2bd00d3d72d91c89f4e4d068
data/CHANGELOG.md CHANGED
@@ -3,6 +3,11 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
5
  ---
6
+ ## [0.4.0] - 2026-06-15
7
+
8
+ ### Added
9
+ - **Rate Limit & Auto-Retry**: Automated retry handling with exponential backoff for HTTP 429 (Rate Limit) responses and temporary connection timeouts. Respects the `Retry-After` header when provided.
10
+ - **Caching**: Built-in, configurable caching mechanism for static/rarely-changing resources (Plans, Products, Discounts, Payment Methods) to minimize redundant network requests.
6
11
 
7
12
  ## [0.3.0] - 2026-06-13
8
13
 
data/CHANGELOG.pt-BR.md CHANGED
@@ -3,6 +3,11 @@
3
3
  Todas as alterações notáveis neste projeto serão documentadas neste arquivo.
4
4
 
5
5
  ---
6
+ ## [0.4.0] - 15-06-2026
7
+
8
+ ### Adicionado
9
+ - **Rate Limit & Auto-Retry**: Suporte automatizado a retentativas com recuo exponencial (exponential backoff) para erros HTTP 429 (Limite de Requisições) e timeouts temporários de conexão. Respeita o cabeçalho `Retry-After` da resposta se fornecido.
10
+ - **Cache**: Mecanismo de cache integrado e configurável para recursos estáticos/pouco mutáveis (Planos, Produtos, Descontos, Meios de Pagamento) para mitigar chamadas de rede desnecessárias.
6
11
 
7
12
  ## [0.3.0] - 13-06-2026
8
13
 
data/README.md CHANGED
@@ -33,6 +33,16 @@ Vindi.configure do |config|
33
33
  config.api_key = 'your_private_api_key'
34
34
  # Optional: Define base URL (default is Sandbox)
35
35
  # config.api_url = 'https://gp.vindi.com.br/api/v1'
36
+
37
+ # Optional: Configure cache store (e.g. ActiveSupport::Cache::MemoryStore.new or Rails.cache)
38
+ # config.cache_store = Rails.cache
39
+ # config.cache_ttl = 300 # Expiration time in seconds (default is 300)
40
+ # config.cached_resources = [:plans, :products, :discounts, :payment_methods] # Resources to cache
41
+
42
+ # Optional: Configure rate limit retries and exponential backoff
43
+ # config.max_retries = 3 # Max retries for rate limits or timeouts (default is 3)
44
+ # config.retry_backoff_factor = 2 # Multiplier for delays (default is 2)
45
+ # config.retry_base_delay = 1.0 # Initial wait time in seconds (default is 1.0)
36
46
  end
37
47
  ```
38
48
 
data/README.pt-BR.md CHANGED
@@ -33,6 +33,16 @@ Vindi.configure do |config|
33
33
  config.api_key = 'sua_chave_privada_da_api'
34
34
  # Opcional: Define a URL base (padrão é o Sandbox da Vindi)
35
35
  # config.api_url = 'https://gp.vindi.com.br/api/v1'
36
+
37
+ # Opcional: Configurar cache (ex: ActiveSupport::Cache::MemoryStore.new ou Rails.cache)
38
+ # config.cache_store = Rails.cache
39
+ # config.cache_ttl = 300 # Tempo de expiração em segundos (padrão é 300)
40
+ # config.cached_resources = [:plans, :products, :discounts, :payment_methods] # Recursos sob cache
41
+
42
+ # Opcional: Configurar limite de retentativas (rate limit / timeouts)
43
+ # config.max_retries = 3 # Máximo de retentativas em falhas de rede ou status 429 (padrão é 3)
44
+ # config.retry_backoff_factor = 2 # Multiplicador de recuo de tempo (padrão é 2)
45
+ # config.retry_base_delay = 1.0 # Tempo inicial de espera em segundos (padrão é 1.0)
36
46
  end
37
47
  ```
38
48
 
data/WIKI.md CHANGED
@@ -477,3 +477,76 @@ def charge
477
477
  )
478
478
  end
479
479
  ```
480
+
481
+ ---
482
+
483
+ ## 5. Caching
484
+
485
+ To reduce network overhead and API requests, the SDK supports optional, configurable caching of GET requests (`list` and `find`) for static/rarely-changing resources (e.g., Plans, Products, Discounts, and Payment Methods).
486
+
487
+ ### Caching Configuration
488
+
489
+ Configure the cache store and duration inside the `vindi.rb` initializer:
490
+
491
+ ```ruby
492
+ Vindi.configure do |config|
493
+ config.api_key = 'your_private_api_key'
494
+
495
+ # Configure a cache store that responds to #fetch(key, options)
496
+ # For Rails applications:
497
+ config.cache_store = Rails.cache
498
+
499
+ # Or a standalone memory store:
500
+ # config.cache_store = ActiveSupport::Cache::MemoryStore.new
501
+
502
+ # Expiration time for cached responses (in seconds)
503
+ config.cache_ttl = 300 # (Default: 5 minutes)
504
+
505
+ # Custom list of endpoints to cache (defaults to plans, products, discounts, and payment methods)
506
+ # config.cached_resources = [:plans, :products, :discounts, :payment_methods]
507
+ end
508
+ ```
509
+
510
+ ### Cache Invalidation
511
+
512
+ Since caching uses standard cache stores, you can manually clear or invalidate cached queries by deleting the keys from your cache store. Cache keys follow the format:
513
+
514
+ `vindi:cache:<endpoint_path>:<md5_hash_of_params>`
515
+
516
+ For example:
517
+ `vindi:cache:plans:81b0730...`
518
+
519
+ ---
520
+
521
+ ## 6. Rate Limiting & Auto-Retry
522
+
523
+ To ensure high availability and prevent transient request failures, the SDK comes with built-in support for auto-retries when the application encounters HTTP 429 (Rate Limit Exceeded) errors or network timeouts.
524
+
525
+ ### How it Works
526
+ When a request fails, the SDK determines if it is retryable:
527
+ 1. HTTP 429 (Rate Limit) responses are retried.
528
+ 2. Network timeouts or broken connections (`RestClient::Exceptions::Timeout`, `RestClient::ServerBrokeConnection`) are retried.
529
+ 3. Other HTTP errors (e.g. 401, 403, 404, 422) are raised immediately and not retried.
530
+
531
+ ### Delay Calculation
532
+ The delay between retries is calculated using:
533
+ - **`Retry-After` Header**: If the Vindi API responds with a `Retry-After` header, the SDK respects it and pauses for that specific duration.
534
+ - **Exponential Backoff**: If no header is provided, the SDK uses an exponential backoff algorithm based on `retry_base_delay * (retry_backoff_factor ** (retry_number - 1))`.
535
+
536
+ ### Configuration Options
537
+ You can tune the retry parameters in your initializer:
538
+
539
+ ```ruby
540
+ Vindi.configure do |config|
541
+ # Maximum number of retries before giving up and raising the exception (Default: 3)
542
+ config.max_retries = 3
543
+
544
+ # Multiplication base factor for exponential delays (Default: 2)
545
+ config.retry_backoff_factor = 2
546
+
547
+ # Initial wait time in seconds for the first retry delay (Default: 1.0)
548
+ config.retry_base_delay = 1.0
549
+ end
550
+ ```
551
+
552
+
data/WIKI.pt-BR.md CHANGED
@@ -477,3 +477,76 @@ def charge
477
477
  )
478
478
  end
479
479
  ```
480
+
481
+ ---
482
+
483
+ ## 5. Cache
484
+
485
+ Para reduzir a sobrecarga de rede e o consumo de requisições à API, o SDK suporta um mecanismo opcional e configurável de cache para chamadas GET (`list` e `find`) em recursos estáticos ou pouco mutáveis (como Planos, Produtos, Descontos e Meios de Pagamento).
486
+
487
+ ### Configuração de Cache
488
+
489
+ Configure o gerenciador e o tempo de expiração do cache no arquivo de inicialização `vindi.rb`:
490
+
491
+ ```ruby
492
+ Vindi.configure do |config|
493
+ config.api_key = 'sua_chave_privada_da_api'
494
+
495
+ # Define um cache store que responda a #fetch(key, options)
496
+ # Em aplicações Rails:
497
+ config.cache_store = Rails.cache
498
+
499
+ # Ou utilize uma instância isolada:
500
+ # config.cache_store = ActiveSupport::Cache::MemoryStore.new
501
+
502
+ # Tempo de expiração do cache (em segundos)
503
+ config.cache_ttl = 300 # (Padrão: 5 minutos)
504
+
505
+ # Lista personalizada de recursos sob cache (padrão inclui plans, products, discounts e payment_methods)
506
+ # config.cached_resources = [:plans, :products, :discounts, :payment_methods]
507
+ end
508
+ ```
509
+
510
+ ### Invalidação de Cache
511
+
512
+ Como o mecanismo utiliza o cache store padrão configurado, você pode remover ou invalidar chaves manualmente limpando chaves específicas do seu cache store. As chaves de cache seguem a estrutura:
513
+
514
+ `vindi:cache:<nome_do_endpoint>:<hash_md5_dos_parametros>`
515
+
516
+ Por exemplo:
517
+ `vindi:cache:plans:81b0730...`
518
+
519
+ ---
520
+
521
+ ## 6. Limite de Requisições & Retentativas Automáticas (Rate Limiting & Auto-Retry)
522
+
523
+ Para garantir alta disponibilidade e evitar falhas temporárias em chamadas de rede, o SDK possui suporte nativo a retentativas automáticas quando ocorrem erros HTTP 429 (Limite de Requisições Excedido) ou timeouts temporários de conexão.
524
+
525
+ ### Como Funciona
526
+ Ao falhar em uma requisição, o SDK avalia se a falha é passível de retentativa:
527
+ 1. Erros HTTP 429 (Rate Limit) são elegíveis para nova tentativa.
528
+ 2. Timeouts de conexão ou de leitura de dados (`RestClient::Exceptions::Timeout`, `RestClient::ServerBrokeConnection`) são elegíveis.
529
+ 3. Outros erros HTTP (como 401, 403, 404, 422) **não** são retentados e a exceção correspondente é lançada imediatamente.
530
+
531
+ ### Cálculo de Tempo de Espera (Delay)
532
+ O tempo entre retentativas é determinado por:
533
+ - **Cabeçalho `Retry-After`**: Se a API da Vindi retornar o cabeçalho `Retry-After`, o SDK suspende a execução pelo tempo estipulado pela API.
534
+ - **Recuo Exponencial (Exponential Backoff)**: Se o cabeçalho não estiver presente, o cálculo utiliza a fórmula `retry_base_delay * (retry_backoff_factor ** (retry_number - 1))`.
535
+
536
+ ### Opções de Configuração
537
+ Você pode ajustar esses parâmetros no initializer do seu projeto:
538
+
539
+ ```ruby
540
+ Vindi.configure do |config|
541
+ # Número máximo de retentativas antes de lançar a exceção final (Padrão: 3)
542
+ config.max_retries = 3
543
+
544
+ # Fator multiplicador de tempo para o recuo exponencial (Padrão: 2)
545
+ config.retry_backoff_factor = 2
546
+
547
+ # Tempo base de espera inicial em segundos (Padrão: 1.0)
548
+ config.retry_base_delay = 1.0
549
+ end
550
+ ```
551
+
552
+
data/lib/vindi/client.rb CHANGED
@@ -1,69 +1,123 @@
1
- # frozen_string_literal: true
2
-
3
- require "base64"
4
-
5
- module Vindi
6
- class Client
7
- class << self
8
- def request(method, path, params = {}, headers = {})
9
- api_key = Vindi.configuration.api_key
10
- raise UnauthorizedError.new("API key is not configured.", status: 401) unless api_key
11
-
12
- url = build_url(path)
13
- payload = build_payload(method, params)
14
- req_headers = build_headers(api_key, headers)
15
-
16
- execute_request(method, url, payload, req_headers)
17
- rescue RestClient::Exception => e
18
- handle_rest_client_error(e)
19
- end
20
-
21
- private
22
-
23
- def build_url(path)
24
- "#{Vindi.configuration.api_url}/#{path.sub(%r{^/}, '')}"
25
- end
26
-
27
- def build_payload(method, params)
28
- return nil if %i[get delete].include?(method.to_sym.downcase)
29
- params.to_json
30
- end
31
-
32
- def build_headers(api_key, custom_headers)
33
- auth = Base64.strict_encode64("#{api_key}:")
34
- {
35
- "Authorization" => "Basic #{auth}",
36
- "Content-Type" => "application/json",
37
- "Accept" => "application/json"
38
- }.merge(custom_headers)
39
- end
40
-
41
- def execute_request(method, url, payload, headers)
42
- options = { method: method.to_sym, url: url, headers: headers }
43
- options[:payload] = payload if payload
44
- response = RestClient::Request.execute(options)
45
- JSON.parse(response.body, symbolize_names: true)
46
- end
47
-
48
- def handle_rest_client_error(error)
49
- status = error.response&.code
50
- body = error.response&.body
51
- message = "HTTP request failed: #{error.message}. Response: #{body}"
52
-
53
- raise_specific_error(message, status, body)
54
- end
55
-
56
- def raise_specific_error(msg, status, body)
57
- case status
58
- when 401 then raise UnauthorizedError.new(msg, status: status, response_body: body)
59
- when 403 then raise ForbiddenError.new(msg, status: status, response_body: body)
60
- when 404 then raise NotFoundError.new(msg, status: status, response_body: body)
61
- when 422 then raise UnprocessableEntityError.new(msg, status: status, response_body: body)
62
- when 429 then raise RateLimitError.new(msg, status: status, response_body: body)
63
- when 500..599 then raise InternalServerError.new(msg, status: status, response_body: body)
64
- else raise APIError.new(msg, status: status, response_body: body)
65
- end
66
- end
67
- end
68
- end
69
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "digest"
5
+
6
+ module Vindi
7
+ class Client
8
+ class << self
9
+ def request(method, path, params = {}, headers = {})
10
+ api_key = Vindi.configuration.api_key
11
+ raise UnauthorizedError.new("API key is not configured.", status: 401) unless api_key
12
+
13
+ url = build_url(path)
14
+ payload = build_payload(method, params)
15
+ req_headers = build_headers(api_key, headers)
16
+
17
+ if cacheable?(method, path)
18
+ fetch_from_cache(path, params) { execute_request_with_retry(method, url, payload, req_headers) }
19
+ else
20
+ execute_request_with_retry(method, url, payload, req_headers)
21
+ end
22
+ rescue RestClient::Exception => e
23
+ handle_rest_client_error(e)
24
+ end
25
+
26
+ private
27
+
28
+ def execute_request_with_retry(method, url, payload, headers)
29
+ retries = 0
30
+ max_retries = Vindi.configuration.max_retries || 3
31
+ begin
32
+ execute_request(method, url, payload, headers)
33
+ rescue RestClient::Exception => e
34
+ if retryable?(e, retries, max_retries)
35
+ retries += 1
36
+ sleep(calculate_delay(e, retries))
37
+ retry
38
+ end
39
+ raise e
40
+ end
41
+ end
42
+
43
+ def retryable?(error, retries, max_retries)
44
+ return false if retries >= max_retries
45
+ return true if error.response&.code == 429
46
+ error.is_a?(RestClient::Exceptions::Timeout) || error.is_a?(RestClient::ServerBrokeConnection)
47
+ end
48
+
49
+ def calculate_delay(error, retries)
50
+ if error.response && error.response.headers
51
+ retry_after = error.response.headers[:retry_after] || error.response.headers["retry-after"]
52
+ return retry_after.to_f if retry_after && retry_after.to_f > 0
53
+ end
54
+ factor = Vindi.configuration.retry_backoff_factor || 2
55
+ base = Vindi.configuration.retry_base_delay || 1.0
56
+ base * (factor ** (retries - 1))
57
+ end
58
+
59
+ def cacheable?(method, path)
60
+ return false unless method.to_sym.downcase == :get
61
+ return false unless Vindi.configuration.cache_store
62
+
63
+ resource = path.split("/").first&.to_sym
64
+ Vindi.configuration.cached_resources&.include?(resource)
65
+ end
66
+
67
+ def fetch_from_cache(path, params, &block)
68
+ key = cache_key(path, params)
69
+ Vindi.configuration.cache_store.fetch(key, expires_in: Vindi.configuration.cache_ttl, &block)
70
+ end
71
+
72
+ def cache_key(path, params)
73
+ hash = Digest::MD5.hexdigest(params.to_json)
74
+ "vindi:cache:#{path}:#{hash}"
75
+ end
76
+
77
+ def build_url(path)
78
+ "#{Vindi.configuration.api_url}/#{path.sub(%r{^/}, '')}"
79
+ end
80
+
81
+ def build_payload(method, params)
82
+ return nil if %i[get delete].include?(method.to_sym.downcase)
83
+ params.to_json
84
+ end
85
+
86
+ def build_headers(api_key, custom_headers)
87
+ auth = Base64.strict_encode64("#{api_key}:")
88
+ {
89
+ "Authorization" => "Basic #{auth}",
90
+ "Content-Type" => "application/json",
91
+ "Accept" => "application/json"
92
+ }.merge(custom_headers)
93
+ end
94
+
95
+ def execute_request(method, url, payload, headers)
96
+ options = { method: method.to_sym, url: url, headers: headers }
97
+ options[:payload] = payload if payload
98
+ response = RestClient::Request.execute(options)
99
+ JSON.parse(response.body, symbolize_names: true)
100
+ end
101
+
102
+ def handle_rest_client_error(error)
103
+ status = error.response&.code
104
+ body = error.response&.body
105
+ message = "HTTP request failed: #{error.message}. Response: #{body}"
106
+
107
+ raise_specific_error(message, status, body)
108
+ end
109
+
110
+ def raise_specific_error(msg, status, body)
111
+ case status
112
+ when 401 then raise UnauthorizedError.new(msg, status: status, response_body: body)
113
+ when 403 then raise ForbiddenError.new(msg, status: status, response_body: body)
114
+ when 404 then raise NotFoundError.new(msg, status: status, response_body: body)
115
+ when 422 then raise UnprocessableEntityError.new(msg, status: status, response_body: body)
116
+ when 429 then raise RateLimitError.new(msg, status: status, response_body: body)
117
+ when 500..599 then raise InternalServerError.new(msg, status: status, response_body: body)
118
+ else raise APIError.new(msg, status: status, response_body: body)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -1,14 +1,21 @@
1
- # frozen_string_literal: true
2
-
3
- module Vindi
4
- class Configuration
5
- attr_accessor :api_key, :api_url
6
-
7
- DEFAULT_API_URL = "https://sandbox-gp.vindi.com.br/api/v1"
8
-
9
- def initialize
10
- @api_key = nil
11
- @api_url = DEFAULT_API_URL
12
- end
13
- end
14
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ class Configuration
5
+ attr_accessor :api_key, :api_url, :cache_store, :cache_ttl, :cached_resources,
6
+ :max_retries, :retry_backoff_factor, :retry_base_delay
7
+
8
+ DEFAULT_API_URL = "https://sandbox-gp.vindi.com.br/api/v1"
9
+
10
+ def initialize
11
+ @api_key = nil
12
+ @api_url = DEFAULT_API_URL
13
+ @cache_store = nil
14
+ @cache_ttl = 300
15
+ @cached_resources = %i[plans products discounts payment_methods]
16
+ @max_retries = 3
17
+ @retry_backoff_factor = 2
18
+ @retry_base_delay = 1.0
19
+ end
20
+ end
21
+ end
data/lib/vindi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vindi
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vindi-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wesley Lima
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-13 00:00:00.000000000 Z
11
+ date: 2026-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client