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
//! The `accounts` subcommand: report what each configured account can do.
//!
//! Split from `main.rs` to keep that file within the repository's 1000-line
//! limit, and kept in the library so the remote form renders through the same
//! printer — an operator reading a table has no way to tell which machine
//! answered, so the two must not be able to drift (issues #294, #306).
use std::process::ExitCode;
use crate::accounts::AccountRouter;
use crate::cli::AccountOp;
/// Render the account pool.
///
/// `credential` is printed beside `healthy` so an operator can see *why* an
/// account is unhealthy without running `doctor`, which was the contradiction
/// issue #242 reported: `accounts list` said `healthy true` while `doctor`
/// said EXPIRED and every request returned 401.
#[must_use]
pub fn run(
router: &AccountRouter,
refreshes: Option<&crate::refresh::TokenCache>,
op: &AccountOp,
) -> ExitCode {
match op {
AccountOp::List { json, .. } if *json => {
let rows: Vec<serde_json::Value> = router
.health_snapshot_with(refreshes)
.into_iter()
.map(|health| {
serde_json::json!({
"name": health.name,
"healthy": health.healthy,
"credential": health.credential.label(),
"used": health.used,
"request_limit": health.request_limit,
"remaining_requests": health.remaining_requests,
"home": health.home.display().to_string(),
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&rows).unwrap_or_else(|_| "[]".to_string())
);
ExitCode::SUCCESS
}
AccountOp::List { .. } => {
println!("{}", header());
for health in router.health_snapshot_with(refreshes) {
println!(
"{}",
row(&AccountRow {
name: &health.name,
healthy: Some(health.healthy),
credential: health.credential.label(),
used: Some(health.used as u64),
limit: health.request_limit.map(|value| value as u64),
remaining: health.remaining_requests.map(|value| value as u64),
home: health.home.display().to_string(),
})
);
}
ExitCode::SUCCESS
}
}
}
/// One account, from either the local pool or a remote router's JSON.
///
/// `None` means the answer is genuinely absent. That distinction is the point:
/// the remote formatter read every field with `as_str()`, so a JSON *number*
/// yielded the same `-` as a field the server never sent, and the table could
/// not show a figure at all (issue #306).
pub struct AccountRow<'a> {
pub name: &'a str,
pub healthy: Option<bool>,
pub credential: &'a str,
pub used: Option<u64>,
pub limit: Option<u64>,
pub remaining: Option<u64>,
pub home: String,
}
/// The column titles, shared by both modes.
///
/// One printer for both paths, for the reason issue #294 gave for `tokens` and
/// `providers`: an operator reading a table has no way to tell which machine
/// answered, so the two must not be able to drift. The remote form rendered
/// three of these eight columns — dropping `healthy`, which is the one the
/// command exists to answer (issue #306).
#[must_use]
pub fn header() -> String {
format!(
"{:<16} {:<8} {:<12} {:<6} {:<9} {:<9} home",
"name", "healthy", "credential", "used", "limit", "remaining"
)
}
/// One rendered row, in the columns [`header`] names.
#[must_use]
pub fn row(account: &AccountRow<'_>) -> String {
let optional =
|value: Option<u64>| value.map_or_else(|| "-".to_string(), |value| value.to_string());
format!(
"{:<16} {:<8} {:<12} {:<6} {:<9} {:<9} {}",
account.name,
account
.healthy
.map_or_else(|| "-".to_string(), |healthy| healthy.to_string()),
account.credential,
optional(account.used),
optional(account.limit),
optional(account.remaining),
account.home
)
}