cpf-gen 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 +350 -0
- data/src/cpf-gen/cpf_gen.rb +32 -0
- data/src/cpf-gen/cpf_generator.rb +138 -0
- data/src/cpf-gen/cpf_generator_options.rb +222 -0
- data/src/cpf-gen/errors.rb +98 -0
- data/src/cpf-gen/types.rb +24 -0
- data/src/cpf-gen/utils.rb +76 -0
- data/src/cpf-gen/version.rb +1 -1
- data/src/cpf-gen.rb +40 -4
- metadata +43 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2298bdcc13739540003bba7d1448506b05ecdcdb0883af12fdcd429fbdc6cfea
|
|
4
|
+
data.tar.gz: 10b903a922490bb5835e63a69a456d7a15b352868929b9437bb9b6f0b70e10e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6b2fc018dd214b637bb2d107c1ea850ae2bfefbec3d12b4e40fcda0d8d783b08bbf31b52c9669137d83649a8f43329399fa3dc5cdf53efb59565c5e7fb9fe7e
|
|
7
|
+
data.tar.gz: 616ea0a6c598b7b628d3c3bc0af9ca7cf26211f6d6ce816222b1b99088478feb900299cc8f8134daf239f42805468754ce4e06be92b6df30b751e70c01688105
|
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,350 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/cpf-gen)
|
|
4
|
+
[](https://rubygems.org/gems/cpf-gen)
|
|
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 generate valid CPF (Brazilian Individual's Taxpayer ID) values.
|
|
13
|
+
|
|
14
|
+
## Ruby Support
|
|
15
|
+
|
|
16
|
+
|  |  |  |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Passing ✔ | Passing ✔ | Passing ✔ |
|
|
19
|
+
|
|
20
|
+
Requires Ruby **≥ 3.1** (see `required_ruby_version` in the gemspec).
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- ✅ **Numeric CPF**: Generates 11-digit numeric CPF values with valid check digits
|
|
25
|
+
- ✅ **Optional prefix**: Provide 0–9 digits to fix the start of the CPF and generate the rest with valid check digits
|
|
26
|
+
- ✅ **Formatting**: Option to return the standard formatted string (`000.000.000-00`)
|
|
27
|
+
- ✅ **Reusable generator**: `CpfGen::CpfGenerator` class with default options and per-call overrides
|
|
28
|
+
- ✅ **Keyword overrides**: Pass `format:` and `prefix:` on `cpf_gen`, `CpfGenerator#generate`, and constructors
|
|
29
|
+
- ✅ **Minimal dependencies**: Only [`cpf-dv`](https://rubygems.org/gems/cpf-dv) and [`lacus-utils`](https://rubygems.org/gems/lacus-utils)
|
|
30
|
+
- ✅ **Error handling**: API misuse vs domain errors with a `CpfGen::Error` marker for library-wide rescue
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Install the gem directly:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
gem install cpf-gen
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or add it to your `Gemfile` and run `bundle install`:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
gem 'cpf-gen'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Require
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
require 'cpf-gen'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
require 'cpf-gen'
|
|
56
|
+
|
|
57
|
+
CpfGen.cpf_gen # => e.g. "47844241055" (11-digit numeric)
|
|
58
|
+
|
|
59
|
+
CpfGen.cpf_gen(format: true) # => e.g. "005.265.352-88"
|
|
60
|
+
|
|
61
|
+
CpfGen.cpf_gen(prefix: '528250911') # => e.g. "52825091138"
|
|
62
|
+
CpfGen.cpf_gen( # => e.g. "528.250.911-38"
|
|
63
|
+
prefix: '528250911',
|
|
64
|
+
format: true
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Options can also be passed as a `Hash`:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
CpfGen.cpf_gen({ format: true, prefix: '528250911' })
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
The main entry points are the module helper `CpfGen.cpf_gen`, the class `CpfGen::CpfGenerator`, and the options class `CpfGen::CpfGeneratorOptions`.
|
|
77
|
+
|
|
78
|
+
### Generator options
|
|
79
|
+
|
|
80
|
+
All options are optional:
|
|
81
|
+
|
|
82
|
+
| Option | Type | Default | Description |
|
|
83
|
+
|--------|------|---------|-------------|
|
|
84
|
+
| `format` | `Boolean` | `false` | When truthy, return the generated CPF in standard format (`000.000.000-00`). Non-boolean values are coerced (`false`, `''`, and `0` become `false`; other values become truthy). |
|
|
85
|
+
| `prefix` | `String` | `''` | Partial start string (0–9 digits). Only digits are kept; missing characters are generated randomly and check digits are computed. Prefixes longer than 9 digits are truncated silently. |
|
|
86
|
+
|
|
87
|
+
Prefix rules: the base (first 9 digits) cannot be all zeros; 9 repeated digits (e.g. `999999999`) are not allowed. Prefixes shorter than 9 digits are never rejected by these rules (e.g. `"00000000"` and `"11111111"` are allowed).
|
|
88
|
+
|
|
89
|
+
`nil` is accepted as a keyword argument on `cpf_gen`, `CpfGenerator.new`, `CpfGenerator#generate`, and `CpfGeneratorOptions.new`/`#set` — it simply means "no override for this option". It is **not** accepted by the `CpfGeneratorOptions` property setters (`options.format = value`, `options.prefix = value`): calling a setter with `nil` directly raises `CpfGen::TypeMismatchError`. To reset a property to its default value through a setter, pass the literal constant instead, e.g. `options.format = CpfGen::CpfGeneratorOptions::DEFAULT_FORMAT`.
|
|
90
|
+
|
|
91
|
+
### `CpfGen.cpf_gen` (helper)
|
|
92
|
+
|
|
93
|
+
Generates a valid CPF string. With no options, returns an 11-digit numeric CPF. This is a convenience wrapper around `CpfGen::CpfGenerator.new(...).generate`.
|
|
94
|
+
|
|
95
|
+
- **`options`** (optional): `CpfGen::CpfGeneratorOptions` instance, a `Hash` of option keys, or `nil`. See [Generator options](#generator-options).
|
|
96
|
+
- **`format`**, **`prefix`** (keyword arguments): Only used when `options` is omitted (`nil`). Passing `options` **and** any non-`nil` of these keywords at the same time raises `InvalidArgumentCombinationError` — the two ways of passing options are never merged.
|
|
97
|
+
|
|
98
|
+
### `CpfGen::CpfGenerator` (class)
|
|
99
|
+
|
|
100
|
+
For reusable defaults or per-call overrides, use the class:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
require 'cpf-gen'
|
|
104
|
+
|
|
105
|
+
generator = CpfGen::CpfGenerator.new(format: true)
|
|
106
|
+
|
|
107
|
+
generator.generate # => e.g. "005.265.352-88"
|
|
108
|
+
generator.generate(prefix: '123456') # override for this call only
|
|
109
|
+
generator.options # current default options (CpfGen::CpfGeneratorOptions)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- **`initialize(options = nil, **keywords)`**: Optional default options. When `options` is given (a `CpfGen::CpfGeneratorOptions` instance or a `Hash`) alone, it determines the default options; a `CpfGen::CpfGeneratorOptions` instance is stored by reference (mutating it later affects future `generate` 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 (`format:`, `prefix:`). Passing `options` together with any non-`nil` keyword raises `InvalidArgumentCombinationError` instead of silently ignoring the keywords.
|
|
113
|
+
- **`generate(options = nil, **keywords)`**: Returns a valid CPF. `options` and the 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`.
|
|
114
|
+
- **`options`**: Reader returning the default options used when per-call options are not provided (same instance as used internally; mutating it affects future `generate` calls).
|
|
115
|
+
|
|
116
|
+
Default options on the instance; per-call overrides:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
require 'cpf-gen'
|
|
120
|
+
|
|
121
|
+
generator = CpfGen::CpfGenerator.new(format: true)
|
|
122
|
+
|
|
123
|
+
generator.generate # formatted CPF
|
|
124
|
+
generator.generate(format: false) # this call only: unformatted
|
|
125
|
+
generator.generate # formatted again (instance defaults preserved)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `CpfGen::CpfGeneratorOptions` (class)
|
|
129
|
+
|
|
130
|
+
Holds options (`format`, `prefix`) with validation and merge support:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
require 'cpf-gen'
|
|
134
|
+
|
|
135
|
+
options = CpfGen::CpfGeneratorOptions.new(
|
|
136
|
+
prefix: '123456',
|
|
137
|
+
format: true
|
|
138
|
+
)
|
|
139
|
+
options.prefix # => "123456"
|
|
140
|
+
options.format # => true
|
|
141
|
+
options.set(format: false) # merge and return self
|
|
142
|
+
options.all # => { format: false, prefix: "123456" }
|
|
143
|
+
|
|
144
|
+
# Resetting a property to its default value requires the literal constant —
|
|
145
|
+
# a bare `nil` on a setter raises TypeMismatchError:
|
|
146
|
+
options.format = CpfGen::CpfGeneratorOptions::DEFAULT_FORMAT
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- **`initialize(*options, **keywords)`**: Every positional `options` argument (each a `Hash` or another `CpfGen::CpfGeneratorOptions` instance) is folded left to right — later arguments win — then the keyword arguments (`format:`, `prefix:`) are applied on top with the highest precedence. At every step, a `nil` value for a given key is ignored in favor of whatever was resolved so far. Any option still unresolved after that is set to its `DEFAULT_*` value.
|
|
150
|
+
- **`format`**, **`prefix`**: Accessors with setters; `prefix` is validated (zeroed base ID, repeated digits). The setters **never accept `nil`** — pass the matching `DEFAULT_*` constant (e.g. `CpfGeneratorOptions::DEFAULT_PREFIX`) to reset a property explicitly.
|
|
151
|
+
- **`set(*options, **keywords)`**: Updates multiple options at once, using the same fold-then-keywords, ignore-`nil` resolution as `initialize`. Any option left unresolved after merging keeps its **current** value on the instance (a partial update, not a re-initialization). Returns `self`.
|
|
152
|
+
- **`all`**: Shallow `Hash` copy of current options (`:format`, `:prefix`).
|
|
153
|
+
|
|
154
|
+
## API
|
|
155
|
+
|
|
156
|
+
### Exports
|
|
157
|
+
|
|
158
|
+
After `require 'cpf-gen'`:
|
|
159
|
+
|
|
160
|
+
- **`CpfGen.cpf_gen`**: `(options = nil, **keywords) -> String` — convenience helper.
|
|
161
|
+
- **`CpfGen::CpfGenerator`**: Class to generate CPF with optional default options and per-call overrides.
|
|
162
|
+
- **`CpfGen::CpfGeneratorOptions`**: Class holding options with validation and merge.
|
|
163
|
+
- **`CpfGen::CPF_LENGTH`**: `11` (constant).
|
|
164
|
+
- **`CpfGen::CPF_PREFIX_MAX_LENGTH`**: `9` (constant).
|
|
165
|
+
- **`CpfGen::VERSION`**: gem version string.
|
|
166
|
+
- **Errors**: `CpfGen::Error`, `CpfGen::DomainError`, `CpfGen::InvalidArgumentCombinationError`, `CpfGen::TypeMismatchError`, `CpfGen::ValidationError`.
|
|
167
|
+
|
|
168
|
+
### Error handling
|
|
169
|
+
|
|
170
|
+
Errors fall into two categories:
|
|
171
|
+
|
|
172
|
+
| Category | Meaning |
|
|
173
|
+
|---|---|
|
|
174
|
+
| **API misuse** | The caller invoked the library incorrectly (wrong type for an option, or an invalid argument combination). |
|
|
175
|
+
| **Domain error** | The call was structurally correct, but a value violates a business rule (invalid `prefix`). |
|
|
176
|
+
|
|
177
|
+
Every custom error includes the `CpfGen::Error` marker module. Domain failures (`ValidationError`) inherit from `CpfGen::DomainError` (`RangeError`).
|
|
178
|
+
|
|
179
|
+
**Important:** passing both an `options` instance/`Hash` and any non-`nil` keyword argument raises `InvalidArgumentCombinationError`.
|
|
180
|
+
|
|
181
|
+
#### Summary
|
|
182
|
+
|
|
183
|
+
| Class | Inherits from | Category | Trigger condition |
|
|
184
|
+
|---|---|---|---|
|
|
185
|
+
| `CpfGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and any non-`nil` keyword argument are passed at once |
|
|
186
|
+
| `CpfGen::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | A generator option has the wrong data type |
|
|
187
|
+
| `CpfGen::ValidationError` | `CpfGen::DomainError` | Domain error | `prefix` is ineligible (zeroed base ID or 9 repeated digits) |
|
|
188
|
+
|
|
189
|
+
#### `CpfGen::Error` (marker module)
|
|
190
|
+
|
|
191
|
+
- **Inheritance:** module marker mixed into every library error via `include` (not a class).
|
|
192
|
+
- **Category:** N/A (rescue target only) — not a failure mode by itself.
|
|
193
|
+
- **When it is raised:** Never raised directly; included by every custom error the library raises.
|
|
194
|
+
- **Example:** N/A
|
|
195
|
+
- **How to rescue it:**
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
rescue CpfGen::Error
|
|
199
|
+
# everything this library raises
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### `CpfGen::DomainError`
|
|
203
|
+
|
|
204
|
+
- **Inheritance:** `CpfGen::DomainError < RangeError` (includes `CpfGen::Error`)
|
|
205
|
+
- **Category:** Domain error — ancestor for all domain failures.
|
|
206
|
+
- **When it is raised:** Not raised directly; prefer raising a leaf subclass.
|
|
207
|
+
- **Example:** Prefer `raise CpfGen::ValidationError` over raising `DomainError` directly.
|
|
208
|
+
- **How to rescue it:**
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
rescue CpfGen::DomainError
|
|
212
|
+
# ValidationError and other DomainError subclasses
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### `CpfGen::TypeMismatchError`
|
|
216
|
+
|
|
217
|
+
- **Inheritance:** `CpfGen::TypeMismatchError < TypeError` (includes `CpfGen::Error`)
|
|
218
|
+
- **Category:** API misuse — the caller passed a value of the wrong type.
|
|
219
|
+
- **When it is raised:** Raised when a generator option (`format` or `prefix`) has the wrong runtime type.
|
|
220
|
+
- **Example:**
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
CpfGen.cpf_gen(prefix: 123) # raises CpfGen::TypeMismatchError
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- **How to rescue it:**
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
rescue CpfGen::TypeMismatchError
|
|
230
|
+
# this library's type-contract violation
|
|
231
|
+
|
|
232
|
+
rescue TypeError
|
|
233
|
+
# native type errors, including this library's TypeMismatchError
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `CpfGen::InvalidArgumentCombinationError`
|
|
237
|
+
|
|
238
|
+
- **Inheritance:** `CpfGen::InvalidArgumentCombinationError < ArgumentError` (includes `CpfGen::Error`)
|
|
239
|
+
- **Category:** API misuse — the caller mixed mutually exclusive argument patterns.
|
|
240
|
+
- **When it is raised:** Raised when `CpfGenerator.new`, `#generate`, or `cpf_gen` receives both an `options` argument (instance or `Hash`) and any non-`nil` keyword argument at the same time.
|
|
241
|
+
- **Example:**
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
begin
|
|
245
|
+
CpfGen::CpfGenerator.new({ format: true }, prefix: '123')
|
|
246
|
+
rescue CpfGen::InvalidArgumentCombinationError => e
|
|
247
|
+
puts e.message
|
|
248
|
+
# Pass either an options instance/Hash to `options`, or keyword arguments (format:, prefix:), not both.
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- **How to rescue it:**
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
rescue CpfGen::InvalidArgumentCombinationError
|
|
256
|
+
# this library's invalid argument combination
|
|
257
|
+
|
|
258
|
+
rescue ArgumentError
|
|
259
|
+
# native argument errors, including this library's InvalidArgumentCombinationError
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### `CpfGen::ValidationError`
|
|
263
|
+
|
|
264
|
+
- **Inheritance:** `CpfGen::ValidationError < CpfGen::DomainError < RangeError` (includes `CpfGen::Error`)
|
|
265
|
+
- **Category:** Domain error — a value fails a non-numeric, non-length domain rule.
|
|
266
|
+
- **When it is raised:** Raised when `prefix` is ineligible (zeroed base ID `"000000000"`, or 9 repeated digits such as `"999999999"`).
|
|
267
|
+
- **Example:**
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
CpfGen.cpf_gen(prefix: '000000000') # raises CpfGen::ValidationError
|
|
271
|
+
CpfGen.cpf_gen(prefix: '999999999') # raises CpfGen::ValidationError
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- **How to rescue it:**
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
rescue CpfGen::ValidationError
|
|
278
|
+
# this exact domain validation failure
|
|
279
|
+
|
|
280
|
+
rescue CpfGen::DomainError
|
|
281
|
+
# RangeError-rooted domain failures from this library
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### Rescue granularity
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
# 1) Single native class — catches type misuse from this library (and other TypeErrors).
|
|
288
|
+
rescue TypeError
|
|
289
|
+
# CpfGen::TypeMismatchError and any other TypeError (library or not)
|
|
290
|
+
|
|
291
|
+
# 2) CpfGen::DomainError — catches business-rule violations under DomainError.
|
|
292
|
+
rescue CpfGen::DomainError
|
|
293
|
+
# CpfGen::ValidationError and other DomainError subclasses
|
|
294
|
+
|
|
295
|
+
# 3) CpfGen::Error — catches everything the library raises.
|
|
296
|
+
rescue CpfGen::Error
|
|
297
|
+
# every custom error that includes CpfGen::Error
|
|
298
|
+
|
|
299
|
+
# 4) Specific leaf class — catches only that exact failure mode.
|
|
300
|
+
rescue CpfGen::ValidationError
|
|
301
|
+
# only CpfGen::ValidationError
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Notable attributes:
|
|
305
|
+
|
|
306
|
+
- `TypeMismatchError`: `option_name`, `actual_input`, `actual_type`, `expected_type`
|
|
307
|
+
- `ValidationError`: `option_name`, `actual_input`, `reason` (prefix failures); `expected_values` is always `nil` for CPF
|
|
308
|
+
|
|
309
|
+
Property setters never accept `nil` directly — pass the matching `DEFAULT_*` constant to reset:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
options = CpfGen::CpfGeneratorOptions.new
|
|
313
|
+
begin
|
|
314
|
+
options.prefix = nil
|
|
315
|
+
rescue CpfGen::TypeMismatchError => e
|
|
316
|
+
puts e.message
|
|
317
|
+
# CPF generator option "prefix" must be of type string. Got nil.
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
options.prefix = CpfGen::CpfGeneratorOptions::DEFAULT_PREFIX # explicit reset instead
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Check-digit computation failures from `cpf-dv` are handled internally by retrying generation with the same resolved options; they are not raised to callers under normal operation.
|
|
324
|
+
|
|
325
|
+
### Other available resources
|
|
326
|
+
|
|
327
|
+
- **`CpfGen::CpfGeneratorOptions::CPF_LENGTH`**: `11`.
|
|
328
|
+
- **`CpfGen::CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH`**: `9`.
|
|
329
|
+
- **`CpfGen::CpfGeneratorOptions::DEFAULT_FORMAT`**, **`DEFAULT_PREFIX`**: Class-level default constants.
|
|
330
|
+
|
|
331
|
+
## Contribution & Support
|
|
332
|
+
|
|
333
|
+
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:
|
|
334
|
+
|
|
335
|
+
- ⭐ Starring the repository
|
|
336
|
+
- 🤝 Contributing to the codebase
|
|
337
|
+
- 💡 [Suggesting new features](https://github.com/LacusSolutions/br-utils-ruby/issues)
|
|
338
|
+
- 🐛 [Reporting bugs](https://github.com/LacusSolutions/br-utils-ruby/issues)
|
|
339
|
+
|
|
340
|
+
## License
|
|
341
|
+
|
|
342
|
+
This project is licensed under the MIT License — see the [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE) file for details.
|
|
343
|
+
|
|
344
|
+
## Changelog
|
|
345
|
+
|
|
346
|
+
See [CHANGELOG](./CHANGELOG.md) for a list of changes and version history.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
Made with ❤️ by [Lacus Solutions](https://github.com/LacusSolutions)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'cpf_generator'
|
|
4
|
+
|
|
5
|
+
module CpfGen
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Helper function to simplify the usage of the {CpfGenerator} class.
|
|
9
|
+
#
|
|
10
|
+
# If no options are provided, it generates an 11-digit unformatted numeric CPF
|
|
11
|
+
# (e.g. +"47844241055"+) using default settings. If options are provided, they
|
|
12
|
+
# control +prefix+ and whether the result is formatted.
|
|
13
|
+
#
|
|
14
|
+
# Generates a valid 11-digit CPF (+prefix+, random numeric body, and computed
|
|
15
|
+
# check digits). With default options the result is unformatted numeric; pass
|
|
16
|
+
# +format: true+ for +000.000.000-00+ style output.
|
|
17
|
+
#
|
|
18
|
+
# @param options [CpfGeneratorOptions, Hash, nil] generator options
|
|
19
|
+
# @param keywords [Hash] option keyword overrides (mutually exclusive with +options+;
|
|
20
|
+
# see {CpfGeneratorOptions})
|
|
21
|
+
# @return [String] generated CPF
|
|
22
|
+
# @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument are both given
|
|
23
|
+
# @raise [TypeMismatchError] if any option has an invalid type
|
|
24
|
+
# @raise [ValidationError] if +prefix+ is invalid
|
|
25
|
+
# @see CpfGenerator for detailed option descriptions
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# CpfGen.cpf_gen # => "47844241055"
|
|
29
|
+
def cpf_gen(options = nil, **keywords)
|
|
30
|
+
CpfGenerator.new(options, **keywords).generate
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cpf-dv'
|
|
4
|
+
require 'lacus-utils'
|
|
5
|
+
|
|
6
|
+
require_relative 'cpf_generator_options'
|
|
7
|
+
|
|
8
|
+
module CpfGen
|
|
9
|
+
# Generator for CPF (Cadastro de Pessoa Física) identifiers. Builds valid
|
|
10
|
+
# 11-digit CPF values by combining an optional +prefix+ with a randomly
|
|
11
|
+
# generated sequence and computed check digits. Options control +prefix+ and
|
|
12
|
+
# whether the result is formatted (+000.000.000-00+).
|
|
13
|
+
class CpfGenerator
|
|
14
|
+
# Returns the default options used by this generator 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 {CpfGeneratorOptions}) affects future {#generate} calls that
|
|
19
|
+
# do not pass +options+.
|
|
20
|
+
#
|
|
21
|
+
# @return [CpfGeneratorOptions] the instance default options
|
|
22
|
+
attr_reader :options
|
|
23
|
+
|
|
24
|
+
# Creates a new {CpfGenerator} with optional default options.
|
|
25
|
+
#
|
|
26
|
+
# Default options apply to every call to {#generate} unless overridden by the
|
|
27
|
+
# per-call +options+ argument or keyword overrides. Options control +prefix+
|
|
28
|
+
# and whether the generated CPF is formatted.
|
|
29
|
+
#
|
|
30
|
+
# +options+ and the keyword arguments are never merged with each other: when
|
|
31
|
+
# +options+ is given (a {CpfGeneratorOptions} instance or a {Hash}), it alone
|
|
32
|
+
# determines the default options; otherwise, the default options are built
|
|
33
|
+
# exclusively from the keyword arguments, with {CpfGeneratorOptions} 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 {CpfGeneratorOptions} 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 {#generate} calls that do
|
|
41
|
+
# not pass per-call options. When a plain {Hash} is passed instead, a new
|
|
42
|
+
# {CpfGeneratorOptions} instance is created from it.
|
|
43
|
+
#
|
|
44
|
+
# @param options [CpfGeneratorOptions, Hash, nil] default options
|
|
45
|
+
# @param keywords [Hash] option keyword overrides (mutually exclusive with +options+;
|
|
46
|
+
# see {CpfGeneratorOptions})
|
|
47
|
+
# @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument are both given
|
|
48
|
+
# @raise [TypeMismatchError] if any option has an invalid type
|
|
49
|
+
# @raise [ValidationError] if +prefix+ is invalid
|
|
50
|
+
def initialize(options = nil, **keywords)
|
|
51
|
+
@options = resolve_default_options(options, keywords)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Generates a valid CPF value.
|
|
55
|
+
#
|
|
56
|
+
# Builds an 11-digit CPF from the configured +prefix+ (if any), a random
|
|
57
|
+
# numeric sequence, and two computed check digits. If formatting is enabled,
|
|
58
|
+
# the result is returned as +000.000.000-00+.
|
|
59
|
+
#
|
|
60
|
+
# +options+ and the keyword arguments are never merged with each other: when
|
|
61
|
+
# +options+ is given (a {CpfGeneratorOptions} instance or a {Hash}), it alone
|
|
62
|
+
# overrides the instance default options for this call; otherwise, any
|
|
63
|
+
# non-+nil+ keyword argument overrides the instance default options for this
|
|
64
|
+
# call. When neither +options+ nor any keyword argument is given, the
|
|
65
|
+
# instance default options are used as-is. In every case, the instance
|
|
66
|
+
# default options themselves are left unchanged. Passing +options+ together
|
|
67
|
+
# with any non-+nil+ keyword argument raises {InvalidArgumentCombinationError}
|
|
68
|
+
# instead of silently ignoring the keywords.
|
|
69
|
+
#
|
|
70
|
+
# @param options [CpfGeneratorOptions, Hash, nil] per-call option overrides
|
|
71
|
+
# @param keywords [Hash] per-call option keyword overrides (mutually exclusive
|
|
72
|
+
# with +options+; see {CpfGeneratorOptions})
|
|
73
|
+
# @return [String] generated CPF
|
|
74
|
+
# @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument are both given
|
|
75
|
+
# @raise [TypeMismatchError] if any option has an invalid type
|
|
76
|
+
# @raise [ValidationError] if +prefix+ is invalid
|
|
77
|
+
def generate(options = nil, **keywords)
|
|
78
|
+
actual_options = resolve_call_options(options, keywords)
|
|
79
|
+
generated_cpf = build_base_cpf(actual_options)
|
|
80
|
+
|
|
81
|
+
with_check_digits(generated_cpf, options, keywords) do |cpf_with_digits|
|
|
82
|
+
actual_options.format ? Utils.format_cpf(cpf_with_digits) : cpf_with_digits
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def resolve_default_options(options, keywords)
|
|
89
|
+
keyword_overrides = compact_keyword_overrides(keywords)
|
|
90
|
+
raise_ambiguous_options! if options && !keyword_overrides.empty?
|
|
91
|
+
return options if options.is_a?(CpfGeneratorOptions)
|
|
92
|
+
return CpfGeneratorOptions.new(options) if options
|
|
93
|
+
|
|
94
|
+
CpfGeneratorOptions.new(**keywords)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resolve_call_options(options, keywords)
|
|
98
|
+
keyword_overrides = compact_keyword_overrides(keywords)
|
|
99
|
+
raise_ambiguous_options! if options && !keyword_overrides.empty?
|
|
100
|
+
return @options.copy.set(options) if options
|
|
101
|
+
return @options if keyword_overrides.empty?
|
|
102
|
+
|
|
103
|
+
@options.copy.set(keyword_overrides)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def compact_keyword_overrides(keywords)
|
|
107
|
+
CpfGeneratorOptions::OPTION_KEYS.each_with_object({}) do |key, overrides|
|
|
108
|
+
value = keywords[key]
|
|
109
|
+
overrides[key] = value unless value.nil?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def raise_ambiguous_options!
|
|
114
|
+
option_keywords = CpfGeneratorOptions::OPTION_KEYS.map { |key| "#{key}:" }.join(', ')
|
|
115
|
+
|
|
116
|
+
raise InvalidArgumentCombinationError,
|
|
117
|
+
"Pass either an options instance/Hash to `options`, or keyword arguments (#{option_keywords}), " \
|
|
118
|
+
'not both.'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_base_cpf(actual_options)
|
|
122
|
+
digits_to_generate = CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH - actual_options.prefix.length
|
|
123
|
+
|
|
124
|
+
actual_options.prefix + LacusUtils.generate_random_sequence(
|
|
125
|
+
digits_to_generate,
|
|
126
|
+
:numeric
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def with_check_digits(generated_cpf, options, keywords)
|
|
131
|
+
cpf_with_digits = CpfDV::CpfCheckDigits.new(generated_cpf).cpf
|
|
132
|
+
|
|
133
|
+
yield cpf_with_digits
|
|
134
|
+
rescue CpfDV::DomainError
|
|
135
|
+
generate(options, **keywords)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'types'
|
|
4
|
+
|
|
5
|
+
module CpfGen
|
|
6
|
+
# Layered option-resolution helpers for {CpfGeneratorOptions}.
|
|
7
|
+
module GeneratorOptionsResolution
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def fold_layers(layers)
|
|
11
|
+
resolved = {}
|
|
12
|
+
|
|
13
|
+
layers.each do |layer|
|
|
14
|
+
source = layer_source(layer)
|
|
15
|
+
next if source.nil?
|
|
16
|
+
|
|
17
|
+
CpfGeneratorOptions::OPTION_KEYS.each do |key|
|
|
18
|
+
value = Utils.fetch_option(source, key)
|
|
19
|
+
resolved[key] = value unless value.nil?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
resolved
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def layer_source(layer)
|
|
27
|
+
return layer.all if layer.is_a?(CpfGeneratorOptions)
|
|
28
|
+
return layer if layer.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def apply_keyword_overrides(resolved, keywords)
|
|
34
|
+
CpfGeneratorOptions::OPTION_KEYS.each do |key|
|
|
35
|
+
value = keywords[key]
|
|
36
|
+
resolved[key] = value unless value.nil?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def assign_resolved_or_default(resolved)
|
|
41
|
+
CpfGeneratorOptions::OPTION_KEYS.each do |key|
|
|
42
|
+
value = resolved.key?(key) ? resolved[key] : CpfGeneratorOptions::DEFAULTS[key]
|
|
43
|
+
public_send("#{key}=", value)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Property accessors for {CpfGeneratorOptions}. Kept as a sibling module in this
|
|
49
|
+
# file (not a separate public API) so the options class stays under RuboCop's
|
|
50
|
+
# +Metrics/ClassLength+ budget.
|
|
51
|
+
module CpfGeneratorOptionProperties
|
|
52
|
+
# Returns a shallow copy of all current options.
|
|
53
|
+
#
|
|
54
|
+
# Exposes resolved +format+ and +prefix+ values. This is useful for creating
|
|
55
|
+
# snapshots of the current configuration.
|
|
56
|
+
#
|
|
57
|
+
# @return [Hash{Symbol => Object}] shallow copy of option values
|
|
58
|
+
def all
|
|
59
|
+
@options.dup
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns whether the generated CPF string will have the standard formatting
|
|
63
|
+
# (+000.000.000-00+).
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def format
|
|
67
|
+
@options[:format]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Sets whether the generated CPF string will have the standard formatting
|
|
71
|
+
# (+000.000.000-00+). The value is converted to a boolean, so truthy/falsy
|
|
72
|
+
# values are handled appropriately.
|
|
73
|
+
#
|
|
74
|
+
# +nil+ is not accepted: pass {CpfGeneratorOptions::DEFAULT_FORMAT} explicitly
|
|
75
|
+
# to reset this option to its default value.
|
|
76
|
+
#
|
|
77
|
+
# @param value [Boolean] enable formatting when truthy
|
|
78
|
+
# @raise [TypeMismatchError] if the value is +nil+
|
|
79
|
+
def format=(value)
|
|
80
|
+
raise TypeMismatchError.new(value, 'boolean', option_name: 'format') if value.nil?
|
|
81
|
+
|
|
82
|
+
@options[:format] = Utils.normalize_boolean(value)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns the string used as the initial string of the generated CPF.
|
|
86
|
+
#
|
|
87
|
+
# Note: If the evaluated +prefix+ (after stripping non-digit characters) is
|
|
88
|
+
# longer than 9 digits, the extra digits are ignored, because a CPF has 9
|
|
89
|
+
# base digits followed by 2 calculated check digits.
|
|
90
|
+
#
|
|
91
|
+
# @return [String]
|
|
92
|
+
def prefix
|
|
93
|
+
@options[:prefix]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Sets the string used as the initial string of the generated CPF. Only
|
|
97
|
+
# digits are kept and the rest is stripped. If provided, only the missing
|
|
98
|
+
# digits are generated randomly. For example, if the +prefix+ +"123456"+ (6
|
|
99
|
+
# digits) is given, only the next 3 digits are randomly generated and
|
|
100
|
+
# concatenated to the +prefix+.
|
|
101
|
+
#
|
|
102
|
+
# Note: If the evaluated +prefix+ (after stripping non-digit characters) is
|
|
103
|
+
# longer than 9 digits, the extra digits are ignored, because a CPF has 9
|
|
104
|
+
# base digits followed by 2 calculated check digits.
|
|
105
|
+
#
|
|
106
|
+
# +nil+ is not accepted: pass {CpfGeneratorOptions::DEFAULT_PREFIX} explicitly
|
|
107
|
+
# to reset this option to its default value.
|
|
108
|
+
#
|
|
109
|
+
# @param value [String] partial start string
|
|
110
|
+
# @raise [TypeMismatchError] if the value is not a +String+
|
|
111
|
+
# @raise [ValidationError] if +prefix+ is invalid
|
|
112
|
+
def prefix=(value)
|
|
113
|
+
actual_prefix = Utils.sanitize_prefix(value)
|
|
114
|
+
|
|
115
|
+
Utils.validate_prefix!(actual_prefix)
|
|
116
|
+
|
|
117
|
+
@options[:prefix] = actual_prefix
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Stores configuration for the CPF generator.
|
|
122
|
+
#
|
|
123
|
+
# Provides a centralized way to configure how CPF digits are generated,
|
|
124
|
+
# including partial start string (+prefix+) and formatting (+format+).
|
|
125
|
+
class CpfGeneratorOptions
|
|
126
|
+
include GeneratorOptionsResolution
|
|
127
|
+
include CpfGeneratorOptionProperties
|
|
128
|
+
|
|
129
|
+
# The standard length of a CPF (Cadastro de Pessoa Física) identifier (11
|
|
130
|
+
# digits).
|
|
131
|
+
CPF_LENGTH = 11
|
|
132
|
+
|
|
133
|
+
# Maximum length of the +prefix+ of a CPF.
|
|
134
|
+
CPF_PREFIX_MAX_LENGTH = CPF_LENGTH - 2
|
|
135
|
+
|
|
136
|
+
# Default value for the +format+ option. When +true+, the generated CPF
|
|
137
|
+
# string will have the standard formatting (+000.000.000-00+).
|
|
138
|
+
DEFAULT_FORMAT = false
|
|
139
|
+
|
|
140
|
+
# Default string used as the initial string of the generated CPF.
|
|
141
|
+
DEFAULT_PREFIX = ''
|
|
142
|
+
|
|
143
|
+
CPF_BASE_ID_LENGTH = 9
|
|
144
|
+
CPF_BASE_ID_LAST_INDEX = CPF_BASE_ID_LENGTH - 1
|
|
145
|
+
ZEROED_CPF_BASE_ID = '0' * CPF_BASE_ID_LENGTH
|
|
146
|
+
|
|
147
|
+
PREFIX_SANITIZE_PATTERN = /\D/
|
|
148
|
+
|
|
149
|
+
# Option keys managed by this class, in assignment order.
|
|
150
|
+
OPTION_KEYS = %i[format prefix].freeze
|
|
151
|
+
|
|
152
|
+
# Default value for each key in {OPTION_KEYS}, used to fill any option that
|
|
153
|
+
# is still unresolved once {#initialize} finishes merging its arguments.
|
|
154
|
+
DEFAULTS = {
|
|
155
|
+
format: DEFAULT_FORMAT,
|
|
156
|
+
prefix: DEFAULT_PREFIX
|
|
157
|
+
}.freeze
|
|
158
|
+
|
|
159
|
+
# Creates a new {CpfGeneratorOptions} instance.
|
|
160
|
+
#
|
|
161
|
+
# Options are resolved in three steps. Each step only overrides a key when it
|
|
162
|
+
# is given a non-+nil+ value; a +nil+ is always ignored in favor of whatever
|
|
163
|
+
# was resolved by a previous step.
|
|
164
|
+
#
|
|
165
|
+
# 1. Every positional +options+ layer (each either a {Hash} or another
|
|
166
|
+
# {CpfGeneratorOptions} instance) is folded left to right, so later
|
|
167
|
+
# layers take precedence over earlier ones.
|
|
168
|
+
# 2. The +format+ and +prefix+ keyword arguments are then applied on top of
|
|
169
|
+
# the folded layers. Keywords always have the highest precedence,
|
|
170
|
+
# overriding every positional layer.
|
|
171
|
+
# 3. Any option that is still unresolved after steps 1 and 2 is assigned its
|
|
172
|
+
# +DEFAULT_*+ value (see {DEFAULTS}).
|
|
173
|
+
#
|
|
174
|
+
# Because every option is fully resolved to a concrete, non-+nil+ value
|
|
175
|
+
# before assignment, the individual property setters (e.g. {#format=}) never
|
|
176
|
+
# receive +nil+ from this method — they always raise if given +nil+ directly.
|
|
177
|
+
#
|
|
178
|
+
# @param options [Array<CpfGeneratorOptions, Hash>] option layers merged in
|
|
179
|
+
# order (later layers win); a missing or +nil+ value for a key inside a
|
|
180
|
+
# layer is ignored and the previously resolved value is kept
|
|
181
|
+
# @param keywords [Hash] highest-precedence option overrides (see {OPTION_KEYS})
|
|
182
|
+
# @raise [TypeMismatchError] if any option has an invalid type
|
|
183
|
+
# @raise [ValidationError] if +prefix+ is invalid
|
|
184
|
+
def initialize(*options, **keywords)
|
|
185
|
+
@options = {}
|
|
186
|
+
|
|
187
|
+
resolved = fold_layers(options)
|
|
188
|
+
apply_keyword_overrides(resolved, keywords)
|
|
189
|
+
assign_resolved_or_default(resolved)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Sets multiple options at once, following the same layered-override
|
|
193
|
+
# semantics as {#initialize} (positional layers folded left to right, then
|
|
194
|
+
# keyword arguments applied with the highest precedence; +nil+ is always
|
|
195
|
+
# ignored). Unlike {#initialize}, any option that is still unresolved after
|
|
196
|
+
# merging keeps its **current** value on this instance instead of falling
|
|
197
|
+
# back to its default — this method performs a partial update, not a
|
|
198
|
+
# re-initialization.
|
|
199
|
+
#
|
|
200
|
+
# @param options [Array<CpfGeneratorOptions, Hash>] option layers merged in
|
|
201
|
+
# order (later layers win)
|
|
202
|
+
# @param keywords [Hash] highest-precedence option overrides (see {OPTION_KEYS})
|
|
203
|
+
# @return [CpfGeneratorOptions] +self+
|
|
204
|
+
# @raise [TypeMismatchError] if any option has an invalid type
|
|
205
|
+
# @raise [ValidationError] if +prefix+ is invalid
|
|
206
|
+
def set(*options, **keywords)
|
|
207
|
+
resolved = fold_layers(options)
|
|
208
|
+
apply_keyword_overrides(resolved, keywords)
|
|
209
|
+
resolved.each { |key, value| public_send("#{key}=", value) }
|
|
210
|
+
self
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Returns a shallow copy of this options instance.
|
|
214
|
+
#
|
|
215
|
+
# @return [CpfGeneratorOptions] duplicated options for per-call merging
|
|
216
|
+
def copy
|
|
217
|
+
duplicate = self.class.allocate
|
|
218
|
+
duplicate.instance_variable_set(:@options, @options.dup)
|
|
219
|
+
duplicate
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lacus-utils'
|
|
4
|
+
|
|
5
|
+
module CpfGen
|
|
6
|
+
# Marker module mixed into every custom error raised by this library.
|
|
7
|
+
#
|
|
8
|
+
# Use +rescue CpfGen::Error+ to catch every library error regardless of native
|
|
9
|
+
# ancestry.
|
|
10
|
+
module Error; end
|
|
11
|
+
|
|
12
|
+
# API misuse error raised when an argument's runtime type does not match the
|
|
13
|
+
# type required by the API contract (a generator option).
|
|
14
|
+
class TypeMismatchError < TypeError
|
|
15
|
+
include Error
|
|
16
|
+
|
|
17
|
+
# @return [Object] the offending input value
|
|
18
|
+
attr_reader :actual_input
|
|
19
|
+
|
|
20
|
+
# @return [String] human-readable type of {#actual_input}
|
|
21
|
+
attr_reader :actual_type
|
|
22
|
+
|
|
23
|
+
# @return [String] description of the expected type
|
|
24
|
+
attr_reader :expected_type
|
|
25
|
+
|
|
26
|
+
# @return [String] the offending option key
|
|
27
|
+
attr_reader :option_name
|
|
28
|
+
|
|
29
|
+
# @param actual_input [Object] the offending option value
|
|
30
|
+
# @param expected_type [String] description of the expected type
|
|
31
|
+
# @param option_name [String] option key when the failure is option-related
|
|
32
|
+
def initialize(actual_input, expected_type, option_name:)
|
|
33
|
+
actual_type = LacusUtils.describe_type(actual_input)
|
|
34
|
+
super(
|
|
35
|
+
%(CPF generator option "#{option_name}" must be of type #{expected_type}. Got #{actual_type}.)
|
|
36
|
+
)
|
|
37
|
+
@actual_input = actual_input
|
|
38
|
+
@actual_type = actual_type
|
|
39
|
+
@expected_type = expected_type
|
|
40
|
+
@option_name = option_name
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# API misuse error raised when the combination of provided arguments does not
|
|
45
|
+
# match any valid overload-style signature.
|
|
46
|
+
class InvalidArgumentCombinationError < ArgumentError
|
|
47
|
+
include Error
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Domain error ancestor for business-rule failures (validation and other domain
|
|
51
|
+
# leaves). Prefer raising a leaf subclass.
|
|
52
|
+
class DomainError < RangeError
|
|
53
|
+
include Error
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Domain error raised when a generator option has a valid type but violates a
|
|
57
|
+
# domain rule (invalid +prefix+). Structurally mirrors {CnpjGen::ValidationError}
|
|
58
|
+
# (including +expected_values+) even though CPF has no +type+ option that would
|
|
59
|
+
# populate that field.
|
|
60
|
+
class ValidationError < DomainError
|
|
61
|
+
# @return [String] the offending option name
|
|
62
|
+
attr_reader :option_name
|
|
63
|
+
|
|
64
|
+
# @return [Object] the offending option value
|
|
65
|
+
attr_reader :actual_input
|
|
66
|
+
|
|
67
|
+
# @return [String, nil] human-readable reason when the failure is prefix-related
|
|
68
|
+
attr_reader :reason
|
|
69
|
+
|
|
70
|
+
# @return [Array<String>, nil] reserved for structural parity with CNPJ; always
|
|
71
|
+
# +nil+ for CPF prefix validation
|
|
72
|
+
attr_reader :expected_values
|
|
73
|
+
|
|
74
|
+
# @param option_name [String] the offending option key
|
|
75
|
+
# @param actual_input [Object] the offending option value
|
|
76
|
+
# @param reason [String, nil] human-readable reason for a prefix failure
|
|
77
|
+
# @param expected_values [Array<String>, nil] unused for CPF (always +nil+)
|
|
78
|
+
def initialize(option_name, actual_input, reason: nil, expected_values: nil)
|
|
79
|
+
super(build_message(option_name, actual_input, reason, expected_values))
|
|
80
|
+
@option_name = option_name
|
|
81
|
+
@actual_input = actual_input
|
|
82
|
+
@reason = reason
|
|
83
|
+
@expected_values = expected_values&.dup&.freeze
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def build_message(option_name, actual_input, reason, expected_values)
|
|
89
|
+
if expected_values
|
|
90
|
+
quoted = expected_values.map { |value| %("#{value}") }.join(', ')
|
|
91
|
+
"CPF generator option \"#{option_name}\" accepts only the following values: #{quoted}. " \
|
|
92
|
+
"Got \"#{actual_input}\"."
|
|
93
|
+
else
|
|
94
|
+
"CPF generator option \"#{option_name}\" with value \"#{actual_input}\" is invalid. #{reason}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CpfGen
|
|
4
|
+
# Options input accepted by constructors and merge helpers.
|
|
5
|
+
#
|
|
6
|
+
# May be a {CpfGeneratorOptions} instance, a {Hash} of option keys, or +nil+.
|
|
7
|
+
#
|
|
8
|
+
# Resolved options contain:
|
|
9
|
+
#
|
|
10
|
+
# - +format+ [Boolean] — whether to format the generated CPF string as
|
|
11
|
+
# +000.000.000-00+ (default: +false+).
|
|
12
|
+
# - +prefix+ [String] — a partial string containing 0 to 9 digits to use as the
|
|
13
|
+
# start of the generated CPF. Only digits are kept; the rest is stripped. If
|
|
14
|
+
# provided, only the missing digits are generated randomly. For example, if
|
|
15
|
+
# the +prefix+ +"123456"+ (6 digits) is given, only the next 3 digits are
|
|
16
|
+
# randomly generated and concatenated to the +prefix+.
|
|
17
|
+
#
|
|
18
|
+
# Note: If the evaluated +prefix+ (after stripping non-digit characters) is
|
|
19
|
+
# longer than 9 digits, the extra digits are ignored, because a CPF has 9
|
|
20
|
+
# base digits followed by 2 calculated check digits.
|
|
21
|
+
#
|
|
22
|
+
# @see CpfGeneratorOptions
|
|
23
|
+
CpfGeneratorOptionsInput = Object
|
|
24
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CpfGen
|
|
4
|
+
# Low-level helpers used by {CpfGenerator} and {CpfGeneratorOptions}.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
module Utils
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# rubocop:disable Naming/PredicateMethod -- coercion helper, not a predicate query
|
|
11
|
+
def normalize_boolean(value)
|
|
12
|
+
return false if [false, '', 0].include?(value)
|
|
13
|
+
|
|
14
|
+
!!value
|
|
15
|
+
end
|
|
16
|
+
# rubocop:enable Naming/PredicateMethod
|
|
17
|
+
|
|
18
|
+
def assert_string_option!(option_name, value)
|
|
19
|
+
return if value.is_a?(String)
|
|
20
|
+
|
|
21
|
+
raise TypeMismatchError.new(value, 'string', option_name: option_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch_option(source, key)
|
|
25
|
+
return source[key] if source.key?(key)
|
|
26
|
+
return source[key.to_s] if source.key?(key.to_s)
|
|
27
|
+
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sanitize_prefix(value)
|
|
32
|
+
assert_string_option!('prefix', value)
|
|
33
|
+
|
|
34
|
+
sanitized = value.gsub(CpfGeneratorOptions::PREFIX_SANITIZE_PATTERN, '')
|
|
35
|
+
sanitized[0, CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_prefix!(partial_cpf)
|
|
39
|
+
validate_prefix_base_id!(partial_cpf)
|
|
40
|
+
validate_prefix_non_repeated_digits!(partial_cpf)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def validate_prefix_base_id!(partial_cpf)
|
|
44
|
+
return if partial_cpf.length < CpfGeneratorOptions::CPF_BASE_ID_LENGTH
|
|
45
|
+
|
|
46
|
+
cpf_base_id = partial_cpf[0, CpfGeneratorOptions::CPF_BASE_ID_LAST_INDEX + 1]
|
|
47
|
+
|
|
48
|
+
return unless cpf_base_id == CpfGeneratorOptions::ZEROED_CPF_BASE_ID
|
|
49
|
+
|
|
50
|
+
raise ValidationError.new(
|
|
51
|
+
'prefix',
|
|
52
|
+
partial_cpf,
|
|
53
|
+
reason: 'Zeroed base ID is not eligible.'
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_prefix_non_repeated_digits!(partial_cpf)
|
|
58
|
+
return if partial_cpf.length < CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH
|
|
59
|
+
|
|
60
|
+
first_character = partial_cpf[0]
|
|
61
|
+
|
|
62
|
+
return unless partial_cpf == first_character * CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH
|
|
63
|
+
|
|
64
|
+
raise ValidationError.new(
|
|
65
|
+
'prefix',
|
|
66
|
+
partial_cpf,
|
|
67
|
+
reason: 'Repeated digits are not considered valid.'
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Formats a raw 11-digit CPF into the standard masked representation.
|
|
72
|
+
def format_cpf(raw)
|
|
73
|
+
"#{raw[0, 3]}.#{raw[3, 3]}.#{raw[6, 3]}-#{raw[9, 2]}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/src/cpf-gen/version.rb
CHANGED
data/src/cpf-gen.rb
CHANGED
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'cpf-dv'
|
|
4
3
|
require_relative 'cpf-gen/version'
|
|
4
|
+
require_relative 'cpf-gen/errors'
|
|
5
|
+
require_relative 'cpf-gen/types'
|
|
6
|
+
require_relative 'cpf-gen/cpf_generator_options'
|
|
7
|
+
require_relative 'cpf-gen/utils'
|
|
8
|
+
require_relative 'cpf-gen/cpf_generator'
|
|
9
|
+
require_relative 'cpf-gen/cpf_gen'
|
|
5
10
|
|
|
11
|
+
# Generates valid CPF (Cadastro de Pessoa Física) identifiers.
|
|
12
|
+
#
|
|
13
|
+
# Errors fall into two categories:
|
|
14
|
+
#
|
|
15
|
+
# - *API misuse* — the caller supplied a wrong type or an incompatible argument
|
|
16
|
+
# combination. Raised as {CpfGen::TypeMismatchError} or
|
|
17
|
+
# {CpfGen::InvalidArgumentCombinationError}.
|
|
18
|
+
# - *Domain errors* — the call shape was valid, but a value violates a business
|
|
19
|
+
# rule. Invalid +prefix+ values raise {CpfGen::ValidationError} under
|
|
20
|
+
# {CpfGen::DomainError}.
|
|
21
|
+
#
|
|
22
|
+
# Every custom error includes the {CpfGen::Error} marker module so consumers can
|
|
23
|
+
# +rescue CpfGen::Error+ for a library-wide catch.
|
|
24
|
+
#
|
|
25
|
+
# Public API:
|
|
26
|
+
#
|
|
27
|
+
# - {CpfGen.cpf_gen}
|
|
28
|
+
# - {CpfGen::CpfGenerator}, {CpfGen::CpfGeneratorOptions}
|
|
29
|
+
# - {CpfGen::CPF_LENGTH}, {CpfGen::CPF_PREFIX_MAX_LENGTH}
|
|
30
|
+
# - Error marker {CpfGen::Error}; domain ancestor {CpfGen::DomainError};
|
|
31
|
+
# misuse errors {CpfGen::TypeMismatchError} and
|
|
32
|
+
# {CpfGen::InvalidArgumentCombinationError}; domain leaf
|
|
33
|
+
# {CpfGen::ValidationError}
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# require 'cpf-gen'
|
|
37
|
+
#
|
|
38
|
+
# CpfGen.cpf_gen # => e.g. "47844241055"
|
|
6
39
|
module CpfGen
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
40
|
+
# The standard length of a CPF (Cadastro de Pessoa Física) identifier (11
|
|
41
|
+
# digits).
|
|
42
|
+
CPF_LENGTH = CpfGeneratorOptions::CPF_LENGTH
|
|
43
|
+
|
|
44
|
+
# Maximum length of the +prefix+ of a CPF.
|
|
45
|
+
CPF_PREFIX_MAX_LENGTH = CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH
|
|
10
46
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cpf-gen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julio L. Muller
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: cpf-dv
|
|
@@ -16,26 +16,62 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
19
|
+
version: 1.0.0
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 1.1.0
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
27
|
- - ">="
|
|
25
28
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
27
|
-
|
|
29
|
+
version: 1.0.0
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.1.0
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: lacus-utils
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 1.1.0
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: 2.0.0
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: 1.1.0
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 2.0.0
|
|
53
|
+
description: Utility to generate CPF (Brazilian Individual's Taxpayer ID)
|
|
28
54
|
email:
|
|
55
|
+
- juliolmuller@outlook.com
|
|
29
56
|
executables: []
|
|
30
57
|
extensions: []
|
|
31
58
|
extra_rdoc_files: []
|
|
32
59
|
files:
|
|
60
|
+
- LICENSE
|
|
61
|
+
- README.md
|
|
33
62
|
- src/cpf-gen.rb
|
|
63
|
+
- src/cpf-gen/cpf_gen.rb
|
|
64
|
+
- src/cpf-gen/cpf_generator.rb
|
|
65
|
+
- src/cpf-gen/cpf_generator_options.rb
|
|
66
|
+
- src/cpf-gen/errors.rb
|
|
67
|
+
- src/cpf-gen/types.rb
|
|
68
|
+
- src/cpf-gen/utils.rb
|
|
34
69
|
- src/cpf-gen/version.rb
|
|
35
70
|
homepage: https://github.com/LacusSolutions/br-utils-ruby
|
|
36
71
|
licenses:
|
|
37
72
|
- MIT
|
|
38
73
|
metadata:
|
|
74
|
+
source_code_uri: https://github.com/LacusSolutions/br-utils-ruby
|
|
39
75
|
rubygems_mfa_required: 'true'
|
|
40
76
|
post_install_message:
|
|
41
77
|
rdoc_options: []
|
|
@@ -45,7 +81,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
45
81
|
requirements:
|
|
46
82
|
- - ">="
|
|
47
83
|
- !ruby/object:Gem::Version
|
|
48
|
-
version: '3.
|
|
84
|
+
version: '3.1'
|
|
49
85
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
86
|
requirements:
|
|
51
87
|
- - ">="
|
|
@@ -55,5 +91,5 @@ requirements: []
|
|
|
55
91
|
rubygems_version: 3.4.19
|
|
56
92
|
signing_key:
|
|
57
93
|
specification_version: 4
|
|
58
|
-
summary: Generate
|
|
94
|
+
summary: Generate CPF (Brazilian Individual's Taxpayer ID)
|
|
59
95
|
test_files: []
|