use std::collections::{BTreeMap, BTreeSet};
use lookup::OwnedTargetPath;
use vrl::value::Kind;
use crate::config::LogNamespace;
use super::Definition;
#[derive(Debug, Clone, PartialEq)]
pub struct Requirement {
meaning: BTreeMap<String, SemanticMeaning>,
}
#[derive(Debug, Clone, PartialEq)]
struct SemanticMeaning {
kind: Kind,
optional: bool,
}
impl Requirement {
pub fn empty() -> Self {
Self {
meaning: BTreeMap::default(),
}
}
pub fn is_empty(&self) -> bool {
self.meaning.is_empty()
}
#[must_use]
pub fn required_meaning(mut self, meaning: impl Into<String>, kind: Kind) -> Self {
self.insert_meaning(meaning, kind, false);
self
}
#[must_use]
pub fn optional_meaning(mut self, meaning: impl Into<String>, kind: Kind) -> Self {
self.insert_meaning(meaning, kind, true);
self
}
fn insert_meaning(&mut self, identifier: impl Into<String>, kind: Kind, optional: bool) {
let meaning = SemanticMeaning { kind, optional };
self.meaning.insert(identifier.into(), meaning);
}
pub fn validate(
&self,
definition: &Definition,
validate_schema_type: bool,
) -> Result<(), ValidationErrors> {
let mut errors = vec![];
if !definition.log_namespaces().contains(&LogNamespace::Vector) {
return Ok(());
}
for (identifier, req_meaning) in &self.meaning {
if let Some(paths) = definition.invalid_meaning(identifier).cloned() {
errors.push(ValidationError::MeaningDuplicate {
identifier: identifier.clone(),
paths,
});
continue;
}
let maybe_meaning_path = definition.meanings().find_map(|(def_id, path)| {
if def_id == identifier {
Some(path)
} else {
None
}
});
match maybe_meaning_path {
Some(target_path) if validate_schema_type => {
let definition_kind = definition.kind_at(target_path);
if req_meaning.kind.is_superset(&definition_kind).is_err() {
errors.push(ValidationError::MeaningKind {
identifier: identifier.clone(),
want: req_meaning.kind.clone(),
got: definition_kind,
});
}
}
None if !req_meaning.optional => {
errors.push(ValidationError::MeaningMissing {
identifier: identifier.clone(),
});
}
_ => {}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(ValidationErrors(errors))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationErrors(Vec<ValidationError>);
impl ValidationErrors {
pub fn is_meaning_missing(&self) -> bool {
self.0.iter().any(ValidationError::is_meaning_missing)
}
pub fn is_meaning_kind(&self) -> bool {
self.0.iter().any(ValidationError::is_meaning_kind)
}
pub fn errors(&self) -> &[ValidationError] {
&self.0
}
}
impl std::error::Error for ValidationErrors {
fn source(&self) -> Option<&(dyn snafu::Error + 'static)> {
Some(&self.0[0])
}
}
impl std::fmt::Display for ValidationErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for error in &self.0 {
error.fmt(f)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(clippy::enum_variant_names)]
pub enum ValidationError {
MeaningMissing { identifier: String },
MeaningKind {
identifier: String,
want: Kind,
got: Kind,
},
MeaningDuplicate {
identifier: String,
paths: BTreeSet<OwnedTargetPath>,
},
}
impl ValidationError {
pub fn is_meaning_missing(&self) -> bool {
matches!(self, Self::MeaningMissing { .. })
}
pub fn is_meaning_kind(&self) -> bool {
matches!(self, Self::MeaningKind { .. })
}
pub fn is_meaning_duplicate(&self) -> bool {
matches!(self, Self::MeaningDuplicate { .. })
}
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MeaningMissing { identifier } => {
write!(f, "missing semantic meaning: {identifier}")
}
Self::MeaningKind {
identifier,
want,
got,
} => write!(
f,
"invalid semantic meaning: {identifier} (expected {want}, got {got})"
),
Self::MeaningDuplicate { identifier, paths } => write!(
f,
"semantic meaning {} pointing to multiple fields: {}",
identifier,
paths
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
),
}
}
}
impl std::error::Error for ValidationError {}
#[cfg(test)]
mod tests {
use lookup::lookup_v2::parse_target_path;
use lookup::owned_value_path;
use std::collections::HashMap;
use super::*;
#[test]
fn test_doesnt_validate_types() {
let requirement = Requirement::empty().required_meaning("foo", Kind::boolean());
let definition = Definition::default_for_namespace(&[LogNamespace::Vector].into())
.with_event_field(&owned_value_path!("foo"), Kind::integer(), Some("foo"));
assert_eq!(Ok(()), requirement.validate(&definition, false));
}
#[test]
fn test_doesnt_validate_legacy_namespace() {
let requirement = Requirement::empty().required_meaning("foo", Kind::boolean());
let definition =
Definition::default_for_namespace(&[LogNamespace::Vector, LogNamespace::Legacy].into())
.with_event_field(&owned_value_path!("foo"), Kind::integer(), Some("foo"));
assert_ne!(Ok(()), requirement.validate(&definition, true));
let definition = Definition::default_for_namespace(&[LogNamespace::Legacy].into())
.with_event_field(&owned_value_path!("foo"), Kind::integer(), Some("foo"));
assert_eq!(Ok(()), requirement.validate(&definition, true));
}
#[test]
#[allow(clippy::too_many_lines)]
fn test_validate() {
struct TestCase {
requirement: Requirement,
definition: Definition,
errors: Vec<ValidationError>,
}
for (
title,
TestCase {
requirement,
definition,
errors,
},
) in HashMap::from([
(
"empty",
TestCase {
requirement: Requirement::empty(),
definition: Definition::default_for_namespace(&[LogNamespace::Vector].into()),
errors: vec![],
},
),
(
"missing required meaning",
TestCase {
requirement: Requirement::empty().required_meaning("foo", Kind::any()),
definition: Definition::default_for_namespace(&[LogNamespace::Vector].into()),
errors: vec![ValidationError::MeaningMissing {
identifier: "foo".into(),
}],
},
),
(
"missing required meanings",
TestCase {
requirement: Requirement::empty()
.required_meaning("foo", Kind::any())
.required_meaning("bar", Kind::any()),
definition: Definition::default_for_namespace(&[LogNamespace::Vector].into()),
errors: vec![
ValidationError::MeaningMissing {
identifier: "bar".into(),
},
ValidationError::MeaningMissing {
identifier: "foo".into(),
},
],
},
),
(
"missing optional meaning",
TestCase {
requirement: Requirement::empty().optional_meaning("foo", Kind::any()),
definition: Definition::default_for_namespace(&[LogNamespace::Vector].into()),
errors: vec![],
},
),
(
"missing mixed meanings",
TestCase {
requirement: Requirement::empty()
.optional_meaning("foo", Kind::any())
.required_meaning("bar", Kind::any()),
definition: Definition::default_for_namespace(&[LogNamespace::Vector].into()),
errors: vec![ValidationError::MeaningMissing {
identifier: "bar".into(),
}],
},
),
(
"invalid required meaning kind",
TestCase {
requirement: Requirement::empty().required_meaning("foo", Kind::boolean()),
definition: Definition::default_for_namespace(&[LogNamespace::Vector].into())
.with_event_field(&owned_value_path!("foo"), Kind::integer(), Some("foo")),
errors: vec![ValidationError::MeaningKind {
identifier: "foo".into(),
want: Kind::boolean(),
got: Kind::integer(),
}],
},
),
(
"invalid optional meaning kind",
TestCase {
requirement: Requirement::empty().optional_meaning("foo", Kind::boolean()),
definition: Definition::default_for_namespace(&[LogNamespace::Vector].into())
.with_event_field(&owned_value_path!("foo"), Kind::integer(), Some("foo")),
errors: vec![ValidationError::MeaningKind {
identifier: "foo".into(),
want: Kind::boolean(),
got: Kind::integer(),
}],
},
),
(
"duplicate meaning pointers",
TestCase {
requirement: Requirement::empty().optional_meaning("foo", Kind::boolean()),
definition: Definition::default_for_namespace(&[LogNamespace::Vector].into())
.with_event_field(&owned_value_path!("foo"), Kind::integer(), Some("foo"))
.merge(
Definition::default_for_namespace(&[LogNamespace::Vector].into())
.with_event_field(
&owned_value_path!("bar"),
Kind::boolean(),
Some("foo"),
),
),
errors: vec![ValidationError::MeaningDuplicate {
identifier: "foo".into(),
paths: BTreeSet::from([
parse_target_path("foo").unwrap(),
parse_target_path("bar").unwrap(),
]),
}],
},
),
]) {
let got = requirement.validate(&definition, true);
let want = if errors.is_empty() {
Ok(())
} else {
Err(ValidationErrors(errors))
};
assert_eq!(got, want, "{title}");
}
}
}