CLI de anotações
Uma ferramenta de terminal com subcomandos, persistência em JSON e saída legível.
O projeto mais comum que existe, e o que mais cedo mostra se a arquitetura aguenta crescer: cada subcomando é uma ação, o estado mora num arquivo, e a leitura dos argumentos fica separada do que eles fazem — senão testar adicionar exige simular a linha de comando inteira.
| Peça | O que ela exercita |
|---|---|
OS.argv() | ler os argumentos sem biblioteca |
to_json / from_json | persistir sem banco |
match | despachar o subcomando |
| record | a nota é imutável; editar devolve outra |
Estrutura#
text
cli-notas/
forge.toml
src/
main.df le argv e despacha — e nada mais
notas.df a regra: adicionar, listar, concluir, buscar
arquivo.df onde o estado mora
tests/
notas_test.dfforge.toml
[project]
name = "cli-notas"
version = "0.1.0"
description = "Anotações no terminal"
entry = "src/main.df"
dataforge = ">=1.1"
[dependencies]
[scripts]
start = "run src/main.df"
test = "test tests/"
O núcleo#
Este bloco roda sozinho — copie para um arquivo e rode dataforge run. Ele termina com assert, e é assim que esta página é conferida a cada build.
src/notas.df
record Nota:
id: Integer
texto: String
feita: Boolean
action adicionar(notas, texto):
given texto.trim() is "":
trigger "uma nota vazia nao e uma nota"
proximo := 1 given len(notas) is 0 otherwise notas[-1].id + 1
yield [...notas, Nota(proximo, texto.trim(), no)]
action concluir(notas, id):
achou := no
novas := []
cycle n in notas:
given n.id is id:
achou := yes
novas.append(n with {"feita": yes})
otherwise:
novas.append(n)
given not achou:
trigger $"nao ha nota {id}"
yield novas
action buscar(notas, termo):
yield notas >> sift n: termo.lower() in n.texto.lower()
action linha(n):
marca := "[x]" given n.feita otherwise "[ ]"
yield $"{marca} {str(n.id).pad_start(3)} {n.texto}"
// O despacho: a unica parte que conhece a linha de comando.
action executar(notas, argv):
given len(argv) is 0:
yield {"notas": notas, "saida": ["uso: notas <adicionar|listar|feita|buscar>"]}
comando := argv[0]
resto := " ".join(argv[1:])
match comando:
point "adicionar":
yield {"notas": adicionar(notas, resto), "saida": ["anotado"]}
point "listar":
yield {"notas": notas, "saida": notas >> morph n: linha(n)}
point "feita":
yield {"notas": concluir(notas, int(resto)), "saida": ["concluida"]}
point "buscar":
yield {"notas": notas, "saida": buscar(notas, resto) >> morph n: linha(n)}
default:
yield {"notas": notas, "saida": [$"comando desconhecido: {comando}"]}
// A persistencia e so texto: o record vira vault na ida e volta na volta.
action para_json(notas):
yield to_json(notas >> morph n: {"id": n.id, "texto": n.texto, "feita": n.feita})
action de_json(texto):
yield from_json(texto) >> morph v: Nota(v["id"], v["texto"], v["feita"])
estado := []
estado := executar(estado, ["adicionar", "comprar", "cafe"])["notas"]
estado := executar(estado, ["adicionar", "revisar", "o", "PR"])["notas"]
estado := executar(estado, ["feita", "1"])["notas"]
cycle l in executar(estado, ["listar"])["saida"]:
out l
volta := de_json(para_json(estado))
assert volta is estado
assert len(buscar(estado, "revisar")) is 1
assert estado[0].feita and not estado[1].feitaO teste#
No projeto, a regra mora em src/ e o teste a importa pelo caminho relativo — dataforge test tests/ descobre o arquivo sozinho.
tests/nucleo_test.df
adopt ../src/notas as N
crucible "notas":
trial "o id continua depois do ultimo":
a := N.adicionar([], "um")
b := N.adicionar(a, "dois")
expect b[1].id is 2
trial "nota vazia e recusada":
expect(lambda => N.adicionar([], " ")).to_raise()
trial "concluir o que nao existe levanta":
expect(lambda => N.concluir([], 7)).to_raise()As decisões#
| Decisão | Sem ela |
|---|---|
executar recebe argv como lista | testar exige mexer no sys.argv do processo |
o record é imutável (with) | concluir muda a lista que outra parte ainda está lendo |
trigger com o motivo | a CLI imprime “erro” e sai com 0 |
| o id vem do último, não do tamanho | apagar a nota 2 de 3 faz a próxima nascer com o id 3 — repetido |
Para ir além#
- Troque o JSON por
Arcane.Databasesem mexer emnotas.df— é para isso que a regra não sabe onde mora. - Acrescente
--jsonpara a saída ser lida por outro programa. - Veja Receitas → CLI para cor, tabela e ajuda gerada.
Volte para todos os tipos de projeto.