4.2 KiB
nbd — Architecture
Overview
nbd is a single-binary CLI tool with no background service. All state lives in
.nbd/tickets/ inside the project directory tree. The binary is structured as
four modules, each with a single responsibility:
src/
├── main.rs CLI definition and command dispatch
├── ticket.rs Data model, ID generation, priority validation
├── store.rs File I/O, directory traversal, CRUD
└── display.rs Human-readable and JSON output formatting
Module Responsibilities
main.rs — CLI entry point
Owns the clap struct definitions (Cli, Commands) and the top-level main
function. Delegates all business logic to command handlers (cmd_create,
cmd_read, cmd_list, cmd_update). The handlers are thin: they call helpers
from store, ticket, and display, then return.
Parsing helpers (parse_status, parse_ticket_type, parse_deps) live here
because they translate raw CLI strings into typed values used only by the command
handlers.
ticket.rs — Data model
Defines the canonical in-memory representation of a ticket:
Ticketstruct — all fieldsStatusenum —Todo | InProgress | DoneTicketTypeenum —Project | Feature | Task | Bug
Provides two free functions:
generate_id() -> String— usesRandomState(seeded with OS entropy) to produce a 6-character lowercase hex string. No external randomness crate needed.validate_priority(u8) -> Result<(), String>— enforces the 0–10 range.
The enums derive serde's Serialize/Deserialize with #[serde(rename_all)]
so the JSON representation uses lowercase strings ("in_progress", "bug", …).
store.rs — File I/O
All filesystem operations are in this module. Uses async-std for async I/O
throughout.
Directory discovery:
find_nbd_root()
└─ find_nbd_root_from(cwd)
Walk parent dirs until .nbd/ is found
find_nbd_root_from accepts an explicit starting path to make it testable
without changing the process's working directory.
CRUD functions:
| Function | Operation |
|---|---|
ensure_tickets_dir(root) |
Create .nbd/tickets/ if missing |
write_ticket(root, ticket) |
Serialise → pretty JSON → write file |
read_ticket(root, id) |
Read file → deserialise; friendly error if not found |
list_tickets(root) |
Read all *.json → sort by priority desc |
Error type: store::Result<T> is aliased to
Result<T, Box<dyn Error + Send + Sync>>, allowing ? to propagate both
io::Error and serde_json::Error without explicit wrapping.
display.rs — Output formatting
Two output modes:
- Tabular — human-readable key–value block (single ticket) or column-aligned table (list). Column widths are compile-time constants.
- JSON — delegates to
serde_json::to_string_pretty.
Each public surface is split into a format_* function (returns String,
testable) and a print_* function (writes to stdout via println!).
Data Flow
CLI args (clap)
│
▼
dispatch() [main.rs]
│
├── parse_* helpers ──► typed values
│
├── store::find_nbd_root() ──► PathBuf
├── store::read_ticket() ──► Ticket
├── store::list_tickets() ──► Vec<Ticket>
├── store::write_ticket() ──► ()
│
└── display::print_ticket() ──► stdout
display::print_list() ──► stdout
Storage Layout
<project-root>/
└── .nbd/
└── tickets/
├── a3f9c2.json
├── b7d41e.json
└── ...
Each file is a pretty-printed JSON object with the fields of Ticket. File name
equals the ticket's id field with a .json extension.
Testing Strategy
- Unit tests (
src/tests.rs): exerciseticket.rs,store.rs, anddisplay.rsin isolation usingtempfile::TempDirfor any file I/O. - Integration tests (
tests/integration.rs): drive full command flows (create → read → list → update) throughcmd_*functions against a temporary directory. Include a directory-traversal test that invokes commands from a nested subdirectory.