Pular para o conteúdo

Reflexão de tipos

Arcane.Tipos: os metadados de um type declarado, conferir sem levantar, a forma estrutural de um valor e os campos de um record com o tipo de cada um.

typeof responde o nome do tipo de um valor. Quem escreve validação genérica, serialização, formulário ou tabela a partir de um record precisa de mais: os metadados do tipo declarado, e uma conferência que responda em vez de levantar.

Os metadados de um `type`#

dataforge
adopt Arcane.Tipos as Tipos

type Id := Integer
type Json := String | Integer
type Positivo := Integer where valor bigger 0
opaque type Cpf := String where len(valor) is 11

assert Tipos.de("Id")["especie"] is "alias"
assert Tipos.de("Json")["especie"] is "uniao"
assert Tipos.de("Json")["partes"] is ["String", "Integer"]
assert Tipos.de("Positivo")["especie"] is "refinamento"
assert Tipos.de("Positivo")["regra"] is "valor bigger 0"
assert Tipos.de("Cpf")["opaco"]
assert Tipos.existe("Id") and not Tipos.existe("NaoExiste")
assert len(Tipos.declarados()) is 4

Conferir sem levantar#

satisfaz responde yes/no. A base vem antes da regra: perguntar len(valor) a um número daria um erro do interpretador em vez de um no honesto.

dataforge
adopt Arcane.Tipos as Tipos

type Positivo := Integer where valor bigger 0
opaque type Cpf := String where len(valor) is 11

assert Tipos.satisfaz(5, "Positivo")
assert not Tipos.satisfaz(-5, "Positivo")
assert not Tipos.satisfaz("texto", "Positivo")      // a base decide primeiro
assert Tipos.satisfaz(Cpf("12345678901"), "Cpf")
assert not Tipos.satisfaz("12345678901", "Cpf")     // nominal

// e quando você QUER o erro de sempre:
monitor:
    Tipos.conferir(-1, "Positivo")
    assert no
handle TypeError as e:
    assert "Positivo" in e.message

A forma estrutural de um valor#

typeof([1, 2]) responde Cluster — o nome do tipo. Tipos.forma([1, 2]) responde Cluster<Integer> — a forma. Uma coleção de tipos misturados responde Cluster<Any>: dizer o tipo do primeiro item seria mentira.

dataforge
adopt Arcane.Tipos as Tipos

assert typeof([1, 2]) is "Cluster"
assert Tipos.forma([1, 2]) is "Cluster<Integer>"
assert Tipos.forma([1, "a"]) is "Cluster<Any>"
assert Tipos.forma((1, "a")) is "Tuple<Integer, String>"
assert Tipos.forma({"a": 1}) is "Vault<String, Integer>"
assert Tipos.forma([[1], [2]]) is "Cluster<Cluster<Integer>>"

assert Tipos.e_colecao((1, 2)) and not Tipos.e_colecao(3)
assert Tipos.e_imutavel((1, 2)) and not Tipos.e_imutavel([1, 2])

Os campos, com o tipo de cada um#

É o que transforma um tipo em formulário, tabela ou esquema sem escrever a lista de campos duas vezes.

dataforge
adopt Arcane.Tipos as Tipos

record Cliente:
    nome: String
    idade: Integer

blueprint Conta:
    saldo := 0.0

c := Cliente("Ana", 30)
campos := Tipos.campos(c)

assert campos["nome"]["tipo"] is "String"
assert campos["idade"]["valor"] is 30
assert Tipos.campos(spawn Conta())["saldo"]["tipo"] is "Float"
assert Tipos.campos({"a": 1})["a"]["tipo"] is "Integer" 

Um validador genérico, em oito linhas#

Juntando as duas peças: os campos vêm da reflexão, a regra vem do tipo declarado, e o relato vem do Resultado.

dataforge
adopt Arcane.Tipos as Tipos
adopt Arcane.Resultado as R

type Positivo := Integer where valor bigger 0
type Email := String where "@" in valor

record Pedido:
    quantidade: Positivo
    contato: Email

action validar(vault, esperado) -> Resultado:
    problemas := []
    cycle campo in esperado:
        given not Tipos.satisfaz(vault[campo] ?? void, esperado[campo]):
            problemas.append($"{campo} não é {esperado[campo]}")
    yield R.falha(problemas) given len(problemas) bigger 0 otherwise R.ok(vault)

esperado := {"quantidade": "Positivo", "contato": "Email"}

assert validar({"quantidade": 2, "contato": "ana@x.com"}, esperado).deu_certo()

ruim := validar({"quantidade": 0, "contato": "ana"}, esperado)
assert ruim.falhou()
assert len(ruim.erro()) is 2