cpf-dv 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3602764d5f2d48821c34fad754e596eaceae640ec7ee8f4909be10d256d5437
4
- data.tar.gz: 3aba3575cf38941dd1a0cc98d8e0630e08bba5fb293987107b9d27043c721f7d
3
+ metadata.gz: c4c1b2490462860148180946616219c920d96507426d6fc6c848dc1dca7b74c2
4
+ data.tar.gz: 80fa518ec814ef85f84b4c76a78105e895e24acaf3ae88f95a86e830286f67b1
5
5
  SHA512:
6
- metadata.gz: 86350f26cd5c36d3c64318a26f38aae28bc4e4709f5e0e6a58c35d925abff7444642e8c4e7b5678c36482ede6b8f52774cf17901f6a2cfe9bb6a66905fc0c723
7
- data.tar.gz: d9f042ab9f7a0e44b2e246ab1afa3f1f70d0b9912d7bbe4764e5c140fa86639833a5805a58a6552bff8e3e6567d6bbd310aee64ad291590489baa54e558d9c69
6
+ metadata.gz: cae6be7695a8892a7ea1b1b6398d05fc2a8e67a7133040e7da356d53635092a82ad427046ad6e632f1aa1a270390b96148f7c7bbb72273850da60593d54fec6a
7
+ data.tar.gz: bf145363a9df1a42912a13b19d495a27d80098f988aa6f2b643eb2d90e95b8172c4bf8d0a447683fdb8dd3b84dd12c49240b7944927801f00d59098a8a62b1a1
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,288 @@
1
+ cpf-dv for Ruby
2
+
3
+ [Gem Version](https://rubygems.org/gems/cpf-dv)
4
+ [Downloads Count](https://rubygems.org/gems/cpf-dv)
5
+ [Ruby Version](https://www.ruby-lang.org/)
6
+ [Test Status](https://github.com/LacusSolutions/br-utils-ruby/actions)
7
+ [Last Update Date](https://github.com/LacusSolutions/br-utils-ruby)
8
+ [Project License](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE)
9
+
10
+ > 🌎 [Acessar documentaΓ§Γ£o em portuguΓͺs](https://github.com/LacusSolutions/br-utils-ruby/blob/main/packages/cpf-dv/README.pt.md)
11
+
12
+ A Ruby utility to calculate check digits on CPF (Brazilian Individual's Taxpayer ID).
13
+
14
+ ## Ruby Support
15
+
16
+
17
+ | Ruby 3.2 | Ruby 3.3 | Ruby 3.4 |
18
+ | --------- | --------- | --------- |
19
+ | Passing βœ” | Passing βœ” | Passing βœ” |
20
+
21
+
22
+
23
+
24
+ ## Features
25
+
26
+ - βœ… **Flexible input**: Accepts `String` or `Array` of strings
27
+ - βœ… **Format agnostic**: Strips non-numeric characters from string input
28
+ - βœ… **Auto-expansion**: Multi-character strings in arrays are joined and parsed like a single string
29
+ - βœ… **Input validation**: Rejects ineligible CPFs (9 identical digits in the base β€” repeated-digit pattern)
30
+ - βœ… **Lazy evaluation**: Check digits are calculated only when accessed (via methods)
31
+ - βœ… **Caching**: Calculated values are cached for subsequent access
32
+ - βœ… **Minimal dependencies**: Only [`lacus-utils`](https://rubygems.org/gems/lacus-utils)
33
+ - βœ… **Error handling**: API misuse vs domain errors with a `CpfDV::Error` marker for library-wide rescue
34
+
35
+
36
+
37
+ ## Installation
38
+
39
+ Install the gem directly:
40
+
41
+ ```bash
42
+ gem install cpf-dv
43
+ ```
44
+
45
+ Or add it to your `Gemfile` and run `bundle install`:
46
+
47
+ ```ruby
48
+ gem 'cpf-dv'
49
+ ```
50
+
51
+
52
+
53
+ ## Require
54
+
55
+ ```ruby
56
+ require 'cpf-dv'
57
+ ```
58
+
59
+
60
+
61
+ ## Quick Start
62
+
63
+ ```ruby
64
+ require 'cpf-dv'
65
+
66
+ check_digits = CpfDV::CpfCheckDigits.new('054496519')
67
+
68
+ check_digits.first # => '1'
69
+ check_digits.second # => '0'
70
+ check_digits.both # => '10'
71
+ check_digits.cpf # => '05449651910'
72
+ ```
73
+
74
+
75
+
76
+ ## Usage
77
+
78
+ The main resource of this package is the class `CpfDV::CpfCheckDigits`. Through an instance, you access CPF check-digit information:
79
+
80
+ - `initialize`: `CpfDV::CpfCheckDigits.new(cpf_input)` β€” `cpf_input` must be a `String` or an `Array` of strings. After sanitization, the value must have 9–11 digits (formatting stripped from strings). Only the **first 9** digits are used as the base; if you pass 10 or 11 digits (e.g. a full CPF including prior check digits), digits 10–11 are **ignored** and the check digits are recalculated. There are **no options**, keyword arguments, or configuration objects β€” the constructor takes only the CPF input.
81
+ - `first`: First check digit (10th digit of the full CPF). Lazy, cached.
82
+ - `second`: Second check digit (11th digit of the full CPF). Lazy, cached.
83
+ - `both`: Both check digits concatenated as a string.
84
+ - `cpf`: The complete CPF as a string of 11 digits (9 base digits + 2 check digits).
85
+
86
+
87
+
88
+ ### Input formats
89
+
90
+ The `CpfCheckDigits` class accepts multiple input formats:
91
+
92
+ **String input:** plain digits or formatted CPF (e.g. `054.496.519-10`, `123.456.789`). Non-numeric characters are removed. Leading zeros are preserved.
93
+
94
+ **Array of strings:** each element must be a string; values are concatenated and then parsed like a single string (e.g. `['0','5','4',…]`, `['054','496','519']`, `['054496519']`). Non-string elements are not allowed.
95
+
96
+ ```ruby
97
+ require 'cpf-dv'
98
+
99
+ # String β€” plain, formatted, or with existing check digits (only first 9 digits used)
100
+ CpfDV::CpfCheckDigits.new('054496519')
101
+ CpfDV::CpfCheckDigits.new('054.496.519-10')
102
+ CpfDV::CpfCheckDigits.new('05449651910')
103
+
104
+ # Array of strings β€” single- or multi-character elements
105
+ CpfDV::CpfCheckDigits.new(%w[0 5 4 4 9 6 5 1 9])
106
+ CpfDV::CpfCheckDigits.new(%w[054 496 519])
107
+ CpfDV::CpfCheckDigits.new(%w[054496519])
108
+ ```
109
+
110
+
111
+
112
+ ### Error handling
113
+
114
+ Errors fall into two categories:
115
+
116
+ | Category | Meaning |
117
+ |---|---|
118
+ | **API misuse** | The caller invoked the library incorrectly (wrong type). Detectable from the call shape. |
119
+ | **Domain error** | The call was structurally correct, but a value violates a business rule (length, eligibility). |
120
+
121
+ Every custom error includes the `CpfDV::Error` marker module. Domain failures (`InvalidLengthError`, `ValidationError`) inherit from `CpfDV::DomainError` (`RangeError`).
122
+
123
+ #### Summary
124
+
125
+ | Class | Inherits from | Category | Trigger condition |
126
+ |---|---|---|---|
127
+ | `CpfDV::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | Argument has the wrong data type |
128
+ | `CpfDV::InvalidLengthError` | `CpfDV::DomainError` | Domain error | Sanitized length is not 9–11 |
129
+ | `CpfDV::ValidationError` | `CpfDV::DomainError` | Domain error | First 9 digits are all identical (repeated-digit pattern) |
130
+
131
+ #### `CpfDV::Error` (marker module)
132
+
133
+ - **Inheritance:** module marker mixed into every library error via `include` (not a class).
134
+ - **Category:** N/A (rescue target only) β€” not a failure mode by itself.
135
+ - **When it is raised:** Never raised directly; included by every custom error the library raises.
136
+ - **Example:** N/A
137
+ - **How to rescue it:**
138
+
139
+ ```ruby
140
+ rescue CpfDV::Error
141
+ # everything this library raises
142
+ ```
143
+
144
+ #### `CpfDV::DomainError`
145
+
146
+ - **Inheritance:** `CpfDV::DomainError < RangeError` (includes `CpfDV::Error`)
147
+ - **Category:** Domain error β€” ancestor for numeric/length domain failures.
148
+ - **When it is raised:** Not raised directly; prefer raising a leaf subclass.
149
+ - **Example:** Prefer `raise CpfDV::InvalidLengthError` over raising `DomainError` directly.
150
+ - **How to rescue it:**
151
+
152
+ ```ruby
153
+ rescue CpfDV::DomainError
154
+ # InvalidLengthError, ValidationError, and any other DomainError subclass
155
+ ```
156
+
157
+ #### `CpfDV::TypeMismatchError`
158
+
159
+ - **Inheritance:** `CpfDV::TypeMismatchError < TypeError` (includes `CpfDV::Error`)
160
+ - **Category:** API misuse β€” the caller passed a value of the wrong type.
161
+ - **When it is raised:** Raised when the CPF input is not a `String` or an `Array` of strings (or an array contains a non-string element).
162
+ - **Example:**
163
+
164
+ ```ruby
165
+ CpfDV::CpfCheckDigits.new(12_345_678_901) # raises CpfDV::TypeMismatchError
166
+ ```
167
+
168
+ - **How to rescue it:**
169
+
170
+ ```ruby
171
+ rescue CpfDV::TypeMismatchError
172
+ # this library's type-contract violation
173
+
174
+ rescue TypeError
175
+ # native type errors, including this library's TypeMismatchError
176
+ ```
177
+
178
+ #### `CpfDV::InvalidLengthError`
179
+
180
+ - **Inheritance:** `CpfDV::InvalidLengthError < CpfDV::DomainError < RangeError` (includes `CpfDV::Error`)
181
+ - **Category:** Domain error β€” a collection or string length violates a business rule.
182
+ - **When it is raised:** Raised when the sanitized CPF input does not contain 9 to 11 digits.
183
+ - **Example:**
184
+
185
+ ```ruby
186
+ CpfDV::CpfCheckDigits.new('12345678') # raises CpfDV::InvalidLengthError
187
+ ```
188
+
189
+ - **How to rescue it:**
190
+
191
+ ```ruby
192
+ rescue CpfDV::InvalidLengthError
193
+ # this exact length violation
194
+
195
+ rescue CpfDV::DomainError
196
+ # RangeError-rooted domain failures from this library
197
+ ```
198
+
199
+ #### `CpfDV::ValidationError`
200
+
201
+ - **Inheritance:** `CpfDV::ValidationError < CpfDV::DomainError < RangeError` (includes `CpfDV::Error`)
202
+ - **Category:** Domain error β€” a value fails a non-numeric, non-length domain rule.
203
+ - **When it is raised:** Raised when the first 9 digits are all the same digit (repeated-digit pattern).
204
+ - **Example:**
205
+
206
+ ```ruby
207
+ CpfDV::CpfCheckDigits.new('111111111') # raises CpfDV::ValidationError
208
+ ```
209
+
210
+ - **How to rescue it:**
211
+
212
+ ```ruby
213
+ rescue CpfDV::ValidationError
214
+ # this exact domain validation failure
215
+
216
+ rescue CpfDV::DomainError
217
+ # RangeError-rooted domain failures from this library
218
+ ```
219
+
220
+ #### Rescue granularity
221
+
222
+ ```ruby
223
+ # 1) Single native class β€” catches type misuse from this library (and other TypeErrors).
224
+ rescue TypeError
225
+ # CpfDV::TypeMismatchError and any other TypeError (library or not)
226
+
227
+ # 2) CpfDV::DomainError β€” catches business-rule violations under DomainError.
228
+ rescue CpfDV::DomainError
229
+ # CpfDV::InvalidLengthError, CpfDV::ValidationError, and other DomainError subclasses
230
+
231
+ # 3) CpfDV::Error β€” catches everything the library raises.
232
+ rescue CpfDV::Error
233
+ # every custom error that includes CpfDV::Error
234
+
235
+ # 4) Specific leaf class β€” catches only that exact failure mode.
236
+ rescue CpfDV::InvalidLengthError
237
+ # only CpfDV::InvalidLengthError
238
+ ```
239
+
240
+ Notable attributes on raised errors:
241
+
242
+ - `TypeMismatchError`: `actual_input`, `actual_type`, `expected_type`
243
+ - `InvalidLengthError`: `actual_input`, `evaluated_input`, `min_expected_length`, `max_expected_length`
244
+ - `ValidationError`: `actual_input`, `reason`
245
+
246
+
247
+
248
+ ### Other available resources
249
+
250
+ After `require 'cpf-dv'`:
251
+
252
+ - `CpfDV::CPF_MIN_LENGTH`: `9`
253
+ - `CpfDV::CPF_MAX_LENGTH`: `11`
254
+ - **Errors**: see above (`CpfDV::Error`, `DomainError`, and raised leaves)
255
+
256
+
257
+
258
+ ## Calculation algorithm
259
+
260
+ The package calculates CPF check digits using the official Brazilian modulo-11 algorithm:
261
+
262
+ 1. **First check digit (10th position):** apply to the first **9** base digits; weights **10, 9, 8, 7, 6, 5, 4, 3, 2** (left to right); let `remainder = 11 - (sum(digit Γ— weight) % 11)`. The digit is `0` if `remainder > 9`, otherwise `remainder`.
263
+ 2. **Second check digit (11th position):** apply to the first 9 base digits **plus** the first check digit; weights **11, 10, 9, 8, 7, 6, 5, 4, 3, 2** (left to right); same formula for `remainder`.
264
+
265
+
266
+
267
+ ## Contribution & Support
268
+
269
+ 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:
270
+
271
+ - ⭐ Starring the repository
272
+ - 🀝 Contributing to the codebase
273
+ - πŸ’‘ [Suggesting new features](https://github.com/LacusSolutions/br-utils-ruby/issues)
274
+ - πŸ› [Reporting bugs](https://github.com/LacusSolutions/br-utils-ruby/issues)
275
+
276
+
277
+
278
+ ## License
279
+
280
+ This project is licensed under the MIT License β€” see the [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE) file for details.
281
+
282
+ ## Changelog
283
+
284
+ See [CHANGELOG](./CHANGELOG.md) for a list of changes and version history.
285
+
286
+ ---
287
+
288
+ Made with ❀️ by [Lacus Solutions](https://github.com/LacusSolutions)
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+
5
+ module CpfDV
6
+ # Minimum number of digits required for the CPF check digits calculation.
7
+ CPF_MIN_LENGTH = 9
8
+
9
+ # Maximum number of digits accepted as input for the CPF check digits
10
+ # calculation.
11
+ CPF_MAX_LENGTH = 11
12
+
13
+ DELTA_FACTOR = '0'.ord
14
+ DIGIT_CHARS = '0123456789'
15
+
16
+ private_constant :DELTA_FACTOR, :DIGIT_CHARS
17
+
18
+ # Calculates and exposes CPF check digits from a valid base input.
19
+ #
20
+ # Validates length and rejects repeated-digit sequences.
21
+ #
22
+ # @example
23
+ # check_digits = CpfDV::CpfCheckDigits.new('054496519')
24
+ # check_digits.first # => '1'
25
+ # check_digits.second # => '0'
26
+ # check_digits.both # => '10'
27
+ # check_digits.cpf # => '05449651910'
28
+ class CpfCheckDigits
29
+ # Creates a calculator for the given CPF base (9 to 11 digits).
30
+ #
31
+ # @param cpf_input [String, Array<String>] digits with or without formatting,
32
+ # or an array of strings
33
+ # @raise [TypeMismatchError] when input is not a +String+ or +Array<String>+
34
+ # @raise [InvalidLengthError] when digit count is not between 9 and 11
35
+ # @raise [ValidationError] when all first 9 digits are the same (repeated
36
+ # digits, e.g. +777.777.777-...+)
37
+ def initialize(cpf_input)
38
+ parsed_input = parse_input(cpf_input)
39
+
40
+ validate_length(parsed_input, cpf_input)
41
+ validate_non_repeated_digits(parsed_input, cpf_input)
42
+
43
+ @cpf_digits = parsed_input[0, CPF_MIN_LENGTH]
44
+ @cached_first_digit = nil
45
+ @cached_second_digit = nil
46
+ end
47
+
48
+ # First check digit (10th digit of the full CPF).
49
+ #
50
+ # @return [String] a single numeric character (+"0"+–+"9"+)
51
+ def first
52
+ @cached_first_digit = _calculate(@cpf_digits) if @cached_first_digit.nil?
53
+
54
+ DIGIT_CHARS[@cached_first_digit]
55
+ end
56
+
57
+ # Second check digit (11th digit of the full CPF).
58
+ #
59
+ # @return [String] a single numeric character (+"0"+–+"9"+)
60
+ def second
61
+ @cached_second_digit = _calculate([*@cpf_digits, first]) if @cached_second_digit.nil?
62
+
63
+ DIGIT_CHARS[@cached_second_digit]
64
+ end
65
+
66
+ # Both check digits concatenated (10th and 11th digits).
67
+ #
68
+ # @return [String] two-character numeric string
69
+ def both
70
+ first + second
71
+ end
72
+
73
+ # Full 11-digit CPF (base 9 digits concatenated with the 2 check digits).
74
+ #
75
+ # @return [String] 11-digit CPF
76
+ def cpf
77
+ @cpf_digits.join + both
78
+ end
79
+
80
+ # Protected (not private) so test spy subclasses can override and call +super+.
81
+ # Leading underscore matches the cross-language helper name (`_calculate`).
82
+ protected
83
+
84
+ # Computes a single check digit using the standard CPF modulo-11 algorithm.
85
+ #
86
+ # @param cpf_sequence [Array<String>] digit characters used in the weighted sum
87
+ # @return [Integer] check digit in the range 0–9
88
+ def _calculate(cpf_sequence)
89
+ factor = cpf_sequence.length + 1
90
+ sum_result = 0
91
+
92
+ cpf_sequence.each do |digit_char|
93
+ sum_result += (digit_char.ord - DELTA_FACTOR) * factor
94
+ factor -= 1
95
+ end
96
+
97
+ remainder = 11 - (sum_result % 11)
98
+
99
+ remainder > 9 ? 0 : remainder
100
+ end
101
+
102
+ private
103
+
104
+ # Parses a string or an array of strings into digit characters.
105
+ #
106
+ # @param cpf_input [Object] candidate CPF input
107
+ # @return [Array<String>] digit characters
108
+ # @raise [TypeMismatchError] when input is not a +String+ or +Array<String>+
109
+ def parse_input(cpf_input)
110
+ return parse_string_input(cpf_input) if cpf_input.is_a?(String)
111
+ return parse_array_input(cpf_input) if cpf_input.is_a?(Array)
112
+
113
+ raise TypeMismatchError.new(cpf_input, 'string or string[]')
114
+ end
115
+
116
+ # Strips non-digit characters and keeps the remainder as characters.
117
+ #
118
+ # @param cpf_string [String] raw or formatted CPF string
119
+ # @return [Array<String>] digit characters
120
+ def parse_string_input(cpf_string)
121
+ result = []
122
+
123
+ cpf_string.each_char do |char|
124
+ code = char.ord
125
+ result << char if code.between?(48, 57)
126
+ end
127
+
128
+ result
129
+ end
130
+
131
+ # Concatenates an array of strings and parses the result.
132
+ #
133
+ # @param cpf_array [Array] candidate array of string chunks
134
+ # @return [Array<String>] digit characters
135
+ # @raise [TypeMismatchError] when input is not a +String+ or +Array<String>+
136
+ def parse_array_input(cpf_array)
137
+ return [] if cpf_array.empty?
138
+
139
+ raise TypeMismatchError.new(cpf_array, 'string or string[]') unless cpf_array.all?(String)
140
+
141
+ parse_string_input(cpf_array.join)
142
+ end
143
+
144
+ # Ensures digit count is between {CPF_MIN_LENGTH} and {CPF_MAX_LENGTH}.
145
+ #
146
+ # @param cpf_digits [Array<String>] normalized digit characters
147
+ # @param original_input [String, Array<String>] original caller input
148
+ # @raise [InvalidLengthError] when digit count is not between 9 and 11
149
+ def validate_length(cpf_digits, original_input)
150
+ digits_count = cpf_digits.length
151
+
152
+ return if digits_count.between?(CPF_MIN_LENGTH, CPF_MAX_LENGTH)
153
+
154
+ raise InvalidLengthError.new(
155
+ original_input,
156
+ cpf_digits.join,
157
+ CPF_MIN_LENGTH,
158
+ CPF_MAX_LENGTH
159
+ )
160
+ end
161
+
162
+ # Rejects inputs where all first 9 digits are the same.
163
+ #
164
+ # @param cpf_digits [Array<String>] normalized digit characters
165
+ # @param original_input [String, Array<String>] original caller input
166
+ # @raise [ValidationError] when all first 9 digits are the same (repeated
167
+ # digits, e.g. +777.777.777-...+)
168
+ def validate_non_repeated_digits(cpf_digits, original_input)
169
+ first_char = cpf_digits[0]
170
+ return unless cpf_digits[1, CPF_MIN_LENGTH - 1].all? { |char| char == first_char }
171
+
172
+ raise ValidationError.new(
173
+ original_input,
174
+ 'Repeated digits are not considered valid.'
175
+ )
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'lacus-utils'
5
+
6
+ module CpfDV
7
+ # Formats the original input for inclusion in an exception message.
8
+ module FormatActualInput
9
+ module_function
10
+
11
+ # @param actual_input [Object] the original input value
12
+ # @return [String] a quoted string, or compact JSON for arrays
13
+ def call(actual_input)
14
+ return "\"#{actual_input}\"" if actual_input.is_a?(String)
15
+
16
+ JSON.generate(actual_input)
17
+ end
18
+ end
19
+ private_constant :FormatActualInput
20
+
21
+ # Marker module mixed into every custom error raised by this library.
22
+ #
23
+ # Use +rescue CpfDV::Error+ to catch every library error regardless of native
24
+ # ancestry.
25
+ module Error; end
26
+
27
+ # API misuse error raised when an argument's runtime type does not match the
28
+ # type required by the API contract.
29
+ #
30
+ # Raised when the input provided to {CpfCheckDigits} is not a +String+ or
31
+ # +Array<String>+. The error message includes both the actual type of the
32
+ # input and the expected type.
33
+ class TypeMismatchError < TypeError
34
+ include Error
35
+
36
+ # @return [Object] the offending input value
37
+ attr_reader :actual_input
38
+
39
+ # @return [String] human-readable type of {#actual_input}
40
+ attr_reader :actual_type
41
+
42
+ # @return [String] description of the expected type
43
+ attr_reader :expected_type
44
+
45
+ # @param actual_input [Object] the offending input value (the whole array
46
+ # when a non-string element is found)
47
+ # @param expected_type [String] description of the expected type (e.g.
48
+ # +"string or string[]"+)
49
+ def initialize(actual_input, expected_type)
50
+ actual_type = LacusUtils.describe_type(actual_input)
51
+
52
+ super("CPF input must be of type #{expected_type}. Got #{actual_type}.")
53
+ @actual_input = actual_input
54
+ @actual_type = actual_type
55
+ @expected_type = expected_type
56
+ end
57
+ end
58
+
59
+ # Domain error ancestor for business-rule failures (length, validation, and
60
+ # other domain leaves). Prefer raising a leaf subclass.
61
+ class DomainError < RangeError
62
+ include Error
63
+ end
64
+
65
+ # Domain error raised when a string, array, or other collection has a length
66
+ # outside the bounds required by the domain rule.
67
+ #
68
+ # A valid CPF input must contain between 9 and 11 digits. The error message
69
+ # distinguishes between the original input and the evaluated one (which strips
70
+ # punctuation characters).
71
+ class InvalidLengthError < DomainError
72
+ # @return [String, Array<String>] the original input
73
+ attr_reader :actual_input
74
+
75
+ # @return [String] the digits-only string used for counting
76
+ attr_reader :evaluated_input
77
+
78
+ # @return [Integer] minimum expected length (9)
79
+ attr_reader :min_expected_length
80
+
81
+ # @return [Integer] maximum expected length (11)
82
+ attr_reader :max_expected_length
83
+
84
+ # @param actual_input [String, Array<String>] the original input
85
+ # @param evaluated_input [String] the digits-only string
86
+ # @param min_expected_length [Integer] minimum expected length
87
+ # @param max_expected_length [Integer] maximum expected length
88
+ def initialize(actual_input, evaluated_input, min_expected_length, max_expected_length)
89
+ super(build_message(actual_input, evaluated_input, min_expected_length, max_expected_length))
90
+ @actual_input = actual_input
91
+ @evaluated_input = evaluated_input
92
+ @min_expected_length = min_expected_length
93
+ @max_expected_length = max_expected_length
94
+ end
95
+
96
+ private
97
+
98
+ def build_message(actual_input, evaluated_input, min_len, max_len)
99
+ fmt_actual = FormatActualInput.call(actual_input)
100
+ fmt_evaluated =
101
+ if actual_input == evaluated_input
102
+ evaluated_input.length.to_s
103
+ else
104
+ "#{evaluated_input.length} in \"#{evaluated_input}\""
105
+ end
106
+
107
+ "CPF input #{fmt_actual} does not contain #{min_len} to #{max_len} digits. " \
108
+ "Got #{fmt_evaluated}."
109
+ end
110
+ end
111
+
112
+ # Domain error raised when a value has a valid type and length but violates a
113
+ # validation rule that is not length-based (e.g. repeated digits).
114
+ #
115
+ # This is a business-logic exception; callers should catch it and handle it
116
+ # appropriately.
117
+ class ValidationError < DomainError
118
+ # @return [String, Array<String>] the original input
119
+ attr_reader :actual_input
120
+
121
+ # @return [String] human-readable reason why the input is invalid
122
+ attr_reader :reason
123
+
124
+ # @param actual_input [String, Array<String>] the original input
125
+ # @param reason [String] human-readable reason why the input is invalid
126
+ def initialize(actual_input, reason)
127
+ super("CPF input #{FormatActualInput.call(actual_input)} is invalid. #{reason}")
128
+ @actual_input = actual_input
129
+ @reason = reason
130
+ end
131
+ end
132
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module CpfDv
4
- VERSION = '0.0.0'
3
+ module CpfDV
4
+ VERSION = '1.0.0'
5
5
  end
data/src/cpf-dv.rb CHANGED
@@ -1,9 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'cpf-dv/version'
4
+ require_relative 'cpf-dv/errors'
5
+ require_relative 'cpf-dv/cpf_check_digits'
4
6
 
5
- module CpfDv
6
- def self.hello
7
- 'cpf-dv'
8
- end
7
+ # Check-digit calculation for Brazilian CPF (numeric format).
8
+ #
9
+ # Errors fall into two categories:
10
+ #
11
+ # - *API misuse* β€” the caller invoked the library incorrectly (wrong type).
12
+ # Raised as {CpfDV::TypeMismatchError} (+TypeError+).
13
+ # - *Domain errors* β€” the call shape was valid, but a value violates a business
14
+ # rule (invalid length, repeated digits). Length failures raise
15
+ # {CpfDV::InvalidLengthError}; other domain failures raise
16
+ # {CpfDV::ValidationError} (both under {CpfDV::DomainError} / +RangeError+).
17
+ #
18
+ # Every custom error includes the {CpfDV::Error} marker module so consumers can
19
+ # +rescue CpfDV::Error+ for a library-wide catch.
20
+ #
21
+ # Public API:
22
+ #
23
+ # - {CpfDV::CpfCheckDigits}
24
+ # - {CpfDV::CPF_MIN_LENGTH}, {CpfDV::CPF_MAX_LENGTH}
25
+ # - Error marker {CpfDV::Error}; domain ancestor {CpfDV::DomainError};
26
+ # raised leaves {CpfDV::TypeMismatchError}, {CpfDV::InvalidLengthError},
27
+ # {CpfDV::ValidationError}
28
+ #
29
+ # @example
30
+ # require 'cpf-dv'
31
+ #
32
+ # check_digits = CpfDV::CpfCheckDigits.new('054496519')
33
+ # check_digits.cpf # => '05449651910'
34
+ module CpfDV
9
35
  end
metadata CHANGED
@@ -1,30 +1,54 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cpf-dv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
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-03-04 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Utility to calculate and verify CPF check digits.
11
+ date: 2026-07-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: lacus-utils
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.1.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.1.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
33
+ description: Utility to calculate check digits on CPF (Brazilian Individual's Taxpayer
34
+ ID)
14
35
  email:
15
36
  - juliolmuller@outlook.com
16
37
  executables: []
17
38
  extensions: []
18
39
  extra_rdoc_files: []
19
40
  files:
41
+ - LICENSE
42
+ - README.md
20
43
  - src/cpf-dv.rb
44
+ - src/cpf-dv/cpf_check_digits.rb
45
+ - src/cpf-dv/errors.rb
21
46
  - src/cpf-dv/version.rb
22
47
  homepage: https://github.com/LacusSolutions/br-utils-ruby
23
48
  licenses:
24
49
  - MIT
25
50
  metadata:
26
51
  source_code_uri: https://github.com/LacusSolutions/br-utils-ruby
27
- changelog_uri: https://github.com/LacusSolutions/br-utils-ruby/blob/main/CHANGELOG.md
28
52
  rubygems_mfa_required: 'true'
29
53
  post_install_message:
30
54
  rdoc_options: []
@@ -34,7 +58,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
34
58
  requirements:
35
59
  - - ">="
36
60
  - !ruby/object:Gem::Version
37
- version: '3.2'
61
+ version: '3.1'
38
62
  required_rubygems_version: !ruby/object:Gem::Requirement
39
63
  requirements:
40
64
  - - ">="
@@ -44,5 +68,5 @@ requirements: []
44
68
  rubygems_version: 3.4.19
45
69
  signing_key:
46
70
  specification_version: 4
47
- summary: Check-digit calculation for CPF (Brazilian personal ID)
71
+ summary: Check-digit calculation for CPF (Brazilian Individual's Taxpayer ID)
48
72
  test_files: []