cpf-fmt 0.0.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +9 -0
- data/README.md +454 -0
- data/src/cpf-fmt/cpf_fmt.rb +29 -0
- data/src/cpf-fmt/cpf_formatter.rb +168 -0
- data/src/cpf-fmt/cpf_formatter_options.rb +423 -0
- data/src/cpf-fmt/errors.rb +175 -0
- data/src/cpf-fmt/types.rb +66 -0
- data/src/cpf-fmt/utils.rb +131 -0
- data/src/cpf-fmt/version.rb +1 -1
- data/src/cpf-fmt.rb +42 -3
- metadata +34 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f276500fe73287c24b46c728a6b9cfd965fdfb038f38b84ffe6df340c6db3de
|
|
4
|
+
data.tar.gz: 80ed01045da22c14c08725718a91f947c2a03fd0e4980e2cb974d476a5fa4933
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e102bda4c2471563b7d3447b1d9307ad90a16ec4f870c8ebc0f3c2b14582e354fca389286fe9fa4066990957ab859390ee42844b1034f1ace6c34857bd86974
|
|
7
|
+
data.tar.gz: bf97c65dff5fe57dde1de3126403ab20fa5dc2ba88f0a0a0337026281c282aac5a33097c2a313d1a717a865237cdd8526476698e170d6920bbcc2d6bf5bd1f5f
|
data/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julio L. Muller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/cpf-fmt)
|
|
4
|
+
[](https://rubygems.org/gems/cpf-fmt)
|
|
5
|
+
[](https://www.ruby-lang.org/)
|
|
6
|
+
[](https://github.com/LacusSolutions/br-utils-ruby/actions)
|
|
7
|
+
[](https://github.com/LacusSolutions/br-utils-ruby)
|
|
8
|
+
[](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
> 🌎 [Acessar documentação em português](./README.pt.md)
|
|
11
|
+
|
|
12
|
+
A Ruby utility to format CPF (Brazilian Individual's Taxpayer ID).
|
|
13
|
+
|
|
14
|
+
## Ruby Support
|
|
15
|
+
|
|
16
|
+
|  |  |  |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Passing ✔ | Passing ✔ | Passing ✔ |
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- ✅ **Flexible input**: Accepts `String` or `Array` of strings; array elements are concatenated in order
|
|
23
|
+
- ✅ **Format agnostic**: Strips non-digit characters before formatting (letters and punctuation are discarded)
|
|
24
|
+
- ✅ **Custom delimiters**: `dot_key` and `dash_key` may be empty, single-, or multi-character strings
|
|
25
|
+
- ✅ **Masking**: Optional hiding of a digit range with a configurable replacement string (`hidden`, `hidden_key`, `hidden_start`, `hidden_end`)
|
|
26
|
+
- ✅ **HTML & URL output**: Optional `escape` (HTML entities) and `encode` (URI component encoding, similar to JavaScript `encodeURIComponent`)
|
|
27
|
+
- ✅ **Length errors without throwing**: Invalid length after sanitization is handled via `on_fail` (default returns an empty string)
|
|
28
|
+
- ✅ **Minimal dependencies**: Only [`lacus-utils`](https://rubygems.org/gems/lacus-utils)
|
|
29
|
+
- ✅ **Error handling**: API misuse vs domain errors with a `CpfFmt::Error` marker for library-wide rescue
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Install the gem directly:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
gem install cpf-fmt
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or add it to your `Gemfile` and run `bundle install`:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
gem 'cpf-fmt'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Require
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
require 'cpf-fmt'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
require 'cpf-fmt'
|
|
55
|
+
|
|
56
|
+
formatter = CpfFmt::CpfFormatter.new
|
|
57
|
+
|
|
58
|
+
formatter.format('03603568195') # => "036.035.681-95"
|
|
59
|
+
formatter.format('123.456.789-10') # => "123.456.789-10"
|
|
60
|
+
formatter.format('12345678910') # => "123.456.789-10"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Basic helper usage:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
require 'cpf-fmt'
|
|
67
|
+
|
|
68
|
+
cpf = '03603568195'
|
|
69
|
+
|
|
70
|
+
CpfFmt.cpf_fmt(cpf) # => "036.035.681-95"
|
|
71
|
+
CpfFmt.cpf_fmt(cpf, hidden: true) # => "036.***.***-**"
|
|
72
|
+
CpfFmt.cpf_fmt( # => "036035681_95"
|
|
73
|
+
cpf,
|
|
74
|
+
dot_key: '',
|
|
75
|
+
dash_key: '_'
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
The main entry points are the class `CpfFmt::CpfFormatter`, the options class `CpfFmt::CpfFormatterOptions`, and the helper `CpfFmt.cpf_fmt`.
|
|
82
|
+
|
|
83
|
+
### `CpfFmt::CpfFormatter`
|
|
84
|
+
|
|
85
|
+
- **`initialize(options = nil, **keywords)`**: Optional default formatting options. When `options` is given (a `CpfFmt::CpfFormatterOptions` instance or a `Hash`) alone, it determines the default options; a `CpfFmt::CpfFormatterOptions` instance is stored by reference (mutating it later affects future `format` calls that do not pass per-call options), while a `Hash` builds a new instance. When `options` is omitted (`nil`), the default options are built exclusively from the keyword arguments (`hidden:`, `hidden_key:`, `dot_key:`, …). Passing `options` together with any non-`nil` keyword raises `InvalidArgumentCombinationError` instead of silently ignoring the keywords. Example: `CpfFmt::CpfFormatter.new(hidden: true, dash_key: '_')`.
|
|
86
|
+
- **`options`**: Returns the instance’s `CpfFmt::CpfFormatterOptions` (same object used internally).
|
|
87
|
+
- **`format(cpf_input, options = nil, **keywords)`**: Formats a CPF value.
|
|
88
|
+
|
|
89
|
+
Input is normalized by removing non-digit characters. If the sanitized length is not exactly **11**, the **`on_fail`** callback is invoked with the original input and a `CpfFmt::DomainError` (`InvalidLengthError`); its return value is the result (nothing is thrown for length).
|
|
90
|
+
|
|
91
|
+
If the input is not a `String` or an `Array` of strings, **`CpfFmt::TypeMismatchError`** is raised.
|
|
92
|
+
|
|
93
|
+
Per-call `options` and keyword arguments are never merged: a given `options` argument alone fully overrides the instance defaults for this call; otherwise, any given keyword overrides the instance defaults for this call. When neither is given, the instance defaults are used as-is. The instance defaults are never mutated by a per-call override. Passing `options` together with any non-`nil` keyword raises `InvalidArgumentCombinationError`.
|
|
94
|
+
|
|
95
|
+
### `CpfFmt::CpfFormatterOptions`
|
|
96
|
+
|
|
97
|
+
Holds all formatter settings, with validation and merge support. Exposes properties: `hidden`, `hidden_key`, `hidden_start`, `hidden_end`, `dot_key`, `dash_key`, `escape`, `encode`, `on_fail`.
|
|
98
|
+
|
|
99
|
+
- **`initialize(options = nil, *extra_overrides, **keywords)`**: Optional default options (plain `Hash`, `CpfFmt::CpfFormatterOptions` instance, or keyword arguments), plus extra override objects merged in order (later overrides win).
|
|
100
|
+
- **`all`**: Returns a shallow `Hash` copy of all current options.
|
|
101
|
+
- **`copy`**: Returns a shallow copy of this options instance.
|
|
102
|
+
- **`set(options)`**: Updates multiple fields at once; returns `self`. Accepts a `Hash` or another `CpfFmt::CpfFormatterOptions` instance. Explicit `nil` values in the update keep the current value.
|
|
103
|
+
- **`set_hidden_range(hidden_start, hidden_end)`**: Validates indices in **`[0, 10]`** (inclusive); if `hidden_start > hidden_end`, values are swapped. `nil` arguments fall back to defaults (`DEFAULT_HIDDEN_START` / `DEFAULT_HIDDEN_END`).
|
|
104
|
+
|
|
105
|
+
**`hidden_start` / `hidden_end`**: Indices refer to the **11-digit normalized CPF string** (before inserting punctuation). The inclusive range is replaced internally by placeholders, then `hidden_key` is substituted (supports multi-character keys and empty string).
|
|
106
|
+
|
|
107
|
+
**Key options** (`hidden_key`, `dot_key`, `dash_key`): Must be strings and must not contain any character in `CpfFmt::CpfFormatterOptions::DISALLOWED_KEY_CHARACTERS` (reserved for internal formatting).
|
|
108
|
+
|
|
109
|
+
### Functional helper
|
|
110
|
+
|
|
111
|
+
`CpfFmt.cpf_fmt` builds a new `CpfFmt::CpfFormatter` from the same constructor parameters and calls `format(cpf_input)` once. Pass either keyword arguments **or** a `Hash`/`CpfFmt::CpfFormatterOptions` instance for options — not both (passing `options` with any non-`nil` keyword raises `InvalidArgumentCombinationError`):
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
require 'cpf-fmt'
|
|
115
|
+
|
|
116
|
+
cpf = '03603568195'
|
|
117
|
+
|
|
118
|
+
CpfFmt.cpf_fmt(cpf) # => "036.035.681-95"
|
|
119
|
+
CpfFmt.cpf_fmt(cpf, hidden: true) # masked with defaults
|
|
120
|
+
CpfFmt.cpf_fmt( # => "036035681_95"
|
|
121
|
+
cpf,
|
|
122
|
+
dot_key: '',
|
|
123
|
+
dash_key: '_'
|
|
124
|
+
)
|
|
125
|
+
CpfFmt.cpf_fmt(cpf, { # Hash form
|
|
126
|
+
hidden: true,
|
|
127
|
+
hidden_key: '#'
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Object-oriented examples
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
require 'cpf-fmt'
|
|
135
|
+
|
|
136
|
+
formatter = CpfFmt::CpfFormatter.new
|
|
137
|
+
cpf = '12345678910'
|
|
138
|
+
|
|
139
|
+
formatter.format(cpf) # => "123.456.789-10"
|
|
140
|
+
formatter.format( # => "123.###.###-##"
|
|
141
|
+
cpf,
|
|
142
|
+
hidden: true,
|
|
143
|
+
hidden_key: '#',
|
|
144
|
+
hidden_start: 3,
|
|
145
|
+
hidden_end: 10
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Default options on the instance; per-call overrides:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
require 'cpf-fmt'
|
|
153
|
+
|
|
154
|
+
formatter = CpfFmt::CpfFormatter.new(hidden: true)
|
|
155
|
+
cpf = '12345678910'
|
|
156
|
+
|
|
157
|
+
formatter.format(cpf) # uses instance masking
|
|
158
|
+
formatter.format(cpf, hidden: false) # this call only: unmasked
|
|
159
|
+
formatter.format(cpf) # back to instance defaults
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Array input:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
require 'cpf-fmt'
|
|
166
|
+
|
|
167
|
+
formatter = CpfFmt::CpfFormatter.new
|
|
168
|
+
|
|
169
|
+
formatter.format([ # => "123.456.789-10"
|
|
170
|
+
'123',
|
|
171
|
+
'456',
|
|
172
|
+
'789',
|
|
173
|
+
'10'
|
|
174
|
+
])
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Input formats
|
|
178
|
+
|
|
179
|
+
**String:** Raw digits, or already formatted CPF (e.g. `123.456.789-10`, `123 456 789 10`). Non-digit characters are removed; leading zeros are preserved.
|
|
180
|
+
|
|
181
|
+
**Array of strings:** Each element must be a `String`; values are concatenated (e.g. per digit, grouped segments, or mixed with punctuation — all non-digits are stripped during normalization). Non-string elements are not allowed.
|
|
182
|
+
|
|
183
|
+
### Formatting options
|
|
184
|
+
|
|
185
|
+
| Parameter | Type | Default | Description |
|
|
186
|
+
|-----------|------|---------|-------------|
|
|
187
|
+
| `hidden` | `Boolean`, `nil` | `false` | When truthy, replaces the inclusive index range `[hidden_start, hidden_end]` on the normalized 11-digit string before punctuation is applied |
|
|
188
|
+
| `hidden_key` | `String`, `nil` | `'*'` | Replacement for each hidden position (may be multi-character or empty); must not use disallowed key characters |
|
|
189
|
+
| `hidden_start` | `Integer`, `nil` | `3` | Start index `0`–`10` (inclusive) |
|
|
190
|
+
| `hidden_end` | `Integer`, `nil` | `10` | End index `0`–`10` (inclusive); if `hidden_start > hidden_end`, they are swapped |
|
|
191
|
+
| `dot_key` | `String`, `nil` | `'.'` | Separator after the 3rd and 6th digits |
|
|
192
|
+
| `dash_key` | `String`, `nil` | `'-'` | Separator after the 9th digit |
|
|
193
|
+
| `escape` | `Boolean`, `nil` | `false` | When truthy, HTML-escapes the final string |
|
|
194
|
+
| `encode` | `Boolean`, `nil` | `false` | When truthy, URL-encodes the final string (similar to `encodeURIComponent`) |
|
|
195
|
+
| `on_fail` | `Proc`, `nil` | see below | `(value, error) -> String` — used when sanitized length ≠ 11 |
|
|
196
|
+
|
|
197
|
+
Default **`on_fail`** returns an empty string. Signature: `(original_input, error) -> String`, where `error` is a **`CpfFmt::DomainError`** (currently an `InvalidLengthError` with `actual_input`, `evaluated_input`, `expected_length`). The callback return value must be a `String`; otherwise **`CpfFmt::TypeMismatchError`** is raised.
|
|
198
|
+
|
|
199
|
+
Example with all options:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
require 'cpf-fmt'
|
|
203
|
+
|
|
204
|
+
cpf = '12345678910'
|
|
205
|
+
|
|
206
|
+
CpfFmt.cpf_fmt(
|
|
207
|
+
cpf,
|
|
208
|
+
hidden: true,
|
|
209
|
+
hidden_key: '#',
|
|
210
|
+
hidden_start: 3,
|
|
211
|
+
hidden_end: 9,
|
|
212
|
+
dot_key: ' ',
|
|
213
|
+
dash_key: '_-_',
|
|
214
|
+
escape: true,
|
|
215
|
+
encode: true,
|
|
216
|
+
on_fail: ->(value, _error) { value.to_s }
|
|
217
|
+
)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Error handling
|
|
221
|
+
|
|
222
|
+
Errors fall into two categories:
|
|
223
|
+
|
|
224
|
+
| Category | Meaning |
|
|
225
|
+
|---|---|
|
|
226
|
+
| **API misuse** | The caller invoked the library incorrectly (wrong type for input or options, or an invalid argument combination). |
|
|
227
|
+
| **Domain error** | The call was structurally correct, but a value violates a business rule (length, range, forbidden characters). |
|
|
228
|
+
|
|
229
|
+
Every custom error includes the `CpfFmt::Error` marker module. Domain failures (`InvalidLengthError`, `OutOfRangeError`, `ValidationError`) inherit from `CpfFmt::DomainError` (`RangeError`).
|
|
230
|
+
|
|
231
|
+
**Important:** length failures are **constructed as `InvalidLengthError` and passed to `on_fail` as a `DomainError`**, not raised from `format` / `cpf_fmt`. Passing both an `options` instance/`Hash` and any non-`nil` keyword argument raises `InvalidArgumentCombinationError`.
|
|
232
|
+
|
|
233
|
+
#### Summary
|
|
234
|
+
|
|
235
|
+
| Class | Inherits from | Category | Trigger condition |
|
|
236
|
+
|---|---|---|---|
|
|
237
|
+
| `CpfFmt::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and any non-`nil` keyword argument are passed at once |
|
|
238
|
+
| `CpfFmt::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | CPF input or option has the wrong data type |
|
|
239
|
+
| `CpfFmt::InvalidLengthError` | `CpfFmt::DomainError` | Domain error | Sanitized length is not exactly 11 (passed to `on_fail` as `DomainError`) |
|
|
240
|
+
| `CpfFmt::OutOfRangeError` | `CpfFmt::DomainError` | Domain error | `hidden_start` / `hidden_end` outside `0`–`10` |
|
|
241
|
+
| `CpfFmt::ValidationError` | `CpfFmt::DomainError` | Domain error | Key option contains a disallowed character |
|
|
242
|
+
|
|
243
|
+
#### `CpfFmt::Error` (marker module)
|
|
244
|
+
|
|
245
|
+
- **Inheritance:** module marker mixed into every library error via `include` (not a class).
|
|
246
|
+
- **Category:** N/A (rescue target only) — not a failure mode by itself.
|
|
247
|
+
- **When it is raised:** Never raised directly; included by every custom error the library raises or constructs for `on_fail`.
|
|
248
|
+
- **Example:** N/A
|
|
249
|
+
- **How to rescue it:**
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
rescue CpfFmt::Error
|
|
253
|
+
# everything this library raises
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### `CpfFmt::DomainError`
|
|
257
|
+
|
|
258
|
+
- **Inheritance:** `CpfFmt::DomainError < RangeError` (includes `CpfFmt::Error`)
|
|
259
|
+
- **Category:** Domain error — ancestor for numeric/length domain failures.
|
|
260
|
+
- **When it is raised:** Not raised directly; prefer raising a leaf subclass.
|
|
261
|
+
- **Example:** Prefer `raise CpfFmt::OutOfRangeError` / construct `InvalidLengthError` over raising `DomainError` directly.
|
|
262
|
+
- **How to rescue it:**
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
rescue CpfFmt::DomainError
|
|
266
|
+
# OutOfRangeError, InvalidLengthError, ValidationError, and other DomainError subclasses
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### `CpfFmt::TypeMismatchError`
|
|
270
|
+
|
|
271
|
+
- **Inheritance:** `CpfFmt::TypeMismatchError < TypeError` (includes `CpfFmt::Error`)
|
|
272
|
+
- **Category:** API misuse — the caller passed a value of the wrong type.
|
|
273
|
+
- **When it is raised:** Raised when the CPF input is not a `String` or an `Array` of strings, when an option has the wrong type, or when `on_fail` does not return a `String`.
|
|
274
|
+
- **Example:**
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
CpfFmt::CpfFormatter.new.format(12_345) # raises CpfFmt::TypeMismatchError
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
- **How to rescue it:**
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
rescue CpfFmt::TypeMismatchError
|
|
284
|
+
# this library's type-contract violation
|
|
285
|
+
|
|
286
|
+
rescue TypeError
|
|
287
|
+
# native type errors, including this library's TypeMismatchError
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### `CpfFmt::InvalidLengthError`
|
|
291
|
+
|
|
292
|
+
- **Inheritance:** `CpfFmt::InvalidLengthError < CpfFmt::DomainError < RangeError` (includes `CpfFmt::Error`)
|
|
293
|
+
- **Category:** Domain error — a collection or string length violates a business rule.
|
|
294
|
+
- **When it is raised:** Not raised from `format`; constructed and passed as the `DomainError` second argument to `on_fail` when the sanitized CPF does not contain exactly 11 digits.
|
|
295
|
+
- **Example:**
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
CpfFmt::CpfFormatter.new.format(
|
|
299
|
+
'short',
|
|
300
|
+
on_fail: ->(_value, error) {
|
|
301
|
+
error # => #<CpfFmt::InvalidLengthError ...> (a DomainError)
|
|
302
|
+
'invalid'
|
|
303
|
+
}
|
|
304
|
+
) # => "invalid"
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
- **How to rescue it:** Handle inside `on_fail` (typical), or rescue if you re-raise:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
rescue CpfFmt::InvalidLengthError
|
|
312
|
+
# this exact length violation
|
|
313
|
+
|
|
314
|
+
rescue CpfFmt::DomainError
|
|
315
|
+
# RangeError-rooted domain failures from this library
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
#### `CpfFmt::InvalidArgumentCombinationError`
|
|
319
|
+
|
|
320
|
+
- **Inheritance:** `CpfFmt::InvalidArgumentCombinationError < ArgumentError` (includes `CpfFmt::Error`)
|
|
321
|
+
- **Category:** API misuse — the caller mixed mutually exclusive argument patterns.
|
|
322
|
+
- **When it is raised:** Raised when `CpfFormatter.new`, `#format`, or `cpf_fmt` receives both an `options` argument (instance or `Hash`) and any non-`nil` keyword argument at the same time.
|
|
323
|
+
- **Example:**
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
begin
|
|
327
|
+
CpfFmt::CpfFormatter.new({ dash_key: '_' }, hidden: true)
|
|
328
|
+
rescue CpfFmt::InvalidArgumentCombinationError => e
|
|
329
|
+
puts e.message
|
|
330
|
+
# Pass either an options instance/Hash to `options`, or keyword arguments (hidden:, ...), not both.
|
|
331
|
+
end
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
- **How to rescue it:**
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
rescue CpfFmt::InvalidArgumentCombinationError
|
|
338
|
+
# this library's invalid argument combination
|
|
339
|
+
|
|
340
|
+
rescue ArgumentError
|
|
341
|
+
# native argument errors, including this library's InvalidArgumentCombinationError
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### `CpfFmt::OutOfRangeError`
|
|
345
|
+
|
|
346
|
+
- **Inheritance:** `CpfFmt::OutOfRangeError < CpfFmt::DomainError < RangeError` (includes `CpfFmt::Error`)
|
|
347
|
+
- **Category:** Domain error — a numeric value violates a business range rule.
|
|
348
|
+
- **When it is raised:** Raised when `hidden_start` or `hidden_end` is outside the inclusive range `0`–`10`.
|
|
349
|
+
- **Example:**
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
CpfFmt::CpfFormatterOptions.new(hidden_start: 11) # raises CpfFmt::OutOfRangeError
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- **How to rescue it:**
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
rescue CpfFmt::OutOfRangeError
|
|
359
|
+
# this exact range violation
|
|
360
|
+
|
|
361
|
+
rescue CpfFmt::DomainError
|
|
362
|
+
# RangeError-rooted domain failures from this library
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
#### `CpfFmt::ValidationError`
|
|
366
|
+
|
|
367
|
+
- **Inheritance:** `CpfFmt::ValidationError < CpfFmt::DomainError < RangeError` (includes `CpfFmt::Error`)
|
|
368
|
+
- **Category:** Domain error — a value fails a non-numeric, non-length domain rule.
|
|
369
|
+
- **When it is raised:** Raised when a key option (`hidden_key`, `dot_key`, `dash_key`) contains a disallowed character.
|
|
370
|
+
- **Example:**
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
CpfFmt::CpfFormatterOptions.new(dot_key: 'å') # raises CpfFmt::ValidationError
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
- **How to rescue it:**
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
rescue CpfFmt::ValidationError
|
|
380
|
+
# this exact domain validation failure
|
|
381
|
+
|
|
382
|
+
rescue CpfFmt::DomainError
|
|
383
|
+
# RangeError-rooted domain failures from this library
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
#### Rescue granularity
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
# 1) Single native class — catches type misuse from this library (and other TypeErrors).
|
|
390
|
+
rescue TypeError
|
|
391
|
+
# CpfFmt::TypeMismatchError and any other TypeError (library or not)
|
|
392
|
+
|
|
393
|
+
# 2) CpfFmt::DomainError — catches business-rule violations under DomainError.
|
|
394
|
+
rescue CpfFmt::DomainError
|
|
395
|
+
# CpfFmt::OutOfRangeError, CpfFmt::InvalidLengthError, CpfFmt::ValidationError,
|
|
396
|
+
# and other DomainError subclasses
|
|
397
|
+
|
|
398
|
+
# 3) CpfFmt::Error — catches everything the library raises.
|
|
399
|
+
rescue CpfFmt::Error
|
|
400
|
+
# every custom error that includes CpfFmt::Error
|
|
401
|
+
|
|
402
|
+
# 4) Specific leaf class — catches only that exact failure mode.
|
|
403
|
+
rescue CpfFmt::OutOfRangeError
|
|
404
|
+
# only CpfFmt::OutOfRangeError
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Notable attributes:
|
|
408
|
+
|
|
409
|
+
- `TypeMismatchError`: `actual_input`, `actual_type`, `expected_type`, `option_name` (nil for CPF input)
|
|
410
|
+
- `InvalidLengthError`: `actual_input`, `evaluated_input`, `expected_length`
|
|
411
|
+
- `OutOfRangeError`: `option_name`, `actual_input`, `min_expected_value`, `max_expected_value`
|
|
412
|
+
- `ValidationError`: `option_name`, `actual_input`, `forbidden_characters`
|
|
413
|
+
|
|
414
|
+
## API
|
|
415
|
+
|
|
416
|
+
### Exports
|
|
417
|
+
|
|
418
|
+
After `require 'cpf-fmt'`:
|
|
419
|
+
|
|
420
|
+
- **`CpfFmt.cpf_fmt`**: `(cpf_input, options = nil, **keywords) -> String` — convenience helper.
|
|
421
|
+
- **`CpfFmt::CpfFormatter`**: Class to format CPF with optional default options; accepts `String` or `Array<String>` in `format`.
|
|
422
|
+
- **`CpfFmt::CpfFormatterOptions`**: Class holding options; supports merge via constructor, `set`, and keyword arguments.
|
|
423
|
+
- **`CpfFmt::CPF_LENGTH`**: `11` (constant).
|
|
424
|
+
- **`CpfFmt::VERSION`**: gem version string.
|
|
425
|
+
- **Type predicate**: `CpfFmt::CpfInput` — `CpfFmt::CpfInput.accept?(value)` / `CpfFmt::CpfInput === value` is true only for `String` or `Array<String>`.
|
|
426
|
+
- **Errors**: `CpfFmt::Error`, `CpfFmt::DomainError`, `CpfFmt::InvalidArgumentCombinationError`, `CpfFmt::TypeMismatchError`, `CpfFmt::InvalidLengthError`, `CpfFmt::OutOfRangeError`, `CpfFmt::ValidationError`.
|
|
427
|
+
|
|
428
|
+
### Other available resources
|
|
429
|
+
|
|
430
|
+
- **`CpfFmt::CpfFormatterOptions::CPF_LENGTH`**: `11`.
|
|
431
|
+
- **`CpfFmt::CpfFormatterOptions::DISALLOWED_KEY_CHARACTERS`**: Characters forbidden in `hidden_key`, `dot_key`, `dash_key`.
|
|
432
|
+
- **`CpfFmt::CpfFormatterOptions::DEFAULT_*`**: Default values for each option.
|
|
433
|
+
- **`CpfFmt::CpfFormatterOptions.default_on_fail`**: Shared default failure callback.
|
|
434
|
+
|
|
435
|
+
## Contribution & Support
|
|
436
|
+
|
|
437
|
+
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:
|
|
438
|
+
|
|
439
|
+
- ⭐ Starring the repository
|
|
440
|
+
- 🤝 Contributing to the codebase
|
|
441
|
+
- 💡 [Suggesting new features](https://github.com/LacusSolutions/br-utils-ruby/issues)
|
|
442
|
+
- 🐛 [Reporting bugs](https://github.com/LacusSolutions/br-utils-ruby/issues)
|
|
443
|
+
|
|
444
|
+
## License
|
|
445
|
+
|
|
446
|
+
This project is licensed under the MIT License — see the [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE) file for details.
|
|
447
|
+
|
|
448
|
+
## Changelog
|
|
449
|
+
|
|
450
|
+
See [CHANGELOG](./CHANGELOG.md) for a list of changes and version history.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
Made with ❤️ by [Lacus Solutions](https://github.com/LacusSolutions)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CpfFmt
|
|
4
|
+
# Helper function to simplify the usage of the {CpfFormatter} class.
|
|
5
|
+
#
|
|
6
|
+
# Formats a CPF string according to the given options. With no options,
|
|
7
|
+
# returns the traditional CPF format (e.g. +123.456.789-10+). Invalid input
|
|
8
|
+
# length is handled by the configured +on_fail+ callback instead of throwing.
|
|
9
|
+
#
|
|
10
|
+
# @param cpf_input [String, Array<String>] CPF value as a string or array of
|
|
11
|
+
# strings
|
|
12
|
+
# @param options [CpfFormatterOptions, Hash, nil] default formatter options
|
|
13
|
+
# @param keywords [Hash] option keyword overrides (mutually exclusive with +options+;
|
|
14
|
+
# see {CpfFormatterOptions})
|
|
15
|
+
# @return [String] formatted CPF string, or the +on_fail+ callback result
|
|
16
|
+
# @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument are both given
|
|
17
|
+
# @raise [TypeMismatchError] if +cpf_input+ is not a +String+ or +Array<String>+
|
|
18
|
+
# @raise [TypeMismatchError] if any option has an invalid type
|
|
19
|
+
# @raise [OutOfRangeError] if +hidden_start+ or +hidden_end+ are out of valid range
|
|
20
|
+
# @raise [ValidationError] if any key option contains a disallowed character
|
|
21
|
+
# @see CpfFormatter#format for detailed option descriptions
|
|
22
|
+
# @see CpfFormatter
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# CpfFmt.cpf_fmt('12345678910') # => "123.456.789-10"
|
|
26
|
+
def self.cpf_fmt(cpf_input, options = nil, **keywords)
|
|
27
|
+
CpfFormatter.new(options, **keywords).format(cpf_input)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'erb'
|
|
5
|
+
|
|
6
|
+
module CpfFmt
|
|
7
|
+
# Formatter for CPF (Cadastro de Pessoas Físicas) identifiers.
|
|
8
|
+
#
|
|
9
|
+
# Normalizes and optionally masks, HTML-escapes, or URL-encodes 11-digit CPF
|
|
10
|
+
# input. Accepts a string or array of strings; non-digit characters are
|
|
11
|
+
# stripped. Invalid input type is handled by throwing; invalid length is
|
|
12
|
+
# handled via the configured +on_fail+ callback instead of throwing.
|
|
13
|
+
class CpfFormatter
|
|
14
|
+
# Returns the default options used by this formatter when per-call options
|
|
15
|
+
# are not provided.
|
|
16
|
+
#
|
|
17
|
+
# The returned object is the same instance used internally; mutating it (e.g.
|
|
18
|
+
# via setters on {CpfFormatterOptions}) affects future {#format} calls that
|
|
19
|
+
# do not pass +options+.
|
|
20
|
+
#
|
|
21
|
+
# @return [CpfFormatterOptions] the instance default options
|
|
22
|
+
attr_reader :options
|
|
23
|
+
|
|
24
|
+
# Creates a new formatter with optional default options.
|
|
25
|
+
#
|
|
26
|
+
# Default options apply to every call to {#format} unless overridden by the
|
|
27
|
+
# per-call +options+ argument or keyword overrides. Options control masking,
|
|
28
|
+
# HTML escaping, URL encoding, and the callback used when formatting fails.
|
|
29
|
+
#
|
|
30
|
+
# +options+ and the keyword arguments are never merged with each other: when
|
|
31
|
+
# +options+ is given (a {CpfFormatterOptions} instance or a {Hash}), it alone
|
|
32
|
+
# determines the default options; otherwise, the default options are built
|
|
33
|
+
# exclusively from the keyword arguments, with {CpfFormatterOptions} filling
|
|
34
|
+
# in its own defaults for every keyword left as +nil+. Passing +options+
|
|
35
|
+
# together with any non-+nil+ keyword argument raises
|
|
36
|
+
# {InvalidArgumentCombinationError} instead of silently ignoring the keywords.
|
|
37
|
+
#
|
|
38
|
+
# When +options+ is a {CpfFormatterOptions} instance, that instance is used
|
|
39
|
+
# directly (no copy is created). Mutating it later (e.g. via the {#options}
|
|
40
|
+
# reader or the original reference) affects future {#format} calls that do
|
|
41
|
+
# not pass per-call options. When a plain {Hash} is passed instead, a new
|
|
42
|
+
# {CpfFormatterOptions} instance is created from it.
|
|
43
|
+
#
|
|
44
|
+
# @param options [CpfFormatterOptions, Hash, nil] default formatter options
|
|
45
|
+
# @param keywords [Hash] option keyword overrides (mutually exclusive with +options+;
|
|
46
|
+
# see {CpfFormatterOptions})
|
|
47
|
+
# @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument are both given
|
|
48
|
+
# @raise [TypeMismatchError] if any option has an invalid type
|
|
49
|
+
# @raise [OutOfRangeError] if +hidden_start+ or +hidden_end+ are out of valid range
|
|
50
|
+
# @raise [ValidationError] if any key option contains a disallowed character
|
|
51
|
+
def initialize(options = nil, **keywords)
|
|
52
|
+
@options = resolve_default_options(options, keywords)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Formats a CPF value into a human-readable string.
|
|
56
|
+
#
|
|
57
|
+
# Input is normalized by stripping non-digit characters. If the result
|
|
58
|
+
# length is not exactly 11, the configured +on_fail+ callback is invoked
|
|
59
|
+
# with the original value and a {DomainError}; its return value is used as
|
|
60
|
+
# the result.
|
|
61
|
+
#
|
|
62
|
+
# When valid, the result may be further transformed according to options:
|
|
63
|
+
#
|
|
64
|
+
# - If +hidden+ is +true+, digits between +hidden_start+ and +hidden_end+
|
|
65
|
+
# (inclusive) are replaced with +hidden_key+.
|
|
66
|
+
# - If +escape+ is +true+, HTML special characters are escaped.
|
|
67
|
+
# - If +encode+ is +true+, the string is URL-encoded (similar to JavaScript's
|
|
68
|
+
# +encodeURIComponent+).
|
|
69
|
+
#
|
|
70
|
+
# +options+ and the keyword arguments are never merged with each other: when
|
|
71
|
+
# +options+ is given (a {CpfFormatterOptions} instance or a {Hash}), it alone
|
|
72
|
+
# overrides the instance default options for this call; otherwise, any
|
|
73
|
+
# non-+nil+ keyword argument overrides the instance default options for this
|
|
74
|
+
# call. When neither +options+ nor any keyword argument is given, the
|
|
75
|
+
# instance default options are used as-is. In every case, the instance
|
|
76
|
+
# default options themselves are left unchanged. Passing +options+ together
|
|
77
|
+
# with any non-+nil+ keyword argument raises {InvalidArgumentCombinationError}
|
|
78
|
+
# instead of silently ignoring the keywords.
|
|
79
|
+
#
|
|
80
|
+
# @param cpf_input [String, Array<String>] CPF value as a string or array of
|
|
81
|
+
# strings
|
|
82
|
+
# @param options [CpfFormatterOptions, Hash, nil] per-call option overrides
|
|
83
|
+
# @param keywords [Hash] per-call option keyword overrides (mutually exclusive
|
|
84
|
+
# with +options+; see {CpfFormatterOptions})
|
|
85
|
+
# @return [String] formatted CPF string, or the +on_fail+ callback result
|
|
86
|
+
# @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument are both given
|
|
87
|
+
# @raise [TypeMismatchError] if the input is not a +String+ or +Array<String>+
|
|
88
|
+
# @raise [TypeMismatchError] if any option has an invalid type
|
|
89
|
+
# @raise [OutOfRangeError] if +hidden_start+ or +hidden_end+ are out of valid range
|
|
90
|
+
# @raise [ValidationError] if any key option contains a disallowed character
|
|
91
|
+
#
|
|
92
|
+
# @example
|
|
93
|
+
# formatter = CpfFmt::CpfFormatter.new
|
|
94
|
+
# formatter.format('12345678910') # => "123.456.789-10"
|
|
95
|
+
def format(cpf_input, options = nil, **keywords)
|
|
96
|
+
actual_input = Utils.to_string_input(cpf_input)
|
|
97
|
+
actual_options = resolve_call_options(options, keywords)
|
|
98
|
+
formatted_cpf = Utils.sanitize_cpf_input(actual_input)
|
|
99
|
+
|
|
100
|
+
return handle_invalid_length(cpf_input, formatted_cpf, actual_options) unless valid_length?(formatted_cpf)
|
|
101
|
+
|
|
102
|
+
format_valid_cpf(formatted_cpf, actual_options)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def valid_length?(formatted_cpf)
|
|
108
|
+
formatted_cpf.length == CpfFormatterOptions::CPF_LENGTH
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def handle_invalid_length(cpf_input, formatted_cpf, actual_options)
|
|
112
|
+
error = InvalidLengthError.new(
|
|
113
|
+
cpf_input,
|
|
114
|
+
formatted_cpf,
|
|
115
|
+
CpfFormatterOptions::CPF_LENGTH
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
Utils.invoke_on_fail(actual_options.on_fail, cpf_input, error)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def format_valid_cpf(formatted_cpf, actual_options)
|
|
122
|
+
formatted_cpf = Utils.apply_hidden_mask(formatted_cpf, actual_options) if actual_options.hidden
|
|
123
|
+
formatted_cpf = Utils.insert_delimiters(formatted_cpf, actual_options)
|
|
124
|
+
|
|
125
|
+
if actual_options.hidden
|
|
126
|
+
formatted_cpf = Utils.replace_hidden_placeholders(
|
|
127
|
+
formatted_cpf,
|
|
128
|
+
actual_options.hidden_key
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
Utils.apply_post_processing(formatted_cpf, actual_options)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def resolve_default_options(options, keywords)
|
|
136
|
+
keyword_overrides = compact_keyword_overrides(keywords)
|
|
137
|
+
raise_ambiguous_options! if options && !keyword_overrides.empty?
|
|
138
|
+
return options if options.is_a?(CpfFormatterOptions)
|
|
139
|
+
return CpfFormatterOptions.new(options) if options
|
|
140
|
+
|
|
141
|
+
CpfFormatterOptions.new(**keywords)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def resolve_call_options(options, keywords)
|
|
145
|
+
keyword_overrides = compact_keyword_overrides(keywords)
|
|
146
|
+
raise_ambiguous_options! if options && !keyword_overrides.empty?
|
|
147
|
+
return @options.copy.set(options) if options
|
|
148
|
+
return @options if keyword_overrides.empty?
|
|
149
|
+
|
|
150
|
+
@options.copy.set(keyword_overrides)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def compact_keyword_overrides(keywords)
|
|
154
|
+
CpfFormatterOptions::OPTION_KEYS.each_with_object({}) do |key, overrides|
|
|
155
|
+
value = keywords[key]
|
|
156
|
+
overrides[key] = value unless value.nil?
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def raise_ambiguous_options!
|
|
161
|
+
option_keywords = CpfFormatterOptions::OPTION_KEYS.map { |key| "#{key}:" }.join(', ')
|
|
162
|
+
|
|
163
|
+
raise InvalidArgumentCombinationError,
|
|
164
|
+
"Pass either an options instance/Hash to `options`, or keyword arguments (#{option_keywords}), " \
|
|
165
|
+
'not both.'
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|