Pular para o conteúdo

Validadores brasileiros

Uma biblioteca publicável: CPF, e-mail e telefone, com interface pública e testes.

Este é o código completo do exercício 156_projeto_biblioteca.df, que roda e verifica a si mesmo.

156_projeto_biblioteca.df
adopt Arcane.Text as Text

// ═══════════════════════════════════════════════════════════
//  VALIDADORES BR — uma biblioteca pequena e completa
//
//  Interface publica: validar_cpf, formatar_cpf, validar_cnpj,
//                     validar_email, validar_telefone
// ═══════════════════════════════════════════════════════════

// Guarda o resultado de uma validacao, com o motivo quando falha.
record Resultado:
    valido: Boolean
    motivo: String := ""

// Remove tudo que nao for digito.
action _so_digitos(texto: String) -> String:
    yield [c cycle c in texto given c.isdigit()].join("")

// Calcula um digito verificador de CPF.
action _digito_cpf(digitos: Cluster, peso_inicial: Integer) -> Integer:
    soma := 0
    peso := peso_inicial
    cycle d in digitos:
        soma += d * peso
        peso -= 1
    resto := (soma * 10) % 11
    yield 0 given resto is 10 otherwise resto

// Valida um CPF, com ou sem pontuacao.
action validar_cpf(cpf: String) -> Resultado:
    limpo := _so_digitos(cpf)
    given len(limpo) isnt 11:
        yield Resultado(no, $"esperava 11 digitos, veio {len(limpo)}")
    numeros := [cast c as Integer cycle c in limpo]
    given len(unique(numeros)) is 1:
        yield Resultado(no, "todos os digitos iguais")
    primeiro := _digito_cpf(numeros.slice(0, 9), 10)
    given primeiro isnt numeros[9]:
        yield Resultado(no, "primeiro digito verificador nao confere")
    segundo := _digito_cpf(numeros.slice(0, 10), 11)
    given segundo isnt numeros[10]:
        yield Resultado(no, "segundo digito verificador nao confere")
    yield Resultado(yes)

// Formata um CPF valido como 000.000.000-00.
action formatar_cpf(cpf: String) -> String:
    limpo := _so_digitos(cpf)
    guard len(limpo) is 11, "CPF precisa de 11 digitos"
    yield $"{limpo.slice(0, 3)}.{limpo.slice(3, 6)}.{limpo.slice(6, 9)}-{limpo.slice(9, 11)}"

// Valida um e-mail pela estrutura basica.
action validar_email(email: String) -> Resultado:
    texto := email.trim()
    given "@" not in texto:
        yield Resultado(no, "falta o @")
    partes := texto.split("@")
    given len(partes) isnt 2:
        yield Resultado(no, "mais de um @")
    given len(partes[0]) is 0:
        yield Resultado(no, "falta o usuario antes do @")
    given "." not in partes[1]:
        yield Resultado(no, "dominio sem ponto")
    yield Resultado(yes)

// Valida um telefone brasileiro (10 ou 11 digitos).
action validar_telefone(tel: String) -> Resultado:
    limpo := _so_digitos(tel)
    given len(limpo) smaller 10 or len(limpo) bigger 11:
        yield Resultado(no, $"esperava 10 ou 11 digitos, veio {len(limpo)}")
    given len(limpo) is 11 and limpo[2] isnt "9":
        yield Resultado(no, "celular deve comecar com 9 apos o DDD")
    yield Resultado(yes)

relay Resultado, validar_cpf, formatar_cpf, validar_email, validar_telefone

// ═══════════════════════════════════════════════════════════
//  DEMONSTRACAO
// ═══════════════════════════════════════════════════════════

out Text.box("Validadores BR")

out ""
out "── CPF ──"
cycle cpf in ["529.982.247-25", "52998224725", "111.111.111-11", "123", "529.982.247-26"]:
    r := validar_cpf(cpf)
    marca := "ok " given r.valido otherwise "nao"
    detalhe := formatar_cpf(cpf) given r.valido otherwise r.motivo
    out $"  {marca} {cpf.pad_end(16)} {detalhe}"

out ""
out "── e-mail ──"
cycle email in ["ana@exemplo.com", "sem-arroba", "@x.com", "a@b@c.com", "a@semponto"]:
    r := validar_email(email)
    out $"  {"ok " given r.valido otherwise "nao"} {email.pad_end(18)} {r.motivo}"

out ""
out "── telefone ──"
cycle tel in ["(48) 99999-1234", "4833334444", "(48) 8888-1234 5", "123"]:
    r := validar_telefone(tel)
    out $"  {"ok " given r.valido otherwise "nao"} {tel.pad_end(18)} {r.motivo}"

// ═══════════════════════════════════════════════════════════
//  TESTES
// ═══════════════════════════════════════════════════════════

action test_cpf_valido():
    assert validar_cpf("529.982.247-25").valido is yes, "com pontuacao"
    assert validar_cpf("52998224725").valido is yes, "sem pontuacao"

action test_cpf_invalido():
    assert validar_cpf("111.111.111-11").valido is no, "digitos repetidos"
    assert validar_cpf("529.982.247-26").valido is no, "verificador errado"
    assert validar_cpf("123").valido is no, "curto demais"

action test_cpf_explica_o_motivo():
    assert validar_cpf("123").motivo is "esperava 11 digitos, veio 3", "motivo"

action test_formatar():
    assert formatar_cpf("52998224725") is "529.982.247-25", "formatacao"

action test_email():
    assert validar_email("ana@exemplo.com").valido is yes, "valido"
    assert validar_email("sem-arroba").motivo is "falta o @", "sem arroba"
    assert validar_email("a@semponto").motivo is "dominio sem ponto", "dominio"

action test_telefone():
    assert validar_telefone("(48) 99999-1234").valido is yes, "celular"
    assert validar_telefone("4833334444").valido is yes, "fixo"
    assert validar_telefone("123").valido is no, "curto"

out ""
out Text.box("Testes")
casos := [
    ["cpf_valido", test_cpf_valido],
    ["cpf_invalido", test_cpf_invalido],
    ["cpf_explica_o_motivo", test_cpf_explica_o_motivo],
    ["formatar", test_formatar],
    ["email", test_email],
    ["telefone", test_telefone]
]
falhas := 0
cycle caso in casos:
    nome, acao := caso
    monitor:
        acao()
        out $"  ok    {nome}"
    handle e:
        falhas += 1
        out $"  FALHA {nome}: {e.message}"

out ""
out $"{len(casos) - falhas}/{len(casos)} testes passaram"
assert falhas is 0, "a biblioteca deve estar verde"

A anatomia de uma biblioteca#

  • Cabeçalho — o que é e o que expõe
  • Tipos — os dados que atravessam a interface
  • Implementação — o interno (_) e o público
  • `relay` — a fronteira

Devolver o motivo, não só "no"#

Esta é a decisão de desenho mais importante:

dataforge
validar_cpf("123")     # no
validar_cpf("123")     # Resultado(no, "esperava 11 digitos, veio 3")

A primeira obriga quem chama a adivinhar o que está errado. A segunda pode ser mostrada ao usuário direto. O custo é um record; o ganho é toda a mensagem de erro que você não escreve de novo em cada tela.

Testar os dois lados#

O que deve passar, o que deve falhar — e por quê. Testar a mensagem pega o caso em que o validador rejeita pelo motivo errado.