1use cli_core::{CliRunnable, CommandError};
2use server::command::{Argument, CmdResult, FinanceEntity};
3use std::collections::HashMap;
4
5pub async fn run_and_print(
9 runnable: &dyn CliRunnable,
10 args: HashMap<&str, Argument>,
11) -> Result<(), CommandError> {
12 let args_ref: HashMap<&str, &Argument> = args.iter().map(|(k, v)| (*k, v)).collect();
13 let result = runnable.run(&args_ref).await?;
14 if let Some(r) = result {
15 print_result(r);
16 }
17 Ok(())
18}
19
20fn entity_identifier(entity: &FinanceEntity) -> String {
21 match entity {
22 FinanceEntity::Tag(t) => t.tag_value.clone(),
23 FinanceEntity::Commodity(c) => c.id.to_string(),
24 FinanceEntity::Account(a) => a.id.to_string(),
25 FinanceEntity::Transaction(t) => t.id.to_string(),
26 FinanceEntity::Split(s) => s.id.to_string(),
27 FinanceEntity::Price(p) => p.id.to_string(),
28 }
29}
30
31fn tag_fields(tags: &HashMap<String, FinanceEntity>) -> Vec<String> {
32 tags.iter()
33 .filter_map(|(k, v)| match v {
34 FinanceEntity::Tag(t) => Some(format!("{k}={}", t.tag_value)),
35 _ => None,
36 })
37 .collect()
38}
39
40fn print_result(result: CmdResult) {
41 match result {
42 CmdResult::Lines(lines) => {
43 for line in lines {
44 println!("{line}");
45 }
46 }
47 CmdResult::String(s) => println!("{s}"),
48 CmdResult::Rational(r) => println!("{r}"),
49 CmdResult::Data(bytes) => {
50 use std::io::Write;
51 let _ = std::io::stdout().write_all(&bytes);
52 }
53 CmdResult::Entity(entity) => println!("{}", entity_identifier(&entity)),
54 CmdResult::Entities(entities) => {
55 for entity in entities {
56 println!("{}", entity_identifier(&entity));
57 }
58 }
59 CmdResult::MultiCurrencyBalance(items) => {
60 for (commodity, balance) in items {
61 println!("{balance} {}", commodity.id);
62 }
63 }
64 CmdResult::CommodityInfoList(items) => {
65 for info in items {
66 println!("{}\t{}\t{}", info.commodity_id, info.symbol, info.name);
67 }
68 }
69 CmdResult::TaggedEntities { entities, .. } => {
70 for (entity, tags) in entities {
71 let name = entity_identifier(&entity);
72 let fields = tag_fields(&tags);
73 if fields.is_empty() {
74 println!("{name}");
75 } else {
76 println!("{name}\t{}", fields.join("\t"));
77 }
78 }
79 }
80 CmdResult::Report(_) | CmdResult::Activity(_) | CmdResult::Breakdown(_) => {
81 eprintln!("(structured report; use `reports` subcommand for rendered output)");
82 }
83 CmdResult::Uuid(id) => println!("{id}"),
84 CmdResult::Bool(b) => println!("{b}"),
85 CmdResult::SshKeys(keys) => {
86 for k in keys {
87 let last_used = k
88 .last_used_at
89 .map_or_else(|| "never".to_string(), |d| d.to_rfc3339());
90 println!(
91 "{}\t{}\t{}\t{}\t{}",
92 k.fingerprint, k.key_type, k.annotation, k.created_at, last_used
93 );
94 }
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use server::command::CmdResult;
103 use std::fmt::Debug;
104 use std::future::Future;
105 use std::pin::Pin;
106
107 #[derive(Debug, Default)]
108 struct StubRunnable {
109 result_lines: Option<Vec<String>>,
110 }
111
112 impl CliRunnable for StubRunnable {
113 fn run<'a>(
114 &'a self,
115 _args: &'a HashMap<&str, &Argument>,
116 ) -> Pin<Box<dyn Future<Output = Result<Option<CmdResult>, CommandError>> + Send + 'a>>
117 {
118 let out = self.result_lines.clone().map(CmdResult::Lines);
119 Box::pin(async move { Ok(out) })
120 }
121 }
122
123 #[tokio::test]
124 async fn run_and_print_accepts_empty_result() {
125 let stub = StubRunnable { result_lines: None };
126 run_and_print(&stub, HashMap::new()).await.unwrap();
127 }
128
129 #[tokio::test]
130 async fn run_and_print_handles_lines_result() {
131 let stub = StubRunnable {
132 result_lines: Some(vec!["a".to_string(), "b".to_string()]),
133 };
134 run_and_print(&stub, HashMap::new()).await.unwrap();
135 }
136
137 #[tokio::test]
138 async fn run_and_print_forwards_runnable_error() {
139 #[derive(Debug)]
140 struct Failing;
141 impl CliRunnable for Failing {
142 fn run<'a>(
143 &'a self,
144 _args: &'a HashMap<&str, &Argument>,
145 ) -> Pin<Box<dyn Future<Output = Result<Option<CmdResult>, CommandError>> + Send + 'a>>
146 {
147 Box::pin(async move { Err(CommandError::Argument("boom".to_string())) })
148 }
149 }
150 let err = run_and_print(&Failing, HashMap::new())
151 .await
152 .expect_err("should bubble error");
153 assert!(matches!(err, CommandError::Argument(_)));
154 }
155}