With domain and use cases in place, we moved to the layer users actually touch: the CLI.
Read this chapter in Spanish: ES
The key question was straightforward: do we want a CLI that merely works, or one that remains predictable under real usage?
Code references:
Strong parsing with clap #
Subcommands:
add {title}list {--status}done {id}todo {id}delete {id}
Plus global --output table|json.
Why ValueEnum instead of manual parsing
#
StatusArg and OutputFormat are typed enums. We could parse raw strings and match manually, but that duplicates validation and custom error text.
With clap, invalid values fail at the input boundary before use-case execution.
UUID as input type, not delayed string parsing #
done/todo/delete receive Uuid directly.
Alternative: accept String and parse later in application.
Trade-off:
- delayed parsing leaks input concerns into business flow,
- earlier parsing gives immediate, precise user feedback.
Dual output contract: table for humans, json for machines #
Many CLIs are nice for humans but brittle for automation.
We intentionally support both:
tablefor terminal readability,jsonfor scripts and integration.
Examples:
cargo run -- list
cargo run -- --output json list --status doneInteresting case: delete
#
delete is idempotent in behavior:
deleted = trueif task existed,deleted = falseif not found.
In JSON mode we return structured payload (id, deleted, message) so scripts do not need fragile text parsing.
main.rs as wiring only
#
run() does three things:
- parse CLI,
- build repository,
- dispatch to use case.
No business rules live there.
Commit that captures this transition #
You can see the shift from “functional CLI” to “stable interface contract” very clearly.
Closing #
A mature CLI is less about command count and more about predictable behavior.
Next chapter closes the core loop: JSON persistence, tests, and the explicit technical debt we kept visible.