diff --git a/README.md b/README.md index 7f0359f..d7a11a6 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ func main() { // Generate a mock address address, err := mocker.NewAddress() if err != nil { + // Error messages are localized where translation keys are available log.Fatal(err) } fmt.Printf("Address: %s, %d - %s, %s (%s) - %s\n", address.Street, address.Number, address.City, address.State, address.UF, address.ZIP) @@ -76,6 +77,7 @@ func main() { // Generate a mock person person, err := mocker.NewPerson() if err != nil { + // Error messages are localized where translation keys are available log.Fatal(err) } fmt.Printf("Person: %s %s, Gender: %s, Age: %d, CPF: %s\n", person.FirstNameMale, person.LastName, person.Gender.Identity, person.Age, person.CPF.Number) @@ -83,10 +85,19 @@ func main() { // Generate a mock company company, err := mocker.NewCompany() if err != nil { + // Error messages are localized where translation keys are available log.Fatal(err) } fmt.Printf("Company: %s, CNPJ: %s\n", company.BrazilianCompany.Name, company.BrazilianCompany.CNPJ) } + +``` + +### Error Messages & Localization + +Error messages are localized where translation keys are available. When an error occurs (e.g., invalid data, unsupported language, or generation failure), the error message will be presented in the language configured for the `Mocker` instance, if a translation exists. In some cases, fallback or hardcoded errors may occur if translation coverage is incomplete. + +You do not need to perform any extra steps for error localization — Mocai handles this automatically for all supported languages where translation keys are present. ``` > **Note:** Each call to a method like `NewPerson()` or `NewAddress()` generates a new mock with random data. The `Mocker` instance is immutable regarding its configuration (language, formatting, random source). diff --git a/docs/localization/pt/README-PT.md b/docs/localization/pt/README-PT.md index 6fd052d..f16022d 100644 --- a/docs/localization/pt/README-PT.md +++ b/docs/localization/pt/README-PT.md @@ -67,6 +67,7 @@ func main() { // Gera um endereço fictício address, err := mocker.NewAddress() if err != nil { + // As mensagens de erro são localizadas quando há chave de tradução disponível log.Fatal(err) } fmt.Printf("Endereço: %s, %d - %s, %s (%s) - %s\n", address.Street, address.Number, address.City, address.State, address.UF, address.ZIP) @@ -74,6 +75,7 @@ func main() { // Gera uma pessoa fictícia person, err := mocker.NewPerson() if err != nil { + // As mensagens de erro são localizadas quando há chave de tradução disponível log.Fatal(err) } fmt.Printf("Pessoa: %s %s, Gênero: %s, Idade: %d, CPF: %s\n", person.FirstNameMale, person.LastName, person.Gender.Identity, person.Age, person.CPF.Number) @@ -81,10 +83,19 @@ func main() { // Gera uma empresa fictícia company, err := mocker.NewCompany() if err != nil { + // As mensagens de erro são localizadas quando há chave de tradução disponível log.Fatal(err) } fmt.Printf("Empresa: %s, CNPJ: %s\n", company.BrazilianCompany.Name, company.BrazilianCompany.CNPJ) } + +``` + +### Mensagens de Erro e Localização + +As mensagens de erro são localizadas quando há chave de tradução disponível. Quando ocorre um erro (ex: dados inválidos, idioma não suportado ou falha na geração), a mensagem será apresentada no idioma configurado para a instância do `Mocker`, se houver tradução. Em alguns casos, mensagens fixas podem aparecer caso a cobertura de traduções não seja completa. + +Não é necessário realizar nenhuma etapa extra para a localização das mensagens de erro — o Mocai faz isso automaticamente para todos os idiomas suportados onde há chave de tradução. ``` > **Nota:** Cada chamada de método como `NewPerson()` ou `NewAddress()` gera um novo mock com dados aleatórios. A instância do `Mocker` é imutável quanto à configuração (idioma, formatação, fonte de aleatoriedade). diff --git a/pkg/mocai/entities/address/errors.go b/pkg/mocai/entities/address/errors.go index fee5523..d7851ab 100644 --- a/pkg/mocai/entities/address/errors.go +++ b/pkg/mocai/entities/address/errors.go @@ -2,13 +2,9 @@ package address import "errors" -// Package address defines common errors used during address generation and validation. -// These errors represent specific failure scenarios and should be wrapped with additional -// context when returned. var ( - ErrNoStreets = errors.New("no data available for streets") - ErrNoCities = errors.New("no data available for cities") - ErrNoStates = errors.New("no data available for states") - ErrNoUFs = errors.New("no data available for UFs") - ErrNoZips = errors.New("no data available for zips") + ErrNoStreets = errors.New("address: no streets available") + ErrNoCities = errors.New("address: no cities available") + ErrNoStates = errors.New("address: no states available") + ErrNoZips = errors.New("address: no zip codes available") ) diff --git a/pkg/mocai/entities/address/generator.go b/pkg/mocai/entities/address/generator.go index 6e64ab4..1ead929 100644 --- a/pkg/mocai/entities/address/generator.go +++ b/pkg/mocai/entities/address/generator.go @@ -32,17 +32,23 @@ func NewAddress(lang string, rnd translations.RandSource) (*Address, error) { } func generateAddress(lang string, rnd translations.RandSource) (*Address, error) { - streets := translations.GetList(lang, "address_street") - cities := translations.GetList(lang, "address_city") - states := translations.GetList(lang, "address_state") - ufs := translations.GetList(lang, "address_uf") - zips := translations.GetList(lang, "address_zip") + supportedLang := lang + if translations.GetUFMap(lang) == nil { + supportedLang = "ptbr" + } + if rnd == nil { + rnd = translations.NewSafeRandSource(translations.DefaultRandSource()) + } + + streets := translations.GetList(supportedLang, "address_street") + cities := translations.GetList(supportedLang, "address_city") + states := translations.GetList(supportedLang, "address_state") + zips := translations.GetList(supportedLang, "address_zip") slicesToCheck := []SlicesToCheck{ {streets, ErrNoStreets}, {cities, ErrNoCities}, {states, ErrNoStates}, - {ufs, ErrNoUFs}, {zips, ErrNoZips}, } @@ -57,11 +63,29 @@ func generateAddress(lang string, rnd translations.RandSource) (*Address, error) } } - // performs a random selection using a custom random source + // select the index for state and UF in a paired manner + stateIdx := rnd.Intn(len(states)) + state := states[stateIdx] + + var uf string + + ufMap := translations.GetUFMap(supportedLang) + if ufMap != nil { + uf = ufMap[state] + } + if uf == "" { + ufs := translations.GetList(supportedLang, "address_uf") + if len(ufs) > stateIdx { + uf = ufs[stateIdx] + } else if len(ufs) > 0 { + uf = ufs[rnd.Intn(len(ufs))] + } else { + uf = "" + } + } + street := streets[rnd.Intn(len(streets))] city := cities[rnd.Intn(len(cities))] - state := states[rnd.Intn(len(states))] - uf := ufs[rnd.Intn(len(ufs))] zip := zips[rnd.Intn(len(zips))] return &Address{ diff --git a/pkg/mocai/entities/certificate/countries/brazilian_certificates.go b/pkg/mocai/entities/certificate/countries/brazilian_certificates.go index ccc4fdc..5162047 100644 --- a/pkg/mocai/entities/certificate/countries/brazilian_certificates.go +++ b/pkg/mocai/entities/certificate/countries/brazilian_certificates.go @@ -2,7 +2,6 @@ package countries import ( "fmt" - "math/rand" "time" "github.com/brazzcore/mocai/pkg/mocai/translations" @@ -55,23 +54,23 @@ type BrazilianCertificates struct { } func NewBrazilCertificatesCustom(lang string, isFormatted bool, rnd translations.RandSource) (*BrazilianCertificates, error) { - cert, err := generateBrazilianCertificatesCustom(lang, isFormatted, rnd) + cert, err := generateBrazilianCertificates(lang, isFormatted, rnd) if err != nil { return nil, err } return cert, nil } -func generateBrazilianCertificatesCustom(lang string, formatted bool, rnd translations.RandSource) (*BrazilianCertificates, error) { - createdBirthCertificate, err := generateBirthCertificateCustom(lang, formatted, rnd) +func generateBrazilianCertificates(lang string, formatted bool, rnd translations.RandSource) (*BrazilianCertificates, error) { + createdBirthCertificate, err := generateBirthCertificate(lang, formatted, rnd) if err != nil { return nil, err } - createdMarriageCertificate, err := generateMarriageCertificateCustom(lang, formatted, rnd) + createdMarriageCertificate, err := generateMarriageCertificate(lang, formatted, rnd) if err != nil { return nil, err } - createdDeathCertificate, err := generateDeathCertificateCustom(lang, formatted, rnd) + createdDeathCertificate, err := generateDeathCertificate(lang, formatted, rnd) if err != nil { return nil, err } @@ -83,37 +82,37 @@ func generateBrazilianCertificatesCustom(lang string, formatted bool, rnd transl return createdBrazilianCertificates, nil } -func generateCertificateCustom(rnd translations.RandSource, formatted bool, certificateType int, lang ...string) (*BaseCertificate, error) { - l := "pt_br" +func generateCertificate(rnd translations.RandSource, formatted bool, certificateType int, lang ...string) (*BaseCertificate, error) { + l := "ptbr" if len(lang) > 0 && lang[0] != "" { l = lang[0] } if rnd == nil { - rnd = rand.New(rand.NewSource(int64(rand.Int()))) + rnd = translations.DefaultRandSource() } vitalRecordsOffice := rnd.Intn(899999-100000+1) + 100000 if vitalRecordsOffice < 0 { - return nil, fmt.Errorf("%w: %s", ErrInvalidVitalRecordsOffice, translations.Get(l, "invalid_vital_records_office_number")) + return nil, fmt.Errorf("%s", translations.Get(l, "invalid_vital_records_office_number")) } archiveCode := 1 serviceType := 55 birthYear := rnd.Intn(time.Now().Year()-2010+1) + 2010 bookNumber := rnd.Intn(89999-10000+1) + 10000 if bookNumber < 0 { - return nil, fmt.Errorf("%w, %s", ErrInvalidBookNumber, translations.Get(l, "invalid_book_number")) + return nil, fmt.Errorf("%s", translations.Get(l, "invalid_book_number")) } pageNumber := rnd.Intn(899-100+1) + 100 if pageNumber < 0 { - return nil, ErrInvalidPageNumber + return nil, fmt.Errorf("%s", translations.Get(l, "invalid_page_number")) } termNumber := rnd.Intn(8999999-1000000+1) + 1000000 if termNumber < 0 { - return nil, fmt.Errorf("%w, %s", ErrInvalidTermNumber, translations.Get(l, "invalid_term_number")) + return nil, fmt.Errorf("%s", translations.Get(l, "invalid_term_number")) } numberWithoutCheckDigits := fmt.Sprintf("%06d%02d%02d%04d%d%05d%03d%07d", vitalRecordsOffice, archiveCode, serviceType, birthYear, certificateType, bookNumber, pageNumber, termNumber) if len(numberWithoutCheckDigits) != 30 { - return nil, fmt.Errorf("%w, %s", ErrInvalidNumberWithoutCheckDigits, translations.Get(l, "invalid_number_without_check_digits")) + return nil, fmt.Errorf("%s", translations.Get(l, "invalid_number_without_check_digits")) } checkDigits := calculateCheckDigits(numberWithoutCheckDigits) certificateNumber := fmt.Sprintf("%s%02s", numberWithoutCheckDigits, checkDigits) @@ -122,7 +121,7 @@ func generateCertificateCustom(rnd translations.RandSource, formatted bool, cert vitalRecordsOffice, archiveCode, serviceType, birthYear, certificateType, bookNumber, pageNumber, termNumber, checkDigits) } if certificateNumber == "" { - return nil, fmt.Errorf("%w: %s", ErrInvalidCertificate, translations.Get(l, "invalid_certificate")) + return nil, fmt.Errorf("%s", translations.Get(l, "invalid_certificate")) } createdBaseCertificate := &BaseCertificate{ VitalRecordsOffice: vitalRecordsOffice, @@ -139,8 +138,8 @@ func generateCertificateCustom(rnd translations.RandSource, formatted bool, cert return createdBaseCertificate, nil } -func generateBirthCertificateCustom(lang string, formatted bool, rnd translations.RandSource) (*BirthCertificate, error) { - base, err := generateCertificateCustom(rnd, formatted, brazilianBirthCertificateType) +func generateBirthCertificate(lang string, formatted bool, rnd translations.RandSource) (*BirthCertificate, error) { + base, err := generateCertificate(rnd, formatted, brazilianBirthCertificateType, lang) if err != nil { return nil, err } @@ -150,8 +149,8 @@ func generateBirthCertificateCustom(lang string, formatted bool, rnd translation return createdBirthCertificate, nil } -func generateMarriageCertificateCustom(lang string, formatted bool, rnd translations.RandSource) (*MarriageCertificate, error) { - base, err := generateCertificateCustom(rnd, formatted, brazilianMarriageCertificateType) +func generateMarriageCertificate(lang string, formatted bool, rnd translations.RandSource) (*MarriageCertificate, error) { + base, err := generateCertificate(rnd, formatted, brazilianMarriageCertificateType, lang) if err != nil { return nil, err } @@ -161,8 +160,8 @@ func generateMarriageCertificateCustom(lang string, formatted bool, rnd translat return createdMarriageCertificate, nil } -func generateDeathCertificateCustom(lang string, formatted bool, rnd translations.RandSource) (*DeathCertificate, error) { - base, err := generateCertificateCustom(rnd, formatted, brazilianDeathCertificateType) +func generateDeathCertificate(lang string, formatted bool, rnd translations.RandSource) (*DeathCertificate, error) { + base, err := generateCertificate(rnd, formatted, brazilianDeathCertificateType, lang) if err != nil { return nil, err } diff --git a/pkg/mocai/entities/certificate/countries/errors.go b/pkg/mocai/entities/certificate/countries/errors.go deleted file mode 100644 index 0ba2f1c..0000000 --- a/pkg/mocai/entities/certificate/countries/errors.go +++ /dev/null @@ -1,18 +0,0 @@ -package countries - -import "errors" - -// Errors for Certificate data generation failures. -// These errors represent specific failure scenarios and should be wrapped with additional -// context when returned. -var ( - ErrInvalidCertificate = errors.New("certificate: invalid certificate") - ErrInvalidVitalRecordsOffice = errors.New("certificate: invalid vital records office number") - ErrInvalidArchive = errors.New("certificate: invalid archive number") - ErrInvalidVitalRecordsService = errors.New("certificate: invalid vital records service number") - ErrInvalidBirthYear = errors.New("certificate: invalid birth year") - ErrInvalidBookNumber = errors.New("certificate: invalid book number") - ErrInvalidPageNumber = errors.New("certificate: invalid page number") - ErrInvalidTermNumber = errors.New("certificate: invalid term number") - ErrInvalidNumberWithoutCheckDigits = errors.New("certificate: invalid number without check digits") -) diff --git a/pkg/mocai/entities/cnpj/errors.go b/pkg/mocai/entities/cnpj/errors.go deleted file mode 100644 index 658cda0..0000000 --- a/pkg/mocai/entities/cnpj/errors.go +++ /dev/null @@ -1,7 +0,0 @@ -package cnpj - -import "errors" - -var ( - ErrInvalidCNPJ = errors.New("cnpj: invalid cnpj") -) diff --git a/pkg/mocai/entities/company/countries/brazilian_company.go b/pkg/mocai/entities/company/countries/brazilian_company.go index a1ba120..eef1a99 100644 --- a/pkg/mocai/entities/company/countries/brazilian_company.go +++ b/pkg/mocai/entities/company/countries/brazilian_company.go @@ -2,7 +2,6 @@ package countries import ( "fmt" - "math/rand" "github.com/brazzcore/mocai/pkg/mocai/entities/cnpj" "github.com/brazzcore/mocai/pkg/mocai/translations" @@ -18,10 +17,10 @@ type BrazilianCompany struct { func GenerateBrazilianCompany(lang string, formatted bool, rnd translations.RandSource) (BrazilianCompany, error) { companyNames := translations.GetList(lang, "company_name") if len(companyNames) == 0 { - return BrazilianCompany{}, fmt.Errorf("%w, %s", ErrNoCompanyNamesAvailable, translations.Get(lang, "no_company_names_available")) + return BrazilianCompany{}, fmt.Errorf("%s", translations.Get(lang, "no_company_names_available")) } if rnd == nil { - rnd = defaultRandSource() + rnd = translations.DefaultRandSource() } companyName := companyNames[rnd.Intn(len(companyNames))] @@ -30,10 +29,10 @@ func GenerateBrazilianCompany(lang string, formatted bool, rnd translations.Rand return BrazilianCompany{}, err } if companyName == "" { - return BrazilianCompany{}, fmt.Errorf("%w, %s", ErrGeneratingBrazilianCompany, translations.Get(lang, "error_generating_brazilian_company")) + return BrazilianCompany{}, fmt.Errorf("%s", translations.Get(lang, "error_generating_brazilian_company")) } if cnpjVal == "" { - return BrazilianCompany{}, fmt.Errorf("%w, %s", ErrGeneratingCNPJ, translations.Get(lang, "invalid_cnpj")) + return BrazilianCompany{}, fmt.Errorf("%s", translations.Get(lang, "invalid_cnpj")) } createdCompany := BrazilianCompany{ Name: companyName, @@ -41,7 +40,3 @@ func GenerateBrazilianCompany(lang string, formatted bool, rnd translations.Rand } return createdCompany, nil } - -func defaultRandSource() translations.RandSource { - return rand.New(rand.NewSource(int64(rand.Int()))) -} diff --git a/pkg/mocai/entities/company/countries/errors.go b/pkg/mocai/entities/company/countries/errors.go deleted file mode 100644 index 770626f..0000000 --- a/pkg/mocai/entities/company/countries/errors.go +++ /dev/null @@ -1,12 +0,0 @@ -package countries - -import "errors" - -// Errors for Company data generation failures. -// These errors represent specific failure scenarios and should be wrapped with additional -// context when returned. -var ( - ErrGeneratingBrazilianCompany = errors.New("company: error generating brazilian company") - ErrGeneratingCNPJ = errors.New("company: error generating CNPJ") - ErrNoCompanyNamesAvailable = errors.New("company: no company names available") -) diff --git a/pkg/mocai/entities/cpf/errors.go b/pkg/mocai/entities/cpf/errors.go deleted file mode 100644 index cd3918a..0000000 --- a/pkg/mocai/entities/cpf/errors.go +++ /dev/null @@ -1,7 +0,0 @@ -package cpf - -import "errors" - -var ( - ErrInvalidCPF = errors.New("cpf: invalid cpf") -) diff --git a/pkg/mocai/entities/cpf/generator.go b/pkg/mocai/entities/cpf/generator.go index a71b850..78e8f07 100644 --- a/pkg/mocai/entities/cpf/generator.go +++ b/pkg/mocai/entities/cpf/generator.go @@ -2,9 +2,7 @@ package cpf import ( "fmt" - "math/rand" "strings" - "time" "github.com/brazzcore/mocai/pkg/mocai/translations" ) @@ -16,7 +14,7 @@ type CPF struct { // NewCPF generates a mock CPF using a custom language and random source func NewCPF(lang string, isFormatted bool, rnd translations.RandSource) (*CPF, error) { if rnd == nil { - rnd = rand.New(rand.NewSource(time.Now().UnixNano())) + rnd = translations.DefaultRandSource() } // generate the first 9 digits @@ -35,7 +33,7 @@ func NewCPF(lang string, isFormatted bool, rnd translations.RandSource) (*CPF, e cpfNumber := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(digits)), ""), "[]") if isFormatted { if len(cpfNumber) != 11 { - return nil, fmt.Errorf("%w, %s", ErrInvalidCPF, translations.Get(lang, "invalid_cpf")) + return nil, fmt.Errorf("%s", translations.Get(lang, "invalid_cpf")) } return &CPF{Number: cpfNumber[:3] + "." + cpfNumber[3:6] + "." + cpfNumber[6:9] + "-" + cpfNumber[9:]}, nil } diff --git a/pkg/mocai/entities/gender/errors.go b/pkg/mocai/entities/gender/errors.go deleted file mode 100644 index eb0e2c0..0000000 --- a/pkg/mocai/entities/gender/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package gender - -import "errors" - -// Package gender defines common errors used during gender generation and validation. -// These errors represent specific failure scenarios and should be wrapped with additional -// context when returned. -var ( - ErrNoGenders = errors.New("gender: no data available for genders") - ErrInvalidGender = errors.New("gender: invalid gender") -) diff --git a/pkg/mocai/entities/gender/gender.go b/pkg/mocai/entities/gender/gender.go index 281ad86..cf3527f 100644 --- a/pkg/mocai/entities/gender/gender.go +++ b/pkg/mocai/entities/gender/gender.go @@ -2,7 +2,6 @@ package gender import ( "fmt" - "math/rand" "github.com/brazzcore/mocai/pkg/mocai/translations" ) @@ -34,15 +33,11 @@ func NewGender(lang string, rnd translations.RandSource) (*Gender, error) { func generateRandomGender(lang string, rnd translations.RandSource) (*Gender, error) { genders := translations.GetList(lang, "gender") if len(genders) == 0 { - return nil, fmt.Errorf("%w, %s", ErrNoGenders, translations.Get(lang, "no_data_available_for_genders")) + return nil, fmt.Errorf("%s", translations.Get(lang, "no_data_available_for_genders")) } if rnd == nil { - rnd = defaultRandSource() + rnd = translations.DefaultRandSource() } genderStr := genders[rnd.Intn(len(genders))] return &Gender{Identity: Identity(genderStr)}, nil } - -func defaultRandSource() translations.RandSource { - return rand.New(rand.NewSource(int64(rand.Int()))) -} diff --git a/pkg/mocai/entities/nationalid/countries/brazilian_national_id.go b/pkg/mocai/entities/nationalid/countries/brazilian_national_id.go index cd08a85..3d07fe7 100644 --- a/pkg/mocai/entities/nationalid/countries/brazilian_national_id.go +++ b/pkg/mocai/entities/nationalid/countries/brazilian_national_id.go @@ -2,7 +2,6 @@ package countries import ( "fmt" - "math/rand" "strconv" "github.com/brazzcore/mocai/pkg/mocai/translations" @@ -46,7 +45,7 @@ func generateBrazilianNationalIDCustom(formatted bool, rnd translations.RandSour func calculateSPRGDigitCustom(rnd translations.RandSource, lang string) (string, error) { if rnd == nil { - rnd = rand.New(rand.NewSource(int64(rand.Int()))) + rnd = translations.DefaultRandSource() } base := rnd.Intn(100000000) baseStr := fmt.Sprintf("%08d", base) @@ -54,7 +53,7 @@ func calculateSPRGDigitCustom(rnd translations.RandSource, lang string) (string, for i := range 8 { val, err := strconv.Atoi(string(baseStr[i])) if err != nil { - return "", fmt.Errorf("%w, %s", ErrToConvertDigit, translations.Get(lang, "error_converting_digit")) + return "", fmt.Errorf("%s", translations.Get(lang, "error_converting_digit")) } d[i] = val } diff --git a/pkg/mocai/entities/nationalid/countries/errors.go b/pkg/mocai/entities/nationalid/countries/errors.go deleted file mode 100644 index 8b01c99..0000000 --- a/pkg/mocai/entities/nationalid/countries/errors.go +++ /dev/null @@ -1,7 +0,0 @@ -package countries - -import "errors" - -var ( - ErrToConvertDigit = errors.New("national id: error converting digit") -) diff --git a/pkg/mocai/entities/person/errors.go b/pkg/mocai/entities/person/errors.go deleted file mode 100644 index 4349ad4..0000000 --- a/pkg/mocai/entities/person/errors.go +++ /dev/null @@ -1,12 +0,0 @@ -package person - -import "errors" - -// Error constants for person-related data generation failures. -// These errors represent specific failure scenarios and should be wrapped with additional -// context when returned. -var ( - ErrGeneratingPerson = errors.New("person: error generating person") - ErrNoFirstNames = errors.New("person: no data available for first names") - ErrNoLastNames = errors.New("person: no data available for last names") -) diff --git a/pkg/mocai/entities/person/generator.go b/pkg/mocai/entities/person/generator.go index a9da752..292b8d1 100644 --- a/pkg/mocai/entities/person/generator.go +++ b/pkg/mocai/entities/person/generator.go @@ -2,7 +2,6 @@ package person import ( "fmt" - "math/rand" "github.com/brazzcore/mocai/pkg/mocai/entities/cpf" "github.com/brazzcore/mocai/pkg/mocai/entities/gender" @@ -31,14 +30,14 @@ func generatePerson(lang string, isFormatted bool, rnd translations.RandSource) lastNames := translations.GetList(lang, "person_last_name") if len(firstNamesMale) == 0 || len(firstNamesFemale) == 0 { - return nil, fmt.Errorf("%w, %s", ErrNoFirstNames, translations.Get(lang, "no_data_available_for_first_names")) + return nil, fmt.Errorf("%s", translations.Get(lang, "no_data_available_for_first_names")) } if len(lastNames) == 0 { - return nil, fmt.Errorf("%w, %s", ErrNoLastNames, translations.Get(lang, "no_data_available_for_last_names")) + return nil, fmt.Errorf("%s", translations.Get(lang, "no_data_available_for_last_names")) } if rnd == nil { - rnd = rand.New(rand.NewSource(int64(rand.Int()))) + rnd = translations.DefaultRandSource() } firstNameMale := firstNamesMale[rnd.Intn(len(firstNamesMale))] @@ -56,7 +55,7 @@ func generatePerson(lang string, isFormatted bool, rnd translations.RandSource) } if firstNameMale == "" || firstNameFemale == "" || lastName == "" { - return nil, fmt.Errorf("%w, %s", ErrGeneratingPerson, translations.Get(lang, "error_generating_person")) + return nil, fmt.Errorf("%s", translations.Get(lang, "error_generating_person")) } return &Person{ diff --git a/pkg/mocai/entities/phone/errors.go b/pkg/mocai/entities/phone/errors.go deleted file mode 100644 index c2d1967..0000000 --- a/pkg/mocai/entities/phone/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package phone - -import "errors" - -// Error constants used for phone number generation failures. -// These errors represent specific failure scenarios and should be wrapped with additional -// context when returned. -var ( - ErrGeneratingPhone = errors.New("phone: error generating phone") - ErrNoAreaCodes = errors.New("phone: no data available for area codes") -) diff --git a/pkg/mocai/entities/phone/generator.go b/pkg/mocai/entities/phone/generator.go index b55be32..d4be88e 100644 --- a/pkg/mocai/entities/phone/generator.go +++ b/pkg/mocai/entities/phone/generator.go @@ -2,7 +2,6 @@ package phone import ( "fmt" - "math/rand" "github.com/brazzcore/mocai/pkg/mocai/translations" ) @@ -21,22 +20,19 @@ func NewPhone(lang string, rnd translations.RandSource) (*Phone, error) { func generatePhone(lang string, rnd translations.RandSource) (*Phone, error) { areaCodes := translations.GetList(lang, "phone_area_code") if len(areaCodes) == 0 { - return nil, fmt.Errorf("%w, %s", ErrNoAreaCodes, translations.Get(lang, "no_data_available_for_area_codes")) + return nil, fmt.Errorf("%s", translations.Get(lang, "no_data_available_for_area_codes")) } if rnd == nil { - rnd = defaultRandSource() + rnd = translations.DefaultRandSource() } + areaCode := areaCodes[rnd.Intn(len(areaCodes))] number := fmt.Sprintf("9%08d", rnd.Intn(100000000)) - if areaCode == "" || number == "" { - return nil, fmt.Errorf("%w, %s", ErrGeneratingPhone, translations.Get(lang, "error_generating_phone")) + if areaCode == "" { + return nil, fmt.Errorf("%s", translations.Get(lang, "error_generating_phone")) } return &Phone{ AreaCode: areaCode, Number: number, }, nil } - -func defaultRandSource() translations.RandSource { - return rand.New(rand.NewSource(int64(rand.Int()))) -} diff --git a/pkg/mocai/entities/voteregistration/countries/brazilian_vote_registration.go b/pkg/mocai/entities/voteregistration/countries/brazilian_vote_registration.go index 6e157aa..dcd507b 100644 --- a/pkg/mocai/entities/voteregistration/countries/brazilian_vote_registration.go +++ b/pkg/mocai/entities/voteregistration/countries/brazilian_vote_registration.go @@ -2,17 +2,11 @@ package countries import ( "fmt" - "math/rand" "strconv" - "time" "github.com/brazzcore/mocai/pkg/mocai/translations" ) -var ( - globalRand = rand.New(rand.NewSource(time.Now().UnixNano())) -) - // BrazilianVoteRegistration represents a Brazilian vote registration type BrazilianVoteRegistration struct { Section string @@ -20,76 +14,20 @@ type BrazilianVoteRegistration struct { Number string } -// NewBrazilianVoteRegistration generates a new Brazilian vote registration +// NewBrazilianVoteRegistration generates a new Brazilian vote registration with localized errors func NewBrazilianVoteRegistration(isFormatted bool) (*BrazilianVoteRegistration, error) { - bvr, err := (&BrazilianVoteRegistration{}).generateBrazilianVoteRegistration(isFormatted) - if err != nil { - return nil, err - } - return bvr, nil -} - -// GenerateBrazilianVoteRegistration generates a valid Brazilian vote registration number -func (b *BrazilianVoteRegistration) generateBrazilianVoteRegistration(formatted bool) (*BrazilianVoteRegistration, error) { - section := randomInt3Digits() - zone := randomInt3Digits() - - // Generate an 8 digit sequence number - sequenceNumber := randomInt(1, 99999999) - sequenceNumberStr := fmt.Sprintf("%08d", sequenceNumber) - - // Generate a random state code 01 to 28 - stateCode := randomInt(1, 28) - stateCodeStr := fmt.Sprintf("%02d", stateCode) - - // Calculate the first check digit - checkDigit1, err := calculateCheckDigit1(sequenceNumberStr) - if err != nil { - return nil, err - } - - // Calculate the second check digit - checkDigit2, err := calculateCheckDigit2(stateCodeStr, checkDigit1, stateCode) - if err != nil { - return nil, err - } - - // Combine everything to form the complete number - number := sequenceNumberStr + stateCodeStr + checkDigit1 + checkDigit2 - if number == "" || len(number) != 12 { - return nil, ErrInvalidVoteRegistration - } - - if formatted { - number = fmt.Sprintf("%s %s %s", number[:4], number[4:8], number[8:]) - } - - return &BrazilianVoteRegistration{ - Section: section, - Zone: zone, - Number: number, - }, nil -} - -// randomInt generates a random integer between min and max -func randomInt(min, max int) int { - return globalRand.Intn(max-min+1) + min -} - -// randomInt3Digits generates a random 3 digit number -func randomInt3Digits() string { - return fmt.Sprintf("%03d", globalRand.Intn(1000)) + return NewBrazilianVoteRegistrationCustom("ptbr", isFormatted, nil) } // calculateCheckDigit1 calculates the first check digit. -// It depends on the sequence number. +// it depends on the sequence number. func calculateCheckDigit1(sequenceNumber string) (string, error) { sum := 0 weights := []int{2, 3, 4, 5, 6, 7, 8, 9} for i := 0; i < len(sequenceNumber); i++ { digit, err := strconv.Atoi(string(sequenceNumber[i])) if err != nil { - return "", ErrInvalidCheckDigit1 + return "", fmt.Errorf("%s", translations.Get("en_us", "invalid_check_digit_1")) } sum += digit * weights[i] } @@ -103,20 +41,20 @@ func calculateCheckDigit1(sequenceNumber string) (string, error) { } // calculateCheckDigit2 calculates the second check digit -// It depends on the state code and the first check digit +// it depends on the state code and the first check digit func calculateCheckDigit2(stateCode, checkDigit1 string, stateCodeInt int) (string, error) { sum := 0 weights := []int{7, 8} for i := 0; i < len(stateCode); i++ { digit, err := strconv.Atoi(string(stateCode[i])) if err != nil { - return "", ErrInvalidCheckDigit2 + return "", fmt.Errorf("%s", translations.Get("en_us", "invalid_check_digit_2")) } sum += digit * weights[i] } checkDigit1Int, err := strconv.Atoi(checkDigit1) if err != nil { - return "", ErrInvalidCheckDigit2 + return "", fmt.Errorf("%s", translations.Get("en_us", "invalid_check_digit_2")) } sum += checkDigit1Int * 9 @@ -125,7 +63,7 @@ func calculateCheckDigit2(stateCode, checkDigit1 string, stateCodeInt int) (stri checkDigit2 = 0 } - // Special case for states 01 SP and 02 MG + // special case for states 01 SP and 02 MG if (stateCodeInt == 1 || stateCodeInt == 2) && checkDigit2 == 0 { checkDigit2 = 1 } @@ -134,30 +72,56 @@ func calculateCheckDigit2(stateCode, checkDigit1 string, stateCodeInt int) (stri } func NewBrazilianVoteRegistrationCustom(lang string, isFormatted bool, rnd translations.RandSource) (*BrazilianVoteRegistration, error) { + if lang == "" { + lang = "en_us" + } + + supported := false + if translations.Get(lang, "invalid_vote_registration") != "invalid_vote_registration" { + supported = true + } + if !supported { + lang = "en_us" + } + if rnd == nil { - rnd = rand.New(rand.NewSource(time.Now().UnixNano())) + rnd = translations.DefaultRandSource() } + + // generate a random 3-digit section and zone section := fmt.Sprintf("%03d", rnd.Intn(1000)) zone := fmt.Sprintf("%03d", rnd.Intn(1000)) + + // generate an 8-digit sequence number sequenceNumber := rnd.Intn(99999999) + 1 sequenceNumberStr := fmt.Sprintf("%08d", sequenceNumber) + + // generate a random state code 01 to 28 stateCode := rnd.Intn(28) + 1 stateCodeStr := fmt.Sprintf("%02d", stateCode) + + // calculate the first check digit checkDigit1, err := calculateCheckDigit1(sequenceNumberStr) if err != nil { - return nil, fmt.Errorf("%w, %s", ErrInvalidCheckDigit1, translations.Get(lang, "invalid_check_digit_1")) + return nil, fmt.Errorf("%s", translations.Get(lang, "invalid_check_digit_1")) } + + // calculate the second check digit checkDigit2, err := calculateCheckDigit2(stateCodeStr, checkDigit1, stateCode) if err != nil { - return nil, fmt.Errorf("%w, %s", ErrInvalidCheckDigit2, translations.Get(lang, "invalid_check_digit_2")) + return nil, fmt.Errorf("%s", translations.Get(lang, "invalid_check_digit_2")) } + + // combine everything to form the complete number number := sequenceNumberStr + stateCodeStr + checkDigit1 + checkDigit2 if number == "" || len(number) != 12 { - return nil, fmt.Errorf("%w, %s", ErrInvalidVoteRegistration, translations.Get(lang, "invalid_vote_registration")) + return nil, fmt.Errorf("%s", translations.Get(lang, "invalid_vote_registration")) } + if isFormatted { number = fmt.Sprintf("%s %s %s", number[:4], number[4:8], number[8:]) } + return &BrazilianVoteRegistration{ Section: section, Zone: zone, diff --git a/pkg/mocai/entities/voteregistration/countries/errors.go b/pkg/mocai/entities/voteregistration/countries/errors.go deleted file mode 100644 index 4661fe5..0000000 --- a/pkg/mocai/entities/voteregistration/countries/errors.go +++ /dev/null @@ -1,12 +0,0 @@ -package countries - -import "errors" - -// Errors for Vote Registration data generation failures. -// These errors represent specific failure scenarios and should be wrapped with additional -// context when returned. -var ( - ErrInvalidVoteRegistration = errors.New("invalid vote registration") - ErrInvalidCheckDigit1 = errors.New("invalid check digit 1") - ErrInvalidCheckDigit2 = errors.New("invalid check digit 2") -) diff --git a/pkg/mocai/mocker.go b/pkg/mocai/mocker.go index 6ee10e3..1180a5c 100644 --- a/pkg/mocai/mocker.go +++ b/pkg/mocai/mocker.go @@ -1,9 +1,6 @@ package mocai import ( - "math/rand" - "time" - "github.com/brazzcore/mocai/pkg/mocai/entities/address" "github.com/brazzcore/mocai/pkg/mocai/entities/certificate" "github.com/brazzcore/mocai/pkg/mocai/entities/company" @@ -11,6 +8,7 @@ import ( "github.com/brazzcore/mocai/pkg/mocai/entities/gender" "github.com/brazzcore/mocai/pkg/mocai/entities/nationalid" "github.com/brazzcore/mocai/pkg/mocai/entities/person" + "github.com/brazzcore/mocai/pkg/mocai/entities/phone" "github.com/brazzcore/mocai/pkg/mocai/entities/voteregistration" "github.com/brazzcore/mocai/pkg/mocai/translations" ) @@ -22,15 +20,10 @@ type Mocker struct { rnd translations.RandSource } -func defaultRandSource() translations.RandSource { - return rand.New(rand.NewSource(time.Now().UnixNano())) -} - // NewMocker creates a new Mocker instance with customizable language, formatting, and random source func NewMocker(lang string, isFormatted bool, rnd translations.RandSource) *Mocker { if rnd == nil { - // fallback to rand.New(rand.NewSource(time.Now().UnixNano())) - rnd = defaultRandSource() + rnd = translations.DefaultRandSource() } return &Mocker{ lang: lang, @@ -79,12 +72,12 @@ func (m *Mocker) NewCertificate() (*certificate.Certificate, error) { return certificate.NewCertificate(m.lang, m.formatted, m.rnd) } +// NewPhone generates a mock phone using a custom language and random source +func (m *Mocker) NewPhone() (*phone.Phone, error) { + return phone.NewPhone(m.lang, m.rnd) +} + // GetLanguage returns the language used in this instance func (m *Mocker) GetLanguage() string { return m.lang } - -// GetRand returns the randomness source used -func (m *Mocker) GetRand() translations.RandSource { - return m.rnd -} diff --git a/pkg/mocai/translations/en_us.go b/pkg/mocai/translations/en_us.go new file mode 100644 index 0000000..c37d38d --- /dev/null +++ b/pkg/mocai/translations/en_us.go @@ -0,0 +1,44 @@ +package translations + +func init() { + Register("en_us", map[string]string{ + // certificate errors + "invalid_certificate": "invalid certificate", + "invalid_vital_records_office_number": "invalid vital records office number", + "invalid_archive_number": "invalid archive number", + "invalid_vital_records_service number": "invalid vital records service number", + "invalid_birth_year": "invalid birth year", + "invalid_book_number": "invalid book number", + "invalid_page_number": "invalid page number", + "invalid_term_number": "invalid term number", + "invalid_number_without_check_digits": "invalid number without check digits", + + // company errors + "error_generating_brazilian_company": "error generating brazilian company", + "invalid_cnpj": "invalid cnpj", + "no_company_names_available": "no company names available", + + // cpf errors + "invalid_cpf": "invalid cpf", + + // gender errors + "no_data_available_for_genders": "no data available for this gender", + + // national ID errors + "error_converting_digit": "error converting digit", + + // person errors + "error_generating_person": "error generating person", + "no_data_available_for_first_names": "no data available for first name", + "no_data_available_for_last_names": "no data available for last name", + + // phone errors + "error_generating_phone": "error generating phone number", + "no_data_available_for_area_codes": "no area code available", + + // voter Registration errors + "invalid_vote_registration": "invalid vote registration", + "invalid_check_digit_1": "invalid check digit 1", + "invalid_check_digit_2": "invalid check digit 2", + }) +} diff --git a/pkg/mocai/translations/errors.go b/pkg/mocai/translations/errors.go deleted file mode 100644 index 0cd6b47..0000000 --- a/pkg/mocai/translations/errors.go +++ /dev/null @@ -1,15 +0,0 @@ -// Package translations provides standardized error messages and translations -// used across the mocai package. These constants ensure consistency and reusability -// throughout the application, especially in error handling and mock data generation. -// -// Example usage: -// -// if !isSupported(lang) { -// return nil, translations.ErrUnsupportedLanguage -// } -package translations - -import "errors" - -// ErrUnsupportedLanguage indicates that the requested language is not supported. -var ErrUnsupportedLanguage = errors.New("unsupported language") diff --git a/pkg/mocai/translations/pt_br.go b/pkg/mocai/translations/pt_br.go index 2ab2466..1be3f77 100644 --- a/pkg/mocai/translations/pt_br.go +++ b/pkg/mocai/translations/pt_br.go @@ -40,8 +40,10 @@ func init() { } return ufs }(), - "address_zip": address_mocks.ZIPCodes, + "address_zip": address_mocks.ZIPCodes, + "phone_area_code": phone_mocks.AreaCodes, }) + // choose a random state state := address_mocks.States[rand.Intn(len(address_mocks.States))] @@ -60,7 +62,6 @@ func init() { "address_state": state, "address_uf": uf, "address_zip": address_mocks.ZIPCodes[rand.Intn(len(address_mocks.ZIPCodes))], - "phone_area_code": phone_mocks.AreaCodes[rand.Intn(len(phone_mocks.AreaCodes))], "company_name": company_mocks.CompanyNames[rand.Intn(len(company_mocks.CompanyNames))], // errors diff --git a/pkg/mocai/translations/safe_rand.go b/pkg/mocai/translations/safe_rand.go new file mode 100644 index 0000000..1b16369 --- /dev/null +++ b/pkg/mocai/translations/safe_rand.go @@ -0,0 +1,28 @@ +package translations + +import ( + "math/rand" + "sync" + "time" +) + +// DefaultRandSource returns a standard thread-safe randomness source for global use +func DefaultRandSource() RandSource { + return NewSafeRandSource(rand.New(rand.NewSource(time.Now().UnixNano()))) +} + +// SafeRandSource wraps a RandSource with a mutex for concurrent safety +type SafeRandSource struct { + rnd RandSource + mu sync.Mutex +} + +func NewSafeRandSource(rnd RandSource) *SafeRandSource { + return &SafeRandSource{rnd: rnd} +} + +func (s *SafeRandSource) Intn(n int) int { + s.mu.Lock() + defer s.mu.Unlock() + return s.rnd.Intn(n) +} diff --git a/pkg/mocai/translations/translations.go b/pkg/mocai/translations/translations.go index 7f6f8b3..584aeef 100644 --- a/pkg/mocai/translations/translations.go +++ b/pkg/mocai/translations/translations.go @@ -2,8 +2,23 @@ package translations import ( "sync" + + address_ptbr "github.com/brazzcore/mocai/pkg/mocai/entities/address/mocks/ptbr" ) +// GetUFMap Returns the mapping of states for the specified language, if available +func GetUFMap(lang string) map[string]string { + if lang == "ptbr" { + // returns a defensive copy to avoid exposing the global map + copyMap := make(map[string]string, len(address_ptbr.UFs)) + for k, v := range address_ptbr.UFs { + copyMap[k] = v + } + return copyMap + } + return nil +} + // registryList stores lists of translations by language and keyword var ( registryList = make(map[string]map[string][]string) @@ -55,6 +70,13 @@ func Get(lang, key string) string { if val, ok := registrySingle[lang][key]; ok { return val } + + // fallback: try en_us if not found in requested language + if lang != "en_us" { + if val, ok := registrySingle["en_us"][key]; ok { + return val + } + } return key }