Skip to content

Commit 0000407

Browse files
committed
refactor: Implement serde custom serializers for cleaner output API
- Add src/out/serializers/{markdown,text}.rs with proper serde Serializer impls - Update OutputMode with write() and write_titled() methods - Simplify command code: replace scattered output handling with cli.output.write(&data)? - Refactored chats.rs, messages.rs, contacts.rs to use unified API - Cleaned up markdown.rs as legacy (kept for backwards compat) - Added docs/output-api-design.md with full design documentation - All tests passing: text, markdown, and json outputs working correctly - Build: clean with no warnings
1 parent 5f0a5a5 commit 0000407

9 files changed

Lines changed: 1434 additions & 289 deletions

File tree

docs/output-api-design.md

Lines changed: 632 additions & 0 deletions
Large diffs are not rendered by default.

src/cmd/chats.rs

Lines changed: 7 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::app::App;
22
use crate::out;
3-
use crate::out::markdown::{format_chats, format_chat_search_results, format_members, MemberMd, SearchChatResultMd};
43
use crate::store::Store;
54
use crate::Cli;
65
use anyhow::Result;
@@ -283,58 +282,17 @@ pub async fn run(cli: &Cli, cmd: &ChatsCommand) -> Result<()> {
283282
} else {
284283
"Chats"
285284
};
286-
out::write_markdown(&format_chats(&chats, title));
285+
cli.output.write_titled(&chats, title)?;
287286
} else {
288-
println!(
289-
"{:<12} {:<30} {:<16} {:<8} LAST MESSAGE",
290-
"KIND", "NAME", "ID", "ARCH"
291-
);
292-
for c in &chats {
293-
let name = out::truncate(&c.name, 28);
294-
let ts = c
295-
.last_message_ts
296-
.map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string())
297-
.unwrap_or_default();
298-
let kind_display = if c.is_forum {
299-
format!("{}[forum]", c.kind)
300-
} else {
301-
c.kind.clone()
302-
};
303-
let archived_display = if c.archived { "yes" } else { "" };
304-
println!(
305-
"{:<12} {:<30} {:<16} {:<8} {}",
306-
kind_display, name, c.id, archived_display, ts
307-
);
308-
}
287+
cli.output.write(&chats)?;
309288
}
310289
}
311290
}
312291
ChatsCommand::Show { id } => {
313292
let chat = store.get_chat(*id).await?;
314293
match chat {
315294
Some(c) => {
316-
if cli.output.is_json() {
317-
out::write_json(&c)?;
318-
} else if cli.output.is_markdown() {
319-
use crate::out::markdown::ToMarkdown;
320-
out::write_markdown(&c.to_markdown());
321-
} else {
322-
println!("ID: {}", c.id);
323-
println!("Kind: {}", c.kind);
324-
println!("Name: {}", c.name);
325-
if let Some(u) = &c.username {
326-
println!("Username: @{}", u);
327-
}
328-
if c.is_forum {
329-
println!("Forum: yes");
330-
}
331-
if c.archived {
332-
println!("Archived: yes");
333-
}
334-
if let Some(ts) = c.last_message_ts {
335-
println!("Last message: {}", ts.to_rfc3339());
336-
}
337-
}
295+
cli.output.write(&c)?;
338296
}
339297
None => {
340298
anyhow::bail!(
@@ -471,37 +429,9 @@ pub async fn run(cli: &Cli, cmd: &ChatsCommand) -> Result<()> {
471429
"members": members,
472430
}))?;
473431
} else if cli.output.is_markdown() {
474-
let members_md: Vec<MemberMd> = members.iter().map(|m| MemberMd {
475-
id: m.id,
476-
username: m.username.clone(),
477-
first_name: m.first_name.clone(),
478-
last_name: m.last_name.clone(),
479-
status: m.status.clone(),
480-
role: m.role.clone(),
481-
}).collect();
482-
out::write_markdown(&format_members(&members_md, &chat_name, *id));
432+
cli.output.write_titled(&members, &format!("Members of \"{}\" ({})", chat_name, id))?;
483433
} else {
484-
println!(
485-
"Members of \"{}\" ({}) - {} total:\n",
486-
chat_name,
487-
id,
488-
members.len()
489-
);
490-
println!(
491-
"{:<12} {:<20} {:<20} {:<20} {:<10} STATUS",
492-
"ID", "USERNAME", "FIRST_NAME", "LAST_NAME", "ROLE"
493-
);
494-
for m in &members {
495-
println!(
496-
"{:<12} {:<20} {:<20} {:<20} {:<10} {}",
497-
m.id,
498-
m.username.as_deref().unwrap_or("-"),
499-
out::truncate(m.first_name.as_deref().unwrap_or("-"), 18),
500-
out::truncate(m.last_name.as_deref().unwrap_or("-"), 18),
501-
m.role,
502-
m.status,
503-
);
504-
}
434+
cli.output.write(&members)?;
505435
}
506436
}
507437
ChatsCommand::Archive { id } => {
@@ -629,25 +559,9 @@ pub async fn run(cli: &Cli, cmd: &ChatsCommand) -> Result<()> {
629559
"chats": results,
630560
}))?;
631561
} else if cli.output.is_markdown() {
632-
let results_md: Vec<SearchChatResultMd> = results.iter().map(|c| SearchChatResultMd {
633-
id: c.id,
634-
kind: c.kind.clone(),
635-
name: c.name.clone(),
636-
username: c.username.clone(),
637-
}).collect();
638-
out::write_markdown(&format_chat_search_results(&results_md, query));
562+
cli.output.write_titled(&results, &format!("Search Results for \"{}\"", query))?;
639563
} else {
640-
println!("Search results for \"{}\":\n", query);
641-
println!("{:<12} {:<30} {:<16} USERNAME", "KIND", "NAME", "ID");
642-
for c in &results {
643-
println!(
644-
"{:<12} {:<30} {:<16} {}",
645-
c.kind,
646-
out::truncate(&c.name, 28),
647-
c.id,
648-
c.username.as_deref().unwrap_or("-")
649-
);
650-
}
564+
cli.output.write(&results)?;
651565
}
652566
}
653567
ChatsCommand::Create {

src/cmd/contacts.rs

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use crate::out;
2-
use crate::out::markdown::{format_contacts, ToMarkdown};
32
use crate::store::Store;
43
use crate::Cli;
54
use anyhow::Result;
@@ -40,22 +39,9 @@ pub async fn run(cli: &Cli, cmd: &ContactsCommand) -> Result<()> {
4039
if cli.output.is_json() {
4140
out::write_json(&contacts)?;
4241
} else if cli.output.is_markdown() {
43-
out::write_markdown(&format_contacts(&contacts, "Contacts"));
42+
cli.output.write_titled(&contacts, "Contacts")?;
4443
} else {
45-
println!(
46-
"{:<16} {:<20} {:<20} {:<16} USERNAME",
47-
"ID", "FIRST", "LAST", "PHONE"
48-
);
49-
for c in &contacts {
50-
println!(
51-
"{:<16} {:<20} {:<20} {:<16} {}",
52-
c.user_id,
53-
out::truncate(&c.first_name, 18),
54-
out::truncate(&c.last_name, 18),
55-
out::truncate(&c.phone, 14),
56-
c.username.as_deref().unwrap_or(""),
57-
);
58-
}
44+
cli.output.write(&contacts)?;
5945
}
6046
}
6147
ContactsCommand::Search { query, limit } => {
@@ -64,42 +50,16 @@ pub async fn run(cli: &Cli, cmd: &ContactsCommand) -> Result<()> {
6450
if cli.output.is_json() {
6551
out::write_json(&contacts)?;
6652
} else if cli.output.is_markdown() {
67-
out::write_markdown(&format_contacts(&contacts, &format!("Contacts matching \"{}\"", query)));
53+
cli.output.write_titled(&contacts, &format!("Contacts matching \"{}\"", query))?;
6854
} else {
69-
println!(
70-
"{:<16} {:<20} {:<20} {:<16} USERNAME",
71-
"ID", "FIRST", "LAST", "PHONE"
72-
);
73-
for c in &contacts {
74-
println!(
75-
"{:<16} {:<20} {:<20} {:<16} {}",
76-
c.user_id,
77-
out::truncate(&c.first_name, 18),
78-
out::truncate(&c.last_name, 18),
79-
out::truncate(&c.phone, 14),
80-
c.username.as_deref().unwrap_or(""),
81-
);
82-
}
55+
cli.output.write(&contacts)?;
8356
}
8457
}
8558
ContactsCommand::Show { id } => {
8659
let contact = store.get_contact(*id).await?;
8760
match contact {
8861
Some(c) => {
89-
if cli.output.is_json() {
90-
out::write_json(&c)?;
91-
} else if cli.output.is_markdown() {
92-
out::write_markdown(&c.to_markdown());
93-
} else {
94-
println!("ID: {}", c.user_id);
95-
println!("Name: {} {}", c.first_name, c.last_name);
96-
if let Some(u) = &c.username {
97-
println!("Username: @{}", u);
98-
}
99-
if !c.phone.is_empty() {
100-
println!("Phone: {}", c.phone);
101-
}
102-
}
62+
cli.output.write(&c)?;
10363
}
10464
None => {
10565
anyhow::bail!(

0 commit comments

Comments
 (0)