From cecdcf7352c8f8aa67d4c6cac6da9c076816064d Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:34:03 -0300 Subject: [PATCH 01/19] chore(cpf-val): update package metadata Added email contact, refined summary, and provided a detailed description for the CPF validation utility. Included source code URI in the metadata for better accessibility. Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/cpf-val.gemspec | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cpf-val/cpf-val.gemspec b/packages/cpf-val/cpf-val.gemspec index 685b92e..0426a20 100644 --- a/packages/cpf-val/cpf-val.gemspec +++ b/packages/cpf-val/cpf-val.gemspec @@ -6,10 +6,13 @@ Gem::Specification.new do |spec| spec.name = 'cpf-val' spec.version = CpfVal::VERSION spec.authors = ['Julio L. Muller'] - spec.summary = 'Validate CPF (Brazilian personal ID)' + spec.email = ['juliolmuller@outlook.com'] + spec.summary = "Validate CPF (Brazilian Individual's Taxpayer ID)" + spec.description = "Utility to validate CPF (Brazilian Individual's Taxpayer ID)" spec.homepage = 'https://github.com/LacusSolutions/br-utils-ruby' spec.license = 'MIT' spec.required_ruby_version = '>= 3.1' + spec.metadata['source_code_uri'] = spec.homepage spec.metadata['rubygems_mfa_required'] = 'true' spec.files = Dir['src/**/*'] + ['LICENSE', 'README.md'].select { |f| File.file?(f) } spec.require_paths = ['src'] From 22550db9e1c684b39f3f1f58934a9d6b5e867275 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:34:45 -0300 Subject: [PATCH 02/19] feat(cpf-val): implement CPF validation functionality Introduced a comprehensive CPF validation module, including the `CpfValidator` class for validating CPF identifiers according to Brazilian standards. Added a helper method `cpf_val` for simplified usage. Implemented custom error handling with `TypeMismatchError` for API misuse. Enhanced documentation to clarify usage and error types. - Added support for both string and array inputs for CPF validation. - Included detailed comments and examples for better understanding. - Created a marker module for custom errors to streamline error handling. This update significantly improves the CPF validation utility, providing a robust and user-friendly API. Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/src/cpf-val.rb | 33 +++++++-- packages/cpf-val/src/cpf-val/cpf_val.rb | 25 +++++++ packages/cpf-val/src/cpf-val/cpf_validator.rb | 70 +++++++++++++++++++ packages/cpf-val/src/cpf-val/errors.rb | 36 ++++++++++ packages/cpf-val/src/cpf-val/types.rb | 15 ++++ 5 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 packages/cpf-val/src/cpf-val/cpf_val.rb create mode 100644 packages/cpf-val/src/cpf-val/cpf_validator.rb create mode 100644 packages/cpf-val/src/cpf-val/errors.rb create mode 100644 packages/cpf-val/src/cpf-val/types.rb diff --git a/packages/cpf-val/src/cpf-val.rb b/packages/cpf-val/src/cpf-val.rb index 0110bec..f5a08b2 100644 --- a/packages/cpf-val/src/cpf-val.rb +++ b/packages/cpf-val/src/cpf-val.rb @@ -1,10 +1,35 @@ # frozen_string_literal: true -require 'cpf-dv' require_relative 'cpf-val/version' +require_relative 'cpf-val/errors' +require_relative 'cpf-val/types' +require_relative 'cpf-val/cpf_validator' +require_relative 'cpf-val/cpf_val' +# Validates CPF (Cadastro de Pessoa Física) identifiers. +# +# Supports formatted or raw digit input. Invalid CPF data returns +false+; only +# documented errors are raised for API misuse (wrong input type). +# +# Errors fall into one category: +# +# - *API misuse* — the caller supplied a wrong type. Raised as +# {CpfVal::TypeMismatchError}. +# +# Every custom error includes the {CpfVal::Error} marker module so consumers can +# +rescue CpfVal::Error+ for a library-wide catch. +# +# Public API: +# +# - {CpfVal.cpf_val} +# - {CpfVal::CpfValidator} +# - {CpfVal::CPF_LENGTH}, {CpfVal::VERSION} +# - Type marker: {CpfVal::CpfInput} +# - Error marker {CpfVal::Error}; misuse error {CpfVal::TypeMismatchError} +# +# @example +# require 'cpf-val' +# +# CpfVal.cpf_val('82911017366') # => true module CpfVal - def self.hello - 'cpf-val' - end end diff --git a/packages/cpf-val/src/cpf-val/cpf_val.rb b/packages/cpf-val/src/cpf-val/cpf_val.rb new file mode 100644 index 0000000..58f6219 --- /dev/null +++ b/packages/cpf-val/src/cpf-val/cpf_val.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_relative 'cpf_validator' + +module CpfVal + # Helper function to simplify the usage of the {CpfValidator} class. + # + # Validates a CPF string or array of strings and returns whether it is a valid + # Brazilian CPF. Invalid CPF data returns +false+; only API misuse raises + # documented errors. + # + # @param cpf_input [String, Array] CPF value as a string or array of + # strings + # @return [Boolean] +true+ when valid, +false+ otherwise + # @raise [TypeMismatchError] if the input is not a +String+ or +Array+ + # @see CpfValidator#is_valid + # @see CpfValidator + # + # @example + # CpfVal.cpf_val('82911017366') # => true + # CpfVal.cpf_val('33528612691') # => false + def self.cpf_val(cpf_input) + CpfValidator.new.is_valid(cpf_input) + end +end diff --git a/packages/cpf-val/src/cpf-val/cpf_validator.rb b/packages/cpf-val/src/cpf-val/cpf_validator.rb new file mode 100644 index 0000000..c977b71 --- /dev/null +++ b/packages/cpf-val/src/cpf-val/cpf_validator.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'cpf-dv' + +require_relative 'errors' + +module CpfVal + # The standard length of a CPF (Cadastro de Pessoa Física) identifier (11 + # digits). + CPF_LENGTH = 11 + + # Validator for CPF (Cadastro de Pessoa Física) identifiers. + # + # Validates CPF strings according to the Brazilian CPF validation algorithm. + # Invalid CPF data returns +false+; only API misuse raises documented errors. + class CpfValidator + NON_DIGIT_PATTERN = /\D/ + + private_constant :NON_DIGIT_PATTERN + + # Validates a CPF input. + # + # A CPF is considered valid when, after stripping every non-digit character, + # it has exactly {CPF_LENGTH} digits, its base is not an all-identical-digit + # sequence, and both check digits match the ones computed via the standard + # modulo-11 algorithm. Invalid values return +false+ instead of raising. + # + # @param cpf_input [String, Array] CPF value as a string or array of + # strings + # @return [Boolean] +true+ when valid, +false+ otherwise + # @raise [TypeMismatchError] if the input is not a +String+ or +Array+ + # + # @example + # CpfValidator.new.is_valid('82911017366') # => true + # rubocop:disable Naming/PredicatePrefix -- public API matches JS/Python `is_valid` + def is_valid(cpf_input) + actual_input = to_string_input(cpf_input) + sanitized_cpf = actual_input.gsub(NON_DIGIT_PATTERN, '') + + return false unless sanitized_cpf.length == CPF_LENGTH + + validate_with_check_digits(sanitized_cpf) + end + # rubocop:enable Naming/PredicatePrefix + + private + + def to_string_input(cpf_input) + return cpf_input if cpf_input.is_a?(String) + + if cpf_input.is_a?(Array) + cpf_input.each do |item| + raise TypeMismatchError.new(cpf_input, 'string or string[]') unless item.is_a?(String) + end + + return cpf_input.join + end + + raise TypeMismatchError.new(cpf_input, 'string or string[]') + end + + def validate_with_check_digits(sanitized_cpf) + cpf_check_digits = CpfDV::CpfCheckDigits.new(sanitized_cpf) + + sanitized_cpf == cpf_check_digits.cpf + rescue CpfDV::Error + false + end + end +end diff --git a/packages/cpf-val/src/cpf-val/errors.rb b/packages/cpf-val/src/cpf-val/errors.rb new file mode 100644 index 0000000..8dcaa5f --- /dev/null +++ b/packages/cpf-val/src/cpf-val/errors.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'lacus-utils' + +module CpfVal + # Marker module mixed into every custom error raised by this library. + # + # Use +rescue CpfVal::Error+ to catch every library error regardless of native + # ancestry. + module Error; end + + # API misuse error raised when an argument's runtime type does not match the + # type required by the API contract (CPF input). + class TypeMismatchError < TypeError + include Error + + # @return [Object] the offending input value + attr_reader :actual_input + + # @return [String] human-readable type of {#actual_input} + attr_reader :actual_type + + # @return [String] description of the expected type + attr_reader :expected_type + + # @param actual_input [Object] the offending input value + # @param expected_type [String] description of the expected type + def initialize(actual_input, expected_type) + actual_type = LacusUtils.describe_type(actual_input) + super("CPF input must be of type #{expected_type}. Got #{actual_type}.") + @actual_input = actual_input + @actual_type = actual_type + @expected_type = expected_type + end + end +end diff --git a/packages/cpf-val/src/cpf-val/types.rb b/packages/cpf-val/src/cpf-val/types.rb new file mode 100644 index 0000000..030974e --- /dev/null +++ b/packages/cpf-val/src/cpf-val/types.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module CpfVal + # Represents valid input types for CPF validation. + # + # A CPF may be given as: + # + # - A string of numeric characters (with or without formatting). + # - An array of strings, each representing one or more numeric characters + # and/or punctuation. + # + # @see CpfValidator#is_valid + # @see CpfVal.cpf_val + CpfInput = Object +end From f37418d04cf928cefbecc489eaac578ffaf952a3 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:35:28 -0300 Subject: [PATCH 03/19] test(cpf-val): create CPF validation unit tests Expanded the test suite for the CPF validation module by adding comprehensive tests for the `CpfValidator` class. Implemented various scenarios to validate CPF inputs, including valid, invalid, and edge cases. Introduced tests for error handling, ensuring that `TypeMismatchError` is raised for incorrect input types. - Added tests for both formatted and unformatted CPF inputs. - Included checks for repeated-digit prefixes and non-digit strings. - Verified the behavior of the `cpf_val` helper method against the `is_valid` method. This update significantly improves test coverage and robustness of the CPF validation functionality. Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/tests/cpf_val.spec.rb | 85 +++++- packages/cpf-val/tests/cpf_validator.spec.rb | 272 +++++++++++++++++++ packages/cpf-val/tests/errors.spec.rb | 46 ++++ 3 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 packages/cpf-val/tests/cpf_validator.spec.rb create mode 100644 packages/cpf-val/tests/errors.spec.rb diff --git a/packages/cpf-val/tests/cpf_val.spec.rb b/packages/cpf-val/tests/cpf_val.spec.rb index 1fb01c3..84574e5 100644 --- a/packages/cpf-val/tests/cpf_val.spec.rb +++ b/packages/cpf-val/tests/cpf_val.spec.rb @@ -3,9 +3,88 @@ require 'spec_helper' RSpec.describe CpfVal do - describe '.hello' do - it 'returns cpf-val' do - expect(CpfVal.hello).to eq('cpf-val') + describe '.cpf_val' do + context 'when called' do + it 'matches CpfValidator#is_valid behavior' do + input = '82911017366' + validator = described_class::CpfValidator.new + + expect(described_class.cpf_val(input)).to eq(validator.is_valid(input)) + end + + it 'returns true for a valid cpf' do + expect(described_class.cpf_val('82911017366')).to be(true) + end + + it 'returns false for an invalid cpf' do + expect(described_class.cpf_val('33528612691')).to be(false) + end + end + + context 'when validating smoke-test vectors' do + it 'returns true for a valid unformatted CPF' do + expect(described_class.cpf_val('33528612690')).to be(true) + end + + it 'returns false for an invalid check digit' do + expect(described_class.cpf_val('33528612691')).to be(false) + end + end + end + + context 'when inspecting constants' do + it 'exposes CPF_LENGTH as 11' do + expect(described_class::CPF_LENGTH).to eq(11) + end + end + + context 'when inspecting public constants' do + it 'defines CpfValidator' do + expect(described_class.const_defined?(:CpfValidator)).to be(true) + end + + it 'defines Error' do + expect(described_class.const_defined?(:Error)).to be(true) + end + + it 'defines TypeMismatchError' do + expect(described_class.const_defined?(:TypeMismatchError)).to be(true) + end + end + + context 'when inspecting public types' do + it 'exposes cpf_val as a callable helper' do + aggregate_failures do + expect(described_class).to respond_to(:cpf_val) + expect(described_class.cpf_val('33528612690')).to be(true) + expect(described_class.cpf_val('33528612691')).to be(false) + end + end + + it 'exposes CpfValidator as an instantiable class' do + validator = described_class::CpfValidator.new + result = validator.is_valid('33528612690') + + aggregate_failures do + expect(validator).to be_a(described_class::CpfValidator) + expect(result).to be(true) + end + end + + it 'exposes TypeMismatchError as a TypeError' do + expect(described_class::TypeMismatchError < TypeError).to be(true) + end + + it 'exposes instantiable TypeMismatchError' do + error = described_class::TypeMismatchError.new(123, 'string') + + aggregate_failures do + expect(error.actual_input).to eq(123) + expect(error.actual_type).to eq('integer number') + expect(error.expected_type).to eq('string') + expect(error).to be_a(described_class::Error) + expect(error.message).to eq('CPF input must be of type string. Got integer number.') + end end end end diff --git a/packages/cpf-val/tests/cpf_validator.spec.rb b/packages/cpf-val/tests/cpf_validator.spec.rb new file mode 100644 index 0000000..172daa5 --- /dev/null +++ b/packages/cpf-val/tests/cpf_validator.spec.rb @@ -0,0 +1,272 @@ +# frozen_string_literal: true + +require 'spec_helper' + +REPEATED_DIGIT_PREFIXES = %w[ + 000000000 + 111111111 + 222222222 + 333333333 + 444444444 + 555555555 + 666666666 + 777777777 + 888888888 + 999999999 +].freeze + +VALID_CPF_SAMPLES = %w[ + 82911017366 + 33528612690 + 86244870050 + 22312659077 + 96215666068 + 67107095072 + 48039958008 + 20954431014 + 11144477735 + 12345678909 + 97705597411 + 71699299960 + 35449963599 + 43571251113 + 43603425197 + 61100255346 + 86845729395 + 03000443991 + 74849560822 + 59980231700 + 90248115707 + 82056229145 + 68988687647 + 59657429161 + 04396907656 + 89702485444 + 49334640499 + 89843515200 + 26627637286 + 96517650466 + 81941692249 + 20838028888 + 00413864855 + 79471093112 + 06897074950 + 70180285661 + 51808354451 + 57541651702 + 07180937045 + 01848900392 + 28917222056 + 34438615399 + 46655439680 + 05928803621 + 88153164007 + 92518925988 + 00377949655 + 60967893402 + 37909039140 + 88407302066 + 74646326213 + 07149896065 + 42752317085 + 58129750864 + 17717087600 +].freeze + +FORMATTED_VALID_CPFS = [ + ['dots and dash', '499.784.420-90'], + ['dots only', '028.062.110.85'], + ['underscores', '011_258_960_00'], + ['dash only', '779953010-30'] +].freeze + +INVALID_CPF_SAMPLES = %w[ + 86244870011 + 33528612691 + 12345678901 + 12345678910 + 499784420-75 + 090.871.219-71 + 081.465.729.10 + 011_258_960_99 +].freeze + +NON_DIGIT_STRINGS = [ + '', + 'abc', + 'abc123', + 'true', + 'false', + 'null' +].freeze + +SHORT_OR_LONG_NUMERIC_STRINGS = %w[ + 1 + 12 + 123 + 1234 + 12345 + 123456 + 1234567 + 12345678 + 123456789 + 1234567890 +].freeze + +INVALID_INPUT_CASES = [ + [nil, 'nil'], + [42, 'integer number'], + [3.14, 'float number'], + [true, 'boolean'], + [{}, 'hash'], + [[1, 2, 3], 'number[]'] +].freeze + +def create_inputs_set(cpf) + formatted = cpf.gsub(/(\d{3})(\d{3})(\d{3})(\d+)/, '\1.\2.\3-\4') + + [ + ['string', cpf], + ['formatted string', formatted], + ['array', cpf.chars], + ['formatted array', formatted.chars], + ['grouped array', formatted.split(/[.-]/)] + ] +end + +RSpec.describe CpfVal::CpfValidator do + describe '#initialize' do + context 'when called' do + it 'creates an instance of CpfValidator' do + expect(described_class.new).to be_a(described_class) + end + end + end + + describe '#is_valid' do + subject(:validator) { described_class.new } + + context 'when given a valid cpf' do + create_inputs_set('82911017366').each do |input_type, input_value| + it "returns true for a valid CPF #{input_type}" do + expect(validator.is_valid(input_value)).to be(true) + end + end + + VALID_CPF_SAMPLES.each do |cpf| + it "returns true for valid sample #{cpf}" do + expect(validator.is_valid(cpf)).to be(true) + end + end + + FORMATTED_VALID_CPFS.each do |format_label, cpf| + it "returns true for a formatted valid CPF (#{format_label})" do + expect(validator.is_valid(cpf)).to be(true) + end + end + end + + context 'when the length is wrong' do + create_inputs_set('8291101736').each do |input_type, input_value| + it "returns false for a CPF #{input_type} with less than 11 digits" do + expect(validator.is_valid(input_value)).to be(false) + end + end + + create_inputs_set('829110173666').each do |input_type, input_value| + it "returns false for a CPF #{input_type} with more than 11 digits" do + expect(validator.is_valid(input_value)).to be(false) + end + end + + SHORT_OR_LONG_NUMERIC_STRINGS.each do |cpf| + it "returns false for numeric string of wrong length #{cpf}" do + expect(validator.is_valid(cpf)).to be(false) + end + end + + it 'returns false for an empty array' do + expect(validator.is_valid([])).to be(false) + end + end + + context 'when the check digits are wrong' do + INVALID_CPF_SAMPLES.each do |cpf| + it "returns false for invalid CPF #{cpf}" do + expect(validator.is_valid(cpf)).to be(false) + end + end + + it 'returns false for every wrong check digit of a known base' do + base = '177170876' + valid_check_digits = '00' + + 100.times do |index| + check_digits = format('%02d', index) + input_value = "#{base}#{check_digits}" + expected = check_digits == valid_check_digits + + expect(validator.is_valid(input_value)).to be(expected) + end + end + end + + context 'when the cpf has all digits the same' do + REPEATED_DIGIT_PREFIXES.each do |prefix| + it "returns false for repeated-digit prefix #{prefix}" do + 100.times do |index| + input_value = format('%s%02d', prefix: prefix, index: index) + + expect(validator.is_valid(input_value)).to be(false) + end + end + end + end + + context 'when given a non-digit string' do + NON_DIGIT_STRINGS.each do |cpf| + it "returns false for non-digit string #{cpf.inspect}" do + expect(validator.is_valid(cpf)).to be(false) + end + end + end + + context 'when called with invalid arguments' do + it 'does not raise with string input' do + expect(validator.is_valid('12345678901')).to be(false) + end + + it 'does not raise with array of strings input' do + expect(validator.is_valid(['12345678901'])).to be(false) + end + + INVALID_INPUT_CASES.each do |input_value, actual_type| + it "raises TypeMismatchError for #{actual_type}" do + expect { validator.is_valid(input_value) } + .to raise_error(CpfVal::TypeMismatchError) do |error| + aggregate_failures do + expect(error.expected_type).to eq('string or string[]') + expect(error.actual_input).to equal(input_value) + expect(error.actual_type).to eq(actual_type) + expect(error.message) + .to eq("CPF input must be of type string or string[]. Got #{actual_type}.") + end + end + end + end + + it 'raises TypeMismatchError for a mixed array' do + input_value = ['1', 2] + + expect { validator.is_valid(input_value) } + .to raise_error(CpfVal::TypeMismatchError) do |error| + aggregate_failures do + expect(error.expected_type).to eq('string or string[]') + expect(error.actual_input).to equal(input_value) + expect(error.message).to match(/CPF input must be of type string or string\[\]/) + end + end + end + end + end +end diff --git a/packages/cpf-val/tests/errors.spec.rb b/packages/cpf-val/tests/errors.spec.rb new file mode 100644 index 0000000..e177ab6 --- /dev/null +++ b/packages/cpf-val/tests/errors.spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe CpfVal::Error do + it 'is a module' do + expect(described_class).to be_a(Module) + expect(described_class).not_to be_a(Class) + end +end + +RSpec.describe CpfVal::TypeMismatchError do + context 'when instantiated for CPF input' do + subject(:error) { described_class.new(123, 'string') } + + it 'is a TypeError' do + expect(error).to be_a(TypeError) + end + + it 'includes CpfVal::Error' do + expect(error).to be_a(CpfVal::Error) + end + + it 'exposes the class name' do + expect(error.class.name).to eq('CpfVal::TypeMismatchError') + end + + it 'sets actual_input' do + expect(error.actual_input).to eq(123) + end + + it 'sets actual_type' do + expect(error.actual_type).to eq('integer number') + end + + it 'sets expected_type' do + expect(described_class.new(123, 'string or string[]').expected_type) + .to eq('string or string[]') + end + + it 'builds a descriptive message' do + expect(described_class.new(123, 'string').message) + .to eq('CPF input must be of type string. Got integer number.') + end + end +end From 5bfa426e4c0475bb01a4d77b80f945cea453062b Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:36:12 -0300 Subject: [PATCH 04/19] docs(cpf-val): create changelogs file Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/cpf-val/CHANGELOG.md b/packages/cpf-val/CHANGELOG.md index 456ba14..f7806a5 100644 --- a/packages/cpf-val/CHANGELOG.md +++ b/packages/cpf-val/CHANGELOG.md @@ -1 +1,17 @@ # cpf-val + +## 1.0.0 + +### 🚀 Stable Version Released! + +Utility module to validate CPF (Brazilian personal ID) strings. Main features: + +- **Multiple interfaces**: supports `CpfVal.cpf_val` and `CpfVal::CpfValidator#is_valid` with no options or keyword arguments. +- **Flexible input**: accepts a `String` or `Array` (formatted or raw); non-digit characters are stripped before validation. +- **Strict validation**: requires exactly `CpfVal::CPF_LENGTH` (`11`) digits and matching check digits; repeated-digit bases return `false`. +- **Structured errors**: `CpfVal::Error` marker with misuse leaf `TypeMismatchError`; invalid CPF data returns `false` without raising. +- **Check-digit delegation**: validation uses `CpfDV::CpfCheckDigits` from `cpf-dv` for eligibility rules and verifier digits. +- **Constants**: `CpfVal::CPF_LENGTH` (`11`) for the required digit count after sanitization. +- **Dependencies**: runtime gems `cpf-dv` and `lacus-utils` (Ruby `>= 3.1`). + +For detailed usage and API reference, see the [README](./README.md). From 5f5b6fe60753cfed1734c3336f825053c4746a66 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:36:31 -0300 Subject: [PATCH 05/19] docs(cpf-val): create README file Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/README.md | 228 +++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 packages/cpf-val/README.md diff --git a/packages/cpf-val/README.md b/packages/cpf-val/README.md new file mode 100644 index 0000000..eb6a662 --- /dev/null +++ b/packages/cpf-val/README.md @@ -0,0 +1,228 @@ +![cpf-val for Ruby](https://br-utils.vercel.app/img/cover_cpf-val.jpg) + +[![Gem Version](https://img.shields.io/gem/v/cpf-val)](https://rubygems.org/gems/cpf-val) +[![Gem Downloads](https://img.shields.io/gem/dt/cpf-val)](https://rubygems.org/gems/cpf-val) +[![Ruby Version](https://img.shields.io/gem/rv/cpf-val)](https://www.ruby-lang.org/) +[![Test Status](https://img.shields.io/github/actions/workflow/status/LacusSolutions/br-utils-ruby/ci.yml?label=ci/cd)](https://github.com/LacusSolutions/br-utils-ruby/actions) +[![Last Update Date](https://img.shields.io/github/last-commit/LacusSolutions/br-utils-ruby)](https://github.com/LacusSolutions/br-utils-ruby) +[![Project License](https://img.shields.io/github/license/LacusSolutions/br-utils-ruby)](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE) + +> 🌎 [Acessar documentação em português](./README.pt.md) + +A Ruby utility to validate CPF (Brazilian Individual's Taxpayer ID) values. + +## Ruby Support + +| ![Ruby 3.2](https://img.shields.io/badge/Ruby-3.2-CC342D?logo=ruby&logoColor=white) | ![Ruby 3.3](https://img.shields.io/badge/Ruby-3.3-CC342D?logo=ruby&logoColor=white) | ![Ruby 3.4](https://img.shields.io/badge/Ruby-3.4-CC342D?logo=ruby&logoColor=white) | +| --- | --- | --- | +| Passing ✔ | Passing ✔ | Passing ✔ | + +## Features + +- ✅ **Fixed 11-digit CPF**: Validates the standard 11-digit Brazilian CPF via the official modulo-11 algorithm +- ✅ **Flexible input**: Accepts `String` or `Array` of strings; array elements are concatenated in order +- ✅ **Format agnostic**: Strips every non-digit character before validation +- ✅ **Repeated-digit rejection**: All-identical-digit bases (e.g. `111.111.111-11`, `00000000000`) are rejected +- ✅ **Error handling**: Typed API-misuse errors with a `CpfVal::Error` marker for library-wide rescue +- ✅ **Minimal dependencies**: [`cpf-dv`](https://rubygems.org/gems/cpf-dv) for check-digit calculation and [`lacus-utils`](https://rubygems.org/gems/lacus-utils) for type descriptions in error messages +- ✅ **Dual API style**: Object-oriented (`CpfVal::CpfValidator`) and functional (`CpfVal.cpf_val`) + +## Installation + +Install the gem directly: + +```bash +gem install cpf-val +``` + +Or add it to your `Gemfile` and run `bundle install`: + +```ruby +gem 'cpf-val' +``` + +## Require + +```ruby +require 'cpf-val' +``` + +## Quick Start + +```ruby +require 'cpf-val' + +validator = CpfVal::CpfValidator.new + +validator.is_valid('12345678909') # => true +validator.is_valid('123.456.789-09') # => true +validator.is_valid('12345678910') # => false (invalid check digits) +validator.is_valid('00000000000') # => false (repeated digits) +``` + +Functional helper: + +```ruby +require 'cpf-val' + +CpfVal.cpf_val('12345678909') # => true +CpfVal.cpf_val('123.456.789-09') # => true +CpfVal.cpf_val('12345678910') # => false +``` + +## Usage + +The main entry points are the class `CpfVal::CpfValidator` and the helper `CpfVal.cpf_val`. + +### `CpfVal::CpfValidator` + +- **`initialize`**: Takes no arguments. CPF validation has no configuration options. +- **`is_valid(cpf_input)`**: Validates a CPF value. + + Input is normalized to a string (arrays of strings are concatenated). Every non-digit character is then stripped. If the sanitized length is not exactly **11**, its base is an all-identical-digit sequence, or the check digits do not match (`CpfDV::CpfCheckDigits` from **`cpf-dv`**), the method returns `false` — no exception is raised for validation failure. + + If the input is not a `String` or an `Array` of strings, **`CpfVal::TypeMismatchError`** is raised. + +```ruby +require 'cpf-val' + +validator = CpfVal::CpfValidator.new + +validator.is_valid('123.456.789-09') # => true +validator.is_valid('12345678909') # => true +validator.is_valid(['123', '456', '789', '09']) # => true +validator.is_valid('12345678910') # => false (invalid check digits) +validator.is_valid('11111111111') # => false (repeated digits) +``` + +### Functional helper + +`CpfVal.cpf_val` builds a new `CpfVal::CpfValidator` and calls `is_valid(cpf_input)` once. It takes only the input value: + +```ruby +require 'cpf-val' + +CpfVal.cpf_val('11144477735') # => true +CpfVal.cpf_val('111.444.777-35') # => true +CpfVal.cpf_val('11144477736') # => false +``` + +### Input formats + +**String:** Plain digits or a formatted CPF (e.g. `123.456.789-09`, `499.784.420-90`, `011_258_960_00`). Non-digit characters are stripped before validation; the result must be exactly 11 digits. + +**Array of strings:** Each element must be a `String`; values are concatenated (e.g. per digit, grouped segments, or mixed with punctuation). Non-string elements raise **`CpfVal::TypeMismatchError`**. + +```ruby +require 'cpf-val' + +CpfVal.cpf_val(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '9']) # => true +CpfVal.cpf_val(['123.456', '789-09']) # => true +``` + +### Error handling + +This package raises only for **API misuse** (wrong input type). Validation failures (wrong length, ineligible base such as repeated digits, invalid check digits) return `false` and do not raise. + +Every custom error includes the `CpfVal::Error` marker module. + +#### Summary + +| Class | Inherits from | Category | Trigger condition | +|---|---|---|---| +| `CpfVal::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | CPF input is not a `String` or `Array` of strings | + +#### `CpfVal::Error` (marker module) + +- **Inheritance:** module marker mixed into every library error via `include` (not a class). +- **Category:** N/A (rescue target only) — not a failure mode by itself. +- **When it is raised:** Never raised directly; included by every custom error the library raises. +- **Example:** N/A +- **How to rescue it:** + +```ruby +rescue CpfVal::Error + # everything this library raises +``` + +#### `CpfVal::TypeMismatchError` + +- **Inheritance:** `CpfVal::TypeMismatchError < TypeError` (includes `CpfVal::Error`) +- **Category:** API misuse — the caller passed a value of the wrong type. +- **When it is raised:** Raised when the CPF input is not a `String` or an `Array` of strings (including when an array contains a non-string element). +- **Example:** + +```ruby +require 'cpf-val' + +begin + CpfVal.cpf_val(12_345_678_909) +rescue CpfVal::TypeMismatchError => e + puts e.message + # CPF input must be of type string or string[]. Got integer number. +end +``` + +- **How to rescue it:** + +```ruby +rescue CpfVal::TypeMismatchError + # this library's type-contract violation + +rescue TypeError + # native type errors, including this library's TypeMismatchError +``` + +#### Rescue granularity + +```ruby +# 1) Single native class — catches misuse errors of that kind. +rescue TypeError + # CpfVal::TypeMismatchError and any other TypeError (library or not) + +# 2) CpfVal::Error — catches everything the library raises. +rescue CpfVal::Error + # every custom error that includes CpfVal::Error + +# 3) Specific leaf class — catches only that exact failure mode. +rescue CpfVal::TypeMismatchError + # only CpfVal::TypeMismatchError +``` + +Notable attributes on raised errors: + +- `TypeMismatchError`: `actual_input`, `actual_type`, `expected_type` + +## API + +### Exports + +After `require 'cpf-val'`: + +- **`CpfVal.cpf_val`**: `(cpf_input) -> Boolean` — convenience helper. +- **`CpfVal::CpfValidator`**: Class to validate CPF (no options); accepts `String` or `Array` in `is_valid`. +- **`CpfVal::CPF_LENGTH`**: `11` (constant). +- **`CpfVal::VERSION`**: gem version string. +- **Type marker**: `CpfVal::CpfInput`. +- **Errors**: `CpfVal::Error`, `CpfVal::TypeMismatchError`. + +## Contribution & Support + +We welcome contributions! Please see our [Contributing Guidelines](https://github.com/LacusSolutions/br-utils-ruby/blob/main/CONTRIBUTING.md) for details. If you find this project helpful, please consider: + +- ⭐ Starring the repository +- 🤝 Contributing to the codebase +- 💡 [Suggesting new features](https://github.com/LacusSolutions/br-utils-ruby/issues) +- 🐛 [Reporting bugs](https://github.com/LacusSolutions/br-utils-ruby/issues) + +## License + +This project is licensed under the MIT License — see the [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE) file for details. + +## Changelog + +See [CHANGELOG](./CHANGELOG.md) for a list of changes and version history. + +--- + +Made with ❤️ by [Lacus Solutions](https://github.com/LacusSolutions) From 0b24140b80d52206670bb2423241536c5568cd7d Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:36:53 -0300 Subject: [PATCH 06/19] docs(cpf-val): create Portuguese version of README file Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/README.pt.md | 215 ++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 packages/cpf-val/README.pt.md diff --git a/packages/cpf-val/README.pt.md b/packages/cpf-val/README.pt.md new file mode 100644 index 0000000..e292766 --- /dev/null +++ b/packages/cpf-val/README.pt.md @@ -0,0 +1,215 @@ +![cpf-val para Ruby](https://br-utils.vercel.app/img/cover_cpf-val.jpg) + +> 🌎 [Access documentation in English](./README.md) + +Utilitário em Ruby para validar CPF (Cadastro de Pessoa Física). + +## Recursos + +- ✅ **CPF de 11 dígitos**: Valida o CPF brasileiro padrão de 11 dígitos pelo algoritmo oficial de módulo 11 +- ✅ **Entrada flexível**: Aceita `String` ou `Array` de strings; elementos do array são concatenados na ordem +- ✅ **Agnóstico ao formato**: Remove todos os caracteres não numéricos antes de validar +- ✅ **Rejeição de dígitos repetidos**: Bases com todos os dígitos iguais (ex.: `111.111.111-11`, `00000000000`) são rejeitadas +- ✅ **Tratamento de erros**: Erros tipados de uso incorreto da API, com marcador `CpfVal::Error` para rescue em toda a biblioteca +- ✅ **Dependências mínimas**: [`cpf-dv`](https://rubygems.org/gems/cpf-dv) para cálculo dos dígitos verificadores e [`lacus-utils`](https://rubygems.org/gems/lacus-utils) para descrição de tipos nas mensagens de erro +- ✅ **API dupla**: Orientada a objetos (`CpfVal::CpfValidator`) e funcional (`CpfVal.cpf_val`) + +## Instalação + +Instale a gem diretamente: + +```bash +gem install cpf-val +``` + +Ou adicione ao seu `Gemfile` e execute `bundle install`: + +```ruby +gem 'cpf-val' +``` + +## Require + +```ruby +require 'cpf-val' +``` + +## Início rápido + +```ruby +require 'cpf-val' + +validator = CpfVal::CpfValidator.new + +validator.is_valid('12345678909') # => true +validator.is_valid('123.456.789-09') # => true +validator.is_valid('12345678910') # => false (dígitos verificadores inválidos) +validator.is_valid('00000000000') # => false (dígitos repetidos) +``` + +Helper funcional: + +```ruby +require 'cpf-val' + +CpfVal.cpf_val('12345678909') # => true +CpfVal.cpf_val('123.456.789-09') # => true +CpfVal.cpf_val('12345678910') # => false +``` + +## Utilização + +Os pontos principais são a classe `CpfVal::CpfValidator` e o helper `CpfVal.cpf_val`. + +### `CpfVal::CpfValidator` + +- **`initialize`**: Não recebe argumentos. A validação de CPF não possui opções de configuração. +- **`is_valid(cpf_input)`**: Valida um valor CPF. + + A entrada é normalizada para string (arrays de strings são concatenados). Em seguida, todos os caracteres não numéricos são removidos. Se o comprimento após sanitização não for exatamente **11**, se a base for uma sequência de dígitos todos iguais ou se os dígitos verificadores não coincidirem (`CpfDV::CpfCheckDigits` de **`cpf-dv`**), o método retorna `false` — nenhuma exceção é lançada por falha de validação. + + Se a entrada não for `String` nem `Array` de strings, é lançada **`CpfVal::TypeMismatchError`**. + +```ruby +require 'cpf-val' + +validator = CpfVal::CpfValidator.new + +validator.is_valid('123.456.789-09') # => true +validator.is_valid('12345678909') # => true +validator.is_valid(['123', '456', '789', '09']) # => true +validator.is_valid('12345678910') # => false (dígitos verificadores inválidos) +validator.is_valid('11111111111') # => false (dígitos repetidos) +``` + +### Helper funcional + +`CpfVal.cpf_val` instancia um novo `CpfVal::CpfValidator` e chama `is_valid(cpf_input)` uma vez. Recebe apenas o valor de entrada: + +```ruby +require 'cpf-val' + +CpfVal.cpf_val('11144477735') # => true +CpfVal.cpf_val('111.444.777-35') # => true +CpfVal.cpf_val('11144477736') # => false +``` + +### Formatos de entrada + +**String:** Apenas dígitos ou CPF já formatado (ex.: `123.456.789-09`, `499.784.420-90`, `011_258_960_00`). Caracteres não numéricos são removidos antes da validação; o resultado deve ter exatamente 11 dígitos. + +**Array de strings:** Cada elemento deve ser `String`; os valores são concatenados (ex.: por dígito, segmentos agrupados ou misturados com pontuação). Elementos que não sejam string lançam **`CpfVal::TypeMismatchError`**. + +```ruby +require 'cpf-val' + +CpfVal.cpf_val(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '9']) # => true +CpfVal.cpf_val(['123.456', '789-09']) # => true +``` + +### Tratamento de erros + +Este pacote levanta erro apenas por **uso incorreto da API** (tipo de entrada incorreto). Falhas de validação (comprimento incorreto, base inelegível como dígitos repetidos, dígitos verificadores inválidos) retornam `false` e não levantam exceção. + +Todo erro customizado inclui o módulo marcador `CpfVal::Error`. + +#### Resumo + +| Classe | Herda de | Categoria | Condição de disparo | +|---|---|---|---| +| `CpfVal::TypeMismatchError` | `TypeError` (+ `include Error`) | Uso incorreto da API | Entrada CPF não é `String` nem `Array` de strings | + +#### `CpfVal::Error` (módulo marcador) + +- **Herança:** módulo marcador misturado em todo erro da biblioteca via `include` (não é uma classe). +- **Categoria:** N/A (apenas alvo de rescue) — não é um modo de falha por si só. +- **Quando é levantado:** Nunca levantado diretamente; incluído por todo erro customizado que a biblioteca levanta. +- **Exemplo:** N/A +- **Como resgatá-lo:** + +```ruby +rescue CpfVal::Error + # tudo o que esta biblioteca levanta +``` + +#### `CpfVal::TypeMismatchError` + +- **Herança:** `CpfVal::TypeMismatchError < TypeError` (inclui `CpfVal::Error`) +- **Categoria:** Uso incorreto da API — o chamador passou um valor do tipo errado. +- **Quando é levantado:** Levantado quando a entrada CPF não é `String` nem `Array` de strings (incluindo quando um array contém um elemento que não é string). +- **Exemplo:** + +```ruby +require 'cpf-val' + +begin + CpfVal.cpf_val(12_345_678_909) +rescue CpfVal::TypeMismatchError => e + puts e.message + # CPF input must be of type string or string[]. Got integer number. +end +``` + +- **Como resgatá-lo:** + +```ruby +rescue CpfVal::TypeMismatchError + # violação de contrato de tipo desta biblioteca + +rescue TypeError + # erros nativos de tipo, incluindo TypeMismatchError desta biblioteca +``` + +#### Granularidade de rescue + +```ruby +# 1) Classe nativa única — captura erros de uso incorreto desse tipo. +rescue TypeError + # CpfVal::TypeMismatchError e qualquer outro TypeError (da biblioteca ou não) + +# 2) CpfVal::Error — captura tudo o que a biblioteca levanta. +rescue CpfVal::Error + # todo erro customizado que inclui CpfVal::Error + +# 3) Classe folha específica — captura apenas aquele modo de falha. +rescue CpfVal::TypeMismatchError + # apenas CpfVal::TypeMismatchError +``` + +Atributos notáveis nos erros levantados: + +- `TypeMismatchError`: `actual_input`, `actual_type`, `expected_type` + +## API + +### Exportações + +Após `require 'cpf-val'`: + +- **`CpfVal.cpf_val`**: `(cpf_input) -> Boolean` — helper de conveniência. +- **`CpfVal::CpfValidator`**: Classe para validar CPF (sem opções); aceita `String` ou `Array` em `is_valid`. +- **`CpfVal::CPF_LENGTH`**: `11` (constante). +- **`CpfVal::VERSION`**: string de versão da gem. +- **Marcador de tipo**: `CpfVal::CpfInput`. +- **Erros**: `CpfVal::Error`, `CpfVal::TypeMismatchError`. + +## Contribuição e suporte + +Contribuições são bem-vindas! Consulte as [Diretrizes de contribuição](https://github.com/LacusSolutions/br-utils-ruby/blob/main/CONTRIBUTING.md). Se este projeto for útil para você, considere: + +- ⭐ Dar uma estrela ao repositório +- 🤝 Contribuir com o código +- 💡 [Sugerir novos recursos](https://github.com/LacusSolutions/br-utils-ruby/issues) +- 🐛 [Reportar bugs](https://github.com/LacusSolutions/br-utils-ruby/issues) + +## Licença + +Este projeto está licenciado sob a MIT License — consulte o arquivo [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE). + +## Changelog + +Consulte o [CHANGELOG](./CHANGELOG.md) para histórico de versões e alterações. + +--- + +Feito com ❤️ por [Lacus Solutions](https://github.com/LacusSolutions) From 38505f6a173e256380dcc925292ab10c7839c359 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 23:28:26 -0300 Subject: [PATCH 07/19] docs(cpf-val): reinforce error hierarchy Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3626433721. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/README.md | 15 ++++++++++----- packages/cpf-val/README.pt.md | 15 ++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/cpf-val/README.md b/packages/cpf-val/README.md index eb6a662..68347f5 100644 --- a/packages/cpf-val/README.md +++ b/packages/cpf-val/README.md @@ -124,13 +124,13 @@ CpfVal.cpf_val(['123.456', '789-09']) # => true This package raises only for **API misuse** (wrong input type). Validation failures (wrong length, ineligible base such as repeated digits, invalid check digits) return `false` and do not raise. -Every custom error includes the `CpfVal::Error` marker module. +Every custom error includes the `CpfVal::Error` marker module. This package defines **no** `CpfVal::DomainError` and no domain leaves — invalid CPF data never raises a domain error. #### Summary | Class | Inherits from | Category | Trigger condition | |---|---|---|---| -| `CpfVal::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | CPF input is not a `String` or `Array` of strings | +| `CpfVal::TypeMismatchError` | `TypeError` (+ `include CpfVal::Error`) | API misuse | CPF input is not a `String` or `Array` of strings | #### `CpfVal::Error` (marker module) @@ -176,15 +176,20 @@ rescue TypeError #### Rescue granularity ```ruby -# 1) Single native class — catches misuse errors of that kind. +# 1) Single native class — catches misuse errors of that kind, +# including non-library ones already handled elsewhere in the consumer's code. rescue TypeError # CpfVal::TypeMismatchError and any other TypeError (library or not) -# 2) CpfVal::Error — catches everything the library raises. +# 2) CpfVal::DomainError — not applicable: this package defines no DomainError +# (and no domain leaves). Invalid CPF data returns false instead of raising. +# rescue CpfVal::DomainError # NameError — constant is not defined + +# 3) CpfVal::Error — catches everything the library raises, regardless of native ancestry. rescue CpfVal::Error # every custom error that includes CpfVal::Error -# 3) Specific leaf class — catches only that exact failure mode. +# 4) Specific leaf class — catches only that exact failure mode. rescue CpfVal::TypeMismatchError # only CpfVal::TypeMismatchError ``` diff --git a/packages/cpf-val/README.pt.md b/packages/cpf-val/README.pt.md index e292766..2350576 100644 --- a/packages/cpf-val/README.pt.md +++ b/packages/cpf-val/README.pt.md @@ -111,13 +111,13 @@ CpfVal.cpf_val(['123.456', '789-09']) # => true Este pacote levanta erro apenas por **uso incorreto da API** (tipo de entrada incorreto). Falhas de validação (comprimento incorreto, base inelegível como dígitos repetidos, dígitos verificadores inválidos) retornam `false` e não levantam exceção. -Todo erro customizado inclui o módulo marcador `CpfVal::Error`. +Todo erro customizado inclui o módulo marcador `CpfVal::Error`. Este pacote **não** define `CpfVal::DomainError` nem folhas de domínio — CPF inválido nunca levanta erro de domínio. #### Resumo | Classe | Herda de | Categoria | Condição de disparo | |---|---|---|---| -| `CpfVal::TypeMismatchError` | `TypeError` (+ `include Error`) | Uso incorreto da API | Entrada CPF não é `String` nem `Array` de strings | +| `CpfVal::TypeMismatchError` | `TypeError` (+ `include CpfVal::Error`) | Uso incorreto da API | Entrada CPF não é `String` nem `Array` de strings | #### `CpfVal::Error` (módulo marcador) @@ -163,15 +163,20 @@ rescue TypeError #### Granularidade de rescue ```ruby -# 1) Classe nativa única — captura erros de uso incorreto desse tipo. +# 1) Classe nativa única — captura erros de uso incorreto desse tipo, +# inclusive TypeError de fora da biblioteca já tratados no código do consumidor. rescue TypeError # CpfVal::TypeMismatchError e qualquer outro TypeError (da biblioteca ou não) -# 2) CpfVal::Error — captura tudo o que a biblioteca levanta. +# 2) CpfVal::DomainError — não se aplica: este pacote não define DomainError +# (nem folhas de domínio). CPF inválido retorna false em vez de levantar. +# rescue CpfVal::DomainError # NameError — a constante não está definida + +# 3) CpfVal::Error — captura tudo o que a biblioteca levanta, independente da ancestralidade nativa. rescue CpfVal::Error # todo erro customizado que inclui CpfVal::Error -# 3) Classe folha específica — captura apenas aquele modo de falha. +# 4) Classe folha específica — captura apenas aquele modo de falha. rescue CpfVal::TypeMismatchError # apenas CpfVal::TypeMismatchError ``` From 765f8dedf472dad22e16cab7868c51375a29b635 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 23:31:41 -0300 Subject: [PATCH 08/19] test(cpf-val): update error message for consistency across codebase Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3626433723. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/tests/cpf_val.spec.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/cpf-val/tests/cpf_val.spec.rb b/packages/cpf-val/tests/cpf_val.spec.rb index 84574e5..b99b613 100644 --- a/packages/cpf-val/tests/cpf_val.spec.rb +++ b/packages/cpf-val/tests/cpf_val.spec.rb @@ -76,14 +76,16 @@ end it 'exposes instantiable TypeMismatchError' do - error = described_class::TypeMismatchError.new(123, 'string') + error = described_class::TypeMismatchError.new(123, 'string or string[]') aggregate_failures do expect(error.actual_input).to eq(123) expect(error.actual_type).to eq('integer number') - expect(error.expected_type).to eq('string') + expect(error.expected_type).to eq('string or string[]') expect(error).to be_a(described_class::Error) - expect(error.message).to eq('CPF input must be of type string. Got integer number.') + expect(error.message).to eq( + 'CPF input must be of type string or string[]. Got integer number.' + ) end end end From 05633bf2b8d374985d2c859a3e8aed5a6502ea34 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 23:44:38 -0300 Subject: [PATCH 09/19] refactor(cpf-val): fix CPF input type Fix as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3626433726. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/README.md | 2 +- packages/cpf-val/README.pt.md | 2 +- packages/cpf-val/src/cpf-val.rb | 2 +- packages/cpf-val/src/cpf-val/cpf_validator.rb | 13 +++---- packages/cpf-val/src/cpf-val/types.rb | 36 +++++++++++++++---- packages/cpf-val/tests/cpf_val.spec.rb | 14 ++++++++ 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/packages/cpf-val/README.md b/packages/cpf-val/README.md index 68347f5..870a62e 100644 --- a/packages/cpf-val/README.md +++ b/packages/cpf-val/README.md @@ -208,7 +208,7 @@ After `require 'cpf-val'`: - **`CpfVal::CpfValidator`**: Class to validate CPF (no options); accepts `String` or `Array` in `is_valid`. - **`CpfVal::CPF_LENGTH`**: `11` (constant). - **`CpfVal::VERSION`**: gem version string. -- **Type marker**: `CpfVal::CpfInput`. +- **Type predicate**: `CpfVal::CpfInput` — `CpfInput.accept?(value)` / `CpfInput === value` is true only for `String` or `Array`. - **Errors**: `CpfVal::Error`, `CpfVal::TypeMismatchError`. ## Contribution & Support diff --git a/packages/cpf-val/README.pt.md b/packages/cpf-val/README.pt.md index 2350576..ccef6d6 100644 --- a/packages/cpf-val/README.pt.md +++ b/packages/cpf-val/README.pt.md @@ -195,7 +195,7 @@ Após `require 'cpf-val'`: - **`CpfVal::CpfValidator`**: Classe para validar CPF (sem opções); aceita `String` ou `Array` em `is_valid`. - **`CpfVal::CPF_LENGTH`**: `11` (constante). - **`CpfVal::VERSION`**: string de versão da gem. -- **Marcador de tipo**: `CpfVal::CpfInput`. +- **Predicado de tipo**: `CpfVal::CpfInput` — `CpfInput.accept?(value)` / `CpfInput === value` é verdadeiro apenas para `String` ou `Array`. - **Erros**: `CpfVal::Error`, `CpfVal::TypeMismatchError`. ## Contribuição e suporte diff --git a/packages/cpf-val/src/cpf-val.rb b/packages/cpf-val/src/cpf-val.rb index f5a08b2..836185c 100644 --- a/packages/cpf-val/src/cpf-val.rb +++ b/packages/cpf-val/src/cpf-val.rb @@ -24,7 +24,7 @@ # - {CpfVal.cpf_val} # - {CpfVal::CpfValidator} # - {CpfVal::CPF_LENGTH}, {CpfVal::VERSION} -# - Type marker: {CpfVal::CpfInput} +# - Type predicate: {CpfVal::CpfInput} (+String+ or +Array+) # - Error marker {CpfVal::Error}; misuse error {CpfVal::TypeMismatchError} # # @example diff --git a/packages/cpf-val/src/cpf-val/cpf_validator.rb b/packages/cpf-val/src/cpf-val/cpf_validator.rb index c977b71..b0f8687 100644 --- a/packages/cpf-val/src/cpf-val/cpf_validator.rb +++ b/packages/cpf-val/src/cpf-val/cpf_validator.rb @@ -3,6 +3,7 @@ require 'cpf-dv' require_relative 'errors' +require_relative 'types' module CpfVal # The standard length of a CPF (Cadastro de Pessoa Física) identifier (11 @@ -46,17 +47,11 @@ def is_valid(cpf_input) private def to_string_input(cpf_input) - return cpf_input if cpf_input.is_a?(String) - - if cpf_input.is_a?(Array) - cpf_input.each do |item| - raise TypeMismatchError.new(cpf_input, 'string or string[]') unless item.is_a?(String) - end + raise TypeMismatchError.new(cpf_input, 'string or string[]') unless CpfInput.accept?(cpf_input) - return cpf_input.join - end + return cpf_input if cpf_input.is_a?(String) - raise TypeMismatchError.new(cpf_input, 'string or string[]') + cpf_input.join end def validate_with_check_digits(sanitized_cpf) diff --git a/packages/cpf-val/src/cpf-val/types.rb b/packages/cpf-val/src/cpf-val/types.rb index 030974e..698ae9e 100644 --- a/packages/cpf-val/src/cpf-val/types.rb +++ b/packages/cpf-val/src/cpf-val/types.rb @@ -1,15 +1,39 @@ # frozen_string_literal: true module CpfVal - # Represents valid input types for CPF validation. + # Case-equality predicate for CPF input: +String+ or +Array+. # - # A CPF may be given as: + # Matches the runtime contract of {CpfValidator#is_valid} and {CpfVal.cpf_val} + # (same shape as +cpf-fmt+ / +cnpj-val+ input handling). Use + # {CpfInput.accept?} or +CpfInput === value+ (in +case+/+when+) to test + # candidacy without raising. # - # - A string of numeric characters (with or without formatting). - # - An array of strings, each representing one or more numeric characters - # and/or punctuation. + # @example + # CpfVal::CpfInput.accept?('82911017366') # => true + # CpfVal::CpfInput.accept?(%w[8 2 9 1 1]) # => true + # CpfVal::CpfInput.accept?(123) # => false + # CpfVal::CpfInput.accept?([1, 2, 3]) # => false # # @see CpfValidator#is_valid # @see CpfVal.cpf_val - CpfInput = Object + module CpfInput + class << self + # @param value [Object] candidate input + # @return [Boolean] whether +value+ is a +String+ or an +Array+ of +String+ + def accept?(value) + return true if value.is_a?(String) + return false unless value.is_a?(Array) + + value.all?(String) + end + + # Case-equality entry point for +case+/+when+ and +===+ checks. + # + # @param value [Object] candidate input + # @return [Boolean] + def ===(value) + accept?(value) + end + end + end end diff --git a/packages/cpf-val/tests/cpf_val.spec.rb b/packages/cpf-val/tests/cpf_val.spec.rb index b99b613..ad396c4 100644 --- a/packages/cpf-val/tests/cpf_val.spec.rb +++ b/packages/cpf-val/tests/cpf_val.spec.rb @@ -53,6 +53,20 @@ end context 'when inspecting public types' do + it 'exposes CpfInput as a String or Array predicate' do + aggregate_failures do + expect(described_class::CpfInput.accept?('82911017366')).to be(true) + expect(described_class::CpfInput.accept?(%w[8 2 9])).to be(true) + expect(described_class::CpfInput.accept?(123)).to be(false) + expect(described_class::CpfInput.accept?([1, 2, 3])).to be(false) + expect(described_class::CpfInput.accept?(['8', 2])).to be(false) + # rubocop:disable Style/CaseEquality -- public case-equality protocol + expect(described_class::CpfInput === 123).to be(false) + expect(described_class::CpfInput === '82911017366').to be(true) + # rubocop:enable Style/CaseEquality + end + end + it 'exposes cpf_val as a callable helper' do aggregate_failures do expect(described_class).to respond_to(:cpf_val) From fe4af3183373c1cb174e9ab7fb1c55824098d63d Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 23:45:51 -0300 Subject: [PATCH 10/19] refactor(cnpj-val): fix CPF input type Fix as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3626433726. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cnpj-val/README.md | 3 +- packages/cnpj-val/README.pt.md | 3 +- packages/cnpj-val/src/cnpj-val.rb | 4 +-- .../cnpj-val/src/cnpj-val/cnpj_validator.rb | 13 +++---- packages/cnpj-val/src/cnpj-val/types.rb | 34 ++++++++++++++++--- packages/cnpj-val/tests/cnpj_val.spec.rb | 16 +++++++++ 6 files changed, 55 insertions(+), 18 deletions(-) diff --git a/packages/cnpj-val/README.md b/packages/cnpj-val/README.md index 8cf17a0..6ad7140 100644 --- a/packages/cnpj-val/README.md +++ b/packages/cnpj-val/README.md @@ -336,7 +336,8 @@ After `require 'cnpj-val'`: - **`CnpjVal::CnpjValidatorOptions`**: Class holding options; supports merge via constructor, `set`, and keyword arguments. - **`CnpjVal::CNPJ_LENGTH`**: `14` (constant). - **`CnpjVal::VERSION`**: gem version string. -- **Type markers**: `CnpjVal::CnpjInput`, `CnpjVal::CnpjType`, `CnpjVal::CnpjValidatorOptionsInput`. +- **Type predicate**: `CnpjVal::CnpjInput` — `CnpjInput.accept?(value)` / `CnpjInput === value` is true only for `String` or `Array`. +- **Type markers**: `CnpjVal::CnpjType`, `CnpjVal::CnpjValidatorOptionsInput`. - **Errors**: `CnpjVal::Error`, `CnpjVal::DomainError`, `CnpjVal::InvalidArgumentCombinationError`, `CnpjVal::TypeMismatchError`, `CnpjVal::ValidationError`. ### Other available resources diff --git a/packages/cnpj-val/README.pt.md b/packages/cnpj-val/README.pt.md index f06b1c9..9c13be9 100644 --- a/packages/cnpj-val/README.pt.md +++ b/packages/cnpj-val/README.pt.md @@ -323,7 +323,8 @@ Após `require 'cnpj-val'`: - **`CnpjVal::CnpjValidatorOptions`**: Classe que armazena opções; suporta mesclagem via construtor, `set` e argumentos nomeados. - **`CnpjVal::CNPJ_LENGTH`**: `14` (constante). - **`CnpjVal::VERSION`**: string de versão da gem. -- **Marcadores de tipo**: `CnpjVal::CnpjInput`, `CnpjVal::CnpjType`, `CnpjVal::CnpjValidatorOptionsInput`. +- **Predicado de tipo**: `CnpjVal::CnpjInput` — `CnpjInput.accept?(value)` / `CnpjInput === value` é verdadeiro apenas para `String` ou `Array`. +- **Marcadores de tipo**: `CnpjVal::CnpjType`, `CnpjVal::CnpjValidatorOptionsInput`. - **Erros**: `CnpjVal::Error`, `CnpjVal::DomainError`, `CnpjVal::InvalidArgumentCombinationError`, `CnpjVal::TypeMismatchError`, `CnpjVal::ValidationError`. ### Outros recursos disponíveis diff --git a/packages/cnpj-val/src/cnpj-val.rb b/packages/cnpj-val/src/cnpj-val.rb index 21439dd..2c46607 100644 --- a/packages/cnpj-val/src/cnpj-val.rb +++ b/packages/cnpj-val/src/cnpj-val.rb @@ -30,8 +30,8 @@ # - {CnpjVal.cnpj_val} # - {CnpjVal::CnpjValidator}, {CnpjVal::CnpjValidatorOptions} # - {CnpjVal::CNPJ_LENGTH}, {CnpjVal::VERSION} -# - Type markers: {CnpjVal::CnpjInput}, {CnpjVal::CnpjType}, -# {CnpjVal::CnpjValidatorOptionsInput} +# - Type predicate: {CnpjVal::CnpjInput} (+String+ or +Array+); +# type markers: {CnpjVal::CnpjType}, {CnpjVal::CnpjValidatorOptionsInput} # - Error marker {CnpjVal::Error}; domain ancestor {CnpjVal::DomainError}; # misuse errors {CnpjVal::TypeMismatchError} and # {CnpjVal::InvalidArgumentCombinationError}; domain leaf diff --git a/packages/cnpj-val/src/cnpj-val/cnpj_validator.rb b/packages/cnpj-val/src/cnpj-val/cnpj_validator.rb index 114da73..4494631 100644 --- a/packages/cnpj-val/src/cnpj-val/cnpj_validator.rb +++ b/packages/cnpj-val/src/cnpj-val/cnpj_validator.rb @@ -4,6 +4,7 @@ require_relative 'cnpj_validator_options' require_relative 'errors' +require_relative 'types' module CnpjVal # Validator for CNPJ (Cadastro Nacional da Pessoa Jurídica) identifiers. @@ -133,17 +134,11 @@ def sanitize_cnpj_input(cnpj_input, actual_options) end def to_string_input(cnpj_input) - return cnpj_input if cnpj_input.is_a?(String) - - if cnpj_input.is_a?(Array) - cnpj_input.each do |item| - raise TypeMismatchError.new(cnpj_input, 'string or string[]') unless item.is_a?(String) - end + raise TypeMismatchError.new(cnpj_input, 'string or string[]') unless CnpjInput.accept?(cnpj_input) - return cnpj_input.join - end + return cnpj_input if cnpj_input.is_a?(String) - raise TypeMismatchError.new(cnpj_input, 'string or string[]') + cnpj_input.join end def sanitize(value, cnpj_type) diff --git a/packages/cnpj-val/src/cnpj-val/types.rb b/packages/cnpj-val/src/cnpj-val/types.rb index 74098a5..5a98ea6 100644 --- a/packages/cnpj-val/src/cnpj-val/types.rb +++ b/packages/cnpj-val/src/cnpj-val/types.rb @@ -11,16 +11,40 @@ module CnpjVal # Allowed values for the +type+ option. CNPJ_TYPE_OPTIONS = %w[alphanumeric numeric].freeze - # Represents valid input types for CNPJ validation. + # Case-equality predicate for CNPJ input: +String+ or +Array+. # - # A CNPJ may be given as: + # Matches the runtime contract of {CnpjValidator#is_valid} and {CnpjVal.cnpj_val}. + # Use {CnpjInput.accept?} or +CnpjInput === value+ (in +case+/+when+) to test + # candidacy without raising. # - # - A string of alphanumeric characters (with or without formatting). - # - An array of strings, each representing one or more alphanumeric characters. + # @example + # CnpjVal::CnpjInput.accept?('91415732000793') # => true + # CnpjVal::CnpjInput.accept?(%w[9 1 4 1]) # => true + # CnpjVal::CnpjInput.accept?(123) # => false + # CnpjVal::CnpjInput.accept?([1, 2, 3]) # => false # # @see CnpjValidator#is_valid # @see CnpjVal.cnpj_val - CnpjInput = Object + module CnpjInput + class << self + # @param value [Object] candidate input + # @return [Boolean] whether +value+ is a +String+ or an +Array+ of +String+ + def accept?(value) + return true if value.is_a?(String) + return false unless value.is_a?(Array) + + value.all?(String) + end + + # Case-equality entry point for +case+/+when+ and +===+ checks. + # + # @param value [Object] candidate input + # @return [Boolean] + def ===(value) + accept?(value) + end + end + end # Character set for CNPJ values (generation or validation). # diff --git a/packages/cnpj-val/tests/cnpj_val.spec.rb b/packages/cnpj-val/tests/cnpj_val.spec.rb index 63c0adc..225a615 100644 --- a/packages/cnpj-val/tests/cnpj_val.spec.rb +++ b/packages/cnpj-val/tests/cnpj_val.spec.rb @@ -59,4 +59,20 @@ end end end + + context 'when inspecting public types' do + it 'exposes CnpjInput as a String or Array predicate' do + aggregate_failures do + expect(described_class::CnpjInput.accept?('91415732000793')).to be(true) + expect(described_class::CnpjInput.accept?(%w[9 1 4])).to be(true) + expect(described_class::CnpjInput.accept?(123)).to be(false) + expect(described_class::CnpjInput.accept?([1, 2, 3])).to be(false) + expect(described_class::CnpjInput.accept?(['9', 1])).to be(false) + # rubocop:disable Style/CaseEquality -- public case-equality protocol + expect(described_class::CnpjInput === 123).to be(false) + expect(described_class::CnpjInput === '91415732000793').to be(true) + # rubocop:enable Style/CaseEquality + end + end + end end From e1e5d5b67678db37a60fa0c600b1893ad4a6688f Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 23:46:05 -0300 Subject: [PATCH 11/19] refactor(cpf-fmt): fix CPF input type Fix as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3626433726. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-fmt/README.md | 1 + packages/cpf-fmt/README.pt.md | 1 + packages/cpf-fmt/src/cpf-fmt.rb | 1 + packages/cpf-fmt/src/cpf-fmt/types.rb | 34 ++++++++++++++++++++++---- packages/cpf-fmt/src/cpf-fmt/utils.rb | 12 +++------ packages/cpf-fmt/tests/cpf_fmt.spec.rb | 16 ++++++++++++ 6 files changed, 51 insertions(+), 14 deletions(-) diff --git a/packages/cpf-fmt/README.md b/packages/cpf-fmt/README.md index 60ec2de..946e498 100644 --- a/packages/cpf-fmt/README.md +++ b/packages/cpf-fmt/README.md @@ -422,6 +422,7 @@ After `require 'cpf-fmt'`: - **`CpfFmt::CpfFormatterOptions`**: Class holding options; supports merge via constructor, `set`, and keyword arguments. - **`CpfFmt::CPF_LENGTH`**: `11` (constant). - **`CpfFmt::VERSION`**: gem version string. +- **Type predicate**: `CpfFmt::CpfInput` — `CpfInput.accept?(value)` / `CpfInput === value` is true only for `String` or `Array`. - **Errors**: `CpfFmt::Error`, `CpfFmt::DomainError`, `CpfFmt::InvalidArgumentCombinationError`, `CpfFmt::TypeMismatchError`, `CpfFmt::InvalidLengthError`, `CpfFmt::OutOfRangeError`, `CpfFmt::ValidationError`. ### Other available resources diff --git a/packages/cpf-fmt/README.pt.md b/packages/cpf-fmt/README.pt.md index f60014c..93eb273 100644 --- a/packages/cpf-fmt/README.pt.md +++ b/packages/cpf-fmt/README.pt.md @@ -408,6 +408,7 @@ Após `require 'cpf-fmt'`: - **`CpfFmt::CpfFormatterOptions`**: Classe que armazena opções; suporta mesclagem via construtor, `set` e argumentos nomeados. - **`CpfFmt::CPF_LENGTH`**: `11` (constante). - **`CpfFmt::VERSION`**: string de versão da gem. +- **Predicado de tipo**: `CpfFmt::CpfInput` — `CpfInput.accept?(value)` / `CpfInput === value` é verdadeiro apenas para `String` ou `Array`. - **Erros**: `CpfFmt::Error`, `CpfFmt::DomainError`, `CpfFmt::InvalidArgumentCombinationError`, `CpfFmt::TypeMismatchError`, `CpfFmt::InvalidLengthError`, `CpfFmt::OutOfRangeError`, `CpfFmt::ValidationError`. ### Outros recursos disponíveis diff --git a/packages/cpf-fmt/src/cpf-fmt.rb b/packages/cpf-fmt/src/cpf-fmt.rb index 924a58d..b35b62f 100644 --- a/packages/cpf-fmt/src/cpf-fmt.rb +++ b/packages/cpf-fmt/src/cpf-fmt.rb @@ -30,6 +30,7 @@ # - {CpfFmt.cpf_fmt} # - {CpfFormatter}, {CpfFormatterOptions} # - {CPF_LENGTH}, {VERSION} +# - Type predicate: {CpfFmt::CpfInput} (+String+ or +Array+) # - Error marker {CpfFmt::Error}; domain ancestor {CpfFmt::DomainError}; # misuse errors {CpfFmt::TypeMismatchError} and # {CpfFmt::InvalidArgumentCombinationError}; domain leaves diff --git a/packages/cpf-fmt/src/cpf-fmt/types.rb b/packages/cpf-fmt/src/cpf-fmt/types.rb index 8fe1392..83e534c 100644 --- a/packages/cpf-fmt/src/cpf-fmt/types.rb +++ b/packages/cpf-fmt/src/cpf-fmt/types.rb @@ -10,16 +10,40 @@ module CpfFmt hidden hidden_key hidden_start hidden_end dot_key dash_key escape encode on_fail ].freeze - # Represents valid input types for CPF formatting. + # Case-equality predicate for CPF input: +String+ or +Array+. # - # A CPF can be provided as: + # Matches the runtime contract of {CpfFormatter#format} and {CpfFmt.cpf_fmt}. + # Use {CpfInput.accept?} or +CpfInput === value+ (in +case+/+when+) to test + # candidacy without raising. # - # - A string containing digits (with or without formatting) - # - An array of strings, where each string represents a digit or group of digits + # @example + # CpfFmt::CpfInput.accept?('82911017366') # => true + # CpfFmt::CpfInput.accept?(%w[8 2 9 1 1]) # => true + # CpfFmt::CpfInput.accept?(123) # => false + # CpfFmt::CpfInput.accept?([1, 2, 3]) # => false # # @see CpfFormatter#format # @see CpfFmt.cpf_fmt - CpfInput = Object + module CpfInput + class << self + # @param value [Object] candidate input + # @return [Boolean] whether +value+ is a +String+ or an +Array+ of +String+ + def accept?(value) + return true if value.is_a?(String) + return false unless value.is_a?(Array) + + value.all?(String) + end + + # Case-equality entry point for +case+/+when+ and +===+ checks. + # + # @param value [Object] candidate input + # @return [Boolean] + def ===(value) + accept?(value) + end + end + end # Callback function type for handling formatting failures. # diff --git a/packages/cpf-fmt/src/cpf-fmt/utils.rb b/packages/cpf-fmt/src/cpf-fmt/utils.rb index 27da09a..439ffc5 100644 --- a/packages/cpf-fmt/src/cpf-fmt/utils.rb +++ b/packages/cpf-fmt/src/cpf-fmt/utils.rb @@ -107,17 +107,11 @@ def apply_post_processing(formatted_cpf, options) # @return [String] joined string input # @raise [TypeMismatchError] if the input is not a +String+ or +Array+ def to_string_input(cpf_input) - return cpf_input if cpf_input.is_a?(String) - - if cpf_input.is_a?(Array) - cpf_input.each do |item| - raise TypeMismatchError.new(cpf_input, 'string or string[]') unless item.is_a?(String) - end + raise TypeMismatchError.new(cpf_input, 'string or string[]') unless CpfInput.accept?(cpf_input) - return cpf_input.join - end + return cpf_input if cpf_input.is_a?(String) - raise TypeMismatchError.new(cpf_input, 'string or string[]') + cpf_input.join end # Invokes the +on_fail+ callback and validates its return type. diff --git a/packages/cpf-fmt/tests/cpf_fmt.spec.rb b/packages/cpf-fmt/tests/cpf_fmt.spec.rb index 4f6acf0..9d775ff 100644 --- a/packages/cpf-fmt/tests/cpf_fmt.spec.rb +++ b/packages/cpf-fmt/tests/cpf_fmt.spec.rb @@ -60,4 +60,20 @@ end end end + + context 'when inspecting public types' do + it 'exposes CpfInput as a String or Array predicate' do + aggregate_failures do + expect(described_class::CpfInput.accept?('82911017366')).to be(true) + expect(described_class::CpfInput.accept?(%w[8 2 9])).to be(true) + expect(described_class::CpfInput.accept?(123)).to be(false) + expect(described_class::CpfInput.accept?([1, 2, 3])).to be(false) + expect(described_class::CpfInput.accept?(['8', 2])).to be(false) + # rubocop:disable Style/CaseEquality -- public case-equality protocol + expect(described_class::CpfInput === 123).to be(false) + expect(described_class::CpfInput === '82911017366').to be(true) + # rubocop:enable Style/CaseEquality + end + end + end end From d39533707a802722292be43ec816784a798c9bb5 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 23:46:20 -0300 Subject: [PATCH 12/19] refactor(cnpj-fmt): fix CPF input type Fix as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3626433726. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cnpj-fmt/README.md | 1 + packages/cnpj-fmt/README.pt.md | 1 + packages/cnpj-fmt/src/cnpj-fmt.rb | 1 + packages/cnpj-fmt/src/cnpj-fmt/types.rb | 35 ++++++++++++++++++++---- packages/cnpj-fmt/src/cnpj-fmt/utils.rb | 12 ++------ packages/cnpj-fmt/tests/cnpj_fmt.spec.rb | 16 +++++++++++ 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/packages/cnpj-fmt/README.md b/packages/cnpj-fmt/README.md index 400a6dc..8b65722 100644 --- a/packages/cnpj-fmt/README.md +++ b/packages/cnpj-fmt/README.md @@ -431,6 +431,7 @@ After `require 'cnpj-fmt'`: - **`CnpjFmt::CnpjFormatterOptions`**: Class holding options; supports merge via constructor, `set`, and keyword arguments. - **`CnpjFmt::CNPJ_LENGTH`**: `14` (constant). - **`CnpjFmt::VERSION`**: gem version string. +- **Type predicate**: `CnpjFmt::CnpjInput` — `CnpjInput.accept?(value)` / `CnpjInput === value` is true only for `String` or `Array`. - **Errors**: `CnpjFmt::Error`, `CnpjFmt::DomainError`, `CnpjFmt::InvalidArgumentCombinationError`, `CnpjFmt::TypeMismatchError`, `CnpjFmt::InvalidLengthError`, `CnpjFmt::OutOfRangeError`, `CnpjFmt::ValidationError`. ### Other available resources diff --git a/packages/cnpj-fmt/README.pt.md b/packages/cnpj-fmt/README.pt.md index c28e0b0..540a35c 100644 --- a/packages/cnpj-fmt/README.pt.md +++ b/packages/cnpj-fmt/README.pt.md @@ -416,6 +416,7 @@ Após `require 'cnpj-fmt'`: - **`CnpjFmt::CnpjFormatterOptions`**: Classe que armazena opções; suporta mesclagem via construtor, `set` e argumentos nomeados. - **`CnpjFmt::CNPJ_LENGTH`**: `14` (constante). - **`CnpjFmt::VERSION`**: string de versão da gem. +- **Predicado de tipo**: `CnpjFmt::CnpjInput` — `CnpjInput.accept?(value)` / `CnpjInput === value` é verdadeiro apenas para `String` ou `Array`. - **Erros**: `CnpjFmt::Error`, `CnpjFmt::DomainError`, `CnpjFmt::InvalidArgumentCombinationError`, `CnpjFmt::TypeMismatchError`, `CnpjFmt::InvalidLengthError`, `CnpjFmt::OutOfRangeError`, `CnpjFmt::ValidationError`. ### Outros recursos disponíveis diff --git a/packages/cnpj-fmt/src/cnpj-fmt.rb b/packages/cnpj-fmt/src/cnpj-fmt.rb index aabe126..79a014d 100644 --- a/packages/cnpj-fmt/src/cnpj-fmt.rb +++ b/packages/cnpj-fmt/src/cnpj-fmt.rb @@ -31,6 +31,7 @@ # - {CnpjFmt.cnpj_fmt} # - {CnpjFormatter}, {CnpjFormatterOptions} # - {CNPJ_LENGTH}, {VERSION} +# - Type predicate: {CnpjFmt::CnpjInput} (+String+ or +Array+) # - Error marker {CnpjFmt::Error}; domain ancestor {CnpjFmt::DomainError}; # misuse errors {CnpjFmt::TypeMismatchError} and # {CnpjFmt::InvalidArgumentCombinationError}; domain leaves diff --git a/packages/cnpj-fmt/src/cnpj-fmt/types.rb b/packages/cnpj-fmt/src/cnpj-fmt/types.rb index 622aa7d..335ad8d 100644 --- a/packages/cnpj-fmt/src/cnpj-fmt/types.rb +++ b/packages/cnpj-fmt/src/cnpj-fmt/types.rb @@ -10,17 +10,40 @@ module CnpjFmt hidden hidden_key hidden_start hidden_end dot_key slash_key dash_key escape encode on_fail ].freeze - # Represents valid input types for CNPJ formatting. + # Case-equality predicate for CNPJ input: +String+ or +Array+. # - # A CNPJ can be provided as: + # Matches the runtime contract of {CnpjFormatter#format} and {CnpjFmt.cnpj_fmt}. + # Use {CnpjInput.accept?} or +CnpjInput === value+ (in +case+/+when+) to test + # candidacy without raising. # - # - A string containing alphanumeric characters (with or without formatting) - # - An array of strings, where each string represents an alphanumeric - # character or group of alphanumeric characters + # @example + # CnpjFmt::CnpjInput.accept?('91415732000793') # => true + # CnpjFmt::CnpjInput.accept?(%w[9 1 4 1]) # => true + # CnpjFmt::CnpjInput.accept?(123) # => false + # CnpjFmt::CnpjInput.accept?([1, 2, 3]) # => false # # @see CnpjFormatter#format # @see CnpjFmt.cnpj_fmt - CnpjInput = Object + module CnpjInput + class << self + # @param value [Object] candidate input + # @return [Boolean] whether +value+ is a +String+ or an +Array+ of +String+ + def accept?(value) + return true if value.is_a?(String) + return false unless value.is_a?(Array) + + value.all?(String) + end + + # Case-equality entry point for +case+/+when+ and +===+ checks. + # + # @param value [Object] candidate input + # @return [Boolean] + def ===(value) + accept?(value) + end + end + end # Callback function type for handling formatting failures. # diff --git a/packages/cnpj-fmt/src/cnpj-fmt/utils.rb b/packages/cnpj-fmt/src/cnpj-fmt/utils.rb index 4342482..390e5fe 100644 --- a/packages/cnpj-fmt/src/cnpj-fmt/utils.rb +++ b/packages/cnpj-fmt/src/cnpj-fmt/utils.rb @@ -111,17 +111,11 @@ def apply_post_processing(formatted_cnpj, options) # @return [String] joined string input # @raise [TypeMismatchError] if the input is not a +String+ or +Array+ def to_string_input(cnpj_input) - return cnpj_input if cnpj_input.is_a?(String) - - if cnpj_input.is_a?(Array) - cnpj_input.each do |item| - raise TypeMismatchError.new(cnpj_input, 'string or string[]') unless item.is_a?(String) - end + raise TypeMismatchError.new(cnpj_input, 'string or string[]') unless CnpjInput.accept?(cnpj_input) - return cnpj_input.join - end + return cnpj_input if cnpj_input.is_a?(String) - raise TypeMismatchError.new(cnpj_input, 'string or string[]') + cnpj_input.join end # Invokes the +on_fail+ callback and validates its return type. diff --git a/packages/cnpj-fmt/tests/cnpj_fmt.spec.rb b/packages/cnpj-fmt/tests/cnpj_fmt.spec.rb index c7ab555..943928b 100644 --- a/packages/cnpj-fmt/tests/cnpj_fmt.spec.rb +++ b/packages/cnpj-fmt/tests/cnpj_fmt.spec.rb @@ -58,4 +58,20 @@ end end end + + context 'when inspecting public types' do + it 'exposes CnpjInput as a String or Array predicate' do + aggregate_failures do + expect(described_class::CnpjInput.accept?('91415732000793')).to be(true) + expect(described_class::CnpjInput.accept?(%w[9 1 4])).to be(true) + expect(described_class::CnpjInput.accept?(123)).to be(false) + expect(described_class::CnpjInput.accept?([1, 2, 3])).to be(false) + expect(described_class::CnpjInput.accept?(['9', 1])).to be(false) + # rubocop:disable Style/CaseEquality -- public case-equality protocol + expect(described_class::CnpjInput === 123).to be(false) + expect(described_class::CnpjInput === '91415732000793').to be(true) + # rubocop:enable Style/CaseEquality + end + end + end end From 50e47fd53e0e7a5f1a42e90dc58815f1b5372776 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 23:50:15 -0300 Subject: [PATCH 13/19] test(cpf-val): add test for `TypeMismatchError` on invalid input Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3626433728. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/tests/cpf_val.spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/cpf-val/tests/cpf_val.spec.rb b/packages/cpf-val/tests/cpf_val.spec.rb index ad396c4..a546eaf 100644 --- a/packages/cpf-val/tests/cpf_val.spec.rb +++ b/packages/cpf-val/tests/cpf_val.spec.rb @@ -21,6 +21,13 @@ end end + context 'when called with invalid arguments' do + it 'raises TypeMismatchError for a non-String, non-Array input' do + expect { described_class.cpf_val(123) } + .to raise_error(described_class::TypeMismatchError) + end + end + context 'when validating smoke-test vectors' do it 'returns true for a valid unformatted CPF' do expect(described_class.cpf_val('33528612690')).to be(true) From ef56bb9e574446a193c81a9cf0b4530149d7d9f9 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Wed, 22 Jul 2026 00:04:51 -0300 Subject: [PATCH 14/19] docs(cnpj-fmt): correct type predicate formatting Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3627092440. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cnpj-fmt/README.md | 2 +- packages/cnpj-fmt/README.pt.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cnpj-fmt/README.md b/packages/cnpj-fmt/README.md index 8b65722..5890f75 100644 --- a/packages/cnpj-fmt/README.md +++ b/packages/cnpj-fmt/README.md @@ -431,7 +431,7 @@ After `require 'cnpj-fmt'`: - **`CnpjFmt::CnpjFormatterOptions`**: Class holding options; supports merge via constructor, `set`, and keyword arguments. - **`CnpjFmt::CNPJ_LENGTH`**: `14` (constant). - **`CnpjFmt::VERSION`**: gem version string. -- **Type predicate**: `CnpjFmt::CnpjInput` — `CnpjInput.accept?(value)` / `CnpjInput === value` is true only for `String` or `Array`. +- **Type predicate**: `CnpjFmt::CnpjInput` — `CnpjFmt::CnpjInput.accept?(value)` / `CnpjFmt::CnpjInput === value` is true only for `String` or `Array`. - **Errors**: `CnpjFmt::Error`, `CnpjFmt::DomainError`, `CnpjFmt::InvalidArgumentCombinationError`, `CnpjFmt::TypeMismatchError`, `CnpjFmt::InvalidLengthError`, `CnpjFmt::OutOfRangeError`, `CnpjFmt::ValidationError`. ### Other available resources diff --git a/packages/cnpj-fmt/README.pt.md b/packages/cnpj-fmt/README.pt.md index 540a35c..9208640 100644 --- a/packages/cnpj-fmt/README.pt.md +++ b/packages/cnpj-fmt/README.pt.md @@ -416,7 +416,7 @@ Após `require 'cnpj-fmt'`: - **`CnpjFmt::CnpjFormatterOptions`**: Classe que armazena opções; suporta mesclagem via construtor, `set` e argumentos nomeados. - **`CnpjFmt::CNPJ_LENGTH`**: `14` (constante). - **`CnpjFmt::VERSION`**: string de versão da gem. -- **Predicado de tipo**: `CnpjFmt::CnpjInput` — `CnpjInput.accept?(value)` / `CnpjInput === value` é verdadeiro apenas para `String` ou `Array`. +- **Predicado de tipo**: `CnpjFmt::CnpjInput` — `CnpjFmt::CnpjInput.accept?(value)` / `CnpjFmt::CnpjInput === value` é verdadeiro apenas para `String` ou `Array`. - **Erros**: `CnpjFmt::Error`, `CnpjFmt::DomainError`, `CnpjFmt::InvalidArgumentCombinationError`, `CnpjFmt::TypeMismatchError`, `CnpjFmt::InvalidLengthError`, `CnpjFmt::OutOfRangeError`, `CnpjFmt::ValidationError`. ### Outros recursos disponíveis From 1f53dfbbdbc4b1631d057af0e10701e78a726ea0 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Wed, 22 Jul 2026 00:05:04 -0300 Subject: [PATCH 15/19] docs(cnpj-val): correct type predicate formatting Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3627092440. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cnpj-val/README.md | 2 +- packages/cnpj-val/README.pt.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cnpj-val/README.md b/packages/cnpj-val/README.md index 6ad7140..9441d54 100644 --- a/packages/cnpj-val/README.md +++ b/packages/cnpj-val/README.md @@ -336,7 +336,7 @@ After `require 'cnpj-val'`: - **`CnpjVal::CnpjValidatorOptions`**: Class holding options; supports merge via constructor, `set`, and keyword arguments. - **`CnpjVal::CNPJ_LENGTH`**: `14` (constant). - **`CnpjVal::VERSION`**: gem version string. -- **Type predicate**: `CnpjVal::CnpjInput` — `CnpjInput.accept?(value)` / `CnpjInput === value` is true only for `String` or `Array`. +- **Type predicate**: `CnpjVal::CnpjInput` — `CnpjVal::CnpjInput.accept?(value)` / `CnpjVal::CnpjInput === value` is true only for `String` or `Array`. - **Type markers**: `CnpjVal::CnpjType`, `CnpjVal::CnpjValidatorOptionsInput`. - **Errors**: `CnpjVal::Error`, `CnpjVal::DomainError`, `CnpjVal::InvalidArgumentCombinationError`, `CnpjVal::TypeMismatchError`, `CnpjVal::ValidationError`. diff --git a/packages/cnpj-val/README.pt.md b/packages/cnpj-val/README.pt.md index 9c13be9..bd320a4 100644 --- a/packages/cnpj-val/README.pt.md +++ b/packages/cnpj-val/README.pt.md @@ -323,7 +323,7 @@ Após `require 'cnpj-val'`: - **`CnpjVal::CnpjValidatorOptions`**: Classe que armazena opções; suporta mesclagem via construtor, `set` e argumentos nomeados. - **`CnpjVal::CNPJ_LENGTH`**: `14` (constante). - **`CnpjVal::VERSION`**: string de versão da gem. -- **Predicado de tipo**: `CnpjVal::CnpjInput` — `CnpjInput.accept?(value)` / `CnpjInput === value` é verdadeiro apenas para `String` ou `Array`. +- **Predicado de tipo**: `CnpjVal::CnpjInput` — `CnpjVal::CnpjInput.accept?(value)` / `CnpjVal::CnpjInput === value` é verdadeiro apenas para `String` ou `Array`. - **Marcadores de tipo**: `CnpjVal::CnpjType`, `CnpjVal::CnpjValidatorOptionsInput`. - **Erros**: `CnpjVal::Error`, `CnpjVal::DomainError`, `CnpjVal::InvalidArgumentCombinationError`, `CnpjVal::TypeMismatchError`, `CnpjVal::ValidationError`. From deeb4f00d28411557277c3cee72e3175252b9726 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Wed, 22 Jul 2026 00:05:19 -0300 Subject: [PATCH 16/19] docs(cpf-fmt): correct type predicate formatting Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3627092440. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-fmt/README.md | 2 +- packages/cpf-fmt/README.pt.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cpf-fmt/README.md b/packages/cpf-fmt/README.md index 946e498..cecbf74 100644 --- a/packages/cpf-fmt/README.md +++ b/packages/cpf-fmt/README.md @@ -422,7 +422,7 @@ After `require 'cpf-fmt'`: - **`CpfFmt::CpfFormatterOptions`**: Class holding options; supports merge via constructor, `set`, and keyword arguments. - **`CpfFmt::CPF_LENGTH`**: `11` (constant). - **`CpfFmt::VERSION`**: gem version string. -- **Type predicate**: `CpfFmt::CpfInput` — `CpfInput.accept?(value)` / `CpfInput === value` is true only for `String` or `Array`. +- **Type predicate**: `CpfFmt::CpfInput` — `CpfFmt::CpfInput.accept?(value)` / `CpfFmt::CpfInput === value` is true only for `String` or `Array`. - **Errors**: `CpfFmt::Error`, `CpfFmt::DomainError`, `CpfFmt::InvalidArgumentCombinationError`, `CpfFmt::TypeMismatchError`, `CpfFmt::InvalidLengthError`, `CpfFmt::OutOfRangeError`, `CpfFmt::ValidationError`. ### Other available resources diff --git a/packages/cpf-fmt/README.pt.md b/packages/cpf-fmt/README.pt.md index 93eb273..1d54708 100644 --- a/packages/cpf-fmt/README.pt.md +++ b/packages/cpf-fmt/README.pt.md @@ -408,7 +408,7 @@ Após `require 'cpf-fmt'`: - **`CpfFmt::CpfFormatterOptions`**: Classe que armazena opções; suporta mesclagem via construtor, `set` e argumentos nomeados. - **`CpfFmt::CPF_LENGTH`**: `11` (constante). - **`CpfFmt::VERSION`**: string de versão da gem. -- **Predicado de tipo**: `CpfFmt::CpfInput` — `CpfInput.accept?(value)` / `CpfInput === value` é verdadeiro apenas para `String` ou `Array`. +- **Predicado de tipo**: `CpfFmt::CpfInput` — `CpfFmt::CpfInput.accept?(value)` / `CpfFmt::CpfInput === value` é verdadeiro apenas para `String` ou `Array`. - **Erros**: `CpfFmt::Error`, `CpfFmt::DomainError`, `CpfFmt::InvalidArgumentCombinationError`, `CpfFmt::TypeMismatchError`, `CpfFmt::InvalidLengthError`, `CpfFmt::OutOfRangeError`, `CpfFmt::ValidationError`. ### Outros recursos disponíveis From 20cd63b2a4e06573a5343b12b5ff456f2b4005c3 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Wed, 22 Jul 2026 00:05:28 -0300 Subject: [PATCH 17/19] docs(cpf-val): correct type predicate formatting Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3627092440. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/README.md | 2 +- packages/cpf-val/README.pt.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cpf-val/README.md b/packages/cpf-val/README.md index 870a62e..80db1b9 100644 --- a/packages/cpf-val/README.md +++ b/packages/cpf-val/README.md @@ -208,7 +208,7 @@ After `require 'cpf-val'`: - **`CpfVal::CpfValidator`**: Class to validate CPF (no options); accepts `String` or `Array` in `is_valid`. - **`CpfVal::CPF_LENGTH`**: `11` (constant). - **`CpfVal::VERSION`**: gem version string. -- **Type predicate**: `CpfVal::CpfInput` — `CpfInput.accept?(value)` / `CpfInput === value` is true only for `String` or `Array`. +- **Type predicate**: `CpfVal::CpfInput` — `CpfVal::CpfInput.accept?(value)` / `CpfVal::CpfInput === value` is true only for `String` or `Array`. - **Errors**: `CpfVal::Error`, `CpfVal::TypeMismatchError`. ## Contribution & Support diff --git a/packages/cpf-val/README.pt.md b/packages/cpf-val/README.pt.md index ccef6d6..fe0c270 100644 --- a/packages/cpf-val/README.pt.md +++ b/packages/cpf-val/README.pt.md @@ -195,7 +195,7 @@ Após `require 'cpf-val'`: - **`CpfVal::CpfValidator`**: Classe para validar CPF (sem opções); aceita `String` ou `Array` em `is_valid`. - **`CpfVal::CPF_LENGTH`**: `11` (constante). - **`CpfVal::VERSION`**: string de versão da gem. -- **Predicado de tipo**: `CpfVal::CpfInput` — `CpfInput.accept?(value)` / `CpfInput === value` é verdadeiro apenas para `String` ou `Array`. +- **Predicado de tipo**: `CpfVal::CpfInput` — `CpfVal::CpfInput.accept?(value)` / `CpfVal::CpfInput === value` é verdadeiro apenas para `String` ou `Array`. - **Erros**: `CpfVal::Error`, `CpfVal::TypeMismatchError`. ## Contribuição e suporte From 39534fd273971152dda36cf5a83440ea25b82c7d Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Wed, 22 Jul 2026 00:07:50 -0300 Subject: [PATCH 18/19] docs(cpf-val): update inheritance details for `TypeMismatchError` Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3627092447. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/README.md | 4 ++-- packages/cpf-val/README.pt.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cpf-val/README.md b/packages/cpf-val/README.md index 80db1b9..a5ee855 100644 --- a/packages/cpf-val/README.md +++ b/packages/cpf-val/README.md @@ -130,7 +130,7 @@ Every custom error includes the `CpfVal::Error` marker module. This package defi | Class | Inherits from | Category | Trigger condition | |---|---|---|---| -| `CpfVal::TypeMismatchError` | `TypeError` (+ `include CpfVal::Error`) | API misuse | CPF input is not a `String` or `Array` of strings | +| `CpfVal::TypeMismatchError` | `CpfVal::TypeMismatchError < TypeError < StandardError` (+ `include CpfVal::Error`) | API misuse | CPF input is not a `String` or `Array` of strings | #### `CpfVal::Error` (marker module) @@ -147,7 +147,7 @@ rescue CpfVal::Error #### `CpfVal::TypeMismatchError` -- **Inheritance:** `CpfVal::TypeMismatchError < TypeError` (includes `CpfVal::Error`) +- **Inheritance:** `CpfVal::TypeMismatchError < TypeError < StandardError` (includes `CpfVal::Error`) - **Category:** API misuse — the caller passed a value of the wrong type. - **When it is raised:** Raised when the CPF input is not a `String` or an `Array` of strings (including when an array contains a non-string element). - **Example:** diff --git a/packages/cpf-val/README.pt.md b/packages/cpf-val/README.pt.md index fe0c270..4f7af17 100644 --- a/packages/cpf-val/README.pt.md +++ b/packages/cpf-val/README.pt.md @@ -117,7 +117,7 @@ Todo erro customizado inclui o módulo marcador `CpfVal::Error`. Este pacote **n | Classe | Herda de | Categoria | Condição de disparo | |---|---|---|---| -| `CpfVal::TypeMismatchError` | `TypeError` (+ `include CpfVal::Error`) | Uso incorreto da API | Entrada CPF não é `String` nem `Array` de strings | +| `CpfVal::TypeMismatchError` | `CpfVal::TypeMismatchError < TypeError < StandardError` (+ `include CpfVal::Error`) | Uso incorreto da API | Entrada CPF não é `String` nem `Array` de strings | #### `CpfVal::Error` (módulo marcador) @@ -134,7 +134,7 @@ rescue CpfVal::Error #### `CpfVal::TypeMismatchError` -- **Herança:** `CpfVal::TypeMismatchError < TypeError` (inclui `CpfVal::Error`) +- **Herança:** `CpfVal::TypeMismatchError < TypeError < StandardError` (inclui `CpfVal::Error`) - **Categoria:** Uso incorreto da API — o chamador passou um valor do tipo errado. - **Quando é levantado:** Levantado quando a entrada CPF não é `String` nem `Array` de strings (incluindo quando um array contém um elemento que não é string). - **Exemplo:** From 9d98a9c30aeb9ccbdc25da867a3cb98cc835dacd Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Wed, 22 Jul 2026 00:09:41 -0300 Subject: [PATCH 19/19] docs(cpf-val): enhance rescue examples for error handling clarity Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/24#discussion_r3627092455. Co-authored-by: CodeRabbit AI <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/README.md | 28 ++++++++++++++++++++++++++++ packages/cpf-val/README.pt.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/packages/cpf-val/README.md b/packages/cpf-val/README.md index a5ee855..a69b065 100644 --- a/packages/cpf-val/README.md +++ b/packages/cpf-val/README.md @@ -175,23 +175,51 @@ rescue TypeError #### Rescue granularity +Each level is shown as its own standalone example (do not merge them into one `rescue` ladder — a broad `TypeError` handler would make narrower clauses unreachable). + ```ruby +require 'cpf-val' + # 1) Single native class — catches misuse errors of that kind, # including non-library ones already handled elsewhere in the consumer's code. +begin + CpfVal.cpf_val(123) rescue TypeError # CpfVal::TypeMismatchError and any other TypeError (library or not) +end +``` + +```ruby +require 'cpf-val' # 2) CpfVal::DomainError — not applicable: this package defines no DomainError # (and no domain leaves). Invalid CPF data returns false instead of raising. +# begin +# CpfVal.cpf_val(123) # rescue CpfVal::DomainError # NameError — constant is not defined +# end +``` + +```ruby +require 'cpf-val' # 3) CpfVal::Error — catches everything the library raises, regardless of native ancestry. +begin + CpfVal.cpf_val(123) rescue CpfVal::Error # every custom error that includes CpfVal::Error +end +``` + +```ruby +require 'cpf-val' # 4) Specific leaf class — catches only that exact failure mode. +begin + CpfVal.cpf_val(123) rescue CpfVal::TypeMismatchError # only CpfVal::TypeMismatchError +end ``` Notable attributes on raised errors: diff --git a/packages/cpf-val/README.pt.md b/packages/cpf-val/README.pt.md index 4f7af17..1e62d14 100644 --- a/packages/cpf-val/README.pt.md +++ b/packages/cpf-val/README.pt.md @@ -162,23 +162,51 @@ rescue TypeError #### Granularidade de rescue +Cada nível é um exemplo independente (não mescle em uma única escada de `rescue` — um handler amplo de `TypeError` tornaria as cláusulas mais específicas inalcançáveis). + ```ruby +require 'cpf-val' + # 1) Classe nativa única — captura erros de uso incorreto desse tipo, # inclusive TypeError de fora da biblioteca já tratados no código do consumidor. +begin + CpfVal.cpf_val(123) rescue TypeError # CpfVal::TypeMismatchError e qualquer outro TypeError (da biblioteca ou não) +end +``` + +```ruby +require 'cpf-val' # 2) CpfVal::DomainError — não se aplica: este pacote não define DomainError # (nem folhas de domínio). CPF inválido retorna false em vez de levantar. +# begin +# CpfVal.cpf_val(123) # rescue CpfVal::DomainError # NameError — a constante não está definida +# end +``` + +```ruby +require 'cpf-val' # 3) CpfVal::Error — captura tudo o que a biblioteca levanta, independente da ancestralidade nativa. +begin + CpfVal.cpf_val(123) rescue CpfVal::Error # todo erro customizado que inclui CpfVal::Error +end +``` + +```ruby +require 'cpf-val' # 4) Classe folha específica — captura apenas aquele modo de falha. +begin + CpfVal.cpf_val(123) rescue CpfVal::TypeMismatchError # apenas CpfVal::TypeMismatchError +end ``` Atributos notáveis nos erros levantados: