use crate::AVAILABLE_REGISTERED_NAMESPACES;
use crate::assets::{DECISION_POINTS, DP_VALUE_KEY_ORDER, DecisionPointId};
use crate::namespaces::validate_namespace;
use crate::selection_list::SelectionList;
use std::ops::Deref;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ValidationResult {
pub success: bool,
pub errors: Vec<ValidationError>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ValidationError {
pub message: String,
#[serde(rename = "instancePath")]
pub instance_path: Vec<String>,
}
#[allow(clippy::too_many_arguments)]
fn validate_field_match<S, B>(
selection_field: &Option<S>,
base_field: &B,
field_name: &str,
ext_namespace: &str,
base_namespace: &str,
context: &str,
i_s: usize,
path_suffix: &[&str],
) -> Option<ValidationError>
where
S: Deref<Target = String>,
B: Deref<Target = String>,
{
if let Some(sel_field) = selection_field
&& sel_field.deref() != base_field.deref()
{
let mut instance_path = vec!["selections".to_string(), i_s.to_string()];
instance_path.extend(path_suffix.iter().map(|s| s.to_string()));
instance_path.push(field_name.to_string());
return Some(ValidationError {
message: format!(
"Extension namespace '{}' {} {} '{}' does not match base namespace '{}' {} '{}'",
ext_namespace,
context,
field_name,
sel_field.deref(),
base_namespace,
field_name,
base_field.deref()
),
instance_path,
});
}
None
}
pub fn validate_selection_list(
selection_list: &SelectionList,
allow_test_namespaces: bool,
) -> ValidationResult {
let mut errors: Vec<ValidationError> = Vec::new();
for (i_s, selection) in selection_list.selections.iter().enumerate() {
let parsed_ns = match validate_namespace(selection.namespace.deref(), allow_test_namespaces)
{
Ok(ns) => ns,
Err(err) => {
errors.push(ValidationError {
message: format!("Invalid SSVC namespace: {}", err),
instance_path: vec![
"selections".to_string(),
i_s.to_string(),
"namespace".to_string(),
],
});
continue;
}
};
if parsed_ns.is_unregistered() {
continue;
}
if !AVAILABLE_REGISTERED_NAMESPACES.contains(&parsed_ns.base) {
continue;
}
let parsed_ns_str = &parsed_ns.base.to_string();
let s_key = selection.key.deref().to_owned();
let version = selection.version.deref().to_owned();
let dp_key = DecisionPointId {
namespace: parsed_ns.base,
key: s_key.clone(),
version: version.clone(),
};
match DECISION_POINTS.get(&dp_key) {
Some(dp) => {
let reference_indices = DP_VALUE_KEY_ORDER.get(&dp_key).expect("As DP_VALUE_KEY_ORDER is derived from DECISION_POINTS, all DECISION_POINTS keys should also exist in DP_VALUE_KEY_ORDER");
if parsed_ns.extensions.is_none() {
if let Some(error) = validate_field_match(
&selection.name,
&dp.name,
"name",
selection.namespace.deref(),
parsed_ns_str.as_str(),
"decision point",
i_s,
&[],
) {
errors.push(error);
}
if let Some(error) = validate_field_match(
&selection.definition,
&dp.definition,
"definition",
selection.namespace.deref(),
parsed_ns_str.as_str(),
"decision point",
i_s,
&[],
) {
errors.push(error);
}
}
let mut last_index: i32 = -1;
for (i_val, sel_val) in selection.values.iter().enumerate() {
let v_key = sel_val.key.deref();
match reference_indices.get(v_key) {
None => {
errors.push(ValidationError {
message: format!(
"The SSVC decision point '{}::{}' (version {}) doesn't have a value with key '{}'",
selection.namespace.deref(),
dp.name.deref(),
version,
v_key
),
instance_path: vec![
"selections".to_string(),
i_s.to_string(),
"values".to_string(),
i_val.to_string(),
],
});
continue;
}
Some(i_dp_val) => {
if last_index > *i_dp_val {
errors.push(ValidationError {
message: format!(
"The values for SSVC decision point '{}::{}' (version {}) are not in correct order",
selection.namespace.deref(),
dp.name.deref(),
version
),
instance_path: vec![
"selections".to_string(),
i_s.to_string(),
"values".to_string(),
i_val.to_string(),
],
});
continue;
}
last_index = *i_dp_val;
if parsed_ns.extensions.is_none() {
let base_val = &dp.values[*i_dp_val as usize];
let context = format!("value '{}'", v_key);
if let Some(error) = validate_field_match(
&sel_val.name,
&base_val.name,
"name",
selection.namespace.deref(),
parsed_ns_str.as_str(),
&context,
i_s,
&["values", &i_val.to_string()],
) {
errors.push(error);
}
if let Some(error) = validate_field_match(
&sel_val.definition,
&base_val.definition,
"definition",
selection.namespace.deref(),
parsed_ns_str.as_str(),
&context,
i_s,
&["values", &i_val.to_string()],
) {
errors.push(error);
}
}
}
}
}
}
None => {
errors.push(ValidationError {
message: format!(
"Unknown SSVC decision point '{}::{}' with version '{}'",
selection.namespace.deref(),
s_key,
version
),
instance_path: vec!["selections".to_string(), i_s.to_string()],
});
continue;
}
}
}
ValidationResult {
success: errors.is_empty(),
errors,
}
}
#[cfg(test)]
mod tests {
use super::ValidationError;
#[test]
fn validation_error_is_serde_serializable() {
let error = ValidationError {
message: "example".to_string(),
instance_path: vec!["selections".to_string(), "0".to_string()],
};
let json = serde_json::to_value(&error).expect("serialize ValidationError");
assert_eq!(
json.get("message").and_then(|v| v.as_str()),
Some("example")
);
assert_eq!(
json.get("instancePath")
.and_then(|v| v.as_array())
.map(|a| a.len()),
Some(2)
);
let roundtrip: ValidationError =
serde_json::from_value(json).expect("deserialize ValidationError");
assert_eq!(roundtrip, error);
}
}