use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
pub mod error;
pub use error::ShieldError;
pub mod log;
pub mod node;
pub mod schema;
pub use schema::{EnvMap, FinalizedSchema, SchemaCheck, ValidatedAttribute, ValidatedOptions};
const MAX_DISPLAY_LEN: usize = 50;
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ShieldStatus {
Hopeless,
Recoverable,
Operational,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ShieldResponse {
pub schema_file: String,
pub status: ShieldStatus,
pub kind: ShieldResponseKind,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ShieldResponseKind {
Failed {
error: String, },
Success {
checks_from_env: Box<SchemaCheck>,
},
}
fn truncated(s: &str) -> String {
if s.len() <= MAX_DISPLAY_LEN {
s.to_string()
} else {
format!("{}...", &s[..MAX_DISPLAY_LEN.saturating_sub(3)])
}
}
impl Display for ShieldResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.kind {
ShieldResponseKind::Failed { error } => {
let _ = writeln!(f, "{} {}", "Error:".red().bold(), error);
write!(f, "")
}
ShieldResponseKind::Success { checks_from_env } => {
let _ = writeln!(
f,
"{} schema at: ./{}",
"Parsed:".green().bold(),
self.schema_file
);
let total_missing = checks_from_env.missing_values.len()
+ checks_from_env.missing_default.len()
+ checks_from_env.missing_secrets.len();
let num_correct = checks_from_env.existing_subset.len();
if num_correct > 0 {
let _ = writeln!(
f,
"{} {} variables",
"Correct: ".green().bold(),
num_correct
);
if total_missing == 0
&& checks_from_env.missing_optional.is_empty()
&& checks_from_env.incorrect_values.is_empty()
{
let _ = writeln!(f, "{}", "Success!".green().bold(),);
}
}
let _ = writeln!(f);
let max_key_len = checks_from_env
.missing_values
.keys()
.chain(checks_from_env.missing_default.keys())
.chain(checks_from_env.incorrect_values.keys())
.chain(checks_from_env.missing_secrets.iter())
.chain(checks_from_env.missing_optional.iter())
.map(|k| k.len())
.max()
.unwrap_or(0);
if !checks_from_env.missing_optional.is_empty() {
let _ = writeln!(
f,
"{} {} optional variables missing from env:",
"Warning:".yellow().bold(),
checks_from_env.missing_optional.len(),
);
for key in checks_from_env.missing_optional.iter() {
let _ = writeln!(f, " {:width$} ", key, width = max_key_len);
}
let _ = writeln!(f);
}
if !checks_from_env.incorrect_values.is_empty() {
let _ = writeln!(
f,
"{} Variables with incorrect values:",
"Error:".red().bold()
);
}
for (key, incorrect_value) in checks_from_env.incorrect_values.iter() {
let _ = writeln!(
f,
" {:width$}: '{}'",
key,
truncated(incorrect_value),
width = max_key_len
);
}
if !checks_from_env.incorrect_values.is_empty() {
let _ = writeln!(f);
}
if total_missing > 0 {
let _ = writeln!(
f,
"{} {} {}",
"Error:".red().bold(),
total_missing.to_string().bold(),
"required variables missing from env:".bold(),
);
}
for (key, missing_value) in checks_from_env.missing_values.iter() {
let _ = writeln!(
f,
"(value) {:width$}: '{}'",
key,
truncated(missing_value),
width = max_key_len
);
}
for (key, missing_value) in checks_from_env.missing_default.iter() {
let _ = writeln!(
f,
"(default) {:width$}: '{}'",
key,
truncated(missing_value),
width = max_key_len
);
}
for key in checks_from_env.missing_secrets.iter() {
let _ = writeln!(f, "(secret) {:width$}", key, width = max_key_len);
}
write!(f, "")
}
}
}
}
impl ShieldResponse {
pub fn new(filename: &str) -> ShieldResponse {
let schema = match FinalizedSchema::new(filename) {
Ok(s) => s,
Err(err) => match err {
ShieldError::Unrecoverable(err) => {
return Self {
status: ShieldStatus::Hopeless,
schema_file: filename.to_string(),
kind: ShieldResponseKind::Failed {
error: err.to_string(),
},
};
}
_ => {
return Self {
status: ShieldStatus::Recoverable,
schema_file: filename.to_string(),
kind: ShieldResponseKind::Failed {
error: err.to_string(),
},
};
}
},
};
let env_vars: EnvMap = std::env::vars().collect();
let checked_from_env = SchemaCheck::new(&schema, &env_vars);
Self {
status: ShieldStatus::Operational,
schema_file: filename.to_string(),
kind: ShieldResponseKind::Success {
checks_from_env: Box::new(checked_from_env),
},
}
}
}
use test_generator::test_resources;
#[allow(unused)]
#[test_resources("test-files/invalid/*.toml")]
fn invalid_test(filename: &str) {
assert!(std::path::Path::new(filename).exists());
let response = ShieldResponse::new(filename);
assert!(matches!(
response.kind,
ShieldResponseKind::Failed { error: _ }
));
}
#[allow(unused)]
#[test_resources("test-files/valid/*.toml")]
fn valid_test(filename: &str) {
assert!(std::path::Path::new(filename).exists());
let response = ShieldResponse::new(filename);
assert!(matches!(
response.kind,
ShieldResponseKind::Success { checks_from_env: _ }
));
}