clitool/
lib.rs

1//! Helpers for creating CLIs with serializable JSON output.
2//!
3//! This package is largely based on code from Aptos CLI.
4
5use std::process::exit;
6
7use anyhow::*;
8use async_trait::async_trait;
9
10use clap::Parser;
11use serde::Serialize;
12
13/// A common result to be returned to users
14pub type UserResult = Result<String, String>;
15
16/// A common trait for all CLI commands to have consistent outputs
17#[async_trait]
18pub trait CliTool<T: Serialize + Send>: Sized + Send + Parser {
19    /// Executes the command, returning a command specific type
20    async fn execute(self) -> Result<T>;
21
22    /// Executes the command, and serializes it to the common JSON output type
23    async fn execute_serialized(self) -> UserResult {
24        to_common_result(self.execute().await).await
25    }
26
27    /// Executes the command, and throws away Ok(result) for the string Success
28    async fn execute_serialized_success(self) -> UserResult {
29        to_common_success_result(self.execute().await).await
30    }
31
32    /// Executes the main function.
33    async fn execute_main() -> Result<()> {
34        let tool = Self::parse();
35        let result = tool.execute_serialized().await;
36        match result {
37            Result::Ok(val) => println!("{}", val),
38            Result::Err(err) => {
39                println!("{}", err);
40                exit(1);
41            }
42        };
43        Ok(())
44    }
45}
46
47/// Convert any successful response to Success
48pub async fn to_common_success_result<T>(result: Result<T>) -> UserResult {
49    to_common_result(result.map(|_| "Success")).await
50}
51
52/// A result wrapper for displaying either a correct execution result or an error.
53///
54/// The purpose of this is to have a pretty easy to recognize JSON output format e.g.
55///
56/// ```json
57/// {
58///   "result":{
59///     "encoded":{ ... }
60///   }
61/// }
62///
63/// {
64///   "error": "Failed to run command"
65/// }
66/// ```
67///
68#[derive(Debug, Serialize)]
69enum ResultWrapper<T> {
70    #[serde(rename = "result")]
71    Result(T),
72    #[serde(rename = "error")]
73    Error(String),
74}
75
76impl<T> From<Result<T>> for ResultWrapper<T> {
77    fn from(result: Result<T>) -> Self {
78        match result {
79            Result::Ok(inner) => ResultWrapper::Result(inner),
80            Result::Err(inner) => ResultWrapper::Error(inner.to_string()),
81        }
82    }
83}
84
85/// For pretty printing outputs in JSON
86pub async fn to_common_result<T: Serialize>(result: Result<T>) -> UserResult {
87    let is_err = result.is_err();
88    let result: ResultWrapper<T> = result.into();
89    let string = serde_json::to_string_pretty(&result)
90        .map_err(|e| format!("could not serialize command output: {}", e))?;
91    if is_err {
92        UserResult::Err(string)
93    } else {
94        UserResult::Ok(string)
95    }
96}