diff --git a/packages/br-utilities/Gemfile b/packages/br-utilities/Gemfile index 62f8842..2d266a0 100644 --- a/packages/br-utilities/Gemfile +++ b/packages/br-utilities/Gemfile @@ -5,10 +5,7 @@ source 'https://rubygems.org' gemspec gem 'cnpj-utilities', path: '../cnpj-utilities' -gem 'cpf-fmt', path: '../cpf-fmt' -gem 'cpf-gen', path: '../cpf-gen' gem 'cpf-utilities', path: '../cpf-utilities' -gem 'cpf-val', path: '../cpf-val' group :test do gem 'rake', '~> 13.2' diff --git a/packages/cnpj-utilities/CHANGELOG.md b/packages/cnpj-utilities/CHANGELOG.md index 291d74d..c98622a 100644 --- a/packages/cnpj-utilities/CHANGELOG.md +++ b/packages/cnpj-utilities/CHANGELOG.md @@ -8,7 +8,7 @@ Unified toolkit to deal with CNPJ (Brazilian legal entity ID): formatting, gener - **Unified façade**: `CnpjUtils` delegates `#format`, `#generate`, and `#is_valid` to `cnpj-fmt`, `cnpj-gen`, and `cnpj-val`. - **Alphanumeric CNPJ**: full support for the [14-character alphanumeric CNPJ](https://www.gov.br/receitafederal/pt-br/assuntos/noticias/2023/julho/cnpj-alfa-numerico); generate with `type` `"numeric"`, `"alphabetic"`, or `"alphanumeric"`. -- **Quick helpers**: `CnpjUtils.format` / `.generate` / `.is_valid` alias mutable `CnpjUtils::DEFAULT`. +- **Quick helpers**: `CnpjUtils.format` / `.generate` / `.is_valid` alias mutable `CnpjUtils::DEFAULT` (process-wide; prefer `CnpjUtils.new` / per-call options under concurrency). - **Two-tier re-exports**: `CnpjUtils::CnpjFormatter` / `CnpjGenerator` / `CnpjValidator` at the façade root; full sibling surface under `CnpjUtils::CnpjFmt` / `CnpjGen` / `CnpjVal`. - **Configurable components**: constructor and setters accept component instances, `*Options`, `Hash`, or `nil`; accessors expose `formatter`, `generator`, and `validator`. - **Per-call overrides**: `#format`, `#generate`, and `#is_valid` accept an options `Hash`/instance or keyword overrides (not both). diff --git a/packages/cnpj-utilities/README.md b/packages/cnpj-utilities/README.md index 694f811..c531bbb 100644 --- a/packages/cnpj-utilities/README.md +++ b/packages/cnpj-utilities/README.md @@ -87,7 +87,7 @@ CnpjUtils.is_valid('98765432000199') # => false You can work in these equivalent ways: 1. **`CnpjUtils.format` / `.generate` / `.is_valid`** — class helpers for quick one-off calls (forward to `DEFAULT`). -2. **`CnpjUtils::DEFAULT`** — mutable shared singleton (same object the class helpers use). +2. **`CnpjUtils::DEFAULT`** — mutable shared singleton (same object the class helpers use; process-wide / not thread-isolated). 3. **`CnpjUtils.new`** — configurable instance with shared defaults across format, generate, and validate. 4. **Main classes under `CnpjUtils`** — `CnpjUtils::CnpjFormatter`, `CnpjUtils::CnpjGenerator`, `CnpjUtils::CnpjValidator`. 5. **Nested package modules** — Options, helpers, errors, and types via `CnpjUtils::CnpjFmt` / `CnpjGen` / `CnpjVal` (e.g. `CnpjUtils::CnpjFmt::CnpjFormatterOptions`, `CnpjUtils::CnpjFmt.cnpj_fmt`). @@ -145,7 +145,7 @@ CnpjUtils.is_valid('98765432000198') ### `CnpjUtils::DEFAULT` (default instance) -`CnpjUtils::DEFAULT` is the pre-built, **mutable** singleton behind the class helpers (parity with the JS default export / Python `cnpj_utils`). Mutating it affects subsequent `CnpjUtils.format` / `.generate` / `.is_valid` calls; custom `CnpjUtils.new` instances stay independent: +`CnpjUtils::DEFAULT` is the pre-built, **mutable** singleton behind the class helpers (parity with the JS default export / Python `cnpj_utils`). Its configuration is **process-wide and shared across threads**: mutating it (e.g. `DEFAULT.formatter = …`) affects subsequent `CnpjUtils.format` / `.generate` / `.is_valid` calls for every caller in the process. Prefer `CnpjUtils.new` or per-call options for concurrent or isolated work; custom instances stay independent of `DEFAULT`: ```ruby CnpjUtils::DEFAULT.formatter = { slash_key: '|' } @@ -265,7 +265,7 @@ After `require 'cnpj-utilities'`: - **`CnpjUtils`**: Façade class to create a utils instance with optional default formatter, generator, and validator settings. - **`CnpjUtils.format` / `.generate` / `.is_valid`**: Class helpers that forward to `CnpjUtils::DEFAULT`. -- **`CnpjUtils::DEFAULT`**: Mutable pre-built `CnpjUtils` instance (same object the class helpers use). +- **`CnpjUtils::DEFAULT`**: Mutable pre-built `CnpjUtils` instance (same object the class helpers use). Process-wide / shared across threads — prefer `CnpjUtils.new` or per-call options under concurrency. - **`CnpjUtils::VERSION`**: Gem version string. - **Main-class shortcuts**: `CnpjUtils::CnpjFormatter`, `CnpjUtils::CnpjGenerator`, `CnpjUtils::CnpjValidator` (same objects as the sibling classes). - **Nested package modules**: `CnpjUtils::CnpjFmt`, `CnpjUtils::CnpjGen`, `CnpjUtils::CnpjVal` — full sibling surface (Options, helpers, errors, types). Options/helpers/errors are **not** aliased at the `CnpjUtils` root. @@ -285,8 +285,8 @@ Errors defined by this gem are **API misuse** only (wrong type or invalid argume | Class | Inherits from | Category | Trigger condition | |-------|---------------|----------|-------------------| -| `CnpjUtils::TypeMismatchError` | `CnpjUtils::TypeMismatchError < TypeError < StandardError` (+ `include CnpjUtils::Error`) | API misuse | Non-`nil` `settings` argument to `CnpjUtils.new` is not a `Hash` | | `CnpjUtils::InvalidArgumentCombinationError` | `CnpjUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include CnpjUtils::Error`) | API misuse | Non-`nil` settings/options `Hash` (or options instance) passed together with any non-`nil` keyword argument | +| `CnpjUtils::TypeMismatchError` | `CnpjUtils::TypeMismatchError < TypeError < StandardError` (+ `include CnpjUtils::Error`) | API misuse | Non-`nil` `settings` argument to `CnpjUtils.new` is not a `Hash` | ##### `CnpjUtils::Error` (marker module) diff --git a/packages/cnpj-utilities/README.pt.md b/packages/cnpj-utilities/README.pt.md index 6f62594..e6dbf29 100644 --- a/packages/cnpj-utilities/README.pt.md +++ b/packages/cnpj-utilities/README.pt.md @@ -72,7 +72,7 @@ CnpjUtils.is_valid('98765432000199') # => false Você pode trabalhar destas formas equivalentes: 1. **`CnpjUtils.format` / `.generate` / `.is_valid`** — helpers de classe para chamadas rápidas (encaminham para `DEFAULT`). -2. **`CnpjUtils::DEFAULT`** — singleton compartilhado mutável (o mesmo objeto usado pelos helpers de classe). +2. **`CnpjUtils::DEFAULT`** — singleton compartilhado mutável (o mesmo objeto usado pelos helpers de classe; em todo o processo / não isolado por thread). 3. **`CnpjUtils.new`** — instância configurável com padrões compartilhados entre formatar, gerar e validar. 4. **Classes principais sob `CnpjUtils`** — `CnpjUtils::CnpjFormatter`, `CnpjUtils::CnpjGenerator`, `CnpjUtils::CnpjValidator`. 5. **Módulos aninhados do pacote** — Options, helpers, erros e tipos via `CnpjUtils::CnpjFmt` / `CnpjGen` / `CnpjVal` (ex.: `CnpjUtils::CnpjFmt::CnpjFormatterOptions`, `CnpjUtils::CnpjFmt.cnpj_fmt`). @@ -130,7 +130,7 @@ CnpjUtils.is_valid('98765432000198') ### `CnpjUtils::DEFAULT` (instância padrão) -`CnpjUtils::DEFAULT` é o singleton pré-construído e **mutável** por trás dos helpers de classe (paridade com o export padrão do JS / `cnpj_utils` do Python). Mutá-lo afeta chamadas seguintes a `CnpjUtils.format` / `.generate` / `.is_valid`; instâncias `CnpjUtils.new` personalizadas permanecem independentes: +`CnpjUtils::DEFAULT` é o singleton pré-construído e **mutável** por trás dos helpers de classe (paridade com o export padrão do JS / `cnpj_utils` do Python). A configuração é **em todo o processo e compartilhada entre threads**: mutá-lo (ex.: `DEFAULT.formatter = …`) afeta chamadas seguintes a `CnpjUtils.format` / `.generate` / `.is_valid` para todos os chamadores no processo. Prefira `CnpjUtils.new` ou opções por chamada para trabalho concorrente ou isolado; instâncias personalizadas permanecem independentes de `DEFAULT`: ```ruby CnpjUtils::DEFAULT.formatter = { slash_key: '|' } @@ -250,7 +250,7 @@ Após `require 'cnpj-utilities'`: - **`CnpjUtils`**: Classe fachada para criar uma instância com configurações padrão opcionais de formatador, gerador e validador. - **`CnpjUtils.format` / `.generate` / `.is_valid`**: Helpers de classe que encaminham para `CnpjUtils::DEFAULT`. -- **`CnpjUtils::DEFAULT`**: Instância pré-construída mutável de `CnpjUtils` (o mesmo objeto usado pelos helpers de classe). +- **`CnpjUtils::DEFAULT`**: Instância pré-construída mutável de `CnpjUtils` (o mesmo objeto usado pelos helpers de classe). Em todo o processo / compartilhada entre threads — prefira `CnpjUtils.new` ou opções por chamada sob concorrência. - **`CnpjUtils::VERSION`**: String da versão da gem. - **Atalhos das classes principais**: `CnpjUtils::CnpjFormatter`, `CnpjUtils::CnpjGenerator`, `CnpjUtils::CnpjValidator` (os mesmos objetos das classes irmãs). - **Módulos aninhados do pacote**: `CnpjUtils::CnpjFmt`, `CnpjUtils::CnpjGen`, `CnpjUtils::CnpjVal` — superfície completa do irmão (Options, helpers, erros, tipos). Options/helpers/erros **não** são aliasados na raiz de `CnpjUtils`. @@ -270,8 +270,8 @@ Os erros definidos por esta gem são apenas de **uso indevido da API** (tipo inc | Classe | Herda de | Categoria | Condição de disparo | |--------|----------|-----------|---------------------| -| `CnpjUtils::TypeMismatchError` | `CnpjUtils::TypeMismatchError < TypeError < StandardError` (+ `include CnpjUtils::Error`) | Uso indevido da API | Argumento `settings` não-`nil` de `CnpjUtils.new` não é um `Hash` | | `CnpjUtils::InvalidArgumentCombinationError` | `CnpjUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include CnpjUtils::Error`) | Uso indevido da API | `Hash`/instância de settings/options não-`nil` passado junto com qualquer argumento nomeado não-`nil` | +| `CnpjUtils::TypeMismatchError` | `CnpjUtils::TypeMismatchError < TypeError < StandardError` (+ `include CnpjUtils::Error`) | Uso indevido da API | Argumento `settings` não-`nil` de `CnpjUtils.new` não é um `Hash` | ##### `CnpjUtils::Error` (módulo marcador) diff --git a/packages/cnpj-utilities/src/cnpj-utilities/cnpj_utils.rb b/packages/cnpj-utilities/src/cnpj-utilities/cnpj_utils.rb index 76446cc..a5f87b7 100644 --- a/packages/cnpj-utilities/src/cnpj-utilities/cnpj_utils.rb +++ b/packages/cnpj-utilities/src/cnpj-utilities/cnpj_utils.rb @@ -15,7 +15,8 @@ # # - {CnpjUtils.format}, {CnpjUtils.generate}, {CnpjUtils.is_valid} — class helpers # that alias {CnpjUtils::DEFAULT} (preferred quick path) -# - {CnpjUtils::DEFAULT} — mutable shared singleton (JS/Python parity) +# - {CnpjUtils::DEFAULT} — mutable process-wide singleton (JS/Python parity; not +# thread-isolated — prefer {.new} / per-call options under concurrency) # - {CnpjUtils#format}, {CnpjUtils#generate}, {CnpjUtils#is_valid} — instance API # - {CnpjUtils::VERSION} # - {CnpjUtils::InvalidArgumentCombinationError} (API misuse) @@ -25,7 +26,9 @@ # {CnpjGen}, and {CnpjVal} remain loadable after +require 'cnpj-utilities'+. # # Mutating {CnpjUtils::DEFAULT} (e.g. via setters) affects subsequent class-helper -# calls. Custom {CnpjUtils.new} instances are independent of +DEFAULT+. +# calls process-wide (shared across threads). Prefer {CnpjUtils.new} or per-call +# options for concurrent or isolated work. Custom instances are independent of +# +DEFAULT+. # # @example # require 'cnpj-utilities' @@ -372,8 +375,11 @@ def is_valid(cnpj_input, options = nil, **keywords) # Default {CnpjUtils} instance with default formatter, generator, and # validator options (parity with the JS default export / Python +cnpj_utils+ - # singleton). Mutating this instance (e.g. via setters) affects subsequent - # {CnpjUtils.format}, {CnpjUtils.generate}, and {CnpjUtils.is_valid} calls. + # singleton). Configuration is process-wide and shared across threads: + # mutating this instance (e.g. via setters) affects subsequent + # {CnpjUtils.format}, {CnpjUtils.generate}, and {CnpjUtils.is_valid} calls for + # every caller in the process. Prefer {CnpjUtils.new} or per-call options for + # threaded or isolated work. DEFAULT = new class << self diff --git a/packages/cnpj-utilities/tests/cnpj_utils.spec.rb b/packages/cnpj-utilities/tests/cnpj_utils.spec.rb index 0559269..15de407 100644 --- a/packages/cnpj-utilities/tests/cnpj_utils.spec.rb +++ b/packages/cnpj-utilities/tests/cnpj_utils.spec.rb @@ -1044,6 +1044,17 @@ def default_validator_options_snapshot end describe 'package smoke' do + it 'is an instantiable class' do + aggregate_failures do + expect(described_class).to be_a(Class) + expect(described_class.new).to be_a(described_class) + end + end + + it 'exposes a VERSION string' do + expect(described_class::VERSION).to be_a(String).and match(/\A\d+\.\d+\.\d+\z/) + end + it 'formats through DEFAULT with a custom slash_key' do result = described_class::DEFAULT.format('01ABC234000X56', slash_key: '|') diff --git a/packages/cpf-utilities/CHANGELOG.md b/packages/cpf-utilities/CHANGELOG.md index 9e976a9..7223092 100644 --- a/packages/cpf-utilities/CHANGELOG.md +++ b/packages/cpf-utilities/CHANGELOG.md @@ -1 +1,18 @@ # cpf-utilities + +## 1.0.0 + +### 🚀 Stable Version Released! + +Unified toolkit to deal with CPF (Brazilian personal tax ID): formatting, generation, and validation. Main features: + +- **Unified façade**: `CpfUtils` delegates `#format`, `#generate`, and `#is_valid` to `cpf-fmt`, `cpf-gen`, and `cpf-val`. +- **Numeric CPF**: digits-only 11-character IDs formatted as `XXX.XXX.XXX-XX` (no alphanumeric / `slash_key` / `type` options). +- **Quick helpers**: `CpfUtils.format` / `.generate` / `.is_valid` alias mutable `CpfUtils::DEFAULT` (process-wide; prefer `CpfUtils.new` / per-call options under concurrency). +- **Two-tier re-exports**: `CpfUtils::CpfFormatter` / `CpfGenerator` / `CpfValidator` at the façade root; full sibling surface under `CpfUtils::CpfFmt` / `CpfGen` / `CpfVal`. +- **Configurable components**: constructor and setters accept component instances, `*Options`/`Hash` (formatter/generator), or `nil`; validator is instance/`nil`/duck-type only (no `CpfValidatorOptions`). +- **Per-call overrides**: `#format` and `#generate` accept an options `Hash`/instance or keyword overrides (not both); `#is_valid` takes input only. +- **Root siblings**: after `require 'cpf-utilities'`, `CpfFmt`, `CpfGen`, and `CpfVal` remain loadable (same objects as the nests). +- **Structured errors**: façade misuse leaves plus full propagated `CpfFmt` / `CpfGen` / `CpfVal` reference in the [README](./README.md) (complete `StandardError` chains; misuse-then-domain; `on_fail` / `false`). + +For detailed usage and API reference, see the [README](./README.md). diff --git a/packages/cpf-utilities/Gemfile b/packages/cpf-utilities/Gemfile index 2295579..c6122eb 100644 --- a/packages/cpf-utilities/Gemfile +++ b/packages/cpf-utilities/Gemfile @@ -4,10 +4,6 @@ source 'https://rubygems.org' gemspec -gem 'cpf-fmt', path: '../cpf-fmt' -gem 'cpf-gen', path: '../cpf-gen' -gem 'cpf-val', path: '../cpf-val' - group :test do gem 'rake', '~> 13.2' gem 'rspec', '~> 3.13' diff --git a/packages/cpf-utilities/README.md b/packages/cpf-utilities/README.md new file mode 100644 index 0000000..923438a --- /dev/null +++ b/packages/cpf-utilities/README.md @@ -0,0 +1,657 @@ +![cpf-utilities for Ruby](https://br-utils.vercel.app/img/cover_cpf-utils.jpg) + +[![Gem Version](https://img.shields.io/gem/v/cpf-utilities)](https://rubygems.org/gems/cpf-utilities) +[![Gem Downloads](https://img.shields.io/gem/dt/cpf-utilities)](https://rubygems.org/gems/cpf-utilities) +[![Ruby Version](https://img.shields.io/gem/rv/cpf-utilities)](https://www.ruby-lang.org/) +[![Test Status](https://img.shields.io/github/actions/workflow/status/LacusSolutions/br-utils-ruby/ci.yml?label=ci/cd)](https://github.com/LacusSolutions/br-utils-ruby/actions) +[![Last Update Date](https://img.shields.io/github/last-commit/LacusSolutions/br-utils-ruby)](https://github.com/LacusSolutions/br-utils-ruby) +[![Project License](https://img.shields.io/github/license/LacusSolutions/br-utils-ruby)](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE) + +> 🌎 [Acessar documentação em português](./README.pt.md) + +A Ruby toolkit to format, generate, and validate CPF (Brazilian Individual's Taxpayer ID). It wraps [`cpf-fmt`](https://rubygems.org/gems/cpf-fmt), [`cpf-gen`](https://rubygems.org/gems/cpf-gen), and [`cpf-val`](https://rubygems.org/gems/cpf-val) in a single façade class (`CpfUtils`). + +## Ruby Support + +| ![Ruby 3.1](https://img.shields.io/badge/Ruby-3.1-CC342D?logo=ruby&logoColor=white) | ![Ruby 3.2](https://img.shields.io/badge/Ruby-3.2-CC342D?logo=ruby&logoColor=white) | ![Ruby 3.3](https://img.shields.io/badge/Ruby-3.3-CC342D?logo=ruby&logoColor=white) | ![Ruby 3.4](https://img.shields.io/badge/Ruby-3.4-CC342D?logo=ruby&logoColor=white) | ![Ruby 4.0](https://img.shields.io/badge/Ruby-4.0-CC342D?logo=ruby&logoColor=white) | +| --- | --- | --- | --- | --- | +| Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | + +Requires Ruby **≥ 3.1** (see `required_ruby_version` in the gemspec). + +## Features + +- ✅ **Unified API**: Class helpers `CpfUtils.format` / `.generate` / `.is_valid` +- ✅ **Two-tier access**: Prefer `CpfUtils::CpfFormatter` / `CpfGenerator` / `CpfValidator` for the main classes; Options, helpers, and errors live under `CpfUtils::CpfFmt` / `CpfGen` / `CpfVal` (root siblings `CpfFmt` / `CpfGen` / `CpfVal` still work) +- ✅ **Numeric CPF**: Format, generate, and validate 11-digit numeric CPF (`XXX.XXX.XXX-XX`) +- ✅ **Reusable instance**: `CpfUtils` class with optional default settings (formatter/generator options or instances; validator instance) +- ✅ **Flexible input**: `#format` and `#is_valid` accept a `String` or an `Array` of strings (elements concatenated in order) +- ✅ **Per-call overrides**: Instance defaults plus a per-call options `Hash`/`*Options` instance **or** keyword overrides on `#format` / `#generate` (not both); `#is_valid` takes input only +- ✅ **Error handling**: Component errors propagate unchanged; this gem defines `CpfUtils::TypeMismatchError` and `CpfUtils::InvalidArgumentCombinationError` for API misuse + +## Installation + +Install the gem directly: + +```bash +gem install cpf-utilities +``` + +Or add it to your `Gemfile` and run `bundle install`: + +```ruby +gem 'cpf-utilities' +``` + +This installs **`cpf-utilities`** together with [`cpf-fmt`](https://rubygems.org/gems/cpf-fmt), [`cpf-gen`](https://rubygems.org/gems/cpf-gen), and [`cpf-val`](https://rubygems.org/gems/cpf-val). You do **not** need separate `gem install` / `gem` lines for the component packages when using **`cpf-utilities`**. + +## Require + +```ruby +require 'cpf-utilities' +``` + +## Quick Start + +Basic usage with class helpers (aliases of `CpfUtils::DEFAULT`): + +```ruby +require 'cpf-utilities' + +cpf = '12345678909' + +CpfUtils.format(cpf) # => "123.456.789-09" +CpfUtils.format(cpf, hidden: true) # => "123.***.***-**" +CpfUtils.format( # => "123456789_09" + cpf, + dot_key: '', + dash_key: '_' +) + +CpfUtils.generate # => e.g. "47844241055" (11-digit numeric) +CpfUtils.generate(format: true) # => e.g. "478.442.410-55" +CpfUtils.generate(prefix: '528250911') # => e.g. "52825091138" + +CpfUtils.is_valid('12345678909') # => true +CpfUtils.is_valid('123.456.789-09') # => true +CpfUtils.is_valid('12345678900') # => false +``` + +## Usage + +You can work in these equivalent ways: + +1. **`CpfUtils.format` / `.generate` / `.is_valid`** — class helpers for quick one-off calls (forward to `DEFAULT`). +2. **`CpfUtils::DEFAULT`** — mutable shared singleton (same object the class helpers use; process-wide / not thread-isolated). +3. **`CpfUtils.new`** — configurable instance with shared defaults across format, generate, and validate. +4. **Main classes under `CpfUtils`** — `CpfUtils::CpfFormatter`, `CpfUtils::CpfGenerator`, `CpfUtils::CpfValidator`. +5. **Nested package modules** — Options, helpers, errors, and types via `CpfUtils::CpfFmt` / `CpfGen` / `CpfVal` (e.g. `CpfUtils::CpfFmt::CpfFormatterOptions`, `CpfUtils::CpfFmt.cpf_fmt`). +6. **Root sibling modules** (still supported) — `CpfFmt`, `CpfGen`, `CpfVal` unchanged. + +All approaches expose the same options and behavior. For exhaustive option tables and component-specific details, see the README of each [bundled package](#bundled-packages). + +### Formatter options + +When calling `#format(cpf_input, options = nil, **keywords)`, all options are optional: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `hidden` | `Boolean` | `false` | When `true`, mask digits in `hidden_start`–`hidden_end` with `hidden_key` | +| `hidden_key` | `String` | `'*'` | Character(s) used to replace masked digits | +| `hidden_start` | `Integer` | `3` | Start index (0–10, inclusive) of the range to hide | +| `hidden_end` | `Integer` | `10` | End index (0–10, inclusive) of the range to hide | +| `dot_key` | `String` | `'.'` | Dot delimiter (e.g. in `123.456.789`) | +| `dash_key` | `String` | `'-'` | Dash delimiter (e.g. before check digits `…-09`) | +| `escape` | `Boolean` | `false` | When `true`, escape HTML special characters in the result | +| `encode` | `Boolean` | `false` | When `true`, URL-encode the result (similar to JavaScript `encodeURIComponent`) | +| `on_fail` | `Proc` / callable | returns `''` | Callback when sanitized input length ≠ 11; return value is used as result | + +### Generator options + +When calling `#generate(options = nil, **keywords)`, all options are optional: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | When `true`, return the generated CPF in standard format (`000.000.000-00`) | +| `prefix` | `String` | `''` | Partial start string (0–9 digits). Non-digits are stripped; missing characters are generated and check digits computed. Prefixes longer than 9 digits are truncated silently. | + +Prefix rules: the base (first 9 digits) cannot be all zeros; 9 repeated digits (e.g. `999999999`) are not allowed. + +### Class helpers (`CpfUtils.format` / `.generate` / `.is_valid`) + +These class methods are aliases of the same methods on `CpfUtils::DEFAULT`. Prefer them for one-off calls: + +```ruby +CpfUtils.format('12345678909') +CpfUtils.generate(format: true) +CpfUtils.is_valid('12345678909') +``` + +### `CpfUtils::DEFAULT` (default instance) + +`CpfUtils::DEFAULT` is the pre-built, **mutable** singleton behind the class helpers (parity with the JS default export / Python `cpf_utils`). Its configuration is **process-wide and shared across threads**: mutating it (e.g. `DEFAULT.formatter = …`) affects subsequent `CpfUtils.format` / `.generate` / `.is_valid` calls for every caller in the process. Prefer `CpfUtils.new` or per-call options for concurrent or isolated work; custom instances stay independent of `DEFAULT`: + +```ruby +CpfUtils::DEFAULT.formatter = { dash_key: '|' } +CpfUtils.format('12345678909') # => "123.456.789|09" + +custom = CpfUtils.new +custom.format('12345678909') # => "123.456.789-09" (unaffected) +``` + +Instance methods on `DEFAULT` (and any `CpfUtils` instance): + +- **`#format(cpf_input, options = nil, **keywords)`**: Formats a CPF string or array of strings. Delegates to the internal formatter. Input must be 11 digits (after sanitization); otherwise `on_fail` is used. +- **`#generate(options = nil, **keywords)`**: Generates a valid CPF. Delegates to the internal generator. +- **`#is_valid(cpf_input)`**: Returns `true` if the CPF is valid. Delegates to the internal validator. No per-call options — the CPF validator has none. + +### `CpfUtils` (class) + +For custom default formatter, generator, or validator, create your own instance: + +```ruby +require 'cpf-utilities' + +utils = CpfUtils.new( + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true, prefix: '123' } +) + +utils.format('47844241055') # => "478.###.###-##" +utils.generate # => e.g. "123.456.789-09" +utils.is_valid('123.456.789-09') # => true + +# Access or replace internal instances +utils.formatter # => CpfFmt::CpfFormatter +utils.generator # => CpfGen::CpfGenerator +utils.validator # => CpfVal::CpfValidator +``` + +- **`CpfUtils.new(settings = nil, **keywords)`**: Optional settings. Pass either a settings `Hash` with `:formatter`, `:generator`, and/or `:validator` keys, **or** the same keys as keyword arguments — not both (passing both raises `CpfUtils::InvalidArgumentCombinationError`). For `:formatter` / `:generator`, each value may be a component instance, a `*Options` instance (stored by reference — mutating it later affects subsequent calls with no per-call override), a plain options `Hash`, or omitted/`nil` for defaults. For `:validator`, pass a `CpfVal::CpfValidator` instance, `nil`, or a duck-typed object — **not** an options `Hash` (there is no `CpfValidatorOptions`). +- **`#format(cpf_input, options = nil, **keywords)`**: Same as the default instance; per-call options override the formatter’s defaults for that call only. Pass either an options `Hash`/`CpfFmt::CpfFormatterOptions` **or** keyword overrides — not both. +- **`#generate(options = nil, **keywords)`**: Same as the default instance; per-call options override the generator’s defaults. Pass either an options `Hash`/`CpfGen::CpfGeneratorOptions` **or** keyword overrides — not both. +- **`#is_valid(cpf_input)`**: Same as the default instance. No per-call options. +- **`#formatter`**, **`#generator`**, **`#validator`**: Accessors (getters and setters) for the internal components. Setters accept the same shapes as the constructor. To change a single formatter/generator option without replacing the instance, mutate the component’s options (e.g. `utils.formatter.options.hidden = true`). + +Instance defaults and per-call overrides: + +```ruby +require 'cpf-utilities' + +utils = CpfUtils.new( + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true } +) + +cpf = '12345678909' + +utils.format(cpf) # masked (instance formatter defaults) +utils.format(cpf, hidden: false) # this call only: unmasked +utils.generate(format: false) # this call only: compact output +utils.is_valid(cpf) # => true +``` + +Options can also be passed as a `Hash` (or options instance) on `#format` / `#generate` — without keyword overrides: + +```ruby +utils.format(cpf, { dash_key: '|' }) +utils.generate({ prefix: '12345', format: true }) +``` + +### Using component classes and nested modules + +Preferred paths after `require 'cpf-utilities'`: + +```ruby +require 'cpf-utilities' + +# Main classes at the façade root +formatter = CpfUtils::CpfFormatter.new(hidden: true) +generator = CpfUtils::CpfGenerator.new(format: true) +validator = CpfUtils::CpfValidator.new + +formatter.format('47844241055') # => "478.***.***-**" + +# Options, helpers, and errors under nested package modules +options = CpfUtils::CpfFmt::CpfFormatterOptions.new(dash_key: '|') +CpfUtils::CpfFmt.cpf_fmt('12345678909') # => "123.456.789-09" + +begin + CpfUtils::CpfFmt.cpf_fmt(12_345) +rescue CpfUtils::CpfFmt::TypeMismatchError + # wrong input type +end +``` + +Root siblings remain supported (same objects as the nests): + +```ruby +CpfFmt.cpf_fmt('12345678909', dash_key: '|') # => "123.456.789|09" +CpfGen.cpf_gen(format: true) # => e.g. "478.442.410-55" +CpfVal.cpf_val('12345678909') # => true +CpfFmt::CpfFormatter.new(hidden: true) +``` + +See [`cpf-fmt`](../cpf-fmt/README.md), [`cpf-gen`](../cpf-gen/README.md), and [`cpf-val`](../cpf-val/README.md) for full option and error details. + +## API + +### Exports + +After `require 'cpf-utilities'`: + +- **`CpfUtils`**: Façade class to create a utils instance with optional default formatter, generator, and validator settings. +- **`CpfUtils.format` / `.generate` / `.is_valid`**: Class helpers that forward to `CpfUtils::DEFAULT`. +- **`CpfUtils::DEFAULT`**: Mutable pre-built `CpfUtils` instance (same object the class helpers use). Process-wide / shared across threads — prefer `CpfUtils.new` or per-call options under concurrency. +- **`CpfUtils::VERSION`**: Gem version string. +- **Main-class shortcuts**: `CpfUtils::CpfFormatter`, `CpfUtils::CpfGenerator`, `CpfUtils::CpfValidator` (same objects as the sibling classes). +- **Nested package modules**: `CpfUtils::CpfFmt`, `CpfUtils::CpfGen`, `CpfUtils::CpfVal` — full sibling surface (Options, helpers, errors, types). Options/helpers/errors are **not** aliased at the `CpfUtils` root. +- **Root sibling modules** (still supported): `CpfFmt`, `CpfGen`, `CpfVal` — same objects as the nests. + +### Errors & Exceptions + +`CpfUtils` defines only API-misuse errors for this gem’s argument rules. Component errors are raised by the bundled packages and propagate unchanged. + +#### Defined by `cpf-utilities` + +Errors defined by this gem are **API misuse** only (wrong type or invalid argument combination). Every custom error includes the `CpfUtils::Error` marker module. This gem defines **no** `CpfUtils::DomainError` and no domain leaves — domain failures come only from the [bundled packages](#propagated-from-bundled-packages) and keep those packages’ namespaces (`CpfFmt::…`, `CpfGen::…`, `CpfVal::…`). + +`rescue CpfUtils::Error` catches **only** errors this gem raises. It does **not** catch component errors that propagate unchanged. + +##### Summary + +| Class | Inherits from | Category | Trigger condition | +|-------|---------------|----------|-------------------| +| `CpfUtils::InvalidArgumentCombinationError` | `CpfUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include CpfUtils::Error`) | API misuse | Constructor: non-`nil` settings `Hash` with any non-`nil` keyword; or `#format`/`#generate`/class helpers: non-`nil` options `Hash`/`*Options` with any non-`nil` keyword | +| `CpfUtils::TypeMismatchError` | `CpfUtils::TypeMismatchError < TypeError < StandardError` (+ `include CpfUtils::Error`) | API misuse | Non-`nil` `settings` argument to `CpfUtils.new` is not a `Hash` | + +##### `CpfUtils::Error` (marker module) + +- **Inheritance:** module marker mixed into every custom error this gem raises via `include` (not a class). +- **Category:** N/A (rescue target only) — not a failure mode by itself. +- **When it is raised:** Never raised directly; included by every custom error this gem raises. +- **Example:** N/A +- **How to rescue it:** + +```ruby +rescue CpfUtils::Error + # TypeMismatchError, InvalidArgumentCombinationError from this gem only + # (not CpfFmt::*, CpfGen::*, or CpfVal::* errors) +``` + +##### `CpfUtils::TypeMismatchError` + +- **Inheritance:** `CpfUtils::TypeMismatchError < TypeError < StandardError` (includes `CpfUtils::Error`) +- **Category:** API misuse — the caller passed a value of the wrong type. +- **When it is raised:** Raised when `CpfUtils.new` receives a non-`nil` `settings` argument that is not a `Hash`. +- **Example:** + +```ruby +CpfUtils.new('not-a-hash') # raises CpfUtils::TypeMismatchError +CpfUtils.new(false) # raises CpfUtils::TypeMismatchError (false is non-nil) +``` + +- **How to rescue it:** + +```ruby +rescue CpfUtils::TypeMismatchError + # this gem's type-contract violation + +rescue TypeError + # native type errors, including this gem's TypeMismatchError +``` + +##### `CpfUtils::InvalidArgumentCombinationError` + +- **Inheritance:** `CpfUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (includes `CpfUtils::Error`) +- **Category:** API misuse — the caller mixed mutually exclusive argument patterns. +- **When it is raised:** Raised when `CpfUtils.new` receives both a non-`nil` settings `Hash` and any non-`nil` keyword argument (`formatter:`, `generator:`, `validator:`); or when `#format`, `#generate`, or the class helpers receive both a non-`nil` options `Hash`/`*Options` instance and any non-`nil` keyword argument at the same time. `#is_valid` has no options path and does not raise this error. +- **Example:** + +```ruby +CpfUtils.new({ formatter: { hidden: true } }, generator: { format: true }) +# raises CpfUtils::InvalidArgumentCombinationError + +CpfUtils.format('12345678909', { hidden: true }, dash_key: '|') +# raises CpfUtils::InvalidArgumentCombinationError +``` + +- **How to rescue it:** + +```ruby +rescue CpfUtils::InvalidArgumentCombinationError + # this gem's invalid signature combination + +rescue ArgumentError + # native argument errors, including this gem's InvalidArgumentCombinationError +``` + +##### Rescue granularity + +Each level is shown as its own standalone example (do not merge them into one `rescue` ladder — a broad native handler would make narrower clauses unreachable). + +```ruby +require 'cpf-utilities' + +# 1) Single native class — catches misuse errors of that kind, +# including non-library ones already handled elsewhere in the consumer's code. +begin + CpfUtils.new('not-a-hash') +rescue TypeError + # CpfUtils::TypeMismatchError and any other TypeError (library or not) +end + +begin + CpfUtils.new({ formatter: { hidden: true } }, generator: { format: true }) +rescue ArgumentError + # CpfUtils::InvalidArgumentCombinationError and any other ArgumentError (library or not) +end +``` + +```ruby +require 'cpf-utilities' + +# 2) Bundled DomainError — this gem defines no DomainError; domain failures +# come from component packages and keep those namespaces (e.g. CpfFmt). +begin + CpfUtils.new.format('12345678909', hidden_start: -1) +rescue CpfFmt::DomainError + # CpfFmt::OutOfRangeError, CpfFmt::ValidationError, and other DomainError subclasses +end +``` + +```ruby +require 'cpf-utilities' + +# 3) CpfUtils::Error — catches everything this gem raises, regardless of native ancestry. +# Does not catch CpfFmt::*, CpfGen::*, or CpfVal::* errors. +begin + CpfUtils.new('not-a-hash') +rescue CpfUtils::Error + # every custom error that includes CpfUtils::Error +end +``` + +```ruby +require 'cpf-utilities' + +# 4) Specific leaf class — catches only that exact failure mode. +begin + CpfUtils.new('not-a-hash') +rescue CpfUtils::TypeMismatchError + # only CpfUtils::TypeMismatchError +end +``` + +#### Propagated from bundled packages + +Component errors keep their package namespaces and propagate unchanged through the façade (and via nested / root sibling APIs). Each package also exposes an `*::Error` marker module for library-wide rescue. Invalid CPF **data** on `#is_valid` returns `false` (no domain raise). Formatting length failure is **not** raised by `#format` — it is delivered to **`on_fail`** as `CpfFmt::InvalidLengthError` (default `on_fail` returns `''`). + +##### Summary + +| Class | Inherits from | Category | Trigger condition | +|-------|---------------|----------|-------------------| +| `CpfFmt::InvalidArgumentCombinationError` | `CpfFmt::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include CpfFmt::Error`) | API misuse | Both an `options` instance/`Hash` and any non-`nil` keyword on `CpfFormatter` / `cpf_fmt` | +| `CpfFmt::TypeMismatchError` | `CpfFmt::TypeMismatchError < TypeError < StandardError` (+ `include CpfFmt::Error`) | API misuse | CPF input or formatter option has the wrong type (or `on_fail` return is not a `String`) | +| `CpfGen::InvalidArgumentCombinationError` | `CpfGen::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include CpfGen::Error`) | API misuse | Both an `options` instance/`Hash` and any non-`nil` keyword on `CpfGenerator` / `cpf_gen` | +| `CpfGen::TypeMismatchError` | `CpfGen::TypeMismatchError < TypeError < StandardError` (+ `include CpfGen::Error`) | API misuse | Generator option (`format` / `prefix`) has the wrong type | +| `CpfVal::TypeMismatchError` | `CpfVal::TypeMismatchError < TypeError < StandardError` (+ `include CpfVal::Error`) | API misuse | CPF input is not a `String` or `Array` of strings | +| `CpfFmt::InvalidLengthError` | `CpfFmt::InvalidLengthError < CpfFmt::DomainError < RangeError < StandardError` (+ `include CpfFmt::Error`) | Domain error | Sanitized length ≠ 11 — **passed to `on_fail`**, not raised by `#format` | +| `CpfFmt::OutOfRangeError` | `CpfFmt::OutOfRangeError < CpfFmt::DomainError < RangeError < StandardError` (+ `include CpfFmt::Error`) | Domain error | `hidden_start` / `hidden_end` outside `0`–`10` | +| `CpfFmt::ValidationError` | `CpfFmt::ValidationError < CpfFmt::DomainError < RangeError < StandardError` (+ `include CpfFmt::Error`) | Domain error | `hidden_key` / `dot_key` / `dash_key` contains a disallowed character | +| `CpfGen::ValidationError` | `CpfGen::ValidationError < CpfGen::DomainError < RangeError < StandardError` (+ `include CpfGen::Error`) | Domain error | `prefix` is ineligible (zeroed base or 9 repeated digits) | + +##### `CpfFmt::DomainError` + +- **Inheritance:** `CpfFmt::DomainError < RangeError < StandardError` (includes `CpfFmt::Error`) +- **Category:** Domain error — ancestor for formatter domain leaves. +- **When it is raised:** Not raised directly; rescue target for `OutOfRangeError`, `ValidationError`, and re-raised `InvalidLengthError`. +- **Example:** Prefer rescuing a leaf, or `CpfFmt::DomainError` for all formatter domain failures. +- **How to rescue it:** + +```ruby +rescue CpfFmt::DomainError + # OutOfRangeError, ValidationError, InvalidLengthError (if re-raised from on_fail) +``` + +##### `CpfFmt::TypeMismatchError` + +- **Inheritance:** `CpfFmt::TypeMismatchError < TypeError < StandardError` (includes `CpfFmt::Error`) +- **Category:** API misuse — wrong type for CPF input or a formatter option. +- **When it is raised:** Raised when `#format` / `cpf_fmt` receives a non-`String` / non-`Array` input, an option has the wrong type, or `on_fail` does not return a `String`. +- **Example:** + +```ruby +CpfUtils.new.format(12_345) # raises CpfFmt::TypeMismatchError +``` + +- **How to rescue it:** + +```ruby +rescue CpfFmt::TypeMismatchError + # formatter type-contract violation + +rescue TypeError + # native type errors, including CpfFmt::TypeMismatchError +``` + +##### `CpfFmt::InvalidArgumentCombinationError` + +- **Inheritance:** `CpfFmt::InvalidArgumentCombinationError < ArgumentError < StandardError` (includes `CpfFmt::Error`) +- **Category:** API misuse — mixed `options` and keywords on the formatter API. +- **When it is raised:** Raised by `CpfFmt::CpfFormatter` / `CpfFmt.cpf_fmt` when both an `options` instance/`Hash` and any non-`nil` keyword are passed. (The façade raises `CpfUtils::InvalidArgumentCombinationError` for the same pattern on `CpfUtils#format`.) +- **Example:** + +```ruby +CpfFmt::CpfFormatter.new({ dash_key: '_' }, hidden: true) +# raises CpfFmt::InvalidArgumentCombinationError +``` + +- **How to rescue it:** + +```ruby +rescue CpfFmt::InvalidArgumentCombinationError + # formatter invalid signature combination + +rescue ArgumentError + # native argument errors, including this one +``` + +##### `CpfFmt::InvalidLengthError` (callback-delivered) + +- **Inheritance:** `CpfFmt::InvalidLengthError < CpfFmt::DomainError < RangeError < StandardError` (includes `CpfFmt::Error`) +- **Category:** Domain error — sanitized CPF length is not exactly 11. +- **When it is raised:** **Not raised** by `#format` / `cpf_fmt`; constructed and passed as the second argument to `on_fail`. +- **Example:** + +```ruby +custom_fail = ->(value, error) { + error # => # + "Invalid CPF: #{value}" +} + +CpfUtils.new.format('123', on_fail: custom_fail) # => "Invalid CPF: 123" +CpfUtils.new.format('123') # => "" (default on_fail) +``` + +- **How to rescue it:** Handle inside `on_fail` (typical), or rescue if you re-raise: + +```ruby +rescue CpfFmt::InvalidLengthError + # this exact length violation + +rescue CpfFmt::DomainError + # RangeError-rooted domain failures from cpf-fmt +``` + +##### `CpfFmt::OutOfRangeError` + +- **Inheritance:** `CpfFmt::OutOfRangeError < CpfFmt::DomainError < RangeError < StandardError` (includes `CpfFmt::Error`) +- **Category:** Domain error — `hidden_start` / `hidden_end` outside `0`–`10`. +- **When it is raised:** Raised when building or applying formatter options with an out-of-range hide index. +- **Example:** + +```ruby +CpfUtils.new.format('12345678909', hidden_start: -1) # raises CpfFmt::OutOfRangeError +``` + +- **How to rescue it:** + +```ruby +rescue CpfFmt::OutOfRangeError + # this exact range violation + +rescue CpfFmt::DomainError + # RangeError-rooted domain failures from cpf-fmt +``` + +##### `CpfFmt::ValidationError` + +- **Inheritance:** `CpfFmt::ValidationError < CpfFmt::DomainError < RangeError < StandardError` (includes `CpfFmt::Error`) +- **Category:** Domain error — a key option contains a disallowed character. +- **When it is raised:** Raised when `hidden_key`, `dot_key`, or `dash_key` contains a forbidden character. +- **Example:** + +```ruby +CpfUtils.new(formatter: { dot_key: 'å' }) # raises CpfFmt::ValidationError +``` + +- **How to rescue it:** + +```ruby +rescue CpfFmt::ValidationError + # this exact domain validation failure + +rescue CpfFmt::DomainError + # RangeError-rooted domain failures from cpf-fmt +``` + +##### `CpfGen::DomainError` + +- **Inheritance:** `CpfGen::DomainError < RangeError < StandardError` (includes `CpfGen::Error`) +- **Category:** Domain error — ancestor for generator domain leaves. +- **When it is raised:** Not raised directly; rescue target for `CpfGen::ValidationError`. +- **Example:** Prefer `rescue CpfGen::ValidationError` or `CpfGen::DomainError`. +- **How to rescue it:** + +```ruby +rescue CpfGen::DomainError + # ValidationError and other DomainError subclasses from cpf-gen +``` + +##### `CpfGen::TypeMismatchError` + +- **Inheritance:** `CpfGen::TypeMismatchError < TypeError < StandardError` (includes `CpfGen::Error`) +- **Category:** API misuse — wrong type for a generator option. +- **When it is raised:** Raised when `format` or `prefix` has the wrong runtime type. +- **Example:** + +```ruby +CpfUtils.new.generate(prefix: 123) # raises CpfGen::TypeMismatchError +``` + +- **How to rescue it:** + +```ruby +rescue CpfGen::TypeMismatchError + # generator type-contract violation + +rescue TypeError + # native type errors, including CpfGen::TypeMismatchError +``` + +##### `CpfGen::InvalidArgumentCombinationError` + +- **Inheritance:** `CpfGen::InvalidArgumentCombinationError < ArgumentError < StandardError` (includes `CpfGen::Error`) +- **Category:** API misuse — mixed `options` and keywords on the generator API. +- **When it is raised:** Raised by `CpfGen::CpfGenerator` / `CpfGen.cpf_gen` when both an `options` instance/`Hash` and any non-`nil` keyword are passed. (The façade raises `CpfUtils::InvalidArgumentCombinationError` for the same pattern on `CpfUtils#generate`.) +- **Example:** + +```ruby +CpfGen::CpfGenerator.new({ format: true }, prefix: '123') +# raises CpfGen::InvalidArgumentCombinationError +``` + +- **How to rescue it:** + +```ruby +rescue CpfGen::InvalidArgumentCombinationError + # generator invalid signature combination + +rescue ArgumentError + # native argument errors, including this one +``` + +##### `CpfGen::ValidationError` + +- **Inheritance:** `CpfGen::ValidationError < CpfGen::DomainError < RangeError < StandardError` (includes `CpfGen::Error`) +- **Category:** Domain error — ineligible `prefix`. +- **When it is raised:** Raised when `prefix` is a zeroed base (`'000000000'`) or 9 repeated digits (e.g. `'999999999'`). +- **Example:** + +```ruby +CpfUtils.new.generate(prefix: '000000000') # raises CpfGen::ValidationError +``` + +- **How to rescue it:** + +```ruby +rescue CpfGen::ValidationError + # this exact domain validation failure + +rescue CpfGen::DomainError + # RangeError-rooted domain failures from cpf-gen +``` + +##### `CpfVal::TypeMismatchError` + +- **Inheritance:** `CpfVal::TypeMismatchError < TypeError < StandardError` (includes `CpfVal::Error`) +- **Category:** API misuse — wrong type for CPF input. +- **When it is raised:** Raised when `#is_valid` / `cpf_val` receives a value that is not a `String` or an `Array` of strings (including a non-string array element). Invalid CPF **data** returns `false` and does not raise. +- **Example:** + +```ruby +CpfUtils.new.is_valid(12_345_678_909) # raises CpfVal::TypeMismatchError +CpfUtils.new.is_valid('12345678900') # => false (invalid data, no raise) +``` + +- **How to rescue it:** + +```ruby +rescue CpfVal::TypeMismatchError + # validator type-contract violation + +rescue TypeError + # native type errors, including CpfVal::TypeMismatchError +``` + +### Bundled packages + +| Package | Main resources | README | +|---------|----------------|--------| +| [`cpf-fmt`](https://rubygems.org/gems/cpf-fmt) | `CpfFmt::CpfFormatter`, `CpfFmt::CpfFormatterOptions`, `CpfFmt.cpf_fmt` | [docs](../cpf-fmt/README.md) | +| [`cpf-gen`](https://rubygems.org/gems/cpf-gen) | `CpfGen::CpfGenerator`, `CpfGen::CpfGeneratorOptions`, `CpfGen.cpf_gen` | [docs](../cpf-gen/README.md) | +| [`cpf-val`](https://rubygems.org/gems/cpf-val) | `CpfVal::CpfValidator`, `CpfVal.cpf_val` | [docs](../cpf-val/README.md) | + +All of the above are pulled in as dependencies of **`cpf-utilities`**. For exhaustive option tables, exception lists, and edge-case behavior, see each package README. + +## Contribution & Support + +We welcome contributions! Please see our [Contributing Guidelines](https://github.com/LacusSolutions/br-utils-ruby/blob/main/CONTRIBUTING.md) for details. If you find this project helpful, please consider: + +- ⭐ Starring the repository +- 🤝 Contributing to the codebase +- 💡 [Suggesting new features](https://github.com/LacusSolutions/br-utils-ruby/issues) +- 🐛 [Reporting bugs](https://github.com/LacusSolutions/br-utils-ruby/issues) + +## License + +This project is licensed under the MIT License — see the [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE) file for details. + +## Changelog + +See [CHANGELOG](./CHANGELOG.md) for a list of changes and version history. + +--- + +Made with ❤️ by [Lacus Solutions](https://github.com/LacusSolutions) diff --git a/packages/cpf-utilities/README.pt.md b/packages/cpf-utilities/README.pt.md new file mode 100644 index 0000000..78d623c --- /dev/null +++ b/packages/cpf-utilities/README.pt.md @@ -0,0 +1,642 @@ +![cpf-utilities para Ruby](https://br-utils.vercel.app/img/cover_cpf-utils.jpg) + +> 🌎 [Access documentation in English](./README.md) + +Kit em Ruby para formatar, gerar e validar CPF (Cadastro de Pessoa Física). Envolve [`cpf-fmt`](https://rubygems.org/gems/cpf-fmt), [`cpf-gen`](https://rubygems.org/gems/cpf-gen) e [`cpf-val`](https://rubygems.org/gems/cpf-val) em uma única classe fachada (`CpfUtils`). + +## Recursos + +- ✅ **API unificada**: Helpers de classe `CpfUtils.format` / `.generate` / `.is_valid` (aliases de `CpfUtils::DEFAULT`); `DEFAULT` mutável para ajustes compartilhados +- ✅ **Acesso em dois níveis**: Prefira `CpfUtils::CpfFormatter` / `CpfGenerator` / `CpfValidator` para as classes principais; Options, helpers e erros ficam em `CpfUtils::CpfFmt` / `CpfGen` / `CpfVal` (os irmãos na raiz `CpfFmt` / `CpfGen` / `CpfVal` continuam funcionando) +- ✅ **CPF numérico**: Formatar, gerar e validar CPF de 11 dígitos (`XXX.XXX.XXX-XX`) +- ✅ **Instância reutilizável**: Classe `CpfUtils` com configurações padrão opcionais (opções ou instâncias do formatador/gerador; instância do validador) +- ✅ **Entrada flexível**: `#format` e `#is_valid` aceitam `String` ou `Array` de strings (elementos concatenados na ordem) +- ✅ **Sobrescritas por chamada**: Padrões da instância mais um `Hash`/instância `*Options` por chamada **ou** sobrescritas por palavra-chave em `#format` / `#generate` (não ambos); `#is_valid` recebe apenas a entrada +- ✅ **Tratamento de erros**: Erros dos componentes propagam inalterados; esta gem define `CpfUtils::TypeMismatchError` e `CpfUtils::InvalidArgumentCombinationError` para uso indevido da API + +## Instalação + +Instale a gem diretamente: + +```bash +gem install cpf-utilities +``` + +Ou adicione ao seu `Gemfile` e execute `bundle install`: + +```ruby +gem 'cpf-utilities' +``` + +Isso instala **`cpf-utilities`** junto com [`cpf-fmt`](https://rubygems.org/gems/cpf-fmt), [`cpf-gen`](https://rubygems.org/gems/cpf-gen) e [`cpf-val`](https://rubygems.org/gems/cpf-val). Você **não** precisa de `gem install` / linhas `gem` separados para os pacotes componentes ao usar **`cpf-utilities`**. + +## Require + +```ruby +require 'cpf-utilities' +``` + +## Início rápido + +Uso básico com helpers de classe (aliases de `CpfUtils::DEFAULT`): + +```ruby +require 'cpf-utilities' + +cpf = '12345678909' + +CpfUtils.format(cpf) # => "123.456.789-09" +CpfUtils.format(cpf, hidden: true) # => "123.***.***-**" +CpfUtils.format( # => "123456789_09" + cpf, + dot_key: '', + dash_key: '_' +) + +CpfUtils.generate # => ex.: "47844241055" (11 dígitos numéricos) +CpfUtils.generate(format: true) # => ex.: "478.442.410-55" +CpfUtils.generate(prefix: '528250911') # => ex.: "52825091138" + +CpfUtils.is_valid('12345678909') # => true +CpfUtils.is_valid('123.456.789-09') # => true +CpfUtils.is_valid('12345678900') # => false +``` + +## Utilização + +Você pode trabalhar destas formas equivalentes: + +1. **`CpfUtils.format` / `.generate` / `.is_valid`** — helpers de classe para chamadas rápidas (encaminham para `DEFAULT`). +2. **`CpfUtils::DEFAULT`** — singleton compartilhado mutável (o mesmo objeto usado pelos helpers de classe; em todo o processo / não isolado por thread). +3. **`CpfUtils.new`** — instância configurável com padrões compartilhados entre formatar, gerar e validar. +4. **Classes principais sob `CpfUtils`** — `CpfUtils::CpfFormatter`, `CpfUtils::CpfGenerator`, `CpfUtils::CpfValidator`. +5. **Módulos aninhados do pacote** — Options, helpers, erros e tipos via `CpfUtils::CpfFmt` / `CpfGen` / `CpfVal` (ex.: `CpfUtils::CpfFmt::CpfFormatterOptions`, `CpfUtils::CpfFmt.cpf_fmt`). +6. **Módulos irmãos na raiz** (ainda suportados) — `CpfFmt`, `CpfGen`, `CpfVal` inalterados. + +Todas as abordagens expõem as mesmas opções e comportamento. Para tabelas de opções exaustivas e detalhes específicos de cada componente, consulte o README de cada [pacote incluído](#pacotes-incluídos). + +### Opções do formatador + +Em `#format(cpf_input, options = nil, **keywords)`, todas as opções são opcionais: + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `hidden` | `Boolean` | `false` | Se `true`, mascara dígitos entre `hidden_start` e `hidden_end` com `hidden_key` | +| `hidden_key` | `String` | `'*'` | Caractere(s) usados para substituir os dígitos mascarados | +| `hidden_start` | `Integer` | `3` | Índice inicial (0–10, inclusivo) do intervalo a ocultar | +| `hidden_end` | `Integer` | `10` | Índice final (0–10, inclusivo) do intervalo a ocultar | +| `dot_key` | `String` | `'.'` | Delimitador de ponto (ex.: em `123.456.789`) | +| `dash_key` | `String` | `'-'` | Delimitador de hífen (ex.: antes dos dígitos verificadores `…-09`) | +| `escape` | `Boolean` | `false` | Se `true`, escapa caracteres especiais HTML no resultado | +| `encode` | `Boolean` | `false` | Se `true`, codifica o resultado para URL (similar ao `encodeURIComponent` do JavaScript) | +| `on_fail` | `Proc` / invocável | retorna `''` | Callback quando o tamanho da entrada sanitizada ≠ 11; o retorno é usado como resultado | + +### Opções do gerador + +Em `#generate(options = nil, **keywords)`, todas as opções são opcionais: + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | Se `true`, retorna o CPF gerado no formato padrão (`000.000.000-00`) | +| `prefix` | `String` | `''` | String inicial parcial (0–9 dígitos). Não-dígitos são removidos; os caracteres faltantes são gerados e os dígitos verificadores calculados. Prefixos com mais de 9 dígitos são truncados silenciosamente. | + +Regras do prefixo: a base (primeiros 9 dígitos) não pode ser todos zeros; 9 dígitos repetidos (ex.: `999999999`) também não são permitidos. + +### Helpers de classe (`CpfUtils.format` / `.generate` / `.is_valid`) + +Esses métodos de classe são aliases dos mesmos métodos em `CpfUtils::DEFAULT`. Prefira-os para chamadas pontuais: + +```ruby +CpfUtils.format('12345678909') +CpfUtils.generate(format: true) +CpfUtils.is_valid('12345678909') +``` + +### `CpfUtils::DEFAULT` (instância padrão) + +`CpfUtils::DEFAULT` é o singleton pré-construído e **mutável** por trás dos helpers de classe (paridade com o export padrão do JS / `cpf_utils` do Python). A configuração é **em todo o processo e compartilhada entre threads**: mutá-lo (ex.: `DEFAULT.formatter = …`) afeta chamadas seguintes a `CpfUtils.format` / `.generate` / `.is_valid` para todos os chamadores no processo. Prefira `CpfUtils.new` ou opções por chamada para trabalho concorrente ou isolado; instâncias personalizadas permanecem independentes de `DEFAULT`: + +```ruby +CpfUtils::DEFAULT.formatter = { dash_key: '|' } +CpfUtils.format('12345678909') # => "123.456.789|09" + +custom = CpfUtils.new +custom.format('12345678909') # => "123.456.789-09" (não afetado) +``` + +Métodos de instância em `DEFAULT` (e em qualquer instância de `CpfUtils`): + +- **`#format(cpf_input, options = nil, **keywords)`**: Formata uma string CPF ou array de strings. Delega ao formatador interno. A entrada deve ter 11 dígitos (após sanitização); caso contrário, `on_fail` é usado. +- **`#generate(options = nil, **keywords)`**: Gera um CPF válido. Delega ao gerador interno. +- **`#is_valid(cpf_input)`**: Retorna `true` se o CPF for válido. Delega ao validador interno. Sem opções por chamada — o validador de CPF não tem nenhuma. + +### `CpfUtils` (classe) + +Para formatador, gerador ou validador padrão personalizados, crie sua própria instância: + +```ruby +require 'cpf-utilities' + +utils = CpfUtils.new( + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true, prefix: '123' } +) + +utils.format('47844241055') # => "478.###.###-##" +utils.generate # => ex.: "123.456.789-09" +utils.is_valid('123.456.789-09') # => true + +# Acessar ou substituir instâncias internas +utils.formatter # => CpfFmt::CpfFormatter +utils.generator # => CpfGen::CpfGenerator +utils.validator # => CpfVal::CpfValidator +``` + +- **`CpfUtils.new(settings = nil, **keywords)`**: Configurações opcionais. Passe um `Hash` de settings com as chaves `:formatter`, `:generator` e/ou `:validator`, **ou** as mesmas chaves como argumentos nomeados — não ambos (passar ambos lança `CpfUtils::InvalidArgumentCombinationError`). Para `:formatter` / `:generator`, cada valor pode ser uma instância de componente, uma instância `*Options` (armazenada por referência — mutá-la depois afeta chamadas subsequentes sem sobrescrita por chamada), um `Hash` de opções, ou omitido/`nil` para os padrões. Para `:validator`, passe uma instância de `CpfVal::CpfValidator`, `nil` ou um objeto duck-typed — **não** um `Hash` de opções (não existe `CpfValidatorOptions`). +- **`#format(cpf_input, options = nil, **keywords)`**: Igual à instância padrão; opções por chamada sobrescrevem os padrões do formatador apenas nessa chamada. Passe um `Hash`/`CpfFmt::CpfFormatterOptions` **ou** sobrescritas por palavra-chave — não ambos. +- **`#generate(options = nil, **keywords)`**: Igual à instância padrão; opções por chamada sobrescrevem os padrões do gerador. Passe um `Hash`/`CpfGen::CpfGeneratorOptions` **ou** sobrescritas por palavra-chave — não ambos. +- **`#is_valid(cpf_input)`**: Igual à instância padrão. Sem opções por chamada. +- **`#formatter`**, **`#generator`**, **`#validator`**: Acessores (getters e setters) dos componentes internos. Os setters aceitam as mesmas formas do construtor. Para alterar uma única opção do formatador/gerador sem substituir a instância, mute as opções do componente (ex.: `utils.formatter.options.hidden = true`). + +Padrões da instância e sobrescritas por chamada: + +```ruby +require 'cpf-utilities' + +utils = CpfUtils.new( + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true } +) + +cpf = '12345678909' + +utils.format(cpf) # mascarado (padrões do formatador da instância) +utils.format(cpf, hidden: false) # só nesta chamada: sem máscara +utils.generate(format: false) # só nesta chamada: saída compacta +utils.is_valid(cpf) # => true +``` + +As opções também podem ser passadas como `Hash` (ou instância de opções) em `#format` / `#generate` — sem sobrescritas por palavra-chave: + +```ruby +utils.format(cpf, { dash_key: '|' }) +utils.generate({ prefix: '12345', format: true }) +``` + +### Usando classes de componente e módulos aninhados + +Caminhos preferidos após `require 'cpf-utilities'`: + +```ruby +require 'cpf-utilities' + +# Classes principais na raiz da fachada +formatter = CpfUtils::CpfFormatter.new(hidden: true) +generator = CpfUtils::CpfGenerator.new(format: true) +validator = CpfUtils::CpfValidator.new + +formatter.format('47844241055') # => "478.***.***-**" + +# Options, helpers e erros sob os módulos aninhados do pacote +options = CpfUtils::CpfFmt::CpfFormatterOptions.new(dash_key: '|') +CpfUtils::CpfFmt.cpf_fmt('12345678909') # => "123.456.789-09" + +begin + CpfUtils::CpfFmt.cpf_fmt(12_345) +rescue CpfUtils::CpfFmt::TypeMismatchError + # tipo de entrada incorreto +end +``` + +Os irmãos na raiz continuam suportados (os mesmos objetos que os aninhados): + +```ruby +CpfFmt.cpf_fmt('12345678909', dash_key: '|') # => "123.456.789|09" +CpfGen.cpf_gen(format: true) # => ex.: "478.442.410-55" +CpfVal.cpf_val('12345678909') # => true +CpfFmt::CpfFormatter.new(hidden: true) +``` + +Consulte [`cpf-fmt`](../cpf-fmt/README.pt.md), [`cpf-gen`](../cpf-gen/README.pt.md) e [`cpf-val`](../cpf-val/README.pt.md) para detalhes completos de opções e erros. + +## API + +### Exportações + +Após `require 'cpf-utilities'`: + +- **`CpfUtils`**: Classe fachada para criar uma instância com configurações padrão opcionais de formatador, gerador e validador. +- **`CpfUtils.format` / `.generate` / `.is_valid`**: Helpers de classe que encaminham para `CpfUtils::DEFAULT`. +- **`CpfUtils::DEFAULT`**: Instância pré-construída mutável de `CpfUtils` (o mesmo objeto usado pelos helpers de classe). Em todo o processo / compartilhada entre threads — prefira `CpfUtils.new` ou opções por chamada sob concorrência. +- **`CpfUtils::VERSION`**: String da versão da gem. +- **Atalhos das classes principais**: `CpfUtils::CpfFormatter`, `CpfUtils::CpfGenerator`, `CpfUtils::CpfValidator` (os mesmos objetos das classes irmãs). +- **Módulos aninhados do pacote**: `CpfUtils::CpfFmt`, `CpfUtils::CpfGen`, `CpfUtils::CpfVal` — superfície completa do irmão (Options, helpers, erros, tipos). Options/helpers/erros **não** são aliasados na raiz de `CpfUtils`. +- **Módulos irmãos na raiz** (ainda suportados): `CpfFmt`, `CpfGen`, `CpfVal` — os mesmos objetos que os aninhados. + +### Erros e exceções + +`CpfUtils` define apenas erros de uso indevido da API para as regras de argumentos desta gem. Erros de componentes são lançados pelos pacotes incluídos e propagam inalterados. + +#### Definidos por `cpf-utilities` + +Os erros definidos por esta gem são apenas de **uso indevido da API** (tipo incorreto ou combinação inválida de argumentos). Todo erro customizado inclui o módulo marcador `CpfUtils::Error`. Esta gem **não** define `CpfUtils::DomainError` nem folhas de domínio — falhas de domínio vêm apenas dos [pacotes incluídos](#propagados-dos-pacotes-incluídos) e mantêm os namespaces desses pacotes (`CpfFmt::…`, `CpfGen::…`, `CpfVal::…`). + +`rescue CpfUtils::Error` captura **apenas** erros que esta gem lança. **Não** captura erros de componentes que propagam inalterados. + +##### Resumo + +| Classe | Herda de | Categoria | Condição de disparo | +|--------|----------|-----------|---------------------| +| `CpfUtils::InvalidArgumentCombinationError` | `CpfUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include CpfUtils::Error`) | Uso indevido da API | Construtor: `Hash` de settings não-`nil` com qualquer argumento nomeado não-`nil`; ou `#format`/`#generate`/helpers de classe: `Hash`/instância `*Options` de options não-`nil` com qualquer argumento nomeado não-`nil` | +| `CpfUtils::TypeMismatchError` | `CpfUtils::TypeMismatchError < TypeError < StandardError` (+ `include CpfUtils::Error`) | Uso indevido da API | Argumento `settings` não-`nil` de `CpfUtils.new` não é um `Hash` | + +##### `CpfUtils::Error` (módulo marcador) + +- **Herança:** módulo marcador misturado em todo erro customizado que esta gem lança via `include` (não é uma classe). +- **Categoria:** N/A (apenas alvo de `rescue`) — não é um modo de falha por si só. +- **Quando é lançado:** Nunca é lançado diretamente; incluído em todo erro customizado que esta gem lança. +- **Exemplo:** N/A +- **Como resgatá-lo:** + +```ruby +rescue CpfUtils::Error + # TypeMismatchError e InvalidArgumentCombinationError apenas desta gem + # (não CpfFmt::*, CpfGen::* nem CpfVal::*) +``` + +##### `CpfUtils::TypeMismatchError` + +- **Herança:** `CpfUtils::TypeMismatchError < TypeError < StandardError` (inclui `CpfUtils::Error`) +- **Categoria:** Uso indevido da API — o chamador passou um valor do tipo errado. +- **Quando é lançado:** Quando `CpfUtils.new` recebe um argumento `settings` não-`nil` que não é um `Hash`. +- **Exemplo:** + +```ruby +CpfUtils.new('not-a-hash') # lança CpfUtils::TypeMismatchError +CpfUtils.new(false) # lança CpfUtils::TypeMismatchError (false é não-nil) +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfUtils::TypeMismatchError + # violação de contrato de tipo desta gem + +rescue TypeError + # erros nativos de tipo, incluindo TypeMismatchError desta gem +``` + +##### `CpfUtils::InvalidArgumentCombinationError` + +- **Herança:** `CpfUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (inclui `CpfUtils::Error`) +- **Categoria:** Uso indevido da API — o chamador misturou padrões de argumentos mutuamente exclusivos. +- **Quando é lançado:** Quando `CpfUtils.new` recebe ao mesmo tempo um `Hash` de settings não-`nil` e qualquer argumento nomeado não-`nil` (`formatter:`, `generator:`, `validator:`); ou quando `#format`, `#generate` ou os helpers de classe recebem ao mesmo tempo um `Hash`/instância `*Options` de options não-`nil` e qualquer argumento nomeado não-`nil`. `#is_valid` não tem caminho de options e não lança este erro. +- **Exemplo:** + +```ruby +CpfUtils.new({ formatter: { hidden: true } }, generator: { format: true }) +# lança CpfUtils::InvalidArgumentCombinationError + +CpfUtils.format('12345678909', { hidden: true }, dash_key: '|') +# lança CpfUtils::InvalidArgumentCombinationError +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfUtils::InvalidArgumentCombinationError + # combinação de assinatura inválida desta gem + +rescue ArgumentError + # erros nativos de argumento, incluindo InvalidArgumentCombinationError desta gem +``` + +##### Granularidade de rescue + +Cada nível é mostrado como exemplo isolado (não os una numa única escada de `rescue` — um handler nativo amplo tornaria as cláusulas mais estreitas inalcançáveis). + +```ruby +require 'cpf-utilities' + +# 1) Uma classe nativa — captura erros de uso indevido daquele tipo, +# inclusive outros TypeError/ArgumentError já tratados no código do consumidor. +begin + CpfUtils.new('not-a-hash') +rescue TypeError + # CpfUtils::TypeMismatchError e qualquer outro TypeError (da biblioteca ou não) +end + +begin + CpfUtils.new({ formatter: { hidden: true } }, generator: { format: true }) +rescue ArgumentError + # CpfUtils::InvalidArgumentCombinationError e qualquer outro ArgumentError (da biblioteca ou não) +end +``` + +```ruby +require 'cpf-utilities' + +# 2) DomainError dos pacotes — esta gem não define DomainError; falhas de domínio +# vêm dos pacotes de componente e mantêm esses namespaces (ex.: CpfFmt). +begin + CpfUtils.new.format('12345678909', hidden_start: -1) +rescue CpfFmt::DomainError + # CpfFmt::OutOfRangeError, CpfFmt::ValidationError e outras subclasses de DomainError +end +``` + +```ruby +require 'cpf-utilities' + +# 3) CpfUtils::Error — captura tudo o que esta gem lança, independentemente da ancestralidade nativa. +# Não captura erros CpfFmt::*, CpfGen::* nem CpfVal::*. +begin + CpfUtils.new('not-a-hash') +rescue CpfUtils::Error + # todo erro customizado que inclui CpfUtils::Error +end +``` + +```ruby +require 'cpf-utilities' + +# 4) Classe folha específica — captura apenas aquele modo de falha. +begin + CpfUtils.new('not-a-hash') +rescue CpfUtils::TypeMismatchError + # apenas CpfUtils::TypeMismatchError +end +``` + +#### Propagados dos pacotes incluídos + +Os erros de componentes mantêm os namespaces dos pacotes e propagam inalterados pela fachada (e pelas APIs aninhadas / irmãos na raiz). Cada pacote também expõe um módulo marcador `*::Error` para rescue em toda a biblioteca. **Dados** de CPF inválidos em `#is_valid` retornam `false` (sem raise de domínio). Falha de comprimento na formatação **não** é lançada por `#format` — é entregue a **`on_fail`** como `CpfFmt::InvalidLengthError` (`on_fail` padrão retorna `''`). + +##### Resumo + +| Classe | Herda de | Categoria | Condição de disparo | +|--------|----------|-----------|---------------------| +| `CpfFmt::InvalidArgumentCombinationError` | `CpfFmt::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include CpfFmt::Error`) | Uso indevido da API | Instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` em `CpfFormatter` / `cpf_fmt` | +| `CpfFmt::TypeMismatchError` | `CpfFmt::TypeMismatchError < TypeError < StandardError` (+ `include CpfFmt::Error`) | Uso indevido da API | Entrada de CPF ou opção do formatador com tipo errado (ou retorno de `on_fail` que não é `String`) | +| `CpfGen::InvalidArgumentCombinationError` | `CpfGen::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include CpfGen::Error`) | Uso indevido da API | Instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` em `CpfGenerator` / `cpf_gen` | +| `CpfGen::TypeMismatchError` | `CpfGen::TypeMismatchError < TypeError < StandardError` (+ `include CpfGen::Error`) | Uso indevido da API | Opção do gerador (`format` / `prefix`) com tipo errado | +| `CpfVal::TypeMismatchError` | `CpfVal::TypeMismatchError < TypeError < StandardError` (+ `include CpfVal::Error`) | Uso indevido da API | Entrada de CPF não é `String` nem `Array` de strings | +| `CpfFmt::InvalidLengthError` | `CpfFmt::InvalidLengthError < CpfFmt::DomainError < RangeError < StandardError` (+ `include CpfFmt::Error`) | Erro de domínio | Comprimento sanitizado ≠ 11 — **passado a `on_fail`**, não lançado por `#format` | +| `CpfFmt::OutOfRangeError` | `CpfFmt::OutOfRangeError < CpfFmt::DomainError < RangeError < StandardError` (+ `include CpfFmt::Error`) | Erro de domínio | `hidden_start` / `hidden_end` fora de `0`–`10` | +| `CpfFmt::ValidationError` | `CpfFmt::ValidationError < CpfFmt::DomainError < RangeError < StandardError` (+ `include CpfFmt::Error`) | Erro de domínio | `hidden_key` / `dot_key` / `dash_key` contém caractere proibido | +| `CpfGen::ValidationError` | `CpfGen::ValidationError < CpfGen::DomainError < RangeError < StandardError` (+ `include CpfGen::Error`) | Erro de domínio | `prefix` inelegível (base zerada ou 9 dígitos repetidos) | + +##### `CpfFmt::DomainError` + +- **Herança:** `CpfFmt::DomainError < RangeError < StandardError` (inclui `CpfFmt::Error`) +- **Categoria:** Erro de domínio — ancestral das folhas de domínio do formatador. +- **Quando é lançado:** Não é lançado diretamente; alvo de rescue para `OutOfRangeError`, `ValidationError` e `InvalidLengthError` re-lançado. +- **Exemplo:** Prefira resgatar uma folha, ou `CpfFmt::DomainError` para todas as falhas de domínio do formatador. +- **Como resgatá-lo:** + +```ruby +rescue CpfFmt::DomainError + # OutOfRangeError, ValidationError, InvalidLengthError (se re-lançado de on_fail) +``` + +##### `CpfFmt::TypeMismatchError` + +- **Herança:** `CpfFmt::TypeMismatchError < TypeError < StandardError` (inclui `CpfFmt::Error`) +- **Categoria:** Uso indevido da API — tipo errado para entrada de CPF ou opção do formatador. +- **Quando é lançado:** Quando `#format` / `cpf_fmt` recebe entrada que não é `String` / `Array`, uma opção tem tipo errado, ou `on_fail` não retorna `String`. +- **Exemplo:** + +```ruby +CpfUtils.new.format(12_345) # lança CpfFmt::TypeMismatchError +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfFmt::TypeMismatchError + # violação de contrato de tipo do formatador + +rescue TypeError + # erros nativos de tipo, incluindo CpfFmt::TypeMismatchError +``` + +##### `CpfFmt::InvalidArgumentCombinationError` + +- **Herança:** `CpfFmt::InvalidArgumentCombinationError < ArgumentError < StandardError` (inclui `CpfFmt::Error`) +- **Categoria:** Uso indevido da API — `options` e keywords misturados na API do formatador. +- **Quando é lançado:** Por `CpfFmt::CpfFormatter` / `CpfFmt.cpf_fmt` quando uma instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` são passados juntos. (A fachada lança `CpfUtils::InvalidArgumentCombinationError` para o mesmo padrão em `CpfUtils#format`.) +- **Exemplo:** + +```ruby +CpfFmt::CpfFormatter.new({ dash_key: '_' }, hidden: true) +# lança CpfFmt::InvalidArgumentCombinationError +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfFmt::InvalidArgumentCombinationError + # combinação de assinatura inválida do formatador + +rescue ArgumentError + # erros nativos de argumento, incluindo este +``` + +##### `CpfFmt::InvalidLengthError` (entregue via callback) + +- **Herança:** `CpfFmt::InvalidLengthError < CpfFmt::DomainError < RangeError < StandardError` (inclui `CpfFmt::Error`) +- **Categoria:** Erro de domínio — comprimento sanitizado do CPF não é exatamente 11. +- **Quando é lançado:** **Não é lançado** por `#format` / `cpf_fmt`; é construído e passado como segundo argumento de `on_fail`. +- **Exemplo:** + +```ruby +custom_fail = ->(value, error) { + error # => # + "CPF inválido: #{value}" +} + +CpfUtils.new.format('123', on_fail: custom_fail) # => "CPF inválido: 123" +CpfUtils.new.format('123') # => "" (on_fail padrão) +``` + +- **Como resgatá-lo:** Trate dentro de `on_fail` (típico), ou faça rescue se re-lançar: + +```ruby +rescue CpfFmt::InvalidLengthError + # esta violação exata de comprimento + +rescue CpfFmt::DomainError + # falhas de domínio com raiz em RangeError de cpf-fmt +``` + +##### `CpfFmt::OutOfRangeError` + +- **Herança:** `CpfFmt::OutOfRangeError < CpfFmt::DomainError < RangeError < StandardError` (inclui `CpfFmt::Error`) +- **Categoria:** Erro de domínio — `hidden_start` / `hidden_end` fora de `0`–`10`. +- **Quando é lançado:** Ao construir ou aplicar opções do formatador com índice de ocultação fora da faixa. +- **Exemplo:** + +```ruby +CpfUtils.new.format('12345678909', hidden_start: -1) # lança CpfFmt::OutOfRangeError +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfFmt::OutOfRangeError + # esta violação exata de faixa + +rescue CpfFmt::DomainError + # falhas de domínio com raiz em RangeError de cpf-fmt +``` + +##### `CpfFmt::ValidationError` + +- **Herança:** `CpfFmt::ValidationError < CpfFmt::DomainError < RangeError < StandardError` (inclui `CpfFmt::Error`) +- **Categoria:** Erro de domínio — opção de chave com caractere proibido. +- **Quando é lançado:** Quando `hidden_key`, `dot_key` ou `dash_key` contém um caractere proibido. +- **Exemplo:** + +```ruby +CpfUtils.new(formatter: { dot_key: 'å' }) # lança CpfFmt::ValidationError +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfFmt::ValidationError + # esta falha exata de validação de domínio + +rescue CpfFmt::DomainError + # falhas de domínio com raiz em RangeError de cpf-fmt +``` + +##### `CpfGen::DomainError` + +- **Herança:** `CpfGen::DomainError < RangeError < StandardError` (inclui `CpfGen::Error`) +- **Categoria:** Erro de domínio — ancestral das folhas de domínio do gerador. +- **Quando é lançado:** Não é lançado diretamente; alvo de rescue para `CpfGen::ValidationError`. +- **Exemplo:** Prefira `rescue CpfGen::ValidationError` ou `CpfGen::DomainError`. +- **Como resgatá-lo:** + +```ruby +rescue CpfGen::DomainError + # ValidationError e outras subclasses de DomainError de cpf-gen +``` + +##### `CpfGen::TypeMismatchError` + +- **Herança:** `CpfGen::TypeMismatchError < TypeError < StandardError` (inclui `CpfGen::Error`) +- **Categoria:** Uso indevido da API — tipo errado para opção do gerador. +- **Quando é lançado:** Quando `format` ou `prefix` tem o tipo de runtime errado. +- **Exemplo:** + +```ruby +CpfUtils.new.generate(prefix: 123) # lança CpfGen::TypeMismatchError +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfGen::TypeMismatchError + # violação de contrato de tipo do gerador + +rescue TypeError + # erros nativos de tipo, incluindo CpfGen::TypeMismatchError +``` + +##### `CpfGen::InvalidArgumentCombinationError` + +- **Herança:** `CpfGen::InvalidArgumentCombinationError < ArgumentError < StandardError` (inclui `CpfGen::Error`) +- **Categoria:** Uso indevido da API — `options` e keywords misturados na API do gerador. +- **Quando é lançado:** Por `CpfGen::CpfGenerator` / `CpfGen.cpf_gen` quando uma instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` são passados juntos. (A fachada lança `CpfUtils::InvalidArgumentCombinationError` para o mesmo padrão em `CpfUtils#generate`.) +- **Exemplo:** + +```ruby +CpfGen::CpfGenerator.new({ format: true }, prefix: '123') +# lança CpfGen::InvalidArgumentCombinationError +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfGen::InvalidArgumentCombinationError + # combinação de assinatura inválida do gerador + +rescue ArgumentError + # erros nativos de argumento, incluindo este +``` + +##### `CpfGen::ValidationError` + +- **Herança:** `CpfGen::ValidationError < CpfGen::DomainError < RangeError < StandardError` (inclui `CpfGen::Error`) +- **Categoria:** Erro de domínio — `prefix` inelegível. +- **Quando é lançado:** Quando `prefix` é base zerada (`'000000000'`) ou 9 dígitos repetidos (ex.: `'999999999'`). +- **Exemplo:** + +```ruby +CpfUtils.new.generate(prefix: '000000000') # lança CpfGen::ValidationError +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfGen::ValidationError + # esta falha exata de validação de domínio + +rescue CpfGen::DomainError + # falhas de domínio com raiz em RangeError de cpf-gen +``` + +##### `CpfVal::TypeMismatchError` + +- **Herança:** `CpfVal::TypeMismatchError < TypeError < StandardError` (inclui `CpfVal::Error`) +- **Categoria:** Uso indevido da API — tipo errado para entrada de CPF. +- **Quando é lançado:** Quando `#is_valid` / `cpf_val` recebe valor que não é `String` nem `Array` de strings (incluindo elemento não-string no array). **Dados** de CPF inválidos retornam `false` e não lançam. +- **Exemplo:** + +```ruby +CpfUtils.new.is_valid(12_345_678_909) # lança CpfVal::TypeMismatchError +CpfUtils.new.is_valid('12345678900') # => false (dados inválidos, sem raise) +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfVal::TypeMismatchError + # violação de contrato de tipo do validador + +rescue TypeError + # erros nativos de tipo, incluindo CpfVal::TypeMismatchError +``` + +### Pacotes incluídos + +| Pacote | Principais recursos | README | +|--------|---------------------|--------| +| [`cpf-fmt`](https://rubygems.org/gems/cpf-fmt) | `CpfFmt::CpfFormatter`, `CpfFmt::CpfFormatterOptions`, `CpfFmt.cpf_fmt` | [docs](../cpf-fmt/README.pt.md) | +| [`cpf-gen`](https://rubygems.org/gems/cpf-gen) | `CpfGen::CpfGenerator`, `CpfGen::CpfGeneratorOptions`, `CpfGen.cpf_gen` | [docs](../cpf-gen/README.pt.md) | +| [`cpf-val`](https://rubygems.org/gems/cpf-val) | `CpfVal::CpfValidator`, `CpfVal.cpf_val` | [docs](../cpf-val/README.pt.md) | + +Todos os pacotes acima são instalados como dependências de **`cpf-utilities`**. Para tabelas de opções exaustivas, listas de exceções e comportamento em casos extremos, consulte o README de cada pacote. + +## Contribuição e suporte + +Contribuições são bem-vindas! Consulte as [Diretrizes de contribuição](https://github.com/LacusSolutions/br-utils-ruby/blob/main/CONTRIBUTING.md). Se o projeto for útil para você, considere: + +- ⭐ Dar uma estrela no repositório +- 🤝 Contribuir com código +- 💡 [Sugerir novas funcionalidades](https://github.com/LacusSolutions/br-utils-ruby/issues) +- 🐛 [Reportar bugs](https://github.com/LacusSolutions/br-utils-ruby/issues) + +## Licença + +Este projeto está sob a licença MIT — veja o arquivo [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE). + +## Changelog + +Veja o [CHANGELOG](./CHANGELOG.md) para alterações e histórico de versões. + +--- + +Feito com ❤️ por [Lacus Solutions](https://github.com/LacusSolutions) diff --git a/packages/cpf-utilities/cpf-utilities.gemspec b/packages/cpf-utilities/cpf-utilities.gemspec index 99a273a..1eee90f 100644 --- a/packages/cpf-utilities/cpf-utilities.gemspec +++ b/packages/cpf-utilities/cpf-utilities.gemspec @@ -6,14 +6,17 @@ Gem::Specification.new do |spec| spec.name = 'cpf-utilities' spec.version = CpfUtils::VERSION spec.authors = ['Julio L. Muller'] - spec.summary = 'CPF utilities: format, generate, validate (Brazilian personal ID)' + spec.email = ['juliolmuller@outlook.com'] + spec.summary = "Utilities to deal with CPF (Brazilian Individual's Taxpayer ID)" + spec.description = "Utilities to deal with CPF (Brazilian Individual's Taxpayer ID)" spec.homepage = 'https://github.com/LacusSolutions/br-utils-ruby' spec.license = 'MIT' spec.required_ruby_version = '>= 3.1' + spec.metadata['source_code_uri'] = spec.homepage spec.metadata['rubygems_mfa_required'] = 'true' spec.files = Dir['src/**/*'] + ['LICENSE', 'README.md', 'README.pt.md', 'CHANGELOG.md'] spec.require_paths = ['src'] - spec.add_dependency 'cpf-fmt', '>= 0' - spec.add_dependency 'cpf-gen', '>= 0' - spec.add_dependency 'cpf-val', '>= 0' + spec.add_dependency 'cpf-fmt', '>= 1.0.0', '< 1.1.0' + spec.add_dependency 'cpf-gen', '>= 1.0.0', '< 1.1.0' + spec.add_dependency 'cpf-val', '>= 1.0.0', '< 1.1.0' end diff --git a/packages/cpf-utilities/src/cpf-utilities.rb b/packages/cpf-utilities/src/cpf-utilities.rb index b9a5f14..b44b6ab 100644 --- a/packages/cpf-utilities/src/cpf-utilities.rb +++ b/packages/cpf-utilities/src/cpf-utilities.rb @@ -5,8 +5,22 @@ require 'cpf-val' require_relative 'cpf-utilities/version' -module CpfUtils - def self.hello - 'cpf-utils' - end -end +# Entry point for the +cpf-utilities+ gem. +# +# Loads sibling packages (+cpf-fmt+, +cpf-gen+, +cpf-val+) and defines the +# {CpfUtils} façade class. +version.rb+ declares the placeholder class so the +# gemspec can read {CpfUtils::VERSION}; later files reopen that same class. +# +# Two-tier access after +require 'cpf-utilities'+: +# +# - *Main shortcuts* at the façade root: {CpfUtils::CpfFormatter}, +# {CpfUtils::CpfGenerator}, {CpfUtils::CpfValidator}. +# - *Package nests* for the full sibling surface (Options, helpers, errors, +# types): {CpfUtils::CpfFmt}, {CpfUtils::CpfGen}, {CpfUtils::CpfVal} +# (same objects as +::CpfFmt+, +::CpfGen+, +::CpfVal+). +# - Root siblings (+CpfFmt+, +CpfGen+, +CpfVal+) remain supported unchanged. +require_relative 'cpf-utilities/errors' +require_relative 'cpf-utilities/cpf_utils' +require_relative 'cpf-utilities/cpf_fmt' +require_relative 'cpf-utilities/cpf_gen' +require_relative 'cpf-utilities/cpf_val' diff --git a/packages/cpf-utilities/src/cpf-utilities/cpf_fmt.rb b/packages/cpf-utilities/src/cpf-utilities/cpf_fmt.rb new file mode 100644 index 0000000..4feb1cd --- /dev/null +++ b/packages/cpf-utilities/src/cpf-utilities/cpf_fmt.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class CpfUtils + # Nested package module — same object as +::CpfFmt+ (Options, helpers, errors, types). + CpfFmt = ::CpfFmt + + CpfFormatter = CpfFmt::CpfFormatter + CpfFormatterOptions = CpfFmt::CpfFormatterOptions + CpfFormatterError = CpfFmt::Error +end diff --git a/packages/cpf-utilities/src/cpf-utilities/cpf_gen.rb b/packages/cpf-utilities/src/cpf-utilities/cpf_gen.rb new file mode 100644 index 0000000..0bedb1a --- /dev/null +++ b/packages/cpf-utilities/src/cpf-utilities/cpf_gen.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class CpfUtils + # Nested package module — same object as +::CpfGen+ (Options, helpers, errors, types). + CpfGen = ::CpfGen + + CpfGenerator = CpfGen::CpfGenerator + CpfGeneratorOptions = CpfGen::CpfGeneratorOptions + CpfGeneratorError = CpfGen::Error +end diff --git a/packages/cpf-utilities/src/cpf-utilities/cpf_utils.rb b/packages/cpf-utilities/src/cpf-utilities/cpf_utils.rb new file mode 100644 index 0000000..04aca90 --- /dev/null +++ b/packages/cpf-utilities/src/cpf-utilities/cpf_utils.rb @@ -0,0 +1,395 @@ +# frozen_string_literal: true + +require 'cpf-fmt' +require 'cpf-gen' +require 'cpf-val' + +require_relative 'errors' + +# Unified API for CPF (Cadastro da Pessoa Física) formatting, generation, and +# validation. Wraps a configurable formatter, generator, and validator so you +# can format, generate, and validate CPF values from a single instance. +# +# Public API: +# +# - {CpfUtils.format}, {CpfUtils.generate}, {CpfUtils.is_valid} — class helpers +# that alias {CpfUtils::DEFAULT} (preferred quick path) +# - {CpfUtils::DEFAULT} — mutable process-wide singleton (JS/Python parity; not +# thread-isolated — prefer {.new} / per-call options under concurrency) +# - {CpfUtils#format}, {CpfUtils#generate}, {CpfUtils#is_valid} — instance API +# - {CpfUtils::VERSION} +# - {CpfUtils::InvalidArgumentCombinationError} (API misuse) +# +# Two-tier access: main-class shortcuts ({CpfUtils::CpfFormatter}, etc.) and +# nested package modules ({CpfUtils::CpfFmt}, etc.). Root siblings {CpfFmt}, +# {CpfGen}, and {CpfVal} remain loadable after +require 'cpf-utilities'+. +# +# Mutating {CpfUtils::DEFAULT} (e.g. via setters) affects subsequent class-helper +# calls process-wide (shared across threads). Prefer {CpfUtils.new} or per-call +# options for concurrent or isolated work. Custom instances are independent of +# +DEFAULT+. +# +# @example +# require 'cpf-utilities' +# +# CpfUtils.format('12345678909') # => "123.456.789-09" +# CpfUtils.generate(format: true) # => e.g. "529.982.247-25" +# CpfUtils.is_valid('52998224725') # => true +class CpfUtils + SETTINGS_KEYS = %i[formatter generator validator].freeze + + FORMATTER_OPTION_KEYS = CpfFmt::CpfFormatterOptions::OPTION_KEYS + GENERATOR_OPTION_KEYS = CpfGen::CpfGeneratorOptions::OPTION_KEYS + + private_constant :SETTINGS_KEYS, :FORMATTER_OPTION_KEYS, :GENERATOR_OPTION_KEYS + + # Internal helpers for constructing owned component instances and merging + # settings / per-call option arguments. + module Helpers + module_function + + def resolve_settings(settings, keywords) + keyword_settings = compact_settings(keywords) + raise_ambiguous_settings! if !settings.nil? && !keyword_settings.empty? + return normalize_settings(settings) unless settings.nil? + + keyword_settings + end + + def normalize_settings(settings) + raise TypeMismatchError, "CpfUtils settings must be a Hash. Got #{settings.class}." unless settings.is_a?(Hash) + + SETTINGS_KEYS.each_with_object({}) do |key, resolved| + if settings.key?(key) + resolved[key] = settings[key] + elsif settings.key?(key.to_s) + resolved[key] = settings[key.to_s] + end + end + end + + def compact_settings(keywords) + SETTINGS_KEYS.each_with_object({}) do |key, resolved| + value = keywords[key] + resolved[key] = value unless value.nil? + end + end + + def resolve_formatter(value) + return CpfFmt::CpfFormatter.new if value.nil? + return value if value.is_a?(CpfFmt::CpfFormatter) + return CpfFmt::CpfFormatter.new(value) if value.is_a?(CpfFmt::CpfFormatterOptions) || value.is_a?(Hash) + + # Duck-typed / test doubles: use the given object by reference (Python parity). + value + end + + def resolve_generator(value) + return CpfGen::CpfGenerator.new if value.nil? + return value if value.is_a?(CpfGen::CpfGenerator) + return CpfGen::CpfGenerator.new(value) if value.is_a?(CpfGen::CpfGeneratorOptions) || value.is_a?(Hash) + + # Duck-typed / test doubles: use the given object by reference (Python parity). + value + end + + def resolve_validator(value) + return CpfVal::CpfValidator.new if value.nil? + return value if value.is_a?(CpfVal::CpfValidator) + + # Duck-typed / test doubles: use the given object by reference (Python parity). + # Unlike CNPJ, CPF has no validator Options class — Hash is not accepted as options. + value + end + + def ensure_exclusive_options!(options, keywords, option_keys) + return if options.nil? + return if keywords.none? { |_key, value| !value.nil? } + + raise_ambiguous_options!(option_keys) + end + + def compact_keyword_overrides(keywords, option_keys) + option_keys.each_with_object({}) do |key, overrides| + value = keywords[key] + overrides[key] = value unless value.nil? + end + end + + def raise_ambiguous_settings! + option_keywords = SETTINGS_KEYS.map { |key| "#{key}:" }.join(', ') + + raise InvalidArgumentCombinationError, + 'Pass either a settings Hash to `settings`, or keyword arguments ' \ + "(#{option_keywords}), not both." + end + + def raise_ambiguous_options!(option_keys) + option_keywords = option_keys.map { |key| "#{key}:" }.join(', ') + + raise InvalidArgumentCombinationError, + "Pass either an options instance/Hash to `options`, or keyword arguments (#{option_keywords}), " \ + 'not both.' + end + end + private_constant :Helpers + + # Creates a new {CpfUtils} with customized options. Each of +:formatter+ and + # +:generator+ can be omitted (defaults are used), or provided as an instance, + # an options object, or a plain {Hash} of options. +:validator+ accepts an + # instance, +nil+, or a duck-typed object — not an options Hash. + # + # When a component instance is passed, it is used directly (same reference). + # When +nil+ is passed for a component, a new instance with default options is + # created. + # + # +settings+ and the keyword arguments are never merged with each other: when + # +settings+ is given (a {Hash} with +:formatter+, +:generator+, and/or + # +:validator+ keys), it alone determines the components; otherwise, the + # components are built exclusively from the keyword arguments. Passing + # +settings+ together with any non-+nil+ keyword argument raises + # {InvalidArgumentCombinationError} instead of silently ignoring the keywords. + # + # @param settings [Hash, nil] settings Hash with +:formatter+, +:generator+, + # and/or +:validator+ keys (+:formatter+/+:generator+: instance, options + # instance, options Hash, or +nil+; +:validator+: instance, +nil+, or + # duck-typed object — not an options Hash) + # @param keywords [Hash] +:formatter+, +:generator+, +:validator+ (mutually + # exclusive with +settings+) + # @raise [InvalidArgumentCombinationError] if +settings+ and a keyword argument + # are both given + # @raise [TypeMismatchError] if +settings+ is given and is not a +Hash+ + # @raise [CpfFmt::TypeMismatchError] if formatter options have an invalid type + # @raise [CpfFmt::OutOfRangeError] if formatter +hidden_start+ or +hidden_end+ + # are out of valid range + # @raise [CpfFmt::ValidationError] if any formatter key option contains a + # disallowed character + # @raise [CpfGen::TypeMismatchError] if generator options have an invalid type + # @raise [CpfGen::ValidationError] if generator +prefix+ is invalid + def initialize(settings = nil, **keywords) + resolved = Helpers.resolve_settings(settings, keywords) + + @formatter = Helpers.resolve_formatter(resolved[:formatter]) + @generator = Helpers.resolve_generator(resolved[:generator]) + @validator = Helpers.resolve_validator(resolved[:validator]) + end + + # Returns the formatter used by this utils instance. + # + # @return [CpfFmt::CpfFormatter] + attr_reader :formatter + + # Returns the generator used by this utils instance. + # + # @return [CpfGen::CpfGenerator] + attr_reader :generator + + # Returns the validator used by this utils instance. + # + # @return [CpfVal::CpfValidator] + attr_reader :validator + + # Sets the active formatter used by this utils instance. + # + # It is flexible and can handle any of these inputs: + # + # 1. A complete new instance of {CpfFmt::CpfFormatter} + # 2. An instance of {CpfFmt::CpfFormatterOptions} + # 3. A partial {Hash} with options for the formatter + # 4. +nil+ creates a brand new {CpfFmt::CpfFormatter} with default options + # + # Note that this resets the formatter instance completely. Any previous + # options will be overridden. To alter only a single option or a few options + # of the existing instance, access it directly (e.g. + # +utils.formatter.options.hidden = true+). + # + # @param value [CpfFmt::CpfFormatter, CpfFmt::CpfFormatterOptions, Hash, nil] + # @raise [CpfFmt::TypeMismatchError] if options have an invalid type + # @raise [CpfFmt::OutOfRangeError] if +hidden_start+ or +hidden_end+ are out + # of valid range + # @raise [CpfFmt::ValidationError] if any key option contains a disallowed + # character + def formatter=(value) + @formatter = Helpers.resolve_formatter(value) + end + + # Sets the active generator used by this utils instance. + # + # It is flexible and can handle any of these inputs: + # + # 1. A complete new instance of {CpfGen::CpfGenerator} + # 2. An instance of {CpfGen::CpfGeneratorOptions} + # 3. A partial {Hash} with options for the generator + # 4. +nil+ creates a brand new {CpfGen::CpfGenerator} with default options + # + # Note that this resets the generator instance completely. Any previous + # options will be overridden. To alter only a single option or a few options + # of the existing instance, access it directly (e.g. + # +utils.generator.options.format = true+). + # + # @param value [CpfGen::CpfGenerator, CpfGen::CpfGeneratorOptions, Hash, nil] + # @raise [CpfGen::TypeMismatchError] if options have an invalid type + # @raise [CpfGen::ValidationError] if +prefix+ is invalid + def generator=(value) + @generator = Helpers.resolve_generator(value) + end + + # Sets the active validator used by this utils instance. + # + # It is flexible and can handle any of these inputs: + # + # 1. A complete new instance of {CpfVal::CpfValidator} + # 2. +nil+ creates a brand new {CpfVal::CpfValidator} + # 3. A duck-typed object used by reference (test doubles) + # + # Note that this resets the validator instance completely. CPF has no + # validator options class — a +Hash+ is treated as a duck-typed object, not + # as options. + # + # @param value [CpfVal::CpfValidator, Object, nil] + def validator=(value) + @validator = Helpers.resolve_validator(value) + end + + # Formats a CPF value into a human-readable string. + # + # Normalizes and optionally masks, HTML-escapes, or URL-encodes the input. + # Delegates to the instance formatter; per-call options override the + # formatter's defaults for this call only. + # + # Input is normalized by stripping non-digit characters. If the result length + # is not exactly 11, the configured +on_fail+ callback is invoked with the + # original value and an error; its return value is used as the result. + # + # When valid, the result may be further transformed according to options: + # + # - If +hidden+ is +true+, characters between +hidden_start+ and +hidden_end+ + # (inclusive) are replaced with +hidden_key+. + # - If +escape+ is +true+, HTML special characters are escaped. + # - If +encode+ is +true+, the string is URL-encoded. + # + # +options+ and the keyword arguments are never merged with each other: when + # +options+ is given alone it is forwarded as the per-call override; otherwise + # any non-+nil+ keyword argument is forwarded. Passing +options+ together with + # any non-+nil+ keyword argument raises {InvalidArgumentCombinationError}. + # + # @param cpf_input [String, Array] CPF value as a string or array of + # strings + # @param options [CpfFmt::CpfFormatterOptions, Hash, nil] per-call overrides + # @param keywords [Hash] per-call option keyword overrides (mutually exclusive + # with +options+; see {CpfFmt::CpfFormatterOptions}) + # @return [String] formatted CPF string, or the +on_fail+ callback result + # @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument + # are both given + # @raise [CpfFmt::TypeMismatchError] if the input is not a +String+ or + # +Array+, or if any option has an invalid type + # @raise [CpfFmt::OutOfRangeError] if +hidden_start+ or +hidden_end+ are out + # of valid range + # @raise [CpfFmt::ValidationError] if any key option contains a disallowed + # character + def format(cpf_input, options = nil, **keywords) + Helpers.ensure_exclusive_options!(options, keywords, FORMATTER_OPTION_KEYS) + return @formatter.format(cpf_input, options) unless options.nil? + + keyword_overrides = Helpers.compact_keyword_overrides(keywords, FORMATTER_OPTION_KEYS) + return @formatter.format(cpf_input, **keyword_overrides) unless keyword_overrides.empty? + + @formatter.format(cpf_input) + end + + # Generates a valid 11-digit CPF, optionally with a prefix and formatting. + # + # Builds an 11-digit CPF from the configured +prefix+ (if any), a random + # sequence of digits, and two computed check digits. If +format+ is enabled, + # the result is returned as +XXX.XXX.XXX-XX+. + # + # Delegates to the instance generator; per-call options override the + # generator's defaults for this call only. + # + # +options+ and the keyword arguments are never merged with each other: when + # +options+ is given alone it is forwarded as the per-call override; otherwise + # any non-+nil+ keyword argument is forwarded. Passing +options+ together with + # any non-+nil+ keyword argument raises {InvalidArgumentCombinationError}. + # + # @param options [CpfGen::CpfGeneratorOptions, Hash, nil] per-call overrides + # @param keywords [Hash] per-call option keyword overrides (mutually exclusive + # with +options+; see {CpfGen::CpfGeneratorOptions}) + # @return [String] generated CPF + # @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument + # are both given + # @raise [CpfGen::TypeMismatchError] if any option has an invalid type + # @raise [CpfGen::ValidationError] if +prefix+ is invalid + def generate(options = nil, **keywords) + Helpers.ensure_exclusive_options!(options, keywords, GENERATOR_OPTION_KEYS) + return @generator.generate(options) unless options.nil? + + keyword_overrides = Helpers.compact_keyword_overrides(keywords, GENERATOR_OPTION_KEYS) + return @generator.generate(**keyword_overrides) unless keyword_overrides.empty? + + @generator.generate + end + + # Returns whether the given value is a valid CPF. + # + # Delegates to the instance validator. CPF has no per-call validator options. + # + # @param cpf_input [String, Array] CPF value as a string or array of + # strings + # @return [Boolean] +true+ when valid, +false+ otherwise + # @raise [CpfVal::TypeMismatchError] if the input is not a +String+ or + # +Array+ + # rubocop:disable Naming/PredicatePrefix -- public API matches JS/Python `is_valid` + def is_valid(cpf_input) + @validator.is_valid(cpf_input) + end + # rubocop:enable Naming/PredicatePrefix + + # Default {CpfUtils} instance with default formatter, generator, and + # validator options (parity with the JS default export / Python +cpf_utils+ + # singleton). Configuration is process-wide and shared across threads: + # mutating this instance (e.g. via setters) affects subsequent + # {CpfUtils.format}, {CpfUtils.generate}, and {CpfUtils.is_valid} calls for + # every caller in the process. Prefer {CpfUtils.new} or per-call options for + # threaded or isolated work. + DEFAULT = new + + class << self + # Formats a CPF using {DEFAULT} (alias of {CpfUtils#format} on that instance). + # + # @param cpf_input [String, Array] CPF value as a string or array of + # strings + # @param options [CpfFmt::CpfFormatterOptions, Hash, nil] per-call overrides + # @param keywords [Hash] per-call option keyword overrides (mutually exclusive + # with +options+) + # @return [String] formatted CPF string, or the +on_fail+ callback result + # @see CpfUtils#format + def format(cpf_input, options = nil, **keywords) + DEFAULT.format(cpf_input, options, **keywords) + end + + # Generates a valid CPF using {DEFAULT} (alias of {CpfUtils#generate} on that + # instance). + # + # @param options [CpfGen::CpfGeneratorOptions, Hash, nil] per-call overrides + # @param keywords [Hash] per-call option keyword overrides (mutually exclusive + # with +options+) + # @return [String] generated CPF + # @see CpfUtils#generate + def generate(options = nil, **keywords) + DEFAULT.generate(options, **keywords) + end + + # Validates a CPF using {DEFAULT} (alias of {CpfUtils#is_valid} on that + # instance). + # + # @param cpf_input [String, Array] CPF value as a string or array of + # strings + # @return [Boolean] +true+ when valid, +false+ otherwise + # @see CpfUtils#is_valid + # rubocop:disable Naming/PredicatePrefix -- public API matches instance `#is_valid` + def is_valid(cpf_input) + DEFAULT.is_valid(cpf_input) + end + # rubocop:enable Naming/PredicatePrefix + end +end diff --git a/packages/cpf-utilities/src/cpf-utilities/cpf_val.rb b/packages/cpf-utilities/src/cpf-utilities/cpf_val.rb new file mode 100644 index 0000000..67c32b0 --- /dev/null +++ b/packages/cpf-utilities/src/cpf-utilities/cpf_val.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class CpfUtils + # Nested package module — same object as +::CpfVal+ (helpers, errors, types). + CpfVal = ::CpfVal + + CpfValidator = CpfVal::CpfValidator + CpfValidatorError = CpfVal::Error +end diff --git a/packages/cpf-utilities/src/cpf-utilities/errors.rb b/packages/cpf-utilities/src/cpf-utilities/errors.rb new file mode 100644 index 0000000..746bf34 --- /dev/null +++ b/packages/cpf-utilities/src/cpf-utilities/errors.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class CpfUtils + # Marker module mixed into every custom error raised by this library. + # + # Use +rescue CpfUtils::Error+ to catch every library error regardless of + # native ancestry. Component packages raise their own error hierarchies; + # this gem only defines the misuse errors it raises itself. + module Error; end + + # API misuse error raised when an argument's runtime type does not match the + # type required by the API contract (for example, a non-Hash +settings+ value). + class TypeMismatchError < TypeError + include Error + end + + # API misuse error raised when the combination of provided arguments does not + # match any valid overload-style signature (for example, a settings/options + # Hash together with keyword overrides). + class InvalidArgumentCombinationError < ArgumentError + include Error + end +end diff --git a/packages/cpf-utilities/src/cpf-utilities/version.rb b/packages/cpf-utilities/src/cpf-utilities/version.rb index 20c36bb..6008e84 100644 --- a/packages/cpf-utilities/src/cpf-utilities/version.rb +++ b/packages/cpf-utilities/src/cpf-utilities/version.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true -module CpfUtils +# Placeholder class so the gemspec (and any early require of this file) can read +# {CpfUtils::VERSION}. The façade implementation reopens this class. +class CpfUtils + # Gem version string. Placeholder replaced at build/publish time. + # + # @return [String] VERSION = '0.0.0' end diff --git a/packages/cpf-utilities/tests/cpf_utilities.spec.rb b/packages/cpf-utilities/tests/cpf_utilities.spec.rb deleted file mode 100644 index 00d6ec1..0000000 --- a/packages/cpf-utilities/tests/cpf_utilities.spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe CpfUtils do - describe '.hello' do - it 'returns cpf-utils' do - expect(CpfUtils.hello).to eq('cpf-utils') - end - end -end diff --git a/packages/cpf-utilities/tests/cpf_utils.spec.rb b/packages/cpf-utilities/tests/cpf_utils.spec.rb new file mode 100644 index 0000000..76b6280 --- /dev/null +++ b/packages/cpf-utilities/tests/cpf_utils.spec.rb @@ -0,0 +1,1313 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Combined behavioural suite for CpfUtils (JS / PHP / Python reference tests). +# +# Dropped cases (not meaningful in Ruby): +# - js/packages/cpf-utils/tests/output.spec.ts — UMD/CJS/ESM bundles, .d.ts wiring, +# global variable attachment, and export-string parsing (JS packaging only). +# - JavaScript prototype spies (spyOn(CpfFormatter.prototype, ...)) — replaced with +# instance doubles that assert the same façade-forwarding premise. +# - PHP getFormatter() / getGenerator() / getValidator() accessor names — Ruby uses +# #formatter / #generator / #validator (JS/Python parity per AGENTS.md). +# - PHP CpfUtils constructor that always builds a fresh CpfValidator with no +# injection — Ruby accepts validator instances like JS/Python. +# - PHP prefix longer than 9 digits raises — JS/Python/Ruby truncate silently. +# - PHP permissive repeated-digit validation (00000000000 etc. true) — Ruby/JS/ +# Python reject those as invalid. +# - PHP default onFail returning the original string — Ruby/JS/Python return ''. +# - PHP string-only format/isValid inputs — Ruby accepts String or Array. +# - PHP native TypeError for bool/INF/closures on isValid — Ruby raises +# CpfVal::TypeMismatchError for non-String / non-Array inputs. +# - Python __slots__ / dynamic-attribute restriction — optional in Ruby; AGENTS.md +# does not require freezing or slot-like attribute locking. +# - Python None-kwargs forwarding quirk on format (forwarding nil keys when any +# keyword is present) — match cpf-fmt Ruby merge / XOR semantics instead. +# - Python dual-merge of options mapping + kwargs — Ruby uses options XOR keywords. +# - Deep sibling exception message / constructor smoke from python package.spec.py — +# those APIs belong to cpf-fmt / cpf-gen / cpf-val; this suite only asserts +# that requiring cpf-utilities loads those modules and that DEFAULT works. +# - CNPJ-only scenarios: slash_key, generator type, validator options / +# CpfValidatorOptions, alphanumeric / 14-character fixtures. + +def compact_options(**kwargs) + kwargs.compact +end + +def expect_options_containing(actual, expected) + expected.each do |key, value| + expect(actual[key]).to eq(value) + end +end + +FORMAT_FACTORIES = { + constructor_hash: lambda { |cpf, dot_key = nil, dash_key = nil| + utils = CpfUtils.new(formatter: compact_options(dot_key: dot_key, dash_key: dash_key)) + utils.format(cpf) + }, + constructor_options: lambda { |cpf, dot_key = nil, dash_key = nil| + options = CpfFmt::CpfFormatterOptions.new(compact_options(dot_key: dot_key, dash_key: dash_key)) + utils = CpfUtils.new(formatter: options) + utils.format(cpf) + }, + method_keywords: lambda { |cpf, dot_key = nil, dash_key = nil| + CpfUtils.new.format(cpf, dot_key: dot_key, dash_key: dash_key) + }, + method_options: lambda { |cpf, dot_key = nil, dash_key = nil| + options = CpfFmt::CpfFormatterOptions.new(compact_options(dot_key: dot_key, dash_key: dash_key)) + CpfUtils.new.format(cpf, options) + } +}.freeze + +GENERATE_FACTORIES = { + constructor_hash: lambda { |format: nil, prefix: nil| + utils = CpfUtils.new(generator: compact_options(format: format, prefix: prefix)) + utils.generate + }, + constructor_options: lambda { |format: nil, prefix: nil| + options = CpfGen::CpfGeneratorOptions.new(compact_options(format: format, prefix: prefix)) + utils = CpfUtils.new(generator: options) + utils.generate + }, + method_keywords: lambda { |format: nil, prefix: nil| + CpfUtils.new.generate(format: format, prefix: prefix) + }, + method_options: lambda { |format: nil, prefix: nil| + options = CpfGen::CpfGeneratorOptions.new(compact_options(format: format, prefix: prefix)) + CpfUtils.new.generate(options) + } +}.freeze + +IS_VALID_FACTORIES = { + default_instance: lambda { |cpf| + CpfUtils.new.is_valid(cpf) + }, + constructor_validator: lambda { |cpf| + utils = CpfUtils.new(validator: CpfVal::CpfValidator.new) + utils.is_valid(cpf) + } +}.freeze + +FORMAT_FACTORY_CONTEXTS = [ + ['when options are passed to the constructor as a Hash', :constructor_hash], + ['when options are passed to the constructor as CpfFormatterOptions', :constructor_options], + ['when options are passed to #format as keywords', :method_keywords], + ['when options are passed to #format as CpfFormatterOptions', :method_options] +].freeze + +GENERATE_FACTORY_CONTEXTS = [ + ['when options are passed to the constructor as a Hash', :constructor_hash], + ['when options are passed to the constructor as CpfGeneratorOptions', :constructor_options], + ['when options are passed to #generate as keywords', :method_keywords], + ['when options are passed to #generate as CpfGeneratorOptions', :method_options] +].freeze + +IS_VALID_FACTORY_CONTEXTS = [ + ['when using a default instance', :default_instance], + ['when a validator instance is passed to the constructor', :constructor_validator] +].freeze + +RSpec.describe CpfUtils do + def default_formatter_options_snapshot + CpfFmt::CpfFormatterOptions.new.all + end + + def default_generator_options_snapshot + CpfGen::CpfGeneratorOptions.new.all + end + + describe 'DEFAULT' do + it 'is an instance of CpfUtils' do + expect(described_class::DEFAULT).to be_a(described_class) + end + + it 'exposes format, generate, and is_valid' do + aggregate_failures do + expect(described_class::DEFAULT).to respond_to(:format) + expect(described_class::DEFAULT).to respond_to(:generate) + expect(described_class::DEFAULT).to respond_to(:is_valid) + end + end + end + + describe 'class helpers' do + it 'exposes format, generate, and is_valid' do + aggregate_failures do + expect(described_class).to respond_to(:format) + expect(described_class).to respond_to(:generate) + expect(described_class).to respond_to(:is_valid) + end + end + + context 'when calling through the class' do + it 'formats like DEFAULT' do + expect(described_class.format('12345678909')).to eq( + described_class::DEFAULT.format('12345678909') + ) + end + + it 'generates like DEFAULT' do + result = described_class.generate(prefix: '123456789') + expect(result).to match(/\A\d{11}\z/) + expect(described_class.is_valid(result)).to be(true) + end + + it 'validates like DEFAULT' do + aggregate_failures do + expect(described_class.is_valid('12345678909')).to eq( + described_class::DEFAULT.is_valid('12345678909') + ) + expect(described_class.is_valid('12345678900')).to eq( + described_class::DEFAULT.is_valid('12345678900') + ) + end + end + end + + context 'when DEFAULT is mutated' do + around do |example| + original_formatter = described_class::DEFAULT.formatter + example.run + described_class::DEFAULT.formatter = original_formatter + end + + it 'affects subsequent class helper calls' do + described_class::DEFAULT.formatter = { dash_key: '|' } + expect(described_class.format('12345678909')).to eq('123.456.789|09') + end + + it 'does not affect a custom instance' do + custom = described_class.new + described_class::DEFAULT.formatter = { dash_key: '|' } + expect(custom.format('12345678909')).to eq('123.456.789-09') + end + end + end + + describe 'loaded sibling packages' do + it 'makes cpf-fmt symbols available' do + aggregate_failures do + expect(defined?(CpfFmt::CpfFormatter)).to eq('constant') + expect(defined?(CpfFmt::CpfFormatterOptions)).to eq('constant') + expect(CpfFmt).to respond_to(:cpf_fmt) + end + end + + it 'makes cpf-gen symbols available' do + aggregate_failures do + expect(defined?(CpfGen::CpfGenerator)).to eq('constant') + expect(defined?(CpfGen::CpfGeneratorOptions)).to eq('constant') + expect(CpfGen).to respond_to(:cpf_gen) + end + end + + it 'makes cpf-val symbols available' do + aggregate_failures do + expect(defined?(CpfVal::CpfValidator)).to eq('constant') + expect(CpfVal).to respond_to(:cpf_val) + end + end + end + + describe 'two-tier CpfUtils re-exports' do + it 'nests sibling modules as the same objects' do + aggregate_failures do + expect(described_class::CpfFmt).to equal(CpfFmt) + expect(described_class::CpfGen).to equal(CpfGen) + expect(described_class::CpfVal).to equal(CpfVal) + end + end + + it 'aliases main cpf-fmt classes at the façade root' do + aggregate_failures do + expect(described_class::CpfFormatter).to equal(CpfFmt::CpfFormatter) + expect(described_class::CpfFormatterOptions).to equal(CpfFmt::CpfFormatterOptions) + expect(described_class::CpfFormatterError).to equal(CpfFmt::Error) + end + end + + it 'aliases main cpf-gen classes at the façade root' do + aggregate_failures do + expect(described_class::CpfGenerator).to equal(CpfGen::CpfGenerator) + expect(described_class::CpfGeneratorOptions).to equal(CpfGen::CpfGeneratorOptions) + expect(described_class::CpfGeneratorError).to equal(CpfGen::Error) + end + end + + it 'aliases main cpf-val classes at the façade root' do + aggregate_failures do + expect(described_class::CpfValidator).to equal(CpfVal::CpfValidator) + expect(described_class::CpfValidatorError).to equal(CpfVal::Error) + end + end + + it 'does not expose CpfValidatorOptions' do + expect(described_class.const_defined?(:CpfValidatorOptions, false)).to be(false) + end + + context 'with nested surface smoke' do + it 'exposes Options through the nest' do + options = described_class::CpfFmt::CpfFormatterOptions.new(hidden: true) + + expect(options.hidden).to be(true) + end + + it 'exposes helpers through the nest' do + expect(described_class::CpfFmt.cpf_fmt('12345678909')).to eq('123.456.789-09') + end + + it 'exposes an error class through the nest' do + expect(described_class::CpfFmt::OutOfRangeError).to equal(CpfFmt::OutOfRangeError) + end + end + end + + describe '#initialize' do + context 'when called with no arguments' do + subject(:utils) { described_class.new } + + it 'creates default component instances' do + aggregate_failures do + expect(utils.formatter).to be_a(CpfFmt::CpfFormatter) + expect(utils.generator).to be_a(CpfGen::CpfGenerator) + expect(utils.validator).to be_a(CpfVal::CpfValidator) + end + end + + it 'uses default component options' do + aggregate_failures do + expect_options_containing(utils.formatter.options.all, default_formatter_options_snapshot) + expect_options_containing(utils.generator.options.all, default_generator_options_snapshot) + end + end + end + + context 'when called with component instances' do + it 'uses the passed formatter directly' do + formatter = CpfFmt::CpfFormatter.new + utils = described_class.new(formatter: formatter) + + aggregate_failures do + expect(utils.formatter).to be_a(CpfFmt::CpfFormatter) + expect(utils.formatter).to equal(formatter) + end + end + + it 'uses the passed generator directly' do + generator = CpfGen::CpfGenerator.new + utils = described_class.new(generator: generator) + + aggregate_failures do + expect(utils.generator).to be_a(CpfGen::CpfGenerator) + expect(utils.generator).to equal(generator) + end + end + + it 'uses the passed validator directly' do + validator = CpfVal::CpfValidator.new + utils = described_class.new(validator: validator) + + aggregate_failures do + expect(utils.validator).to be_a(CpfVal::CpfValidator) + expect(utils.validator).to equal(validator) + end + end + + it 'uses all passed components directly' do + formatter = CpfFmt::CpfFormatter.new + generator = CpfGen::CpfGenerator.new + validator = CpfVal::CpfValidator.new + utils = described_class.new( + formatter: formatter, + generator: generator, + validator: validator + ) + + aggregate_failures do + expect(utils.formatter).to equal(formatter) + expect(utils.generator).to equal(generator) + expect(utils.validator).to equal(validator) + end + end + end + + context 'when called with options instances' do + it 'builds a formatter that keeps the options reference' do + formatter_options = CpfFmt::CpfFormatterOptions.new + utils = described_class.new(formatter: formatter_options) + + aggregate_failures do + expect(utils.formatter).to be_a(CpfFmt::CpfFormatter) + expect(utils.formatter.options).to equal(formatter_options) + end + end + + it 'builds a generator that keeps the options reference' do + generator_options = CpfGen::CpfGeneratorOptions.new + utils = described_class.new(generator: generator_options) + + aggregate_failures do + expect(utils.generator).to be_a(CpfGen::CpfGenerator) + expect(utils.generator.options).to equal(generator_options) + end + end + + it 'builds formatter and generator from the passed options' do + formatter_options = CpfFmt::CpfFormatterOptions.new + generator_options = CpfGen::CpfGeneratorOptions.new + utils = described_class.new( + formatter: formatter_options, + generator: generator_options + ) + + aggregate_failures do + expect(utils.formatter.options).to equal(formatter_options) + expect(utils.generator.options).to equal(generator_options) + expect(utils.validator).to be_a(CpfVal::CpfValidator) + end + end + + it 'reflects later mutations on shared options' do + generator_options = CpfGen::CpfGeneratorOptions.new(format: false) + utils = described_class.new(generator: generator_options) + + generator_options.format = true + generator_options.prefix = '12345678' + + aggregate_failures do + expect(utils.generator.options.all[:format]).to be(true) + expect(utils.generator.options.all[:prefix]).to eq('12345678') + end + end + end + + context 'when called with partial option hashes' do + let(:formatter_options) do + { + hidden: true, + hidden_key: '#', + hidden_start: 8, + hidden_end: 10, + dot_key: '_', + dash_key: ' dv ' + } + end + + let(:generator_options) do + { + format: true, + prefix: '12345678' + } + end + + it 'creates a formatter with the passed options' do + utils = described_class.new(formatter: formatter_options) + + aggregate_failures do + expect(utils.formatter).to be_a(CpfFmt::CpfFormatter) + expect_options_containing(utils.formatter.options.all, formatter_options) + end + end + + it 'creates a generator with the passed options' do + utils = described_class.new(generator: generator_options) + + aggregate_failures do + expect(utils.generator).to be_a(CpfGen::CpfGenerator) + expect_options_containing(utils.generator.options.all, generator_options) + end + end + + it 'creates formatter and generator with the passed options' do + utils = described_class.new( + formatter: formatter_options, + generator: generator_options + ) + + aggregate_failures do + expect_options_containing(utils.formatter.options.all, formatter_options) + expect_options_containing(utils.generator.options.all, generator_options) + expect(utils.validator).to be_a(CpfVal::CpfValidator) + end + end + + it 'configures components from mixed hashes' do + formatter_hash = { hidden: true, hidden_key: 'X' } + generator_hash = { format: true, prefix: '12345' } + + utils = described_class.new( + formatter: formatter_hash, + generator: generator_hash + ) + + aggregate_failures do + expect_options_containing(utils.formatter.options.all, formatter_hash) + expect_options_containing(utils.generator.options.all, generator_hash) + end + end + end + + context 'when called with a settings Hash' do + it 'accepts formatter, generator, and validator keys' do + formatter = CpfFmt::CpfFormatter.new + generator = CpfGen::CpfGenerator.new + validator = CpfVal::CpfValidator.new + + utils = described_class.new( + { + formatter: formatter, + generator: generator, + validator: validator + } + ) + + aggregate_failures do + expect(utils.formatter).to equal(formatter) + expect(utils.generator).to equal(generator) + expect(utils.validator).to equal(validator) + end + end + end + + context 'when called with a non-Hash settings value' do + it 'raises TypeMismatchError' do + expect { described_class.new('not-a-hash') } + .to raise_error(CpfUtils::TypeMismatchError, /settings must be a Hash/) + end + + it 'raises TypeMismatchError for false (non-nil falsy settings)' do + expect { described_class.new(false) } + .to raise_error(CpfUtils::TypeMismatchError, /settings must be a Hash/) + end + + it 'is rescuable via CpfUtils::Error' do + expect { described_class.new([]) } + .to raise_error(CpfUtils::Error) + end + end + + context 'when called with invalid formatter options' do + it 'raises OutOfRangeError for a bad hidden_start' do + expect { described_class.new(formatter: { hidden_start: -1 }) } + .to raise_error(CpfFmt::OutOfRangeError) + end + + it 'raises ValidationError for a forbidden key character' do + expect { described_class.new(formatter: { dash_key: "\u00e5" }) } + .to raise_error(CpfFmt::ValidationError) + end + end + + context 'when called with invalid generator options' do + it 'raises ValidationError for an invalid prefix' do + expect { described_class.new(generator: { prefix: '000000000' }) } + .to raise_error(CpfGen::ValidationError) + end + + it 'raises TypeMismatchError for a non-string prefix' do + expect { described_class.new(generator: { prefix: 123 }) } + .to raise_error(CpfGen::TypeMismatchError) + end + end + + context 'when called with both a settings Hash and keywords' do + it 'raises InvalidArgumentCombinationError' do + expect do + described_class.new({ formatter: {} }, generator: CpfGen::CpfGenerator.new) + end.to raise_error(CpfUtils::InvalidArgumentCombinationError) + end + + it 'raises InvalidArgumentCombinationError for false settings with keywords' do + expect do + described_class.new(false, formatter: {}) + end.to raise_error(CpfUtils::InvalidArgumentCombinationError) + end + end + end + + describe 'resource accessors' do + subject(:utils) { described_class.new } + + it 'returns the formatter used internally' do + expect(utils.formatter).to be_a(CpfFmt::CpfFormatter) + end + + it 'returns the generator used internally' do + expect(utils.generator).to be_a(CpfGen::CpfGenerator) + end + + it 'returns the validator used internally' do + expect(utils.validator).to be_a(CpfVal::CpfValidator) + end + end + + describe '#formatter=' do + subject(:utils) { described_class.new } + + context 'when called with a CpfFormatter instance' do + it 'sets the formatter instance' do + formatter = CpfFmt::CpfFormatter.new + + utils.formatter = formatter + + expect(utils.formatter).to equal(formatter) + end + end + + context 'when called with a CpfFormatterOptions instance' do + it 'sets a formatter that keeps the options' do + formatter_options = CpfFmt::CpfFormatterOptions.new + + utils.formatter = formatter_options + + expect(utils.formatter.options).to equal(formatter_options) + end + end + + context 'when called with a partial options Hash' do + let(:formatter_options) do + { + hidden: true, + hidden_key: '#', + hidden_start: 8, + hidden_end: 10, + dot_key: '_', + dash_key: ' dv ' + } + end + + it 'sets a formatter with the given options' do + utils.formatter = formatter_options + + expect_options_containing(utils.formatter.options.all, formatter_options) + end + + it 'replaces the formatter when given an empty Hash' do + original_formatter = utils.formatter + original_options = original_formatter.options.all + + utils.formatter = {} + + aggregate_failures do + expect(utils.formatter).not_to equal(original_formatter) + expect_options_containing(utils.formatter.options.all, original_options) + end + end + end + + context 'when called with nil' do + it 'resets to a new default formatter' do + original_formatter = utils.formatter + + utils.formatter = nil + + aggregate_failures do + expect(utils.formatter).to be_a(CpfFmt::CpfFormatter) + expect(utils.formatter).not_to equal(original_formatter) + end + end + end + end + + describe '#generator=' do + subject(:utils) { described_class.new } + + context 'when called with a CpfGenerator instance' do + it 'sets the generator instance' do + generator = CpfGen::CpfGenerator.new + + utils.generator = generator + + expect(utils.generator).to equal(generator) + end + end + + context 'when called with a CpfGeneratorOptions instance' do + it 'sets a generator that keeps the options' do + generator_options = CpfGen::CpfGeneratorOptions.new + + utils.generator = generator_options + + expect(utils.generator.options).to equal(generator_options) + end + end + + context 'when called with a partial options Hash' do + let(:generator_options) do + { + format: true, + prefix: '12345678' + } + end + + it 'sets a generator with the given options' do + utils.generator = generator_options + + expect_options_containing(utils.generator.options.all, generator_options) + end + + it 'replaces the generator when given an empty Hash' do + original_generator = utils.generator + original_options = original_generator.options.all + + utils.generator = {} + + aggregate_failures do + expect(utils.generator).not_to equal(original_generator) + expect_options_containing(utils.generator.options.all, original_options) + end + end + end + + context 'when called with nil' do + it 'resets to a new default generator' do + original_generator = utils.generator + + utils.generator = nil + + aggregate_failures do + expect(utils.generator).to be_a(CpfGen::CpfGenerator) + expect(utils.generator).not_to equal(original_generator) + end + end + end + end + + describe '#validator=' do + subject(:utils) { described_class.new } + + context 'when called with a CpfValidator instance' do + it 'sets the validator instance' do + validator = CpfVal::CpfValidator.new + + utils.validator = validator + + expect(utils.validator).to equal(validator) + end + end + + context 'when called with nil' do + it 'resets to a new default validator' do + original_validator = utils.validator + + utils.validator = nil + + aggregate_failures do + expect(utils.validator).to be_a(CpfVal::CpfValidator) + expect(utils.validator).not_to equal(original_validator) + end + end + end + end + + describe '#format' do + subject(:utils) { described_class.new } + + context 'when delegating to the owned formatter' do + let(:formatter) { instance_double(CpfFmt::CpfFormatter) } + + before do + utils.formatter = formatter + end + + it 'invokes format with the same arguments' do + cpf = '12345678909' + options = CpfFmt::CpfFormatterOptions.new + allow(formatter).to receive(:format).and_return('formatted') + + utils.format(cpf, options) + + expect(formatter).to have_received(:format).with(cpf, options) + end + + it 'returns the formatted CPF' do + allow(formatter).to receive(:format).and_return('formatted-cpf') + + expect(utils.format('12345678909')).to eq('formatted-cpf') + end + + it 'forwards named formatting keywords' do + allow(formatter).to receive(:format).and_return('123.456.789-09') + + result = utils.format('12345678909', hidden: true, hidden_key: 'X', escape: true) + + aggregate_failures do + expect(result).to eq('123.456.789-09') + expect(formatter).to have_received(:format).with( + '12345678909', + hidden: true, + hidden_key: 'X', + escape: true + ) + end + end + + it 'rethrows errors from the formatter' do + allow(formatter).to receive(:format).and_raise(RuntimeError, 'test error') + + expect { utils.format('12345678909') }.to raise_error(RuntimeError, 'test error') + end + end + + context 'when constructor formatter defaults are set' do + it 'applies them when method options are omitted' do + utils = described_class.new( + formatter: { + hidden: true, + hidden_key: '#' + } + ) + + expect(utils.format('12345678909')).to include('#') + end + end + + context 'when options and keywords are both given' do + it 'raises InvalidArgumentCombinationError for an options instance' do + options = CpfFmt::CpfFormatterOptions.new(dash_key: '|') + + expect { utils.format('12345678909', options, hidden: true) } + .to raise_error(CpfUtils::InvalidArgumentCombinationError) + end + + it 'raises InvalidArgumentCombinationError for an options Hash' do + expect { utils.format('12345678909', { dash_key: '|' }, hidden: true) } + .to raise_error(CpfUtils::InvalidArgumentCombinationError) + end + end + + context 'with array and encode inputs' do + it 'formats an array of strings' do + expect(utils.format(%w[123 456 78909])).to eq('123.456.789-09') + end + + it 'URL-encodes when encode is true' do + expect(utils.format('12345678909', encode: true, dash_key: '/')) + .to eq('123.456.789%2F09') + end + end + + FORMAT_FACTORY_CONTEXTS.each do |context_description, factory_key| + context context_description do + let(:format_cpf) { FORMAT_FACTORIES.fetch(factory_key) } + + it 'matches CpfFormatter#format behaviour' do + input = '80976511061' + formatter = CpfFmt::CpfFormatter.new + + expect(format_cpf.call(input)).to eq(formatter.format(input)) + end + + it 'forwards formatting options' do + input = '12345678909' + dot_key = '_' + dash_key = ' dv ' + + expect(format_cpf.call(input, dot_key, dash_key)).to eq('123_456_789 dv 09') + end + end + end + + context 'with PHP formatter fixtures' do + it 'formats a dotted-dashed CPF unchanged' do + expect(utils.format('809.765.110-61')).to eq('809.765.110-61') + end + + it 'formats an unformatted CPF with dots and dash' do + expect(utils.format('80976511061')).to eq('809.765.110-61') + end + + it 'formats a dash-separated CPF with dots and dash' do + expect(utils.format('809-765-110-61')).to eq('809.765.110-61') + end + + it 'formats a space-separated CPF with dots and dash' do + expect(utils.format('809 765 110 61')).to eq('809.765.110-61') + end + + it 'formats a trailing-space CPF with dots and dash' do + expect(utils.format('80976511061 ')).to eq('809.765.110-61') + end + + it 'formats a leading-space CPF with dots and dash' do + expect(utils.format(' 80976511061')).to eq('809.765.110-61') + end + + it 'formats individually dotted digits with dots and dash' do + expect(utils.format('8.0.9.7.6.5.1.1.0.6.1')).to eq('809.765.110-61') + end + + it 'formats individually dashed digits with dots and dash' do + expect(utils.format('8-0-9-7-6-5-1-1-0-6-1')).to eq('809.765.110-61') + end + + it 'formats individually spaced digits with dots and dash' do + expect(utils.format('8 0 9 7 6 5 1 1 0 6 1')).to eq('809.765.110-61') + end + + it 'strips letters before formatting' do + expect(utils.format('80976511061abc')).to eq('809.765.110-61') + end + + it 'strips mixed non-digit characters before formatting' do + expect(utils.format('809765110 dv 61')).to eq('809.765.110-61') + end + + it 'formats with empty dot_key' do + expect(utils.format('80976511061', dot_key: '')).to eq('809765110-61') + end + + it 'formats with dash_key as a dot' do + expect(utils.format('80976511061', dash_key: '.')).to eq('809.765.110.61') + end + + it 'formats with empty delimiters' do + expect(utils.format('809.765.110-61', dot_key: '', dash_key: '')).to eq('80976511061') + end + + it 'formats with escape and custom delimiters' do + expect(utils.format('80976511061', escape: true, dot_key: '<', dash_key: '>')) + .to eq('809<765<110>61') + end + + it 'formats with the default hidden mask' do + expect(utils.format('80976511061', hidden: true)).to eq('809.***.***-**') + end + + it 'formats with a custom hidden_start' do + expect(utils.format('80976511061', hidden: true, hidden_start: 6)) + .to eq('809.765.***-**') + end + + it 'formats with a custom hidden_end' do + expect(utils.format('80976511061', hidden: true, hidden_end: 8)) + .to eq('809.***.***-61') + end + + it 'formats with a custom hidden range' do + expect(utils.format('80976511061', hidden: true, hidden_start: 0, hidden_end: 8)) + .to eq('***.***.***-61') + end + + it 'formats with a reversed hidden range' do + expect(utils.format('80976511061', hidden: true, hidden_start: 9, hidden_end: 3)) + .to eq('809.***.***-*1') + end + + it 'formats with a custom hidden_key' do + expect(utils.format('80976511061', hidden: true, hidden_key: '#')) + .to eq('809.###.###-##') + end + + it 'formats with a custom hidden_key and range' do + expect( + utils.format('80976511061', hidden: true, hidden_key: '#', hidden_start: 6) + ).to eq('809.765.###-##') + end + + it 'falls back to on_fail for invalid input' do + expect( + utils.format('abc', on_fail: ->(value, _error) { value.upcase }) + ).to eq('ABC') + end + + it 'raises OutOfRangeError for hidden_start out of range' do + aggregate_failures do + expect { utils.format('80976511061', hidden: true, hidden_start: -1) } + .to raise_error(CpfFmt::OutOfRangeError) + expect { utils.format('80976511061', hidden: true, hidden_start: 11) } + .to raise_error(CpfFmt::OutOfRangeError) + end + end + + it 'raises OutOfRangeError for hidden_end out of range' do + aggregate_failures do + expect { utils.format('80976511061', hidden: true, hidden_end: -1) } + .to raise_error(CpfFmt::OutOfRangeError) + expect { utils.format('80976511061', hidden: true, hidden_end: 11) } + .to raise_error(CpfFmt::OutOfRangeError) + end + end + + it 'raises TypeMismatchError when on_fail is not callable' do + expect { utils.format('80976511061', on_fail: 'testing') } + .to raise_error(CpfFmt::TypeMismatchError) + end + end + end + + describe '#generate' do + subject(:utils) { described_class.new } + + context 'when delegating to the owned generator' do + let(:generator) { instance_double(CpfGen::CpfGenerator) } + + before do + utils.generator = generator + end + + it 'invokes generate with the same arguments' do + options = CpfGen::CpfGeneratorOptions.new + allow(generator).to receive(:generate).and_return('generated') + + utils.generate(options) + + expect(generator).to have_received(:generate).with(options) + end + + it 'returns the generated CPF' do + allow(generator).to receive(:generate).and_return('generated-cpf') + + expect(utils.generate).to eq('generated-cpf') + end + + it 'forwards named generation keywords' do + allow(generator).to receive(:generate).and_return('123.456.789-09') + + result = utils.generate(format: true, prefix: '12345678') + + aggregate_failures do + expect(result).to eq('123.456.789-09') + expect(generator).to have_received(:generate).with(format: true, prefix: '12345678') + end + end + + it 'rethrows errors from the generator' do + allow(generator).to receive(:generate).and_raise(RuntimeError, 'test error') + + expect { utils.generate }.to raise_error(RuntimeError, 'test error') + end + end + + context 'when options and keywords are both given' do + it 'raises InvalidArgumentCombinationError for an options instance' do + options = CpfGen::CpfGeneratorOptions.new(format: true) + + expect { utils.generate(options, prefix: '12345') } + .to raise_error(CpfUtils::InvalidArgumentCombinationError) + end + + it 'raises InvalidArgumentCombinationError for an options Hash' do + expect { utils.generate({ format: true }, prefix: '12345') } + .to raise_error(CpfUtils::InvalidArgumentCombinationError) + end + end + + GENERATE_FACTORY_CONTEXTS.each do |context_description, factory_key| + context context_description do + let(:generate) { GENERATE_FACTORIES.fetch(factory_key) } + + it 'matches CpfGenerator#generate shape' do + validator = CpfVal::CpfValidator.new + result = generate.call + + aggregate_failures do + expect(result).to match(/\A\d{11}\z/) + expect(validator.is_valid(result)).to be(true) + end + end + + it 'forwards generation options' do + result = generate.call(format: true, prefix: '12345678') + + expect(result).to match(/\A123\.456\.78\d-\d{2}\z/) + end + + it 'returns a deterministic CPF for a full 9-digit prefix' do + prefix = '123456789' + results = Array.new(20) { generate.call(prefix: prefix) } + + expect(results.uniq.size).to eq(1) + end + end + end + + context 'with PHP generator fixtures' do + it 'generates 11-digit strings without formatting' do + 25.times do + expect(utils.generate.length).to eq(11) + end + end + + it 'generates 14-character strings with formatting' do + 25.times do + expect(utils.generate(format: true).length).to eq(14) + end + end + + it 'generates valid unformatted CPFs' do + 25.times do + expect(utils.is_valid(utils.generate)).to be(true) + end + end + + it 'generates valid formatted CPFs' do + 25.times do + expect(utils.is_valid(utils.generate(format: true))).to be(true) + end + end + + it 'generates valid CPFs for each prefix length' do + prefixes = %w[ + 1 12 123 1234 12345 123456 1234567 12345678 123456789 123.456.789 + ] + + prefixes.each do |prefix| + expect(utils.is_valid(utils.generate(prefix: prefix))).to be(true) + end + end + + it 'generates formatted CPFs matching the default pattern' do + 25.times do + expect(utils.generate(format: true)).to match(/\A\d{3}\.\d{3}\.\d{3}-\d{2}\z/) + end + end + + it 'generates a CPF whose body matches a short prefix' do + expect(utils.generate({ prefix: '12345' })).to match(/\A12345\d{6}\z/) + end + end + end + + describe '#is_valid' do + subject(:utils) { described_class.new } + + context 'when delegating to the owned validator' do + let(:validator) { instance_double(CpfVal::CpfValidator) } + + before do + utils.validator = validator + end + + it 'invokes is_valid with the same arguments' do + cpf = '12345678909' + allow(validator).to receive(:is_valid).and_return(true) + + utils.is_valid(cpf) + + expect(validator).to have_received(:is_valid).with(cpf) + end + + it 'returns the validation result' do + allow(validator).to receive(:is_valid).and_return(true) + + expect(utils.is_valid('12345678909')).to be(true) + end + + it 'returns false when the validator returns false' do + allow(validator).to receive(:is_valid).and_return(false) + + result = utils.is_valid('12345678900') + + aggregate_failures do + expect(result).to be(false) + expect(validator).to have_received(:is_valid).with('12345678900') + end + end + + it 'rethrows errors from the validator' do + allow(validator).to receive(:is_valid).and_raise(RuntimeError, 'test error') + + expect { utils.is_valid('12345678909') }.to raise_error(RuntimeError, 'test error') + end + end + + IS_VALID_FACTORY_CONTEXTS.each do |context_description, factory_key| + context context_description do + let(:is_valid) { IS_VALID_FACTORIES.fetch(factory_key) } + + it 'matches CpfValidator#is_valid behaviour' do + input = '86244870050' + validator = CpfVal::CpfValidator.new + + expect(is_valid.call(input)).to eq(validator.is_valid(input)) + end + + it 'validates formatted and unformatted CPF strings' do + aggregate_failures do + expect(is_valid.call('12345678909')).to be(true) + expect(is_valid.call('123.456.789-09')).to be(true) + expect(is_valid.call('12345678900')).to be(false) + end + end + end + end + + context 'with PHP validator fixtures' do + it 'validates a dotted-dashed CPF' do + expect(utils.is_valid('499.784.420-90')).to be(true) + end + + it 'validates a dotted CPF' do + expect(utils.is_valid('028.062.110.85')).to be(true) + end + + it 'validates an underscored CPF' do + expect(utils.is_valid('011_258_960_00')).to be(true) + end + + it 'validates a dashed CPF' do + expect(utils.is_valid('779953010-30')).to be(true) + end + + it 'validates an unformatted CPF' do + expect(utils.is_valid('86244870050')).to be(true) + end + + it 'validates known valid samples' do + %w[22312659077 96215666068 67107095072 48039958008 20954431014].each do |cpf| + expect(utils.is_valid(cpf)).to be(true) + end + end + + it 'rejects 090.871.219-71' do + expect(utils.is_valid('090.871.219-71')).to be(false) + end + + it 'rejects 081.465.729.10' do + expect(utils.is_valid('081.465.729.10')).to be(false) + end + + it 'rejects 011_258_960_99' do + expect(utils.is_valid('011_258_960_99')).to be(false) + end + + it 'rejects 499784420-75' do + expect(utils.is_valid('499784420-75')).to be(false) + end + + it 'rejects 86244870011' do + expect(utils.is_valid('86244870011')).to be(false) + end + + it 'rejects abc' do + expect(utils.is_valid('abc')).to be(false) + end + + it 'rejects abc123' do + expect(utils.is_valid('abc123')).to be(false) + end + + it 'rejects repeated-digit CPFs' do + aggregate_failures do + expect(utils.is_valid('00000000000')).to be(false) + expect(utils.is_valid('11111111111')).to be(false) + end + end + + it 'validates an array of strings' do + expect(utils.is_valid(%w[123 456 78909])).to be(true) + end + + it 'raises TypeMismatchError for a non-string input' do + expect { utils.is_valid(123) }.to raise_error(CpfVal::TypeMismatchError) + end + + it 'raises TypeMismatchError for a boolean input' do + expect { utils.is_valid(true) }.to raise_error(CpfVal::TypeMismatchError) + end + + it 'raises TypeMismatchError for a nil input' do + expect { utils.is_valid(nil) }.to raise_error(CpfVal::TypeMismatchError) + end + + it 'raises TypeMismatchError for a non-string array' do + expect { utils.is_valid([1, 2, 3]) }.to raise_error(CpfVal::TypeMismatchError) + end + + it 'raises TypeMismatchError for a Hash input' do + expect { utils.is_valid({ a: 1 }) }.to raise_error(CpfVal::TypeMismatchError) + end + end + end + + describe 'integration' do + it 'uses the owned component instances for all methods' do + utils = described_class.new + formatter = instance_double(CpfFmt::CpfFormatter) + generator = instance_double(CpfGen::CpfGenerator) + validator = instance_double(CpfVal::CpfValidator) + + allow(formatter).to receive(:format).and_return('formatted') + allow(generator).to receive(:generate).and_return('generated') + allow(validator).to receive(:is_valid).and_return(true) + + utils.formatter = formatter + utils.generator = generator + utils.validator = validator + + aggregate_failures do + expect(utils.format('123')).to eq('formatted') + expect(utils.generate).to eq('generated') + expect(utils.is_valid('123')).to be(true) + expect(formatter).to have_received(:format).once + expect(generator).to have_received(:generate).once + expect(validator).to have_received(:is_valid).once + end + end + end + + describe 'package smoke' do + it 'is an instantiable class' do + aggregate_failures do + expect(described_class).to be_a(Class) + expect(described_class.new).to be_a(described_class) + end + end + + it 'exposes a VERSION string' do + expect(described_class::VERSION).to be_a(String).and match(/\A\d+\.\d+\.\d+\z/) + end + + it 'formats through DEFAULT with custom delimiters' do + result = described_class::DEFAULT.format('12345678909', dot_key: '_', dash_key: ' dv ') + + expect(result).to eq('123_456_789 dv 09') + end + + it 'formats through CpfFmt.cpf_fmt' do + result = CpfFmt.cpf_fmt('12345678909', dot_key: '_', dash_key: ' dv ') + + expect(result).to eq('123_456_789 dv 09') + end + + it 'formats through an owned CpfFormatter' do + formatter = CpfFmt::CpfFormatter.new(hidden: true) + + expect(formatter.format('12345678909')).to eq('123.***.***-**') + end + + it 'generates a CPF through DEFAULT' do + result = described_class::DEFAULT.generate + + aggregate_failures do + expect(result.length).to eq(11) + expect(result).to match(/\A\d{11}\z/) + end + end + + it 'generates through CpfGen.cpf_gen' do + result = CpfGen.cpf_gen + + aggregate_failures do + expect(result.length).to eq(11) + expect(result).to match(/\A\d{11}\z/) + end + end + + it 'validates through DEFAULT' do + aggregate_failures do + expect(described_class::DEFAULT.is_valid('12345678909')).to be(true) + expect(described_class::DEFAULT.is_valid('12345678900')).to be(false) + end + end + + it 'validates through CpfVal.cpf_val' do + aggregate_failures do + expect(CpfVal.cpf_val('12345678909')).to be(true) + expect(CpfVal.cpf_val('12345678900')).to be(false) + end + end + end +end