From 0dcb592a53545538ea39362d73525a26cfb148ba Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Mon, 10 Aug 2026 17:29:19 -0300 Subject: [PATCH 1/7] chore(br-utils): set package metadata Co-authored-by: Claude Opus 5 Co-authored-by: Cursor Agent --- packages/br-utilities/Gemfile | 3 --- packages/br-utilities/br-utilities.gemspec | 9 +++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/br-utilities/Gemfile b/packages/br-utilities/Gemfile index 2d266a0..c6122eb 100644 --- a/packages/br-utilities/Gemfile +++ b/packages/br-utilities/Gemfile @@ -4,9 +4,6 @@ source 'https://rubygems.org' gemspec -gem 'cnpj-utilities', path: '../cnpj-utilities' -gem 'cpf-utilities', path: '../cpf-utilities' - group :test do gem 'rake', '~> 13.2' gem 'rspec', '~> 3.13' diff --git a/packages/br-utilities/br-utilities.gemspec b/packages/br-utilities/br-utilities.gemspec index addc19d..60438d3 100644 --- a/packages/br-utilities/br-utilities.gemspec +++ b/packages/br-utilities/br-utilities.gemspec @@ -6,8 +6,9 @@ Gem::Specification.new do |spec| spec.name = 'br-utilities' spec.version = BrUtils::VERSION spec.authors = ['Julio L. Muller'] - spec.summary = 'Brazilian data utilities: CPF, CNPJ, and more' - spec.description = 'Unified API for CPF/CNPJ format, generate, validate (Brazilian IDs).' + spec.email = ['juliolmuller@outlook.com'] + spec.summary = 'Utilities to deal with Brazilian-related data' + spec.description = 'Utilities to deal with Brazilian-related data' spec.homepage = 'https://github.com/LacusSolutions/br-utils-ruby' spec.license = 'MIT' spec.required_ruby_version = '>= 3.1' @@ -15,6 +16,6 @@ Gem::Specification.new do |spec| spec.metadata['rubygems_mfa_required'] = 'true' spec.files = Dir['src/**/*'] + ['LICENSE', 'README.md', 'README.pt.md', 'CHANGELOG.md'] spec.require_paths = ['src'] - spec.add_dependency 'cnpj-utilities', '>= 0' - spec.add_dependency 'cpf-utilities', '>= 0' + spec.add_dependency 'cnpj-utilities', '>= 1.0.0', '< 1.1.0' + spec.add_dependency 'cpf-utilities', '>= 1.0.0', '< 1.1.0' end From f1b90d8452e85292f1daddfc2871a0e32e149419 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Mon, 10 Aug 2026 17:38:28 -0300 Subject: [PATCH 2/7] feat(br-utils): introduce unified API for BRF utilities Co-authored-by: Claude Opus 5 Co-authored-by: Cursor Agent --- packages/br-utilities/src/br-utilities.rb | 36 ++- .../br-utilities/src/br-utilities/br_utils.rb | 268 ++++++++++++++++++ .../br-utilities/src/br-utilities/cnpj_fmt.rb | 18 ++ .../br-utilities/src/br-utilities/cnpj_gen.rb | 18 ++ .../src/br-utilities/cnpj_utils.rb | 8 + .../br-utilities/src/br-utilities/cnpj_val.rb | 18 ++ .../br-utilities/src/br-utilities/cpf_fmt.rb | 18 ++ .../br-utilities/src/br-utilities/cpf_gen.rb | 18 ++ .../src/br-utilities/cpf_utils.rb | 8 + .../br-utilities/src/br-utilities/cpf_val.rb | 14 + .../br-utilities/src/br-utilities/errors.rb | 23 ++ .../br-utilities/src/br-utilities/version.rb | 6 + 12 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 packages/br-utilities/src/br-utilities/br_utils.rb create mode 100644 packages/br-utilities/src/br-utilities/cnpj_fmt.rb create mode 100644 packages/br-utilities/src/br-utilities/cnpj_gen.rb create mode 100644 packages/br-utilities/src/br-utilities/cnpj_utils.rb create mode 100644 packages/br-utilities/src/br-utilities/cnpj_val.rb create mode 100644 packages/br-utilities/src/br-utilities/cpf_fmt.rb create mode 100644 packages/br-utilities/src/br-utilities/cpf_gen.rb create mode 100644 packages/br-utilities/src/br-utilities/cpf_utils.rb create mode 100644 packages/br-utilities/src/br-utilities/cpf_val.rb create mode 100644 packages/br-utilities/src/br-utilities/errors.rb diff --git a/packages/br-utilities/src/br-utilities.rb b/packages/br-utilities/src/br-utilities.rb index ac51953..ef0846e 100644 --- a/packages/br-utilities/src/br-utilities.rb +++ b/packages/br-utilities/src/br-utilities.rb @@ -4,8 +4,36 @@ require 'cnpj-utilities' require_relative 'br-utilities/version' -module BrUtils - def self.hello - 'br-utilities' - end +# Entry point for the +br-utilities+ gem. +# +# Loads sibling packages (+cpf-utilities+, +cnpj-utilities+) and defines the +# {BrUtils} façade class. +version.rb+ defines a placeholder module so the +# gemspec can read {BrUtils::VERSION}; this file promotes it to the class +# consumers instantiate. +# +# Two-tier access after +require 'br-utilities'+: +# +# - *Main shortcuts* at the façade root: {BrUtils::CpfFormatter}, +# {BrUtils::CnpjFormatter}, etc. +# - *Package nests* for the full sibling surface (Options, helpers, errors, +# types): {BrUtils::CpfFmt}, {BrUtils::CnpjUtils}, etc. (same objects as +# +::CpfFmt+, +::CnpjUtils+, …). +# - Root siblings (+CpfUtils+, +CnpjUtils+, +CpfFmt+, …) remain supported +# unchanged. +unless BrUtils.is_a?(Class) + version = BrUtils::VERSION + Object.send(:remove_const, :BrUtils) + BrUtils = Class.new + BrUtils.const_set(:VERSION, version) end + +require_relative 'br-utilities/errors' +require_relative 'br-utilities/br_utils' +require_relative 'br-utilities/cpf_fmt' +require_relative 'br-utilities/cpf_gen' +require_relative 'br-utilities/cpf_val' +require_relative 'br-utilities/cpf_utils' +require_relative 'br-utilities/cnpj_fmt' +require_relative 'br-utilities/cnpj_gen' +require_relative 'br-utilities/cnpj_val' +require_relative 'br-utilities/cnpj_utils' diff --git a/packages/br-utilities/src/br-utilities/br_utils.rb b/packages/br-utilities/src/br-utilities/br_utils.rb new file mode 100644 index 0000000..4680a7e --- /dev/null +++ b/packages/br-utilities/src/br-utilities/br_utils.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true + +require 'cpf-utilities' +require 'cnpj-utilities' + +require_relative 'errors' + +# Unified API for Brazilian-related data, like CPF (Cadastro de Pessoa Física) +# and CNPJ (Cadastro Nacional da Pessoa Jurídica). Provides a unified interface +# for formatting, generating, and validating data. Aggregates configurable +# {CpfUtils} and {CnpjUtils} instances behind a single façade. +# +# Public API: +# +# - {BrUtils.cpf}, {BrUtils.cnpj} — class helpers that alias {BrUtils::DEFAULT} +# (preferred quick path for domain accessors) +# - {BrUtils::DEFAULT} — mutable process-wide singleton (JS/Python parity; not +# thread-isolated — prefer {.new} under concurrency) +# - {BrUtils#cpf}, {BrUtils#cnpj} — instance accessors with setters +# - {BrUtils::VERSION} +# - {BrUtils::InvalidArgumentCombinationError}, {BrUtils::TypeMismatchError} +# +# Two-tier access: main-class shortcuts ({BrUtils::CpfFormatter}, etc.) and +# nested package modules ({BrUtils::CpfFmt}, {BrUtils::CpfUtils}, etc.). Root +# siblings ({CpfUtils}, {CnpjUtils}, {CpfFmt}, …) remain loadable after +# +require 'br-utilities'+. +# +# Mutating {BrUtils::DEFAULT} (e.g. via setters) affects subsequent class-helper +# calls process-wide (shared across threads). Prefer {BrUtils.new} for concurrent +# or isolated work. Custom instances are independent of +DEFAULT+. +# +# @example +# require 'br-utilities' +# +# BrUtils.cpf.format('12345678909') # => "123.456.789-09" +# BrUtils.cnpj.is_valid('91415732000793') # => true +class BrUtils + SETTINGS_KEYS = %i[cpf cnpj].freeze + FLAT_KEYS = %i[cpf_formatter cpf_generator cnpj_formatter cnpj_generator cnpj_validator].freeze + KEYWORD_KEYS = (SETTINGS_KEYS + FLAT_KEYS).freeze + + private_constant :SETTINGS_KEYS, :FLAT_KEYS, :KEYWORD_KEYS + + # Internal helpers for resolving domain utils from settings / keyword arguments. + module Helpers + module_function + + def resolve_settings(settings, keywords) + keyword_settings = compact_keywords(keywords) + raise_ambiguous_settings! if !settings.nil? && !keyword_settings.empty? + return normalize_settings(settings) unless settings.nil? + + keyword_settings + end + + def normalize_settings(settings) + raise TypeMismatchError, "BrUtils settings must be a Hash. Got #{settings.class}." unless settings.is_a?(Hash) + + SETTINGS_KEYS.each_with_object({}) do |key, resolved| + if settings.key?(key) + resolved[key] = settings[key] + elsif settings.key?(key.to_s) + resolved[key] = settings[key.to_s] + end + end + end + + def compact_keywords(keywords) + KEYWORD_KEYS.each_with_object({}) do |key, resolved| + value = keywords[key] + resolved[key] = value unless value.nil? + end + end + + def resolve_cpf_utils(resolved) + return resolve_utils(CpfUtils, resolved[:cpf]) if resolved.key?(:cpf) + return CpfUtils.new unless cpf_flat?(resolved) + + CpfUtils.new(formatter: resolved[:cpf_formatter], generator: resolved[:cpf_generator]) + end + + def cpf_flat?(resolved) + resolved.key?(:cpf_formatter) || resolved.key?(:cpf_generator) + end + + def resolve_cnpj_utils(resolved) + return resolve_utils(CnpjUtils, resolved[:cnpj]) if resolved.key?(:cnpj) + return CnpjUtils.new unless cnpj_flat?(resolved) + + CnpjUtils.new( + formatter: resolved[:cnpj_formatter], + generator: resolved[:cnpj_generator], + validator: resolved[:cnpj_validator] + ) + end + + def cnpj_flat?(resolved) + resolved.key?(:cnpj_formatter) || resolved.key?(:cnpj_generator) || resolved.key?(:cnpj_validator) + end + + def resolve_utils(utils_cls, value) + return utils_cls.new if value.nil? + return value if value.is_a?(utils_cls) + return utils_cls.new(value) if value.is_a?(Hash) + + # Duck-typed / test doubles: use the given object by reference (Python parity). + value + end + + def raise_ambiguous_settings! + option_keywords = KEYWORD_KEYS.map { |key| "#{key}:" }.join(', ') + + raise InvalidArgumentCombinationError, + 'Pass either a settings Hash to `settings`, or keyword arguments ' \ + "(#{option_keywords}), not both." + end + end + private_constant :Helpers + + # Creates a new {BrUtils} instance with customized options. All options are + # optional. If any option is omitted, it falls back to its default value. + # + # Each of +:cpf+ and +:cnpj+ accepts either a pre-built utils instance or a + # configuration {Hash} spread into the corresponding {CpfUtils} / {CnpjUtils} + # constructor. Within that Hash, each resource key (+formatter+, +generator+, + # and +validator+ for CNPJ) accepts either an options object or a mapping of + # option values. + # + # Flat +:cpf_formatter+ / +:cpf_generator+ and +:cnpj_formatter+ / + # +:cnpj_generator+ / +:cnpj_validator+ arguments are supported as a + # convenience when only individual components need customization. They are + # ignored when the corresponding +:cpf+ or +:cnpj+ argument is provided. + # + # +settings+ and the keyword arguments are never merged with each other: when + # +settings+ is given (a {Hash} with +:cpf+ and/or +:cnpj+ keys), it alone + # determines the domains; otherwise, the domains are built exclusively from the + # keyword arguments. Passing +settings+ together with any non-+nil+ keyword + # argument raises {InvalidArgumentCombinationError} instead of silently + # ignoring the keywords. + # + # @param settings [Hash, nil] settings Hash with +:cpf+ and/or +:cnpj+ keys + # (each a utils instance, settings Hash, or +nil+) + # @param keywords [Hash] +:cpf+, +:cnpj+, and/or flat component kwargs + # (mutually exclusive with +settings+) + # @raise [InvalidArgumentCombinationError] if +settings+ and a keyword argument + # are both given + # @raise [TypeMismatchError] if +settings+ is given and is not a +Hash+ + # @raise [CnpjFmt::TypeMismatchError] if CNPJ formatter options have an invalid + # type + # @raise [CnpjFmt::OutOfRangeError] if CNPJ formatter +hidden_start+ or + # +hidden_end+ are out of valid range + # @raise [CnpjFmt::ValidationError] if any CNPJ formatter key option contains a + # disallowed character + # @raise [CnpjGen::TypeMismatchError] if CNPJ generator options have an invalid + # type + # @raise [CnpjGen::ValidationError] if CNPJ generator +prefix+ is invalid or + # +type+ is not allowed + # @raise [CnpjVal::TypeMismatchError] if CNPJ validator options have an invalid + # type + # @raise [CnpjVal::ValidationError] if CNPJ validator +type+ is not allowed + # @raise [CpfFmt::TypeMismatchError] if CPF formatter options have an invalid + # type + # @raise [CpfFmt::OutOfRangeError] if CPF formatter +hidden_start+ or + # +hidden_end+ are out of valid range + # @raise [CpfFmt::ValidationError] if any CPF formatter key option contains a + # disallowed character + # @raise [CpfGen::TypeMismatchError] if CPF generator options have an invalid + # type + # @raise [CpfGen::ValidationError] if CPF generator +prefix+ is invalid + def initialize(settings = nil, **keywords) + resolved = Helpers.resolve_settings(settings, keywords) + + @cpf = Helpers.resolve_cpf_utils(resolved) + @cnpj = Helpers.resolve_cnpj_utils(resolved) + end + + # Access the CPF utilities instance. + # + # @return [CpfUtils] + attr_reader :cpf + + # Access the CNPJ utilities instance. + # + # @return [CnpjUtils] + attr_reader :cnpj + + # Sets the active CPF utilities instance. + # + # It is flexible and can handle any of these inputs: + # + # 1. A complete new instance of {CpfUtils} + # 2. A {Hash} of {CpfUtils} component settings + # 3. A partial {Hash} with options for the CPF utilities + # 4. +nil+ creates a brand new instance of {CpfUtils} with the default options + # + # Note that this resets the CPF utilities instance completely. Any previous + # options will be overridden. To alter only a single option or a few options + # of the existing instance, access it directly and use the CPF utilities' + # setters and methods (e.g. +utils.cpf.formatter.options.hidden = true+). + # + # @param value [CpfUtils, Hash, nil] + # @raise [CpfFmt::TypeMismatchError] if formatter options have an invalid type + # @raise [CpfFmt::OutOfRangeError] if formatter +hidden_start+ or +hidden_end+ + # are out of valid range + # @raise [CpfFmt::ValidationError] if any formatter key option contains a + # disallowed character + # @raise [CpfGen::TypeMismatchError] if generator options have an invalid type + # @raise [CpfGen::ValidationError] if generator +prefix+ is invalid + def cpf=(value) + @cpf = Helpers.resolve_utils(CpfUtils, value) + end + + # Sets the active CNPJ utilities instance. + # + # It is flexible and can handle any of these inputs: + # + # 1. A complete new instance of {CnpjUtils} + # 2. A {Hash} of {CnpjUtils} component settings + # 3. A partial {Hash} with options for the CNPJ utilities + # 4. +nil+ creates a brand new instance of {CnpjUtils} with the default options + # + # Note that this resets the CNPJ utilities instance completely. Any previous + # options will be overridden. To alter only a single option or a few options + # of the existing instance, access it directly and use the CNPJ utilities' + # setters and methods (e.g. +utils.cnpj.generator.options.type = 'numeric'+). + # + # @param value [CnpjUtils, Hash, nil] + # @raise [CnpjFmt::TypeMismatchError] if formatter options have an invalid type + # @raise [CnpjFmt::OutOfRangeError] if formatter +hidden_start+ or +hidden_end+ + # are out of valid range + # @raise [CnpjFmt::ValidationError] if any formatter key option contains a + # disallowed character + # @raise [CnpjGen::TypeMismatchError] if generator options have an invalid type + # @raise [CnpjGen::ValidationError] if generator +prefix+ is invalid or +type+ + # is not allowed + # @raise [CnpjVal::TypeMismatchError] if validator options have an invalid type + # @raise [CnpjVal::ValidationError] if validator +type+ is not allowed + def cnpj=(value) + @cnpj = Helpers.resolve_utils(CnpjUtils, value) + end + + # Default {BrUtils} instance with default CPF and CNPJ utilities (parity with + # the JS default export / Python +br_utils+ singleton). Configuration is + # process-wide and shared across threads: mutating this instance (e.g. via + # setters) affects subsequent {BrUtils.cpf} and {BrUtils.cnpj} calls for every + # caller in the process. Prefer {BrUtils.new} for threaded or isolated work. + DEFAULT = new + + class << self + # Access the CPF utilities from {DEFAULT} (alias of {BrUtils#cpf} on that + # instance). + # + # @return [CpfUtils] + # @see BrUtils#cpf + def cpf + DEFAULT.cpf + end + + # Access the CNPJ utilities from {DEFAULT} (alias of {BrUtils#cnpj} on that + # instance). + # + # @return [CnpjUtils] + # @see BrUtils#cnpj + def cnpj + DEFAULT.cnpj + end + end +end diff --git a/packages/br-utilities/src/br-utilities/cnpj_fmt.rb b/packages/br-utilities/src/br-utilities/cnpj_fmt.rb new file mode 100644 index 0000000..a050fec --- /dev/null +++ b/packages/br-utilities/src/br-utilities/cnpj_fmt.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class BrUtils + # CNPJ formatting utilities re-exported from +cnpj-fmt+ (via +cnpj-utilities+). + # + # Nested package module — same object as +::CnpjFmt+ (Options, helpers, errors, + # types). + CnpjFmt = ::CnpjFmt + + # Main-class shortcut for {CnpjFmt::CnpjFormatter}. + CnpjFormatter = CnpjFmt::CnpjFormatter + + # Main-class shortcut for {CnpjFmt::CnpjFormatterOptions}. + CnpjFormatterOptions = CnpjFmt::CnpjFormatterOptions + + # Main-class shortcut for {CnpjFmt::Error}. + CnpjFormatterError = CnpjFmt::Error +end diff --git a/packages/br-utilities/src/br-utilities/cnpj_gen.rb b/packages/br-utilities/src/br-utilities/cnpj_gen.rb new file mode 100644 index 0000000..94c374e --- /dev/null +++ b/packages/br-utilities/src/br-utilities/cnpj_gen.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class BrUtils + # CNPJ generation utilities re-exported from +cnpj-gen+ (via +cnpj-utilities+). + # + # Nested package module — same object as +::CnpjGen+ (Options, helpers, errors, + # types). + CnpjGen = ::CnpjGen + + # Main-class shortcut for {CnpjGen::CnpjGenerator}. + CnpjGenerator = CnpjGen::CnpjGenerator + + # Main-class shortcut for {CnpjGen::CnpjGeneratorOptions}. + CnpjGeneratorOptions = CnpjGen::CnpjGeneratorOptions + + # Main-class shortcut for {CnpjGen::Error}. + CnpjGeneratorError = CnpjGen::Error +end diff --git a/packages/br-utilities/src/br-utilities/cnpj_utils.rb b/packages/br-utilities/src/br-utilities/cnpj_utils.rb new file mode 100644 index 0000000..369f195 --- /dev/null +++ b/packages/br-utilities/src/br-utilities/cnpj_utils.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class BrUtils + # CNPJ utilities re-exported from +cnpj-utilities+. + # + # Nested package class — same object as +::CnpjUtils+. + CnpjUtils = ::CnpjUtils +end diff --git a/packages/br-utilities/src/br-utilities/cnpj_val.rb b/packages/br-utilities/src/br-utilities/cnpj_val.rb new file mode 100644 index 0000000..68713e3 --- /dev/null +++ b/packages/br-utilities/src/br-utilities/cnpj_val.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class BrUtils + # CNPJ validation utilities re-exported from +cnpj-val+ (via +cnpj-utilities+). + # + # Nested package module — same object as +::CnpjVal+ (Options, helpers, errors, + # types). + CnpjVal = ::CnpjVal + + # Main-class shortcut for {CnpjVal::CnpjValidator}. + CnpjValidator = CnpjVal::CnpjValidator + + # Main-class shortcut for {CnpjVal::CnpjValidatorOptions}. + CnpjValidatorOptions = CnpjVal::CnpjValidatorOptions + + # Main-class shortcut for {CnpjVal::Error}. + CnpjValidatorError = CnpjVal::Error +end diff --git a/packages/br-utilities/src/br-utilities/cpf_fmt.rb b/packages/br-utilities/src/br-utilities/cpf_fmt.rb new file mode 100644 index 0000000..b1588db --- /dev/null +++ b/packages/br-utilities/src/br-utilities/cpf_fmt.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class BrUtils + # CPF formatting utilities re-exported from +cpf-fmt+ (via +cpf-utilities+). + # + # Nested package module — same object as +::CpfFmt+ (Options, helpers, errors, + # types). + CpfFmt = ::CpfFmt + + # Main-class shortcut for {CpfFmt::CpfFormatter}. + CpfFormatter = CpfFmt::CpfFormatter + + # Main-class shortcut for {CpfFmt::CpfFormatterOptions}. + CpfFormatterOptions = CpfFmt::CpfFormatterOptions + + # Main-class shortcut for {CpfFmt::Error}. + CpfFormatterError = CpfFmt::Error +end diff --git a/packages/br-utilities/src/br-utilities/cpf_gen.rb b/packages/br-utilities/src/br-utilities/cpf_gen.rb new file mode 100644 index 0000000..fbfe4b5 --- /dev/null +++ b/packages/br-utilities/src/br-utilities/cpf_gen.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class BrUtils + # CPF generation utilities re-exported from +cpf-gen+ (via +cpf-utilities+). + # + # Nested package module — same object as +::CpfGen+ (Options, helpers, errors, + # types). + CpfGen = ::CpfGen + + # Main-class shortcut for {CpfGen::CpfGenerator}. + CpfGenerator = CpfGen::CpfGenerator + + # Main-class shortcut for {CpfGen::CpfGeneratorOptions}. + CpfGeneratorOptions = CpfGen::CpfGeneratorOptions + + # Main-class shortcut for {CpfGen::Error}. + CpfGeneratorError = CpfGen::Error +end diff --git a/packages/br-utilities/src/br-utilities/cpf_utils.rb b/packages/br-utilities/src/br-utilities/cpf_utils.rb new file mode 100644 index 0000000..d86704c --- /dev/null +++ b/packages/br-utilities/src/br-utilities/cpf_utils.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class BrUtils + # CPF utilities re-exported from +cpf-utilities+. + # + # Nested package class — same object as +::CpfUtils+. + CpfUtils = ::CpfUtils +end diff --git a/packages/br-utilities/src/br-utilities/cpf_val.rb b/packages/br-utilities/src/br-utilities/cpf_val.rb new file mode 100644 index 0000000..e27d3ed --- /dev/null +++ b/packages/br-utilities/src/br-utilities/cpf_val.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class BrUtils + # CPF validation utilities re-exported from +cpf-val+ (via +cpf-utilities+). + # + # Nested package module — same object as +::CpfVal+ (helpers, errors, types). + CpfVal = ::CpfVal + + # Main-class shortcut for {CpfVal::CpfValidator}. + CpfValidator = CpfVal::CpfValidator + + # Main-class shortcut for {CpfVal::Error}. + CpfValidatorError = CpfVal::Error +end diff --git a/packages/br-utilities/src/br-utilities/errors.rb b/packages/br-utilities/src/br-utilities/errors.rb new file mode 100644 index 0000000..6619be7 --- /dev/null +++ b/packages/br-utilities/src/br-utilities/errors.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class BrUtils + # Marker module mixed into every custom error raised by this library. + # + # Use +rescue BrUtils::Error+ to catch every library error regardless of + # native ancestry. Domain packages raise their own error hierarchies; + # this gem only defines the misuse errors it raises itself. + module Error; end + + # API misuse error raised when an argument's runtime type does not match the + # type required by the API contract (for example, a non-Hash +settings+ value). + class TypeMismatchError < TypeError + include Error + end + + # API misuse error raised when the combination of provided arguments does not + # match any valid overload-style signature (for example, a settings Hash + # together with keyword overrides). + class InvalidArgumentCombinationError < ArgumentError + include Error + end +end diff --git a/packages/br-utilities/src/br-utilities/version.rb b/packages/br-utilities/src/br-utilities/version.rb index b8ff3f8..59b60db 100644 --- a/packages/br-utilities/src/br-utilities/version.rb +++ b/packages/br-utilities/src/br-utilities/version.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true +# Placeholder module so the gemspec (and any early require of this file) can read +# {BrUtils::VERSION}. The gem entry point promotes +BrUtils+ to a class and +# reopens it for the façade implementation. module BrUtils + # Gem version string. Placeholder replaced at build/publish time. + # + # @return [String] VERSION = '0.0.0' end From fd11e9c512515f0da6859ded786f3220d9e9c7a3 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Mon, 10 Aug 2026 17:38:38 -0300 Subject: [PATCH 3/7] test(br-utils): create tests suite Co-authored-by: Claude Opus 5 Co-authored-by: Cursor Agent --- .../br-utilities/tests/br_utilities.spec.rb | 11 - packages/br-utilities/tests/br_utils.spec.rb | 1054 +++++++++++++++++ 2 files changed, 1054 insertions(+), 11 deletions(-) delete mode 100644 packages/br-utilities/tests/br_utilities.spec.rb create mode 100644 packages/br-utilities/tests/br_utils.spec.rb diff --git a/packages/br-utilities/tests/br_utilities.spec.rb b/packages/br-utilities/tests/br_utilities.spec.rb deleted file mode 100644 index c334532..0000000 --- a/packages/br-utilities/tests/br_utilities.spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe BrUtils do - describe '.hello' do - it 'returns br-utilities' do - expect(BrUtils.hello).to eq('br-utilities') - end - end -end diff --git a/packages/br-utilities/tests/br_utils.spec.rb b/packages/br-utilities/tests/br_utils.spec.rb new file mode 100644 index 0000000..6a02322 --- /dev/null +++ b/packages/br-utilities/tests/br_utils.spec.rb @@ -0,0 +1,1054 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Combined behavioural suite for BrUtils (JS / PHP / Python reference tests). +# +# Dropped cases (not meaningful in Ruby): +# - js/packages/br-utils/tests/output.spec.ts — UMD/CJS/ESM bundles, .d.ts wiring, +# global variable attachment, and export-string parsing (JS packaging only). +# - JavaScript `undefined` nullish values — Ruby uses `nil` only. +# - PHP getCpfUtils() / getCnpjUtils() accessor names — Ruby uses #cpf / #cnpj +# (JS/Python parity per AGENTS.md). +# - PHP read-only cpf/cnpj (no setters) — Ruby implements setters like JS/Python. +# - PHP CnpjType / CnpjValidationType enums — Ruby uses string type values. +# - PHP legacy CPF v1 InvalidArgumentException for formatter/generator options — +# Ruby mirrors JS/Python v2 structured errors (CpfFmt::*/CpfGen::*). +# - PHP phpunit/Cpf/* legacy CPF v1 component suites — Ruby aligns with +# cpf-utilities v2, not PHP CPF v1. +# - Python __slots__ / dynamic-attribute restriction — optional in Ruby; AGENTS.md +# does not require freezing or slot-like attribute locking. +# - BrUtils Options class fold/#set/nil-setter contracts — BrUtils has no Options +# class (settings are a Hash or domain utils instances; Options live on +# cpf-fmt / cnpj-fmt / … siblings). +# - BrUtils.format / .generate / .is_valid class helpers — façade owns wiring only +# (cpf/cnpj); domain operations live on CpfUtils / CnpjUtils (and their DEFAULT). + +def compact_options(**kwargs) + kwargs.compact +end + +def expect_options_containing(actual, expected) + expected.each do |key, value| + expect(actual[key]).to eq(value) + end +end + +CPF_FORMAT_FACTORIES = { + constructor_hash: lambda { |cpf, dot_key = nil, dash_key = nil| + utils = BrUtils.new(cpf: { formatter: compact_options(dot_key: dot_key, dash_key: dash_key) }) + utils.cpf.format(cpf) + }, + method_keywords: lambda { |cpf, dot_key = nil, dash_key = nil| + BrUtils.new.cpf.format(cpf, dot_key: dot_key, dash_key: dash_key) + } +}.freeze + +CPF_GENERATE_FACTORIES = { + constructor_hash: lambda { |format: nil, prefix: nil| + utils = BrUtils.new(cpf: { generator: compact_options(format: format, prefix: prefix) }) + utils.cpf.generate + }, + method_keywords: lambda { |format: nil, prefix: nil| + BrUtils.new.cpf.generate(format: format, prefix: prefix) + } +}.freeze + +CNPJ_FORMAT_FACTORIES = { + constructor_hash: lambda { |cnpj, slash_key = nil| + utils = BrUtils.new(cnpj: { formatter: compact_options(slash_key: slash_key) }) + utils.cnpj.format(cnpj) + }, + constructor_options: lambda { |cnpj, slash_key = nil| + options = CnpjFmt::CnpjFormatterOptions.new(compact_options(slash_key: slash_key)) + utils = BrUtils.new(cnpj: { formatter: options }) + utils.cnpj.format(cnpj) + }, + method_keywords: lambda { |cnpj, slash_key = nil| + BrUtils.new.cnpj.format(cnpj, slash_key: slash_key) + }, + method_options: lambda { |cnpj, slash_key = nil| + options = CnpjFmt::CnpjFormatterOptions.new(compact_options(slash_key: slash_key)) + BrUtils.new.cnpj.format(cnpj, options) + } +}.freeze + +CNPJ_GENERATE_FACTORIES = { + constructor_hash: lambda { |format: nil, prefix: nil, type: nil| + utils = BrUtils.new( + cnpj: { generator: compact_options(format: format, prefix: prefix, type: type) } + ) + utils.cnpj.generate + }, + constructor_options: lambda { |format: nil, prefix: nil, type: nil| + options = CnpjGen::CnpjGeneratorOptions.new( + compact_options(format: format, prefix: prefix, type: type) + ) + utils = BrUtils.new(cnpj: { generator: options }) + utils.cnpj.generate + }, + method_keywords: lambda { |format: nil, prefix: nil, type: nil| + BrUtils.new.cnpj.generate(format: format, prefix: prefix, type: type) + }, + method_options: lambda { |format: nil, prefix: nil, type: nil| + options = CnpjGen::CnpjGeneratorOptions.new( + compact_options(format: format, prefix: prefix, type: type) + ) + BrUtils.new.cnpj.generate(options) + } +}.freeze + +CNPJ_IS_VALID_FACTORIES = { + constructor_hash: lambda { |cnpj, type: nil, case_sensitive: nil| + utils = BrUtils.new( + cnpj: { validator: compact_options(type: type, case_sensitive: case_sensitive) } + ) + utils.cnpj.is_valid(cnpj) + }, + constructor_options: lambda { |cnpj, type: nil, case_sensitive: nil| + options = CnpjVal::CnpjValidatorOptions.new( + compact_options(type: type, case_sensitive: case_sensitive) + ) + utils = BrUtils.new(cnpj: { validator: options }) + utils.cnpj.is_valid(cnpj) + }, + method_keywords: lambda { |cnpj, type: nil, case_sensitive: nil| + BrUtils.new.cnpj.is_valid(cnpj, type: type, case_sensitive: case_sensitive) + }, + method_options: lambda { |cnpj, type: nil, case_sensitive: nil| + options = CnpjVal::CnpjValidatorOptions.new( + compact_options(type: type, case_sensitive: case_sensitive) + ) + BrUtils.new.cnpj.is_valid(cnpj, options) + } +}.freeze + +CPF_FORMAT_FACTORY_CONTEXTS = [ + ['when options are passed to the constructor as a Hash', :constructor_hash], + ['when options are passed to #format as keywords', :method_keywords] +].freeze + +CPF_GENERATE_FACTORY_CONTEXTS = [ + ['when options are passed to the constructor as a Hash', :constructor_hash], + ['when options are passed to #generate as keywords', :method_keywords] +].freeze + +CNPJ_FORMAT_FACTORY_CONTEXTS = [ + ['when options are passed to the constructor as a Hash', :constructor_hash], + ['when options are passed to the constructor as CnpjFormatterOptions', :constructor_options], + ['when options are passed to #format as keywords', :method_keywords], + ['when options are passed to #format as CnpjFormatterOptions', :method_options] +].freeze + +CNPJ_GENERATE_FACTORY_CONTEXTS = [ + ['when options are passed to the constructor as a Hash', :constructor_hash], + ['when options are passed to the constructor as CnpjGeneratorOptions', :constructor_options], + ['when options are passed to #generate as keywords', :method_keywords], + ['when options are passed to #generate as CnpjGeneratorOptions', :method_options] +].freeze + +CNPJ_IS_VALID_FACTORY_CONTEXTS = [ + ['when options are passed to the constructor as a Hash', :constructor_hash], + ['when options are passed to the constructor as CnpjValidatorOptions', :constructor_options], + ['when options are passed to #is_valid as keywords', :method_keywords], + ['when options are passed to #is_valid as CnpjValidatorOptions', :method_options] +].freeze + +RSpec.describe BrUtils do + def default_cpf_formatter_options_snapshot + CpfFmt::CpfFormatterOptions.new.all + end + + def default_cpf_generator_options_snapshot + CpfGen::CpfGeneratorOptions.new.all + end + + def default_cnpj_formatter_options_snapshot + CnpjFmt::CnpjFormatterOptions.new.all + end + + def default_cnpj_generator_options_snapshot + CnpjGen::CnpjGeneratorOptions.new.all + end + + def default_cnpj_validator_options_snapshot + CnpjVal::CnpjValidatorOptions.new.all + end + + describe 'DEFAULT' do + it 'is an instance of BrUtils' do + expect(described_class::DEFAULT).to be_a(described_class) + end + + it 'exposes cpf and cnpj domain utils' do + aggregate_failures do + expect(described_class::DEFAULT.cpf).to be_a(CpfUtils) + expect(described_class::DEFAULT.cnpj).to be_a(CnpjUtils) + end + end + end + + describe 'class helpers' do + # BrUtils façade operations are domain accessors; class helpers forward to DEFAULT. + it 'exposes cpf and cnpj' do + aggregate_failures do + expect(described_class).to respond_to(:cpf) + expect(described_class).to respond_to(:cnpj) + end + end + + context 'when calling through the class' do + it 'returns the same cpf as DEFAULT' do + expect(described_class.cpf).to equal(described_class::DEFAULT.cpf) + end + + it 'returns the same cnpj as DEFAULT' do + expect(described_class.cnpj).to equal(described_class::DEFAULT.cnpj) + end + end + + context 'when DEFAULT is mutated' do + around do |example| + original_cpf = described_class::DEFAULT.cpf + original_cnpj = described_class::DEFAULT.cnpj + example.run + described_class::DEFAULT.cpf = original_cpf + described_class::DEFAULT.cnpj = original_cnpj + end + + it 'affects subsequent class helper cpf calls' do + replacement = CpfUtils.new(formatter: { dash_key: '|' }) + described_class::DEFAULT.cpf = replacement + + aggregate_failures do + expect(described_class.cpf).to equal(replacement) + expect(described_class.cpf.format('12345678909')).to eq('123.456.789|09') + end + end + + it 'affects subsequent class helper cnpj calls' do + replacement = CnpjUtils.new(formatter: { slash_key: '|' }) + described_class::DEFAULT.cnpj = replacement + + aggregate_failures do + expect(described_class.cnpj).to equal(replacement) + expect(described_class.cnpj.format('01ABC234000X56')).to eq('01.ABC.234|000X-56') + end + end + + it 'does not affect a custom instance' do + custom = described_class.new + described_class::DEFAULT.cpf = CpfUtils.new(formatter: { dash_key: '|' }) + described_class::DEFAULT.cnpj = CnpjUtils.new(formatter: { slash_key: '|' }) + + aggregate_failures do + expect(custom.cpf.format('12345678909')).to eq('123.456.789-09') + expect(custom.cnpj.format('01ABC234000X56')).to eq('01.ABC.234/000X-56') + end + end + end + end + + describe 'loaded sibling packages' do + it 'makes cpf-utilities symbols available' do + aggregate_failures do + expect(defined?(CpfUtils)).to eq('constant') + expect(defined?(CpfFmt::CpfFormatter)).to eq('constant') + expect(defined?(CpfGen::CpfGenerator)).to eq('constant') + expect(defined?(CpfVal::CpfValidator)).to eq('constant') + expect(CpfFmt).to respond_to(:cpf_fmt) + expect(CpfGen).to respond_to(:cpf_gen) + expect(CpfVal).to respond_to(:cpf_val) + end + end + + it 'makes cnpj-utilities symbols available' do + aggregate_failures do + expect(defined?(CnpjUtils)).to eq('constant') + expect(defined?(CnpjFmt::CnpjFormatter)).to eq('constant') + expect(defined?(CnpjGen::CnpjGenerator)).to eq('constant') + expect(defined?(CnpjVal::CnpjValidator)).to eq('constant') + expect(CnpjFmt).to respond_to(:cnpj_fmt) + expect(CnpjGen).to respond_to(:cnpj_gen) + expect(CnpjVal).to respond_to(:cnpj_val) + end + end + end + + describe 'two-tier BrUtils re-exports' do + it 'nests sibling modules as the same objects' do + aggregate_failures do + expect(described_class::CpfUtils).to equal(CpfUtils) + expect(described_class::CnpjUtils).to equal(CnpjUtils) + expect(described_class::CpfFmt).to equal(CpfFmt) + expect(described_class::CpfGen).to equal(CpfGen) + expect(described_class::CpfVal).to equal(CpfVal) + expect(described_class::CnpjFmt).to equal(CnpjFmt) + expect(described_class::CnpjGen).to equal(CnpjGen) + expect(described_class::CnpjVal).to equal(CnpjVal) + end + end + + it 'aliases main cpf classes at the façade root' do + aggregate_failures do + expect(described_class::CpfFormatter).to equal(CpfFmt::CpfFormatter) + expect(described_class::CpfFormatterOptions).to equal(CpfFmt::CpfFormatterOptions) + expect(described_class::CpfGenerator).to equal(CpfGen::CpfGenerator) + expect(described_class::CpfGeneratorOptions).to equal(CpfGen::CpfGeneratorOptions) + expect(described_class::CpfValidator).to equal(CpfVal::CpfValidator) + end + end + + it 'aliases main cnpj classes at the façade root' do + aggregate_failures do + expect(described_class::CnpjFormatter).to equal(CnpjFmt::CnpjFormatter) + expect(described_class::CnpjFormatterOptions).to equal(CnpjFmt::CnpjFormatterOptions) + expect(described_class::CnpjGenerator).to equal(CnpjGen::CnpjGenerator) + expect(described_class::CnpjGeneratorOptions).to equal(CnpjGen::CnpjGeneratorOptions) + expect(described_class::CnpjValidator).to equal(CnpjVal::CnpjValidator) + expect(described_class::CnpjValidatorOptions).to equal(CnpjVal::CnpjValidatorOptions) + end + end + + context 'with nested surface smoke' do + it 'exposes Options through the nest' do + options = described_class::CpfFmt::CpfFormatterOptions.new(hidden: true) + + expect(options.hidden).to be(true) + end + + it 'exposes helpers through the nest' do + expect(described_class::CpfFmt.cpf_fmt('12345678909')).to eq('123.456.789-09') + end + + it 'exposes an error class through the nest' do + expect(described_class::CnpjFmt::OutOfRangeError).to equal(CnpjFmt::OutOfRangeError) + end + end + end + + describe '#initialize' do + context 'when called with no arguments' do + subject(:utils) { described_class.new } + + it 'creates default domain utils instances' do + aggregate_failures do + expect(utils.cpf).to be_a(CpfUtils) + expect(utils.cnpj).to be_a(CnpjUtils) + end + end + + it 'uses default domain component options' do + aggregate_failures do + expect_options_containing( + utils.cpf.formatter.options.all, + default_cpf_formatter_options_snapshot + ) + expect_options_containing( + utils.cpf.generator.options.all, + default_cpf_generator_options_snapshot + ) + expect_options_containing( + utils.cnpj.formatter.options.all, + default_cnpj_formatter_options_snapshot + ) + expect_options_containing( + utils.cnpj.generator.options.all, + default_cnpj_generator_options_snapshot + ) + expect_options_containing( + utils.cnpj.validator.options.all, + default_cnpj_validator_options_snapshot + ) + end + end + end + + context 'when called with instances of resources' do + it 'uses the passed CnpjUtils directly' do + cnpj_utils = CnpjUtils.new + utils = described_class.new(cnpj: cnpj_utils) + + aggregate_failures do + expect(utils.cnpj).to be_a(CnpjUtils) + expect(utils.cnpj).to equal(cnpj_utils) + end + end + + it 'uses the passed CpfUtils directly' do + cpf_utils = CpfUtils.new + utils = described_class.new(cpf: cpf_utils) + + aggregate_failures do + expect(utils.cpf).to be_a(CpfUtils) + expect(utils.cpf).to equal(cpf_utils) + end + end + + it 'uses the passed resources directly' do + cnpj_utils = CnpjUtils.new + cpf_utils = CpfUtils.new + utils = described_class.new(cnpj: cnpj_utils, cpf: cpf_utils) + + aggregate_failures do + expect(utils.cnpj).to equal(cnpj_utils) + expect(utils.cpf).to equal(cpf_utils) + end + end + end + + context 'when called with literal Hash parameters' do + let(:cnpj_utils_options) do + { + formatter: { + hidden: true, + hidden_key: '#', + hidden_start: 8, + hidden_end: 11, + dot_key: '_', + slash_key: '|', + dash_key: ' dv ' + }, + generator: { + format: true, + prefix: '12345678', + type: 'numeric' + }, + validator: { + type: 'numeric' + } + } + end + + let(:cpf_utils_options) do + { + formatter: { + hidden: true, + hidden_key: '#', + hidden_start: 8, + hidden_end: 10, + dot_key: '_', + dash_key: ' dv ' + }, + generator: { + format: true, + prefix: '12345678' + } + } + end + + it 'builds CnpjUtils from nested option hashes' do + utils = described_class.new(cnpj: cnpj_utils_options) + + aggregate_failures do + expect(utils.cnpj).to be_a(CnpjUtils) + expect(utils.cnpj.formatter).to be_a(CnpjFmt::CnpjFormatter) + expect_options_containing(utils.cnpj.formatter.options.all, cnpj_utils_options[:formatter]) + expect(utils.cnpj.generator).to be_a(CnpjGen::CnpjGenerator) + expect_options_containing(utils.cnpj.generator.options.all, cnpj_utils_options[:generator]) + expect(utils.cnpj.validator).to be_a(CnpjVal::CnpjValidator) + expect_options_containing(utils.cnpj.validator.options.all, cnpj_utils_options[:validator]) + end + end + + it 'builds CpfUtils from nested option hashes' do + utils = described_class.new(cpf: cpf_utils_options) + + aggregate_failures do + expect(utils.cpf).to be_a(CpfUtils) + expect(utils.cpf.formatter).to be_a(CpfFmt::CpfFormatter) + expect_options_containing(utils.cpf.formatter.options.all, cpf_utils_options[:formatter]) + expect(utils.cpf.generator).to be_a(CpfGen::CpfGenerator) + expect_options_containing(utils.cpf.generator.options.all, cpf_utils_options[:generator]) + end + end + end + + context 'when called with flat formatter and generator options' do + it 'applies cpf_formatter options' do + formatter_options = CpfFmt::CpfFormatterOptions.new(hidden: true, hidden_key: 'X') + utils = described_class.new(cpf_formatter: formatter_options) + + aggregate_failures do + expect(utils.cpf.formatter.options).to equal(formatter_options) + expect(utils.cpf.format('12345678901')).to eq('123.XXX.XXX-XX') + end + end + + it 'applies cpf_generator options' do + generator_options = CpfGen::CpfGeneratorOptions.new(format: true, prefix: '123456789') + utils = described_class.new(cpf_generator: generator_options) + + aggregate_failures do + expect(utils.cpf.generator.options).to equal(generator_options) + expect(utils.cpf.generate).to start_with('123.456.789-') + end + end + + it 'applies cnpj_formatter options' do + formatter_options = CnpjFmt::CnpjFormatterOptions.new(hidden: true, hidden_key: 'X') + utils = described_class.new(cnpj_formatter: formatter_options) + + aggregate_failures do + expect(utils.cnpj.formatter.options).to equal(formatter_options) + expect(utils.cnpj.format('11222333000181')).to eq('11.222.XXX/XXXX-XX') + end + end + + it 'applies cnpj_generator options' do + generator_options = CnpjGen::CnpjGeneratorOptions.new(format: true, prefix: '11222333') + utils = described_class.new(cnpj_generator: generator_options) + + aggregate_failures do + expect(utils.cnpj.generator.options).to equal(generator_options) + expect(utils.cnpj.generate).to start_with('11.222.333/') + end + end + + it 'applies all flat options together' do + cpf_fmt_opts = CpfFmt::CpfFormatterOptions.new(hidden: true) + cpf_gen_opts = CpfGen::CpfGeneratorOptions.new(format: true) + cnpj_fmt_opts = CnpjFmt::CnpjFormatterOptions.new(hidden: true) + cnpj_gen_opts = CnpjGen::CnpjGeneratorOptions.new(format: true) + + utils = described_class.new( + cpf_formatter: cpf_fmt_opts, + cpf_generator: cpf_gen_opts, + cnpj_formatter: cnpj_fmt_opts, + cnpj_generator: cnpj_gen_opts + ) + + aggregate_failures do + expect(utils.cpf.formatter.options).to equal(cpf_fmt_opts) + expect(utils.cpf.generator.options).to equal(cpf_gen_opts) + expect(utils.cnpj.formatter.options).to equal(cnpj_fmt_opts) + expect(utils.cnpj.generator.options).to equal(cnpj_gen_opts) + end + end + end + + context 'when called with nested settings hashes' do + it 'configures CPF formatter and generator from hashes' do + formatter_options = { + hidden: true, + hidden_key: '#', + hidden_start: 8, + hidden_end: 10, + dot_key: '_', + dash_key: ' dv ' + } + generator_options = { format: true, prefix: '12345678' } + + utils = described_class.new( + cpf: { + formatter: formatter_options, + generator: generator_options + } + ) + + aggregate_failures do + expect_options_containing(utils.cpf.formatter.options.all, formatter_options) + expect_options_containing(utils.cpf.generator.options.all, generator_options) + end + end + + it 'configures CNPJ components from hashes' do + formatter_options = { slash_key: '|' } + generator_options = { format: true, prefix: '12345' } + validator_options = { type: 'numeric', case_sensitive: false } + + utils = described_class.new( + cnpj: { + formatter: formatter_options, + generator: generator_options, + validator: validator_options + } + ) + + aggregate_failures do + expect_options_containing(utils.cnpj.formatter.options.all, formatter_options) + expect_options_containing(utils.cnpj.generator.options.all, generator_options) + expect_options_containing(utils.cnpj.validator.options.all, validator_options) + end + end + + it 'keeps provided options instances by reference' do + cnpj_formatter_options = CnpjFmt::CnpjFormatterOptions.new + cnpj_generator_options = CnpjGen::CnpjGeneratorOptions.new + cnpj_validator_options = CnpjVal::CnpjValidatorOptions.new + + utils = described_class.new( + cnpj: { + formatter: cnpj_formatter_options, + generator: cnpj_generator_options, + validator: cnpj_validator_options + } + ) + + aggregate_failures do + expect(utils.cnpj.formatter.options).to equal(cnpj_formatter_options) + expect(utils.cnpj.generator.options).to equal(cnpj_generator_options) + expect(utils.cnpj.validator.options).to equal(cnpj_validator_options) + end + end + + it 'reflects later mutation of shared options' do + cnpj_formatter_options = CnpjFmt::CnpjFormatterOptions.new + utils = described_class.new(cnpj: { formatter: cnpj_formatter_options }) + + cnpj_formatter_options.dash_key = '|' + + expect(utils.cnpj.formatter.options.all[:dash_key]).to eq('|') + end + end + + context 'when called with a settings Hash' do + it 'adopts nested domain utils from the Hash' do + cpf_utils = CpfUtils.new + cnpj_utils = CnpjUtils.new + utils = described_class.new({ cpf: cpf_utils, cnpj: cnpj_utils }) + + aggregate_failures do + expect(utils.cpf).to equal(cpf_utils) + expect(utils.cnpj).to equal(cnpj_utils) + end + end + end + + context 'when called with a non-Hash settings value' do + it 'raises TypeMismatchError for a string' do + expect { described_class.new('not-a-hash') } + .to raise_error(BrUtils::TypeMismatchError, /settings must be a Hash/) + end + + it 'raises TypeMismatchError for false' do + expect { described_class.new(false) } + .to raise_error(BrUtils::TypeMismatchError, /settings must be a Hash/) + end + + it 'raises TypeMismatchError for an array' do + expect { described_class.new([]) } + .to raise_error(BrUtils::TypeMismatchError, /settings must be a Hash/) + end + end + + context 'when called with invalid options' do + it 'raises CPF formatter OutOfRangeError' do + expect { described_class.new(cpf: { formatter: { hidden_start: -1 } }) } + .to raise_error(CpfFmt::OutOfRangeError) + end + + it 'raises CPF generator ValidationError' do + expect { described_class.new(cpf: { generator: { prefix: '000000000' } }) } + .to raise_error(CpfGen::ValidationError) + end + + it 'raises CNPJ formatter OutOfRangeError' do + expect { described_class.new(cnpj: { formatter: { hidden_start: -1 } }) } + .to raise_error(CnpjFmt::OutOfRangeError) + end + + it 'raises CNPJ formatter ValidationError' do + expect { described_class.new(cnpj: { formatter: { dash_key: "\u00e5" } }) } + .to raise_error(CnpjFmt::ValidationError) + end + + it 'raises CNPJ generator ValidationError for prefix' do + expect { described_class.new(cnpj: { generator: { prefix: '00000000' } }) } + .to raise_error(CnpjGen::ValidationError) + end + + it 'raises CNPJ generator ValidationError for type' do + expect { described_class.new(cnpj: { generator: { type: 'invalid' } }) } + .to raise_error(CnpjGen::ValidationError) + end + + it 'raises CNPJ generator TypeMismatchError' do + expect { described_class.new(cnpj: { generator: { prefix: 123 } }) } + .to raise_error(CnpjGen::TypeMismatchError) + end + + it 'raises CNPJ validator ValidationError' do + expect { described_class.new(cnpj: { validator: { type: 'invalid' } }) } + .to raise_error(CnpjVal::ValidationError) + end + end + + context 'when called with both a settings Hash and keywords' do + it 'raises InvalidArgumentCombinationError' do + expect do + described_class.new({ cpf: {} }, cnpj: CnpjUtils.new) + end.to raise_error(BrUtils::InvalidArgumentCombinationError) + end + + it 'raises for false settings with keywords' do + expect do + described_class.new(false, cpf: {}) + end.to raise_error(BrUtils::InvalidArgumentCombinationError) + end + end + end + + describe 'resource accessors' do + subject(:utils) { described_class.new } + + it 'returns the CpfUtils instance' do + expect(utils.cpf).to be_a(CpfUtils) + end + + it 'returns the CpfFormatter instance' do + expect(utils.cpf.formatter).to be_a(CpfFmt::CpfFormatter) + end + + it 'returns the CpfGenerator instance' do + expect(utils.cpf.generator).to be_a(CpfGen::CpfGenerator) + end + + it 'returns the CpfValidator instance' do + expect(utils.cpf.validator).to be_a(CpfVal::CpfValidator) + end + + it 'returns the CnpjUtils instance' do + expect(utils.cnpj).to be_a(CnpjUtils) + end + + it 'returns the CnpjFormatter instance' do + expect(utils.cnpj.formatter).to be_a(CnpjFmt::CnpjFormatter) + end + + it 'returns the CnpjGenerator instance' do + expect(utils.cnpj.generator).to be_a(CnpjGen::CnpjGenerator) + end + + it 'returns the CnpjValidator instance' do + expect(utils.cnpj.validator).to be_a(CnpjVal::CnpjValidator) + end + end + + describe '#cnpj=' do + subject(:utils) { described_class.new } + + context 'when called with a CnpjUtils instance' do + it 'sets the CnpjUtils instance' do + cnpj_utils = CnpjUtils.new + utils.cnpj = cnpj_utils + + expect(utils.cnpj).to equal(cnpj_utils) + end + end + + context 'when called with literal Hash parameters' do + let(:cnpj_utils_options) do + { + formatter: { + hidden: true, + hidden_key: '#', + hidden_start: 8, + hidden_end: 11, + dot_key: '_', + slash_key: '|', + dash_key: ' dv ' + }, + generator: { + format: true, + prefix: '12345678', + type: 'numeric' + }, + validator: { + type: 'numeric' + } + } + end + + it 'sets CnpjUtils built from options' do + utils.cnpj = cnpj_utils_options + + aggregate_failures do + expect(utils.cnpj).to be_a(CnpjUtils) + expect_options_containing(utils.cnpj.formatter.options.all, cnpj_utils_options[:formatter]) + expect_options_containing(utils.cnpj.generator.options.all, cnpj_utils_options[:generator]) + expect_options_containing(utils.cnpj.validator.options.all, cnpj_utils_options[:validator]) + end + end + end + + context 'when called with nil' do + it 'creates a new default CnpjUtils' do + original = utils.cnpj + utils.cnpj = nil + + aggregate_failures do + expect(utils.cnpj).to be_a(CnpjUtils) + expect(utils.cnpj).not_to equal(original) + end + end + end + end + + describe '#cpf=' do + subject(:utils) { described_class.new } + + context 'when called with a CpfUtils instance' do + it 'sets the CpfUtils instance' do + cpf_utils = CpfUtils.new + utils.cpf = cpf_utils + + expect(utils.cpf).to equal(cpf_utils) + end + end + + context 'when called with literal Hash parameters' do + let(:cpf_utils_options) do + { + formatter: { + hidden: true, + hidden_key: '#', + hidden_start: 8, + hidden_end: 10, + dot_key: '_', + dash_key: ' dv ' + }, + generator: { + format: true, + prefix: '12345678' + } + } + end + + it 'sets CpfUtils built from options' do + utils.cpf = cpf_utils_options + + aggregate_failures do + expect(utils.cpf).to be_a(CpfUtils) + expect_options_containing(utils.cpf.formatter.options.all, cpf_utils_options[:formatter]) + expect_options_containing(utils.cpf.generator.options.all, cpf_utils_options[:generator]) + end + end + end + + context 'when called with nil' do + it 'creates a new default CpfUtils' do + original = utils.cpf + utils.cpf = nil + + aggregate_failures do + expect(utils.cpf).to be_a(CpfUtils) + expect(utils.cpf).not_to equal(original) + end + end + end + end + + describe 'CPF utils through BrUtils' do + describe '#format' do + CPF_FORMAT_FACTORY_CONTEXTS.each do |context_description, factory_key| + context context_description do + let(:format_fn) { CPF_FORMAT_FACTORIES.fetch(factory_key) } + + it 'matches CpfFormatter#format behavior' do + input = '80976511061' + formatter = CpfFmt::CpfFormatter.new + + expect(format_fn.call(input)).to eq(formatter.format(input)) + end + + it 'forwards formatting options' do + expect(format_fn.call('80976511061', '_', ' dv ')).to eq('809_765_110 dv 61') + end + end + end + + context 'when constructor formatter defaults are set' do + it 'applies defaults when method options are omitted' do + utils = described_class.new( + cpf: { + formatter: { + hidden: true, + hidden_key: '#' + } + } + ) + + expect(utils.cpf.format('80976511061')).to include('#') + end + end + + it 'formats a basic CPF string' do + expect(described_class.new.cpf.format('12345678901')).to eq('123.456.789-01') + end + end + + describe '#generate' do + CPF_GENERATE_FACTORY_CONTEXTS.each do |context_description, factory_key| + context context_description do + let(:generate_fn) { CPF_GENERATE_FACTORIES.fetch(factory_key) } + + it 'matches CpfGenerator length behavior' do + generator = CpfGen::CpfGenerator.new + result = generate_fn.call + + aggregate_failures do + expect(result).to match(/\A\d{11}\z/) + expect(result.length).to eq(generator.generate.length) + end + end + + it 'forwards generation options' do + result = generate_fn.call(format: true, prefix: '12345') + + expect(result).to match(/\A123\.45\d\.\d{3}-\d{2}\z/) + end + + it 'returns a deterministic CPF for a full prefix' do + prefix = '123456789' + results = Array.new(20) { generate_fn.call(prefix: prefix) } + + expect(results.uniq.size).to eq(1) + end + end + end + + it 'generates an 11-digit CPF' do + expect(described_class.new.cpf.generate.length).to eq(11) + end + end + + describe '#is_valid' do + it 'returns true for a valid CPF' do + expect(described_class.new.cpf.is_valid('52998224725')).to be(true) + end + + it 'returns false for an invalid CPF' do + expect(described_class.new.cpf.is_valid('12345678901')).to be(false) + end + end + end + + describe 'CNPJ utils through BrUtils' do + describe '#format' do + CNPJ_FORMAT_FACTORY_CONTEXTS.each do |context_description, factory_key| + context context_description do + let(:format_fn) { CNPJ_FORMAT_FACTORIES.fetch(factory_key) } + + it 'matches CnpjFormatter#format behavior' do + input = '91415732000793' + formatter = CnpjFmt::CnpjFormatter.new + + expect(format_fn.call(input)).to eq(formatter.format(input)) + end + + it 'forwards formatting options' do + expect(format_fn.call('01ABC234000X56', '|')).to eq('01.ABC.234|000X-56') + end + end + end + + context 'when constructor formatter defaults are set' do + it 'applies defaults when method options are omitted' do + utils = described_class.new( + cnpj: { + formatter: { + hidden: true, + hidden_key: '#' + } + } + ) + + expect(utils.cnpj.format('12ABC34500DE99')).to include('#') + end + end + + it 'formats a basic CNPJ string' do + expect(described_class.new.cnpj.format('12345678000195')).to eq('12.345.678/0001-95') + end + end + + describe '#generate' do + CNPJ_GENERATE_FACTORY_CONTEXTS.each do |context_description, factory_key| + context context_description do + let(:generate_fn) { CNPJ_GENERATE_FACTORIES.fetch(factory_key) } + + it 'matches CnpjGenerator length behavior' do + generator = CnpjGen::CnpjGenerator.new + result = generate_fn.call + + aggregate_failures do + expect(result).to match(/\A[0-9A-Z]{14}\z/) + expect(result.length).to eq(generator.generate.length) + end + end + + it 'forwards generation options' do + result = generate_fn.call(format: true, prefix: '12345', type: 'numeric') + + expect(result).to match(%r{\A12\.345\.\d{3}/\d{4}-\d{2}\z}) + end + + it 'returns a deterministic CNPJ for a full prefix' do + prefix = '123456780009' + results = Array.new(20) { generate_fn.call(prefix: prefix) } + + expect(results.uniq.size).to eq(1) + end + end + end + + it 'generates a 14-character CNPJ' do + expect(described_class.new.cnpj.generate.length).to eq(14) + end + end + + describe '#is_valid' do + CNPJ_IS_VALID_FACTORY_CONTEXTS.each do |context_description, factory_key| + context context_description do + let(:is_valid_fn) { CNPJ_IS_VALID_FACTORIES.fetch(factory_key) } + + it 'matches CnpjValidator#is_valid behavior' do + input = '91415732000793' + validator = CnpjVal::CnpjValidator.new + + expect(is_valid_fn.call(input)).to eq(validator.is_valid(input)) + end + + it 'forwards validation options' do + input = '1QB5UKALPYFP59' + + aggregate_failures do + expect(is_valid_fn.call(input, type: 'numeric')).to be(false) + expect(is_valid_fn.call(input, type: 'alphanumeric')).to be(true) + end + end + + it 'validates formatted and unformatted strings' do + aggregate_failures do + expect(is_valid_fn.call('1QB5UKALPYFP59')).to be(true) + expect(is_valid_fn.call('1QB5.UKAL.PYF/P59')).to be(true) + expect(is_valid_fn.call('AB123CDE0001555')).to be(false) + end + end + end + end + + it 'returns true for a valid numeric CNPJ' do + expect(described_class.new.cnpj.is_valid('11222333000181')).to be(true) + end + + it 'returns false for an invalid CNPJ' do + expect(described_class.new.cnpj.is_valid('11111111111111')).to be(false) + end + end + end + + describe 'package smoke' do + it 'exposes BrUtils as a class' do + aggregate_failures do + expect(described_class).to be_a(Class) + expect(described_class.new).to be_a(described_class) + end + end + + it 'exposes a VERSION string' do + expect(described_class::VERSION).to be_a(String).and match(/\A\d+\.\d+\.\d+\z/) + end + end +end From 2ab2772ce09f46995fc2a352a84a890caef86537 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Mon, 10 Aug 2026 17:39:25 -0300 Subject: [PATCH 4/7] docs(br-utils): create CHANGELOG file Co-authored-by: Claude Opus 5 Co-authored-by: Cursor Agent --- packages/br-utilities/CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/br-utilities/CHANGELOG.md b/packages/br-utilities/CHANGELOG.md index f2eea00..87656f0 100644 --- a/packages/br-utilities/CHANGELOG.md +++ b/packages/br-utilities/CHANGELOG.md @@ -1 +1,17 @@ # br-utilities + +## 1.0.0 + +### 🚀 Stable Version Released! + +Unified toolkit to deal with Brazilian documents (CPF and CNPJ): validation, formatting, and generation of valid IDs. Main features: + +- **Unified façade**: `BrUtils` aggregates configurable `CpfUtils` and `CnpjUtils` behind `#cpf` / `#cnpj` accessors (with setters; `nil` resets to defaults). +- **Constructor flexibility**: accept a settings `Hash` or keyword args (`cpf:` / `cnpj:` nested mappings, or flat `cpf_formatter:` / `cnpj_validator:`-style kwargs; nested wins when both present). +- **Quick helpers**: `BrUtils.cpf` / `.cnpj` alias mutable `BrUtils::DEFAULT` (process-wide; prefer `BrUtils.new` under concurrency). +- **Two-tier re-exports**: main classes at the façade root (`BrUtils::CpfFormatter`, `BrUtils::CnpjValidator`, …); full sibling surface under `BrUtils::CpfFmt` / `CpfUtils` / `CnpjFmt` / …. +- **One install**: depends on `cpf-utilities` and `cnpj-utilities` so both domains ship without requiring each gem separately. +- **Alphanumeric CNPJ**: CNPJ path supports 14-character alphanumeric IDs via `cnpj-utilities` (numeric CPF via `cpf-utilities`). +- **Structured errors**: `BrUtils::TypeMismatchError` / `InvalidArgumentCombinationError` (+ `BrUtils::Error` marker) for façade misuse; domain errors propagate from nested packages. + +For detailed usage and API reference, see the [README](./README.md). From bc8c67d768cce794da2841adde426e9629ac477b Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Mon, 10 Aug 2026 17:39:46 -0300 Subject: [PATCH 5/7] docs(br-utils): create README file Co-authored-by: Claude Opus 5 Co-authored-by: Cursor Agent --- packages/br-utilities/README.md | 707 ++++++++++++++++++++++++++++++++ 1 file changed, 707 insertions(+) create mode 100644 packages/br-utilities/README.md diff --git a/packages/br-utilities/README.md b/packages/br-utilities/README.md new file mode 100644 index 0000000..abc1e1d --- /dev/null +++ b/packages/br-utilities/README.md @@ -0,0 +1,707 @@ +![br-utilities for Ruby](https://br-utils.vercel.app/img/cover_br-utils.jpg) + +[![Gem Version](https://img.shields.io/gem/v/br-utilities)](https://rubygems.org/gems/br-utilities) +[![Gem Downloads](https://img.shields.io/gem/dt/br-utilities)](https://rubygems.org/gems/br-utilities) +[![Ruby Version](https://img.shields.io/gem/rv/br-utilities)](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) + +> 🚀 **Full support for the [new alphanumeric CNPJ format](https://github.com/user-attachments/files/23937961/calculodvcnpjalfanaumerico.pdf).** + +> 🌎 [Acessar documentação em português](./README.pt.md) + +A Ruby toolkit to handle the main operations with Brazilian-related data: CPF (Individual's Taxpayer ID) and CNPJ (Business Tax ID). It wraps [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) and [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) in a single façade class (`BrUtils`). + +## Ruby Support + +| ![Ruby 3.1](https://img.shields.io/badge/Ruby-3.1-CC342D?logo=ruby&logoColor=white) | ![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) | ![Ruby 4.0](https://img.shields.io/badge/Ruby-4.0-CC342D?logo=ruby&logoColor=white) | +| --- | --- | --- | --- | --- | +| Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | + +Requires Ruby **≥ 3.1** (see `required_ruby_version` in the gemspec). + +## Features + +- ✅ **Unified top-level API**: Class helpers `BrUtils.cpf` / `.cnpj` alias `BrUtils::DEFAULT`; each domain offers `format`, `generate`, and `is_valid` +- ✅ **Bundled domains**: [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) and [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) installed together +- ✅ **Alphanumeric CNPJ**: Full support for the new alphanumeric CNPJ format (introduced in 2026) +- ✅ **Reusable instance**: `BrUtils` class with optional default CPF and CNPJ settings (nested mappings, flat component kwargs, or pre-built utils instances) +- ✅ **Two-tier access**: Prefer main-class shortcuts at the façade root (`BrUtils::CpfFormatter`, `BrUtils::CnpjValidator`, …); Options, helpers, and errors live under nested package modules (`BrUtils::CpfFmt`, `BrUtils::CnpjUtils`, …). Root siblings (`CpfUtils`, `CnpjUtils`, `CpfFmt`, …) still work +- ✅ **Per-call overrides**: Configure defaults on the façade / domain utils; override options on a single `format` / `generate` / `is_valid` call +- ✅ **Error handling**: Domain errors propagate unchanged from the bundled packages; this gem defines `BrUtils::TypeMismatchError` and `BrUtils::InvalidArgumentCombinationError` for API misuse + +## Installation + +Install the gem directly: + +```bash +gem install br-utilities +``` + +Or add it to your `Gemfile` and run `bundle install`: + +```ruby +gem 'br-utilities' +``` + +This installs **`br-utilities`** together with [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) and [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) (which in turn pull in the CPF and CNPJ component packages). You do **not** need separate `gem install` / `gem` lines for the domain packages when using **`br-utilities`**. + +## Require + +```ruby +require 'br-utilities' +``` + +## Quick Start + +Prefer the aggregator class helpers (`BrUtils.cpf` / `BrUtils.cnpj`) for one-off calls — they forward to `BrUtils::DEFAULT`: + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +# CPF (personal ID) +BrUtils.cpf.format(cpf) # => "123.456.789-09" +BrUtils.cpf.generate(format: true) # => e.g. "478.442.410-55" +BrUtils.cpf.is_valid('123.456.789-09') # => true + +# CNPJ (business ID) +BrUtils.cnpj.format(cnpj) # => "03.603.568/0001-95" +BrUtils.cnpj.generate(format: true) # => e.g. "AB.123.CDE/0001-55" +BrUtils.cnpj.is_valid('98765432000198') # => true +``` + +**With domain aggregators:** + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +CpfUtils.format(cpf) # => "123.456.789-09" +CnpjUtils.format(cnpj) # => "03.603.568/0001-95" +CpfUtils.is_valid(cpf) # => true +CnpjUtils.is_valid(cnpj) # => true +``` + +**With functional helpers** (root sibling modules, loaded by this gem): + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +CpfFmt.cpf_fmt(cpf) # => "123.456.789-09" +CpfVal.cpf_val(cpf) # => true +CnpjFmt.cnpj_fmt(cnpj) # => "03.603.568/0001-95" +CnpjVal.cnpj_val(cnpj) # => true +``` + +## Usage + +You can work in these equivalent ways: + +1. **`BrUtils.cpf` / `.cnpj`** — class helpers for quick one-off calls (forward to `DEFAULT`). +2. **`BrUtils::DEFAULT`** — mutable shared singleton (same object the class helpers use; process-wide / not thread-isolated). +3. **`BrUtils.new`** — configurable instance with shared defaults across both CPF and CNPJ domains. +4. **Domain aggregators** — `CpfUtils` / `CnpjUtils` (or `BrUtils::CpfUtils` / `BrUtils::CnpjUtils`) directly. +5. **Main classes under `BrUtils`** — `BrUtils::CpfFormatter`, `BrUtils::CnpjGenerator`, and related shortcuts. +6. **Nested package modules** — Options, helpers, errors, and types via `BrUtils::CpfFmt` / `CpfGen` / `CpfVal` / `CnpjFmt` / `CnpjGen` / `CnpjVal` / `CpfUtils` / `CnpjUtils`. +7. **Root sibling modules** (still supported) — `CpfFmt`, `CnpjUtils`, and the rest unchanged. + +All approaches expose the same options and behavior within each domain. For exhaustive option tables and component-specific details, see the README of each [bundled package](#bundled-packages). + +### Class helpers (`BrUtils.cpf` / `.cnpj`) + +These class methods return the same domain utils instances as `BrUtils::DEFAULT`. Prefer them for one-off calls: + +```ruby +BrUtils.cpf.format('12345678909') +BrUtils.cpf.generate(format: true) +BrUtils.cpf.is_valid('12345678909') + +BrUtils.cnpj.format('03603568000195') +BrUtils.cnpj.generate(type: 'numeric') +BrUtils.cnpj.is_valid('98765432000198') +``` + +### `BrUtils::DEFAULT` (default instance) + +`BrUtils::DEFAULT` is the pre-built, **mutable** singleton behind the class helpers (parity with the JS default export / Python `br_utils`). Its configuration is **process-wide and shared across threads**: mutating it (e.g. `DEFAULT.cpf = …`) affects subsequent `BrUtils.cpf` / `.cnpj` calls for every caller in the process. Prefer `BrUtils.new` or per-call options for concurrent or isolated work; custom instances stay independent of `DEFAULT`: + +```ruby +BrUtils::DEFAULT.cpf = CpfUtils.new(formatter: { dash_key: '|' }) +BrUtils.cpf.format('12345678909') # => "123.456.789|09" + +custom = BrUtils.new +custom.cpf.format('12345678909') # => "123.456.789-09" (unaffected) +``` + +### `BrUtils` (class) + +For custom default CPF or CNPJ utils, create your own instance: + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true } + }, + cnpj: { + formatter: { hidden: true }, + generator: { type: 'numeric', format: true }, + validator: { type: 'numeric' } + } +) + +utils.cpf.format('12345678909') # => "123.###.###-##" +utils.cpf.generate # => e.g. "005.265.352-88" +utils.cnpj.format('03603568000195') # => "03.603.***/****-**" +utils.cnpj.generate # => e.g. "73.008.535/0005-06" + +# Access or replace internal domain instances +utils.cpf # => CpfUtils +utils.cnpj # => CnpjUtils +``` + +- **`BrUtils.new(settings = nil, **keywords)`**: Optional settings. Pass either a settings `Hash` with `:cpf` and/or `:cnpj` keys, **or** the same keys (plus flat component kwargs) as keyword arguments — not both (passing both raises `BrUtils::InvalidArgumentCombinationError`). + - **`:cpf` / `:cnpj`**: A pre-built `CpfUtils` / `CnpjUtils` instance **or** a configuration `Hash` spread into the corresponding utils constructor. Within that `Hash`, each resource key (`:formatter`, `:generator`, and `:validator` for CNPJ) accepts either an options object or a mapping of option values. + - **`:cpf_formatter`**, **`:cpf_generator`**, **`:cnpj_formatter`**, **`:cnpj_generator`**, **`:cnpj_validator`**: Flat convenience arguments when only individual components need customization. They are ignored when the corresponding `:cpf` or `:cnpj` argument is provided. +- **`#cpf`**, **`#cnpj`**: Accessors (getters and setters) for the domain utils instances. Setters accept a utils instance, a configuration `Hash`, or `nil` to reset to defaults (replaces the entire instance; does not merge). + +Flat constructor options (alternative to nested `:cpf` / `:cnpj` mappings): + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf_formatter: CpfFmt::CpfFormatterOptions.new(hidden: true, hidden_key: '#'), + cpf_generator: CpfGen::CpfGeneratorOptions.new(format: true), + cnpj_formatter: CnpjFmt::CnpjFormatterOptions.new(hidden: true, hidden_key: '#'), + cnpj_generator: CnpjGen::CnpjGeneratorOptions.new(format: true, type: 'numeric'), + cnpj_validator: CnpjVal::CnpjValidatorOptions.new(type: 'numeric') +) +``` + +Passing a settings `Hash` positional argument together with any keyword raises: + +```ruby +BrUtils.new({ cpf: {} }, cnpj: CnpjUtils.new) +# raises BrUtils::InvalidArgumentCombinationError +``` + +### Instance defaults and per-call overrides + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true } + }, + cnpj: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true }, + validator: { type: 'numeric' } + } +) + +cpf = '12345678909' +cnpj = '03603568000195' + +utils.cpf.format(cpf) # => "123.###.###-##" +utils.cpf.format(cpf, hidden: false) # this call only: unmasked +utils.cpf.generate(format: false) # this call only: compact output + +utils.cnpj.format(cnpj) # => "03.603.###/####-##" +utils.cnpj.format(cnpj, hidden: false) # this call only: unmasked +utils.cnpj.is_valid('1QB5UKALPYFP59') # => false (instance validator is numeric-only) +utils.cnpj.is_valid( # => true for this call + '1QB5UKALPYFP59', + type: 'alphanumeric' +) +``` + +Passing a `CnpjFmt::CnpjFormatterOptions`, `CnpjGen::CnpjGeneratorOptions`, or `CnpjVal::CnpjValidatorOptions` instance into the `BrUtils` constructor stores that object by reference — mutating it later affects subsequent calls with no per-call override. + +To change a single nested option without replacing the whole domain utils, mutate via the domain accessors (e.g. `utils.cpf.formatter.options.hidden = true`). + +### CPF operations + +CPF methods are accessed via `BrUtils.cpf`, `utils.cpf`, `CpfUtils`, or the `CpfFmt` / `CpfGen` / `CpfVal` helpers. CPF uses the API from [`cpf-utilities`](../cpf-utilities/README.md). + +#### Formatting (`#format` / `CpfFmt.cpf_fmt`) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `hidden` | `Boolean` | `false` | When `true`, mask digits in `hidden_start`–`hidden_end` with `hidden_key` | +| `hidden_key` | `String` | `'*'` | Character(s) used to replace masked digits | +| `hidden_start` | `Integer` | `3` | Start index (0–10, inclusive) of the range to hide | +| `hidden_end` | `Integer` | `10` | End index (0–10, inclusive) of the range to hide | +| `dot_key` | `String` | `'.'` | Dot delimiter (e.g. in `123.456.789`) | +| `dash_key` | `String` | `'-'` | Dash delimiter (e.g. before check digits `…-09`) | +| `escape` | `Boolean` | `false` | When `true`, escape HTML special characters in the result | +| `encode` | `Boolean` | `false` | When `true`, URL-encode the result (similar to JavaScript `encodeURIComponent`) | +| `on_fail` | `Proc` / callable | returns `''` | Callback when sanitized input length ≠ 11; return value is used as result | + +Default **`on_fail`** returns an empty string. Invalid length does **not** raise from `#format`. + +```ruby +require 'br-utilities' + +cpf = '12345678909' + +BrUtils.cpf.format(cpf) # => "123.456.789-09" +BrUtils.cpf.format(cpf, hidden: true, hidden_key: '#') # => "123.###.###-##" +BrUtils.cpf.format(cpf, dot_key: '', dash_key: '_') # => "123456789_09" + +CpfFmt.cpf_fmt(cpf, hidden: true) # => "123.***.***-**" +``` + +#### Generation (`#generate` / `CpfGen.cpf_gen`) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | When `true`, return the generated CPF in standard format (`000.000.000-00`) | +| `prefix` | `String` | `''` | Partial start string (0–9 digits). Non-digits are stripped; missing characters are generated and check digits 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. + +```ruby +require 'br-utilities' + +BrUtils.cpf.generate # => e.g. "11508890048" +BrUtils.cpf.generate(format: true) # => e.g. "661.134.831-00" +BrUtils.cpf.generate(prefix: '123456789') # => "12345678909" +CpfGen.cpf_gen(prefix: '123456789', format: true) # => "123.456.789-09" +``` + +#### Validation (`#is_valid` / `CpfVal.cpf_val`) + +Accepts formatted or unformatted CPF strings (or an `Array` of strings). Returns **`true`** or **`false`** without raising for invalid CPF. No validator options exist. + +```ruby +require 'br-utilities' + +BrUtils.cpf.is_valid('12345678909') # => true +BrUtils.cpf.is_valid('123.456.789-09') # => true +BrUtils.cpf.is_valid('12345678900') # => false +CpfVal.cpf_val('12345678909') # => true +``` + +### CNPJ operations + +CNPJ methods are accessed via `BrUtils.cnpj`, `utils.cnpj`, `CnpjUtils`, or the `CnpjFmt` / `CnpjGen` / `CnpjVal` helpers. CNPJ uses the API from [`cnpj-utilities`](../cnpj-utilities/README.md). + +#### Formatting (`#format` / `CnpjFmt.cnpj_fmt`) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `hidden` | `Boolean` | `false` | When `true`, mask characters in `hidden_start`–`hidden_end` with `hidden_key` | +| `hidden_key` | `String` | `'*'` | Character(s) used to replace masked characters | +| `hidden_start` | `Integer` | `5` | Start index (0–13, inclusive) of the range to hide | +| `hidden_end` | `Integer` | `13` | End index (0–13, inclusive) of the range to hide | +| `dot_key` | `String` | `'.'` | Dot delimiter (e.g. in `12.345.678`) | +| `slash_key` | `String` | `'/'` | Slash delimiter (e.g. before branch `…/0001-90`) | +| `dash_key` | `String` | `'-'` | Dash delimiter (e.g. before check digits `…-90`) | +| `escape` | `Boolean` | `false` | When `true`, escape HTML special characters in the result | +| `encode` | `Boolean` | `false` | When `true`, URL-encode the result (similar to JavaScript `encodeURIComponent`) | +| `on_fail` | `Proc` / callable | returns `''` | Callback when sanitized input length ≠ 14; return value is used as result | + +Default **`on_fail`** returns an empty string. Wrong input types raise **`CnpjFmt::TypeMismatchError`**. + +```ruby +require 'br-utilities' + +cnpj = '03603568000195' + +BrUtils.cnpj.format(cnpj) # => "03.603.568/0001-95" +BrUtils.cnpj.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" +BrUtils.cnpj.format( # => "03.603.###/####-##" + cnpj, + hidden: true, + hidden_key: '#' +) +BrUtils.cnpj.format( # => "03603568|0001_95" + cnpj, + dot_key: '', + slash_key: '|', + dash_key: '_' +) + +CnpjFmt.cnpj_fmt(cnpj) # => "03.603.568/0001-95" +``` + +#### Generation (`#generate` / `CnpjGen.cnpj_gen`) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | When `true`, return the generated CNPJ in standard format (`00.000.000/0000-00`) | +| `prefix` | `String` | `''` | Partial start string (0–12 alphanumeric chars). Missing characters are generated and check digits computed. | +| `type` | `String` | `'alphanumeric'` | Character set for the randomly generated part: `'numeric'`, `'alphabetic'`, or `'alphanumeric'`. **Check digits are always numeric.** | + +Prefix rules: base ID (first 8 chars) and branch ID (chars 9–12) cannot be all zeros; 12 repeated digits (e.g. `111111111111`) are also not allowed. + +```ruby +require 'br-utilities' + +BrUtils.cnpj.generate # => e.g. "1GJTR3J3XSSA96" +BrUtils.cnpj.generate(format: true) # => e.g. "V1.J0V.8WE/DVZ7-50" +BrUtils.cnpj.generate( # => e.g. "12345678855883" + prefix: '12345678', + type: 'numeric' +) +CnpjGen.cnpj_gen(type: 'numeric') # => e.g. "65453043000178" +``` + +#### Validation (`#is_valid` / `CnpjVal.cnpj_val`) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `case_sensitive` | `Boolean` | `true` | When `false`, lowercase letters are accepted for alphanumeric CNPJ (input is uppercased before validation). | +| `type` | `String` | `'alphanumeric'` | `'numeric'`: only digits (0–9); `'alphanumeric'`: digits and letters (0–9, A–Z). | + +```ruby +require 'br-utilities' + +BrUtils.cnpj.is_valid('98765432000198') # => true +BrUtils.cnpj.is_valid('98765432000199') # => false +BrUtils.cnpj.is_valid('1QB5UKALPYFP59') # => true +BrUtils.cnpj.is_valid('1QB5UKALpyfp59') # => false +BrUtils.cnpj.is_valid( # => true + '1QB5UKALpyfp59', + case_sensitive: false +) +BrUtils.cnpj.is_valid( # => false + '1QB5UKALPYFP59', + type: 'numeric' +) + +CnpjVal.cnpj_val('98765432000198') # => true +CnpjVal.cnpj_val('1QB5UKALpyfp59', case_sensitive: false) # => true +CnpjVal.cnpj_val('1QB5UKALPYFP59', type: 'numeric') # => false +``` + +Invalid CNPJ returns **`false`** without raising. Wrong input types raise **`CnpjVal::TypeMismatchError`**. + +### Domain aggregators (standalone) + +Use `CpfUtils` or `CnpjUtils` directly when you only need one domain: + +```ruby +require 'br-utilities' + +cpf_utils = CpfUtils.new( + formatter: { hidden: true }, + generator: { format: true } +) + +cnpj_utils = CnpjUtils.new( + formatter: { hidden: true }, + generator: { format: true }, + validator: { type: 'numeric' } +) + +cpf_utils.format('12345678909') # => "123.***.***-**" +cnpj_utils.format('03603568000195') # => "03.603.***/****-**" +``` + +### Accessing components + +Each domain aggregator exposes its internal formatter, generator, and validator: + +```ruby +require 'br-utilities' + +utils = BrUtils.new + +utils.cpf.formatter.format('12345678909', hidden: true) # => "123.***.***-**" +utils.cpf.generator.generate(format: true) # => e.g. "545.507.690-68" +utils.cpf.validator.is_valid('12345678909') # => true + +utils.cnpj.formatter.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" +utils.cnpj.generator.generate(format: true) # => e.g. "8O.BE5.2KL/UI0Y-06" +utils.cnpj.validator.is_valid('03603568000195') # => true +``` + +### Using component classes and nested modules + +Preferred paths after `require 'br-utilities'`: + +```ruby +require 'br-utilities' + +# Main classes at the façade root +formatter = BrUtils::CpfFormatter.new(hidden: true) +generator = BrUtils::CnpjGenerator.new(type: 'numeric') +validator = BrUtils::CnpjValidator.new + +formatter.format('12345678909') # => "123.***.***-**" + +# Options, helpers, and errors under nested package modules +options = BrUtils::CpfFmt::CpfFormatterOptions.new(dash_key: '|') +BrUtils::CpfFmt.cpf_fmt('12345678909') # => "123.456.789-09" + +begin + BrUtils::CnpjFmt.cnpj_fmt(12_345) +rescue BrUtils::CnpjFmt::TypeMismatchError + # wrong input type +end +``` + +Root siblings remain supported (same objects as the nests): + +```ruby +CpfFmt.cpf_fmt('12345678909', dash_key: '|') # => "123.456.789|09" +CpfGen.cpf_gen(format: true) # => e.g. "478.442.410-55" +CpfVal.cpf_val('12345678909') # => true +CnpjFmt.cnpj_fmt('01ABC234000X56', slash_key: '|') # => "01.ABC.234|000X-56" +CnpjGen.cnpj_gen(type: 'numeric') # => e.g. "65453043000178" +CnpjVal.cnpj_val('9JN7MGLJZXIO50') # => true +``` + +See [`cpf-utilities`](../cpf-utilities/README.md) and [`cnpj-utilities`](../cnpj-utilities/README.md) for full option and error details. + +### Mixing styles + +Use `BrUtils` where a shared configuration helps, and standalone components or helpers elsewhere — they are the same underlying classes: + +```ruby +require 'br-utilities' + +utils = BrUtils.new(cnpj: { validator: { type: 'numeric' } }) + +# Via façade +utils.cpf.format('12345678909') # => "123.456.789-09" + +# Via component returned by the façade +utils.cnpj.formatter.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" + +# Via a separate component instance +BrUtils::CnpjFormatter.new.format('03603568000195') # => "03.603.568/0001-95" + +# Via functional helpers +CpfFmt.cpf_fmt('12345678909') # => "123.456.789-09" +CnpjVal.cnpj_val('98.765.432/0001-98') # => true +``` + +## API + +### Exports + +After `require 'br-utilities'`: + +- **`BrUtils`**: Façade class to create an instance with optional default CPF and CNPJ utils settings. +- **`BrUtils.cpf` / `.cnpj`**: Class helpers that forward to `BrUtils::DEFAULT` domain accessors. +- **`BrUtils::DEFAULT`**: Mutable pre-built `BrUtils` instance (same object the class helpers use). Process-wide / shared across threads — prefer `BrUtils.new` or per-call options under concurrency. +- **`BrUtils::VERSION`**: Gem version string. +- **Main-class shortcuts**: `BrUtils::CpfFormatter`, `BrUtils::CpfFormatterOptions`, `BrUtils::CpfGenerator`, `BrUtils::CpfGeneratorOptions`, `BrUtils::CpfValidator`, `BrUtils::CnpjFormatter`, `BrUtils::CnpjFormatterOptions`, `BrUtils::CnpjGenerator`, `BrUtils::CnpjGeneratorOptions`, `BrUtils::CnpjValidator`, `BrUtils::CnpjValidatorOptions` (same objects as the sibling classes). Error-marker shortcuts: `BrUtils::CpfFormatterError`, `BrUtils::CpfGeneratorError`, `BrUtils::CpfValidatorError`, `BrUtils::CnpjFormatterError`, `BrUtils::CnpjGeneratorError`, `BrUtils::CnpjValidatorError`. +- **Nested package modules**: `BrUtils::CpfUtils`, `BrUtils::CnpjUtils`, `BrUtils::CpfFmt`, `BrUtils::CpfGen`, `BrUtils::CpfVal`, `BrUtils::CnpjFmt`, `BrUtils::CnpjGen`, `BrUtils::CnpjVal` — full sibling surface (Options, helpers, errors, types). +- **Root sibling modules** (still supported): `CpfUtils`, `CnpjUtils`, `CpfFmt`, `CpfGen`, `CpfVal`, `CnpjFmt`, `CnpjGen`, `CnpjVal` — same objects as the nests. + +### Errors & Exceptions + +`BrUtils` defines only API-misuse errors for this gem’s argument rules. Domain errors are raised by the bundled packages and propagate unchanged. + +#### Defined by `br-utilities` + +Errors defined by this gem are **API misuse** only (wrong type or invalid argument combination). Every custom error includes the `BrUtils::Error` marker module. This gem defines **no** `BrUtils::DomainError` and no domain leaves — domain failures come only from the [bundled packages](#propagated-from-bundled-packages) and keep those packages’ namespaces (`CpfFmt::…`, `CnpjGen::…`, …). + +`rescue BrUtils::Error` catches **only** errors this gem raises. It does **not** catch component errors that propagate unchanged. + +##### Summary + +| Class | Inherits from | Category | Trigger condition | +|-------|---------------|----------|-------------------| +| `BrUtils::InvalidArgumentCombinationError` | `BrUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include BrUtils::Error`) | API misuse | Non-`nil` settings `Hash` passed together with any non-`nil` keyword argument | +| `BrUtils::TypeMismatchError` | `BrUtils::TypeMismatchError < TypeError < StandardError` (+ `include BrUtils::Error`) | API misuse | Non-`nil` `settings` argument to `BrUtils.new` is not a `Hash` | + +##### `BrUtils::Error` (marker module) + +- **Inheritance:** module marker mixed into every custom error this gem raises 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 this gem raises. +- **Example:** N/A +- **How to rescue it:** + +```ruby +rescue BrUtils::Error + # TypeMismatchError, InvalidArgumentCombinationError from this gem only + # (not CpfFmt::*, CnpjGen::*, or other bundled-package errors) +``` + +##### `BrUtils::TypeMismatchError` + +- **Inheritance:** `BrUtils::TypeMismatchError < TypeError < StandardError` (includes `BrUtils::Error`) +- **Category:** API misuse — the caller passed a value of the wrong type. +- **When it is raised:** Raised when `BrUtils.new` receives a non-`nil` `settings` argument that is not a `Hash`. +- **Example:** + +```ruby +BrUtils.new('not-a-hash') # raises BrUtils::TypeMismatchError +BrUtils.new(false) # raises BrUtils::TypeMismatchError (false is non-nil) +``` + +- **How to rescue it:** + +```ruby +rescue BrUtils::TypeMismatchError + # this gem's type-contract violation + +rescue TypeError + # native type errors, including this gem's TypeMismatchError +``` + +##### `BrUtils::InvalidArgumentCombinationError` + +- **Inheritance:** `BrUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (includes `BrUtils::Error`) +- **Category:** API misuse — the caller mixed mutually exclusive argument patterns. +- **When it is raised:** Raised when `BrUtils.new` receives both a non-`nil` settings `Hash` and any non-`nil` keyword argument (`cpf:`, `cnpj:`, `cpf_formatter:`, …) at the same time. +- **Example:** + +```ruby +BrUtils.new({ cpf: { formatter: { hidden: true } } }, cnpj: { formatter: { hidden: true } }) +# raises BrUtils::InvalidArgumentCombinationError +``` + +- **How to rescue it:** + +```ruby +rescue BrUtils::InvalidArgumentCombinationError + # this gem's invalid signature combination + +rescue ArgumentError + # native argument errors, including this gem's InvalidArgumentCombinationError +``` + +##### Rescue granularity + +Each level is shown as its own standalone example (do not merge them into one `rescue` ladder — a broad native handler would make narrower clauses unreachable). + +```ruby +require 'br-utilities' + +# 1) Single native class — catches misuse errors of that kind, +# including non-library ones already handled elsewhere in the consumer's code. +begin + BrUtils.new('not-a-hash') +rescue TypeError + # BrUtils::TypeMismatchError and any other TypeError (library or not) +end + +begin + BrUtils.new({ cpf: {} }, cnpj: CnpjUtils.new) +rescue ArgumentError + # BrUtils::InvalidArgumentCombinationError and any other ArgumentError (library or not) +end +``` + +```ruby +require 'br-utilities' + +# 2) BrUtils::DomainError — not applicable: this gem defines no DomainError +# (and no domain leaves). Domain failures come from bundled packages only. +# begin +# BrUtils.new(cpf: { formatter: { hidden_start: -1 } }) +# rescue BrUtils::DomainError # NameError — constant is not defined +# end +``` + +```ruby +require 'br-utilities' + +# 3) BrUtils::Error — catches everything this gem raises, regardless of native ancestry. +# Does not catch CpfFmt::*, CnpjGen::*, or other bundled-package errors. +begin + BrUtils.new('not-a-hash') +rescue BrUtils::Error + # every custom error that includes BrUtils::Error +end +``` + +```ruby +require 'br-utilities' + +# 4) Specific leaf class — catches only that exact failure mode. +begin + BrUtils.new('not-a-hash') +rescue BrUtils::TypeMismatchError + # only BrUtils::TypeMismatchError +end +``` + +#### Propagated from bundled packages + +`BrUtils` does not redefine domain exception types. Construction, setters, and domain method calls raise the same errors as [`cpf-utilities`](../cpf-utilities/README.md) and [`cnpj-utilities`](../cnpj-utilities/README.md): + +- **CPF formatting**: `CpfFmt::TypeMismatchError`, `CpfFmt::OutOfRangeError`, `CpfFmt::ValidationError`, `CpfFmt::InvalidLengthError` (passed to `on_fail`, not raised by `#format`), and related classes. +- **CPF generation**: `CpfGen::TypeMismatchError`, `CpfGen::ValidationError`, and related classes. +- **CPF validation**: `CpfVal::TypeMismatchError` and related classes. +- **CNPJ formatting**: `CnpjFmt::TypeMismatchError`, `CnpjFmt::OutOfRangeError`, `CnpjFmt::ValidationError`, `CnpjFmt::InvalidLengthError` (passed to `on_fail`), and related classes. +- **CNPJ generation**: `CnpjGen::TypeMismatchError`, `CnpjGen::ValidationError`, and related classes. +- **CNPJ validation**: `CnpjVal::TypeMismatchError`, `CnpjVal::ValidationError`, and related classes. + +Invalid option types are typically **`TypeError`** subclasses (`*::TypeMismatchError`); invalid option values are domain errors under each package’s `DomainError` hierarchy. CPF and CNPJ validation failures return `false`. Formatting length failures are handled by **`on_fail`** (default returns an empty string). + +```ruby +require 'br-utilities' + +begin + BrUtils.new.cnpj.format(12_345) +rescue CnpjFmt::TypeMismatchError => e + puts e.message +end + +begin + BrUtils.new.cnpj.is_valid(12_345_678_000_198) +rescue CnpjVal::TypeMismatchError => e + puts e.message +end + +# Custom on_fail for invalid length +custom_fail = ->(value, _exception) { "Invalid: #{value}" } + +BrUtils.cpf.format('short', on_fail: custom_fail) # => "Invalid: short" +BrUtils.cnpj.format('short', on_fail: custom_fail) # => "Invalid: short" +BrUtils.cpf.format('short') # => "" (default on_fail) +``` + +For exhaustive exception lists and edge-case behavior, see each [bundled package](#bundled-packages) README. + +### Bundled packages + +| Package | Main resources | README | +|---------|----------------|--------| +| [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) | `CpfUtils`, `CpfFormatter`, `CpfGenerator`, `CpfValidator`, `CpfFmt.cpf_fmt`, `CpfGen.cpf_gen`, `CpfVal.cpf_val` | [docs](../cpf-utilities/README.md) | +| [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) | `CnpjUtils`, `CnpjFormatter`, `CnpjGenerator`, `CnpjValidator`, `CnpjFmt.cnpj_fmt`, `CnpjGen.cnpj_gen`, `CnpjVal.cnpj_val` | [docs](../cnpj-utilities/README.md) | + +All of the above are pulled in as dependencies of **`br-utilities`**. Interactive demos: [CPF](https://cpf-utils.vercel.app/) and [CNPJ](https://cnpj-utils.vercel.app/). + +## 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 cd6b3eb72467305762bf9e3361dc69fad2fef196 Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Mon, 10 Aug 2026 17:40:10 -0300 Subject: [PATCH 6/7] docs(br-utils): create Portuguese version of README file Co-authored-by: Claude Opus 5 Co-authored-by: Cursor Agent --- packages/br-utilities/README.pt.md | 700 +++++++++++++++++++++++++++++ 1 file changed, 700 insertions(+) create mode 100644 packages/br-utilities/README.pt.md diff --git a/packages/br-utilities/README.pt.md b/packages/br-utilities/README.pt.md new file mode 100644 index 0000000..40d50ac --- /dev/null +++ b/packages/br-utilities/README.pt.md @@ -0,0 +1,700 @@ +![br-utilities para Ruby](https://br-utils.vercel.app/img/cover_br-utils.jpg) + +> 🚀 **Suporte total ao [novo formato alfanumérico de CNPJ](https://github.com/user-attachments/files/23937961/calculodvcnpjalfanaumerico.pdf).** + +> 🌎 [Access documentation in English](./README.md) + +Kit em Ruby para as principais operações com dados brasileiros: CPF (Cadastro de Pessoa Física) e CNPJ (Cadastro Nacional da Pessoa Jurídica). Envolve [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) e [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) em uma única classe fachada (`BrUtils`). + +## Suporte a Ruby + +| ![Ruby 3.1](https://img.shields.io/badge/Ruby-3.1-CC342D?logo=ruby&logoColor=white) | ![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) | ![Ruby 4.0](https://img.shields.io/badge/Ruby-4.0-CC342D?logo=ruby&logoColor=white) | +| --- | --- | --- | --- | --- | +| Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | + +Requer Ruby **≥ 3.1** (veja `required_ruby_version` no gemspec). + +## Recursos + +- ✅ **API unificada de alto nível**: Helpers de classe `BrUtils.cpf` / `.cnpj` alias de `BrUtils::DEFAULT`; cada domínio oferece `format`, `generate` e `is_valid` +- ✅ **Domínios empacotados**: [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) e [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) instalados juntos +- ✅ **CNPJ alfanumérico**: Suporte completo ao novo formato alfanumérico de CNPJ (a partir de 2026) +- ✅ **Instância reutilizável**: Classe `BrUtils` com configurações padrão opcionais de CPF e CNPJ (mapeamentos aninhados, kwargs planos de componentes ou instâncias prontas de utils) +- ✅ **Acesso em dois níveis**: Prefira atalhos de classes principais na raiz da fachada (`BrUtils::CpfFormatter`, `BrUtils::CnpjValidator`, …); Options, helpers e erros ficam nos módulos aninhados (`BrUtils::CpfFmt`, `BrUtils::CnpjUtils`, …). Os irmãos na raiz (`CpfUtils`, `CnpjUtils`, `CpfFmt`, …) continuam funcionando +- ✅ **Sobrescritas por chamada**: Configure padrões na fachada / utils de domínio; sobrescreva opções em uma única chamada de `format` / `generate` / `is_valid` +- ✅ **Tratamento de erros**: Erros de domínio propagam inalterados dos pacotes incluídos; esta gem define `BrUtils::TypeMismatchError` e `BrUtils::InvalidArgumentCombinationError` para uso indevido da API + +## Instalação + +Instale a gem diretamente: + +```bash +gem install br-utilities +``` + +Ou adicione ao seu `Gemfile` e execute `bundle install`: + +```ruby +gem 'br-utilities' +``` + +Isso instala **`br-utilities`** junto com [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) e [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) (que por sua vez trazem os pacotes componentes de CPF e CNPJ). Você **não** precisa de `gem install` / linhas `gem` separados para os pacotes de domínio ao usar **`br-utilities`**. + +## Require + +```ruby +require 'br-utilities' +``` + +## Início rápido + +Prefira os helpers de classe do agregador (`BrUtils.cpf` / `BrUtils.cnpj`) para chamadas pontuais — eles encaminham para `BrUtils::DEFAULT`: + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +# CPF (pessoa física) +BrUtils.cpf.format(cpf) # => "123.456.789-09" +BrUtils.cpf.generate(format: true) # => ex.: "478.442.410-55" +BrUtils.cpf.is_valid('123.456.789-09') # => true + +# CNPJ (pessoa jurídica) +BrUtils.cnpj.format(cnpj) # => "03.603.568/0001-95" +BrUtils.cnpj.generate(format: true) # => ex.: "AB.123.CDE/0001-55" +BrUtils.cnpj.is_valid('98765432000198') # => true +``` + +**Com agregadores de domínio:** + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +CpfUtils.format(cpf) # => "123.456.789-09" +CnpjUtils.format(cnpj) # => "03.603.568/0001-95" +CpfUtils.is_valid(cpf) # => true +CnpjUtils.is_valid(cnpj) # => true +``` + +**Com helpers funcionais** (módulos irmãos na raiz, carregados por esta gem): + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +CpfFmt.cpf_fmt(cpf) # => "123.456.789-09" +CpfVal.cpf_val(cpf) # => true +CnpjFmt.cnpj_fmt(cnpj) # => "03.603.568/0001-95" +CnpjVal.cnpj_val(cnpj) # => true +``` + +## Utilização + +Você pode trabalhar destas formas equivalentes: + +1. **`BrUtils.cpf` / `.cnpj`** — helpers de classe para chamadas rápidas (encaminham para `DEFAULT`). +2. **`BrUtils::DEFAULT`** — singleton compartilhado mutável (o mesmo objeto usado pelos helpers de classe; em todo o processo / não isolado por thread). +3. **`BrUtils.new`** — instância configurável com padrões compartilhados entre os domínios CPF e CNPJ. +4. **Agregadores de domínio** — `CpfUtils` / `CnpjUtils` (ou `BrUtils::CpfUtils` / `BrUtils::CnpjUtils`) diretamente. +5. **Classes principais sob `BrUtils`** — `BrUtils::CpfFormatter`, `BrUtils::CnpjGenerator` e atalhos relacionados. +6. **Módulos aninhados do pacote** — Options, helpers, erros e tipos via `BrUtils::CpfFmt` / `CpfGen` / `CpfVal` / `CnpjFmt` / `CnpjGen` / `CnpjVal` / `CpfUtils` / `CnpjUtils`. +7. **Módulos irmãos na raiz** (ainda suportados) — `CpfFmt`, `CnpjUtils` e demais inalterados. + +Todas as abordagens expõem as mesmas opções e comportamento dentro de cada domínio. Para tabelas de opções exaustivas e detalhes específicos de cada componente, consulte o README de cada [pacote incluído](#pacotes-incluídos). + +### Helpers de classe (`BrUtils.cpf` / `.cnpj`) + +Esses métodos de classe retornam as mesmas instâncias de utils de domínio que `BrUtils::DEFAULT`. Prefira-os para chamadas pontuais: + +```ruby +BrUtils.cpf.format('12345678909') +BrUtils.cpf.generate(format: true) +BrUtils.cpf.is_valid('12345678909') + +BrUtils.cnpj.format('03603568000195') +BrUtils.cnpj.generate(type: 'numeric') +BrUtils.cnpj.is_valid('98765432000198') +``` + +### `BrUtils::DEFAULT` (instância padrão) + +`BrUtils::DEFAULT` é o singleton pré-construído e **mutável** por trás dos helpers de classe (paridade com a exportação padrão do JS / `br_utils` do Python). Sua configuração é **em todo o processo e compartilhada entre threads**: mutá-lo (ex.: `DEFAULT.cpf = …`) afeta chamadas subsequentes de `BrUtils.cpf` / `.cnpj` para todos os callers no processo. Prefira `BrUtils.new` ou opções por chamada para trabalho concorrente ou isolado; instâncias customizadas permanecem independentes de `DEFAULT`: + +```ruby +BrUtils::DEFAULT.cpf = CpfUtils.new(formatter: { dash_key: '|' }) +BrUtils.cpf.format('12345678909') # => "123.456.789|09" + +custom = BrUtils.new +custom.cpf.format('12345678909') # => "123.456.789-09" (não afetado) +``` + +### `BrUtils` (classe) + +Para utils de CPF ou CNPJ padrão customizados, crie sua própria instância: + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true } + }, + cnpj: { + formatter: { hidden: true }, + generator: { type: 'numeric', format: true }, + validator: { type: 'numeric' } + } +) + +utils.cpf.format('12345678909') # => "123.###.###-##" +utils.cpf.generate # => ex.: "005.265.352-88" +utils.cnpj.format('03603568000195') # => "03.603.***/****-**" +utils.cnpj.generate # => ex.: "73.008.535/0005-06" + +# Acessar ou substituir instâncias internas de domínio +utils.cpf # => CpfUtils +utils.cnpj # => CnpjUtils +``` + +- **`BrUtils.new(settings = nil, **keywords)`**: Configurações opcionais. Passe um `Hash` de settings com chaves `:cpf` e/ou `:cnpj`, **ou** as mesmas chaves (mais kwargs planos de componentes) como argumentos nomeados — não ambos (passar ambos lança `BrUtils::InvalidArgumentCombinationError`). + - **`:cpf` / `:cnpj`**: Uma instância pronta de `CpfUtils` / `CnpjUtils` **ou** um `Hash` de configuração repassado ao construtor do utils correspondente. Dentro desse `Hash`, cada chave de recurso (`:formatter`, `:generator` e `:validator` para CNPJ) aceita um objeto de opções ou um mapeamento de valores de opção. + - **`:cpf_formatter`**, **`:cpf_generator`**, **`:cnpj_formatter`**, **`:cnpj_generator`**, **`:cnpj_validator`**: Argumentos planos de conveniência quando apenas componentes individuais precisam de customização. São ignorados quando o argumento `:cpf` ou `:cnpj` correspondente é fornecido. +- **`#cpf`**, **`#cnpj`**: Acessores (getters e setters) das instâncias de utils de domínio. Os setters aceitam uma instância de utils, um `Hash` de configuração ou `nil` para voltar aos padrões (substitui a instância inteira; não faz merge). + +Opções planas no construtor (alternativa aos mapeamentos aninhados `:cpf` / `:cnpj`): + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf_formatter: CpfFmt::CpfFormatterOptions.new(hidden: true, hidden_key: '#'), + cpf_generator: CpfGen::CpfGeneratorOptions.new(format: true), + cnpj_formatter: CnpjFmt::CnpjFormatterOptions.new(hidden: true, hidden_key: '#'), + cnpj_generator: CnpjGen::CnpjGeneratorOptions.new(format: true, type: 'numeric'), + cnpj_validator: CnpjVal::CnpjValidatorOptions.new(type: 'numeric') +) +``` + +Passar um `Hash` de settings posicional junto com qualquer palavra-chave lança: + +```ruby +BrUtils.new({ cpf: {} }, cnpj: CnpjUtils.new) +# lança BrUtils::InvalidArgumentCombinationError +``` + +### Padrões da instância e sobrescritas por chamada + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true } + }, + cnpj: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true }, + validator: { type: 'numeric' } + } +) + +cpf = '12345678909' +cnpj = '03603568000195' + +utils.cpf.format(cpf) # => "123.###.###-##" +utils.cpf.format(cpf, hidden: false) # só nesta chamada: sem máscara +utils.cpf.generate(format: false) # só nesta chamada: saída compacta + +utils.cnpj.format(cnpj) # => "03.603.###/####-##" +utils.cnpj.format(cnpj, hidden: false) # só nesta chamada: sem máscara +utils.cnpj.is_valid('1QB5UKALPYFP59') # => false (validador da instância é só numérico) +utils.cnpj.is_valid( # => true nesta chamada + '1QB5UKALPYFP59', + type: 'alphanumeric' +) +``` + +Passar uma instância de `CnpjFmt::CnpjFormatterOptions`, `CnpjGen::CnpjGeneratorOptions` ou `CnpjVal::CnpjValidatorOptions` ao construtor de `BrUtils` armazena esse objeto por referência — mutá-lo depois afeta chamadas subsequentes sem sobrescrita por chamada. + +Para alterar uma única opção aninhada sem substituir o utils de domínio inteiro, mute via os acessores de domínio (ex.: `utils.cpf.formatter.options.hidden = true`). + +### Operações de CPF + +Os métodos de CPF são acessados via `BrUtils.cpf`, `utils.cpf`, `CpfUtils` ou os helpers `CpfFmt` / `CpfGen` / `CpfVal`. O CPF usa a API de [`cpf-utilities`](../cpf-utilities/README.pt.md). + +#### Formatação (`#format` / `CpfFmt.cpf_fmt`) + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `hidden` | `Boolean` | `false` | Se `true`, mascara dígitos entre `hidden_start` e `hidden_end` com `hidden_key` | +| `hidden_key` | `String` | `'*'` | Caractere(s) usados para substituir dígitos mascarados | +| `hidden_start` | `Integer` | `3` | Índice inicial (0–10, inclusivo) do intervalo a ocultar | +| `hidden_end` | `Integer` | `10` | Índice final (0–10, inclusivo) do intervalo a ocultar | +| `dot_key` | `String` | `'.'` | Delimitador de ponto (ex.: em `123.456.789`) | +| `dash_key` | `String` | `'-'` | Delimitador de hífen (ex.: antes dos dígitos verificadores `…-09`) | +| `escape` | `Boolean` | `false` | Se `true`, escapa caracteres especiais HTML no resultado | +| `encode` | `Boolean` | `false` | Se `true`, codifica o resultado para URL (similar ao `encodeURIComponent` do JavaScript) | +| `on_fail` | `Proc` / invocável | retorna `''` | Callback quando o tamanho da entrada sanitizada ≠ 11; o retorno é usado como resultado | + +O **`on_fail`** padrão retorna uma string vazia. Comprimento inválido **não** lança exceção em `#format`. + +```ruby +require 'br-utilities' + +cpf = '12345678909' + +BrUtils.cpf.format(cpf) # => "123.456.789-09" +BrUtils.cpf.format(cpf, hidden: true, hidden_key: '#') # => "123.###.###-##" +BrUtils.cpf.format(cpf, dot_key: '', dash_key: '_') # => "123456789_09" + +CpfFmt.cpf_fmt(cpf, hidden: true) # => "123.***.***-**" +``` + +#### Geração (`#generate` / `CpfGen.cpf_gen`) + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | Se `true`, retorna o CPF gerado no formato padrão (`000.000.000-00`) | +| `prefix` | `String` | `''` | String parcial inicial (0–9 dígitos). Não dígitos são removidos; caracteres faltantes são gerados e os dígitos verificadores calculados. Prefixos com mais de 9 dígitos são truncados silenciosamente. | + +Regras de prefixo: a base (primeiros 9 dígitos) não pode ser toda zeros; 9 dígitos repetidos (ex.: `999999999`) não são permitidos. + +```ruby +require 'br-utilities' + +BrUtils.cpf.generate # => ex.: "11508890048" +BrUtils.cpf.generate(format: true) # => ex.: "661.134.831-00" +BrUtils.cpf.generate(prefix: '123456789') # => "12345678909" +CpfGen.cpf_gen(prefix: '123456789', format: true) # => "123.456.789-09" +``` + +#### Validação (`#is_valid` / `CpfVal.cpf_val`) + +Aceita CPF formatado ou não (ou um `Array` de strings). Retorna **`true`** ou **`false`** sem lançar exceção para CPF inválido. Não há opções de validador. + +```ruby +require 'br-utilities' + +BrUtils.cpf.is_valid('12345678909') # => true +BrUtils.cpf.is_valid('123.456.789-09') # => true +BrUtils.cpf.is_valid('12345678900') # => false +CpfVal.cpf_val('12345678909') # => true +``` + +### Operações de CNPJ + +Os métodos de CNPJ são acessados via `BrUtils.cnpj`, `utils.cnpj`, `CnpjUtils` ou os helpers `CnpjFmt` / `CnpjGen` / `CnpjVal`. O CNPJ usa a API de [`cnpj-utilities`](../cnpj-utilities/README.pt.md). + +#### Formatação (`#format` / `CnpjFmt.cnpj_fmt`) + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `hidden` | `Boolean` | `false` | Se `true`, mascara caracteres entre `hidden_start` e `hidden_end` com `hidden_key` | +| `hidden_key` | `String` | `'*'` | Caractere(s) usados para substituir caracteres mascarados | +| `hidden_start` | `Integer` | `5` | Índice inicial (0–13, inclusivo) do intervalo a ocultar | +| `hidden_end` | `Integer` | `13` | Índice final (0–13, inclusivo) do intervalo a ocultar | +| `dot_key` | `String` | `'.'` | Delimitador de ponto (ex.: em `12.345.678`) | +| `slash_key` | `String` | `'/'` | Delimitador de barra (ex.: antes da filial `…/0001-90`) | +| `dash_key` | `String` | `'-'` | Delimitador de hífen (ex.: antes dos dígitos verificadores `…-90`) | +| `escape` | `Boolean` | `false` | Se `true`, escapa caracteres especiais HTML no resultado | +| `encode` | `Boolean` | `false` | Se `true`, codifica o resultado para URL (similar ao `encodeURIComponent` do JavaScript) | +| `on_fail` | `Proc` / invocável | retorna `''` | Callback quando o tamanho da entrada sanitizada ≠ 14; o retorno é usado como resultado | + +O **`on_fail`** padrão retorna uma string vazia. Tipos de entrada incorretos lançam **`CnpjFmt::TypeMismatchError`**. + +```ruby +require 'br-utilities' + +cnpj = '03603568000195' + +BrUtils.cnpj.format(cnpj) # => "03.603.568/0001-95" +BrUtils.cnpj.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" +BrUtils.cnpj.format( # => "03.603.###/####-##" + cnpj, + hidden: true, + hidden_key: '#' +) +BrUtils.cnpj.format( # => "03603568|0001_95" + cnpj, + dot_key: '', + slash_key: '|', + dash_key: '_' +) + +CnpjFmt.cnpj_fmt(cnpj) # => "03.603.568/0001-95" +``` + +#### Geração (`#generate` / `CnpjGen.cnpj_gen`) + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | Se `true`, retorna o CNPJ gerado no formato padrão (`00.000.000/0000-00`) | +| `prefix` | `String` | `''` | String parcial inicial (0–12 caracteres alfanuméricos). Caracteres faltantes são gerados e os dígitos verificadores calculados. | +| `type` | `String` | `'alphanumeric'` | Conjunto de caracteres para a parte gerada aleatoriamente: `'numeric'`, `'alphabetic'` ou `'alphanumeric'`. **Os dígitos verificadores são sempre numéricos.** | + +Regras de prefixo: o ID base (primeiros 8 caracteres) e o ID da filial (caracteres 9–12) não podem ser todos zeros; 12 dígitos repetidos (ex.: `111111111111`) também não são permitidos. + +```ruby +require 'br-utilities' + +BrUtils.cnpj.generate # => ex.: "1GJTR3J3XSSA96" +BrUtils.cnpj.generate(format: true) # => ex.: "V1.J0V.8WE/DVZ7-50" +BrUtils.cnpj.generate( # => ex.: "12345678855883" + prefix: '12345678', + type: 'numeric' +) +CnpjGen.cnpj_gen(type: 'numeric') # => ex.: "65453043000178" +``` + +#### Validação (`#is_valid` / `CnpjVal.cnpj_val`) + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `case_sensitive` | `Boolean` | `true` | Se `false`, letras minúsculas são aceitas para CNPJ alfanumérico (a entrada é convertida para maiúsculas antes da validação). | +| `type` | `String` | `'alphanumeric'` | `'numeric'`: apenas dígitos (0–9); `'alphanumeric'`: dígitos e letras (0–9, A–Z). | + +```ruby +require 'br-utilities' + +BrUtils.cnpj.is_valid('98765432000198') # => true +BrUtils.cnpj.is_valid('98765432000199') # => false +BrUtils.cnpj.is_valid('1QB5UKALPYFP59') # => true +BrUtils.cnpj.is_valid('1QB5UKALpyfp59') # => false +BrUtils.cnpj.is_valid( # => true + '1QB5UKALpyfp59', + case_sensitive: false +) +BrUtils.cnpj.is_valid( # => false + '1QB5UKALPYFP59', + type: 'numeric' +) + +CnpjVal.cnpj_val('98765432000198') # => true +CnpjVal.cnpj_val('1QB5UKALpyfp59', case_sensitive: false) # => true +CnpjVal.cnpj_val('1QB5UKALPYFP59', type: 'numeric') # => false +``` + +CNPJ inválido retorna **`false`** sem lançar exceção. Tipos de entrada incorretos lançam **`CnpjVal::TypeMismatchError`**. + +### Agregadores de domínio (isolados) + +Use `CpfUtils` ou `CnpjUtils` diretamente quando precisar de apenas um domínio: + +```ruby +require 'br-utilities' + +cpf_utils = CpfUtils.new( + formatter: { hidden: true }, + generator: { format: true } +) + +cnpj_utils = CnpjUtils.new( + formatter: { hidden: true }, + generator: { format: true }, + validator: { type: 'numeric' } +) + +cpf_utils.format('12345678909') # => "123.***.***-**" +cnpj_utils.format('03603568000195') # => "03.603.***/****-**" +``` + +### Acessando componentes + +Cada agregador de domínio expõe seu formatador, gerador e validador internos: + +```ruby +require 'br-utilities' + +utils = BrUtils.new + +utils.cpf.formatter.format('12345678909', hidden: true) # => "123.***.***-**" +utils.cpf.generator.generate(format: true) # => ex.: "545.507.690-68" +utils.cpf.validator.is_valid('12345678909') # => true + +utils.cnpj.formatter.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" +utils.cnpj.generator.generate(format: true) # => ex.: "8O.BE5.2KL/UI0Y-06" +utils.cnpj.validator.is_valid('03603568000195') # => true +``` + +### Usando classes componentes e módulos aninhados + +Caminhos preferidos após `require 'br-utilities'`: + +```ruby +require 'br-utilities' + +# Classes principais na raiz da fachada +formatter = BrUtils::CpfFormatter.new(hidden: true) +generator = BrUtils::CnpjGenerator.new(type: 'numeric') +validator = BrUtils::CnpjValidator.new + +formatter.format('12345678909') # => "123.***.***-**" + +# Options, helpers e erros nos módulos aninhados do pacote +options = BrUtils::CpfFmt::CpfFormatterOptions.new(dash_key: '|') +BrUtils::CpfFmt.cpf_fmt('12345678909') # => "123.456.789-09" + +begin + BrUtils::CnpjFmt.cnpj_fmt(12_345) +rescue BrUtils::CnpjFmt::TypeMismatchError + # tipo de entrada incorreto +end +``` + +Os irmãos na raiz continuam suportados (os mesmos objetos dos nests): + +```ruby +CpfFmt.cpf_fmt('12345678909', dash_key: '|') # => "123.456.789|09" +CpfGen.cpf_gen(format: true) # => ex.: "478.442.410-55" +CpfVal.cpf_val('12345678909') # => true +CnpjFmt.cnpj_fmt('01ABC234000X56', slash_key: '|') # => "01.ABC.234|000X-56" +CnpjGen.cnpj_gen(type: 'numeric') # => ex.: "65453043000178" +CnpjVal.cnpj_val('9JN7MGLJZXIO50') # => true +``` + +Consulte [`cpf-utilities`](../cpf-utilities/README.pt.md) e [`cnpj-utilities`](../cnpj-utilities/README.pt.md) para detalhes completos de opções e erros. + +### Misturando estilos + +Use `BrUtils` onde uma configuração compartilhada ajuda, e componentes ou helpers isolados em outros pontos — são as mesmas classes subjacentes: + +```ruby +require 'br-utilities' + +utils = BrUtils.new(cnpj: { validator: { type: 'numeric' } }) + +# Via fachada +utils.cpf.format('12345678909') # => "123.456.789-09" + +# Via componente retornado pela fachada +utils.cnpj.formatter.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" + +# Via instância de componente separada +BrUtils::CnpjFormatter.new.format('03603568000195') # => "03.603.568/0001-95" + +# Via helpers funcionais +CpfFmt.cpf_fmt('12345678909') # => "123.456.789-09" +CnpjVal.cnpj_val('98.765.432/0001-98') # => true +``` + +## API + +### Exportações + +Após `require 'br-utilities'`: + +- **`BrUtils`**: Classe fachada para criar uma instância com configurações opcionais dos utils de CPF e CNPJ. +- **`BrUtils.cpf` / `.cnpj`**: Helpers de classe que encaminham para os acessores de domínio de `BrUtils::DEFAULT`. +- **`BrUtils::DEFAULT`**: Instância pré-construída e mutável de `BrUtils` (o mesmo objeto usado pelos helpers de classe). Em todo o processo / compartilhada entre threads — prefira `BrUtils.new` ou opções por chamada sob concorrência. +- **`BrUtils::VERSION`**: String da versão da gem. +- **Atalhos de classes principais**: `BrUtils::CpfFormatter`, `BrUtils::CpfFormatterOptions`, `BrUtils::CpfGenerator`, `BrUtils::CpfGeneratorOptions`, `BrUtils::CpfValidator`, `BrUtils::CnpjFormatter`, `BrUtils::CnpjFormatterOptions`, `BrUtils::CnpjGenerator`, `BrUtils::CnpjGeneratorOptions`, `BrUtils::CnpjValidator`, `BrUtils::CnpjValidatorOptions` (os mesmos objetos das classes irmãs). Atalhos de marcadores de erro: `BrUtils::CpfFormatterError`, `BrUtils::CpfGeneratorError`, `BrUtils::CpfValidatorError`, `BrUtils::CnpjFormatterError`, `BrUtils::CnpjGeneratorError`, `BrUtils::CnpjValidatorError`. +- **Módulos aninhados do pacote**: `BrUtils::CpfUtils`, `BrUtils::CnpjUtils`, `BrUtils::CpfFmt`, `BrUtils::CpfGen`, `BrUtils::CpfVal`, `BrUtils::CnpjFmt`, `BrUtils::CnpjGen`, `BrUtils::CnpjVal` — superfície completa dos irmãos (Options, helpers, erros, tipos). +- **Módulos irmãos na raiz** (ainda suportados): `CpfUtils`, `CnpjUtils`, `CpfFmt`, `CpfGen`, `CpfVal`, `CnpjFmt`, `CnpjGen`, `CnpjVal` — os mesmos objetos dos nests. + +### Erros e exceções + +`BrUtils` define apenas erros de uso indevido da API para as regras de argumentos desta gem. Erros de domínio são lançados pelos pacotes incluídos e propagam inalterados. + +#### Definidos por `br-utilities` + +Os erros definidos por esta gem são **apenas uso indevido da API** (tipo errado ou combinação inválida de argumentos). Todo erro customizado inclui o módulo marcador `BrUtils::Error`. Esta gem **não** define `BrUtils::DomainError` nem folhas de domínio — falhas de domínio vêm apenas dos [pacotes incluídos](#propagados-dos-pacotes-incluídos) e mantêm os namespaces desses pacotes (`CpfFmt::…`, `CnpjGen::…`, …). + +`rescue BrUtils::Error` captura **apenas** erros que esta gem lança. **Não** captura erros de componentes que propagam inalterados. + +##### Resumo + +| Classe | Herda de | Categoria | Condição de disparo | +|-------|---------------|----------|-------------------| +| `BrUtils::InvalidArgumentCombinationError` | `BrUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include BrUtils::Error`) | Uso indevido da API | `Hash` de settings não-`nil` passado junto com qualquer argumento nomeado não-`nil` | +| `BrUtils::TypeMismatchError` | `BrUtils::TypeMismatchError < TypeError < StandardError` (+ `include BrUtils::Error`) | Uso indevido da API | Argumento `settings` não-`nil` em `BrUtils.new` não é um `Hash` | + +##### `BrUtils::Error` (módulo marcador) + +- **Herança:** módulo marcador misturado em todo erro customizado que esta gem lança via `include` (não é uma classe). +- **Categoria:** N/A (apenas alvo de rescue) — não é um modo de falha por si só. +- **Quando é lançado:** Nunca lançado diretamente; incluído por todo erro customizado que esta gem lança. +- **Exemplo:** N/A +- **Como resgatar:** + +```ruby +rescue BrUtils::Error + # TypeMismatchError, InvalidArgumentCombinationError apenas desta gem + # (não CpfFmt::*, CnpjGen::* ou outros erros dos pacotes incluídos) +``` + +##### `BrUtils::TypeMismatchError` + +- **Herança:** `BrUtils::TypeMismatchError < TypeError < StandardError` (inclui `BrUtils::Error`) +- **Categoria:** Uso indevido da API — o caller passou um valor do tipo errado. +- **Quando é lançado:** Quando `BrUtils.new` recebe um argumento `settings` não-`nil` que não é um `Hash`. +- **Exemplo:** + +```ruby +BrUtils.new('not-a-hash') # lança BrUtils::TypeMismatchError +BrUtils.new(false) # lança BrUtils::TypeMismatchError (false é não-nil) +``` + +- **Como resgatar:** + +```ruby +rescue BrUtils::TypeMismatchError + # violação de contrato de tipo desta gem + +rescue TypeError + # erros nativos de tipo, incluindo TypeMismatchError desta gem +``` + +##### `BrUtils::InvalidArgumentCombinationError` + +- **Herança:** `BrUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (inclui `BrUtils::Error`) +- **Categoria:** Uso indevido da API — o caller misturou padrões de argumentos mutuamente exclusivos. +- **Quando é lançado:** Quando `BrUtils.new` recebe um `Hash` de settings não-`nil` e qualquer argumento nomeado não-`nil` (`cpf:`, `cnpj:`, `cpf_formatter:`, …) ao mesmo tempo. +- **Exemplo:** + +```ruby +BrUtils.new({ cpf: { formatter: { hidden: true } } }, cnpj: { formatter: { hidden: true } }) +# lança BrUtils::InvalidArgumentCombinationError +``` + +- **Como resgatar:** + +```ruby +rescue BrUtils::InvalidArgumentCombinationError + # combinação inválida de assinatura desta gem + +rescue ArgumentError + # erros nativos de argumento, incluindo InvalidArgumentCombinationError desta gem +``` + +##### Granularidade de rescue + +Cada nível é mostrado como seu próprio exemplo isolado (não os una em uma única escada de `rescue` — um handler nativo amplo tornaria cláusulas mais estreitas inalcançáveis). + +```ruby +require 'br-utilities' + +# 1) Classe nativa única — captura erros de uso indevido desse tipo, +# incluindo os não-biblioteca já tratados em outro ponto do código do consumidor. +begin + BrUtils.new('not-a-hash') +rescue TypeError + # BrUtils::TypeMismatchError e qualquer outro TypeError (biblioteca ou não) +end + +begin + BrUtils.new({ cpf: {} }, cnpj: CnpjUtils.new) +rescue ArgumentError + # BrUtils::InvalidArgumentCombinationError e qualquer outro ArgumentError (biblioteca ou não) +end +``` + +```ruby +require 'br-utilities' + +# 2) BrUtils::DomainError — não aplicável: esta gem não define DomainError +# (nem folhas de domínio). Falhas de domínio vêm apenas dos pacotes incluídos. +# begin +# BrUtils.new(cpf: { formatter: { hidden_start: -1 } }) +# rescue BrUtils::DomainError # NameError — constante não definida +# end +``` + +```ruby +require 'br-utilities' + +# 3) BrUtils::Error — captura tudo que esta gem lança, independentemente da ancestralidade nativa. +# Não captura CpfFmt::*, CnpjGen::* ou outros erros dos pacotes incluídos. +begin + BrUtils.new('not-a-hash') +rescue BrUtils::Error + # todo erro customizado que inclui BrUtils::Error +end +``` + +```ruby +require 'br-utilities' + +# 4) Classe folha específica — captura apenas aquele modo de falha exato. +begin + BrUtils.new('not-a-hash') +rescue BrUtils::TypeMismatchError + # apenas BrUtils::TypeMismatchError +end +``` + +#### Propagados dos pacotes incluídos + +`BrUtils` não redefine tipos de exceção de domínio. Construção, setters e chamadas de métodos de domínio lançam os mesmos erros de [`cpf-utilities`](../cpf-utilities/README.pt.md) e [`cnpj-utilities`](../cnpj-utilities/README.pt.md): + +- **Formatação de CPF**: `CpfFmt::TypeMismatchError`, `CpfFmt::OutOfRangeError`, `CpfFmt::ValidationError`, `CpfFmt::InvalidLengthError` (passado a `on_fail`, não lançado por `#format`) e classes relacionadas. +- **Geração de CPF**: `CpfGen::TypeMismatchError`, `CpfGen::ValidationError` e classes relacionadas. +- **Validação de CPF**: `CpfVal::TypeMismatchError` e classes relacionadas. +- **Formatação de CNPJ**: `CnpjFmt::TypeMismatchError`, `CnpjFmt::OutOfRangeError`, `CnpjFmt::ValidationError`, `CnpjFmt::InvalidLengthError` (passado a `on_fail`) e classes relacionadas. +- **Geração de CNPJ**: `CnpjGen::TypeMismatchError`, `CnpjGen::ValidationError` e classes relacionadas. +- **Validação de CNPJ**: `CnpjVal::TypeMismatchError`, `CnpjVal::ValidationError` e classes relacionadas. + +Tipos de opção inválidos são tipicamente subclasses de **`TypeError`** (`*::TypeMismatchError`); valores de opção inválidos são erros de domínio sob a hierarquia `DomainError` de cada pacote. Falhas de validação de CPF e CNPJ retornam `false`. Falhas de comprimento na formatação são tratadas por **`on_fail`** (padrão retorna string vazia). + +```ruby +require 'br-utilities' + +begin + BrUtils.new.cnpj.format(12_345) +rescue CnpjFmt::TypeMismatchError => e + puts e.message +end + +begin + BrUtils.new.cnpj.is_valid(12_345_678_000_198) +rescue CnpjVal::TypeMismatchError => e + puts e.message +end + +# on_fail customizado para comprimento inválido +custom_fail = ->(value, _exception) { "Invalid: #{value}" } + +BrUtils.cpf.format('short', on_fail: custom_fail) # => "Invalid: short" +BrUtils.cnpj.format('short', on_fail: custom_fail) # => "Invalid: short" +BrUtils.cpf.format('short') # => "" (on_fail padrão) +``` + +Para listas exaustivas de exceções e comportamento em casos extremos, consulte o README de cada [pacote incluído](#pacotes-incluídos). + +### Pacotes incluídos + +| Pacote | Principais recursos | README | +|---------|----------------|--------| +| [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) | `CpfUtils`, `CpfFormatter`, `CpfGenerator`, `CpfValidator`, `CpfFmt.cpf_fmt`, `CpfGen.cpf_gen`, `CpfVal.cpf_val` | [docs](../cpf-utilities/README.pt.md) | +| [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) | `CnpjUtils`, `CnpjFormatter`, `CnpjGenerator`, `CnpjValidator`, `CnpjFmt.cnpj_fmt`, `CnpjGen.cnpj_gen`, `CnpjVal.cnpj_val` | [docs](../cnpj-utilities/README.pt.md) | + +Todos os acima são puxados como dependências de **`br-utilities`**. Demos interativas: [CPF](https://cpf-utils.vercel.app/) e [CNPJ](https://cnpj-utils.vercel.app/). + +## 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 o projeto for útil para você, considere: + +- ⭐ Dar uma estrela no repositório +- 🤝 Contribuir com código +- 💡 [Sugerir novas funcionalidades](https://github.com/LacusSolutions/br-utils-ruby/issues) +- 🐛 [Reportar bugs](https://github.com/LacusSolutions/br-utils-ruby/issues) + +## Licença + +Este projeto está sob a licença MIT — veja o arquivo [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE). + +## Changelog + +Veja o [CHANGELOG](./CHANGELOG.md) para alterações e histórico de versões. + +--- + +Feito com ❤️ por [Lacus Solutions](https://github.com/LacusSolutions) From a199a1fe0b1cbf1c1597cc3088746e11d5a9b3af Mon Sep 17 00:00:00 2001 From: juliolmuller Date: Mon, 10 Aug 2026 17:42:11 -0300 Subject: [PATCH 7/7] docs: copy `br-utilities_ docs to project root --- README.md | 735 +++++++++++++++++++++++++++++++++++++++++++++++---- README.pt.md | 700 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1383 insertions(+), 52 deletions(-) create mode 100644 README.pt.md diff --git a/README.md b/README.md index 868d39c..abc1e1d 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,707 @@ -![br-utils for Ruby](https://br-utils.vercel.app/img/cover_br-utils.jpg) +![br-utilities for Ruby](https://br-utils.vercel.app/img/cover_br-utils.jpg) -Brazilian data utilities (CPF, CNPJ, etc.) as a **multi-gem monorepo**, publishable to RubyGems with independent versioning and GitHub Actions (Trusted Publishing / OIDC). +[![Gem Version](https://img.shields.io/gem/v/br-utilities)](https://rubygems.org/gems/br-utilities) +[![Gem Downloads](https://img.shields.io/gem/dt/br-utilities)](https://rubygems.org/gems/br-utilities) +[![Ruby Version](https://img.shields.io/gem/rv/br-utilities)](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) -## Structure +> 🚀 **Full support for the [new alphanumeric CNPJ format](https://github.com/user-attachments/files/23937961/calculodvcnpjalfanaumerico.pdf).** -- **Root**: Tooling (Rake, RuboCop), shared config in `config/gems.yml`, no app code. -- **Packages**: Under `packages/` — each is a gem (e.g. `cpf-dv`, `cpf-utilities`, `br-utilities`). Internal dependencies use path in development and version constraints when published. +> 🌎 [Acessar documentação em português](./README.pt.md) -See [CONTRIBUTING.md](CONTRIBUTING.md) for folder layout, tagging, dependency resolution, and development workflow. +A Ruby toolkit to handle the main operations with Brazilian-related data: CPF (Individual's Taxpayer ID) and CNPJ (Business Tax ID). It wraps [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) and [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) in a single façade class (`BrUtils`). -## Local setup +## Ruby Support + +| ![Ruby 3.1](https://img.shields.io/badge/Ruby-3.1-CC342D?logo=ruby&logoColor=white) | ![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) | ![Ruby 4.0](https://img.shields.io/badge/Ruby-4.0-CC342D?logo=ruby&logoColor=white) | +| --- | --- | --- | --- | --- | +| Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | + +Requires Ruby **≥ 3.1** (see `required_ruby_version` in the gemspec). + +## Features + +- ✅ **Unified top-level API**: Class helpers `BrUtils.cpf` / `.cnpj` alias `BrUtils::DEFAULT`; each domain offers `format`, `generate`, and `is_valid` +- ✅ **Bundled domains**: [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) and [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) installed together +- ✅ **Alphanumeric CNPJ**: Full support for the new alphanumeric CNPJ format (introduced in 2026) +- ✅ **Reusable instance**: `BrUtils` class with optional default CPF and CNPJ settings (nested mappings, flat component kwargs, or pre-built utils instances) +- ✅ **Two-tier access**: Prefer main-class shortcuts at the façade root (`BrUtils::CpfFormatter`, `BrUtils::CnpjValidator`, …); Options, helpers, and errors live under nested package modules (`BrUtils::CpfFmt`, `BrUtils::CnpjUtils`, …). Root siblings (`CpfUtils`, `CnpjUtils`, `CpfFmt`, …) still work +- ✅ **Per-call overrides**: Configure defaults on the façade / domain utils; override options on a single `format` / `generate` / `is_valid` call +- ✅ **Error handling**: Domain errors propagate unchanged from the bundled packages; this gem defines `BrUtils::TypeMismatchError` and `BrUtils::InvalidArgumentCombinationError` for API misuse + +## Installation + +Install the gem directly: ```bash -bundle install -rake hooks:install # enable the git hooks (pre-commit, pre-push, commit-msg) -rake monorepo:check_cycles -cd packages/cpf-dv && bundle install && rake test +gem install br-utilities +``` + +Or add it to your `Gemfile` and run `bundle install`: + +```ruby +gem 'br-utilities' +``` + +This installs **`br-utilities`** together with [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) and [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) (which in turn pull in the CPF and CNPJ component packages). You do **not** need separate `gem install` / `gem` lines for the domain packages when using **`br-utilities`**. + +## Require + +```ruby +require 'br-utilities' ``` -## Git hooks +## Quick Start -`rake hooks:install` points `core.hooksPath` at `.githooks/`, enabling: +Prefer the aggregator class helpers (`BrUtils.cpf` / `BrUtils.cnpj`) for one-off calls — they forward to `BrUtils::DEFAULT`: -- **pre-commit**: RuboCop auto-corrects the staged Ruby files and re-stages only the - linting changes (unstaged edits in the same files are preserved, never committed). - Aborts if offenses remain that safe auto-correction can't fix. -- **pre-push**: runs the repository specs and every package's tests in build order, - aborting the push if any test fails. -- **commit-msg**: rejects commit messages that don't follow Conventional Commits. +```ruby +require 'br-utilities' -Undo with `rake hooks:uninstall`. See [CONTRIBUTING.md](CONTRIBUTING.md#git-hooks) for details. +cpf = '12345678909' +cnpj = '03603568000195' + +# CPF (personal ID) +BrUtils.cpf.format(cpf) # => "123.456.789-09" +BrUtils.cpf.generate(format: true) # => e.g. "478.442.410-55" +BrUtils.cpf.is_valid('123.456.789-09') # => true + +# CNPJ (business ID) +BrUtils.cnpj.format(cnpj) # => "03.603.568/0001-95" +BrUtils.cnpj.generate(format: true) # => e.g. "AB.123.CDE/0001-55" +BrUtils.cnpj.is_valid('98765432000198') # => true +``` + +**With domain aggregators:** + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +CpfUtils.format(cpf) # => "123.456.789-09" +CnpjUtils.format(cnpj) # => "03.603.568/0001-95" +CpfUtils.is_valid(cpf) # => true +CnpjUtils.is_valid(cnpj) # => true +``` + +**With functional helpers** (root sibling modules, loaded by this gem): + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +CpfFmt.cpf_fmt(cpf) # => "123.456.789-09" +CpfVal.cpf_val(cpf) # => true +CnpjFmt.cnpj_fmt(cnpj) # => "03.603.568/0001-95" +CnpjVal.cnpj_val(cnpj) # => true +``` -## Commit messages +## Usage -Commits follow [Conventional Commits](https://www.conventionalcommits.org). A pure-Ruby -linter (`bin/commit-lint`) enforces this in two places: +You can work in these equivalent ways: -- **Locally**: `rake hooks:install` sets `core.hooksPath` to `.githooks`, so the - `commit-msg` hook rejects non-conforming messages. Undo with `rake hooks:uninstall`. -- **CI**: the `Commit Lint` workflow validates every commit in a push/PR. +1. **`BrUtils.cpf` / `.cnpj`** — class helpers for quick one-off calls (forward to `DEFAULT`). +2. **`BrUtils::DEFAULT`** — mutable shared singleton (same object the class helpers use; process-wide / not thread-isolated). +3. **`BrUtils.new`** — configurable instance with shared defaults across both CPF and CNPJ domains. +4. **Domain aggregators** — `CpfUtils` / `CnpjUtils` (or `BrUtils::CpfUtils` / `BrUtils::CnpjUtils`) directly. +5. **Main classes under `BrUtils`** — `BrUtils::CpfFormatter`, `BrUtils::CnpjGenerator`, and related shortcuts. +6. **Nested package modules** — Options, helpers, errors, and types via `BrUtils::CpfFmt` / `CpfGen` / `CpfVal` / `CnpjFmt` / `CnpjGen` / `CnpjVal` / `CpfUtils` / `CnpjUtils`. +7. **Root sibling modules** (still supported) — `CpfFmt`, `CnpjUtils`, and the rest unchanged. -Allowed `type`s: `build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test`. -The `(scope)` is optional; when present it must be one of the per-package scopes below -(a few differ from the gem name). Example: `feat(cpf-gen): add batch generator`. +All approaches expose the same options and behavior within each domain. For exhaustive option tables and component-specific details, see the README of each [bundled package](#bundled-packages). + +### Class helpers (`BrUtils.cpf` / `.cnpj`) + +These class methods return the same domain utils instances as `BrUtils::DEFAULT`. Prefer them for one-off calls: + +```ruby +BrUtils.cpf.format('12345678909') +BrUtils.cpf.generate(format: true) +BrUtils.cpf.is_valid('12345678909') + +BrUtils.cnpj.format('03603568000195') +BrUtils.cnpj.generate(type: 'numeric') +BrUtils.cnpj.is_valid('98765432000198') +``` + +### `BrUtils::DEFAULT` (default instance) + +`BrUtils::DEFAULT` is the pre-built, **mutable** singleton behind the class helpers (parity with the JS default export / Python `br_utils`). Its configuration is **process-wide and shared across threads**: mutating it (e.g. `DEFAULT.cpf = …`) affects subsequent `BrUtils.cpf` / `.cnpj` calls for every caller in the process. Prefer `BrUtils.new` or per-call options for concurrent or isolated work; custom instances stay independent of `DEFAULT`: + +```ruby +BrUtils::DEFAULT.cpf = CpfUtils.new(formatter: { dash_key: '|' }) +BrUtils.cpf.format('12345678909') # => "123.456.789|09" + +custom = BrUtils.new +custom.cpf.format('12345678909') # => "123.456.789-09" (unaffected) +``` + +### `BrUtils` (class) + +For custom default CPF or CNPJ utils, create your own instance: + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true } + }, + cnpj: { + formatter: { hidden: true }, + generator: { type: 'numeric', format: true }, + validator: { type: 'numeric' } + } +) + +utils.cpf.format('12345678909') # => "123.###.###-##" +utils.cpf.generate # => e.g. "005.265.352-88" +utils.cnpj.format('03603568000195') # => "03.603.***/****-**" +utils.cnpj.generate # => e.g. "73.008.535/0005-06" + +# Access or replace internal domain instances +utils.cpf # => CpfUtils +utils.cnpj # => CnpjUtils +``` + +- **`BrUtils.new(settings = nil, **keywords)`**: Optional settings. Pass either a settings `Hash` with `:cpf` and/or `:cnpj` keys, **or** the same keys (plus flat component kwargs) as keyword arguments — not both (passing both raises `BrUtils::InvalidArgumentCombinationError`). + - **`:cpf` / `:cnpj`**: A pre-built `CpfUtils` / `CnpjUtils` instance **or** a configuration `Hash` spread into the corresponding utils constructor. Within that `Hash`, each resource key (`:formatter`, `:generator`, and `:validator` for CNPJ) accepts either an options object or a mapping of option values. + - **`:cpf_formatter`**, **`:cpf_generator`**, **`:cnpj_formatter`**, **`:cnpj_generator`**, **`:cnpj_validator`**: Flat convenience arguments when only individual components need customization. They are ignored when the corresponding `:cpf` or `:cnpj` argument is provided. +- **`#cpf`**, **`#cnpj`**: Accessors (getters and setters) for the domain utils instances. Setters accept a utils instance, a configuration `Hash`, or `nil` to reset to defaults (replaces the entire instance; does not merge). + +Flat constructor options (alternative to nested `:cpf` / `:cnpj` mappings): + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf_formatter: CpfFmt::CpfFormatterOptions.new(hidden: true, hidden_key: '#'), + cpf_generator: CpfGen::CpfGeneratorOptions.new(format: true), + cnpj_formatter: CnpjFmt::CnpjFormatterOptions.new(hidden: true, hidden_key: '#'), + cnpj_generator: CnpjGen::CnpjGeneratorOptions.new(format: true, type: 'numeric'), + cnpj_validator: CnpjVal::CnpjValidatorOptions.new(type: 'numeric') +) +``` + +Passing a settings `Hash` positional argument together with any keyword raises: + +```ruby +BrUtils.new({ cpf: {} }, cnpj: CnpjUtils.new) +# raises BrUtils::InvalidArgumentCombinationError +``` -| Scope | Package | -|-------|---------| -| `utils` | `lacus-utils` | -| `cnpj-dv` | `cnpj-dv` | -| `cnpj-fmt` | `cnpj-fmt` | -| `cnpj-gen` | `cnpj-gen` | -| `cnpj-val` | `cnpj-val` | -| `cnpj-utils` | `cnpj-utilities` | -| `cpf-dv` | `cpf-dv` | -| `cpf-fmt` | `cpf-fmt` | -| `cpf-gen` | `cpf-gen` | -| `cpf-val` | `cpf-val` | -| `cpf-utils` | `cpf-utilities` | -| `br-utils` | `br-utilities` | +### Instance defaults and per-call overrides -Lint a range manually with `rake lint:commits` (defaults to `origin/main..HEAD`, override -with `COMMIT_RANGE`). +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true } + }, + cnpj: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true }, + validator: { type: 'numeric' } + } +) + +cpf = '12345678909' +cnpj = '03603568000195' + +utils.cpf.format(cpf) # => "123.###.###-##" +utils.cpf.format(cpf, hidden: false) # this call only: unmasked +utils.cpf.generate(format: false) # this call only: compact output + +utils.cnpj.format(cnpj) # => "03.603.###/####-##" +utils.cnpj.format(cnpj, hidden: false) # this call only: unmasked +utils.cnpj.is_valid('1QB5UKALPYFP59') # => false (instance validator is numeric-only) +utils.cnpj.is_valid( # => true for this call + '1QB5UKALPYFP59', + type: 'alphanumeric' +) +``` + +Passing a `CnpjFmt::CnpjFormatterOptions`, `CnpjGen::CnpjGeneratorOptions`, or `CnpjVal::CnpjValidatorOptions` instance into the `BrUtils` constructor stores that object by reference — mutating it later affects subsequent calls with no per-call override. + +To change a single nested option without replacing the whole domain utils, mutate via the domain accessors (e.g. `utils.cpf.formatter.options.hidden = true`). + +### CPF operations + +CPF methods are accessed via `BrUtils.cpf`, `utils.cpf`, `CpfUtils`, or the `CpfFmt` / `CpfGen` / `CpfVal` helpers. CPF uses the API from [`cpf-utilities`](../cpf-utilities/README.md). + +#### Formatting (`#format` / `CpfFmt.cpf_fmt`) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `hidden` | `Boolean` | `false` | When `true`, mask digits in `hidden_start`–`hidden_end` with `hidden_key` | +| `hidden_key` | `String` | `'*'` | Character(s) used to replace masked digits | +| `hidden_start` | `Integer` | `3` | Start index (0–10, inclusive) of the range to hide | +| `hidden_end` | `Integer` | `10` | End index (0–10, inclusive) of the range to hide | +| `dot_key` | `String` | `'.'` | Dot delimiter (e.g. in `123.456.789`) | +| `dash_key` | `String` | `'-'` | Dash delimiter (e.g. before check digits `…-09`) | +| `escape` | `Boolean` | `false` | When `true`, escape HTML special characters in the result | +| `encode` | `Boolean` | `false` | When `true`, URL-encode the result (similar to JavaScript `encodeURIComponent`) | +| `on_fail` | `Proc` / callable | returns `''` | Callback when sanitized input length ≠ 11; return value is used as result | + +Default **`on_fail`** returns an empty string. Invalid length does **not** raise from `#format`. + +```ruby +require 'br-utilities' + +cpf = '12345678909' + +BrUtils.cpf.format(cpf) # => "123.456.789-09" +BrUtils.cpf.format(cpf, hidden: true, hidden_key: '#') # => "123.###.###-##" +BrUtils.cpf.format(cpf, dot_key: '', dash_key: '_') # => "123456789_09" + +CpfFmt.cpf_fmt(cpf, hidden: true) # => "123.***.***-**" +``` + +#### Generation (`#generate` / `CpfGen.cpf_gen`) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | When `true`, return the generated CPF in standard format (`000.000.000-00`) | +| `prefix` | `String` | `''` | Partial start string (0–9 digits). Non-digits are stripped; missing characters are generated and check digits 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. + +```ruby +require 'br-utilities' + +BrUtils.cpf.generate # => e.g. "11508890048" +BrUtils.cpf.generate(format: true) # => e.g. "661.134.831-00" +BrUtils.cpf.generate(prefix: '123456789') # => "12345678909" +CpfGen.cpf_gen(prefix: '123456789', format: true) # => "123.456.789-09" +``` + +#### Validation (`#is_valid` / `CpfVal.cpf_val`) + +Accepts formatted or unformatted CPF strings (or an `Array` of strings). Returns **`true`** or **`false`** without raising for invalid CPF. No validator options exist. + +```ruby +require 'br-utilities' + +BrUtils.cpf.is_valid('12345678909') # => true +BrUtils.cpf.is_valid('123.456.789-09') # => true +BrUtils.cpf.is_valid('12345678900') # => false +CpfVal.cpf_val('12345678909') # => true +``` + +### CNPJ operations + +CNPJ methods are accessed via `BrUtils.cnpj`, `utils.cnpj`, `CnpjUtils`, or the `CnpjFmt` / `CnpjGen` / `CnpjVal` helpers. CNPJ uses the API from [`cnpj-utilities`](../cnpj-utilities/README.md). + +#### Formatting (`#format` / `CnpjFmt.cnpj_fmt`) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `hidden` | `Boolean` | `false` | When `true`, mask characters in `hidden_start`–`hidden_end` with `hidden_key` | +| `hidden_key` | `String` | `'*'` | Character(s) used to replace masked characters | +| `hidden_start` | `Integer` | `5` | Start index (0–13, inclusive) of the range to hide | +| `hidden_end` | `Integer` | `13` | End index (0–13, inclusive) of the range to hide | +| `dot_key` | `String` | `'.'` | Dot delimiter (e.g. in `12.345.678`) | +| `slash_key` | `String` | `'/'` | Slash delimiter (e.g. before branch `…/0001-90`) | +| `dash_key` | `String` | `'-'` | Dash delimiter (e.g. before check digits `…-90`) | +| `escape` | `Boolean` | `false` | When `true`, escape HTML special characters in the result | +| `encode` | `Boolean` | `false` | When `true`, URL-encode the result (similar to JavaScript `encodeURIComponent`) | +| `on_fail` | `Proc` / callable | returns `''` | Callback when sanitized input length ≠ 14; return value is used as result | + +Default **`on_fail`** returns an empty string. Wrong input types raise **`CnpjFmt::TypeMismatchError`**. + +```ruby +require 'br-utilities' + +cnpj = '03603568000195' + +BrUtils.cnpj.format(cnpj) # => "03.603.568/0001-95" +BrUtils.cnpj.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" +BrUtils.cnpj.format( # => "03.603.###/####-##" + cnpj, + hidden: true, + hidden_key: '#' +) +BrUtils.cnpj.format( # => "03603568|0001_95" + cnpj, + dot_key: '', + slash_key: '|', + dash_key: '_' +) + +CnpjFmt.cnpj_fmt(cnpj) # => "03.603.568/0001-95" +``` + +#### Generation (`#generate` / `CnpjGen.cnpj_gen`) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | When `true`, return the generated CNPJ in standard format (`00.000.000/0000-00`) | +| `prefix` | `String` | `''` | Partial start string (0–12 alphanumeric chars). Missing characters are generated and check digits computed. | +| `type` | `String` | `'alphanumeric'` | Character set for the randomly generated part: `'numeric'`, `'alphabetic'`, or `'alphanumeric'`. **Check digits are always numeric.** | + +Prefix rules: base ID (first 8 chars) and branch ID (chars 9–12) cannot be all zeros; 12 repeated digits (e.g. `111111111111`) are also not allowed. + +```ruby +require 'br-utilities' + +BrUtils.cnpj.generate # => e.g. "1GJTR3J3XSSA96" +BrUtils.cnpj.generate(format: true) # => e.g. "V1.J0V.8WE/DVZ7-50" +BrUtils.cnpj.generate( # => e.g. "12345678855883" + prefix: '12345678', + type: 'numeric' +) +CnpjGen.cnpj_gen(type: 'numeric') # => e.g. "65453043000178" +``` + +#### Validation (`#is_valid` / `CnpjVal.cnpj_val`) + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `case_sensitive` | `Boolean` | `true` | When `false`, lowercase letters are accepted for alphanumeric CNPJ (input is uppercased before validation). | +| `type` | `String` | `'alphanumeric'` | `'numeric'`: only digits (0–9); `'alphanumeric'`: digits and letters (0–9, A–Z). | + +```ruby +require 'br-utilities' + +BrUtils.cnpj.is_valid('98765432000198') # => true +BrUtils.cnpj.is_valid('98765432000199') # => false +BrUtils.cnpj.is_valid('1QB5UKALPYFP59') # => true +BrUtils.cnpj.is_valid('1QB5UKALpyfp59') # => false +BrUtils.cnpj.is_valid( # => true + '1QB5UKALpyfp59', + case_sensitive: false +) +BrUtils.cnpj.is_valid( # => false + '1QB5UKALPYFP59', + type: 'numeric' +) + +CnpjVal.cnpj_val('98765432000198') # => true +CnpjVal.cnpj_val('1QB5UKALpyfp59', case_sensitive: false) # => true +CnpjVal.cnpj_val('1QB5UKALPYFP59', type: 'numeric') # => false +``` -## Releasing (one gem at a time) +Invalid CNPJ returns **`false`** without raising. Wrong input types raise **`CnpjVal::TypeMismatchError`**. -1. Bump version in `packages//src//version.rb`, commit. -2. Push tag: `git tag @` (e.g. `cpf-dv@1.0.1`), then `git push origin `. -3. Configure [RubyGems Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) for this repo and the `release` environment. -4. The Release workflow runs: tests the package, builds the gem, publishes to RubyGems via OIDC. No secrets required. +### Domain aggregators (standalone) -Release leaves first (e.g. `cpf-dv`, `cpf-fmt`), then dependents (`cpf-utilities`, `cnpj-utilities`), then `br-utilities`. +Use `CpfUtils` or `CnpjUtils` directly when you only need one domain: + +```ruby +require 'br-utilities' + +cpf_utils = CpfUtils.new( + formatter: { hidden: true }, + generator: { format: true } +) + +cnpj_utils = CnpjUtils.new( + formatter: { hidden: true }, + generator: { format: true }, + validator: { type: 'numeric' } +) + +cpf_utils.format('12345678909') # => "123.***.***-**" +cnpj_utils.format('03603568000195') # => "03.603.***/****-**" +``` + +### Accessing components + +Each domain aggregator exposes its internal formatter, generator, and validator: + +```ruby +require 'br-utilities' + +utils = BrUtils.new + +utils.cpf.formatter.format('12345678909', hidden: true) # => "123.***.***-**" +utils.cpf.generator.generate(format: true) # => e.g. "545.507.690-68" +utils.cpf.validator.is_valid('12345678909') # => true + +utils.cnpj.formatter.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" +utils.cnpj.generator.generate(format: true) # => e.g. "8O.BE5.2KL/UI0Y-06" +utils.cnpj.validator.is_valid('03603568000195') # => true +``` + +### Using component classes and nested modules + +Preferred paths after `require 'br-utilities'`: + +```ruby +require 'br-utilities' + +# Main classes at the façade root +formatter = BrUtils::CpfFormatter.new(hidden: true) +generator = BrUtils::CnpjGenerator.new(type: 'numeric') +validator = BrUtils::CnpjValidator.new + +formatter.format('12345678909') # => "123.***.***-**" + +# Options, helpers, and errors under nested package modules +options = BrUtils::CpfFmt::CpfFormatterOptions.new(dash_key: '|') +BrUtils::CpfFmt.cpf_fmt('12345678909') # => "123.456.789-09" + +begin + BrUtils::CnpjFmt.cnpj_fmt(12_345) +rescue BrUtils::CnpjFmt::TypeMismatchError + # wrong input type +end +``` + +Root siblings remain supported (same objects as the nests): + +```ruby +CpfFmt.cpf_fmt('12345678909', dash_key: '|') # => "123.456.789|09" +CpfGen.cpf_gen(format: true) # => e.g. "478.442.410-55" +CpfVal.cpf_val('12345678909') # => true +CnpjFmt.cnpj_fmt('01ABC234000X56', slash_key: '|') # => "01.ABC.234|000X-56" +CnpjGen.cnpj_gen(type: 'numeric') # => e.g. "65453043000178" +CnpjVal.cnpj_val('9JN7MGLJZXIO50') # => true +``` + +See [`cpf-utilities`](../cpf-utilities/README.md) and [`cnpj-utilities`](../cnpj-utilities/README.md) for full option and error details. + +### Mixing styles + +Use `BrUtils` where a shared configuration helps, and standalone components or helpers elsewhere — they are the same underlying classes: + +```ruby +require 'br-utilities' + +utils = BrUtils.new(cnpj: { validator: { type: 'numeric' } }) + +# Via façade +utils.cpf.format('12345678909') # => "123.456.789-09" + +# Via component returned by the façade +utils.cnpj.formatter.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" + +# Via a separate component instance +BrUtils::CnpjFormatter.new.format('03603568000195') # => "03.603.568/0001-95" + +# Via functional helpers +CpfFmt.cpf_fmt('12345678909') # => "123.456.789-09" +CnpjVal.cnpj_val('98.765.432/0001-98') # => true +``` + +## API + +### Exports + +After `require 'br-utilities'`: + +- **`BrUtils`**: Façade class to create an instance with optional default CPF and CNPJ utils settings. +- **`BrUtils.cpf` / `.cnpj`**: Class helpers that forward to `BrUtils::DEFAULT` domain accessors. +- **`BrUtils::DEFAULT`**: Mutable pre-built `BrUtils` instance (same object the class helpers use). Process-wide / shared across threads — prefer `BrUtils.new` or per-call options under concurrency. +- **`BrUtils::VERSION`**: Gem version string. +- **Main-class shortcuts**: `BrUtils::CpfFormatter`, `BrUtils::CpfFormatterOptions`, `BrUtils::CpfGenerator`, `BrUtils::CpfGeneratorOptions`, `BrUtils::CpfValidator`, `BrUtils::CnpjFormatter`, `BrUtils::CnpjFormatterOptions`, `BrUtils::CnpjGenerator`, `BrUtils::CnpjGeneratorOptions`, `BrUtils::CnpjValidator`, `BrUtils::CnpjValidatorOptions` (same objects as the sibling classes). Error-marker shortcuts: `BrUtils::CpfFormatterError`, `BrUtils::CpfGeneratorError`, `BrUtils::CpfValidatorError`, `BrUtils::CnpjFormatterError`, `BrUtils::CnpjGeneratorError`, `BrUtils::CnpjValidatorError`. +- **Nested package modules**: `BrUtils::CpfUtils`, `BrUtils::CnpjUtils`, `BrUtils::CpfFmt`, `BrUtils::CpfGen`, `BrUtils::CpfVal`, `BrUtils::CnpjFmt`, `BrUtils::CnpjGen`, `BrUtils::CnpjVal` — full sibling surface (Options, helpers, errors, types). +- **Root sibling modules** (still supported): `CpfUtils`, `CnpjUtils`, `CpfFmt`, `CpfGen`, `CpfVal`, `CnpjFmt`, `CnpjGen`, `CnpjVal` — same objects as the nests. + +### Errors & Exceptions + +`BrUtils` defines only API-misuse errors for this gem’s argument rules. Domain errors are raised by the bundled packages and propagate unchanged. + +#### Defined by `br-utilities` + +Errors defined by this gem are **API misuse** only (wrong type or invalid argument combination). Every custom error includes the `BrUtils::Error` marker module. This gem defines **no** `BrUtils::DomainError` and no domain leaves — domain failures come only from the [bundled packages](#propagated-from-bundled-packages) and keep those packages’ namespaces (`CpfFmt::…`, `CnpjGen::…`, …). + +`rescue BrUtils::Error` catches **only** errors this gem raises. It does **not** catch component errors that propagate unchanged. + +##### Summary + +| Class | Inherits from | Category | Trigger condition | +|-------|---------------|----------|-------------------| +| `BrUtils::InvalidArgumentCombinationError` | `BrUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include BrUtils::Error`) | API misuse | Non-`nil` settings `Hash` passed together with any non-`nil` keyword argument | +| `BrUtils::TypeMismatchError` | `BrUtils::TypeMismatchError < TypeError < StandardError` (+ `include BrUtils::Error`) | API misuse | Non-`nil` `settings` argument to `BrUtils.new` is not a `Hash` | + +##### `BrUtils::Error` (marker module) + +- **Inheritance:** module marker mixed into every custom error this gem raises 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 this gem raises. +- **Example:** N/A +- **How to rescue it:** + +```ruby +rescue BrUtils::Error + # TypeMismatchError, InvalidArgumentCombinationError from this gem only + # (not CpfFmt::*, CnpjGen::*, or other bundled-package errors) +``` + +##### `BrUtils::TypeMismatchError` + +- **Inheritance:** `BrUtils::TypeMismatchError < TypeError < StandardError` (includes `BrUtils::Error`) +- **Category:** API misuse — the caller passed a value of the wrong type. +- **When it is raised:** Raised when `BrUtils.new` receives a non-`nil` `settings` argument that is not a `Hash`. +- **Example:** + +```ruby +BrUtils.new('not-a-hash') # raises BrUtils::TypeMismatchError +BrUtils.new(false) # raises BrUtils::TypeMismatchError (false is non-nil) +``` + +- **How to rescue it:** + +```ruby +rescue BrUtils::TypeMismatchError + # this gem's type-contract violation + +rescue TypeError + # native type errors, including this gem's TypeMismatchError +``` + +##### `BrUtils::InvalidArgumentCombinationError` + +- **Inheritance:** `BrUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (includes `BrUtils::Error`) +- **Category:** API misuse — the caller mixed mutually exclusive argument patterns. +- **When it is raised:** Raised when `BrUtils.new` receives both a non-`nil` settings `Hash` and any non-`nil` keyword argument (`cpf:`, `cnpj:`, `cpf_formatter:`, …) at the same time. +- **Example:** + +```ruby +BrUtils.new({ cpf: { formatter: { hidden: true } } }, cnpj: { formatter: { hidden: true } }) +# raises BrUtils::InvalidArgumentCombinationError +``` + +- **How to rescue it:** + +```ruby +rescue BrUtils::InvalidArgumentCombinationError + # this gem's invalid signature combination + +rescue ArgumentError + # native argument errors, including this gem's InvalidArgumentCombinationError +``` + +##### Rescue granularity + +Each level is shown as its own standalone example (do not merge them into one `rescue` ladder — a broad native handler would make narrower clauses unreachable). + +```ruby +require 'br-utilities' + +# 1) Single native class — catches misuse errors of that kind, +# including non-library ones already handled elsewhere in the consumer's code. +begin + BrUtils.new('not-a-hash') +rescue TypeError + # BrUtils::TypeMismatchError and any other TypeError (library or not) +end + +begin + BrUtils.new({ cpf: {} }, cnpj: CnpjUtils.new) +rescue ArgumentError + # BrUtils::InvalidArgumentCombinationError and any other ArgumentError (library or not) +end +``` + +```ruby +require 'br-utilities' + +# 2) BrUtils::DomainError — not applicable: this gem defines no DomainError +# (and no domain leaves). Domain failures come from bundled packages only. +# begin +# BrUtils.new(cpf: { formatter: { hidden_start: -1 } }) +# rescue BrUtils::DomainError # NameError — constant is not defined +# end +``` + +```ruby +require 'br-utilities' + +# 3) BrUtils::Error — catches everything this gem raises, regardless of native ancestry. +# Does not catch CpfFmt::*, CnpjGen::*, or other bundled-package errors. +begin + BrUtils.new('not-a-hash') +rescue BrUtils::Error + # every custom error that includes BrUtils::Error +end +``` + +```ruby +require 'br-utilities' + +# 4) Specific leaf class — catches only that exact failure mode. +begin + BrUtils.new('not-a-hash') +rescue BrUtils::TypeMismatchError + # only BrUtils::TypeMismatchError +end +``` + +#### Propagated from bundled packages + +`BrUtils` does not redefine domain exception types. Construction, setters, and domain method calls raise the same errors as [`cpf-utilities`](../cpf-utilities/README.md) and [`cnpj-utilities`](../cnpj-utilities/README.md): + +- **CPF formatting**: `CpfFmt::TypeMismatchError`, `CpfFmt::OutOfRangeError`, `CpfFmt::ValidationError`, `CpfFmt::InvalidLengthError` (passed to `on_fail`, not raised by `#format`), and related classes. +- **CPF generation**: `CpfGen::TypeMismatchError`, `CpfGen::ValidationError`, and related classes. +- **CPF validation**: `CpfVal::TypeMismatchError` and related classes. +- **CNPJ formatting**: `CnpjFmt::TypeMismatchError`, `CnpjFmt::OutOfRangeError`, `CnpjFmt::ValidationError`, `CnpjFmt::InvalidLengthError` (passed to `on_fail`), and related classes. +- **CNPJ generation**: `CnpjGen::TypeMismatchError`, `CnpjGen::ValidationError`, and related classes. +- **CNPJ validation**: `CnpjVal::TypeMismatchError`, `CnpjVal::ValidationError`, and related classes. + +Invalid option types are typically **`TypeError`** subclasses (`*::TypeMismatchError`); invalid option values are domain errors under each package’s `DomainError` hierarchy. CPF and CNPJ validation failures return `false`. Formatting length failures are handled by **`on_fail`** (default returns an empty string). + +```ruby +require 'br-utilities' + +begin + BrUtils.new.cnpj.format(12_345) +rescue CnpjFmt::TypeMismatchError => e + puts e.message +end + +begin + BrUtils.new.cnpj.is_valid(12_345_678_000_198) +rescue CnpjVal::TypeMismatchError => e + puts e.message +end + +# Custom on_fail for invalid length +custom_fail = ->(value, _exception) { "Invalid: #{value}" } + +BrUtils.cpf.format('short', on_fail: custom_fail) # => "Invalid: short" +BrUtils.cnpj.format('short', on_fail: custom_fail) # => "Invalid: short" +BrUtils.cpf.format('short') # => "" (default on_fail) +``` + +For exhaustive exception lists and edge-case behavior, see each [bundled package](#bundled-packages) README. + +### Bundled packages + +| Package | Main resources | README | +|---------|----------------|--------| +| [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) | `CpfUtils`, `CpfFormatter`, `CpfGenerator`, `CpfValidator`, `CpfFmt.cpf_fmt`, `CpfGen.cpf_gen`, `CpfVal.cpf_val` | [docs](../cpf-utilities/README.md) | +| [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) | `CnpjUtils`, `CnpjFormatter`, `CnpjGenerator`, `CnpjValidator`, `CnpjFmt.cnpj_fmt`, `CnpjGen.cnpj_gen`, `CnpjVal.cnpj_val` | [docs](../cnpj-utilities/README.md) | + +All of the above are pulled in as dependencies of **`br-utilities`**. Interactive demos: [CPF](https://cpf-utils.vercel.app/) and [CNPJ](https://cnpj-utils.vercel.app/). + +## 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 -MIT. See [LICENSE](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) diff --git a/README.pt.md b/README.pt.md new file mode 100644 index 0000000..40d50ac --- /dev/null +++ b/README.pt.md @@ -0,0 +1,700 @@ +![br-utilities para Ruby](https://br-utils.vercel.app/img/cover_br-utils.jpg) + +> 🚀 **Suporte total ao [novo formato alfanumérico de CNPJ](https://github.com/user-attachments/files/23937961/calculodvcnpjalfanaumerico.pdf).** + +> 🌎 [Access documentation in English](./README.md) + +Kit em Ruby para as principais operações com dados brasileiros: CPF (Cadastro de Pessoa Física) e CNPJ (Cadastro Nacional da Pessoa Jurídica). Envolve [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) e [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) em uma única classe fachada (`BrUtils`). + +## Suporte a Ruby + +| ![Ruby 3.1](https://img.shields.io/badge/Ruby-3.1-CC342D?logo=ruby&logoColor=white) | ![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) | ![Ruby 4.0](https://img.shields.io/badge/Ruby-4.0-CC342D?logo=ruby&logoColor=white) | +| --- | --- | --- | --- | --- | +| Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | Passing ✔ | + +Requer Ruby **≥ 3.1** (veja `required_ruby_version` no gemspec). + +## Recursos + +- ✅ **API unificada de alto nível**: Helpers de classe `BrUtils.cpf` / `.cnpj` alias de `BrUtils::DEFAULT`; cada domínio oferece `format`, `generate` e `is_valid` +- ✅ **Domínios empacotados**: [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) e [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) instalados juntos +- ✅ **CNPJ alfanumérico**: Suporte completo ao novo formato alfanumérico de CNPJ (a partir de 2026) +- ✅ **Instância reutilizável**: Classe `BrUtils` com configurações padrão opcionais de CPF e CNPJ (mapeamentos aninhados, kwargs planos de componentes ou instâncias prontas de utils) +- ✅ **Acesso em dois níveis**: Prefira atalhos de classes principais na raiz da fachada (`BrUtils::CpfFormatter`, `BrUtils::CnpjValidator`, …); Options, helpers e erros ficam nos módulos aninhados (`BrUtils::CpfFmt`, `BrUtils::CnpjUtils`, …). Os irmãos na raiz (`CpfUtils`, `CnpjUtils`, `CpfFmt`, …) continuam funcionando +- ✅ **Sobrescritas por chamada**: Configure padrões na fachada / utils de domínio; sobrescreva opções em uma única chamada de `format` / `generate` / `is_valid` +- ✅ **Tratamento de erros**: Erros de domínio propagam inalterados dos pacotes incluídos; esta gem define `BrUtils::TypeMismatchError` e `BrUtils::InvalidArgumentCombinationError` para uso indevido da API + +## Instalação + +Instale a gem diretamente: + +```bash +gem install br-utilities +``` + +Ou adicione ao seu `Gemfile` e execute `bundle install`: + +```ruby +gem 'br-utilities' +``` + +Isso instala **`br-utilities`** junto com [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) e [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) (que por sua vez trazem os pacotes componentes de CPF e CNPJ). Você **não** precisa de `gem install` / linhas `gem` separados para os pacotes de domínio ao usar **`br-utilities`**. + +## Require + +```ruby +require 'br-utilities' +``` + +## Início rápido + +Prefira os helpers de classe do agregador (`BrUtils.cpf` / `BrUtils.cnpj`) para chamadas pontuais — eles encaminham para `BrUtils::DEFAULT`: + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +# CPF (pessoa física) +BrUtils.cpf.format(cpf) # => "123.456.789-09" +BrUtils.cpf.generate(format: true) # => ex.: "478.442.410-55" +BrUtils.cpf.is_valid('123.456.789-09') # => true + +# CNPJ (pessoa jurídica) +BrUtils.cnpj.format(cnpj) # => "03.603.568/0001-95" +BrUtils.cnpj.generate(format: true) # => ex.: "AB.123.CDE/0001-55" +BrUtils.cnpj.is_valid('98765432000198') # => true +``` + +**Com agregadores de domínio:** + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +CpfUtils.format(cpf) # => "123.456.789-09" +CnpjUtils.format(cnpj) # => "03.603.568/0001-95" +CpfUtils.is_valid(cpf) # => true +CnpjUtils.is_valid(cnpj) # => true +``` + +**Com helpers funcionais** (módulos irmãos na raiz, carregados por esta gem): + +```ruby +require 'br-utilities' + +cpf = '12345678909' +cnpj = '03603568000195' + +CpfFmt.cpf_fmt(cpf) # => "123.456.789-09" +CpfVal.cpf_val(cpf) # => true +CnpjFmt.cnpj_fmt(cnpj) # => "03.603.568/0001-95" +CnpjVal.cnpj_val(cnpj) # => true +``` + +## Utilização + +Você pode trabalhar destas formas equivalentes: + +1. **`BrUtils.cpf` / `.cnpj`** — helpers de classe para chamadas rápidas (encaminham para `DEFAULT`). +2. **`BrUtils::DEFAULT`** — singleton compartilhado mutável (o mesmo objeto usado pelos helpers de classe; em todo o processo / não isolado por thread). +3. **`BrUtils.new`** — instância configurável com padrões compartilhados entre os domínios CPF e CNPJ. +4. **Agregadores de domínio** — `CpfUtils` / `CnpjUtils` (ou `BrUtils::CpfUtils` / `BrUtils::CnpjUtils`) diretamente. +5. **Classes principais sob `BrUtils`** — `BrUtils::CpfFormatter`, `BrUtils::CnpjGenerator` e atalhos relacionados. +6. **Módulos aninhados do pacote** — Options, helpers, erros e tipos via `BrUtils::CpfFmt` / `CpfGen` / `CpfVal` / `CnpjFmt` / `CnpjGen` / `CnpjVal` / `CpfUtils` / `CnpjUtils`. +7. **Módulos irmãos na raiz** (ainda suportados) — `CpfFmt`, `CnpjUtils` e demais inalterados. + +Todas as abordagens expõem as mesmas opções e comportamento dentro de cada domínio. Para tabelas de opções exaustivas e detalhes específicos de cada componente, consulte o README de cada [pacote incluído](#pacotes-incluídos). + +### Helpers de classe (`BrUtils.cpf` / `.cnpj`) + +Esses métodos de classe retornam as mesmas instâncias de utils de domínio que `BrUtils::DEFAULT`. Prefira-os para chamadas pontuais: + +```ruby +BrUtils.cpf.format('12345678909') +BrUtils.cpf.generate(format: true) +BrUtils.cpf.is_valid('12345678909') + +BrUtils.cnpj.format('03603568000195') +BrUtils.cnpj.generate(type: 'numeric') +BrUtils.cnpj.is_valid('98765432000198') +``` + +### `BrUtils::DEFAULT` (instância padrão) + +`BrUtils::DEFAULT` é o singleton pré-construído e **mutável** por trás dos helpers de classe (paridade com a exportação padrão do JS / `br_utils` do Python). Sua configuração é **em todo o processo e compartilhada entre threads**: mutá-lo (ex.: `DEFAULT.cpf = …`) afeta chamadas subsequentes de `BrUtils.cpf` / `.cnpj` para todos os callers no processo. Prefira `BrUtils.new` ou opções por chamada para trabalho concorrente ou isolado; instâncias customizadas permanecem independentes de `DEFAULT`: + +```ruby +BrUtils::DEFAULT.cpf = CpfUtils.new(formatter: { dash_key: '|' }) +BrUtils.cpf.format('12345678909') # => "123.456.789|09" + +custom = BrUtils.new +custom.cpf.format('12345678909') # => "123.456.789-09" (não afetado) +``` + +### `BrUtils` (classe) + +Para utils de CPF ou CNPJ padrão customizados, crie sua própria instância: + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true } + }, + cnpj: { + formatter: { hidden: true }, + generator: { type: 'numeric', format: true }, + validator: { type: 'numeric' } + } +) + +utils.cpf.format('12345678909') # => "123.###.###-##" +utils.cpf.generate # => ex.: "005.265.352-88" +utils.cnpj.format('03603568000195') # => "03.603.***/****-**" +utils.cnpj.generate # => ex.: "73.008.535/0005-06" + +# Acessar ou substituir instâncias internas de domínio +utils.cpf # => CpfUtils +utils.cnpj # => CnpjUtils +``` + +- **`BrUtils.new(settings = nil, **keywords)`**: Configurações opcionais. Passe um `Hash` de settings com chaves `:cpf` e/ou `:cnpj`, **ou** as mesmas chaves (mais kwargs planos de componentes) como argumentos nomeados — não ambos (passar ambos lança `BrUtils::InvalidArgumentCombinationError`). + - **`:cpf` / `:cnpj`**: Uma instância pronta de `CpfUtils` / `CnpjUtils` **ou** um `Hash` de configuração repassado ao construtor do utils correspondente. Dentro desse `Hash`, cada chave de recurso (`:formatter`, `:generator` e `:validator` para CNPJ) aceita um objeto de opções ou um mapeamento de valores de opção. + - **`:cpf_formatter`**, **`:cpf_generator`**, **`:cnpj_formatter`**, **`:cnpj_generator`**, **`:cnpj_validator`**: Argumentos planos de conveniência quando apenas componentes individuais precisam de customização. São ignorados quando o argumento `:cpf` ou `:cnpj` correspondente é fornecido. +- **`#cpf`**, **`#cnpj`**: Acessores (getters e setters) das instâncias de utils de domínio. Os setters aceitam uma instância de utils, um `Hash` de configuração ou `nil` para voltar aos padrões (substitui a instância inteira; não faz merge). + +Opções planas no construtor (alternativa aos mapeamentos aninhados `:cpf` / `:cnpj`): + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf_formatter: CpfFmt::CpfFormatterOptions.new(hidden: true, hidden_key: '#'), + cpf_generator: CpfGen::CpfGeneratorOptions.new(format: true), + cnpj_formatter: CnpjFmt::CnpjFormatterOptions.new(hidden: true, hidden_key: '#'), + cnpj_generator: CnpjGen::CnpjGeneratorOptions.new(format: true, type: 'numeric'), + cnpj_validator: CnpjVal::CnpjValidatorOptions.new(type: 'numeric') +) +``` + +Passar um `Hash` de settings posicional junto com qualquer palavra-chave lança: + +```ruby +BrUtils.new({ cpf: {} }, cnpj: CnpjUtils.new) +# lança BrUtils::InvalidArgumentCombinationError +``` + +### Padrões da instância e sobrescritas por chamada + +```ruby +require 'br-utilities' + +utils = BrUtils.new( + cpf: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true } + }, + cnpj: { + formatter: { hidden: true, hidden_key: '#' }, + generator: { format: true }, + validator: { type: 'numeric' } + } +) + +cpf = '12345678909' +cnpj = '03603568000195' + +utils.cpf.format(cpf) # => "123.###.###-##" +utils.cpf.format(cpf, hidden: false) # só nesta chamada: sem máscara +utils.cpf.generate(format: false) # só nesta chamada: saída compacta + +utils.cnpj.format(cnpj) # => "03.603.###/####-##" +utils.cnpj.format(cnpj, hidden: false) # só nesta chamada: sem máscara +utils.cnpj.is_valid('1QB5UKALPYFP59') # => false (validador da instância é só numérico) +utils.cnpj.is_valid( # => true nesta chamada + '1QB5UKALPYFP59', + type: 'alphanumeric' +) +``` + +Passar uma instância de `CnpjFmt::CnpjFormatterOptions`, `CnpjGen::CnpjGeneratorOptions` ou `CnpjVal::CnpjValidatorOptions` ao construtor de `BrUtils` armazena esse objeto por referência — mutá-lo depois afeta chamadas subsequentes sem sobrescrita por chamada. + +Para alterar uma única opção aninhada sem substituir o utils de domínio inteiro, mute via os acessores de domínio (ex.: `utils.cpf.formatter.options.hidden = true`). + +### Operações de CPF + +Os métodos de CPF são acessados via `BrUtils.cpf`, `utils.cpf`, `CpfUtils` ou os helpers `CpfFmt` / `CpfGen` / `CpfVal`. O CPF usa a API de [`cpf-utilities`](../cpf-utilities/README.pt.md). + +#### Formatação (`#format` / `CpfFmt.cpf_fmt`) + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `hidden` | `Boolean` | `false` | Se `true`, mascara dígitos entre `hidden_start` e `hidden_end` com `hidden_key` | +| `hidden_key` | `String` | `'*'` | Caractere(s) usados para substituir dígitos mascarados | +| `hidden_start` | `Integer` | `3` | Índice inicial (0–10, inclusivo) do intervalo a ocultar | +| `hidden_end` | `Integer` | `10` | Índice final (0–10, inclusivo) do intervalo a ocultar | +| `dot_key` | `String` | `'.'` | Delimitador de ponto (ex.: em `123.456.789`) | +| `dash_key` | `String` | `'-'` | Delimitador de hífen (ex.: antes dos dígitos verificadores `…-09`) | +| `escape` | `Boolean` | `false` | Se `true`, escapa caracteres especiais HTML no resultado | +| `encode` | `Boolean` | `false` | Se `true`, codifica o resultado para URL (similar ao `encodeURIComponent` do JavaScript) | +| `on_fail` | `Proc` / invocável | retorna `''` | Callback quando o tamanho da entrada sanitizada ≠ 11; o retorno é usado como resultado | + +O **`on_fail`** padrão retorna uma string vazia. Comprimento inválido **não** lança exceção em `#format`. + +```ruby +require 'br-utilities' + +cpf = '12345678909' + +BrUtils.cpf.format(cpf) # => "123.456.789-09" +BrUtils.cpf.format(cpf, hidden: true, hidden_key: '#') # => "123.###.###-##" +BrUtils.cpf.format(cpf, dot_key: '', dash_key: '_') # => "123456789_09" + +CpfFmt.cpf_fmt(cpf, hidden: true) # => "123.***.***-**" +``` + +#### Geração (`#generate` / `CpfGen.cpf_gen`) + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | Se `true`, retorna o CPF gerado no formato padrão (`000.000.000-00`) | +| `prefix` | `String` | `''` | String parcial inicial (0–9 dígitos). Não dígitos são removidos; caracteres faltantes são gerados e os dígitos verificadores calculados. Prefixos com mais de 9 dígitos são truncados silenciosamente. | + +Regras de prefixo: a base (primeiros 9 dígitos) não pode ser toda zeros; 9 dígitos repetidos (ex.: `999999999`) não são permitidos. + +```ruby +require 'br-utilities' + +BrUtils.cpf.generate # => ex.: "11508890048" +BrUtils.cpf.generate(format: true) # => ex.: "661.134.831-00" +BrUtils.cpf.generate(prefix: '123456789') # => "12345678909" +CpfGen.cpf_gen(prefix: '123456789', format: true) # => "123.456.789-09" +``` + +#### Validação (`#is_valid` / `CpfVal.cpf_val`) + +Aceita CPF formatado ou não (ou um `Array` de strings). Retorna **`true`** ou **`false`** sem lançar exceção para CPF inválido. Não há opções de validador. + +```ruby +require 'br-utilities' + +BrUtils.cpf.is_valid('12345678909') # => true +BrUtils.cpf.is_valid('123.456.789-09') # => true +BrUtils.cpf.is_valid('12345678900') # => false +CpfVal.cpf_val('12345678909') # => true +``` + +### Operações de CNPJ + +Os métodos de CNPJ são acessados via `BrUtils.cnpj`, `utils.cnpj`, `CnpjUtils` ou os helpers `CnpjFmt` / `CnpjGen` / `CnpjVal`. O CNPJ usa a API de [`cnpj-utilities`](../cnpj-utilities/README.pt.md). + +#### Formatação (`#format` / `CnpjFmt.cnpj_fmt`) + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `hidden` | `Boolean` | `false` | Se `true`, mascara caracteres entre `hidden_start` e `hidden_end` com `hidden_key` | +| `hidden_key` | `String` | `'*'` | Caractere(s) usados para substituir caracteres mascarados | +| `hidden_start` | `Integer` | `5` | Índice inicial (0–13, inclusivo) do intervalo a ocultar | +| `hidden_end` | `Integer` | `13` | Índice final (0–13, inclusivo) do intervalo a ocultar | +| `dot_key` | `String` | `'.'` | Delimitador de ponto (ex.: em `12.345.678`) | +| `slash_key` | `String` | `'/'` | Delimitador de barra (ex.: antes da filial `…/0001-90`) | +| `dash_key` | `String` | `'-'` | Delimitador de hífen (ex.: antes dos dígitos verificadores `…-90`) | +| `escape` | `Boolean` | `false` | Se `true`, escapa caracteres especiais HTML no resultado | +| `encode` | `Boolean` | `false` | Se `true`, codifica o resultado para URL (similar ao `encodeURIComponent` do JavaScript) | +| `on_fail` | `Proc` / invocável | retorna `''` | Callback quando o tamanho da entrada sanitizada ≠ 14; o retorno é usado como resultado | + +O **`on_fail`** padrão retorna uma string vazia. Tipos de entrada incorretos lançam **`CnpjFmt::TypeMismatchError`**. + +```ruby +require 'br-utilities' + +cnpj = '03603568000195' + +BrUtils.cnpj.format(cnpj) # => "03.603.568/0001-95" +BrUtils.cnpj.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" +BrUtils.cnpj.format( # => "03.603.###/####-##" + cnpj, + hidden: true, + hidden_key: '#' +) +BrUtils.cnpj.format( # => "03603568|0001_95" + cnpj, + dot_key: '', + slash_key: '|', + dash_key: '_' +) + +CnpjFmt.cnpj_fmt(cnpj) # => "03.603.568/0001-95" +``` + +#### Geração (`#generate` / `CnpjGen.cnpj_gen`) + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `format` | `Boolean` | `false` | Se `true`, retorna o CNPJ gerado no formato padrão (`00.000.000/0000-00`) | +| `prefix` | `String` | `''` | String parcial inicial (0–12 caracteres alfanuméricos). Caracteres faltantes são gerados e os dígitos verificadores calculados. | +| `type` | `String` | `'alphanumeric'` | Conjunto de caracteres para a parte gerada aleatoriamente: `'numeric'`, `'alphabetic'` ou `'alphanumeric'`. **Os dígitos verificadores são sempre numéricos.** | + +Regras de prefixo: o ID base (primeiros 8 caracteres) e o ID da filial (caracteres 9–12) não podem ser todos zeros; 12 dígitos repetidos (ex.: `111111111111`) também não são permitidos. + +```ruby +require 'br-utilities' + +BrUtils.cnpj.generate # => ex.: "1GJTR3J3XSSA96" +BrUtils.cnpj.generate(format: true) # => ex.: "V1.J0V.8WE/DVZ7-50" +BrUtils.cnpj.generate( # => ex.: "12345678855883" + prefix: '12345678', + type: 'numeric' +) +CnpjGen.cnpj_gen(type: 'numeric') # => ex.: "65453043000178" +``` + +#### Validação (`#is_valid` / `CnpjVal.cnpj_val`) + +| Opção | Tipo | Padrão | Descrição | +|--------|------|---------|-------------| +| `case_sensitive` | `Boolean` | `true` | Se `false`, letras minúsculas são aceitas para CNPJ alfanumérico (a entrada é convertida para maiúsculas antes da validação). | +| `type` | `String` | `'alphanumeric'` | `'numeric'`: apenas dígitos (0–9); `'alphanumeric'`: dígitos e letras (0–9, A–Z). | + +```ruby +require 'br-utilities' + +BrUtils.cnpj.is_valid('98765432000198') # => true +BrUtils.cnpj.is_valid('98765432000199') # => false +BrUtils.cnpj.is_valid('1QB5UKALPYFP59') # => true +BrUtils.cnpj.is_valid('1QB5UKALpyfp59') # => false +BrUtils.cnpj.is_valid( # => true + '1QB5UKALpyfp59', + case_sensitive: false +) +BrUtils.cnpj.is_valid( # => false + '1QB5UKALPYFP59', + type: 'numeric' +) + +CnpjVal.cnpj_val('98765432000198') # => true +CnpjVal.cnpj_val('1QB5UKALpyfp59', case_sensitive: false) # => true +CnpjVal.cnpj_val('1QB5UKALPYFP59', type: 'numeric') # => false +``` + +CNPJ inválido retorna **`false`** sem lançar exceção. Tipos de entrada incorretos lançam **`CnpjVal::TypeMismatchError`**. + +### Agregadores de domínio (isolados) + +Use `CpfUtils` ou `CnpjUtils` diretamente quando precisar de apenas um domínio: + +```ruby +require 'br-utilities' + +cpf_utils = CpfUtils.new( + formatter: { hidden: true }, + generator: { format: true } +) + +cnpj_utils = CnpjUtils.new( + formatter: { hidden: true }, + generator: { format: true }, + validator: { type: 'numeric' } +) + +cpf_utils.format('12345678909') # => "123.***.***-**" +cnpj_utils.format('03603568000195') # => "03.603.***/****-**" +``` + +### Acessando componentes + +Cada agregador de domínio expõe seu formatador, gerador e validador internos: + +```ruby +require 'br-utilities' + +utils = BrUtils.new + +utils.cpf.formatter.format('12345678909', hidden: true) # => "123.***.***-**" +utils.cpf.generator.generate(format: true) # => ex.: "545.507.690-68" +utils.cpf.validator.is_valid('12345678909') # => true + +utils.cnpj.formatter.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" +utils.cnpj.generator.generate(format: true) # => ex.: "8O.BE5.2KL/UI0Y-06" +utils.cnpj.validator.is_valid('03603568000195') # => true +``` + +### Usando classes componentes e módulos aninhados + +Caminhos preferidos após `require 'br-utilities'`: + +```ruby +require 'br-utilities' + +# Classes principais na raiz da fachada +formatter = BrUtils::CpfFormatter.new(hidden: true) +generator = BrUtils::CnpjGenerator.new(type: 'numeric') +validator = BrUtils::CnpjValidator.new + +formatter.format('12345678909') # => "123.***.***-**" + +# Options, helpers e erros nos módulos aninhados do pacote +options = BrUtils::CpfFmt::CpfFormatterOptions.new(dash_key: '|') +BrUtils::CpfFmt.cpf_fmt('12345678909') # => "123.456.789-09" + +begin + BrUtils::CnpjFmt.cnpj_fmt(12_345) +rescue BrUtils::CnpjFmt::TypeMismatchError + # tipo de entrada incorreto +end +``` + +Os irmãos na raiz continuam suportados (os mesmos objetos dos nests): + +```ruby +CpfFmt.cpf_fmt('12345678909', dash_key: '|') # => "123.456.789|09" +CpfGen.cpf_gen(format: true) # => ex.: "478.442.410-55" +CpfVal.cpf_val('12345678909') # => true +CnpjFmt.cnpj_fmt('01ABC234000X56', slash_key: '|') # => "01.ABC.234|000X-56" +CnpjGen.cnpj_gen(type: 'numeric') # => ex.: "65453043000178" +CnpjVal.cnpj_val('9JN7MGLJZXIO50') # => true +``` + +Consulte [`cpf-utilities`](../cpf-utilities/README.pt.md) e [`cnpj-utilities`](../cnpj-utilities/README.pt.md) para detalhes completos de opções e erros. + +### Misturando estilos + +Use `BrUtils` onde uma configuração compartilhada ajuda, e componentes ou helpers isolados em outros pontos — são as mesmas classes subjacentes: + +```ruby +require 'br-utilities' + +utils = BrUtils.new(cnpj: { validator: { type: 'numeric' } }) + +# Via fachada +utils.cpf.format('12345678909') # => "123.456.789-09" + +# Via componente retornado pela fachada +utils.cnpj.formatter.format('12ABC34500DE99') # => "12.ABC.345/00DE-99" + +# Via instância de componente separada +BrUtils::CnpjFormatter.new.format('03603568000195') # => "03.603.568/0001-95" + +# Via helpers funcionais +CpfFmt.cpf_fmt('12345678909') # => "123.456.789-09" +CnpjVal.cnpj_val('98.765.432/0001-98') # => true +``` + +## API + +### Exportações + +Após `require 'br-utilities'`: + +- **`BrUtils`**: Classe fachada para criar uma instância com configurações opcionais dos utils de CPF e CNPJ. +- **`BrUtils.cpf` / `.cnpj`**: Helpers de classe que encaminham para os acessores de domínio de `BrUtils::DEFAULT`. +- **`BrUtils::DEFAULT`**: Instância pré-construída e mutável de `BrUtils` (o mesmo objeto usado pelos helpers de classe). Em todo o processo / compartilhada entre threads — prefira `BrUtils.new` ou opções por chamada sob concorrência. +- **`BrUtils::VERSION`**: String da versão da gem. +- **Atalhos de classes principais**: `BrUtils::CpfFormatter`, `BrUtils::CpfFormatterOptions`, `BrUtils::CpfGenerator`, `BrUtils::CpfGeneratorOptions`, `BrUtils::CpfValidator`, `BrUtils::CnpjFormatter`, `BrUtils::CnpjFormatterOptions`, `BrUtils::CnpjGenerator`, `BrUtils::CnpjGeneratorOptions`, `BrUtils::CnpjValidator`, `BrUtils::CnpjValidatorOptions` (os mesmos objetos das classes irmãs). Atalhos de marcadores de erro: `BrUtils::CpfFormatterError`, `BrUtils::CpfGeneratorError`, `BrUtils::CpfValidatorError`, `BrUtils::CnpjFormatterError`, `BrUtils::CnpjGeneratorError`, `BrUtils::CnpjValidatorError`. +- **Módulos aninhados do pacote**: `BrUtils::CpfUtils`, `BrUtils::CnpjUtils`, `BrUtils::CpfFmt`, `BrUtils::CpfGen`, `BrUtils::CpfVal`, `BrUtils::CnpjFmt`, `BrUtils::CnpjGen`, `BrUtils::CnpjVal` — superfície completa dos irmãos (Options, helpers, erros, tipos). +- **Módulos irmãos na raiz** (ainda suportados): `CpfUtils`, `CnpjUtils`, `CpfFmt`, `CpfGen`, `CpfVal`, `CnpjFmt`, `CnpjGen`, `CnpjVal` — os mesmos objetos dos nests. + +### Erros e exceções + +`BrUtils` define apenas erros de uso indevido da API para as regras de argumentos desta gem. Erros de domínio são lançados pelos pacotes incluídos e propagam inalterados. + +#### Definidos por `br-utilities` + +Os erros definidos por esta gem são **apenas uso indevido da API** (tipo errado ou combinação inválida de argumentos). Todo erro customizado inclui o módulo marcador `BrUtils::Error`. Esta gem **não** define `BrUtils::DomainError` nem folhas de domínio — falhas de domínio vêm apenas dos [pacotes incluídos](#propagados-dos-pacotes-incluídos) e mantêm os namespaces desses pacotes (`CpfFmt::…`, `CnpjGen::…`, …). + +`rescue BrUtils::Error` captura **apenas** erros que esta gem lança. **Não** captura erros de componentes que propagam inalterados. + +##### Resumo + +| Classe | Herda de | Categoria | Condição de disparo | +|-------|---------------|----------|-------------------| +| `BrUtils::InvalidArgumentCombinationError` | `BrUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (+ `include BrUtils::Error`) | Uso indevido da API | `Hash` de settings não-`nil` passado junto com qualquer argumento nomeado não-`nil` | +| `BrUtils::TypeMismatchError` | `BrUtils::TypeMismatchError < TypeError < StandardError` (+ `include BrUtils::Error`) | Uso indevido da API | Argumento `settings` não-`nil` em `BrUtils.new` não é um `Hash` | + +##### `BrUtils::Error` (módulo marcador) + +- **Herança:** módulo marcador misturado em todo erro customizado que esta gem lança via `include` (não é uma classe). +- **Categoria:** N/A (apenas alvo de rescue) — não é um modo de falha por si só. +- **Quando é lançado:** Nunca lançado diretamente; incluído por todo erro customizado que esta gem lança. +- **Exemplo:** N/A +- **Como resgatar:** + +```ruby +rescue BrUtils::Error + # TypeMismatchError, InvalidArgumentCombinationError apenas desta gem + # (não CpfFmt::*, CnpjGen::* ou outros erros dos pacotes incluídos) +``` + +##### `BrUtils::TypeMismatchError` + +- **Herança:** `BrUtils::TypeMismatchError < TypeError < StandardError` (inclui `BrUtils::Error`) +- **Categoria:** Uso indevido da API — o caller passou um valor do tipo errado. +- **Quando é lançado:** Quando `BrUtils.new` recebe um argumento `settings` não-`nil` que não é um `Hash`. +- **Exemplo:** + +```ruby +BrUtils.new('not-a-hash') # lança BrUtils::TypeMismatchError +BrUtils.new(false) # lança BrUtils::TypeMismatchError (false é não-nil) +``` + +- **Como resgatar:** + +```ruby +rescue BrUtils::TypeMismatchError + # violação de contrato de tipo desta gem + +rescue TypeError + # erros nativos de tipo, incluindo TypeMismatchError desta gem +``` + +##### `BrUtils::InvalidArgumentCombinationError` + +- **Herança:** `BrUtils::InvalidArgumentCombinationError < ArgumentError < StandardError` (inclui `BrUtils::Error`) +- **Categoria:** Uso indevido da API — o caller misturou padrões de argumentos mutuamente exclusivos. +- **Quando é lançado:** Quando `BrUtils.new` recebe um `Hash` de settings não-`nil` e qualquer argumento nomeado não-`nil` (`cpf:`, `cnpj:`, `cpf_formatter:`, …) ao mesmo tempo. +- **Exemplo:** + +```ruby +BrUtils.new({ cpf: { formatter: { hidden: true } } }, cnpj: { formatter: { hidden: true } }) +# lança BrUtils::InvalidArgumentCombinationError +``` + +- **Como resgatar:** + +```ruby +rescue BrUtils::InvalidArgumentCombinationError + # combinação inválida de assinatura desta gem + +rescue ArgumentError + # erros nativos de argumento, incluindo InvalidArgumentCombinationError desta gem +``` + +##### Granularidade de rescue + +Cada nível é mostrado como seu próprio exemplo isolado (não os una em uma única escada de `rescue` — um handler nativo amplo tornaria cláusulas mais estreitas inalcançáveis). + +```ruby +require 'br-utilities' + +# 1) Classe nativa única — captura erros de uso indevido desse tipo, +# incluindo os não-biblioteca já tratados em outro ponto do código do consumidor. +begin + BrUtils.new('not-a-hash') +rescue TypeError + # BrUtils::TypeMismatchError e qualquer outro TypeError (biblioteca ou não) +end + +begin + BrUtils.new({ cpf: {} }, cnpj: CnpjUtils.new) +rescue ArgumentError + # BrUtils::InvalidArgumentCombinationError e qualquer outro ArgumentError (biblioteca ou não) +end +``` + +```ruby +require 'br-utilities' + +# 2) BrUtils::DomainError — não aplicável: esta gem não define DomainError +# (nem folhas de domínio). Falhas de domínio vêm apenas dos pacotes incluídos. +# begin +# BrUtils.new(cpf: { formatter: { hidden_start: -1 } }) +# rescue BrUtils::DomainError # NameError — constante não definida +# end +``` + +```ruby +require 'br-utilities' + +# 3) BrUtils::Error — captura tudo que esta gem lança, independentemente da ancestralidade nativa. +# Não captura CpfFmt::*, CnpjGen::* ou outros erros dos pacotes incluídos. +begin + BrUtils.new('not-a-hash') +rescue BrUtils::Error + # todo erro customizado que inclui BrUtils::Error +end +``` + +```ruby +require 'br-utilities' + +# 4) Classe folha específica — captura apenas aquele modo de falha exato. +begin + BrUtils.new('not-a-hash') +rescue BrUtils::TypeMismatchError + # apenas BrUtils::TypeMismatchError +end +``` + +#### Propagados dos pacotes incluídos + +`BrUtils` não redefine tipos de exceção de domínio. Construção, setters e chamadas de métodos de domínio lançam os mesmos erros de [`cpf-utilities`](../cpf-utilities/README.pt.md) e [`cnpj-utilities`](../cnpj-utilities/README.pt.md): + +- **Formatação de CPF**: `CpfFmt::TypeMismatchError`, `CpfFmt::OutOfRangeError`, `CpfFmt::ValidationError`, `CpfFmt::InvalidLengthError` (passado a `on_fail`, não lançado por `#format`) e classes relacionadas. +- **Geração de CPF**: `CpfGen::TypeMismatchError`, `CpfGen::ValidationError` e classes relacionadas. +- **Validação de CPF**: `CpfVal::TypeMismatchError` e classes relacionadas. +- **Formatação de CNPJ**: `CnpjFmt::TypeMismatchError`, `CnpjFmt::OutOfRangeError`, `CnpjFmt::ValidationError`, `CnpjFmt::InvalidLengthError` (passado a `on_fail`) e classes relacionadas. +- **Geração de CNPJ**: `CnpjGen::TypeMismatchError`, `CnpjGen::ValidationError` e classes relacionadas. +- **Validação de CNPJ**: `CnpjVal::TypeMismatchError`, `CnpjVal::ValidationError` e classes relacionadas. + +Tipos de opção inválidos são tipicamente subclasses de **`TypeError`** (`*::TypeMismatchError`); valores de opção inválidos são erros de domínio sob a hierarquia `DomainError` de cada pacote. Falhas de validação de CPF e CNPJ retornam `false`. Falhas de comprimento na formatação são tratadas por **`on_fail`** (padrão retorna string vazia). + +```ruby +require 'br-utilities' + +begin + BrUtils.new.cnpj.format(12_345) +rescue CnpjFmt::TypeMismatchError => e + puts e.message +end + +begin + BrUtils.new.cnpj.is_valid(12_345_678_000_198) +rescue CnpjVal::TypeMismatchError => e + puts e.message +end + +# on_fail customizado para comprimento inválido +custom_fail = ->(value, _exception) { "Invalid: #{value}" } + +BrUtils.cpf.format('short', on_fail: custom_fail) # => "Invalid: short" +BrUtils.cnpj.format('short', on_fail: custom_fail) # => "Invalid: short" +BrUtils.cpf.format('short') # => "" (on_fail padrão) +``` + +Para listas exaustivas de exceções e comportamento em casos extremos, consulte o README de cada [pacote incluído](#pacotes-incluídos). + +### Pacotes incluídos + +| Pacote | Principais recursos | README | +|---------|----------------|--------| +| [`cpf-utilities`](https://rubygems.org/gems/cpf-utilities) | `CpfUtils`, `CpfFormatter`, `CpfGenerator`, `CpfValidator`, `CpfFmt.cpf_fmt`, `CpfGen.cpf_gen`, `CpfVal.cpf_val` | [docs](../cpf-utilities/README.pt.md) | +| [`cnpj-utilities`](https://rubygems.org/gems/cnpj-utilities) | `CnpjUtils`, `CnpjFormatter`, `CnpjGenerator`, `CnpjValidator`, `CnpjFmt.cnpj_fmt`, `CnpjGen.cnpj_gen`, `CnpjVal.cnpj_val` | [docs](../cnpj-utilities/README.pt.md) | + +Todos os acima são puxados como dependências de **`br-utilities`**. Demos interativas: [CPF](https://cpf-utils.vercel.app/) e [CNPJ](https://cnpj-utils.vercel.app/). + +## 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 o projeto for útil para você, considere: + +- ⭐ Dar uma estrela no repositório +- 🤝 Contribuir com código +- 💡 [Sugerir novas funcionalidades](https://github.com/LacusSolutions/br-utils-ruby/issues) +- 🐛 [Reportar bugs](https://github.com/LacusSolutions/br-utils-ruby/issues) + +## Licença + +Este projeto está sob a licença MIT — veja o arquivo [LICENSE](https://github.com/LacusSolutions/br-utils-ruby/blob/main/LICENSE). + +## Changelog + +Veja o [CHANGELOG](./CHANGELOG.md) para alterações e histórico de versões. + +--- + +Feito com ❤️ por [Lacus Solutions](https://github.com/LacusSolutions)