const SEPARATOR_HEAVY: &str = "======================================";
const SEPARATOR_LIGHT: &str = "--------------------------------------";
pub struct StartupBanner {
lines: Vec<BannerLine>,
service_name: String,
version: String,
}
enum BannerLine {
Kv(String, String),
Section(String),
Line(String),
}
impl StartupBanner {
pub fn new(service_name: &str, version: &str) -> Self {
Self {
lines: Vec::new(),
service_name: service_name.to_string(),
version: version.to_string(),
}
}
pub fn kv(mut self, key: &str, value: &str) -> Self {
self.lines.push(BannerLine::Kv(key.to_string(), value.to_string()));
self
}
pub fn section(mut self, title: &str) -> Self {
self.lines.push(BannerLine::Section(title.to_string()));
self
}
pub fn line(mut self, text: &str) -> Self {
self.lines.push(BannerLine::Line(text.to_string()));
self
}
pub fn print(&self) {
eprintln!("{}", SEPARATOR_HEAVY);
eprintln!("{} v{}", self.service_name, self.version);
eprintln!("{}", SEPARATOR_HEAVY);
for line in &self.lines {
match line {
| BannerLine::Kv(key, value) => eprintln!("{}: {}", key, value),
| BannerLine::Section(title) => {
eprintln!("{}", SEPARATOR_LIGHT);
eprintln!("{}:", title);
}
| BannerLine::Line(text) => eprintln!("{}", text),
}
}
eprintln!("{}", SEPARATOR_HEAVY);
}
}
pub fn mask_url_credentials(url: &str) -> String {
if let Some(at_pos) = url.find('@') {
if let Some(proto_end) = url.find("://") {
let proto = &url[.. proto_end + 3];
let after_at = &url[at_pos ..];
return format!("{}***:***{}", proto, after_at);
}
}
url.to_string()
}
pub fn mask_secret(secret: &str) -> String {
if secret.is_empty() {
return "(empty)".to_string();
}
let show = if secret.len() > 8 {
8
} else {
secret.len().min(4).max(secret.len().min(2))
};
let visible: String = secret.chars().take(show).collect();
format!("{}***", visible)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_mask_url_credentials_with_credentials() {
assert_eq!(
mask_url_credentials("mysql://user:pass@host:3306/db"),
"mysql://***:***@host:3306/db"
);
}
#[test]
fn test_mask_url_credentials_amqp() {
assert_eq!(
mask_url_credentials("amqp://admin:secret@localhost:5672"),
"amqp://***:***@localhost:5672"
);
}
#[test]
fn test_mask_url_credentials_no_credentials() {
assert_eq!(
mask_url_credentials("https://example.com/api"),
"https://example.com/api"
);
}
#[test]
fn test_mask_url_credentials_no_protocol() {
assert_eq!(mask_url_credentials("host:3306/db"), "host:3306/db");
}
#[test]
fn test_mask_secret_long() {
assert_eq!(mask_secret("abcdefghijklmnop"), "abcdefgh***");
}
#[test]
fn test_mask_secret_short() {
assert_eq!(mask_secret("short"), "shor***");
}
#[test]
fn test_mask_secret_very_short() {
assert_eq!(mask_secret("ab"), "ab***");
}
#[test]
fn test_mask_secret_empty() {
assert_eq!(mask_secret(""), "(empty)");
}
#[test]
fn test_banner_prints_without_panic() {
StartupBanner::new("test-service", "0.1.0")
.kv("env", "test")
.section("Database")
.kv(" url", "mysql://***:***@localhost:3306/db")
.line("extra info")
.print();
}
}