use core::fmt;
use serde::{Deserialize, Serialize};
use crate::config::AppConfig;
#[cfg(doc)]
use super::{Request, Response};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SheepDrift {
pub name: String,
pub fields: Vec<String>,
}
impl SheepDrift {
#[must_use]
pub fn new(name: impl Into<String>, fields: Vec<String>) -> Self {
Self {
name: name.into(),
fields,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SheepApplied {
pub name: String,
pub applied: Vec<String>,
pub pending: Vec<String>,
pub refused: Option<String>,
}
impl SheepApplied {
#[must_use]
pub fn new(
name: impl Into<String>,
applied: Vec<String>,
pending: Vec<String>,
refused: Option<String>,
) -> Self {
Self {
name: name.into(),
applied,
pending,
refused,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SheepRefusal {
pub name: String,
pub reason: String,
}
impl SheepRefusal {
#[must_use]
pub fn new(name: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
name: name.into(),
reason: reason.into(),
}
}
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SheepConfigView {
pub name: String,
pub config: AppConfig,
pub env_keys: Vec<String>,
pub env_secrets: Vec<String>,
pub overridden: Vec<String>,
pub pending: Vec<String>,
}
impl SheepConfigView {
#[must_use]
pub fn new(mut config: AppConfig, overridden: Vec<String>, pending: Vec<String>) -> Self {
let env_secrets = crate::secrets::sealed_keys(&config);
let env_keys = config.env.keys().cloned().collect();
config.env.clear();
Self {
name: config.name.clone(),
config,
env_keys,
env_secrets,
overridden,
pending,
}
}
}
impl fmt::Debug for SheepConfigView {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"SheepConfigView {{ name: {:?}, env_keys: {}, env_secrets: {}, overridden: {}, pending: {} }}",
self.name,
self.env_keys.len(),
self.env_secrets.len(),
self.overridden.len(),
self.pending.len()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_sheep_config_view_never_carries_an_env_value() {
let mut config = AppConfig::minimal("web", "./srv");
config
.env
.insert("DB_PASS".to_string(), "hunter2".to_string());
let view = SheepConfigView::new(config, Vec::new(), Vec::new());
assert!(view.config.env.is_empty());
assert_eq!(view.env_keys, ["DB_PASS"]);
let json = serde_json::to_string(&view).unwrap();
assert!(!json.contains("hunter2"), "{json}");
}
#[test]
fn a_sheep_config_views_debug_is_the_exact_redacted_string() {
let mut config = AppConfig::minimal("web", "./srv");
config.env.insert("A".to_string(), "1".to_string());
let view = SheepConfigView::new(config, vec!["max_restarts".to_string()], Vec::new());
assert_eq!(
format!("{view:?}"),
r#"SheepConfigView { name: "web", env_keys: 1, env_secrets: 0, overridden: 1, pending: 0 }"#
);
}
#[test]
fn a_config_view_records_which_env_keys_are_sealed() {
let mut config = AppConfig::minimal("web", "./srv");
config.env.insert("PLAIN".into(), "value".into());
config.env.insert("SEALED".into(), "{{secret:PW}}".into());
let view = SheepConfigView::new(config, Vec::new(), Vec::new());
assert!(view.config.env.is_empty());
assert_eq!(view.env_keys, ["PLAIN", "SEALED"]);
assert_eq!(view.env_secrets, ["SEALED"]);
}
#[test]
fn a_sheep_applied_carries_names_and_never_values() {
let applied = SheepApplied::new(
"web",
vec!["cwd".to_string()],
vec!["env".to_string()],
None,
);
let json = serde_json::to_string(&applied).unwrap();
assert!(json.contains("\"env\""), "the NAME travels: {json}");
assert!(
!json.contains("DATABASE_URL"),
"and no value ever does: {json}"
);
}
#[test]
fn a_sheep_applied_debug_prints_the_names_it_was_given() {
let applied = SheepApplied::new("web", vec!["cwd".to_string()], Vec::new(), None);
assert_eq!(
format!("{applied:?}"),
"SheepApplied { name: \"web\", applied: [\"cwd\"], pending: [], refused: None }"
);
}
}