use serde::{Deserialize, Serialize};
pub const RESOLUTION_REPORT_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResolutionStatus {
Resolved,
MissingRequired,
MissingOptional,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecretResolution {
pub name: String,
pub status: ResolutionStatus,
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_provider: Option<String>,
pub default_applied: bool,
pub generated: bool,
pub as_path: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolutionReport {
pub schema_version: u32,
pub provider: String,
pub profile: String,
pub secrets: Vec<SecretResolution>,
}
impl ResolutionReport {
pub fn new(provider: String, profile: String, mut secrets: Vec<SecretResolution>) -> Self {
secrets.sort_by(|a, b| a.name.cmp(&b.name));
Self {
schema_version: RESOLUTION_REPORT_SCHEMA_VERSION,
provider,
profile,
secrets,
}
}
pub fn all_required_present(&self) -> bool {
!self
.secrets
.iter()
.any(|s| s.status == ResolutionStatus::MissingRequired)
}
pub fn to_explain_string(&self) -> String {
let mut out = String::new();
out.push_str(&format!("profile: {}\n", self.profile));
out.push_str(&format!("provider: {}\n", self.provider));
let width = self.secrets.iter().map(|s| s.name.len()).max().unwrap_or(0);
for s in &self.secrets {
let detail = match s.status {
ResolutionStatus::Resolved => {
if s.generated {
"ok generated".to_string()
} else if s.default_applied {
"ok default value".to_string()
} else if let Some(uri) = &s.source_provider {
format!("ok source {}", uri)
} else {
"ok".to_string()
}
}
ResolutionStatus::MissingRequired => "MISSING required".to_string(),
ResolutionStatus::MissingOptional => "missing optional".to_string(),
};
let path = if s.as_path { " (as path)" } else { "" };
out.push_str(&format!(
" {:width$} {}{}\n",
s.name,
detail,
path,
width = width
));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> ResolutionReport {
ResolutionReport::new(
"keyring://".to_string(),
"production".to_string(),
vec![
SecretResolution {
name: "STRIPE_KEY".to_string(),
status: ResolutionStatus::MissingRequired,
required: true,
source_provider: None,
default_applied: false,
generated: false,
as_path: false,
},
SecretResolution {
name: "DATABASE_URL".to_string(),
status: ResolutionStatus::Resolved,
required: true,
source_provider: Some("keyring://".to_string()),
default_applied: false,
generated: false,
as_path: false,
},
SecretResolution {
name: "JWT_SECRET".to_string(),
status: ResolutionStatus::Resolved,
required: true,
source_provider: None,
default_applied: false,
generated: true,
as_path: false,
},
SecretResolution {
name: "LOG_LEVEL".to_string(),
status: ResolutionStatus::Resolved,
required: false,
source_provider: None,
default_applied: true,
generated: false,
as_path: false,
},
SecretResolution {
name: "SENTRY_DSN".to_string(),
status: ResolutionStatus::MissingOptional,
required: false,
source_provider: None,
default_applied: false,
generated: false,
as_path: false,
},
],
)
}
#[test]
fn entries_are_sorted_by_name() {
let report = sample();
let names: Vec<&str> = report.secrets.iter().map(|s| s.name.as_str()).collect();
assert_eq!(
names,
vec![
"DATABASE_URL",
"JWT_SECRET",
"LOG_LEVEL",
"SENTRY_DSN",
"STRIPE_KEY"
]
);
}
#[test]
fn all_required_present_tracks_missing_required() {
assert!(!sample().all_required_present());
let mut report = sample();
report
.secrets
.retain(|s| s.status != ResolutionStatus::MissingRequired);
assert!(report.all_required_present());
}
#[test]
fn serializes_to_golden_wire_format() {
let golden = include_str!("../tests/fixtures/resolution_report.golden.json");
let actual = serde_json::to_string_pretty(&sample()).unwrap();
assert_eq!(
actual.replace("\r\n", "\n").trim(),
golden.replace("\r\n", "\n").trim()
);
}
#[test]
fn round_trips_through_json() {
let report = sample();
let json = serde_json::to_string(&report).unwrap();
let back: ResolutionReport = serde_json::from_str(&json).unwrap();
assert_eq!(report, back);
}
}