use crate::bootstrap::ProvenanceStartMode;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
pub type ValidationErrors = Vec<String>;
pub type ValidationResult = Result<(), ValidationErrors>;
pub fn push_if_blank(errors: &mut ValidationErrors, field_name: &str, value: &str) {
if value.trim().is_empty() {
errors.push(format!("{field_name} must not be empty"));
}
}
pub fn push_if_empty_vec<T>(errors: &mut ValidationErrors, field_name: &str, values: &[T]) {
if values.is_empty() {
errors.push(format!("{field_name} must not be empty"));
}
}
pub fn validate_rfc3339(errors: &mut ValidationErrors, field_name: &str, value: &str) {
if OffsetDateTime::parse(value, &Rfc3339).is_err() {
errors.push(format!("{field_name} must be valid RFC3339"));
}
}
pub fn validate_ref_hash_pair(
errors: &mut ValidationErrors,
ref_name: &str,
hash_name: &str,
ref_value: &Option<String>,
hash_value: &Option<String>,
) {
let has_ref = ref_value.as_ref().is_some_and(|v| !v.trim().is_empty());
let has_hash = hash_value.as_ref().is_some_and(|v| !v.trim().is_empty());
if has_ref ^ has_hash {
errors.push(format!("{ref_name} and {hash_name} must be set together"));
}
}
pub fn push_if_blank_option(
errors: &mut ValidationErrors,
field_name: &str,
value: &Option<String>,
) {
if value.as_ref().is_none_or(|v| v.trim().is_empty()) {
errors.push(format!("{field_name} must not be empty"));
}
}
pub fn validate_start_mode(errors: &mut ValidationErrors, value: &str) {
if ProvenanceStartMode::parse(value).is_none() {
errors.push(
"provenance_start_mode must be one of: source_capture, transport_capture, dataset_bootstrap, dataset_incremental"
.to_string(),
);
}
}
pub fn validate_capture_role(errors: &mut ValidationErrors, value: &str) {
if !matches!(value, "source" | "transport") {
errors.push("capture_role must be one of: source, transport".to_string());
}
}
pub fn canonical_signed_bytes<T: serde::Serialize>(value: &T) -> Vec<u8> {
serde_json::to_vec(value).expect("signed envelope serialization should succeed")
}