|
|
# 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:
|
|
|
|
|
|
- `Ticket` struct — all fields
|
|
|
- `Status` enum — `Todo | InProgress | Done`
|
|
|
- `TicketType` enum — `Project | Feature | Task | Bug`
|
|
|
|
|
|
Provides two free functions:
|
|
|
|
|
|
- `generate_id() -> String` — uses `RandomState` (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`): exercise `ticket.rs`, `store.rs`, and
|
|
|
`display.rs` in isolation using `tempfile::TempDir` for any file I/O.
|
|
|
- **Integration tests** (`tests/integration.rs`): drive full command flows
|
|
|
(create → read → list → update) through `cmd_*` functions against a temporary
|
|
|
directory. Include a directory-traversal test that invokes commands from a
|
|
|
nested subdirectory.
|