From e7fee4706348db0f9f4b686fe24b7d17f8c0e116 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 16:59:50 -0300 Subject: [PATCH 01/15] chore(cpf-gen): update `cpf-dv` dependency version Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-gen/Gemfile | 2 -- packages/cpf-gen/cpf-gen.gemspec | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/cpf-gen/Gemfile b/packages/cpf-gen/Gemfile index 533428d..c6122eb 100644 --- a/packages/cpf-gen/Gemfile +++ b/packages/cpf-gen/Gemfile @@ -4,8 +4,6 @@ source 'https://rubygems.org' gemspec -gem 'cpf-dv', path: '../cpf-dv' - group :test do gem 'rake', '~> 13.2' gem 'rspec', '~> 3.13' diff --git a/packages/cpf-gen/cpf-gen.gemspec b/packages/cpf-gen/cpf-gen.gemspec index 85c6f08..cb7b90d 100644 --- a/packages/cpf-gen/cpf-gen.gemspec +++ b/packages/cpf-gen/cpf-gen.gemspec @@ -13,6 +13,6 @@ Gem::Specification.new do |spec| spec.metadata['rubygems_mfa_required'] = 'true' spec.files = Dir['src/**/*'] + ['LICENSE', 'README.md'].select { |f| File.file?(f) } spec.require_paths = ['src'] - spec.add_dependency 'cpf-dv', '>= 0' + spec.add_dependency 'cpf-dv', '>= 1.0.0', '< 1.1.0' spec.add_dependency 'lacus-utils', '>= 1.1.0', '< 2.0.0' end From 8a10208ed7296a1c1f17bc1d37fc5c6e4beed445 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:00:04 -0300 Subject: [PATCH 02/15] chore(cpf-val): update `cpf-dv` dependency version Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-val/Gemfile | 2 -- packages/cpf-val/cpf-val.gemspec | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/cpf-val/Gemfile b/packages/cpf-val/Gemfile index 533428d..c6122eb 100644 --- a/packages/cpf-val/Gemfile +++ b/packages/cpf-val/Gemfile @@ -4,8 +4,6 @@ source 'https://rubygems.org' gemspec -gem 'cpf-dv', path: '../cpf-dv' - group :test do gem 'rake', '~> 13.2' gem 'rspec', '~> 3.13' diff --git a/packages/cpf-val/cpf-val.gemspec b/packages/cpf-val/cpf-val.gemspec index 31f94a7..685b92e 100644 --- a/packages/cpf-val/cpf-val.gemspec +++ b/packages/cpf-val/cpf-val.gemspec @@ -13,6 +13,6 @@ Gem::Specification.new do |spec| spec.metadata['rubygems_mfa_required'] = 'true' spec.files = Dir['src/**/*'] + ['LICENSE', 'README.md'].select { |f| File.file?(f) } spec.require_paths = ['src'] - spec.add_dependency 'cpf-dv', '>= 0' + spec.add_dependency 'cpf-dv', '>= 1.0.0', '< 1.1.0' spec.add_dependency 'lacus-utils', '>= 1.1.0', '< 2.0.0' end From 7cf6960c0b46f0d86ab3f0c9b5866ec6cb184683 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:00:15 -0300 Subject: [PATCH 03/15] chore(cpf-utils): update `cpf-dv` dependency version Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-utilities/Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cpf-utilities/Gemfile b/packages/cpf-utilities/Gemfile index f2be271..2295579 100644 --- a/packages/cpf-utilities/Gemfile +++ b/packages/cpf-utilities/Gemfile @@ -4,7 +4,6 @@ source 'https://rubygems.org' gemspec -gem 'cpf-dv', path: '../cpf-dv' gem 'cpf-fmt', path: '../cpf-fmt' gem 'cpf-gen', path: '../cpf-gen' gem 'cpf-val', path: '../cpf-val' From 66386bae0edfbbbd71d7579f3ab605f07eba0132 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:00:34 -0300 Subject: [PATCH 04/15] chore(br-utils): update `cpf-dv` dependency version Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/br-utilities/Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/br-utilities/Gemfile b/packages/br-utilities/Gemfile index af4d876..12f5a87 100644 --- a/packages/br-utilities/Gemfile +++ b/packages/br-utilities/Gemfile @@ -8,7 +8,6 @@ gem 'cnpj-fmt', path: '../cnpj-fmt' gem 'cnpj-gen', path: '../cnpj-gen' gem 'cnpj-utilities', path: '../cnpj-utilities' gem 'cnpj-val', path: '../cnpj-val' -gem 'cpf-dv', path: '../cpf-dv' gem 'cpf-fmt', path: '../cpf-fmt' gem 'cpf-gen', path: '../cpf-gen' gem 'cpf-utilities', path: '../cpf-utilities' From 4d6ddca7e2b4ead694a7dc8812585f766f3b8ebd Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:15:27 -0300 Subject: [PATCH 05/15] chore(cpf-gen): update package metadata Added email contact, refined summary, and added detailed description for CPF generation utility. Included source code URI in metadata. Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-gen/cpf-gen.gemspec | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cpf-gen/cpf-gen.gemspec b/packages/cpf-gen/cpf-gen.gemspec index cb7b90d..54130b9 100644 --- a/packages/cpf-gen/cpf-gen.gemspec +++ b/packages/cpf-gen/cpf-gen.gemspec @@ -6,10 +6,13 @@ Gem::Specification.new do |spec| spec.name = 'cpf-gen' spec.version = CpfGen::VERSION spec.authors = ['Julio L. Muller'] - spec.summary = 'Generate random valid CPF numbers (Brazilian personal ID)' + spec.email = ['juliolmuller@outlook.com'] + spec.summary = "Generate CPF (Brazilian Individual's Taxpayer ID)" + spec.description = "Utility to generate 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 8bb75e78cf469dfdf0fae1eb418b1278e25903a6 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:16:02 -0300 Subject: [PATCH 06/15] feat(cpf-gen): implement CPF generation functionality with error handling Added core functionality for generating valid CPF identifiers, including a new `CpfGenerator` class and associated options management through `CpfGeneratorOptions`. Introduced custom error handling for API misuse and domain validation. Updated module structure to enhance usability and maintainability. - Implemented `cpf_gen` method for simplified CPF generation. - Added error classes for type mismatches and validation failures. - Created utility functions for option handling and prefix validation. This update enhances the CPF generation utility, providing a robust API for users. Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-gen/src/cpf-gen.rb | 44 +++- packages/cpf-gen/src/cpf-gen/cpf_gen.rb | 32 +++ packages/cpf-gen/src/cpf-gen/cpf_generator.rb | 138 +++++++++++ .../src/cpf-gen/cpf_generator_options.rb | 222 ++++++++++++++++++ packages/cpf-gen/src/cpf-gen/errors.rb | 98 ++++++++ packages/cpf-gen/src/cpf-gen/types.rb | 24 ++ packages/cpf-gen/src/cpf-gen/utils.rb | 76 ++++++ 7 files changed, 630 insertions(+), 4 deletions(-) create mode 100644 packages/cpf-gen/src/cpf-gen/cpf_gen.rb create mode 100644 packages/cpf-gen/src/cpf-gen/cpf_generator.rb create mode 100644 packages/cpf-gen/src/cpf-gen/cpf_generator_options.rb create mode 100644 packages/cpf-gen/src/cpf-gen/errors.rb create mode 100644 packages/cpf-gen/src/cpf-gen/types.rb create mode 100644 packages/cpf-gen/src/cpf-gen/utils.rb diff --git a/packages/cpf-gen/src/cpf-gen.rb b/packages/cpf-gen/src/cpf-gen.rb index 3fee331..6b1c63b 100644 --- a/packages/cpf-gen/src/cpf-gen.rb +++ b/packages/cpf-gen/src/cpf-gen.rb @@ -1,10 +1,46 @@ # frozen_string_literal: true -require 'cpf-dv' require_relative 'cpf-gen/version' +require_relative 'cpf-gen/errors' +require_relative 'cpf-gen/types' +require_relative 'cpf-gen/cpf_generator_options' +require_relative 'cpf-gen/utils' +require_relative 'cpf-gen/cpf_generator' +require_relative 'cpf-gen/cpf_gen' +# Generates valid CPF (Cadastro de Pessoa Física) identifiers. +# +# Errors fall into two categories: +# +# - *API misuse* — the caller supplied a wrong type or an incompatible argument +# combination. Raised as {CpfGen::TypeMismatchError} or +# {CpfGen::InvalidArgumentCombinationError}. +# - *Domain errors* — the call shape was valid, but a value violates a business +# rule. Invalid +prefix+ values raise {CpfGen::ValidationError} under +# {CpfGen::DomainError}. +# +# Every custom error includes the {CpfGen::Error} marker module so consumers can +# +rescue CpfGen::Error+ for a library-wide catch. +# +# Public API: +# +# - {CpfGen.cpf_gen} +# - {CpfGen::CpfGenerator}, {CpfGen::CpfGeneratorOptions} +# - {CpfGen::CPF_LENGTH}, {CpfGen::CPF_PREFIX_MAX_LENGTH} +# - Error marker {CpfGen::Error}; domain ancestor {CpfGen::DomainError}; +# misuse errors {CpfGen::TypeMismatchError} and +# {CpfGen::InvalidArgumentCombinationError}; domain leaf +# {CpfGen::ValidationError} +# +# @example +# require 'cpf-gen' +# +# CpfGen.cpf_gen # => e.g. "47844241055" module CpfGen - def self.hello - 'cpf-gen' - end + # The standard length of a CPF (Cadastro de Pessoa Física) identifier (11 + # digits). + CPF_LENGTH = CpfGeneratorOptions::CPF_LENGTH + + # Maximum length of the +prefix+ of a CPF. + CPF_PREFIX_MAX_LENGTH = CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH end diff --git a/packages/cpf-gen/src/cpf-gen/cpf_gen.rb b/packages/cpf-gen/src/cpf-gen/cpf_gen.rb new file mode 100644 index 0000000..c326607 --- /dev/null +++ b/packages/cpf-gen/src/cpf-gen/cpf_gen.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative 'cpf_generator' + +module CpfGen + module_function + + # Helper function to simplify the usage of the {CpfGenerator} class. + # + # If no options are provided, it generates an 11-digit unformatted numeric CPF + # (e.g. +"47844241055"+) using default settings. If options are provided, they + # control +prefix+ and whether the result is formatted. + # + # Generates a valid 11-digit CPF (+prefix+, random numeric body, and computed + # check digits). With default options the result is unformatted numeric; pass + # +format: true+ for +000.000.000-00+ style output. + # + # @param options [CpfGeneratorOptions, Hash, nil] generator options + # @param keywords [Hash] option keyword overrides (mutually exclusive with +options+; + # see {CpfGeneratorOptions}) + # @return [String] generated CPF + # @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument are both given + # @raise [TypeMismatchError] if any option has an invalid type + # @raise [ValidationError] if +prefix+ is invalid + # @see CpfGenerator for detailed option descriptions + # + # @example + # CpfGen.cpf_gen # => "47844241055" + def cpf_gen(options = nil, **keywords) + CpfGenerator.new(options, **keywords).generate + end +end diff --git a/packages/cpf-gen/src/cpf-gen/cpf_generator.rb b/packages/cpf-gen/src/cpf-gen/cpf_generator.rb new file mode 100644 index 0000000..4de89b1 --- /dev/null +++ b/packages/cpf-gen/src/cpf-gen/cpf_generator.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'cpf-dv' +require 'lacus-utils' + +require_relative 'cpf_generator_options' + +module CpfGen + # Generator for CPF (Cadastro de Pessoa Física) identifiers. Builds valid + # 11-digit CPF values by combining an optional +prefix+ with a randomly + # generated sequence and computed check digits. Options control +prefix+ and + # whether the result is formatted (+000.000.000-00+). + class CpfGenerator + # Returns the default options used by this generator when per-call options + # are not provided. + # + # The returned object is the same instance used internally; mutating it (e.g. + # via setters on {CpfGeneratorOptions}) affects future {#generate} calls that + # do not pass +options+. + # + # @return [CpfGeneratorOptions] the instance default options + attr_reader :options + + # Creates a new {CpfGenerator} with optional default options. + # + # Default options apply to every call to {#generate} unless overridden by the + # per-call +options+ argument or keyword overrides. Options control +prefix+ + # and whether the generated CPF is formatted. + # + # +options+ and the keyword arguments are never merged with each other: when + # +options+ is given (a {CpfGeneratorOptions} instance or a {Hash}), it alone + # determines the default options; otherwise, the default options are built + # exclusively from the keyword arguments, with {CpfGeneratorOptions} filling + # in its own defaults for every keyword left as +nil+. Passing +options+ + # together with any non-+nil+ keyword argument raises + # {InvalidArgumentCombinationError} instead of silently ignoring the keywords. + # + # When +options+ is a {CpfGeneratorOptions} instance, that instance is used + # directly (no copy is created). Mutating it later (e.g. via the {#options} + # reader or the original reference) affects future {#generate} calls that do + # not pass per-call options. When a plain {Hash} is passed instead, a new + # {CpfGeneratorOptions} instance is created from it. + # + # @param options [CpfGeneratorOptions, Hash, nil] default options + # @param keywords [Hash] option keyword overrides (mutually exclusive with +options+; + # see {CpfGeneratorOptions}) + # @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument are both given + # @raise [TypeMismatchError] if any option has an invalid type + # @raise [ValidationError] if +prefix+ is invalid + def initialize(options = nil, **keywords) + @options = resolve_default_options(options, keywords) + end + + # Generates a valid CPF value. + # + # Builds an 11-digit CPF from the configured +prefix+ (if any), a random + # numeric sequence, and two computed check digits. If formatting is enabled, + # the result is returned as +000.000.000-00+. + # + # +options+ and the keyword arguments are never merged with each other: when + # +options+ is given (a {CpfGeneratorOptions} instance or a {Hash}), it alone + # overrides the instance default options for this call; otherwise, any + # non-+nil+ keyword argument overrides the instance default options for this + # call. When neither +options+ nor any keyword argument is given, the + # instance default options are used as-is. In every case, the instance + # default options themselves are left unchanged. Passing +options+ together + # with any non-+nil+ keyword argument raises {InvalidArgumentCombinationError} + # instead of silently ignoring the keywords. + # + # @param options [CpfGeneratorOptions, Hash, nil] per-call option overrides + # @param keywords [Hash] per-call option keyword overrides (mutually exclusive + # with +options+; see {CpfGeneratorOptions}) + # @return [String] generated CPF + # @raise [InvalidArgumentCombinationError] if +options+ and a keyword argument are both given + # @raise [TypeMismatchError] if any option has an invalid type + # @raise [ValidationError] if +prefix+ is invalid + def generate(options = nil, **keywords) + actual_options = resolve_call_options(options, keywords) + generated_cpf = build_base_cpf(actual_options) + + with_check_digits(generated_cpf, options, keywords) do |cpf_with_digits| + actual_options.format ? Utils.format_cpf(cpf_with_digits) : cpf_with_digits + end + end + + private + + def resolve_default_options(options, keywords) + keyword_overrides = compact_keyword_overrides(keywords) + raise_ambiguous_options! if options && !keyword_overrides.empty? + return options if options.is_a?(CpfGeneratorOptions) + return CpfGeneratorOptions.new(options) if options + + CpfGeneratorOptions.new(**keywords) + end + + def resolve_call_options(options, keywords) + keyword_overrides = compact_keyword_overrides(keywords) + raise_ambiguous_options! if options && !keyword_overrides.empty? + return @options.copy.set(options) if options + return @options if keyword_overrides.empty? + + @options.copy.set(keyword_overrides) + end + + def compact_keyword_overrides(keywords) + CpfGeneratorOptions::OPTION_KEYS.each_with_object({}) do |key, overrides| + value = keywords[key] + overrides[key] = value unless value.nil? + end + end + + def raise_ambiguous_options! + option_keywords = CpfGeneratorOptions::OPTION_KEYS.map { |key| "#{key}:" }.join(', ') + + raise InvalidArgumentCombinationError, + "Pass either an options instance/Hash to `options`, or keyword arguments (#{option_keywords}), " \ + 'not both.' + end + + def build_base_cpf(actual_options) + digits_to_generate = CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH - actual_options.prefix.length + + actual_options.prefix + LacusUtils.generate_random_sequence( + digits_to_generate, + :numeric + ) + end + + def with_check_digits(generated_cpf, options, keywords) + cpf_with_digits = CpfDV::CpfCheckDigits.new(generated_cpf).cpf + + yield cpf_with_digits + rescue CpfDV::DomainError + generate(options, **keywords) + end + end +end diff --git a/packages/cpf-gen/src/cpf-gen/cpf_generator_options.rb b/packages/cpf-gen/src/cpf-gen/cpf_generator_options.rb new file mode 100644 index 0000000..da976c1 --- /dev/null +++ b/packages/cpf-gen/src/cpf-gen/cpf_generator_options.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +require_relative 'types' + +module CpfGen + # Layered option-resolution helpers for {CpfGeneratorOptions}. + module GeneratorOptionsResolution + private + + def fold_layers(layers) + resolved = {} + + layers.each do |layer| + source = layer_source(layer) + next if source.nil? + + CpfGeneratorOptions::OPTION_KEYS.each do |key| + value = Utils.fetch_option(source, key) + resolved[key] = value unless value.nil? + end + end + + resolved + end + + def layer_source(layer) + return layer.all if layer.is_a?(CpfGeneratorOptions) + return layer if layer.is_a?(Hash) + + nil + end + + def apply_keyword_overrides(resolved, keywords) + CpfGeneratorOptions::OPTION_KEYS.each do |key| + value = keywords[key] + resolved[key] = value unless value.nil? + end + end + + def assign_resolved_or_default(resolved) + CpfGeneratorOptions::OPTION_KEYS.each do |key| + value = resolved.key?(key) ? resolved[key] : CpfGeneratorOptions::DEFAULTS[key] + public_send("#{key}=", value) + end + end + end + + # Property accessors for {CpfGeneratorOptions}. Kept as a sibling module in this + # file (not a separate public API) so the options class stays under RuboCop's + # +Metrics/ClassLength+ budget. + module CpfGeneratorOptionProperties + # Returns a shallow copy of all current options. + # + # Exposes resolved +format+ and +prefix+ values. This is useful for creating + # snapshots of the current configuration. + # + # @return [Hash{Symbol => Object}] shallow copy of option values + def all + @options.dup + end + + # Returns whether the generated CPF string will have the standard formatting + # (+000.000.000-00+). + # + # @return [Boolean] + def format + @options[:format] + end + + # Sets whether the generated CPF string will have the standard formatting + # (+000.000.000-00+). The value is converted to a boolean, so truthy/falsy + # values are handled appropriately. + # + # +nil+ is not accepted: pass {CpfGeneratorOptions::DEFAULT_FORMAT} explicitly + # to reset this option to its default value. + # + # @param value [Boolean] enable formatting when truthy + # @raise [TypeMismatchError] if the value is +nil+ + def format=(value) + raise TypeMismatchError.new(value, 'boolean', option_name: 'format') if value.nil? + + @options[:format] = Utils.normalize_boolean(value) + end + + # Returns the string used as the initial string of the generated CPF. + # + # Note: If the evaluated +prefix+ (after stripping non-digit characters) is + # longer than 9 digits, the extra digits are ignored, because a CPF has 9 + # base digits followed by 2 calculated check digits. + # + # @return [String] + def prefix + @options[:prefix] + end + + # Sets the string used as the initial string of the generated CPF. Only + # digits are kept and the rest is stripped. If provided, only the missing + # digits are generated randomly. For example, if the +prefix+ +"123456"+ (6 + # digits) is given, only the next 3 digits are randomly generated and + # concatenated to the +prefix+. + # + # Note: If the evaluated +prefix+ (after stripping non-digit characters) is + # longer than 9 digits, the extra digits are ignored, because a CPF has 9 + # base digits followed by 2 calculated check digits. + # + # +nil+ is not accepted: pass {CpfGeneratorOptions::DEFAULT_PREFIX} explicitly + # to reset this option to its default value. + # + # @param value [String] partial start string + # @raise [TypeMismatchError] if the value is not a +String+ + # @raise [ValidationError] if +prefix+ is invalid + def prefix=(value) + actual_prefix = Utils.sanitize_prefix(value) + + Utils.validate_prefix!(actual_prefix) + + @options[:prefix] = actual_prefix + end + end + + # Stores configuration for the CPF generator. + # + # Provides a centralized way to configure how CPF digits are generated, + # including partial start string (+prefix+) and formatting (+format+). + class CpfGeneratorOptions + include GeneratorOptionsResolution + include CpfGeneratorOptionProperties + + # The standard length of a CPF (Cadastro de Pessoa Física) identifier (11 + # digits). + CPF_LENGTH = 11 + + # Maximum length of the +prefix+ of a CPF. + CPF_PREFIX_MAX_LENGTH = CPF_LENGTH - 2 + + # Default value for the +format+ option. When +true+, the generated CPF + # string will have the standard formatting (+000.000.000-00+). + DEFAULT_FORMAT = false + + # Default string used as the initial string of the generated CPF. + DEFAULT_PREFIX = '' + + CPF_BASE_ID_LENGTH = 9 + CPF_BASE_ID_LAST_INDEX = CPF_BASE_ID_LENGTH - 1 + ZEROED_CPF_BASE_ID = '0' * CPF_BASE_ID_LENGTH + + PREFIX_SANITIZE_PATTERN = /\D/ + + # Option keys managed by this class, in assignment order. + OPTION_KEYS = %i[format prefix].freeze + + # Default value for each key in {OPTION_KEYS}, used to fill any option that + # is still unresolved once {#initialize} finishes merging its arguments. + DEFAULTS = { + format: DEFAULT_FORMAT, + prefix: DEFAULT_PREFIX + }.freeze + + # Creates a new {CpfGeneratorOptions} instance. + # + # Options are resolved in three steps. Each step only overrides a key when it + # is given a non-+nil+ value; a +nil+ is always ignored in favor of whatever + # was resolved by a previous step. + # + # 1. Every positional +options+ layer (each either a {Hash} or another + # {CpfGeneratorOptions} instance) is folded left to right, so later + # layers take precedence over earlier ones. + # 2. The +format+ and +prefix+ keyword arguments are then applied on top of + # the folded layers. Keywords always have the highest precedence, + # overriding every positional layer. + # 3. Any option that is still unresolved after steps 1 and 2 is assigned its + # +DEFAULT_*+ value (see {DEFAULTS}). + # + # Because every option is fully resolved to a concrete, non-+nil+ value + # before assignment, the individual property setters (e.g. {#format=}) never + # receive +nil+ from this method — they always raise if given +nil+ directly. + # + # @param options [Array] option layers merged in + # order (later layers win); a missing or +nil+ value for a key inside a + # layer is ignored and the previously resolved value is kept + # @param keywords [Hash] highest-precedence option overrides (see {OPTION_KEYS}) + # @raise [TypeMismatchError] if any option has an invalid type + # @raise [ValidationError] if +prefix+ is invalid + def initialize(*options, **keywords) + @options = {} + + resolved = fold_layers(options) + apply_keyword_overrides(resolved, keywords) + assign_resolved_or_default(resolved) + end + + # Sets multiple options at once, following the same layered-override + # semantics as {#initialize} (positional layers folded left to right, then + # keyword arguments applied with the highest precedence; +nil+ is always + # ignored). Unlike {#initialize}, any option that is still unresolved after + # merging keeps its **current** value on this instance instead of falling + # back to its default — this method performs a partial update, not a + # re-initialization. + # + # @param options [Array] option layers merged in + # order (later layers win) + # @param keywords [Hash] highest-precedence option overrides (see {OPTION_KEYS}) + # @return [CpfGeneratorOptions] +self+ + # @raise [TypeMismatchError] if any option has an invalid type + # @raise [ValidationError] if +prefix+ is invalid + def set(*options, **keywords) + resolved = fold_layers(options) + apply_keyword_overrides(resolved, keywords) + resolved.each { |key, value| public_send("#{key}=", value) } + self + end + + # Returns a shallow copy of this options instance. + # + # @return [CpfGeneratorOptions] duplicated options for per-call merging + def copy + duplicate = self.class.allocate + duplicate.instance_variable_set(:@options, @options.dup) + duplicate + end + end +end diff --git a/packages/cpf-gen/src/cpf-gen/errors.rb b/packages/cpf-gen/src/cpf-gen/errors.rb new file mode 100644 index 0000000..9ca4fb9 --- /dev/null +++ b/packages/cpf-gen/src/cpf-gen/errors.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'lacus-utils' + +module CpfGen + # Marker module mixed into every custom error raised by this library. + # + # Use +rescue CpfGen::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 (a generator option). + 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 + + # @return [String] the offending option key + attr_reader :option_name + + # @param actual_input [Object] the offending option value + # @param expected_type [String] description of the expected type + # @param option_name [String] option key when the failure is option-related + def initialize(actual_input, expected_type, option_name:) + actual_type = LacusUtils.describe_type(actual_input) + super( + %(CPF generator option "#{option_name}" must be of type #{expected_type}. Got #{actual_type}.) + ) + @actual_input = actual_input + @actual_type = actual_type + @expected_type = expected_type + @option_name = option_name + end + end + + # API misuse error raised when the combination of provided arguments does not + # match any valid overload-style signature. + class InvalidArgumentCombinationError < ArgumentError + include Error + end + + # Domain error ancestor for business-rule failures (validation and other domain + # leaves). Prefer raising a leaf subclass. + class DomainError < RangeError + include Error + end + + # Domain error raised when a generator option has a valid type but violates a + # domain rule (invalid +prefix+). Structurally mirrors {CnpjGen::ValidationError} + # (including +expected_values+) even though CPF has no +type+ option that would + # populate that field. + class ValidationError < DomainError + # @return [String] the offending option name + attr_reader :option_name + + # @return [Object] the offending option value + attr_reader :actual_input + + # @return [String, nil] human-readable reason when the failure is prefix-related + attr_reader :reason + + # @return [Array, nil] reserved for structural parity with CNPJ; always + # +nil+ for CPF prefix validation + attr_reader :expected_values + + # @param option_name [String] the offending option key + # @param actual_input [Object] the offending option value + # @param reason [String, nil] human-readable reason for a prefix failure + # @param expected_values [Array, nil] unused for CPF (always +nil+) + def initialize(option_name, actual_input, reason: nil, expected_values: nil) + super(build_message(option_name, actual_input, reason, expected_values)) + @option_name = option_name + @actual_input = actual_input + @reason = reason + @expected_values = expected_values&.dup&.freeze + end + + private + + def build_message(option_name, actual_input, reason, expected_values) + if expected_values + quoted = expected_values.map { |value| %("#{value}") }.join(', ') + "CPF generator option \"#{option_name}\" accepts only the following values: #{quoted}. " \ + "Got \"#{actual_input}\"." + else + "CPF generator option \"#{option_name}\" with value \"#{actual_input}\" is invalid. #{reason}" + end + end + end +end diff --git a/packages/cpf-gen/src/cpf-gen/types.rb b/packages/cpf-gen/src/cpf-gen/types.rb new file mode 100644 index 0000000..7055364 --- /dev/null +++ b/packages/cpf-gen/src/cpf-gen/types.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module CpfGen + # Options input accepted by constructors and merge helpers. + # + # May be a {CpfGeneratorOptions} instance, a {Hash} of option keys, or +nil+. + # + # Resolved options contain: + # + # - +format+ [Boolean] — whether to format the generated CPF string as + # +000.000.000-00+ (default: +false+). + # - +prefix+ [String] — a partial string containing 0 to 9 digits to use as the + # start of the generated CPF. Only digits are kept; the rest is stripped. If + # provided, only the missing digits are generated randomly. For example, if + # the +prefix+ +"123456"+ (6 digits) is given, only the next 3 digits are + # randomly generated and concatenated to the +prefix+. + # + # Note: If the evaluated +prefix+ (after stripping non-digit characters) is + # longer than 9 digits, the extra digits are ignored, because a CPF has 9 + # base digits followed by 2 calculated check digits. + # + # @see CpfGeneratorOptions + CpfGeneratorOptionsInput = Object +end diff --git a/packages/cpf-gen/src/cpf-gen/utils.rb b/packages/cpf-gen/src/cpf-gen/utils.rb new file mode 100644 index 0000000..47a9310 --- /dev/null +++ b/packages/cpf-gen/src/cpf-gen/utils.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module CpfGen + # Low-level helpers used by {CpfGenerator} and {CpfGeneratorOptions}. + # + # @api private + module Utils + module_function + + # rubocop:disable Naming/PredicateMethod -- coercion helper, not a predicate query + def normalize_boolean(value) + return false if [false, '', 0].include?(value) + + !!value + end + # rubocop:enable Naming/PredicateMethod + + def assert_string_option!(option_name, value) + return if value.is_a?(String) + + raise TypeMismatchError.new(value, 'string', option_name: option_name) + end + + def fetch_option(source, key) + return source[key] if source.key?(key) + return source[key.to_s] if source.key?(key.to_s) + + nil + end + + def sanitize_prefix(value) + assert_string_option!('prefix', value) + + sanitized = value.gsub(CpfGeneratorOptions::PREFIX_SANITIZE_PATTERN, '') + sanitized[0, CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH] + end + + def validate_prefix!(partial_cpf) + validate_prefix_base_id!(partial_cpf) + validate_prefix_non_repeated_digits!(partial_cpf) + end + + def validate_prefix_base_id!(partial_cpf) + return if partial_cpf.length < CpfGeneratorOptions::CPF_BASE_ID_LENGTH + + cpf_base_id = partial_cpf[0, CpfGeneratorOptions::CPF_BASE_ID_LAST_INDEX + 1] + + return unless cpf_base_id == CpfGeneratorOptions::ZEROED_CPF_BASE_ID + + raise ValidationError.new( + 'prefix', + partial_cpf, + reason: 'Zeroed base ID is not eligible.' + ) + end + + def validate_prefix_non_repeated_digits!(partial_cpf) + return if partial_cpf.length < CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH + + first_character = partial_cpf[0] + + return unless partial_cpf == first_character * CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH + + raise ValidationError.new( + 'prefix', + partial_cpf, + reason: 'Repeated digits are not considered valid.' + ) + end + + # Formats a raw 11-digit CPF into the standard masked representation. + def format_cpf(raw) + "#{raw[0, 3]}.#{raw[3, 3]}.#{raw[6, 3]}-#{raw[9, 2]}" + end + end +end From 0526274b82a575dbe75f34031ac0aa33b0e06873 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:17:06 -0300 Subject: [PATCH 07/15] test(cpf-gen): implement CPF generation unit tests Updated the test suite for the CPF generation functionality by introducing comprehensive tests for the `CpfGen` class. Added tests for generating valid CPF identifiers with various options, including format and prefix handling. Implemented error handling tests to ensure proper exceptions are raised for invalid argument combinations and type mismatches. - Refactored existing tests to improve coverage and clarity. - Added new test files for `CpfGeneratorOptions` and error classes. - Ensured all edge cases for CPF generation and validation are covered. This update strengthens the reliability of the CPF generation utility through rigorous testing. Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-gen/tests/cpf_gen.spec.rb | 23 +- packages/cpf-gen/tests/cpf_generator.spec.rb | 337 ++++++++++++++++++ .../tests/cpf_generator_options.spec.rb | 328 +++++++++++++++++ packages/cpf-gen/tests/errors.spec.rb | 141 ++++++++ packages/cpf-gen/tests/utils.spec.rb | 92 +++++ 5 files changed, 918 insertions(+), 3 deletions(-) create mode 100644 packages/cpf-gen/tests/cpf_generator.spec.rb create mode 100644 packages/cpf-gen/tests/cpf_generator_options.spec.rb create mode 100644 packages/cpf-gen/tests/errors.spec.rb create mode 100644 packages/cpf-gen/tests/utils.spec.rb diff --git a/packages/cpf-gen/tests/cpf_gen.spec.rb b/packages/cpf-gen/tests/cpf_gen.spec.rb index a2f4839..633e2a4 100644 --- a/packages/cpf-gen/tests/cpf_gen.spec.rb +++ b/packages/cpf-gen/tests/cpf_gen.spec.rb @@ -3,9 +3,26 @@ require 'spec_helper' RSpec.describe CpfGen do - describe '.hello' do - it 'returns cpf-gen' do - expect(CpfGen.hello).to eq('cpf-gen') + describe '.cpf_gen' do + context 'when called with no arguments' do + it 'returns an 11-digit numeric CPF' do + expect(described_class.cpf_gen).to match(/\A\d{11}\z/) + end + end + + context 'when called with options' do + it 'forwards format and prefix' do + result = described_class.cpf_gen(format: true, prefix: '12345') + + expect(result).to match(/\A123\.45\d\.\d{3}-\d{2}\z/) + end + end + + context 'when called with keyword options' do + it 'raises InvalidArgumentCombinationError when options and keywords are both given' do + expect { described_class.cpf_gen({ format: true }, prefix: '12') } + .to raise_error(CpfGen::InvalidArgumentCombinationError, /options.*keyword arguments.*not both/) + end end end end diff --git a/packages/cpf-gen/tests/cpf_generator.spec.rb b/packages/cpf-gen/tests/cpf_generator.spec.rb new file mode 100644 index 0000000..3d4a3dd --- /dev/null +++ b/packages/cpf-gen/tests/cpf_generator.spec.rb @@ -0,0 +1,337 @@ +# frozen_string_literal: true + +require 'spec_helper' + +CPF_GENERATOR_PREFIX_CASES = %w[ + 1 12 123 1234 12345 123456 1234567 12345678 123456789 +].freeze + +CPF_GENERATOR_FACTORIES = { + constructor_literal: lambda { |options| + generator = CpfGen::CpfGenerator.new(options) + ->(override = nil) { generator.generate(override) } + }, + constructor_options: lambda { |options| + generator_options = CpfGen::CpfGeneratorOptions.new(options) + generator = CpfGen::CpfGenerator.new(generator_options) + ->(override = nil) { generator.generate(override) } + }, + method_literal: lambda { |options| + generator = CpfGen::CpfGenerator.new + lambda do |override = nil| + if override.nil? + generator.generate(options) + elsif override.is_a?(CpfGen::CpfGeneratorOptions) + generator.generate(CpfGen::CpfGeneratorOptions.new(options, override)) + else + generator.generate(options.merge(override)) + end + end + }, + method_options: lambda { |options| + generator = CpfGen::CpfGenerator.new + lambda do |override = nil| + generator_options = CpfGen::CpfGeneratorOptions.new(options, override || {}) + generator.generate(generator_options) + end + } +}.freeze + +class CpfGeneratorCallsSpy < CpfGen::CpfGenerator + attr_reader :calls_count, :calls_arguments + + def initialize(*args, **kwargs) + super + @calls_count = 0 + @calls_arguments = [] + end + + def generate(options = nil, **kwargs) + @calls_count += 1 + @calls_arguments << [options, kwargs] + super + end +end + +RSpec.describe CpfGen::CpfGenerator do + def default_options_snapshot + CpfGen::CpfGeneratorOptions.new.all + end + + def unique_result_count(generate, count: 100, **kwargs) + Array.new(count) { generate.call(**kwargs) }.uniq.size + end + + describe '#initialize' do + context 'when called with no arguments' do + it 'creates an instance with default options' do + generator = described_class.new + + expect(generator.options.all).to eq(default_options_snapshot) + end + end + + context 'when called with an empty hash' do + it 'creates an instance with default options' do + generator = described_class.new({}) + + expect(generator.options.all).to eq(default_options_snapshot) + end + end + + context 'when called with a CpfGeneratorOptions instance' do + let(:options) do + CpfGen::CpfGeneratorOptions.new(format: true, prefix: '123456') + end + + it 'uses that instance directly without copying' do + generator = described_class.new(options) + + aggregate_failures do + expect(generator.options).to equal(options) + expect(generator.options.all).to eq(options.all) + end + end + + it 'reflects mutations on future generate calls' do + generator = described_class.new( + CpfGen::CpfGeneratorOptions.new(prefix: '123456', format: true) + ) + + generator.options.prefix = '112233' + generator.options.format = false + + result = generator.generate + + aggregate_failures do + expect(result.length).to eq(11) + expect(result).to match(/\A112233\d{5}\z/) + end + end + end + + context 'when called with a literal options hash' do + it 'creates a new CpfGeneratorOptions instance' do + input = { format: true, prefix: '123456' } + generator = described_class.new(input) + + aggregate_failures do + expect(generator.options).to be_a(CpfGen::CpfGeneratorOptions) + expect(generator.options.format).to be(true) + expect(generator.options.prefix).to eq('123456') + end + end + end + + context 'when called with invalid options' do + it 'raises ValidationError' do + expect { described_class.new(prefix: '000000000') } + .to raise_error(CpfGen::ValidationError) + end + + it 'raises TypeMismatchError' do + expect { described_class.new(prefix: 123) } + .to raise_error(CpfGen::TypeMismatchError) + end + end + + context 'when called with both an options instance and keyword arguments' do + it 'raises InvalidArgumentCombinationError' do + options = CpfGen::CpfGeneratorOptions.new(format: true, prefix: '123456') + + expect { described_class.new(options, format: false) } + .to raise_error(CpfGen::InvalidArgumentCombinationError, /options.*keyword arguments.*not both/) + end + end + + context 'when called with both an options Hash and keyword arguments' do + it 'raises InvalidArgumentCombinationError' do + expect { described_class.new({ format: true }, prefix: '12') } + .to raise_error(CpfGen::InvalidArgumentCombinationError, /options.*keyword arguments.*not both/) + end + end + end + + describe '#generate' do + { + 'constructor literal' => :constructor_literal, + 'constructor CpfGeneratorOptions' => :constructor_options, + 'method literal' => :method_literal, + 'method CpfGeneratorOptions' => :method_options + }.each do |label, factory_key| + context "when options are passed via #{label}" do + let(:factory) { CPF_GENERATOR_FACTORIES.fetch(factory_key) } + + context 'when no options are passed' do + let(:generate) { factory.call({}) } + + it 'returns an 11-digit numeric string' do + 100.times do + result = generate.call + + aggregate_failures do + expect(result.length).to eq(11) + expect(result).not_to match(/[a-z]/i) + expect(result).not_to match(%r{[./-]}) + expect(result).to match(/\A\d+\z/) + end + end + end + + it 'returns mostly unique values' do + expect(unique_result_count(generate)).to be >= 99 + end + end + + context 'when format is true' do + let(:generate) { factory.call(format: true) } + + it 'returns a 14-character formatted string' do + 100.times do + result = generate.call + + aggregate_failures do + expect(result.length).to eq(14) + expect(result).not_to match(/[a-z]/i) + expect(result).to match(%r{[./-]}) + expect(result).to match(/\d{2,3}/) + end + end + end + + it 'matches the standard CPF mask' do + 100.times do + expect(generate.call).to match(/\A\d{3}\.\d{3}\.\d{3}-\d{2}\z/) + end + end + + it 'returns mostly unique values' do + expect(unique_result_count(generate)).to be >= 99 + end + end + + context 'when prefix is passed' do + CPF_GENERATOR_PREFIX_CASES.each do |prefix| + it "returns an 11-digit string starting with #{prefix}" do + generate = factory.call(prefix: prefix) + + 100.times do + result = generate.call + + aggregate_failures do + expect(result.length).to eq(11) + expect(result).to match(/\A\d+\z/) + expect(result).to start_with(prefix) + end + end + end + end + + it 'drops characters after the 9th position' do + generate = factory.call(prefix: '12345678910') + result = generate.call + + aggregate_failures do + expect(result.length).to eq(11) + expect(result).not_to end_with('10') + expect(result).to match(/\A123456789\d{2}\z/) + end + end + + it 'returns a deterministic CPF for a 9-digit prefix' do + generate = factory.call(prefix: '987654321') + results = Array.new(100) { generate.call }.uniq + + expect(results.size).to eq(1) + end + + it 'strips non-digit characters from prefix' do + generate = factory.call(prefix: 'ABC.123.DEF.456.GHI.789', format: false) + + expect(generate.call).to start_with('123456789') + end + end + + context 'when different options are combined' do + it 'returns a 14-character CPF with format and prefix' do + generate = factory.call(format: true, prefix: '12345678') + result = generate.call + + aggregate_failures do + expect(result.length).to eq(14) + expect(result).not_to match(/[a-z]/i) + expect(result).to match(/\A123\.456\.78\d-\d{2}\z/) + end + end + end + end + end + + context 'when called with both an options instance and keyword arguments' do + it 'raises InvalidArgumentCombinationError' do + generator = described_class.new(format: false, prefix: '12') + per_call_options = CpfGen::CpfGeneratorOptions.new(format: true, prefix: '123456') + + expect { generator.generate(per_call_options, format: false) } + .to raise_error(CpfGen::InvalidArgumentCombinationError, /options.*keyword arguments.*not both/) + end + end + + context 'when called with both an options Hash and keyword arguments' do + it 'raises InvalidArgumentCombinationError' do + generator = described_class.new + + expect { generator.generate({ format: true }, prefix: '12') } + .to raise_error(CpfGen::InvalidArgumentCombinationError, /options.*keyword arguments.*not both/) + end + end + + context 'when CpfCheckDigits raises a DomainError' do + before do + allow(LacusUtils).to receive(:generate_random_sequence) + .and_return('111111111', '123456789') + end + + it 'retries generation and returns a valid CPF' do + result = described_class.new.generate + + aggregate_failures do + expect(result.length).to eq(11) + expect(result).to start_with('123456789') + expect(LacusUtils).to have_received(:generate_random_sequence).twice + end + end + + it 'uses the same options on retry' do + allow(LacusUtils).to receive(:generate_random_sequence).and_return('111111', '222333') + + result = described_class.new(prefix: '111', format: true).generate + + aggregate_failures do + expect(result.length).to eq(14) + expect(result).to start_with('111.222.333-') + expect(LacusUtils).to have_received(:generate_random_sequence).with(6, :numeric).twice + end + end + + it 'retries with the same per-call options' do + allow(LacusUtils).to receive(:generate_random_sequence).and_return('111111', '222333') + + generator = CpfGeneratorCallsSpy.new + result = generator.generate(format: true, prefix: '111') + + aggregate_failures do + expect(result.length).to eq(14) + expect(result).to start_with('111.222.333-') + expect(generator.calls_count).to eq(2) + expect(generator.calls_arguments).to eq( + [ + [nil, { format: true, prefix: '111' }], + [nil, { format: true, prefix: '111' }] + ] + ) + end + end + end + end +end diff --git a/packages/cpf-gen/tests/cpf_generator_options.spec.rb b/packages/cpf-gen/tests/cpf_generator_options.spec.rb new file mode 100644 index 0000000..db6e4f1 --- /dev/null +++ b/packages/cpf-gen/tests/cpf_generator_options.spec.rb @@ -0,0 +1,328 @@ +# frozen_string_literal: true + +require 'spec_helper' + +CPF_GENERATOR_OPTIONS_REPEATED_DIGIT_PREFIXES = %w[ + 111111111 + 222222222 + 333333333 + 444444444 + 555555555 + 666666666 + 777777777 + 888888888 + 999999999 +].freeze + +RSpec.describe CpfGen::CpfGeneratorOptions do + def expect_options_match(actual, expected) + expected.each do |key, value| + expect(actual[key]).to eq(value) + end + end + + let(:default_parameters) do + { + format: described_class::DEFAULT_FORMAT, + prefix: described_class::DEFAULT_PREFIX + } + end + + describe '#initialize' do + context 'when called with no parameters' do + it 'sets all options to default values' do + expect_options_match(described_class.new.all, default_parameters) + end + end + + context 'when called with all parameters set to nil' do + it 'sets all options to default values' do + options = described_class.new(format: nil, prefix: nil) + + expect_options_match(options.all, default_parameters) + end + end + + context 'when called with all parameters' do + it 'sets all options to the provided values' do + parameters = { format: true, prefix: '12345' } + + expect_options_match(described_class.new(parameters).all, parameters) + end + end + + context 'when called with some parameters' do + it 'sets only the provided non-nil values' do + options = described_class.new(format: true) + + expect_options_match( + options.all, + default_parameters.merge(format: true) + ) + end + end + + context 'when called with a CpfGeneratorOptions instance' do + it 'creates a new instance with the same values' do + original_options = described_class.new(format: true, prefix: '12345') + options = described_class.new(original_options) + + aggregate_failures do + expect(options).not_to equal(original_options) + expect_options_match(options.all, original_options.all) + end + end + end + + context 'when called with override parameters' do + it 'uses the last option with two params' do + options = described_class.new({ prefix: '12345' }, { prefix: '11222333' }) + + expect(options.prefix).to eq('11222333') + end + + it 'uses the last option with one hash and one instance' do + options = described_class.new( + { prefix: '12345' }, + described_class.new(prefix: '11222333') + ) + + expect(options.prefix).to eq('11222333') + end + + it 'uses the last option with five params' do + options = described_class.new( + { prefix: '123456780009' }, + { prefix: '11' }, + { prefix: '22333' }, + { prefix: '44555666' }, + { prefix: '77888999' } + ) + + expect(options.prefix).to eq('77888999') + end + + it 'ignores a nil value inside a later layer' do + options = described_class.new({ prefix: '11222333' }, { prefix: nil }) + + expect(options.prefix).to eq('11222333') + end + + it 'gives keyword arguments precedence over every positional layer' do + options = described_class.new({ prefix: '12345' }, { prefix: '11222333' }, prefix: '99999999') + + expect(options.prefix).to eq('99999999') + end + + it 'ignores a nil keyword argument in favor of the positional layers' do + options = described_class.new({ prefix: '11222333' }, prefix: nil) + + expect(options.prefix).to eq('11222333') + end + end + end + + describe '#set' do + context 'when called with partial overrides' do + it 'updates only the provided keys and keeps current values' do + options = described_class.new(format: true, prefix: '12345') + options.set(prefix: '11222333') + + expect_options_match(options.all, format: true, prefix: '11222333') + end + + it 'folds positional layers left to right then applies keywords' do + options = described_class.new(format: false, prefix: '12345') + options.set({ prefix: '11' }, { prefix: '22333' }, format: true) + + expect_options_match(options.all, format: true, prefix: '22333') + end + + it 'ignores nil values and keeps the previously resolved value' do + options = described_class.new(format: true, prefix: '12345') + options.set(format: nil, prefix: '11222333') + + expect_options_match(options.all, format: true, prefix: '11222333') + end + + it 'returns self' do + options = described_class.new + + expect(options.set(format: true)).to equal(options) + end + end + end + + describe '#format=' do + context 'when setting to a boolean value' do + it 'sets format to true' do + options = described_class.new(format: false) + options.format = true + + expect(options.format).to be(true) + end + + it 'sets format to false' do + options = described_class.new(format: true) + options.format = false + + expect(options.format).to be(false) + end + end + + context 'when setting to a nil value' do + it 'raises TypeMismatchError' do + options = described_class.new(format: !described_class::DEFAULT_FORMAT) + + expect { options.format = nil } + .to raise_error( + CpfGen::TypeMismatchError, + 'CPF generator option "format" must be of type boolean. Got nil.' + ) + end + end + + context 'when setting to a non-boolean value' do + it 'coerces an object to true' do + options = described_class.new(format: false) + options.format = { not: 'a boolean' } + + expect(options.format).to be(true) + end + + it 'coerces a truthy string to true' do + options = described_class.new(format: false) + options.format = 'not a boolean' + + expect(options.format).to be(true) + end + + it 'coerces a truthy number to true' do + options = described_class.new(format: false) + options.format = 123 + + expect(options.format).to be(true) + end + + it 'coerces an empty string to false' do + options = described_class.new(format: false) + options.format = '' + + expect(options.format).to be(false) + end + + it 'coerces zero to false' do + options = described_class.new(format: false) + options.format = 0 + + expect(options.format).to be(false) + end + end + end + + describe '#prefix=' do + context 'when setting to a valid string value' do + it 'sets prefix to the provided value' do + options = described_class.new(prefix: '12345') + options.prefix = '11222333' + + expect(options.prefix).to eq('11222333') + end + + it 'strips non-digit characters' do + options = described_class.new + options.prefix = '123.ABC.def' + + expect(options.prefix).to eq('123') + end + + it 'truncates extra characters beyond 9' do + options = described_class.new + options.prefix = '12345678910' + + expect(options.prefix).to eq('123456789') + end + end + + context 'when setting to a nil value' do + it 'raises TypeMismatchError' do + options = described_class.new(prefix: '12345') + + expect { options.prefix = nil } + .to raise_error( + CpfGen::TypeMismatchError, + 'CPF generator option "prefix" must be of type string. Got nil.' + ) + end + end + + context 'when setting to a non-string value' do + it 'raises TypeMismatchError for an object' do + options = described_class.new + + expect { options.prefix = { not: 'a string' } } + .to raise_error( + CpfGen::TypeMismatchError, + 'CPF generator option "prefix" must be of type string. Got hash.' + ) + end + + it 'raises TypeMismatchError for a number' do + options = described_class.new + + expect { options.prefix = 123 } + .to raise_error( + CpfGen::TypeMismatchError, + 'CPF generator option "prefix" must be of type string. Got integer number.' + ) + end + + it 'raises TypeMismatchError for a boolean' do + options = described_class.new + + expect { options.prefix = true } + .to raise_error( + CpfGen::TypeMismatchError, + 'CPF generator option "prefix" must be of type string. Got boolean.' + ) + end + end + + context 'when setting to an invalid string' do + it 'raises for a zeroed base ID' do + options = described_class.new + + expect { options.prefix = '000000000' } + .to raise_error( + CpfGen::ValidationError, + 'CPF generator option "prefix" with value "000000000" is invalid. ' \ + 'Zeroed base ID is not eligible.' + ) + end + + CPF_GENERATOR_OPTIONS_REPEATED_DIGIT_PREFIXES.each do |prefix| + it "raises for repeated digits in #{prefix}" do + options = described_class.new + + expect { options.prefix = prefix } + .to raise_error( + CpfGen::ValidationError, + %(CPF generator option "prefix" with value "#{prefix}" is invalid. ) \ + 'Repeated digits are not considered valid.' + ) + end + end + end + end + + describe '#all' do + it 'returns all properties with expected types' do + snapshot = described_class.new.all + + aggregate_failures do + expect([true, false]).to include(snapshot[:format]) + expect(snapshot[:prefix]).to be_a(String) + end + end + end +end diff --git a/packages/cpf-gen/tests/errors.spec.rb b/packages/cpf-gen/tests/errors.spec.rb new file mode 100644 index 0000000..5116f17 --- /dev/null +++ b/packages/cpf-gen/tests/errors.spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe CpfGen::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 CpfGen::TypeMismatchError do + subject(:error) { described_class.new(123, 'string', option_name: 'format') } + + context 'when instantiated' do + it 'is a TypeError' do + expect(error).to be_a(TypeError) + end + + it 'includes CpfGen::Error' do + expect(error).to be_a(CpfGen::Error) + end + + it 'exposes the class name' do + expect(error.class.name).to eq('CpfGen::TypeMismatchError') + end + + it 'sets option_name' do + expect(described_class.new(123, 'string', option_name: 'prefix').option_name).to eq('prefix') + 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(error.expected_type).to eq('string') + end + + it 'builds a descriptive message' do + expect(error.message).to eq( + 'CPF generator option "format" must be of type string. Got integer number.' + ) + end + end +end + +RSpec.describe CpfGen::InvalidArgumentCombinationError do + subject(:error) { described_class.new('invalid combination') } + + it 'is an ArgumentError' do + expect(error).to be_a(ArgumentError) + end + + it 'includes CpfGen::Error' do + expect(error).to be_a(CpfGen::Error) + end + + it 'is not a DomainError' do + expect(error).not_to be_a(CpfGen::DomainError) + end +end + +RSpec.describe CpfGen::DomainError do + before do + stub_const('CpfGen::TestDomainError', Class.new(described_class)) + end + + subject(:error) { CpfGen::TestDomainError.new('some error') } + + context 'when instantiated through a subclass' do + it 'is a RangeError' do + expect(error).to be_a(RangeError) + end + + it 'is a DomainError' do + expect(error).to be_a(described_class) + end + + it 'includes CpfGen::Error' do + expect(error).to be_a(CpfGen::Error) + end + + it 'exposes the subclass name' do + expect(error.class.name).to eq('CpfGen::TestDomainError') + end + + it 'exposes the message' do + expect(error.message).to eq('some error') + end + end +end + +RSpec.describe CpfGen::ValidationError do + context 'when instantiated for an invalid prefix' do + subject(:error) { described_class.new('prefix', '1.2.3.4.5', reason: 'repeated digits') } + + it 'is a RangeError' do + expect(error).to be_a(RangeError) + end + + it 'is a DomainError' do + expect(error).to be_a(CpfGen::DomainError) + end + + it 'includes CpfGen::Error' do + expect(error).to be_a(CpfGen::Error) + end + + it 'exposes the class name' do + expect(error.class.name).to eq('CpfGen::ValidationError') + end + + it 'sets option_name' do + expect(error.option_name).to eq('prefix') + end + + it 'sets actual_input' do + expect(described_class.new('prefix', '77777777', reason: 'repeated digits').actual_input) + .to eq('77777777') + end + + it 'sets reason' do + expect(error.reason).to eq('repeated digits') + end + + it 'leaves expected_values nil' do + expect(error.expected_values).to be_nil + end + + it 'builds a descriptive message' do + expect(error.message).to eq( + 'CPF generator option "prefix" with value "1.2.3.4.5" is invalid. repeated digits' + ) + end + end +end diff --git a/packages/cpf-gen/tests/utils.spec.rb b/packages/cpf-gen/tests/utils.spec.rb new file mode 100644 index 0000000..015059f --- /dev/null +++ b/packages/cpf-gen/tests/utils.spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe CpfGen::Utils do + describe '.normalize_boolean' do + it 'returns false for false, empty string, and zero' do + aggregate_failures do + expect(described_class.normalize_boolean(false)).to be(false) + expect(described_class.normalize_boolean('')).to be(false) + expect(described_class.normalize_boolean(0)).to be(false) + end + end + + it 'returns true for other truthy values' do + aggregate_failures do + expect(described_class.normalize_boolean(true)).to be(true) + expect(described_class.normalize_boolean('yes')).to be(true) + expect(described_class.normalize_boolean(1)).to be(true) + end + end + end + + describe '.assert_string_option!' do + it 'accepts a string' do + expect { described_class.assert_string_option!('prefix', '123') }.not_to raise_error + end + + it 'raises TypeMismatchError for a non-string' do + expect { described_class.assert_string_option!('prefix', 1) } + .to raise_error(CpfGen::TypeMismatchError) + end + end + + describe '.fetch_option' do + it 'reads a symbol key' do + expect(described_class.fetch_option({ format: true }, :format)).to be(true) + end + + it 'reads a string key' do + expect(described_class.fetch_option({ 'format' => true }, :format)).to be(true) + end + + it 'returns nil when the key is absent' do + expect(described_class.fetch_option({}, :format)).to be_nil + end + end + + describe '.sanitize_prefix' do + it 'strips non-digit characters' do + expect(described_class.sanitize_prefix('ABC.123.DEF.456')).to eq('123456') + end + + it 'truncates to the maximum prefix length' do + long_prefix = '1' * 20 + + expect(described_class.sanitize_prefix(long_prefix).length) + .to eq(CpfGen::CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH) + end + + it 'raises TypeMismatchError for a non-string' do + expect { described_class.sanitize_prefix(123) } + .to raise_error(CpfGen::TypeMismatchError) + end + end + + describe '.validate_prefix!' do + it 'accepts a valid partial prefix' do + expect { described_class.validate_prefix!('123456') }.not_to raise_error + end + + it 'accepts eight zeros as a partial prefix' do + expect { described_class.validate_prefix!('00000000') }.not_to raise_error + end + + it 'raises when the base ID is zeroed' do + expect { described_class.validate_prefix!('000000000') } + .to raise_error(CpfGen::ValidationError, /Zeroed base ID/) + end + + it 'raises when the full prefix is repeated digits' do + expect { described_class.validate_prefix!('111111111') } + .to raise_error(CpfGen::ValidationError, /Repeated digits/) + end + end + + describe '.format_cpf' do + it 'inserts the standard CPF delimiters' do + expect(described_class.format_cpf('47844241055')).to eq('478.442.410-55') + end + end +end From 5ef65ad88b0d29acea868f78f2475439bbe0e3a6 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:17:34 -0300 Subject: [PATCH 08/15] docs(cpf-gen): create changelogs file Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-gen/CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/cpf-gen/CHANGELOG.md b/packages/cpf-gen/CHANGELOG.md index 147c889..ff400dc 100644 --- a/packages/cpf-gen/CHANGELOG.md +++ b/packages/cpf-gen/CHANGELOG.md @@ -1 +1,17 @@ # cpf-gen + +## 1.0.0 + +### 🚀 Stable Version Released! + +Utility module to generate valid CPF (Brazilian personal ID) strings. Main features: + +- **Multiple interfaces**: supports `CpfGen.cpf_gen` and `CpfGen::CpfGenerator` with shared `CpfGen::CpfGeneratorOptions` defaults; an `options` argument (instance or `Hash`) is never merged with keyword overrides — passing both at once raises `InvalidArgumentCombinationError`. +- **Numeric CPF**: generates 11-digit numeric identifiers; random body is always digits (`0-9`). +- **Prefix support**: partial start strings are sanitized (digits only, truncated to 9) and validated — rejects zeroed base ID and 9 repeated digits via `ValidationError`; only missing digits up to 9 are randomly generated. +- **Optional formatting**: `format: true` returns the standard masked `XXX.XXX.XXX-XX` layout; default output is unformatted. +- **Strict options merging**: `CpfGeneratorOptions.new`/`#set` fold positional `Hash`/instance layers left to right, then apply keyword overrides with the highest precedence, filling any still-unresolved option with its `DEFAULT_*` value; property setters (`format=`, `prefix=`) never accept `nil` directly — pass the matching `DEFAULT_*` constant to reset explicitly. +- **Structured errors**: `CpfGen::Error` marker with misuse leaves (`TypeMismatchError`, `InvalidArgumentCombinationError`) and domain leaf (`ValidationError` under `DomainError`). +- **Check digits**: computed via `cpf-dv`; generator retries when check-digit computation rejects a random body candidate. + +For detailed usage and API reference, see the [README](./README.md). From 6030022dab586ca86065f21d3b61e3a9ab79ba46 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:17:42 -0300 Subject: [PATCH 09/15] docs(cpf-gen): create README file Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-gen/README.md | 350 +++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 packages/cpf-gen/README.md diff --git a/packages/cpf-gen/README.md b/packages/cpf-gen/README.md new file mode 100644 index 0000000..d5d6c1a --- /dev/null +++ b/packages/cpf-gen/README.md @@ -0,0 +1,350 @@ +![cpf-gen for Ruby](https://br-utils.vercel.app/img/cover_cpf-gen.jpg) + +[![Gem Version](https://img.shields.io/gem/v/cpf-gen)](https://rubygems.org/gems/cpf-gen) +[![Gem Downloads](https://img.shields.io/gem/dt/cpf-gen)](https://rubygems.org/gems/cpf-gen) +[![Ruby Version](https://img.shields.io/gem/rv/cpf-gen)](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 generate valid 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 ✔ | + +Requires Ruby **≥ 3.1** (see `required_ruby_version` in the gemspec). + +## Features + +- ✅ **Numeric CPF**: Generates 11-digit numeric CPF values with valid check digits +- ✅ **Optional prefix**: Provide 0–9 digits to fix the start of the CPF and generate the rest with valid check digits +- ✅ **Formatting**: Option to return the standard formatted string (`000.000.000-00`) +- ✅ **Reusable generator**: `CpfGen::CpfGenerator` class with default options and per-call overrides +- ✅ **Keyword overrides**: Pass `format:` and `prefix:` on `cpf_gen`, `CpfGenerator#generate`, and constructors +- ✅ **Minimal dependencies**: Only [`cpf-dv`](https://rubygems.org/gems/cpf-dv) and [`lacus-utils`](https://rubygems.org/gems/lacus-utils) +- ✅ **Error handling**: API misuse vs domain errors with a `CpfGen::Error` marker for library-wide rescue + +## Installation + +Install the gem directly: + +```bash +gem install cpf-gen +``` + +Or add it to your `Gemfile` and run `bundle install`: + +```ruby +gem 'cpf-gen' +``` + +## Require + +```ruby +require 'cpf-gen' +``` + +## Quick Start + +```ruby +require 'cpf-gen' + +CpfGen.cpf_gen # => e.g. "47844241055" (11-digit numeric) + +CpfGen.cpf_gen(format: true) # => e.g. "005.265.352-88" + +CpfGen.cpf_gen(prefix: '528250911') # => e.g. "52825091138" +CpfGen.cpf_gen( # => e.g. "528.250.911-38" + prefix: '528250911', + format: true +) +``` + +Options can also be passed as a `Hash`: + +```ruby +CpfGen.cpf_gen({ format: true, prefix: '528250911' }) +``` + +## Usage + +The main entry points are the module helper `CpfGen.cpf_gen`, the class `CpfGen::CpfGenerator`, and the options class `CpfGen::CpfGeneratorOptions`. + +### Generator options + +All options are optional: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | When truthy, return the generated CPF in standard format (`000.000.000-00`). Non-boolean values are coerced (`false`, `''`, and `0` become `false`; other values become truthy). | +| `prefix` | `String` | `''` | Partial start string (0–9 digits). Only digits are kept; missing characters are generated randomly and check digits are computed. Prefixes longer than 9 digits are truncated silently. | + +Prefix rules: the base (first 9 digits) cannot be all zeros; 9 repeated digits (e.g. `999999999`) are not allowed. Prefixes shorter than 9 digits are never rejected by these rules (e.g. `"00000000"` and `"11111111"` are allowed). + +`nil` is accepted as a keyword argument on `cpf_gen`, `CpfGenerator.new`, `CpfGenerator#generate`, and `CpfGeneratorOptions.new`/`#set` — it simply means "no override for this option". It is **not** accepted by the `CpfGeneratorOptions` property setters (`options.format = value`, `options.prefix = value`): calling a setter with `nil` directly raises `CpfGen::TypeMismatchError`. To reset a property to its default value through a setter, pass the literal constant instead, e.g. `options.format = CpfGen::CpfGeneratorOptions::DEFAULT_FORMAT`. + +### `CpfGen.cpf_gen` (helper) + +Generates a valid CPF string. With no options, returns an 11-digit numeric CPF. This is a convenience wrapper around `CpfGen::CpfGenerator.new(...).generate`. + +- **`options`** (optional): `CpfGen::CpfGeneratorOptions` instance, a `Hash` of option keys, or `nil`. See [Generator options](#generator-options). +- **`format`**, **`prefix`** (keyword arguments): Only used when `options` is omitted (`nil`). Passing `options` **and** any of these keywords at the same time raises `InvalidArgumentCombinationError` — the two ways of passing options are never merged together. + +### `CpfGen::CpfGenerator` (class) + +For reusable defaults or per-call overrides, use the class: + +```ruby +require 'cpf-gen' + +generator = CpfGen::CpfGenerator.new(format: true) + +generator.generate # => e.g. "005.265.352-88" +generator.generate(prefix: '123456') # override for this call only +generator.options # current default options (CpfGen::CpfGeneratorOptions) +``` + +- **`initialize(options = nil, **keywords)`**: Optional default options. When `options` is given (a `CpfGen::CpfGeneratorOptions` instance or a `Hash`) alone, it determines the default options; a `CpfGen::CpfGeneratorOptions` instance is stored by reference (mutating it later affects future `generate` calls that do not pass per-call options), while a `Hash` builds a new instance. When `options` is omitted (`nil`), the default options are built exclusively from the keyword arguments (`format:`, `prefix:`). Passing `options` together with any non-`nil` keyword raises `InvalidArgumentCombinationError` instead of silently ignoring the keywords. +- **`generate(options = nil, **keywords)`**: Returns a valid CPF. `options` and the keyword arguments are never merged: a given `options` argument alone fully overrides the instance defaults for this call; otherwise, any given keyword overrides the instance defaults for this call. When neither is given, the instance defaults are used as-is. The instance defaults are never mutated by a per-call override. Passing `options` together with any non-`nil` keyword raises `InvalidArgumentCombinationError`. +- **`options`**: Reader returning the default options used when per-call options are not provided (same instance as used internally; mutating it affects future `generate` calls). + +Default options on the instance; per-call overrides: + +```ruby +require 'cpf-gen' + +generator = CpfGen::CpfGenerator.new(format: true) + +generator.generate # formatted CPF +generator.generate(format: false) # this call only: unformatted +generator.generate # formatted again (instance defaults preserved) +``` + +### `CpfGen::CpfGeneratorOptions` (class) + +Holds options (`format`, `prefix`) with validation and merge support: + +```ruby +require 'cpf-gen' + +options = CpfGen::CpfGeneratorOptions.new( + prefix: '123456', + format: true +) +options.prefix # => "123456" +options.format # => true +options.set(format: false) # merge and return self +options.all # => { format: false, prefix: "123456" } + +# Resetting a property to its default value requires the literal constant — +# a bare `nil` on a setter raises TypeMismatchError: +options.format = CpfGen::CpfGeneratorOptions::DEFAULT_FORMAT +``` + +- **`initialize(*options, **keywords)`**: Every positional `options` argument (each a `Hash` or another `CpfGen::CpfGeneratorOptions` instance) is folded left to right — later arguments win — then the keyword arguments (`format:`, `prefix:`) are applied on top with the highest precedence. At every step, a `nil` value for a given key is ignored in favor of whatever was resolved so far. Any option still unresolved after that is set to its `DEFAULT_*` value. +- **`format`**, **`prefix`**: Accessors with setters; `prefix` is validated (zeroed base ID, repeated digits). The setters **never accept `nil`** — pass the matching `DEFAULT_*` constant (e.g. `CpfGeneratorOptions::DEFAULT_PREFIX`) to reset a property explicitly. +- **`set(*options, **keywords)`**: Updates multiple options at once, using the same fold-then-keywords, ignore-`nil` resolution as `initialize`. Any option left unresolved after merging keeps its **current** value on the instance (a partial update, not a re-initialization). Returns `self`. +- **`all`**: Shallow `Hash` copy of current options (`:format`, `:prefix`). + +## API + +### Exports + +After `require 'cpf-gen'`: + +- **`CpfGen.cpf_gen`**: `(options = nil, **keywords) -> String` — convenience helper. +- **`CpfGen::CpfGenerator`**: Class to generate CPF with optional default options and per-call overrides. +- **`CpfGen::CpfGeneratorOptions`**: Class holding options with validation and merge. +- **`CpfGen::CPF_LENGTH`**: `11` (constant). +- **`CpfGen::CPF_PREFIX_MAX_LENGTH`**: `9` (constant). +- **`CpfGen::VERSION`**: gem version string. +- **Errors**: `CpfGen::Error`, `CpfGen::DomainError`, `CpfGen::InvalidArgumentCombinationError`, `CpfGen::TypeMismatchError`, `CpfGen::ValidationError`. + +### Error handling + +Errors fall into two categories: + +| Category | Meaning | +|---|---| +| **API misuse** | The caller invoked the library incorrectly (wrong type for an option, or an invalid argument combination). | +| **Domain error** | The call was structurally correct, but a value violates a business rule (invalid `prefix`). | + +Every custom error includes the `CpfGen::Error` marker module. Domain failures (`ValidationError`) inherit from `CpfGen::DomainError` (`RangeError`). + +**Important:** passing both an `options` instance/`Hash` and keyword arguments raises `InvalidArgumentCombinationError`. + +#### Summary + +| Class | Inherits from | Category | Trigger condition | +|---|---|---|---| +| `CpfGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and keyword arguments are passed at once | +| `CpfGen::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | A generator option has the wrong data type | +| `CpfGen::ValidationError` | `CpfGen::DomainError` | Domain error | `prefix` is ineligible (zeroed base ID or 9 repeated digits) | + +#### `CpfGen::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 CpfGen::Error + # everything this library raises +``` + +#### `CpfGen::DomainError` + +- **Inheritance:** `CpfGen::DomainError < RangeError` (includes `CpfGen::Error`) +- **Category:** Domain error — ancestor for all domain failures. +- **When it is raised:** Not raised directly; prefer raising a leaf subclass. +- **Example:** Prefer `raise CpfGen::ValidationError` over raising `DomainError` directly. +- **How to rescue it:** + +```ruby +rescue CpfGen::DomainError + # ValidationError and other DomainError subclasses +``` + +#### `CpfGen::TypeMismatchError` + +- **Inheritance:** `CpfGen::TypeMismatchError < TypeError` (includes `CpfGen::Error`) +- **Category:** API misuse — the caller passed a value of the wrong type. +- **When it is raised:** Raised when a generator option (`format` or `prefix`) has the wrong runtime type. +- **Example:** + +```ruby +CpfGen.cpf_gen(prefix: 123) # raises CpfGen::TypeMismatchError +``` + +- **How to rescue it:** + +```ruby +rescue CpfGen::TypeMismatchError + # this library's type-contract violation + +rescue TypeError + # native type errors, including this library's TypeMismatchError +``` + +#### `CpfGen::InvalidArgumentCombinationError` + +- **Inheritance:** `CpfGen::InvalidArgumentCombinationError < ArgumentError` (includes `CpfGen::Error`) +- **Category:** API misuse — the caller mixed mutually exclusive argument patterns. +- **When it is raised:** Raised when `CpfGenerator.new`, `#generate`, or `cpf_gen` receives both an `options` argument (instance or `Hash`) and any non-`nil` keyword argument at the same time. +- **Example:** + +```ruby +begin + CpfGen::CpfGenerator.new({ format: true }, prefix: '123') +rescue CpfGen::InvalidArgumentCombinationError => e + puts e.message + # Pass either an options instance/Hash to `options`, or keyword arguments (format:, prefix:), not both. +end +``` + +- **How to rescue it:** + +```ruby +rescue CpfGen::InvalidArgumentCombinationError + # this library's invalid argument combination + +rescue ArgumentError + # native argument errors, including this library's InvalidArgumentCombinationError +``` + +#### `CpfGen::ValidationError` + +- **Inheritance:** `CpfGen::ValidationError < CpfGen::DomainError < RangeError` (includes `CpfGen::Error`) +- **Category:** Domain error — a value fails a non-numeric, non-length domain rule. +- **When it is raised:** Raised when `prefix` is ineligible (zeroed base ID `"000000000"`, or 9 repeated digits such as `"999999999"`). +- **Example:** + +```ruby +CpfGen.cpf_gen(prefix: '000000000') # raises CpfGen::ValidationError +CpfGen.cpf_gen(prefix: '999999999') # raises CpfGen::ValidationError +``` + +- **How to rescue it:** + +```ruby +rescue CpfGen::ValidationError + # this exact domain validation failure + +rescue CpfGen::DomainError + # RangeError-rooted domain failures from this library +``` + +#### Rescue granularity + +```ruby +# 1) Single native class — catches type misuse from this library (and other TypeErrors). +rescue TypeError + # CpfGen::TypeMismatchError and any other TypeError (library or not) + +# 2) CpfGen::DomainError — catches business-rule violations under DomainError. +rescue CpfGen::DomainError + # CpfGen::ValidationError and other DomainError subclasses + +# 3) CpfGen::Error — catches everything the library raises. +rescue CpfGen::Error + # every custom error that includes CpfGen::Error + +# 4) Specific leaf class — catches only that exact failure mode. +rescue CpfGen::ValidationError + # only CpfGen::ValidationError +``` + +Notable attributes: + +- `TypeMismatchError`: `option_name`, `actual_input`, `actual_type`, `expected_type` +- `ValidationError`: `option_name`, `actual_input`, `reason` (prefix failures); `expected_values` is always `nil` for CPF + +Property setters never accept `nil` directly — pass the matching `DEFAULT_*` constant to reset: + +```ruby +options = CpfGen::CpfGeneratorOptions.new +begin + options.prefix = nil +rescue CpfGen::TypeMismatchError => e + puts e.message + # CPF generator option "prefix" must be of type string. Got nil. +end + +options.prefix = CpfGen::CpfGeneratorOptions::DEFAULT_PREFIX # explicit reset instead +``` + +Check-digit computation failures from `cpf-dv` are handled internally by retrying generation with the same resolved options; they are not raised to callers under normal operation. + +### Other available resources + +- **`CpfGen::CpfGeneratorOptions::CPF_LENGTH`**: `11`. +- **`CpfGen::CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH`**: `9`. +- **`CpfGen::CpfGeneratorOptions::DEFAULT_FORMAT`**, **`DEFAULT_PREFIX`**: Class-level default constants. + +## 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 e044835787ceb0668cdc87ca0385377f14a7e2d8 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:17:59 -0300 Subject: [PATCH 10/15] docs(cpf-gen): create Portuguese version of README file Co-authored-by: Cursor Grok 4.5 Co-authored-by: Cursor Agent --- packages/cpf-gen/README.pt.md | 335 ++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 packages/cpf-gen/README.pt.md diff --git a/packages/cpf-gen/README.pt.md b/packages/cpf-gen/README.pt.md new file mode 100644 index 0000000..fac7cc5 --- /dev/null +++ b/packages/cpf-gen/README.pt.md @@ -0,0 +1,335 @@ +![cpf-gen para Ruby](https://br-utils.vercel.app/img/cover_cpf-gen.jpg) + +> 🌎 [Access documentation in English](./README.md) + +Utilitário em Ruby para gerar CPFs válidos (Cadastro de Pessoa Física). + +## Recursos + +- ✅ **CPF numérico**: Gera CPF de 11 dígitos numéricos com dígitos verificadores válidos +- ✅ **Prefixo opcional**: Informe de 0 a 9 dígitos para fixar o início do CPF e gerar o restante com dígitos verificadores válidos +- ✅ **Formatação**: Opção de retornar a string no formato padrão (`000.000.000-00`) +- ✅ **Gerador reutilizável**: Classe `CpfGen::CpfGenerator` com opções padrão e sobrescritas por chamada +- ✅ **Sobrescritas por palavra-chave**: Passe `format:` e `prefix:` em `cpf_gen`, `CpfGenerator#generate` e nos construtores +- ✅ **Dependências mínimas**: Apenas [`cpf-dv`](https://rubygems.org/gems/cpf-dv) e [`lacus-utils`](https://rubygems.org/gems/lacus-utils) +- ✅ **Tratamento de erros**: Uso incorreto da API vs erros de domínio, com marcador `CpfGen::Error` para captura em toda a biblioteca + +## Instalação + +Instale a gem diretamente: + +```bash +gem install cpf-gen +``` + +Ou adicione ao seu `Gemfile` e execute `bundle install`: + +```ruby +gem 'cpf-gen' +``` + +## Require + +```ruby +require 'cpf-gen' +``` + +## Início rápido + +```ruby +require 'cpf-gen' + +CpfGen.cpf_gen # => ex.: "47844241055" (11 dígitos numéricos) + +CpfGen.cpf_gen(format: true) # => ex.: "005.265.352-88" + +CpfGen.cpf_gen(prefix: '528250911') # => ex.: "52825091138" +CpfGen.cpf_gen( # => ex.: "528.250.911-38" + prefix: '528250911', + format: true +) +``` + +As opções também podem ser passadas como `Hash`: + +```ruby +CpfGen.cpf_gen({ format: true, prefix: '528250911' }) +``` + +## Utilização + +Os pontos principais são o helper de módulo `CpfGen.cpf_gen`, a classe `CpfGen::CpfGenerator` e a classe de opções `CpfGen::CpfGeneratorOptions`. + +### Opções do gerador + +Todas as opções são opcionais: + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | Se truthy, retorna o CPF gerado no formato padrão (`000.000.000-00`). Valores não booleanos são convertidos (`false`, `''` e `0` viram `false`; demais valores viram truthy). | +| `prefix` | `String` | `''` | String inicial parcial (0–9 dígitos). Apenas dígitos são mantidos; os caracteres faltantes são gerados aleatoriamente e os dígitos verificadores são calculados. Prefixos com mais de 9 dígitos são truncados silenciosamente. | + +Regras do prefixo: a base (primeiros 9 dígitos) não pode ser toda zerada; 9 dígitos repetidos (ex.: `999999999`) não são permitidos. Prefixos com menos de 9 dígitos nunca são rejeitados por essas regras (ex.: `"00000000"` e `"11111111"` são permitidos). + +`nil` é aceito como argumento nomeado em `cpf_gen`, `CpfGenerator.new`, `CpfGenerator#generate` e `CpfGeneratorOptions.new`/`#set` — significa apenas "sem sobrescrita para esta opção". **Não** é aceito pelos setters de propriedade de `CpfGeneratorOptions` (`options.format = valor`, `options.prefix = valor`): chamar um setter com `nil` diretamente lança `CpfGen::TypeMismatchError`. Para redefinir uma propriedade ao seu valor padrão via setter, passe a constante literal, ex.: `options.format = CpfGen::CpfGeneratorOptions::DEFAULT_FORMAT`. + +### `CpfGen.cpf_gen` (helper) + +Gera uma string de CPF válida. Sem opções, retorna um CPF numérico de 11 dígitos. É um atalho para `CpfGen::CpfGenerator.new(...).generate`. + +- **`options`** (opcional): instância de `CpfGen::CpfGeneratorOptions`, `Hash` de chaves de opção ou `nil`. Veja [Opções do gerador](#opções-do-gerador). +- **`format`**, **`prefix`** (argumentos nomeados): Usados apenas quando `options` é omitido (`nil`). Passar `options` **e** qualquer um desses argumentos nomeados ao mesmo tempo gera `InvalidArgumentCombinationError` — as duas formas de passar opções nunca são mescladas entre si. + +### `CpfGen::CpfGenerator` (classe) + +Para padrões reutilizáveis ou sobrescritas por chamada, use a classe: + +```ruby +require 'cpf-gen' + +generator = CpfGen::CpfGenerator.new(format: true) + +generator.generate # => ex.: "005.265.352-88" +generator.generate(prefix: '123456') # sobrescrita apenas nesta chamada +generator.options # opções padrão atuais (CpfGen::CpfGeneratorOptions) +``` + +- **`initialize(options = nil, **keywords)`**: Opções padrão opcionais. Quando `options` é fornecido isoladamente (instância de `CpfGen::CpfGeneratorOptions` ou `Hash`), ele determina as opções padrão; uma instância de `CpfGen::CpfGeneratorOptions` é armazenada por referência (mutações posteriores afetam futuras chamadas de `generate` que não passarem opções por chamada), enquanto um `Hash` cria uma nova instância. Quando `options` é omitido (`nil`), as opções padrão são construídas exclusivamente a partir dos argumentos nomeados (`format:`, `prefix:`). Passar `options` junto com qualquer argumento nomeado não `nil` gera `InvalidArgumentCombinationError`, em vez de ignorar os argumentos nomeados silenciosamente. +- **`generate(options = nil, **keywords)`**: Retorna um CPF válido. `options` e os argumentos nomeados nunca são mesclados: um `options` fornecido isoladamente sobrescreve totalmente os padrões da instância nesta chamada; caso contrário, qualquer argumento nomeado fornecido sobrescreve os padrões da instância nesta chamada. Quando nenhum dos dois é fornecido, os padrões da instância são usados como estão. Os padrões da instância nunca são alterados por uma sobrescrita pontual. Passar `options` junto com qualquer argumento nomeado não `nil` gera `InvalidArgumentCombinationError`. +- **`options`**: Reader que retorna as opções padrão usadas quando não há opções por chamada (mesma instância usada internamente; mutá-la afeta futuras chamadas de `generate`). + +Opções padrão na instância; sobrescritas por chamada: + +```ruby +require 'cpf-gen' + +generator = CpfGen::CpfGenerator.new(format: true) + +generator.generate # CPF formatado +generator.generate(format: false) # somente nesta chamada: sem formato +generator.generate # volta ao padrão da instância +``` + +### `CpfGen::CpfGeneratorOptions` (classe) + +Armazena opções (`format`, `prefix`) com validação e suporte a mesclagem: + +```ruby +require 'cpf-gen' + +options = CpfGen::CpfGeneratorOptions.new( + prefix: '123456', + format: true +) +options.prefix # => "123456" +options.format # => true +options.set(format: false) # mescla e retorna self +options.all # => { format: false, prefix: "123456" } + +# Redefinir uma propriedade ao seu valor padrão exige a constante literal — +# um `nil` direto no setter lança TypeMismatchError: +options.format = CpfGen::CpfGeneratorOptions::DEFAULT_FORMAT +``` + +- **`initialize(*options, **keywords)`**: Cada argumento posicional `options` (um `Hash` ou outra instância de `CpfGen::CpfGeneratorOptions`) é combinado da esquerda para a direita — os últimos prevalecem — e então os argumentos nomeados (`format:`, `prefix:`) são aplicados por cima com a maior precedência. Em cada etapa, um valor `nil` para uma dada chave é ignorado em favor do que já foi resolvido. Qualquer opção ainda não resolvida recebe seu valor `DEFAULT_*`. +- **`format`**, **`prefix`**: Acessores com setters; `prefix` é validado (base zerada, dígitos repetidos). Os setters **nunca aceitam `nil`** — passe a constante `DEFAULT_*` correspondente (ex.: `CpfGeneratorOptions::DEFAULT_PREFIX`) para redefinir uma propriedade explicitamente. +- **`set(*options, **keywords)`**: Atualiza várias opções de uma vez, usando a mesma resolução de `initialize` (combinação seguida de argumentos nomeados, ignorando `nil`). Qualquer opção não resolvida após a combinação mantém seu valor **atual** na instância (uma atualização parcial, não uma reinicialização). Retorna `self`. +- **`all`**: Cópia superficial em `Hash` das opções atuais (`:format`, `:prefix`). + +## API + +### Exportações + +Após `require 'cpf-gen'`: + +- **`CpfGen.cpf_gen`**: `(options = nil, **keywords) -> String` — helper de conveniência. +- **`CpfGen::CpfGenerator`**: Classe para gerar CPF com opções padrão e sobrescritas por chamada. +- **`CpfGen::CpfGeneratorOptions`**: Classe que armazena opções com validação e mesclagem. +- **`CpfGen::CPF_LENGTH`**: `11` (constante). +- **`CpfGen::CPF_PREFIX_MAX_LENGTH`**: `9` (constante). +- **`CpfGen::VERSION`**: string da versão da gem. +- **Erros**: `CpfGen::Error`, `CpfGen::DomainError`, `CpfGen::InvalidArgumentCombinationError`, `CpfGen::TypeMismatchError`, `CpfGen::ValidationError`. + +### Tratamento de erros + +Os erros se dividem em duas categorias: + +| Categoria | Significado | +|---|---| +| **Uso incorreto da API** | O chamador usou a biblioteca de forma incorreta (tipo errado para uma opção, ou combinação inválida de argumentos). | +| **Erro de domínio** | A chamada estava estruturalmente correta, mas um valor viola uma regra de negócio (`prefix` inválido). | + +Todo erro customizado inclui o módulo marcador `CpfGen::Error`. Falhas de domínio (`ValidationError`) herdam de `CpfGen::DomainError` (`RangeError`). + +**Importante:** passar ao mesmo tempo uma instância/`Hash` de `options` e argumentos nomeados levanta `InvalidArgumentCombinationError`. + +#### Resumo + +| Classe | Herda de | Categoria | Condição de disparo | +|---|---|---|---| +| `CpfGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e argumentos nomeados passados ao mesmo tempo | +| `CpfGen::TypeMismatchError` | `TypeError` (+ `include Error`) | Uso incorreto da API | Uma opção do gerador tem o tipo de dado incorreto | +| `CpfGen::ValidationError` | `CpfGen::DomainError` | Erro de domínio | `prefix` inelegível (base zerada ou 9 dígitos repetidos) | + +#### `CpfGen::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 diretamente; incluído por todo erro customizado que a biblioteca levanta. +- **Exemplo:** N/A +- **Como resgatar:** + +```ruby +rescue CpfGen::Error + # tudo o que esta biblioteca levanta +``` + +#### `CpfGen::DomainError` + +- **Herança:** `CpfGen::DomainError < RangeError` (inclui `CpfGen::Error`) +- **Categoria:** Erro de domínio — ancestral de todas as falhas de domínio. +- **Quando é levantado:** Não é levantado diretamente; prefira uma subclasse folha. +- **Exemplo:** Prefira `raise CpfGen::ValidationError` a levantar `DomainError` diretamente. +- **Como resgatar:** + +```ruby +rescue CpfGen::DomainError + # ValidationError e outras subclasses de DomainError +``` + +#### `CpfGen::TypeMismatchError` + +- **Herança:** `CpfGen::TypeMismatchError < TypeError` (inclui `CpfGen::Error`) +- **Categoria:** Uso incorreto da API — o chamador passou um valor do tipo errado. +- **Quando é levantado:** Levantado quando uma opção do gerador (`format` ou `prefix`) tem o tipo de runtime incorreto. +- **Exemplo:** + +```ruby +CpfGen.cpf_gen(prefix: 123) # levanta CpfGen::TypeMismatchError +``` + +- **Como resgatar:** + +```ruby +rescue CpfGen::TypeMismatchError + # violação de contrato de tipo desta biblioteca + +rescue TypeError + # erros nativos de tipo, incluindo TypeMismatchError desta biblioteca +``` + +#### `CpfGen::InvalidArgumentCombinationError` + +- **Herança:** `CpfGen::InvalidArgumentCombinationError < ArgumentError` (inclui `CpfGen::Error`) +- **Categoria:** Uso incorreto da API — o chamador misturou padrões de argumentos mutuamente exclusivos. +- **Quando é levantado:** Levantado quando `CpfGenerator.new`, `#generate` ou `cpf_gen` recebe ao mesmo tempo um argumento `options` (instância ou `Hash`) e qualquer argumento nomeado não-`nil`. +- **Exemplo:** + +```ruby +begin + CpfGen::CpfGenerator.new({ format: true }, prefix: '123') +rescue CpfGen::InvalidArgumentCombinationError => e + puts e.message + # Pass either an options instance/Hash to `options`, or keyword arguments (format:, prefix:), not both. +end +``` + +- **Como resgatar:** + +```ruby +rescue CpfGen::InvalidArgumentCombinationError + # combinação inválida de argumentos desta biblioteca + +rescue ArgumentError + # erros nativos de argumento, incluindo InvalidArgumentCombinationError desta biblioteca +``` + +#### `CpfGen::ValidationError` + +- **Herança:** `CpfGen::ValidationError < CpfGen::DomainError < RangeError` (inclui `CpfGen::Error`) +- **Categoria:** Erro de domínio — um valor falha uma regra de domínio que não é numérica nem de tamanho. +- **Quando é levantado:** Levantado quando `prefix` é inelegível (base zerada `"000000000"`, ou 9 dígitos repetidos como `"999999999"`). +- **Exemplo:** + +```ruby +CpfGen.cpf_gen(prefix: '000000000') # levanta CpfGen::ValidationError +CpfGen.cpf_gen(prefix: '999999999') # levanta CpfGen::ValidationError +``` + +- **Como resgatar:** + +```ruby +rescue CpfGen::ValidationError + # esta falha exata de validação de domínio + +rescue CpfGen::DomainError + # falhas de domínio com raiz em RangeError desta biblioteca +``` + +#### Granularidade de rescue + +```ruby +# 1) Classe nativa — captura uso incorreto de tipo desta biblioteca (e outros TypeErrors). +rescue TypeError + # CpfGen::TypeMismatchError e qualquer outro TypeError (da biblioteca ou não) + +# 2) CpfGen::DomainError — captura violações de regra de negócio sob DomainError. +rescue CpfGen::DomainError + # CpfGen::ValidationError e outras subclasses de DomainError + +# 3) CpfGen::Error — captura tudo o que a biblioteca levanta. +rescue CpfGen::Error + # todo erro customizado que inclui CpfGen::Error + +# 4) Classe folha específica — captura apenas aquele modo de falha. +rescue CpfGen::ValidationError + # apenas CpfGen::ValidationError +``` + +Atributos relevantes: + +- `TypeMismatchError`: `option_name`, `actual_input`, `actual_type`, `expected_type` +- `ValidationError`: `option_name`, `actual_input`, `reason` (falhas de prefixo); `expected_values` é sempre `nil` para CPF + +Os setters de propriedade nunca aceitam `nil` diretamente — passe a constante `DEFAULT_*` correspondente para redefinir: + +```ruby +options = CpfGen::CpfGeneratorOptions.new +begin + options.prefix = nil +rescue CpfGen::TypeMismatchError => e + puts e.message + # CPF generator option "prefix" must be of type string. Got nil. +end + +options.prefix = CpfGen::CpfGeneratorOptions::DEFAULT_PREFIX # redefinição explícita +``` + +Falhas de cálculo de dígitos verificadores pelo `cpf-dv` são tratadas internamente com nova tentativa de geração usando as mesmas opções resolvidas; em operação normal não são propagadas ao chamador. + +### Outros recursos disponíveis + +- **`CpfGen::CpfGeneratorOptions::CPF_LENGTH`**: `11`. +- **`CpfGen::CpfGeneratorOptions::CPF_PREFIX_MAX_LENGTH`**: `9`. +- **`CpfGen::CpfGeneratorOptions::DEFAULT_FORMAT`**, **`DEFAULT_PREFIX`**: Constantes de padrão no nível da classe. + +## 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 no 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 + +Veja o [CHANGELOG](./CHANGELOG.md) para histórico de versões e alterações. + +--- + +Feito com ❤️ por [Lacus Solutions](https://github.com/LacusSolutions) From 1fe7cc00f3cb9d8e1e43d04fcde276e570824e32 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:41:33 -0300 Subject: [PATCH 11/15] docs(cpf-gen): reinforce arguments combination rule Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/23#discussion_r3625478116. 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-gen/README.md | 6 +++--- packages/cpf-gen/README.pt.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cpf-gen/README.md b/packages/cpf-gen/README.md index d5d6c1a..605e810 100644 --- a/packages/cpf-gen/README.md +++ b/packages/cpf-gen/README.md @@ -93,7 +93,7 @@ Prefix rules: the base (first 9 digits) cannot be all zeros; 9 repeated digits ( Generates a valid CPF string. With no options, returns an 11-digit numeric CPF. This is a convenience wrapper around `CpfGen::CpfGenerator.new(...).generate`. - **`options`** (optional): `CpfGen::CpfGeneratorOptions` instance, a `Hash` of option keys, or `nil`. See [Generator options](#generator-options). -- **`format`**, **`prefix`** (keyword arguments): Only used when `options` is omitted (`nil`). Passing `options` **and** any of these keywords at the same time raises `InvalidArgumentCombinationError` — the two ways of passing options are never merged together. +- **`format`**, **`prefix`** (keyword arguments): Only used when `options` is omitted (`nil`). Passing `options` **and** any non-`nil` of these keywords at the same time raises `InvalidArgumentCombinationError` — the two ways of passing options are never merged. ### `CpfGen::CpfGenerator` (class) @@ -176,13 +176,13 @@ Errors fall into two categories: Every custom error includes the `CpfGen::Error` marker module. Domain failures (`ValidationError`) inherit from `CpfGen::DomainError` (`RangeError`). -**Important:** passing both an `options` instance/`Hash` and keyword arguments raises `InvalidArgumentCombinationError`. +**Important:** passing both an `options` instance/`Hash` and any non-`nil` keyword argument raises `InvalidArgumentCombinationError`. #### Summary | Class | Inherits from | Category | Trigger condition | |---|---|---|---| -| `CpfGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and keyword arguments are passed at once | +| `CpfGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and any non-`nil` keyword argument are passed at once | | `CpfGen::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | A generator option has the wrong data type | | `CpfGen::ValidationError` | `CpfGen::DomainError` | Domain error | `prefix` is ineligible (zeroed base ID or 9 repeated digits) | diff --git a/packages/cpf-gen/README.pt.md b/packages/cpf-gen/README.pt.md index fac7cc5..7de3618 100644 --- a/packages/cpf-gen/README.pt.md +++ b/packages/cpf-gen/README.pt.md @@ -78,7 +78,7 @@ Regras do prefixo: a base (primeiros 9 dígitos) não pode ser toda zerada; 9 d Gera uma string de CPF válida. Sem opções, retorna um CPF numérico de 11 dígitos. É um atalho para `CpfGen::CpfGenerator.new(...).generate`. - **`options`** (opcional): instância de `CpfGen::CpfGeneratorOptions`, `Hash` de chaves de opção ou `nil`. Veja [Opções do gerador](#opções-do-gerador). -- **`format`**, **`prefix`** (argumentos nomeados): Usados apenas quando `options` é omitido (`nil`). Passar `options` **e** qualquer um desses argumentos nomeados ao mesmo tempo gera `InvalidArgumentCombinationError` — as duas formas de passar opções nunca são mescladas entre si. +- **`format`**, **`prefix`** (argumentos nomeados): Usados apenas quando `options` é omitido (`nil`). Passar `options` **e** qualquer um desses argumentos nomeados não-`nil` ao mesmo tempo gera `InvalidArgumentCombinationError` — as duas formas de passar opções nunca são mescladas entre si. ### `CpfGen::CpfGenerator` (classe) @@ -161,13 +161,13 @@ Os erros se dividem em duas categorias: Todo erro customizado inclui o módulo marcador `CpfGen::Error`. Falhas de domínio (`ValidationError`) herdam de `CpfGen::DomainError` (`RangeError`). -**Importante:** passar ao mesmo tempo uma instância/`Hash` de `options` e argumentos nomeados levanta `InvalidArgumentCombinationError`. +**Importante:** passar ao mesmo tempo uma instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` levanta `InvalidArgumentCombinationError`. #### Resumo | Classe | Herda de | Categoria | Condição de disparo | |---|---|---|---| -| `CpfGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e argumentos nomeados passados ao mesmo tempo | +| `CpfGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` passados ao mesmo tempo | | `CpfGen::TypeMismatchError` | `TypeError` (+ `include Error`) | Uso incorreto da API | Uma opção do gerador tem o tipo de dado incorreto | | `CpfGen::ValidationError` | `CpfGen::DomainError` | Erro de domínio | `prefix` inelegível (base zerada ou 9 dígitos repetidos) | From a29fa98f21722f7ce8b9aaaad9ff47b600a942f3 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:45:17 -0300 Subject: [PATCH 12/15] docs(cnpj-fmt): reinforce arguments combination rule Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/23#discussion_r3625478116. 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 | 6 +++--- packages/cnpj-fmt/README.pt.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cnpj-fmt/README.md b/packages/cnpj-fmt/README.md index 400a6dc..27e715f 100644 --- a/packages/cnpj-fmt/README.md +++ b/packages/cnpj-fmt/README.md @@ -112,7 +112,7 @@ Holds all formatter settings, with validation and merge support. Exposes propert ### Functional helper -`CnpjFmt.cnpj_fmt` builds a new `CnpjFmt::CnpjFormatter` from the same constructor parameters and calls `format(cnpj_input)` once. Pass either keyword arguments **or** a `Hash`/`CnpjFmt::CnpjFormatterOptions` instance for options — not both (passing both raises `InvalidArgumentCombinationError`): +`CnpjFmt.cnpj_fmt` builds a new `CnpjFmt::CnpjFormatter` from the same constructor parameters and calls `format(cnpj_input)` once. Pass either keyword arguments **or** a `Hash`/`CnpjFmt::CnpjFormatterOptions` instance for options — not both (passing `options` with any non-`nil` keyword raises `InvalidArgumentCombinationError`): ```ruby require 'cnpj-fmt' @@ -237,13 +237,13 @@ Errors fall into two categories: Every custom error includes the `CnpjFmt::Error` marker module. Domain failures (`InvalidLengthError`, `OutOfRangeError`, `ValidationError`) inherit from `CnpjFmt::DomainError` (`RangeError`). -**Important:** length failures are **constructed as `InvalidLengthError` and passed to `on_fail` as a `DomainError`**, not raised from `format` / `cnpj_fmt`. Passing both an `options` instance/`Hash` and keyword arguments raises `InvalidArgumentCombinationError`. +**Important:** length failures are **constructed as `InvalidLengthError` and passed to `on_fail` as a `DomainError`**, not raised from `format` / `cnpj_fmt`. Passing both an `options` instance/`Hash` and any non-`nil` keyword argument raises `InvalidArgumentCombinationError`. #### Summary | Class | Inherits from | Category | Trigger condition | |---|---|---|---| -| `CnpjFmt::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and keyword arguments are passed at once | +| `CnpjFmt::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and any non-`nil` keyword argument are passed at once | | `CnpjFmt::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | CNPJ input or option has the wrong data type | | `CnpjFmt::InvalidLengthError` | `CnpjFmt::DomainError` | Domain error | Sanitized length is not exactly 14 (passed to `on_fail` as `DomainError`) | | `CnpjFmt::OutOfRangeError` | `CnpjFmt::DomainError` | Domain error | `hidden_start` / `hidden_end` outside `0`–`13` | diff --git a/packages/cnpj-fmt/README.pt.md b/packages/cnpj-fmt/README.pt.md index c28e0b0..960327f 100644 --- a/packages/cnpj-fmt/README.pt.md +++ b/packages/cnpj-fmt/README.pt.md @@ -99,7 +99,7 @@ Armazena todas as configurações do formatador, com validação e suporte a mes ### Helper funcional -`CnpjFmt.cnpj_fmt` instancia um novo `CnpjFmt::CnpjFormatter` com os mesmos parâmetros do construtor e chama `format(cnpj_input)` uma vez. Passe argumentos nomeados **ou** um `Hash`/instância de `CnpjFmt::CnpjFormatterOptions` para as opções — não ambos (passar ambos lança `InvalidArgumentCombinationError`): +`CnpjFmt.cnpj_fmt` instancia um novo `CnpjFmt::CnpjFormatter` com os mesmos parâmetros do construtor e chama `format(cnpj_input)` uma vez. Passe argumentos nomeados **ou** um `Hash`/instância de `CnpjFmt::CnpjFormatterOptions` para as opções — não ambos (passar `options` com qualquer argumento nomeado não-`nil` lança `InvalidArgumentCombinationError`): ```ruby require 'cnpj-fmt' @@ -224,13 +224,13 @@ Os erros se dividem em duas categorias: Todo erro customizado inclui o módulo marcador `CnpjFmt::Error`. Falhas de domínio (`InvalidLengthError`, `OutOfRangeError`, `ValidationError`) herdam de `CnpjFmt::DomainError` (`RangeError`). -**Importante:** falhas de tamanho são **construídas como `InvalidLengthError` e passadas ao `on_fail` como `DomainError`**, não levantadas por `format` / `cnpj_fmt`. Passar ao mesmo tempo um argumento `options` (instância/`Hash`) e argumentos nomeados lança `InvalidArgumentCombinationError`. +**Importante:** falhas de tamanho são **construídas como `InvalidLengthError` e passadas ao `on_fail` como `DomainError`**, não levantadas por `format` / `cnpj_fmt`. Passar ao mesmo tempo um argumento `options` (instância/`Hash`) e qualquer argumento nomeado não-`nil` lança `InvalidArgumentCombinationError`. #### Resumo | Classe | Herda de | Categoria | Condição de disparo | |---|---|---|---| -| `CnpjFmt::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e argumentos nomeados passados ao mesmo tempo | +| `CnpjFmt::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` passados ao mesmo tempo | | `CnpjFmt::TypeMismatchError` | `TypeError` (+ `include Error`) | Uso incorreto da API | Entrada de CNPJ ou opção com tipo de dado incorreto | | `CnpjFmt::InvalidLengthError` | `CnpjFmt::DomainError` | Erro de domínio | Tamanho após sanitização não é exatamente 14 (passado ao `on_fail` como `DomainError`) | | `CnpjFmt::OutOfRangeError` | `CnpjFmt::DomainError` | Erro de domínio | `hidden_start` / `hidden_end` fora de `0`–`13` | From 8483e2e587a4cfb5b6e389a2b78a4bb8cae73aab Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:45:29 -0300 Subject: [PATCH 13/15] docs(cnpj-gen): reinforce arguments combination rule Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/23#discussion_r3625478116. 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-gen/README.md | 6 +++--- packages/cnpj-gen/README.pt.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cnpj-gen/README.md b/packages/cnpj-gen/README.md index 8a8bf27..8d4b23d 100644 --- a/packages/cnpj-gen/README.md +++ b/packages/cnpj-gen/README.md @@ -99,7 +99,7 @@ Prefix rules: base ID (first 8 chars) and branch ID (chars 9–12) cannot be all Generates a valid CNPJ string. With no options, returns a 14-character alphanumeric CNPJ. This is a convenience wrapper around `CnpjGen::CnpjGenerator.new(...).generate`. - **`options`** (optional): `CnpjGen::CnpjGeneratorOptions` instance, a `Hash` of option keys, or `nil`. See [Generator options](#generator-options). -- **`format`**, **`prefix`**, **`type`** (keyword arguments): Only used when `options` is omitted (`nil`). Passing `options` **and** any of these keywords at the same time raises `InvalidArgumentCombinationError` — the two ways of passing options are never merged together. +- **`format`**, **`prefix`**, **`type`** (keyword arguments): Only used when `options` is omitted (`nil`). Passing `options` **and** any non-`nil` of these keywords at the same time raises `InvalidArgumentCombinationError` — the two ways of passing options are never merged. ### `CnpjGen::CnpjGenerator` (class) @@ -185,13 +185,13 @@ Errors fall into two categories: Every custom error includes the `CnpjGen::Error` marker module. Domain failures (`ValidationError`) inherit from `CnpjGen::DomainError` (`RangeError`). -**Important:** passing both an `options` instance/`Hash` and keyword arguments raises `InvalidArgumentCombinationError`. +**Important:** passing both an `options` instance/`Hash` and any non-`nil` keyword argument raises `InvalidArgumentCombinationError`. #### Summary | Class | Inherits from | Category | Trigger condition | |---|---|---|---| -| `CnpjGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and keyword arguments are passed at once | +| `CnpjGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and any non-`nil` keyword argument are passed at once | | `CnpjGen::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | A generator option has the wrong data type | | `CnpjGen::ValidationError` | `CnpjGen::DomainError` | Domain error | `prefix` is ineligible, or `type` is not one of the allowed values | diff --git a/packages/cnpj-gen/README.pt.md b/packages/cnpj-gen/README.pt.md index 53f59a5..d0ed602 100644 --- a/packages/cnpj-gen/README.pt.md +++ b/packages/cnpj-gen/README.pt.md @@ -84,7 +84,7 @@ Regras do prefixo: a base (primeiros 8 caracteres) e a filial (caracteres 9–12 Gera uma string de CNPJ válida. Sem opções, retorna um CNPJ alfanumérico de 14 caracteres. É um atalho para `CnpjGen::CnpjGenerator.new(...).generate`. - **`options`** (opcional): instância de `CnpjGen::CnpjGeneratorOptions`, `Hash` de chaves de opção ou `nil`. Veja [Opções do gerador](#opções-do-gerador). -- **`format`**, **`prefix`**, **`type`** (argumentos nomeados): Usados apenas quando `options` é omitido (`nil`). Passar `options` **e** qualquer um desses argumentos nomeados ao mesmo tempo gera `InvalidArgumentCombinationError` — as duas formas de passar opções nunca são mescladas entre si. +- **`format`**, **`prefix`**, **`type`** (argumentos nomeados): Usados apenas quando `options` é omitido (`nil`). Passar `options` **e** qualquer um desses argumentos nomeados não-`nil` ao mesmo tempo gera `InvalidArgumentCombinationError` — as duas formas de passar opções nunca são mescladas entre si. ### `CnpjGen::CnpjGenerator` (classe) @@ -170,13 +170,13 @@ Os erros se dividem em duas categorias: Todo erro customizado inclui o módulo marcador `CnpjGen::Error`. Falhas de domínio (`ValidationError`) herdam de `CnpjGen::DomainError` (`RangeError`). -**Importante:** passar ao mesmo tempo uma instância/`Hash` de `options` e argumentos nomeados levanta `InvalidArgumentCombinationError`. +**Importante:** passar ao mesmo tempo uma instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` levanta `InvalidArgumentCombinationError`. #### Resumo | Classe | Herda de | Categoria | Condição de disparo | |---|---|---|---| -| `CnpjGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e argumentos nomeados passados ao mesmo tempo | +| `CnpjGen::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` passados ao mesmo tempo | | `CnpjGen::TypeMismatchError` | `TypeError` (+ `include Error`) | Uso incorreto da API | Uma opção do gerador tem o tipo de dado incorreto | | `CnpjGen::ValidationError` | `CnpjGen::DomainError` | Erro de domínio | `prefix` inelegível, ou `type` fora dos valores permitidos | From 59328cd176676f660ff6aff3c7f2a6d0a0145c8f Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:45:41 -0300 Subject: [PATCH 14/15] docs(cnpj-val): reinforce arguments combination rule Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/23#discussion_r3625478116. 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 | 6 +++--- packages/cnpj-val/README.pt.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cnpj-val/README.md b/packages/cnpj-val/README.md index 8cf17a0..88fa034 100644 --- a/packages/cnpj-val/README.md +++ b/packages/cnpj-val/README.md @@ -136,7 +136,7 @@ options.all # => frozen snapshot of current options ### Functional helper -`CnpjVal.cnpj_val` builds a new `CnpjVal::CnpjValidator` from the same constructor parameters and calls `is_valid(cnpj_input)` once. Pass either keyword arguments **or** a `Hash`/`CnpjVal::CnpjValidatorOptions` instance for options — not both (passing both raises `InvalidArgumentCombinationError`): +`CnpjVal.cnpj_val` builds a new `CnpjVal::CnpjValidator` from the same constructor parameters and calls `is_valid(cnpj_input)` once. Pass either keyword arguments **or** a `Hash`/`CnpjVal::CnpjValidatorOptions` instance for options — not both (passing `options` with any non-`nil` keyword raises `InvalidArgumentCombinationError`): ```ruby require 'cnpj-val' @@ -195,13 +195,13 @@ Errors fall into two categories: Every custom error includes the `CnpjVal::Error` marker module. Domain failures (`ValidationError`) inherit from `CnpjVal::DomainError` (`RangeError`). Invalid CNPJ data returns `false` (it does not raise). -**Important:** passing both an `options` instance/`Hash` and keyword arguments raises `InvalidArgumentCombinationError`. +**Important:** passing both an `options` instance/`Hash` and any non-`nil` keyword argument raises `InvalidArgumentCombinationError`. #### Summary | Class | Inherits from | Category | Trigger condition | |---|---|---|---| -| `CnpjVal::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and keyword arguments are passed at once | +| `CnpjVal::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and any non-`nil` keyword argument are passed at once | | `CnpjVal::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | CNPJ input or option has the wrong data type | | `CnpjVal::ValidationError` | `CnpjVal::DomainError` | Domain error | `type` is not one of the allowed values | diff --git a/packages/cnpj-val/README.pt.md b/packages/cnpj-val/README.pt.md index f06b1c9..d4de73f 100644 --- a/packages/cnpj-val/README.pt.md +++ b/packages/cnpj-val/README.pt.md @@ -123,7 +123,7 @@ options.all # => snapshot congelado das opções atuais ### Helper funcional -`CnpjVal.cnpj_val` instancia um novo `CnpjVal::CnpjValidator` com os mesmos parâmetros do construtor e chama `is_valid(cnpj_input)` uma vez. Passe argumentos nomeados **ou** um `Hash`/instância de `CnpjVal::CnpjValidatorOptions` para as opções — não ambos (passar ambos lança `InvalidArgumentCombinationError`): +`CnpjVal.cnpj_val` instancia um novo `CnpjVal::CnpjValidator` com os mesmos parâmetros do construtor e chama `is_valid(cnpj_input)` uma vez. Passe argumentos nomeados **ou** um `Hash`/instância de `CnpjVal::CnpjValidatorOptions` para as opções — não ambos (passar `options` com qualquer argumento nomeado não-`nil` lança `InvalidArgumentCombinationError`): ```ruby require 'cnpj-val' @@ -182,13 +182,13 @@ Os erros se dividem em duas categorias: Todo erro customizado inclui o módulo marcador `CnpjVal::Error`. Falhas de domínio (`ValidationError`) herdam de `CnpjVal::DomainError` (`RangeError`). Dados de CNPJ inválidos retornam `false` (não levantam erro). -**Importante:** passar ao mesmo tempo uma instância/`Hash` de `options` e argumentos nomeados levanta `InvalidArgumentCombinationError`. +**Importante:** passar ao mesmo tempo uma instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` levanta `InvalidArgumentCombinationError`. #### Resumo | Classe | Herda de | Categoria | Condição de disparo | |---|---|---|---| -| `CnpjVal::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e argumentos nomeados passados ao mesmo tempo | +| `CnpjVal::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` passados ao mesmo tempo | | `CnpjVal::TypeMismatchError` | `TypeError` (+ `include Error`) | Uso incorreto da API | Entrada CNPJ ou opção com tipo de dado incorreto | | `CnpjVal::ValidationError` | `CnpjVal::DomainError` | Erro de domínio | `type` fora dos valores permitidos | From 5a4a8d33182872dda65c1ca0f3a4eedd44030ae1 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Tue, 21 Jul 2026 17:46:01 -0300 Subject: [PATCH 15/15] docs(cpf-fmt): reinforce arguments combination rule Adjustment as per @coderabbitai review comment at https://github.com/LacusSolutions/br-utils-ruby/pull/23#discussion_r3625478116. 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 | 6 +++--- packages/cpf-fmt/README.pt.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cpf-fmt/README.md b/packages/cpf-fmt/README.md index 60ec2de..2a2fd0f 100644 --- a/packages/cpf-fmt/README.md +++ b/packages/cpf-fmt/README.md @@ -108,7 +108,7 @@ Holds all formatter settings, with validation and merge support. Exposes propert ### Functional helper -`CpfFmt.cpf_fmt` builds a new `CpfFmt::CpfFormatter` from the same constructor parameters and calls `format(cpf_input)` once. Pass either keyword arguments **or** a `Hash`/`CpfFmt::CpfFormatterOptions` instance for options — not both (passing both raises `InvalidArgumentCombinationError`): +`CpfFmt.cpf_fmt` builds a new `CpfFmt::CpfFormatter` from the same constructor parameters and calls `format(cpf_input)` once. Pass either keyword arguments **or** a `Hash`/`CpfFmt::CpfFormatterOptions` instance for options — not both (passing `options` with any non-`nil` keyword raises `InvalidArgumentCombinationError`): ```ruby require 'cpf-fmt' @@ -228,13 +228,13 @@ Errors fall into two categories: Every custom error includes the `CpfFmt::Error` marker module. Domain failures (`InvalidLengthError`, `OutOfRangeError`, `ValidationError`) inherit from `CpfFmt::DomainError` (`RangeError`). -**Important:** length failures are **constructed as `InvalidLengthError` and passed to `on_fail` as a `DomainError`**, not raised from `format` / `cpf_fmt`. Passing both an `options` instance/`Hash` and keyword arguments raises `InvalidArgumentCombinationError`. +**Important:** length failures are **constructed as `InvalidLengthError` and passed to `on_fail` as a `DomainError`**, not raised from `format` / `cpf_fmt`. Passing both an `options` instance/`Hash` and any non-`nil` keyword argument raises `InvalidArgumentCombinationError`. #### Summary | Class | Inherits from | Category | Trigger condition | |---|---|---|---| -| `CpfFmt::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and keyword arguments are passed at once | +| `CpfFmt::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | API misuse | Both an `options` instance/`Hash` and any non-`nil` keyword argument are passed at once | | `CpfFmt::TypeMismatchError` | `TypeError` (+ `include Error`) | API misuse | CPF input or option has the wrong data type | | `CpfFmt::InvalidLengthError` | `CpfFmt::DomainError` | Domain error | Sanitized length is not exactly 11 (passed to `on_fail` as `DomainError`) | | `CpfFmt::OutOfRangeError` | `CpfFmt::DomainError` | Domain error | `hidden_start` / `hidden_end` outside `0`–`10` | diff --git a/packages/cpf-fmt/README.pt.md b/packages/cpf-fmt/README.pt.md index f60014c..ec6c3d4 100644 --- a/packages/cpf-fmt/README.pt.md +++ b/packages/cpf-fmt/README.pt.md @@ -95,7 +95,7 @@ Armazena todas as configurações do formatador, com validação e suporte a mes ### Helper funcional -`CpfFmt.cpf_fmt` instancia um novo `CpfFmt::CpfFormatter` com os mesmos parâmetros do construtor e chama `format(cpf_input)` uma vez. Passe argumentos nomeados **ou** um `Hash`/instância de `CpfFmt::CpfFormatterOptions` para as opções — não ambos (passar ambos lança `InvalidArgumentCombinationError`): +`CpfFmt.cpf_fmt` instancia um novo `CpfFmt::CpfFormatter` com os mesmos parâmetros do construtor e chama `format(cpf_input)` uma vez. Passe argumentos nomeados **ou** um `Hash`/instância de `CpfFmt::CpfFormatterOptions` para as opções — não ambos (passar `options` com qualquer argumento nomeado não-`nil` lança `InvalidArgumentCombinationError`): ```ruby require 'cpf-fmt' @@ -215,13 +215,13 @@ Os erros se dividem em duas categorias: Todo erro customizado inclui o módulo marcador `CpfFmt::Error`. Falhas de domínio (`InvalidLengthError`, `OutOfRangeError`, `ValidationError`) herdam de `CpfFmt::DomainError` (`RangeError`). -**Importante:** falhas de tamanho são **construídas como `InvalidLengthError` e passadas ao `on_fail` como `DomainError`**, não levantadas por `format` / `cpf_fmt`. Passar ao mesmo tempo um argumento `options` (instância/`Hash`) e argumentos nomeados lança `InvalidArgumentCombinationError`. +**Importante:** falhas de tamanho são **construídas como `InvalidLengthError` e passadas ao `on_fail` como `DomainError`**, não levantadas por `format` / `cpf_fmt`. Passar ao mesmo tempo um argumento `options` (instância/`Hash`) e qualquer argumento nomeado não-`nil` lança `InvalidArgumentCombinationError`. #### Resumo | Classe | Herda de | Categoria | Condição de disparo | |---|---|---|---| -| `CpfFmt::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e argumentos nomeados passados ao mesmo tempo | +| `CpfFmt::InvalidArgumentCombinationError` | `ArgumentError` (+ `include Error`) | Uso incorreto da API | Instância/`Hash` de `options` e qualquer argumento nomeado não-`nil` passados ao mesmo tempo | | `CpfFmt::TypeMismatchError` | `TypeError` (+ `include Error`) | Uso incorreto da API | Entrada de CPF ou opção com tipo de dado incorreto | | `CpfFmt::InvalidLengthError` | `CpfFmt::DomainError` | Erro de domínio | Tamanho após sanitização não é exatamente 11 (passado ao `on_fail` como `DomainError`) | | `CpfFmt::OutOfRangeError` | `CpfFmt::DomainError` | Erro de domínio | `hidden_start` / `hidden_end` fora de `0`–`10` |