br_validators 0.1.1 → 0.2.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: 6f48e272ba459e1e9962ac199162b257eea94025b41c02c64501a663c71058b3
4
- data.tar.gz: 3c194886a75d84f28c23b4a62414383735ba70509ef25f2d960fc23996efdcc7
3
+ metadata.gz: 7297d8bd1acfbea15d3f7ef85f6bbb6cd5cc5555129c6a83bfade0e69358c14b
4
+ data.tar.gz: 9eab13d77a0a931e3c7a6dc34542dc9a0a069f67f5b9c72f38b9517f457e8336
5
5
  SHA512:
6
- metadata.gz: 1fa44a6062e29f87a87fdca434a07b7cada13d7b091b06cfc21c1cb98de331ef0c44add691368d2d651703f209349d41478cdae03736e6bd566358caf26298f9
7
- data.tar.gz: cd6d9864713501561878ceff7c9a505de1f6791dcd6fb61001a224dbe762bf8c863b40d1f9414f891634c540ba19c149524547ee612372616cfbbf8cff59854a
6
+ metadata.gz: c8de53e6d4904f91915313b8f3f68ccfbe2b4e9269cff2bc0ea2f26c5cc87d2e13b3660cbd6130e4297a8d94d249758d4f1a5e5199b188882f16307413f37e7b
7
+ data.tar.gz: 6d948e9f10e7acd07d35c8e773f770931681afe19976fc447cbe29c6f37969a012629dac68ca187eb6b3f55fa8131cae69a0fd23d646eea250e2f8a719c085d7
data/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  As mudanças de cada versão ficam aqui. O projeto segue o
4
4
  [versionamento semântico](https://semver.org/lang/pt-BR/).
5
5
 
6
+ ## [0.2.0] - 2026-10-26
7
+
8
+ ### Novo
9
+
10
+ - `BrValidators::Consulta` (opcional, `require "br_validators/consulta"`): consultas online
11
+ pela BrasilAPI de CEP (endereço), CNPJ (dados da Receita), banco (código de compensação)
12
+ e DDD (UF e cidades). Só usa a biblioteca padrão do Ruby; valida o dado antes de
13
+ consultar, devolve `nil` quando não acha e levanta `BrValidators::Consulta::Erro` quando
14
+ a BrasilAPI não responde.
15
+
16
+ - `BrValidators::CEP`: formato do CEP.
17
+ - `BrValidators::Telefone`: fixo e celular, com os 67 DDDs da Anatel; `type`, `format` e `ddd`.
18
+ - `BrValidators::Placa`: placa antiga e Mercosul; `type`, `mercosul` e `antiga` para converter.
19
+ - `BrValidators::RENAVAM`: 11 dígitos com dígito verificador.
20
+ - `BrValidators::TituloEleitor`: 12 dígitos, dígitos verificadores e `uf`.
21
+ - `BrValidators::ProcessoCNJ`: numeração única do CNJ (módulo 97) e `segmento`.
22
+ - `BrValidators::ChaveAcesso`: chave de NF-e, NFC-e, CT-e e MDF-e, inclusive com CNPJ
23
+ alfanumérico, e `modelo`.
24
+ - Validadores do ActiveModel para todos eles (`cep: true`, `telefone: true`, `placa: true`,
25
+ `renavam: true`, `titulo_eleitor: true`, `processo_cnj: true`, `chave_acesso: true`),
26
+ com mensagens em pt-BR e en.
27
+
6
28
  ## [0.1.1] - 2026-10-25
7
29
 
8
30
  ### Corrigido
data/README.md CHANGED
@@ -15,13 +15,25 @@ Ou direto: `gem install br_validators`. A gem está publicada no
15
15
 
16
16
  ## O que tem
17
17
 
18
- | Documento | Observação |
19
- | --- | --- |
20
- | CPF | |
21
- | CNPJ | numérico e alfanumérico (o formato novo da Receita Federal, a partir de julho de 2026) |
22
- | CNS | Cartão Nacional de Saúde |
23
- | Chave Pix | CPF, CNPJ, e-mail, celular e chave aleatória |
24
- | Boleto | linha digitável de cobrança e de arrecadação |
18
+ | Documento | Módulo | Observação |
19
+ | --- | --- | --- |
20
+ | CPF | `CPF` | |
21
+ | CNPJ | `CNPJ` | numérico e alfanumérico (o formato novo da Receita Federal, a partir de julho de 2026) |
22
+ | CNS | `CNS` | Cartão Nacional de Saúde |
23
+ | Título de eleitor | `TituloEleitor` | 12 dígitos, com a UF de emissão |
24
+ | Chave Pix | `Pix` | CPF, CNPJ, e-mail, celular e chave aleatória |
25
+ | Boleto | `Boleto` | linha digitável de cobrança e de arrecadação |
26
+ | CEP | `CEP` | só o formato (o CEP não tem dígito verificador) |
27
+ | Telefone | `Telefone` | fixo e celular, com os 67 DDDs da Anatel |
28
+ | Placa | `Placa` | antiga e Mercosul, com conversão entre as duas |
29
+ | RENAVAM | `RENAVAM` | 11 dígitos |
30
+ | Chave de acesso | `ChaveAcesso` | NF-e, NFC-e, CT-e e MDF-e, inclusive com CNPJ alfanumérico |
31
+ | Processo judicial | `ProcessoCNJ` | numeração única do CNJ |
32
+
33
+ A regra de cada um está no comentário do módulo, com o link da fonte oficial (Receita
34
+ Federal, Ministério da Saúde, Banco Central, FEBRABAN, Anatel, CONTRAN, CNJ...). O
35
+ título de eleitor é a exceção: o TSE não publica o cálculo, e a regra usada é a que a
36
+ OBMEP documenta.
25
37
 
26
38
  Cada documento tem os mesmos três métodos:
27
39
 
@@ -59,6 +71,48 @@ BrValidators::Boleto.type("00190.00009 02345.678904 12345.678176 1 1000000001234
59
71
  BrValidators::Boleto.valid?("826700000019000000000000000000000000000000000000") # => true
60
72
  ```
61
73
 
74
+ Os documentos novos seguem o mesmo jeito, com alguns métodos a mais:
75
+
76
+ ```ruby
77
+ BrValidators::Telefone.type("+55 11 98765-4321") # => :celular
78
+ BrValidators::Telefone.format("1134567890") # => "(11) 3456-7890"
79
+
80
+ BrValidators::Placa.type("ABC1C34") # => :mercosul
81
+ BrValidators::Placa.mercosul("ABC-1234") # => "ABC1C34"
82
+ BrValidators::Placa.antiga("ABC1C34") # => "ABC1234"
83
+
84
+ BrValidators::TituloEleitor.uf("1023 8501 0671") # => "PR"
85
+ BrValidators::ProcessoCNJ.segmento("0000456-41.2023.5.02.9999") # => "Justiça do Trabalho"
86
+ BrValidators::ChaveAcesso.modelo("3526 1011 2223 3300 0181 5500 1000 0001 2311 2345 6783") # => "55" (NF-e)
87
+
88
+ BrValidators::CEP.format("01001000") # => "01001-000"
89
+ BrValidators::RENAVAM.valid?("00123456789") # => true
90
+ ```
91
+
92
+ ## Consultas online
93
+
94
+ Validar não diz se o documento existe. Para isso há consultas pela
95
+ [BrasilAPI](https://brasilapi.com.br), um serviço gratuito, de código aberto e sem chave.
96
+ Elas são opcionais (a validação continua funcionando sem internet):
97
+
98
+ ```ruby
99
+ require "br_validators/consulta"
100
+
101
+ BrValidators::Consulta.cep("01001-000")
102
+ # => {"cep"=>"01001000", "state"=>"SP", "city"=>"São Paulo", "street"=>..., ...}
103
+ BrValidators::Consulta.cnpj("11.222.333/0001-81") # => dados da Receita Federal (razao_social, uf, ...)
104
+ BrValidators::Consulta.banco("001") # => {"code"=>1, "name"=>"BCO DO BRASIL S.A.", ...}
105
+ BrValidators::Consulta.ddd("11") # => {"state"=>"SP", "cities"=>[...]}
106
+ ```
107
+
108
+ - Dado inválido devolve `nil` sem chamar a internet; dado que a BrasilAPI não encontra
109
+ também devolve `nil`.
110
+ - Se a BrasilAPI não responder em 5 segundos ou der erro, levanta
111
+ `BrValidators::Consulta::Erro`.
112
+ - Para usar cache ou outro cliente HTTP, troque o `BrValidators::Consulta.cliente` por um
113
+ objeto com `get(uri)` que devolva `[status, corpo]`.
114
+ - A resposta é o JSON da BrasilAPI como `Hash`. Os campos são os dela e podem mudar.
115
+
62
116
  ## Rails e ActiveModel
63
117
 
64
118
  Em projetos com ActiveModel (todo app Rails tem), os validadores entram sozinhos:
@@ -70,19 +124,32 @@ class Cliente < ApplicationRecord
70
124
  validates :cartao_sus, cns: true
71
125
  validates :chave_pix, pix: { message: "não parece uma chave Pix" }
72
126
  validates :linha_digitavel, boleto: true
127
+ validates :cep, cep: true
128
+ validates :celular, telefone: true
129
+ validates :titulo, titulo_eleitor: true
130
+ end
131
+
132
+ class Veiculo < ApplicationRecord
133
+ validates :placa, placa: true
134
+ validates :renavam, renavam: true
135
+ end
136
+
137
+ class Nota < ApplicationRecord
138
+ validates :chave, chave_acesso: true
139
+ validates :processo, processo_cnj: true
73
140
  end
74
141
  ```
75
142
 
76
143
  As mensagens de erro vêm em português (`pt-BR`) e inglês (`en`). Para trocar o texto,
77
- sobrescreva as chaves `errors.messages.cpf_invalid`, `cnpj_invalid`, `cns_invalid`,
78
- `pix_invalid` e `boleto_invalid` no seu arquivo de tradução.
144
+ sobrescreva as chaves `errors.messages.<documento>_invalid` (`cpf_invalid`, `cep_invalid`,
145
+ `titulo_eleitor_invalid`...) no seu arquivo de tradução.
79
146
 
80
- Os validadores se chamam `CpfValidator`, `CnpjValidator`, `CnsValidator`, `PixValidator` e
81
- `BoletoValidator`, no nível de cima, porque é assim que o Rails encontra `cpf: true`.
147
+ Os validadores ficam no nível de cima (`CpfValidator`, `TelefoneValidator`,
148
+ `TituloEleitorValidator`...), porque é assim que o Rails encontra `cpf: true`.
82
149
 
83
150
  Já existem boas gems para CPF e CNPJ. A ideia aqui é juntar num lugar só os documentos
84
- que costumam aparecer em sistemas brasileiros (inclusive CNS, Pix e boleto), com
85
- mensagens de erro em português e inglês.
151
+ que costumam aparecer em sistemas brasileiros (inclusive CNS, Pix, boleto, placa, NF-e e
152
+ processo judicial), com mensagens de erro em português e inglês.
86
153
 
87
154
  ## Página de teste
88
155
 
@@ -31,3 +31,10 @@ require_relative "validators/cnpj_validator"
31
31
  require_relative "validators/cns_validator"
32
32
  require_relative "validators/pix_validator"
33
33
  require_relative "validators/boleto_validator"
34
+ require_relative "validators/cep_validator"
35
+ require_relative "validators/telefone_validator"
36
+ require_relative "validators/placa_validator"
37
+ require_relative "validators/renavam_validator"
38
+ require_relative "validators/titulo_eleitor_validator"
39
+ require_relative "validators/processo_cnj_validator"
40
+ require_relative "validators/chave_acesso_validator"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrValidators
4
+ # CEP (Código de Endereçamento Postal): 8 dígitos, escritos como 00000-000.
5
+ #
6
+ # Os 5 primeiros dígitos dizem a região, a sub-região, o setor, o subsetor e o
7
+ # divisor de subsetor; os 3 últimos (o sufixo) identificam a rua, a localidade
8
+ # ou a unidade dos Correios.
9
+ #
10
+ # O CEP não tem dígito verificador: aqui só dá para conferir o formato. Para
11
+ # saber se o CEP existe (e qual é o endereço), use a consulta online da gem
12
+ # (BrValidators::Consulta.cep).
13
+ #
14
+ # Fonte: "Tudo sobre CEP", dos Correios.
15
+ # https://www.correios.com.br/enviar/precisa-de-ajuda/imagens/tudo-sobre-cep
16
+ module CEP
17
+ # Pontos, hífens e espaços (inclusive os Unicode, como o não separável).
18
+ MASK = /[.\-[:space:]]/
19
+
20
+ # O scrub está explicado no CPF: texto corrompido vira inválido, sem erro.
21
+ def self.strip(value)
22
+ value.to_s.scrub.gsub(MASK, "")
23
+ end
24
+
25
+ # 8 dígitos. "00000-000" é o CEP que sistemas usam como "não informado";
26
+ # nenhuma região dos Correios começa com 00000.
27
+ def self.valid?(value)
28
+ cep = strip(value)
29
+ cep.match?(/\A\d{8}\z/) && cep != "00000000"
30
+ end
31
+
32
+ # Devolve "01001-000", ou nil se o CEP for inválido.
33
+ def self.format(value)
34
+ return nil unless valid?(value)
35
+
36
+ cep = strip(value)
37
+ "#{cep[0, 5]}-#{cep[5, 3]}"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrValidators
4
+ # Chave de acesso dos documentos fiscais eletrônicos (NF-e, NFC-e, CT-e, MDF-e):
5
+ # 44 caracteres.
6
+ #
7
+ # cUF (2) código IBGE da UF do emitente
8
+ # AAMM (4) ano e mês de emissão
9
+ # CNPJ (14) CNPJ do emitente (ou CPF com zeros à esquerda)
10
+ # mod (2) modelo do documento: 55 NF-e, 65 NFC-e, 57 CT-e, 58 MDF-e...
11
+ # serie (3), nNF (9), tpEmis (1), cNF (8) e o dígito verificador (1)
12
+ #
13
+ # Com o CNPJ alfanumérico, os 12 primeiros caracteres do CNPJ podem ter letras;
14
+ # todo o resto continua só com números.
15
+ #
16
+ # O dígito verificador usa módulo 11 com pesos de 2 a 9, da direita para a
17
+ # esquerda, sobre os 43 primeiros caracteres. Cada caractere vale o código ASCII
18
+ # dele menos 48 (como no CNPJ alfanumérico: "0" vale 0, "A" vale 17). Se o resto
19
+ # for 0 ou 1, o dígito é 0; senão, é 11 menos o resto.
20
+ #
21
+ # Fontes: Manual de Orientação do Contribuinte da NF-e e Nota Técnica Conjunta
22
+ # 2025.001 (CNPJ alfanumérico nos documentos fiscais eletrônicos), publicados no
23
+ # Portal da NF-e. https://www.nfe.fazenda.gov.br/portal/
24
+ module ChaveAcesso
25
+ # 6 números (cUF e AAMM), 12 letras ou números (raiz e ordem do CNPJ) e 26
26
+ # números (dígitos do CNPJ e o resto da chave).
27
+ PATTERN = /\A\d{6}[0-9A-Z]{12}\d{26}\z/
28
+
29
+ # O DANFE imprime a chave em grupos de 4, separados por espaço.
30
+ MASK = /[.\-[:space:]]/
31
+
32
+ # Tira a máscara e passa as letras do CNPJ para maiúsculas.
33
+ # upcase(:ascii) e scrub estão explicados no CNPJ e no CPF.
34
+ def self.strip(value)
35
+ value.to_s.scrub.gsub(MASK, "").upcase(:ascii)
36
+ end
37
+
38
+ def self.valid?(value)
39
+ chave = strip(value)
40
+ return false unless chave.match?(PATTERN)
41
+
42
+ chave[43] == check_digit(chave[0, 43])
43
+ end
44
+
45
+ # Devolve a chave em 11 grupos de 4 ("3526 1011 2223 ..."), ou nil se for inválida.
46
+ def self.format(value)
47
+ return nil unless valid?(value)
48
+
49
+ strip(value).scan(/.{4}/).join(" ")
50
+ end
51
+
52
+ # O código do modelo ("55" para NF-e, "65" para NFC-e...), ou nil se for inválida.
53
+ def self.modelo(value)
54
+ valid?(value) ? strip(value)[20, 2] : nil
55
+ end
56
+
57
+ def self.check_digit(chars)
58
+ sum = chars.reverse.chars.each_with_index.sum { |char, index| (char.ord - 48) * ((index % 8) + 2) }
59
+ rest = sum % 11
60
+ (rest < 2 ? 0 : 11 - rest).to_s
61
+ end
62
+
63
+ private_class_method :check_digit
64
+ end
65
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "openssl"
6
+ require_relative "../br_validators"
7
+
8
+ module BrValidators
9
+ # Consultas online pela BrasilAPI (https://brasilapi.com.br), um serviço gratuito,
10
+ # de código aberto e sem chave de acesso. É opcional: não vem com o
11
+ # require "br_validators" (a validação continua sem rede); carregue com
12
+ #
13
+ # require "br_validators/consulta"
14
+ #
15
+ # BrValidators::Consulta.cep("01001-000")
16
+ # # => {"cep"=>"01001000", "state"=>"SP", "city"=>"São Paulo", "street"=>..., ...}
17
+ # BrValidators::Consulta.cnpj("11.222.333/0001-81") # dados da Receita Federal
18
+ # BrValidators::Consulta.banco("001") # => {"code"=>1, "name"=>"BCO DO BRASIL S.A.", ...}
19
+ # BrValidators::Consulta.ddd("11") # => {"state"=>"SP", "cities"=>[...]}
20
+ #
21
+ # Cada método:
22
+ # - valida o dado antes (documento inválido devolve nil sem chamar a internet);
23
+ # - devolve o JSON da BrasilAPI como Hash (chaves em texto), ou nil se não achar;
24
+ # - levanta BrValidators::Consulta::Erro se a BrasilAPI não responder ou der erro.
25
+ module Consulta
26
+ BASE = "https://brasilapi.com.br/api"
27
+
28
+ # Segundos de espera para conectar e para receber a resposta.
29
+ TIMEOUT = 5
30
+
31
+ # A BrasilAPI não respondeu, demorou demais ou respondeu com erro.
32
+ class Erro < StandardError; end
33
+
34
+ class << self
35
+ # Quem faz a requisição: qualquer objeto com get(uri) que devolva
36
+ # [status, corpo]. O padrão é o HTTP abaixo; os testes trocam por um falso,
37
+ # e quem usa a gem pode trocar por um com cache, por exemplo.
38
+ attr_writer :cliente
39
+
40
+ def cliente
41
+ @cliente ||= HTTP
42
+ end
43
+ end
44
+
45
+ # Endereço do CEP: cep, state, city, neighborhood, street e service.
46
+ def self.cep(valor)
47
+ return nil unless CEP.valid?(valor)
48
+
49
+ buscar("/cep/v1/#{CEP.strip(valor)}")
50
+ end
51
+
52
+ # Dados do CNPJ na Receita Federal (razao_social, nome_fantasia,
53
+ # descricao_situacao_cadastral, uf, municipio...). O CNPJ alfanumérico só é
54
+ # encontrado quando a BrasilAPI passar a aceitá-lo.
55
+ def self.cnpj(valor)
56
+ return nil unless CNPJ.valid?(valor)
57
+
58
+ buscar("/cnpj/v1/#{CNPJ.strip(valor)}")
59
+ end
60
+
61
+ # Banco pelo código de compensação ("001", "237", 341): code, name, fullName e ispb.
62
+ def self.banco(codigo)
63
+ codigo = codigo.to_s.strip
64
+ return nil unless codigo.match?(/\A\d{1,3}\z/)
65
+
66
+ buscar("/banks/v1/#{codigo.to_i}")
67
+ end
68
+
69
+ # UF e cidades de um DDD: state e cities.
70
+ def self.ddd(valor)
71
+ ddd = valor.to_s.strip
72
+ return nil unless Telefone::DDDS.include?(ddd)
73
+
74
+ buscar("/ddd/v1/#{ddd}")
75
+ end
76
+
77
+ # 200 vira Hash; 400 e 404 (não achou) viram nil; o resto é erro.
78
+ def self.buscar(caminho)
79
+ status, corpo = cliente.get(URI("#{BASE}#{caminho}"))
80
+ case status
81
+ when 200 then JSON.parse(corpo)
82
+ when 400, 404 then nil
83
+ else raise Erro, "A BrasilAPI respondeu #{status} em #{caminho}."
84
+ end
85
+ rescue JSON::ParserError
86
+ raise Erro, "A BrasilAPI respondeu algo que não é JSON em #{caminho}."
87
+ end
88
+
89
+ private_class_method :buscar
90
+
91
+ # Cliente padrão, só com a biblioteca padrão do Ruby (Net::HTTP).
92
+ module HTTP
93
+ HEADERS = { "Accept" => "application/json", "User-Agent" => "br_validators/#{VERSION}" }.freeze
94
+
95
+ # Problemas de rede: não conectou, a conexão caiu, demorou demais ou o
96
+ # certificado falhou. Viram Consulta::Erro, para quem chama tratar um erro só.
97
+ ERROS_DE_REDE = [Timeout::Error, SocketError, SystemCallError, IOError, OpenSSL::SSL::SSLError].freeze
98
+
99
+ def self.get(uri)
100
+ http = Net::HTTP.new(uri.host, uri.port)
101
+ http.use_ssl = uri.scheme == "https"
102
+ http.open_timeout = TIMEOUT
103
+ http.read_timeout = TIMEOUT
104
+ resposta = http.get(uri.request_uri, HEADERS)
105
+ [resposta.code.to_i, resposta.body]
106
+ rescue *ERROS_DE_REDE => e
107
+ raise Erro, "Não deu para falar com a BrasilAPI (#{e.class})."
108
+ end
109
+ end
110
+ end
111
+ end
@@ -6,3 +6,10 @@ en:
6
6
  cns_invalid: "is not a valid CNS"
7
7
  pix_invalid: "is not a valid Pix key"
8
8
  boleto_invalid: "is not a valid boleto line"
9
+ cep_invalid: "is not a valid CEP"
10
+ telefone_invalid: "is not a valid phone number"
11
+ placa_invalid: "is not a valid license plate"
12
+ renavam_invalid: "is not a valid RENAVAM"
13
+ titulo_eleitor_invalid: "is not a valid voter ID"
14
+ processo_cnj_invalid: "is not a valid case number"
15
+ chave_acesso_invalid: "is not a valid access key"
@@ -6,3 +6,10 @@ pt-BR:
6
6
  cns_invalid: "não é um CNS válido"
7
7
  pix_invalid: "não é uma chave Pix válida"
8
8
  boleto_invalid: "não é uma linha digitável de boleto válida"
9
+ cep_invalid: "não é um CEP válido"
10
+ telefone_invalid: "não é um telefone válido"
11
+ placa_invalid: "não é uma placa válida"
12
+ renavam_invalid: "não é um RENAVAM válido"
13
+ titulo_eleitor_invalid: "não é um título de eleitor válido"
14
+ processo_cnj_invalid: "não é um número de processo válido"
15
+ chave_acesso_invalid: "não é uma chave de acesso válida"
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrValidators
4
+ # Placa de veículo, nos dois modelos que circulam:
5
+ #
6
+ # - Antiga (cinza): 3 letras e 4 números, escrita "ABC-1234".
7
+ # - Mercosul: LLLNLNN (letra, letra, letra, número, letra, número, número),
8
+ # escrita "ABC1D23".
9
+ #
10
+ # Para converter a placa antiga, troca-se só o quinto caractere (o segundo
11
+ # número) por uma letra: 0 vira A, 1 vira B... 9 vira J. Ex.: ABC-1234 → ABC1C34.
12
+ # As letras de A a J nessa posição existem justamente para essa conversão, e os
13
+ # dois formatos valem para buscar o mesmo veículo.
14
+ #
15
+ # Fonte: Resolução CONTRAN nº 780/2019, Anexo I (item 1.2) e Anexo II.
16
+ module Placa
17
+ ANTIGA = /\A[A-Z]{3}\d{4}\z/
18
+ MERCOSUL = /\A[A-Z]{3}\d[A-Z]\d{2}\z/
19
+
20
+ # Pontos, hífens e espaços.
21
+ MASK = /[.\-[:space:]]/
22
+
23
+ # Tira a máscara e passa as letras para maiúsculas: "abc-1234" vira "ABC1234".
24
+ # upcase(:ascii) e scrub estão explicados no CNPJ e no CPF.
25
+ def self.strip(value)
26
+ value.to_s.scrub.gsub(MASK, "").upcase(:ascii)
27
+ end
28
+
29
+ # :antiga, :mercosul ou nil se a placa for inválida.
30
+ def self.type(value)
31
+ placa = strip(value)
32
+ return :antiga if placa.match?(ANTIGA)
33
+ return :mercosul if placa.match?(MERCOSUL)
34
+
35
+ nil
36
+ end
37
+
38
+ def self.valid?(value)
39
+ !type(value).nil?
40
+ end
41
+
42
+ # Devolve "ABC-1234" (antiga) ou "ABC1D23" (Mercosul), ou nil se for inválida.
43
+ def self.format(value)
44
+ placa = strip(value)
45
+ case type(placa)
46
+ when :antiga then "#{placa[0, 3]}-#{placa[3, 4]}"
47
+ when :mercosul then placa
48
+ end
49
+ end
50
+
51
+ # A placa no modelo Mercosul: converte a antiga e devolve a Mercosul como está.
52
+ # mercosul("ABC-1234") # => "ABC1C34"
53
+ def self.mercosul(value)
54
+ placa = strip(value)
55
+ case type(placa)
56
+ when :antiga then placa[0, 4] + ("A".ord + placa[4].to_i).chr + placa[5, 2]
57
+ when :mercosul then placa
58
+ end
59
+ end
60
+
61
+ # A placa no modelo antigo: desfaz a conversão (só para letras de A a J na
62
+ # quinta posição) e devolve a antiga como está. Placa Mercosul emitida já no
63
+ # modelo novo, com outra letra, não tem placa antiga: devolve nil.
64
+ # antiga("ABC1C34") # => "ABC1234"
65
+ def self.antiga(value)
66
+ placa = strip(value)
67
+ return placa if type(placa) == :antiga
68
+ return nil unless type(placa) == :mercosul && placa[4].between?("A", "J")
69
+
70
+ placa[0, 4] + (placa[4].ord - "A".ord).to_s + placa[5, 2]
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrValidators
4
+ # Número de processo judicial na numeração única do CNJ:
5
+ #
6
+ # NNNNNNN-DD.AAAA.J.TR.OOOO (20 dígitos)
7
+ # NNNNNNN número sequencial do processo no ano
8
+ # DD dígito verificador
9
+ # AAAA ano em que o processo começou
10
+ # J segmento do Judiciário (1 a 9, ver SEGMENTOS)
11
+ # TR tribunal dentro do segmento
12
+ # OOOO unidade de origem
13
+ #
14
+ # O dígito verificador usa módulo 97 (norma ISO 7064). Para conferir, junta-se
15
+ # NNNNNNN AAAA J TR OOOO e coloca-se o DD no fim; o número de 20 dígitos que sai
16
+ # dividido por 97 precisa deixar resto 1.
17
+ #
18
+ # Fonte: Resolução CNJ nº 65/2008, art. 1º e Anexo VIII.
19
+ # https://atos.cnj.jus.br/files/resolucao_comp_65_16122008_26032019140041.pdf
20
+ module ProcessoCNJ
21
+ SEGMENTOS = {
22
+ "1" => "Supremo Tribunal Federal",
23
+ "2" => "Conselho Nacional de Justiça",
24
+ "3" => "Superior Tribunal de Justiça",
25
+ "4" => "Justiça Federal",
26
+ "5" => "Justiça do Trabalho",
27
+ "6" => "Justiça Eleitoral",
28
+ "7" => "Justiça Militar da União",
29
+ "8" => "Justiça dos Estados e do Distrito Federal",
30
+ "9" => "Justiça Militar Estadual"
31
+ }.freeze
32
+
33
+ # Pontos, hífens e espaços.
34
+ MASK = /[.\-[:space:]]/
35
+
36
+ # O scrub está explicado no CPF: texto corrompido vira inválido, sem erro.
37
+ def self.strip(value)
38
+ value.to_s.scrub.gsub(MASK, "")
39
+ end
40
+
41
+ def self.valid?(value)
42
+ numero = strip(value)
43
+ return false unless numero.match?(/\A\d{20}\z/)
44
+ return false unless SEGMENTOS.key?(numero[13])
45
+
46
+ # NNNNNNN + AAAA J TR OOOO + DD
47
+ (numero[0, 7] + numero[9, 11] + numero[7, 2]).to_i % 97 == 1
48
+ end
49
+
50
+ # Devolve "0000123-45.2024.8.26.0100", ou nil se o número for inválido.
51
+ def self.format(value)
52
+ return nil unless valid?(value)
53
+
54
+ numero = strip(value)
55
+ "#{numero[0, 7]}-#{numero[7, 2]}.#{numero[9, 4]}.#{numero[13]}.#{numero[14, 2]}.#{numero[16, 4]}"
56
+ end
57
+
58
+ # O nome do segmento do Judiciário ("Justiça do Trabalho"), ou nil se o número
59
+ # for inválido.
60
+ def self.segmento(value)
61
+ valid?(value) ? SEGMENTOS[strip(value)[13]] : nil
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrValidators
4
+ # RENAVAM (Registro Nacional de Veículos Automotores): 11 dígitos, sendo 10 de
5
+ # número e 1 dígito verificador.
6
+ #
7
+ # O dígito verificador usa módulo 11 com pesos de 2 a 9 ("peso 9"), da direita
8
+ # para a esquerda, voltando ao 2 depois do 9. Se o resto da divisão por 11 for
9
+ # 0 ou 1, o dígito é 0; senão, é 11 menos o resto.
10
+ #
11
+ # Os códigos antigos tinham 9 dígitos. Para validar um deles, complete com
12
+ # zeros à esquerda até ficar com 11 ("123456789" vira "00123456789"): os zeros
13
+ # não mudam a conta.
14
+ #
15
+ # Fonte: Portaria DENATRAN nº 27/2013 (11 dígitos, "dígito verificador,
16
+ # calculado através do módulo 11, peso 9"), válida desde abril de 2013.
17
+ module RENAVAM
18
+ # O RENAVAM não tem máscara oficial; aceitamos pontos, hífens e espaços.
19
+ MASK = /[.\-[:space:]]/
20
+
21
+ # O scrub está explicado no CPF: texto corrompido vira inválido, sem erro.
22
+ def self.strip(value)
23
+ value.to_s.scrub.gsub(MASK, "")
24
+ end
25
+
26
+ def self.valid?(value)
27
+ renavam = strip(value)
28
+ return false unless renavam.match?(/\A\d{11}\z/)
29
+
30
+ # 00000000000, 11111111111... não são códigos reais.
31
+ return false if renavam.squeeze.size == 1
32
+
33
+ renavam[10] == check_digit(renavam[0, 10])
34
+ end
35
+
36
+ # O RENAVAM não tem máscara: devolve os 11 dígitos, ou nil se for inválido.
37
+ def self.format(value)
38
+ valid?(value) ? strip(value) : nil
39
+ end
40
+
41
+ def self.check_digit(digits)
42
+ sum = digits.reverse.chars.each_with_index.sum { |digit, index| digit.to_i * ((index % 8) + 2) }
43
+ rest = sum % 11
44
+ (rest < 2 ? 0 : 11 - rest).to_s
45
+ end
46
+
47
+ private_class_method :check_digit
48
+ end
49
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrValidators
4
+ # Telefone brasileiro, fixo ou celular, com DDD.
5
+ #
6
+ # - DDD: um dos 67 códigos nacionais da Anatel.
7
+ # - Celular: 9 dígitos começando com 9 (o "nono dígito", Resolução Anatel
8
+ # 553/2010).
9
+ # - Fixo: 8 dígitos começando com 2, 3, 4 ou 5 (faixa da telefonia fixa no
10
+ # Regulamento de Numeração, Resolução Anatel 86/1998, art. 19, com a redação da
11
+ # Resolução 553/2010; a 86/1998 foi revogada pela 749/2022 e os números em uso
12
+ # continuam nesse formato).
13
+ #
14
+ # Aceita o código do país (+55) e o zero antes do DDD ("011..."). Não aceita o
15
+ # código da operadora (0 XX DDD ...) nem números especiais, como 0800 e 190.
16
+ #
17
+ # Fontes:
18
+ # - Lista dos 67 códigos nacionais (DDD): perguntas frequentes da Anatel sobre
19
+ # áreas locais, 2026.
20
+ # https://sistemas.anatel.gov.br/anexar-api/publico/anexos/download/a8c6650c72539ff2334c327ab5c169e4
21
+ # - Resolução 86/1998 (Regulamento de Numeração do STFC) e Resolução 553/2010.
22
+ module Telefone
23
+ # Os 67 DDDs em uso. Os que faltam na sequência (20, 23, 25, 26, 29, 30,
24
+ # 36, 39, 40, 50, 52, 56 a 60, 70, 72, 76, 78, 80 e 90) não existem.
25
+ DDDS = %w[
26
+ 11 12 13 14 15 16 17 18 19
27
+ 21 22 24 27 28
28
+ 31 32 33 34 35 37 38
29
+ 41 42 43 44 45 46 47 48 49
30
+ 51 53 54 55
31
+ 61 62 63 64 65 66 67 68 69
32
+ 71 73 74 75 77 79
33
+ 81 82 83 84 85 86 87 88 89
34
+ 91 92 93 94 95 96 97 98 99
35
+ ].freeze
36
+
37
+ CELULAR = /\A9\d{8}\z/
38
+ FIXO = /\A[2-5]\d{7}\z/
39
+
40
+ # Parênteses, "+", pontos, hífens e espaços.
41
+ MASK = /[()+.\-[:space:]]/
42
+
43
+ # Tira a máscara: "+55 (11) 98765-4321" vira "5511987654321".
44
+ # O scrub está explicado no CPF: texto corrompido vira inválido, sem erro.
45
+ def self.strip(value)
46
+ value.to_s.scrub.gsub(MASK, "")
47
+ end
48
+
49
+ # :celular, :fixo ou nil se o número for inválido.
50
+ def self.type(value)
51
+ numero = nacional(value)
52
+ return nil unless numero && DDDS.include?(numero[0, 2])
53
+
54
+ assinante = numero[2..]
55
+ return :celular if assinante.match?(CELULAR)
56
+ return :fixo if assinante.match?(FIXO)
57
+
58
+ nil
59
+ end
60
+
61
+ def self.valid?(value)
62
+ !type(value).nil?
63
+ end
64
+
65
+ # Devolve "(11) 98765-4321" ou "(11) 3456-7890", ou nil se for inválido.
66
+ def self.format(value)
67
+ return nil unless valid?(value)
68
+
69
+ numero = nacional(value)
70
+ "(#{numero[0, 2]}) #{numero[2...-4]}-#{numero[-4..]}"
71
+ end
72
+
73
+ # O DDD ("11"), ou nil se o número for inválido.
74
+ def self.ddd(value)
75
+ valid?(value) ? nacional(value)[0, 2] : nil
76
+ end
77
+
78
+ # O número só com DDD e assinante (10 ou 11 dígitos), sem o código do país
79
+ # (55) nem o zero antes do DDD. Devolve nil se não sobrar um número desse tamanho.
80
+ def self.nacional(value)
81
+ digits = strip(value)
82
+ return nil unless digits.match?(/\A\d+\z/)
83
+
84
+ # Com o código do país: 55 + 10 ou 11 dígitos.
85
+ digits = digits.delete_prefix("55") if digits.start_with?("55") && digits.size.between?(12, 13)
86
+ # O DDD nunca começa com 0; um zero na frente é o prefixo de chamada nacional.
87
+ digits = digits.delete_prefix("0") if digits.start_with?("0")
88
+
89
+ digits.size.between?(10, 11) ? digits : nil
90
+ end
91
+
92
+ private_class_method :nacional
93
+ end
94
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrValidators
4
+ # Título de eleitor: 12 dígitos.
5
+ # - 8 primeiros: número sequencial;
6
+ # - 9º e 10º: código da UF onde foi emitido (01 a 28; 28 é o exterior, "ZZ");
7
+ # - 11º e 12º: dígitos verificadores.
8
+ #
9
+ # 1º dígito: soma dos 8 primeiros com pesos de 2 a 9, da esquerda para a
10
+ # direita; o dígito é o resto da divisão por 11.
11
+ # 2º dígito: soma dos 2 dígitos da UF e do 1º dígito, com pesos 7, 8 e 9;
12
+ # também é o resto da divisão por 11.
13
+ # Nos dois casos, resto 10 vira 0. Em títulos de São Paulo (01) e Minas Gerais
14
+ # (02), resto 0 vira 1.
15
+ #
16
+ # Alguns títulos antigos de SP e MG têm 13 dígitos (9 no número sequencial).
17
+ # Eles não são aceitos aqui: a regra pública só cobre os de 12.
18
+ #
19
+ # Fonte: o TSE não publica o cálculo. A regra é a usada pelos sistemas e
20
+ # explicada pela OBMEP em "A Matemática nos Documentos: Título de Eleitor".
21
+ # https://clubes.obmep.org.br/blog/a-matematica-nos-documentos-titulo-de-eleitor/
22
+ module TituloEleitor
23
+ UFS = {
24
+ "01" => "SP", "02" => "MG", "03" => "RJ", "04" => "RS", "05" => "BA", "06" => "PR",
25
+ "07" => "CE", "08" => "PE", "09" => "SC", "10" => "GO", "11" => "MA", "12" => "PB",
26
+ "13" => "PA", "14" => "ES", "15" => "PI", "16" => "RN", "17" => "AL", "18" => "MT",
27
+ "19" => "MS", "20" => "DF", "21" => "SE", "22" => "AM", "23" => "RO", "24" => "AC",
28
+ "25" => "AP", "26" => "RR", "27" => "TO", "28" => "ZZ"
29
+ }.freeze
30
+
31
+ # O título impresso mostra o número em grupos separados por espaço.
32
+ MASK = %r{[./\-[:space:]]}
33
+
34
+ # O scrub está explicado no CPF: texto corrompido vira inválido, sem erro.
35
+ def self.strip(value)
36
+ value.to_s.scrub.gsub(MASK, "")
37
+ end
38
+
39
+ def self.valid?(value)
40
+ titulo = strip(value)
41
+ return false unless titulo.match?(/\A\d{12}\z/)
42
+
43
+ uf = titulo[8, 2]
44
+ return false unless UFS.key?(uf)
45
+
46
+ first = check_digit(weighted_sum(titulo[0, 8], 2..9), uf)
47
+ second = check_digit(weighted_sum("#{uf}#{first}", 7..9), uf)
48
+ titulo[10, 2] == "#{first}#{second}"
49
+ end
50
+
51
+ # Devolve "1023 8501 0671", ou nil se o título for inválido.
52
+ def self.format(value)
53
+ return nil unless valid?(value)
54
+
55
+ titulo = strip(value)
56
+ "#{titulo[0, 4]} #{titulo[4, 4]} #{titulo[8, 4]}"
57
+ end
58
+
59
+ # A sigla da UF onde o título foi emitido ("SP", "ZZ" para o exterior), ou nil.
60
+ def self.uf(value)
61
+ valid?(value) ? UFS[strip(value)[8, 2]] : nil
62
+ end
63
+
64
+ def self.weighted_sum(digits, weights)
65
+ digits.chars.zip(weights.to_a).sum { |digit, weight| digit.to_i * weight }
66
+ end
67
+
68
+ # codigo_uf: "01" (SP) e "02" (MG) têm a regra especial do resto 0.
69
+ def self.check_digit(sum, codigo_uf)
70
+ rest = sum % 11
71
+ return 1 if rest.zero? && %w[01 02].include?(codigo_uf)
72
+
73
+ rest == 10 ? 0 : rest
74
+ end
75
+
76
+ private_class_method :weighted_sum, :check_digit
77
+ end
78
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # validates :cep, cep: true
4
+ class CepValidator < BrValidators::DocumentValidator
5
+ DOCUMENT = BrValidators::CEP
6
+ ERROR = :cep_invalid
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # validates :chave_nfe, chave_acesso: true
4
+ class ChaveAcessoValidator < BrValidators::DocumentValidator
5
+ DOCUMENT = BrValidators::ChaveAcesso
6
+ ERROR = :chave_acesso_invalid
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # validates :placa, placa: true
4
+ class PlacaValidator < BrValidators::DocumentValidator
5
+ DOCUMENT = BrValidators::Placa
6
+ ERROR = :placa_invalid
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # validates :processo, processo_cnj: true
4
+ class ProcessoCnjValidator < BrValidators::DocumentValidator
5
+ DOCUMENT = BrValidators::ProcessoCNJ
6
+ ERROR = :processo_cnj_invalid
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # validates :renavam, renavam: true
4
+ class RenavamValidator < BrValidators::DocumentValidator
5
+ DOCUMENT = BrValidators::RENAVAM
6
+ ERROR = :renavam_invalid
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # validates :celular, telefone: true
4
+ class TelefoneValidator < BrValidators::DocumentValidator
5
+ DOCUMENT = BrValidators::Telefone
6
+ ERROR = :telefone_invalid
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # validates :titulo, titulo_eleitor: true
4
+ class TituloEleitorValidator < BrValidators::DocumentValidator
5
+ DOCUMENT = BrValidators::TituloEleitor
6
+ ERROR = :titulo_eleitor_invalid
7
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Versão da gem, lida também pelo gemspec.
4
4
  module BrValidators
5
- VERSION = "0.1.1"
5
+ VERSION = "0.2.0"
6
6
  end
data/lib/br_validators.rb CHANGED
@@ -6,6 +6,13 @@ require_relative "br_validators/cnpj"
6
6
  require_relative "br_validators/cns"
7
7
  require_relative "br_validators/pix"
8
8
  require_relative "br_validators/boleto"
9
+ require_relative "br_validators/cep"
10
+ require_relative "br_validators/telefone"
11
+ require_relative "br_validators/placa"
12
+ require_relative "br_validators/renavam"
13
+ require_relative "br_validators/titulo_eleitor"
14
+ require_relative "br_validators/processo_cnj"
15
+ require_relative "br_validators/chave_acesso"
9
16
 
10
17
  # Validação e formatação de documentos brasileiros.
11
18
  #
@@ -15,7 +22,9 @@ require_relative "br_validators/boleto"
15
22
  # format(valor) -> o documento com a máscara, ou nil se for inválido
16
23
  # strip(valor) -> o documento sem a máscara
17
24
  # A chave Pix é a exceção: tem valid? e type, mas não format nem strip, porque
18
- # precisa estar exatamente como o Banco Central guarda. Pix e Boleto têm type.
25
+ # precisa estar exatamente como o Banco Central guarda.
26
+ # Alguns têm métodos a mais: type (Pix, Boleto, Telefone, Placa), uf (TituloEleitor),
27
+ # segmento (ProcessoCNJ), modelo (ChaveAcesso), ddd (Telefone), mercosul e antiga (Placa).
19
28
  module BrValidators
20
29
  end
21
30
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: br_validators
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pedro Pedrassolli
@@ -21,17 +21,32 @@ files:
21
21
  - lib/br_validators.rb
22
22
  - lib/br_validators/active_model.rb
23
23
  - lib/br_validators/boleto.rb
24
+ - lib/br_validators/cep.rb
25
+ - lib/br_validators/chave_acesso.rb
24
26
  - lib/br_validators/cnpj.rb
25
27
  - lib/br_validators/cns.rb
28
+ - lib/br_validators/consulta.rb
26
29
  - lib/br_validators/cpf.rb
27
30
  - lib/br_validators/locale/en.yml
28
31
  - lib/br_validators/locale/pt-BR.yml
29
32
  - lib/br_validators/pix.rb
33
+ - lib/br_validators/placa.rb
34
+ - lib/br_validators/processo_cnj.rb
35
+ - lib/br_validators/renavam.rb
36
+ - lib/br_validators/telefone.rb
37
+ - lib/br_validators/titulo_eleitor.rb
30
38
  - lib/br_validators/validators/boleto_validator.rb
39
+ - lib/br_validators/validators/cep_validator.rb
40
+ - lib/br_validators/validators/chave_acesso_validator.rb
31
41
  - lib/br_validators/validators/cnpj_validator.rb
32
42
  - lib/br_validators/validators/cns_validator.rb
33
43
  - lib/br_validators/validators/cpf_validator.rb
34
44
  - lib/br_validators/validators/pix_validator.rb
45
+ - lib/br_validators/validators/placa_validator.rb
46
+ - lib/br_validators/validators/processo_cnj_validator.rb
47
+ - lib/br_validators/validators/renavam_validator.rb
48
+ - lib/br_validators/validators/telefone_validator.rb
49
+ - lib/br_validators/validators/titulo_eleitor_validator.rb
35
50
  - lib/br_validators/version.rb
36
51
  homepage: https://github.com/PedroPedrassolli/br_validators
37
52
  licenses:
@@ -57,5 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
57
72
  requirements: []
58
73
  rubygems_version: 3.6.9
59
74
  specification_version: 4
60
- summary: Valida e formata CPF, CNPJ (inclusive alfanumérico), CNS, chave Pix e boleto.
75
+ summary: 'Valida e formata documentos brasileiros: CPF, CNPJ alfanumérico, CNS, Pix,
76
+ boleto, CEP, telefone, placa, RENAVAM, título de eleitor, chave de NF-e e processo
77
+ do CNJ.'
61
78
  test_files: []