use crate::config::Resolved;
use crate::report::{ResolutionReport, SecretResolution};
use secrecy::SecretString;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use tempfile::NamedTempFile;
pub struct ValidatedSecrets {
pub resolved: Resolved<HashMap<String, SecretString>>,
pub missing_optional: Vec<String>,
pub with_defaults: Vec<(String, String)>,
pub resolution: Vec<SecretResolution>,
#[doc(hidden)]
pub(crate) temp_files: Vec<NamedTempFile>,
}
impl ValidatedSecrets {
#[doc(hidden)]
pub fn into_resolved<T>(self, secrets: T) -> Resolved<T> {
let Self {
resolved,
temp_files,
..
} = self;
resolved
.replace_secrets(secrets)
.with_temp_files(temp_files)
}
pub fn report(&self) -> ResolutionReport {
ResolutionReport::new(
self.resolved.provider.clone(),
self.resolved.profile.clone(),
self.resolution.clone(),
)
}
pub fn keep_temp_files(&mut self) -> Result<Vec<std::path::PathBuf>, std::io::Error> {
let mut paths = Vec::new();
let temp_files = std::mem::take(&mut self.temp_files);
for temp_file in temp_files {
let temp_path = temp_file.into_temp_path();
let path = temp_path.keep().map_err(|e| {
std::io::Error::other(format!("Failed to persist temporary file: {}", e))
})?;
paths.push(path);
}
Ok(paths)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConstraintKind {
AtLeastOne,
ExactlyOne,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConstraintViolation {
pub kind: ConstraintKind,
pub group: String,
pub secrets: Vec<String>,
pub present: Vec<String>,
}
impl fmt::Display for ConstraintViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
ConstraintKind::AtLeastOne => {
write!(
f,
"at least one secret in group '{}' must be provided ({})",
self.group,
self.secrets.join(", ")
)
}
ConstraintKind::ExactlyOne if self.present.is_empty() => {
write!(
f,
"exactly one secret in group '{}' must be provided ({})",
self.group,
self.secrets.join(", ")
)
}
ConstraintKind::ExactlyOne => write!(
f,
"exactly one secret in group '{}' must be provided ({}); found {}",
self.group,
self.secrets.join(", "),
self.present.join(", ")
),
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationErrors {
pub missing_required: Vec<String>,
pub missing_optional: Vec<String>,
pub with_defaults: Vec<(String, String)>,
pub provider: String,
pub profile: String,
pub resolution: Vec<SecretResolution>,
pub constraint_violations: Vec<ConstraintViolation>,
}
impl ValidationErrors {
pub fn new(
missing_required: Vec<String>,
missing_optional: Vec<String>,
with_defaults: Vec<(String, String)>,
provider: String,
profile: String,
) -> Self {
Self {
missing_required,
missing_optional,
with_defaults,
provider,
profile,
resolution: Vec::new(),
constraint_violations: Vec::new(),
}
}
pub fn has_errors(&self) -> bool {
!self.missing_required.is_empty() || !self.constraint_violations.is_empty()
}
pub fn report(&self) -> ResolutionReport {
ResolutionReport::new(
self.provider.clone(),
self.profile.clone(),
self.resolution.clone(),
)
.with_constraint_violations(self.constraint_violations.clone())
}
}
impl fmt::Display for ValidationErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.missing_required.is_empty() {
write!(
f,
"Missing required secrets: {}",
self.missing_required.join(", ")
)?;
}
if !self.constraint_violations.is_empty() {
if !self.missing_required.is_empty() {
write!(f, "; ")?;
}
let messages: Vec<String> = self
.constraint_violations
.iter()
.map(ToString::to_string)
.collect();
write!(f, "Secret constraints failed: {}", messages.join("; "))?;
}
Ok(())
}
}
impl std::error::Error for ValidationErrors {}
#[cfg(test)]
mod tests {
use super::*;
fn errors(missing_required: Vec<&str>) -> ValidationErrors {
ValidationErrors::new(
missing_required.into_iter().map(String::from).collect(),
vec![],
vec![],
"keyring".to_string(),
"default".to_string(),
)
}
#[test]
fn has_errors_true_only_when_required_missing() {
assert!(errors(vec!["A", "B"]).has_errors());
assert!(!errors(vec![]).has_errors());
let only_optional = ValidationErrors::new(
vec![],
vec!["OPT".to_string()],
vec![("X".to_string(), "v".to_string())],
"keyring".to_string(),
"default".to_string(),
);
assert!(!only_optional.has_errors());
}
#[test]
fn display_lists_missing_required_or_is_empty() {
assert_eq!(
errors(vec!["A", "B"]).to_string(),
"Missing required secrets: A, B"
);
assert_eq!(errors(vec![]).to_string(), "");
}
}