Mudanças
Escrever pelo Lavra — e as quatro regras que separam uma mudança de um GET com efeito colateral.
Uma busca pode ser repetida, cacheada e feita em paralelo. Uma mudança não: ela é a única parte do Lavra que altera o mundo, e por isso as regras são outras — inclusive a ordem de execução.
dataforge
adopt Arcane.Lavra as Lavra
record Tarefa:
id: Integer
titulo: String
feita: Boolean
BANCO := {"proximo": 1, "itens": []}
action criar(raiz, args, ctx):
id := BANCO["proximo"]
BANCO["proximo"] := id + 1
t := Tarefa(id, args["titulo"], no)
BANCO["itens"].append(t)
yield t
esq := Lavra.esquema("tarefas")
Lavra.tipo(esq, Tarefa)
// Todo esquema precisa de ao menos uma BUSCA — 'conferir' recusa um
// esquema só de mudanças, porque um cliente não teria como ler nada.
Lavra.busca(esq, "tarefas", "[Tarefa!]!",
resolve := lambda r, a, c => BANCO["itens"])
Lavra.mudanca(esq, "criarTarefa", "Tarefa!",
args := {"titulo": "String!"}, resolve := criar)
Lavra.conferir(esq)
r := Lavra.executar(esq, """
mudanca:
criarTarefa(titulo: "comprar café"):
id
titulo
feita
""")
assert r["dados"]["criarTarefa"]["id"] is 1
assert r["dados"]["criarTarefa"]["feita"] is no
out r["dados"]As quatro regras#
| Regra | Sem ela |
|---|---|
| as mudanças de uma requisição rodam em série | duas escritas na mesma linha competem, e o resultado depende do escalonador |
| a mudança devolve o que mudou | o cliente precisa de uma segunda ida à rede para ver o resultado |
| o argumento de entrada é um tipo de entrada | quinze argumentos soltos, e nenhum lugar para validar o conjunto |
a falha de uma mudança é erro, não void no campo | o cliente grava um estado que não aconteceu |
O tipo de entrada#
Um formulário de dez campos como dez argumentos é ilegível, e não tem onde declarar o que é obrigatório junto. Lavra.entrada dá nome ao conjunto:
dataforge
adopt Arcane.Lavra as Lavra
record Cliente:
id: Integer
nome: String
email: String
record NovoCliente:
nome: String
email: String
esq := Lavra.esquema("crm")
Lavra.tipo(esq, Cliente)
// Entrada e saída são tipos DIFERENTES de propósito: o 'Cliente' que
// sai tem 'id'; o que entra, não. Usar o mesmo tipo nos dois lados
// obrigaria a marcar metade dos campos como opcionais — e aí nenhum
// deles seria conferido.
Lavra.entrada(esq, NovoCliente)
Lavra.campo(esq, "NovoCliente", "nome", "String!")
Lavra.campo(esq, "NovoCliente", "email", "String!")
action criar(raiz, args, ctx):
dados := args["dados"]
given "@" not in dados["email"]:
trigger Lavra.erro("e-mail inválido", "validacao", {"campo": "email"})
yield Cliente(1, dados["nome"], dados["email"])
Lavra.busca(esq, "clientes", "[Cliente!]!", resolve := lambda r, a, c => [])
Lavra.mudanca(esq, "criarCliente", "Cliente!",
args := {"dados": "NovoCliente!"}, resolve := criar)
Lavra.conferir(esq)
r := Lavra.executar(esq, """
mudanca:
criarCliente(dados: {nome: "Ana", email: "ana@ex.com"}):
id
nome
""")
assert r["dados"]["criarCliente"]["nome"] is "Ana"
// e o e-mail errado vira ERRO, com código e campo
ruim := Lavra.executar(esq, """
mudanca:
criarCliente(dados: {nome: "Ana", email: "sem-arroba"}):
id
""")
assert len(ruim["erros"]) is 1
out ruim["erros"][0]O erro tem código, e o código é o contrato#
"e-mail inválido" é para a pessoa; "validacao" é para o programa. Um cliente que decide pelo texto da mensagem quebra na primeira tradução — e essa é a razão de o erro carregar um código e um vault de extras.
dataforge
adopt Arcane.Lavra as Lavra
// Os três desfechos que um resolvedor tem, e o que cada um significa:
action nao_achei(r, a, c):
yield void // ausência
action negado(r, a, c):
trigger Lavra.recusar("sem permissão") // autorização
action quebrado(r, a, c):
trigger Lavra.erro("o banco caiu", "indisponivel") // falha
esq := Lavra.esquema("x")
Lavra.busca(esq, "vazio", "String", resolve := nao_achei)
Lavra.busca(esq, "negado", "String", resolve := negado)
Lavra.busca(esq, "quebrado", "String", resolve := quebrado)
Lavra.conferir(esq)
assert Lavra.executar(esq, "busca:\n vazio")["dados"]["vazio"] is void
assert len(Lavra.executar(esq, "busca:\n negado")["erros"]) is 1
assert len(Lavra.executar(esq, "busca:\n quebrado")["erros"]) is 1
out "void é ausência; erro é falha — e o cliente trata os dois de formas diferentes"