use serde_json::{Value, json};
use silicon_commit_client::Error as ClientError;
use silicon_commit_client::auth::SignInRefusal;
pub const SILICON_LOGIN: &str = "silicon-accounts login --app commit -q | commit login --slt-stdin";
pub const CARBON_LOGIN: &str = "commit login";
#[derive(Debug)]
pub struct CliError(Box<ErrorData>);
#[derive(Debug)]
pub struct ErrorData {
pub code: String,
pub message: String,
pub hint: Option<String>,
pub status: Option<u16>,
pub request_id: Option<String>,
pub details: Option<Value>,
pub reason: Option<String>,
pub exit: u8,
}
impl std::ops::Deref for CliError {
type Target = ErrorData;
fn deref(&self) -> &ErrorData {
&self.0
}
}
impl std::ops::DerefMut for CliError {
fn deref_mut(&mut self) -> &mut ErrorData {
&mut self.0
}
}
impl CliError {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::from_data(ErrorData {
code: code.into(),
message: message.into(),
hint: None,
status: None,
request_id: None,
details: None,
reason: None,
exit: 1,
})
}
fn from_data(data: ErrorData) -> Self {
Self(Box::new(data))
}
#[must_use]
pub fn hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
#[must_use]
pub fn reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
pub fn io(context: impl Into<String>, error: &std::io::Error) -> Self {
Self::new("io_error", format!("{}: {error}", context.into()))
}
pub fn not_signed_in() -> Self {
Self::new(
"not_signed_in",
format!("Not signed in to Commit on profile `{}`.", crate::state::profile()),
)
.hint(format!(
"Carbon: `{CARBON_LOGIN}`. Silicon: `{SILICON_LOGIN}`. Check with `commit login status`. Or pass an access token with --token."
))
}
#[must_use]
pub fn context(mut self, what: &str) -> Self {
self.message = format!("{what}: {}", self.message);
self
}
pub fn to_json(&self) -> Value {
let mut error = json!({ "code": self.code, "message": self.message });
for (key, value) in [
("hint", self.hint.clone().map(Value::from)),
("status", self.status.map(Value::from)),
("request_id", self.request_id.clone().map(Value::from)),
("reason", self.reason.clone().map(Value::from)),
("details", self.details.clone()),
] {
if let Some(value) = value {
error[key] = value;
}
}
json!({ "error": error })
}
pub fn print(&self, json_mode: bool) {
if json_mode {
eprintln!("{}", self.to_json());
return;
}
let mut meta = Vec::new();
if let Some(status) = self.status {
meta.push(format!("HTTP {status}"));
}
meta.push(self.code.clone());
if let Some(id) = &self.request_id {
meta.push(format!("request ID {id}"));
}
eprintln!("commit: {} ({})", self.message, meta.join(", "));
if let Some(details) = &self.details {
eprintln!(" details: {details}");
}
if let Some(hint) = &self.hint {
eprintln!(" hint: {hint}");
}
}
}
impl From<ClientError> for CliError {
fn from(error: ClientError) -> Self {
if let Some(refusal) = SignInRefusal::of(&error) {
return Self::from_data(ErrorData {
code: error.code().to_owned(),
message: error
.as_api()
.map(|a| a.message.clone())
.unwrap_or_else(|| accounts_message(&error)),
hint: Some(format!(
"Sign in again. Carbon: `{CARBON_LOGIN}`. Silicon: mint a fresh short-lived token and pass it: `{SILICON_LOGIN}`."
)),
status: error.status(),
request_id: error.request_id().map(str::to_owned),
details: None,
reason: Some(refusal.as_str().to_owned()),
exit: 1,
});
}
match &error {
ClientError::Api(api) => Self::from_data(ErrorData {
code: api.code.clone(),
message: api.message.clone(),
hint: api
.hint
.clone()
.or_else(|| default_api_hint(api.status, &api.code)),
status: Some(api.status),
request_id: api.request_id.clone(),
details: api.details.clone(),
reason: None,
exit: 1,
}),
ClientError::Accounts(inner) => Self::from_data(ErrorData {
code: inner.code().to_owned(),
message: inner.message(),
hint: inner.hint(),
status: inner.status(),
request_id: inner.request_id().map(str::to_owned),
details: inner.details().cloned(),
reason: None,
exit: 1,
}),
ClientError::Transport { .. } => Self::new(error.code(), error.to_string())
.hint("Check --api-url (COMMIT_API_URL) and your network, then retry; a write may or may not have been applied, so retry it with the same --idempotency-key."),
_ => Self::new(error.code(), error.to_string()),
}
}
}
fn accounts_message(error: &ClientError) -> String {
match error {
ClientError::Accounts(inner) => inner.message(),
other => other.to_string(),
}
}
fn default_api_hint(status: u16, code: &str) -> Option<String> {
Some(match (status, code) {
(401, _) => format!(
"The Commit API refused the credential. Sign in again (`{CARBON_LOGIN}`, or `{SILICON_LOGIN}`), or pass a current --token."
),
(403, "silicon_not_reachable") => {
"That Silicon takes work only from its custodian, the custodian's other Silicons, and accounts it allowed: ask it (or its custodian) to run `commit silicons allow <silicon> <your id>`.".to_owned()
}
(403, _) => "You can see this but not change it; ask its owner (or a member) to make the change or share it with you.".to_owned(),
(404, _) => "Check the identifier; it may be mistyped, deleted, or not shared with you.".to_owned(),
(406, _) => "This CLI and the Commit API speak different contracts; update the CLI (`silicon-apps install commit`).".to_owned(),
(409, _) => "The resource changed meanwhile; read it again and retry with the current version (--if-match).".to_owned(),
(422, _) => "Fix the fields named in the details; `commit <command> --help` lists each command's fields.".to_owned(),
(428, _) => "This update needs --if-match VERSION; read the resource first to get its version.".to_owned(),
(429, _) => "Too many requests; wait and retry with the same --idempotency-key.".to_owned(),
(500..=599, _) => "A problem on Commit's side; retry shortly, and send `commit report` with the request ID if it persists.".to_owned(),
_ => return None,
})
}
pub fn rfc3339(seconds: i64) -> String {
time::OffsetDateTime::from_unix_timestamp(seconds)
.ok()
.and_then(|t| {
t.format(&time::format_description::well_known::Rfc3339)
.ok()
})
.unwrap_or_else(|| seconds.to_string())
}
pub fn now() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
}
pub fn print_json(value: &Value) {
println!(
"{}",
serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
);
}