cnpj-val 0.0.0 → 1.0.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.
data/README.pt.md ADDED
@@ -0,0 +1,355 @@
1
+ ![cnpj-val para Ruby](https://br-utils.vercel.app/img/cover_cnpj-val.jpg)
2
+
3
+ > 🚀 **Suporte total ao [novo formato alfanumérico de CNPJ](https://github.com/user-attachments/files/23937961/calculodvcnpjalfanaumerico.pdf).**
4
+
5
+ > 🌎 [Access documentation in English](./README.md)
6
+
7
+ Utilitário em Ruby para validar CNPJ (Cadastro Nacional da Pessoa Jurídica).
8
+
9
+ ## Recursos
10
+
11
+ - ✅ **CNPJ alfanumérico**: Valida CNPJ de 14 caracteres no formato numérico ou alfanumérico
12
+ - ✅ **Entrada flexível**: Aceita `String` ou `Array` de strings; elementos do array são concatenados na ordem
13
+ - ✅ **Agnóstico ao formato**: Remove caracteres não alfanuméricos (ou não numéricos quando `type` é `numeric`) e opcionalmente converte para maiúsculas antes de validar
14
+ - ✅ **Sensibilidade a maiúsculas opcional**: Com `case_sensitive` em `false`, letras minúsculas são aceitas para CNPJ alfanumérico
15
+ - ✅ **Sobrescrita por chamada**: Padrões da instância podem ser sobrescritos apenas naquela chamada a `is_valid`
16
+ - ✅ **Tratamento de erros**: Uso incorreto da API vs erros de domínio, com marcador `CnpjVal::Error` para rescue em toda a biblioteca
17
+ - ✅ **Dependências mínimas**: [`cnpj-dv`](https://rubygems.org/gems/cnpj-dv) para cálculo dos dígitos verificadores e [`lacus-utils`](https://rubygems.org/gems/lacus-utils) para descrição de tipos nas mensagens de erro
18
+ - ✅ **API dupla**: Orientada a objetos (`CnpjVal::CnpjValidator`) e funcional (`CnpjVal.cnpj_val`)
19
+
20
+ ## Instalação
21
+
22
+ Instale a gem diretamente:
23
+
24
+ ```bash
25
+ gem install cnpj-val
26
+ ```
27
+
28
+ Ou adicione ao seu `Gemfile` e execute `bundle install`:
29
+
30
+ ```ruby
31
+ gem 'cnpj-val'
32
+ ```
33
+
34
+ ## Require
35
+
36
+ ```ruby
37
+ require 'cnpj-val'
38
+ ```
39
+
40
+ ## Início rápido
41
+
42
+ ```ruby
43
+ require 'cnpj-val'
44
+
45
+ validator = CnpjVal::CnpjValidator.new
46
+
47
+ validator.is_valid('98765432000198') # => true
48
+ validator.is_valid('98.765.432/0001-98') # => true
49
+ validator.is_valid('98765432000199') # => false
50
+
51
+ validator.is_valid('1QB5UKALPYFP59') # => true (alfanumérico)
52
+ validator.is_valid('1QB5UKALpyfp59') # => false (padrão é case-sensitive)
53
+ validator.is_valid('1QB5UKALpyfp59', case_sensitive: false) # => true
54
+
55
+ validator.is_valid('96206256120884') # => true (numérico)
56
+ validator.is_valid('1QB5UKALPYFP59', type: 'numeric') # => false (letras removidas → comprimento ≠ 14)
57
+ ```
58
+
59
+ Helper funcional:
60
+
61
+ ```ruby
62
+ require 'cnpj-val'
63
+
64
+ CnpjVal.cnpj_val('98765432000198') # => true
65
+ CnpjVal.cnpj_val('98.765.432/0001-98') # => true
66
+ CnpjVal.cnpj_val('98765432000199') # => false
67
+ ```
68
+
69
+ ## Utilização
70
+
71
+ Os pontos principais são a classe `CnpjVal::CnpjValidator`, a classe de opções `CnpjVal::CnpjValidatorOptions` e o helper `CnpjVal.cnpj_val`.
72
+
73
+ ### `CnpjVal::CnpjValidator`
74
+
75
+ - **`initialize(options = nil, **keywords)`**: Opções padrão de validação. Quando `options` é fornecido isoladamente (instância de `CnpjVal::CnpjValidatorOptions` ou `Hash`), ele determina as opções padrão; uma instância de `CnpjVal::CnpjValidatorOptions` é armazenada por referência (mutações posteriores afetam futuras chamadas de `is_valid` que não passarem opções por chamada), enquanto um `Hash` cria uma nova instância. Quando `options` é omitido (`nil`), as opções padrão são construídas exclusivamente a partir dos argumentos nomeados (`case_sensitive:`, `type:`). Passar `options` junto com qualquer argumento nomeado não `nil` gera `InvalidArgumentCombinationError`, em vez de ignorar os argumentos nomeados silenciosamente. Exemplo: `CnpjVal::CnpjValidator.new(type: 'numeric', case_sensitive: false)`.
76
+ - **`options`**: Retorna o `CnpjVal::CnpjValidatorOptions` da instância (o mesmo objeto usado internamente).
77
+ - **`is_valid(cnpj_input, options = nil, **keywords)`**: Valida um valor CNPJ.
78
+
79
+ A entrada é normalizada para string (arrays de strings são concatenados). Quando `case_sensitive` é `false`, a string é convertida para maiúsculas antes da sanitização. Caracteres são removidos conforme `type`. Se o comprimento após sanitização não for exatamente **14**, se os dois últimos caracteres não forem dígitos ou se os dígitos verificadores não coincidirem (`CnpjDV::CnpjCheckDigits` de **`cnpj-dv`**), o método retorna `false` — nenhuma exceção é lançada por falha de validação.
80
+
81
+ Se a entrada não for `String` nem `Array` de strings, é lançada **`CnpjVal::TypeMismatchError`**.
82
+
83
+ `options` por chamada e argumentos nomeados nunca são mesclados: um `options` fornecido isoladamente sobrescreve totalmente os padrões da instância nesta chamada; caso contrário, qualquer argumento nomeado fornecido sobrescreve os padrões da instância nesta chamada. Quando nenhum dos dois é fornecido, os padrões da instância são usados como estão. Os padrões da instância nunca são alterados por uma sobrescrita pontual. Passar `options` junto com qualquer argumento nomeado não `nil` gera `InvalidArgumentCombinationError`.
84
+
85
+ ```ruby
86
+ require 'cnpj-val'
87
+
88
+ validator = CnpjVal::CnpjValidator.new(type: 'numeric')
89
+
90
+ validator.is_valid('98.765.432/0001-98') # => true
91
+ validator.is_valid('1QB5UKALPYFP59') # => false (letras removidas → comprimento ≠ 14)
92
+ validator.is_valid('1QB5UKALpyfp59', type: 'alphanumeric', case_sensitive: false) # => true
93
+ ```
94
+
95
+ Padrões na instância; sobrescrita por chamada:
96
+
97
+ ```ruby
98
+ require 'cnpj-val'
99
+
100
+ validator = CnpjVal::CnpjValidator.new(case_sensitive: false)
101
+
102
+ validator.is_valid('1qb5ukalpyfp59') # => true (padrões da instância)
103
+ validator.is_valid('1qb5ukalpyfp59', case_sensitive: true) # só nesta chamada: false
104
+ validator.is_valid('1qb5ukalpyfp59') # => true de novo
105
+ ```
106
+
107
+ ### `CnpjVal::CnpjValidatorOptions`
108
+
109
+ Armazena configurações do validador (`case_sensitive`, `type`). Construa com um `Hash` opcional ou instância de `CnpjVal::CnpjValidatorOptions`, objetos extras de sobrescrita (mesclados em ordem) e/ou argumentos nomeados. Expõe acessores: `case_sensitive`, `type`.
110
+
111
+ - **`all`**: Retorna um snapshot superficial congelado (`Hash`) de todas as opções atuais.
112
+ - **`set(options)`**: Atualiza vários campos de uma vez; retorna `self`. Aceita um `Hash` ou outra instância de `CnpjVal::CnpjValidatorOptions`. Chaves omitidas mantêm o valor atual.
113
+
114
+ ```ruby
115
+ require 'cnpj-val'
116
+
117
+ options = CnpjVal::CnpjValidatorOptions.new(case_sensitive: false, type: 'numeric')
118
+ options.case_sensitive # => false
119
+ options.type # => "numeric"
120
+ options.set({ type: 'alphanumeric' }) # mescla e retorna self
121
+ options.all # => snapshot congelado das opções atuais
122
+ ```
123
+
124
+ ### Helper funcional
125
+
126
+ `CnpjVal.cnpj_val` instancia um novo `CnpjVal::CnpjValidator` com os mesmos parâmetros do construtor e chama `is_valid(cnpj_input)` uma vez. Passe argumentos nomeados **ou** um `Hash`/instância de `CnpjVal::CnpjValidatorOptions` para as opções — não ambos (passar `options` com qualquer argumento nomeado não-`nil` lança `InvalidArgumentCombinationError`):
127
+
128
+ ```ruby
129
+ require 'cnpj-val'
130
+
131
+ CnpjVal.cnpj_val('98765432000198') # => true
132
+ CnpjVal.cnpj_val('1QB5UKALpyfp59', case_sensitive: false) # => true
133
+ CnpjVal.cnpj_val('1QB5UKALPYFP59', type: 'numeric') # => false
134
+ CnpjVal.cnpj_val('1QB5UKALpyfp59', { # forma com Hash
135
+ type: 'alphanumeric',
136
+ case_sensitive: false,
137
+ }) # => true
138
+ ```
139
+
140
+ ### Formatos de entrada
141
+
142
+ **String:** Dígitos e/ou letras brutos, ou CNPJ já formatado (ex.: `98.765.432/0001-98`, `1Q.B5U.KAL/PYFP-59`). Caracteres são removidos conforme `type`; quando `case_sensitive` é `false`, letras são convertidas para maiúsculas antes da validação alfanumérica.
143
+
144
+ **Array de strings:** Cada elemento deve ser `String`; os valores são concatenados (ex.: por dígito, segmentos agrupados ou misturados com pontuação). Elementos que não sejam string lançam **`CnpjVal::TypeMismatchError`**.
145
+
146
+ ```ruby
147
+ require 'cnpj-val'
148
+
149
+ CnpjVal.cnpj_val(['1', 'Q', 'B', '5', 'U', 'K', 'A', 'L', 'P', 'Y', 'F', 'P', '5', '9']) # => true
150
+ CnpjVal.cnpj_val(['1Q.B5U', 'KAL', 'PYFP-59']) # => true
151
+ ```
152
+
153
+ ### Opções de validação
154
+
155
+ | Parâmetro | Tipo | Padrão | Descrição |
156
+ |-----------|------|---------|-------------|
157
+ | `type` | `'alphanumeric'` \| `'numeric'` \| `nil` | `'alphanumeric'` | Conjunto de caracteres após sanitização: alfanumérico (`0`–`9`, `A`–`Z`, `a`–`z`) ou apenas numérico (`0`–`9`) |
158
+ | `case_sensitive` | `Boolean`, `nil` | `true` | Se `false`, letras minúsculas são convertidas para maiúsculas antes da validação alfanumérica |
159
+
160
+ CNPJ inválido (comprimento errado após sanitização, dígitos verificadores inválidos, base/filial inelegíveis `00000000` / `0000`, dígitos repetidos, caracteres não numéricos nos verificadores) retorna **`false`** — nenhuma exceção é lançada por falha de validação.
161
+
162
+ Exemplo com todas as opções:
163
+
164
+ ```ruby
165
+ require 'cnpj-val'
166
+
167
+ CnpjVal.cnpj_val(
168
+ '1QB5UKALpyfp59',
169
+ type: 'alphanumeric',
170
+ case_sensitive: false,
171
+ )
172
+ ```
173
+
174
+ ### Tratamento de erros
175
+
176
+ Os erros se dividem em duas categorias:
177
+
178
+ | Categoria | Significado |
179
+ |---|---|
180
+ | **Uso incorreto da API** | O chamador usou a biblioteca de forma incorreta (tipo errado para a entrada CNPJ ou para uma opção, ou combinação inválida de argumentos). |
181
+ | **Erro de domínio** | A forma da chamada era válida, mas um valor viola uma regra de negócio (`type` inválido). |
182
+
183
+ Todo erro customizado inclui o módulo marcador `CnpjVal::Error`. Falhas de domínio (`ValidationError`) herdam de `CnpjVal::DomainError` (`RangeError`). Dados de CNPJ inválidos retornam `false` (não levantam erro).
184
+
185
+ **Importante:** passar ao mesmo tempo uma instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` levanta `InvalidArgumentCombinationError`.
186
+
187
+ #### Resumo
188
+
189
+ | Classe | Herda de | Categoria | Condição de disparo |
190
+ |---|---|---|---|
191
+ | `CnpjVal::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` passados ao mesmo tempo |
192
+ | `CnpjVal::TypeMismatchError` | `TypeError` (+ `include Error`) | Uso incorreto da API | Entrada CNPJ ou opção com tipo de dado incorreto |
193
+ | `CnpjVal::ValidationError` | `CnpjVal::DomainError` | Erro de domínio | `type` fora dos valores permitidos |
194
+
195
+ #### `CnpjVal::Error` (módulo marcador)
196
+
197
+ - **Herança:** módulo marcador misturado em todo erro da biblioteca via `include` (não é uma classe).
198
+ - **Categoria:** N/A (apenas alvo de rescue) — não é um modo de falha por si só.
199
+ - **Quando é levantado:** Nunca levantado diretamente; incluído por todo erro customizado que a biblioteca levanta.
200
+ - **Exemplo:** N/A
201
+ - **Como resgatá-lo:**
202
+
203
+ ```ruby
204
+ rescue CnpjVal::Error
205
+ # tudo o que esta biblioteca levanta
206
+ ```
207
+
208
+ #### `CnpjVal::DomainError`
209
+
210
+ - **Herança:** `CnpjVal::DomainError < RangeError` (inclui `CnpjVal::Error`)
211
+ - **Categoria:** Erro de domínio — ancestral de todas as falhas de domínio.
212
+ - **Quando é levantado:** Não levantado diretamente; prefira levantar uma subclasse folha.
213
+ - **Exemplo:** Prefira `raise CnpjVal::ValidationError` a levantar `DomainError` diretamente.
214
+ - **Como resgatá-lo:**
215
+
216
+ ```ruby
217
+ rescue CnpjVal::DomainError
218
+ # ValidationError e outras subclasses de DomainError
219
+ ```
220
+
221
+ #### `CnpjVal::TypeMismatchError`
222
+
223
+ - **Herança:** `CnpjVal::TypeMismatchError < TypeError` (inclui `CnpjVal::Error`)
224
+ - **Categoria:** Uso incorreto da API — o chamador passou um valor do tipo errado.
225
+ - **Quando é levantado:** Levantado quando a entrada CNPJ não é `String` nem `Array` de strings, ou quando uma opção do validador (`type`) tem o tipo de runtime incorreto.
226
+ - **Exemplo:**
227
+
228
+ ```ruby
229
+ CnpjVal.cnpj_val(12_345_678_000_198) # levanta CnpjVal::TypeMismatchError
230
+ CnpjVal.cnpj_val('98765432000198', type: 123) # levanta CnpjVal::TypeMismatchError
231
+ ```
232
+
233
+ - **Como resgatá-lo:**
234
+
235
+ ```ruby
236
+ rescue CnpjVal::TypeMismatchError
237
+ # violação de contrato de tipo desta biblioteca
238
+
239
+ rescue TypeError
240
+ # erros nativos de tipo, incluindo TypeMismatchError desta biblioteca
241
+ ```
242
+
243
+ #### `CnpjVal::InvalidArgumentCombinationError`
244
+
245
+ - **Herança:** `CnpjVal::InvalidArgumentCombinationError < ArgumentError` (inclui `CnpjVal::Error`)
246
+ - **Categoria:** Uso incorreto da API — os argumentos fornecidos não formam uma assinatura válida da API.
247
+ - **Quando é levantado:** Levantado quando `CnpjValidator.new`, `#is_valid` ou `cnpj_val` recebe ao mesmo tempo um argumento `options` (instância ou `Hash`) e qualquer argumento nomeado não `nil`.
248
+ - **Exemplo:**
249
+
250
+ ```ruby
251
+ begin
252
+ CnpjVal.cnpj_val('98765432000198', { type: 'numeric' }, case_sensitive: false)
253
+ rescue CnpjVal::InvalidArgumentCombinationError => e
254
+ puts e.message
255
+ # Pass either an options instance/Hash to `options`, or keyword arguments (case_sensitive:, type:), not both.
256
+ end
257
+ ```
258
+
259
+ - **Como resgatá-lo:**
260
+
261
+ ```ruby
262
+ rescue CnpjVal::InvalidArgumentCombinationError
263
+ # combinação inválida de assinatura desta biblioteca
264
+
265
+ rescue ArgumentError
266
+ # erros nativos de argumento, incluindo InvalidArgumentCombinationError desta biblioteca
267
+ ```
268
+
269
+ #### `CnpjVal::ValidationError`
270
+
271
+ - **Herança:** `CnpjVal::ValidationError < CnpjVal::DomainError < RangeError` (inclui `CnpjVal::Error`)
272
+ - **Categoria:** Erro de domínio — um valor falha uma regra de domínio que não é numérica nem de comprimento.
273
+ - **Quando é levantado:** Levantado quando `type` não é `'alphanumeric'` nem `'numeric'`.
274
+ - **Exemplo:**
275
+
276
+ ```ruby
277
+ CnpjVal.cnpj_val('98765432000198', type: 'invalid') # levanta CnpjVal::ValidationError
278
+ ```
279
+
280
+ - **Como resgatá-lo:**
281
+
282
+ ```ruby
283
+ rescue CnpjVal::ValidationError
284
+ # esta falha exata de validação de domínio
285
+
286
+ rescue CnpjVal::DomainError
287
+ # ValidationError e outras subclasses de DomainError
288
+ ```
289
+
290
+ #### Granularidade de rescue
291
+
292
+ ```ruby
293
+ # 1) Classe nativa única — captura erros de uso incorreto desse tipo.
294
+ rescue TypeError
295
+ # CnpjVal::TypeMismatchError e qualquer outro TypeError (da biblioteca ou não)
296
+
297
+ # 2) CnpjVal::DomainError — captura todas as violações de regra de negócio sob DomainError.
298
+ rescue CnpjVal::DomainError
299
+ # CnpjVal::ValidationError e outras subclasses de DomainError
300
+
301
+ # 3) CnpjVal::Error — captura tudo o que a biblioteca levanta.
302
+ rescue CnpjVal::Error
303
+ # todo erro customizado que inclui CnpjVal::Error
304
+
305
+ # 4) Classe folha específica — captura apenas aquele modo de falha.
306
+ rescue CnpjVal::ValidationError
307
+ # apenas CnpjVal::ValidationError
308
+ ```
309
+
310
+ Atributos notáveis nos erros levantados:
311
+
312
+ - `TypeMismatchError`: `option_name` (nil para entrada CNPJ), `actual_input`, `actual_type`, `expected_type`
313
+ - `ValidationError`: `option_name`, `actual_input`, `expected_values`
314
+
315
+ ## API
316
+
317
+ ### Exportações
318
+
319
+ Após `require 'cnpj-val'`:
320
+
321
+ - **`CnpjVal.cnpj_val`**: `(cnpj_input, options = nil, **keywords) -> Boolean` — helper de conveniência.
322
+ - **`CnpjVal::CnpjValidator`**: Classe para validar CNPJ com opções padrão opcionais; aceita `String` ou `Array<String>` em `is_valid`.
323
+ - **`CnpjVal::CnpjValidatorOptions`**: Classe que armazena opções; suporta mesclagem via construtor, `set` e argumentos nomeados.
324
+ - **`CnpjVal::CNPJ_LENGTH`**: `14` (constante).
325
+ - **`CnpjVal::VERSION`**: string de versão da gem.
326
+ - **Predicado de tipo**: `CnpjVal::CnpjInput` — `CnpjVal::CnpjInput.accept?(value)` / `CnpjVal::CnpjInput === value` é verdadeiro apenas para `String` ou `Array<String>`.
327
+ - **Marcadores de tipo**: `CnpjVal::CnpjType`, `CnpjVal::CnpjValidatorOptionsInput`.
328
+ - **Erros**: `CnpjVal::Error`, `CnpjVal::DomainError`, `CnpjVal::InvalidArgumentCombinationError`, `CnpjVal::TypeMismatchError`, `CnpjVal::ValidationError`.
329
+
330
+ ### Outros recursos disponíveis
331
+
332
+ - **`CnpjVal::CnpjValidatorOptions::CNPJ_LENGTH`**: `14`.
333
+ - **`CnpjVal::CnpjValidatorOptions::DEFAULT_CASE_SENSITIVE`**: `true`.
334
+ - **`CnpjVal::CnpjValidatorOptions::DEFAULT_TYPE`**: `'alphanumeric'`.
335
+
336
+ ## Contribuição e suporte
337
+
338
+ Contribuições são bem-vindas! Consulte as [Diretrizes de contribuição](https://github.com/LacusSolutions/br-utils-ruby/blob/main/CONTRIBUTING.md). Se este projeto for útil para você, considere:
339
+
340
+ - ⭐ Dar uma estrela ao repositório
341
+ - 🤝 Contribuir com o código
342
+ - 💡 [Sugerir novos recursos](https://github.com/LacusSolutions/br-utils-ruby/issues)
343
+ - 🐛 [Reportar bugs](https://github.com/LacusSolutions/br-utils-ruby/issues)
344
+
345
+ ## Licença
346
+
347
+ Este projeto está licenciado sob a MIT License — consulte o arquivo [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE).
348
+
349
+ ## Changelog
350
+
351
+ Consulte o [CHANGELOG](./CHANGELOG.md) para histórico de versões e alterações.
352
+
353
+ ---
354
+
355
+ Feito com ❤️ por [Lacus Solutions](https://github.com/LacusSolutions)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cnpj_validator'
4
+
5
+ module CnpjVal
6
+ # Helper function to simplify the usage of the {CnpjValidator} class.
7
+ #
8
+ # If no options are provided, it validates a CNPJ string or array of strings
9
+ # using default settings. If options are provided, they control case
10
+ # sensitivity and the type of characters to be validated. Invalid CNPJ data
11
+ # returns +false+; only API misuse raises documented errors.
12
+ #
13
+ # Pass either keyword arguments **or** a {Hash}/{CnpjValidatorOptions} instance
14
+ # for options — not both.
15
+ #
16
+ # @param cnpj_input [String, Array<String>] CNPJ value as a string or array of
17
+ # strings
18
+ # @param options [CnpjValidatorOptions, Hash, nil] default validator options
19
+ # @param keywords [Hash] option keyword overrides (mutually exclusive with
20
+ # +options+; see {CnpjValidatorOptions::OPTION_KEYS})
21
+ # @return [Boolean] +true+ when valid, +false+ otherwise
22
+ # @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument
23
+ # are both given
24
+ # @raise [TypeMismatchError] if the input is not a +String+ or +Array<String>+,
25
+ # or if any option has an invalid type
26
+ # @raise [ValidationError] if +type+ is not one of the allowed values
27
+ # @see CnpjValidator#is_valid for detailed option descriptions
28
+ # @see CnpjValidator
29
+ #
30
+ # @example
31
+ # CnpjVal.cnpj_val('91415732000793') # => true
32
+ # CnpjVal.cnpj_val('9JN7MGLJZXIO50') # => true
33
+ # CnpjVal.cnpj_val('9JN7MGLJZXIO51') # => false
34
+ def self.cnpj_val(cnpj_input, options = nil, **keywords)
35
+ CnpjValidator.new(options, **keywords).is_valid(cnpj_input)
36
+ end
37
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cnpj-dv'
4
+
5
+ require_relative 'cnpj_validator_options'
6
+ require_relative 'errors'
7
+ require_relative 'types'
8
+
9
+ module CnpjVal
10
+ # Validator for CNPJ (Cadastro Nacional da Pessoa Jurídica) identifiers.
11
+ #
12
+ # Validates CNPJ strings according to the Brazilian CNPJ validation algorithm.
13
+ # Invalid CNPJ data returns +false+; only API misuse raises documented errors.
14
+ class CnpjValidator
15
+ NUMERIC_PATTERN = /\D/
16
+ ALPHANUMERIC_PATTERN = /[^0-9A-Za-z]/
17
+
18
+ private_constant :NUMERIC_PATTERN, :ALPHANUMERIC_PATTERN
19
+
20
+ # Returns the default options used by this validator when per-call options
21
+ # are not provided.
22
+ #
23
+ # Note that the returned object is the same instance used internally;
24
+ # mutating it (e.g. via setters on {CnpjValidatorOptions}) affects future
25
+ # {#is_valid} calls that do not pass +options+.
26
+ #
27
+ # @return [CnpjValidatorOptions] the instance default options
28
+ attr_reader :options
29
+
30
+ # Creates a new validator with optional default options.
31
+ #
32
+ # Default options apply to every call to {#is_valid} unless overridden by the
33
+ # per-call +options+ argument or keyword overrides. Options control case
34
+ # sensitivity and whether the CNPJ input is alphanumeric or numeric.
35
+ #
36
+ # +options+ and the keyword arguments are never merged with each other: when
37
+ # +options+ is given alone, it determines the default options; a
38
+ # {CnpjValidatorOptions} instance is stored by reference, while a {Hash}
39
+ # builds a new instance. When +options+ is omitted (+nil+), the default
40
+ # options are built exclusively from the keyword arguments, with
41
+ # {CnpjValidatorOptions} filling in its own defaults for every keyword left
42
+ # as +nil+. Passing +options+ together with any non-+nil+ keyword argument
43
+ # raises {InvalidArgumentCombinationError} instead of silently ignoring the
44
+ # keywords.
45
+ #
46
+ # @param options [CnpjValidatorOptions, Hash, nil] default validator options
47
+ # @param keywords [Hash] option keyword overrides (mutually exclusive with
48
+ # +options+; see {CnpjValidatorOptions::OPTION_KEYS})
49
+ # @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument
50
+ # are both given
51
+ # @raise [TypeMismatchError] if any option has an invalid type
52
+ # @raise [ValidationError] if +type+ is not one of the allowed values
53
+ def initialize(options = nil, **keywords)
54
+ @options = resolve_default_options(options, keywords)
55
+ end
56
+
57
+ # Validates a CNPJ input.
58
+ #
59
+ # +options+ and the keyword arguments are never merged with each other: when
60
+ # +options+ is given alone, it fully overrides the instance defaults for this
61
+ # call; otherwise, any given non-+nil+ keyword argument overrides the
62
+ # instance default options for this call. When neither +options+ nor any
63
+ # keyword argument is given, the instance defaults are used as-is. The
64
+ # instance defaults are never mutated by a per-call override. Passing
65
+ # +options+ together with any non-+nil+ keyword argument raises
66
+ # {InvalidArgumentCombinationError} instead of silently ignoring the keywords.
67
+ #
68
+ # @param cnpj_input [String, Array<String>] CNPJ value as a string or array of
69
+ # strings
70
+ # @param options [CnpjValidatorOptions, Hash, nil] per-call option overrides
71
+ # @param keywords [Hash] per-call option keyword overrides (mutually exclusive
72
+ # with +options+; see {CnpjValidatorOptions::OPTION_KEYS})
73
+ # @return [Boolean] +true+ when valid, +false+ otherwise
74
+ # @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument
75
+ # are both given
76
+ # @raise [TypeMismatchError] if the input is not a +String+ or +Array<String>+,
77
+ # or if any option has an invalid type
78
+ # @raise [ValidationError] if +type+ is not one of the allowed values
79
+ #
80
+ # @example
81
+ # CnpjValidator.new(type: 'numeric').is_valid('12651319934215') # => true
82
+ # rubocop:disable Naming/PredicatePrefix -- public API matches JS/Python `is_valid`
83
+ def is_valid(cnpj_input, options = nil, **keywords)
84
+ actual_options = resolve_call_options(options, keywords)
85
+ sanitized_cnpj = sanitize_cnpj_input(cnpj_input, actual_options)
86
+
87
+ return false unless valid_sanitized_length?(sanitized_cnpj)
88
+ return false unless numeric_check_digits?(sanitized_cnpj)
89
+
90
+ validate_with_check_digits(sanitized_cnpj)
91
+ end
92
+ # rubocop:enable Naming/PredicatePrefix
93
+
94
+ private
95
+
96
+ def resolve_default_options(options, keywords)
97
+ keyword_overrides = compact_keyword_overrides(keywords)
98
+ raise_ambiguous_options! if options && !keyword_overrides.empty?
99
+ return options if options.is_a?(CnpjValidatorOptions)
100
+ return CnpjValidatorOptions.new(options) if options
101
+
102
+ CnpjValidatorOptions.new(**keywords)
103
+ end
104
+
105
+ def resolve_call_options(options, keywords)
106
+ keyword_overrides = compact_keyword_overrides(keywords)
107
+ raise_ambiguous_options! if options && !keyword_overrides.empty?
108
+ return @options.copy.set(options) if options
109
+ return @options if keyword_overrides.empty?
110
+
111
+ @options.copy.set(keyword_overrides)
112
+ end
113
+
114
+ def compact_keyword_overrides(keywords)
115
+ CnpjValidatorOptions::OPTION_KEYS.each_with_object({}) do |key, overrides|
116
+ value = keywords[key]
117
+ overrides[key] = value unless value.nil?
118
+ end
119
+ end
120
+
121
+ def raise_ambiguous_options!
122
+ option_keywords = CnpjValidatorOptions::OPTION_KEYS.map { |key| "#{key}:" }.join(', ')
123
+
124
+ raise InvalidArgumentCombinationError,
125
+ "Pass either an options instance/Hash to `options`, or keyword arguments (#{option_keywords}), " \
126
+ 'not both.'
127
+ end
128
+
129
+ def sanitize_cnpj_input(cnpj_input, actual_options)
130
+ actual_input = to_string_input(cnpj_input)
131
+ working_input = actual_options.case_sensitive ? actual_input : actual_input.upcase
132
+
133
+ sanitize(working_input, actual_options.type)
134
+ end
135
+
136
+ def to_string_input(cnpj_input)
137
+ raise TypeMismatchError.new(cnpj_input, 'string or string[]') unless CnpjInput.accept?(cnpj_input)
138
+
139
+ return cnpj_input if cnpj_input.is_a?(String)
140
+
141
+ cnpj_input.join
142
+ end
143
+
144
+ def sanitize(value, cnpj_type)
145
+ if cnpj_type == 'numeric'
146
+ value.gsub(NUMERIC_PATTERN, '')
147
+ else
148
+ value.gsub(ALPHANUMERIC_PATTERN, '')
149
+ end
150
+ end
151
+
152
+ def numeric_check_digits?(sanitized_cnpj)
153
+ twelfth = sanitized_cnpj[12]
154
+ thirteenth = sanitized_cnpj[13]
155
+
156
+ twelfth.between?('0', '9') && thirteenth.between?('0', '9')
157
+ end
158
+
159
+ def valid_sanitized_length?(sanitized_cnpj)
160
+ sanitized_cnpj.length == CnpjValidatorOptions::CNPJ_LENGTH
161
+ end
162
+
163
+ def validate_with_check_digits(sanitized_cnpj)
164
+ cnpj_check_digits = CnpjDV::CnpjCheckDigits.new(sanitized_cnpj)
165
+
166
+ sanitized_cnpj == cnpj_check_digits.cnpj
167
+ rescue CnpjDV::Error
168
+ false
169
+ end
170
+ end
171
+ end