1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
//! Custom-made solution to output a JSON return message and ensure a return code
//! from a CLI command. The main use-case for this module is to provide a consistent output for
//! queries and transactions.
//!
//! The examples below rely on crate-private methods (for this reason, doctests are ignored).
//! They are intended for contributors to crate `relayer-cli`, and _not_ for users of this binary.
//!
//! ## Examples on how to use the quick-access constructors:
//!
//! - Exit from a query/tx with a `String` error:
//!
//! ```ignore
//! let e = String::from("error message");
//! Output::error(e).exit();
//! // or as an alternative:
//! Output::error(json!("error occurred")).exit();
//! ```
//!
//! - Exit from a query/tx with an error of type `anomaly`:
//!   In the case where the error is a complex type such as anomaly (including backtraces), it is
//!   better to simplify the output and only write out the chain of error sources, which we can
//!   achieve with `format!("{}", e)`. The complete solution is as follows:
//!
//! ```ignore
//! let e: Error = Kind::Query.into();
//! Output::error(format!("{}", e)).exit();
//! ```
//!
//! #### Note:
//! The resulting output that this approach generates is determined by the 'error' annotation given
//! to the error object `Kind::Query`. If this error object comprises any positional arguments,
//! e.g. as achieved by `Query(String, String)`, then it is important to cover these arguments
//! in the `error` annotation, for instance:
//! ```ignore
//! #[derive(Debug, Error)]
//! pub enum Kind {
//!     #[error("failed with underlying causes: {0}, {1}")]
//!     Query(String, String),
//!     // ...
//! }
//! ```
//!
//! - Exit from a query/tx with success:
//!
//! ```ignore
//! let cs = ChannelEnd::default();
//! Output::success(cs).exit();
//! ```
//!
//! - Exit from a query/tx with success and multiple objects in the result:
//!
//! ```ignore
//! let h = Height::default();
//! let end = ConnectionEnd::default();
//! Output::success(h).with_result(end).exit();
//! ```

use core::fmt::{self, Debug};
use std::sync::OnceLock;

use serde::Serialize;
use tracing::{error, info};

static JSON: OnceLock<bool> = OnceLock::new();

/// Functional-style method to exit a program.
///
/// ## Note: See `Output::exit()` for the preferred method of exiting a relayer command.
pub fn exit_with(out: Output) -> ! {
    let status = out.status;

    out.print();

    // The return code
    if status == Status::Error {
        std::process::exit(1);
    } else {
        std::process::exit(0);
    }
}

/// Return whether or not JSON output is enabled.
pub fn json() -> bool {
    *JSON.get_or_init(|| false)
}

/// Set whether or not JSON output is enabled.
pub fn set_json(enabled: bool) {
    JSON.set(enabled).expect("failed to set JSON mode")
}

/// Exits the program. Useful when a type produces an error which can no longer be propagated, and
/// the program must exit instead.
///
/// ## Example of use
/// - Without this function:
/// ```ignore
/// let res = ForeignClient::new(chains.src.clone(), chains.dst.clone());
/// let client = match res {
///     Ok(client) => client,
///     Err(e) => Output::error(format!("{}", e)).exit(),
/// };
/// ```
/// - With support from `exit_with_unrecoverable_error`:
/// ```ignore
/// let client_a = ForeignClient::new(chains.src.clone(), chains.dst.clone())
///     .unwrap_or_else(exit_with_unrecoverable_error);
/// ```
pub fn exit_with_unrecoverable_error<T, E: fmt::Display>(err: E) -> T {
    Output::error(format!("{err}")).exit()
}

/// The result to display before quitting, can either be a JSON value, some plain text,
/// a value to print with its Debug instance, or nothing.
#[derive(Debug)]
pub enum Result {
    Json(serde_json::Value),
    Text(String),
    Value(Box<dyn fmt::Debug + Send + Sync + 'static>),
    Nothing,
}

impl fmt::Display for Result {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Result::Json(v) => write!(f, "{}", serde_json::to_string_pretty(v).unwrap()),
            Result::Text(t) => write!(f, "{t}"),
            Result::Value(v) => write!(f, "{v:#?}"),
            Result::Nothing => Ok(()),
        }
    }
}

/// A CLI output with support for JSON serialization. The only mandatory field is the `status`,
/// which typically signals a success (UNIX process return code `0`) or an error (code `1`). An
/// optional `result` can be added to an output.
///
pub struct Output {
    /// The return status
    pub status: Status,

    /// The result of a command, such as the output from a query or transaction.
    pub result: Result,
}

impl Output {
    /// Constructs a new `Output` with the provided `status` and an empty `result`.
    pub fn new(status: Status) -> Self {
        Output {
            status,
            result: Result::Nothing,
        }
    }

    /// Constructor that returns a new `Output` having a `Success` status and empty `result`.
    pub fn with_success() -> Self {
        Output::new(Status::Success)
    }

    /// Constructor that returns a new `Output` having an `Error` status and empty `result`.
    pub fn with_error() -> Self {
        Output::new(Status::Error)
    }

    /// Builder-style method for attaching a result to an output object.
    pub fn with_result<R>(mut self, result: R) -> Self
    where
        R: Serialize + Debug + Send + Sync + 'static,
    {
        if json() {
            self.result = Result::Json(serde_json::to_value(result).unwrap());
        } else {
            self.result = Result::Value(Box::new(result));
        }

        self
    }

    /// Builder-style method for attaching a plain text message to an output object.
    pub fn with_msg(mut self, msg: impl ToString) -> Self {
        self.result = Result::Text(msg.to_string());
        self
    }

    /// Quick-access constructor for an output signalling a success `status` and tagged with the
    /// input `result`.
    pub fn success<R>(result: R) -> Self
    where
        R: Serialize + Debug + Send + Sync + 'static,
    {
        Output::with_success().with_result(result)
    }

    /// Quick-access constructor for an output message signalling a error `status`.
    pub fn error(msg: impl ToString) -> Self {
        Output::with_error().with_msg(msg)
    }

    /// Quick-access constructor for an output signalling a success `status` and tagged with the
    /// input `result`.
    pub fn success_msg(msg: impl ToString) -> Self {
        Output::with_success().with_msg(msg)
    }

    pub fn print(self) {
        if json() {
            println!("{}", serde_json::to_string(&self.into_json()).unwrap());
        } else {
            match self.status {
                Status::Success => info!("{}", self.result),
                Status::Error => error!("{}", self.result),
            }
        }
    }

    /// Exits from the process with the current output. Convenience wrapper over `exit_with`.
    pub fn exit(self) -> ! {
        exit_with(self);
    }

    /// Convert this output value to a JSON value
    pub fn into_json(self) -> serde_json::Value {
        let result = match self.result {
            Result::Json(v) => v,
            Result::Text(v) => serde_json::Value::String(v),
            Result::Value(v) => serde_json::Value::String(format!("{v:#?}")),
            Result::Nothing => serde_json::Value::String("no output".to_string()),
        };

        serde_json::json!({
            "status": self.status,
            "result": result,
        })
    }
}

/// Represents the exit status of any CLI command
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
pub enum Status {
    #[serde(rename(serialize = "success"))]
    Success,

    #[serde(rename(serialize = "error"))]
    Error,
}

impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Status::Success => write!(f, "Success"),
            Status::Error => write!(f, "Error"),
        }
    }
}