Lines
46.53 %
Functions
35.14 %
Branches
100 %
use cli_core::{CliRunnable, CommandError};
use server::command::{Argument, CmdResult, FinanceEntity};
use std::collections::HashMap;
/// Run a `CliRunnable` with the given argument map and render its result
/// to stdout in a script-friendly format. Returns the same `CommandError`
/// the runnable raised; the caller decides the exit code.
pub async fn run_and_print(
runnable: &dyn CliRunnable,
args: HashMap<&str, Argument>,
) -> Result<(), CommandError> {
let args_ref: HashMap<&str, &Argument> = args.iter().map(|(k, v)| (*k, v)).collect();
let result = runnable.run(&args_ref).await?;
if let Some(r) = result {
print_result(r);
}
Ok(())
fn entity_identifier(entity: &FinanceEntity) -> String {
match entity {
FinanceEntity::Tag(t) => t.tag_value.clone(),
FinanceEntity::Commodity(c) => c.id.to_string(),
FinanceEntity::Account(a) => a.id.to_string(),
FinanceEntity::Transaction(t) => t.id.to_string(),
FinanceEntity::Split(s) => s.id.to_string(),
FinanceEntity::Price(p) => p.id.to_string(),
fn tag_fields(tags: &HashMap<String, FinanceEntity>) -> Vec<String> {
tags.iter()
.filter_map(|(k, v)| match v {
FinanceEntity::Tag(t) => Some(format!("{k}={}", t.tag_value)),
_ => None,
})
.collect()
fn print_result(result: CmdResult) {
match result {
CmdResult::Lines(lines) => {
for line in lines {
println!("{line}");
CmdResult::String(s) => println!("{s}"),
CmdResult::Rational(r) => println!("{r}"),
CmdResult::Data(bytes) => {
use std::io::Write;
let _ = std::io::stdout().write_all(&bytes);
CmdResult::Entity(entity) => println!("{}", entity_identifier(&entity)),
CmdResult::Entities(entities) => {
for entity in entities {
println!("{}", entity_identifier(&entity));
CmdResult::MultiCurrencyBalance(items) => {
for (commodity, balance) in items {
println!("{balance} {}", commodity.id);
CmdResult::CommodityInfoList(items) => {
for info in items {
println!("{}\t{}\t{}", info.commodity_id, info.symbol, info.name);
CmdResult::TaggedEntities { entities, .. } => {
for (entity, tags) in entities {
let name = entity_identifier(&entity);
let fields = tag_fields(&tags);
if fields.is_empty() {
println!("{name}");
} else {
println!("{name}\t{}", fields.join("\t"));
CmdResult::Report(_) | CmdResult::Activity(_) | CmdResult::Breakdown(_) => {
eprintln!("(structured report; use `reports` subcommand for rendered output)");
CmdResult::Uuid(id) => println!("{id}"),
CmdResult::Bool(b) => println!("{b}"),
CmdResult::SshKeys(keys) => {
for k in keys {
let last_used = k
.last_used_at
.map_or_else(|| "never".to_string(), |d| d.to_rfc3339());
println!(
"{}\t{}\t{}\t{}\t{}",
k.fingerprint, k.key_type, k.annotation, k.created_at, last_used
);
#[cfg(test)]
mod tests {
use super::*;
use server::command::CmdResult;
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
#[derive(Debug, Default)]
struct StubRunnable {
result_lines: Option<Vec<String>>,
impl CliRunnable for StubRunnable {
fn run<'a>(
&'a self,
_args: &'a HashMap<&str, &Argument>,
) -> Pin<Box<dyn Future<Output = Result<Option<CmdResult>, CommandError>> + Send + 'a>>
{
let out = self.result_lines.clone().map(CmdResult::Lines);
Box::pin(async move { Ok(out) })
#[tokio::test]
async fn run_and_print_accepts_empty_result() {
let stub = StubRunnable { result_lines: None };
run_and_print(&stub, HashMap::new()).await.unwrap();
async fn run_and_print_handles_lines_result() {
let stub = StubRunnable {
result_lines: Some(vec!["a".to_string(), "b".to_string()]),
};
async fn run_and_print_forwards_runnable_error() {
#[derive(Debug)]
struct Failing;
impl CliRunnable for Failing {
Box::pin(async move { Err(CommandError::Argument("boom".to_string())) })
let err = run_and_print(&Failing, HashMap::new())
.await
.expect_err("should bubble error");
assert!(matches!(err, CommandError::Argument(_)));