feat(nbd): render tickets as TOML frontmatter markdown [4036aa]

Replace the key-value table format with TOML frontmatter + body when
printing tickets to stdout (nbd read, nbd create, nbd archive, nbd next
— all non-JSON paths). The --json output is unchanged.

New format:
  +++
  id = "a3f9c2"
  title = "Fix login bug"
  priority = 8
  status = "in_progress"
  ticket_type = "bug"
  dependencies = ["b7d41e"]
  +++
  Body text here.

Changes:
- display.rs: add DisplayFrontmatter struct, rewrite format_ticket using
  toml::to_string with id prepended as first frontmatter key
- tests.rs: update format_ticket_joins_dependencies and
  format_ticket_empty_dependencies for the new format
- integration.rs: update TestEnv::create to use --json for reliable
  ID extraction instead of parsing the key-value text format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
quotesdb
Elijah Voigt 3 months ago
parent 05beff7752
commit 8f4d25b141

@ -9,6 +9,8 @@
use std::collections::HashSet;
use serde::Serialize;
use crate::graph::TicketGraph;
use crate::store::MigrateReport;
use crate::ticket::{Status, Ticket, TicketType};
@ -23,15 +25,29 @@ const COL_PRI: usize = 5;
const COL_TYPE: usize = 9;
/// Width of the status column ("in_progress" = 11 chars, +2 padding).
const COL_STATUS: usize = 13;
/// Width of each label in the full ticket view ("Dependencies:" = 13 + 1 space).
/// Width of each label in the diff view ("dependencies:" = 13 + 1 space).
const LABEL_WIDTH: usize = 14;
// ── Internal helpers ──────────────────────────────────────────────────────────
/// Ticket metadata serialised into the TOML frontmatter block for display.
///
/// Mirrors the on-disk `MarkdownFrontmatter` in `store.rs` but adds `id` as
/// the first field so that human-readable output is self-contained.
#[derive(Serialize)]
struct DisplayFrontmatter<'a> {
id: &'a str,
title: &'a str,
priority: u8,
status: &'a Status,
ticket_type: &'a TicketType,
dependencies: &'a [String],
}
/// Return the canonical display string for a [`Status`] variant.
///
/// The strings match the serde serialisation: `"todo"`, `"in_progress"`,
/// `"done"`.
/// `"done"`, etc.
fn status_str(status: &Status) -> &'static str {
match status {
Status::Todo => "todo",
@ -58,37 +74,34 @@ fn ticket_type_str(ticket_type: &TicketType) -> &'static str {
// ── Public formatting functions ───────────────────────────────────────────────
/// Format a single ticket as a human-readable keyvalue table.
/// Format a single ticket as a TOML-frontmatter markdown document.
///
/// Each field is displayed on its own line with a label padded to
/// [`LABEL_WIDTH`] characters. An empty `Dependencies` field is rendered as
/// an empty string rather than being omitted.
/// The output mirrors the `.md` file format used on disk, with `id` added as
/// the first frontmatter key so the output is self-contained. The ticket body
/// follows the closing `+++` delimiter.
///
/// ```text
/// ID: a3f9c2
/// Title: Fix login bug
/// Body: Users cannot log in with email addresses containing +
/// Priority: 8
/// Status: in_progress
/// Type: bug
/// Dependencies: b7d41e, c9e823
/// +++
/// id = "a3f9c2"
/// title = "Fix login bug"
/// priority = 8
/// status = "in_progress"
/// ticket_type = "bug"
/// dependencies = ["b7d41e", "c9e823"]
/// +++
/// Users cannot log in with email addresses containing +
/// ```
pub fn format_ticket(ticket: &Ticket) -> String {
let deps = ticket.dependencies.join(", ");
[
format!("{:<LABEL_WIDTH$}{}", "ID:", ticket.id),
format!("{:<LABEL_WIDTH$}{}", "Title:", ticket.title),
format!("{:<LABEL_WIDTH$}{}", "Body:", ticket.body),
format!("{:<LABEL_WIDTH$}{}", "Priority:", ticket.priority),
format!("{:<LABEL_WIDTH$}{}", "Status:", status_str(&ticket.status)),
format!(
"{:<LABEL_WIDTH$}{}",
"Type:",
ticket_type_str(&ticket.ticket_type)
),
format!("{:<LABEL_WIDTH$}{}", "Dependencies:", deps),
]
.join("\n")
let fm = DisplayFrontmatter {
id: &ticket.id,
title: &ticket.title,
priority: ticket.priority,
status: &ticket.status,
ticket_type: &ticket.ticket_type,
dependencies: &ticket.dependencies,
};
let toml_str = toml::to_string(&fm).expect("frontmatter serialisation must not fail");
format!("+++\n{toml_str}+++\n{}", ticket.body)
}
/// Print a full tabular representation of a single ticket to stdout.

@ -1387,25 +1387,30 @@ mod display {
);
}
/// `format_ticket` renders dependency IDs as a comma-separated list.
/// `format_ticket` renders dependency IDs as a TOML array.
#[test]
fn format_ticket_joins_dependencies() {
let t = sample_ticket();
let output = format_ticket(&t);
assert!(
output.contains("b7d41e, c9e823"),
"dependencies should be comma-separated: {output}"
output.contains("b7d41e") && output.contains("c9e823"),
"dependencies should appear in frontmatter: {output}"
);
// TOML array format: dependencies = ["b7d41e", "c9e823"]
assert!(
output.contains("dependencies"),
"dependencies key should appear: {output}"
);
}
/// `format_ticket` renders an empty `Dependencies:` line when there are none.
/// `format_ticket` renders a `dependencies` key even when there are none.
#[test]
fn format_ticket_empty_dependencies() {
let t = Ticket::new("aaaaaa".to_string(), "No deps".to_string());
let output = format_ticket(&t);
assert!(
output.contains("Dependencies:"),
"Dependencies label should always appear"
output.contains("dependencies"),
"dependencies key should always appear in frontmatter"
);
}

@ -42,11 +42,12 @@ impl TestEnv {
self.run_from(&self.root, args)
}
/// Run `nbd create` with the given extra arguments and return the printed
/// ticket ID extracted from stdout.
/// Run `nbd create --json` with the given extra arguments and return the
/// ticket ID extracted from JSON stdout.
fn create(&self, extra_args: &[&str]) -> String {
let mut args = vec!["create"];
args.extend_from_slice(extra_args);
args.push("--json");
let output = self.run(&args);
assert!(
output.status.success(),
@ -54,11 +55,11 @@ impl TestEnv {
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
stdout
.lines()
.find(|l| l.trim_start().starts_with("ID:"))
.and_then(|l| l.split_whitespace().last())
.expect("could not extract ID from create output")
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("create --json output should be valid JSON");
parsed["id"]
.as_str()
.expect("create --json output should have 'id' field")
.to_string()
}
}

Loading…
Cancel
Save