use std::{
collections::{BTreeMap, HashMap},
process::Command,
sync::OnceLock,
};
use colored::Colorize;
use regex::Regex;
use semver::Version;
use crate::{
bgworker,
config::{self, Config},
depcheck, Error, Result,
};
const SEAORM_INSTALLED: &str = "SeaORM CLI is installed";
const SEAORM_NOT_INSTALLED: &str = "SeaORM CLI was not found";
const SEAORM_NOT_FIX: &str = r"To fix, run:
$ cargo install sea-orm-cli";
const QUEUE_CONN_OK: &str = "queue connection: success";
const QUEUE_CONN_FAILED: &str = "queue connection: failed";
const QUEUE_NOT_CONFIGURED: &str = "queue not configured?";
const MIN_SEAORMCLI_VER: &str = "1.1.0";
static MIN_DEP_VERSIONS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
fn get_min_dep_versions() -> &'static HashMap<&'static str, &'static str> {
MIN_DEP_VERSIONS.get_or_init(|| {
let mut min_vers = HashMap::new();
min_vers.insert("tokio", "1.33.0");
min_vers.insert("sea-orm", "1.1.0");
min_vers.insert("validator", "0.18.0");
min_vers.insert("axum", "0.7.5");
min_vers
})
}
#[derive(PartialOrd, PartialEq, Eq, Ord, Debug)]
pub enum Resource {
SeaOrmCLI,
Database,
Queue,
Deps,
}
#[derive(Debug, PartialEq, Eq)]
pub enum CheckStatus {
Ok,
NotOk,
NotConfigure,
}
#[derive(Debug)]
pub struct Check {
pub status: CheckStatus,
pub message: String,
pub description: Option<String>,
}
impl Check {
#[must_use]
pub fn valid(&self) -> bool {
self.status != CheckStatus::NotOk
}
pub fn to_result(&self) -> Result<()> {
if self.valid() {
Ok(())
} else {
Err(Error::Message(format!(
"{} {}",
self.message,
self.description.clone().unwrap_or_default()
)))
}
}
}
impl std::fmt::Display for Check {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let icon = match self.status {
CheckStatus::Ok => "✅",
CheckStatus::NotOk => "❌",
CheckStatus::NotConfigure => "⚠️ ",
};
write!(
f,
"{} {}{}",
icon,
self.message,
self.description
.as_ref()
.map(|d| format!("\n{d}"))
.unwrap_or_default()
)
}
}
pub async fn run_all(config: &Config, production: bool) -> Result<BTreeMap<Resource, Check>> {
let mut checks = BTreeMap::from(
#[cfg(feature = "with-db")]
[(Resource::Database, check_db(&config.database).await)],
#[cfg(not(feature = "with-db"))]
[],
);
if config.workers.mode == config::WorkerMode::BackgroundQueue {
checks.insert(Resource::Queue, check_queue(config).await);
}
if !production {
checks.insert(Resource::Deps, check_deps()?);
checks.insert(Resource::SeaOrmCLI, check_seaorm_cli()?);
}
Ok(checks)
}
pub fn check_deps() -> Result<Check> {
let cargolock = fs_err::read_to_string("Cargo.lock")?;
let crate_statuses =
depcheck::check_crate_versions(&cargolock, get_min_dep_versions().clone())?;
let mut report = String::new();
report.push_str("Dependencies\n");
let mut all_ok = true;
for status in &crate_statuses {
if let depcheck::VersionStatus::Invalid {
version,
min_version,
} = &status.status
{
report.push_str(&format!(
" {}: version {} does not meet minimum version {}\n",
status.crate_name.yellow(),
version.red(),
min_version.green()
));
all_ok = false;
}
}
Ok(Check {
status: if all_ok {
CheckStatus::Ok
} else {
CheckStatus::NotOk
},
message: report,
description: None,
})
}
#[cfg(feature = "with-db")]
pub async fn check_db(config: &crate::config::Database) -> Check {
let db_connection_failed = "DB connection: fails";
let db_connection_success = "DB connection: success";
match crate::db::connect(config).await {
Ok(conn) => match conn.ping().await {
Ok(()) => match crate::db::verify_access(&conn).await {
Ok(()) => Check {
status: CheckStatus::Ok,
message: db_connection_success.to_string(),
description: None,
},
Err(err) => Check {
status: CheckStatus::NotOk,
message: db_connection_failed.to_string(),
description: Some(err.to_string()),
},
},
Err(err) => Check {
status: CheckStatus::NotOk,
message: db_connection_failed.to_string(),
description: Some(err.to_string()),
},
},
Err(err) => Check {
status: CheckStatus::NotOk,
message: db_connection_failed.to_string(),
description: Some(err.to_string()),
},
}
}
pub async fn check_queue(config: &Config) -> Check {
if let Ok(Some(queue)) = bgworker::create_queue_provider(config).await {
match queue.ping().await {
Ok(()) => Check {
status: CheckStatus::Ok,
message: format!("{}: {}", queue.describe(), QUEUE_CONN_OK),
description: None,
},
Err(err) => Check {
status: CheckStatus::NotOk,
message: format!("{}: {}", queue.describe(), QUEUE_CONN_FAILED),
description: Some(err.to_string()),
},
}
} else {
Check {
status: CheckStatus::NotConfigure,
message: QUEUE_NOT_CONFIGURED.to_string(),
description: None,
}
}
}
pub fn check_seaorm_cli() -> Result<Check> {
match Command::new("sea-orm-cli").arg("--version").output() {
Ok(out) => {
let input = String::from_utf8_lossy(&out.stdout);
let re = Regex::new(r"(\d+\.\d+\.\d+)").unwrap();
let version_str = re
.captures(&input)
.and_then(|caps| caps.get(0))
.map(|m| m.as_str())
.ok_or("SeaORM CLI version not found")
.map_err(Box::from)?;
let version = Version::parse(version_str).map_err(Box::from)?;
let min_version = Version::parse(MIN_SEAORMCLI_VER).map_err(Box::from)?;
if version >= min_version {
Ok(Check {
status: CheckStatus::Ok,
message: SEAORM_INSTALLED.to_string(),
description: None,
})
} else {
Ok(Check {
status: CheckStatus::NotOk,
message: format!(
"SeaORM CLI minimal version is `{min_version}` (you have `{version}`). \
Run `cargo install sea-orm-cli` to update."
),
description: Some(SEAORM_NOT_FIX.to_string()),
})
}
}
Err(_) => Ok(Check {
status: CheckStatus::NotOk,
message: SEAORM_NOT_INSTALLED.to_string(),
description: Some(SEAORM_NOT_FIX.to_string()),
}),
}
}