Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pyavd_utils/validation.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,20 @@ class Deprecation:
url: str | None
"""Url where more information can be found."""

class IgnoredEosConfigKey:
"""EOS CLI Config Gen key found in EOS Designs input."""

message: str
"""String detailing the ignored key."""
key: str
"""The top-level key name that was ignored."""

class ValidationResult:
"""Result of data validation."""

violations: list[Violation]
deprecations: list[Deprecation]
ignored_eos_config_keys: list[IgnoredEosConfigKey]

class ValidatedDataResult:
"""Result of data validation including the validated data as JSON."""
Expand Down
115 changes: 111 additions & 4 deletions rust/avdschema/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::path::PathBuf;

use crate::{
resolve_schema,
schema::any::AnySchema,
schema::{any::AnySchema, dict::Dict},
utils::{
dump::Dump,
load::{Load, LoadError},
Expand All @@ -27,6 +27,17 @@ impl Store {
Schema::EosCliConfigGen => &self.eos_cli_config_gen,
}
}

/// Get the top-level keys for a schema.
/// Returns `None` if the schema is not a Dict or has no keys defined.
pub fn get_keys(&self, schema: Schema) -> Option<&ordermap::OrderMap<String, AnySchema>> {
let schema_any = self.get(schema);
if let Ok(dict) = TryInto::<&Dict>::try_into(schema_any) {
dict.keys.as_ref()
} else {
None
}
}
pub fn as_resolved(mut self) -> Self {
// Extract copies of each schema so we can resolve them.
let mut eos_cli_config_gen_schema = self.eos_cli_config_gen.to_owned();
Expand Down Expand Up @@ -62,7 +73,7 @@ impl Store {
impl Dump for Store {}
impl Load for Store {}

#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Schema {
EosDesigns,
EosCliConfigGen,
Expand Down Expand Up @@ -104,12 +115,22 @@ pub struct SchemaName {
#[cfg(test)]
mod tests {

use super::Load;
use super::Schema;

use crate::utils::test_utils::get_test_store;
use crate::Store;
use serde::Deserialize as _;
use serde_json::json;

#[cfg(feature = "dump_load_files")]
use super::Load;
#[cfg(feature = "dump_load_files")]
use crate::utils::test_utils::{get_avd_store, get_tmp_file};
use crate::{Dump as _, Store};
#[cfg(feature = "dump_load_files")]
use crate::Dump as _;

#[test]
#[cfg(feature = "dump_load_files")]
fn dump_avd_store() {
// Dumping uncompressed and compressed schema.
let store = get_avd_store();
Expand All @@ -132,6 +153,7 @@ mod tests {
}

#[test]
#[cfg(feature = "dump_load_files")]
fn load_avd_store() {
dump_avd_store();
let store = get_avd_store();
Expand All @@ -157,6 +179,7 @@ mod tests {
}

#[test]
#[cfg(feature = "dump_load_files")]
#[ignore = "Test only used for manual performance testing"]
fn quick_load_avd_store_json() {
//Depends on dump to be done before. This is just here to test the speed of loading from the file.
Expand All @@ -166,6 +189,7 @@ mod tests {
}

#[test]
#[cfg(feature = "dump_load_files")]
#[ignore = "Test only used for manual performance testing"]
fn quick_load_avd_store_gz() {
//Depends on dump to be done before. This is just here to test the speed of loading from the file.
Expand All @@ -175,11 +199,94 @@ mod tests {
}

#[test]
#[cfg(feature = "dump_load_files")]
#[ignore = "Test only used for manual performance testing"]
fn quick_load_avd_store_xz2() {
//Depends on dump to be done before. This is just here to test the speed of loading from the file.
let file_path = get_tmp_file("test_dump_avd_store_resolved.xz2");
let result = Store::from_file(Some(&file_path));
assert!(result.is_ok());
}

#[test]
fn get_keys_eos_designs() {
// Test that get_keys returns the correct keys for eos_designs schema
let store = get_test_store();
let keys = store.get_keys(Schema::EosDesigns);

assert!(keys.is_some());
let keys = keys.unwrap();

// The test store has key3 in eos_designs
assert!(keys.contains_key("key3"));
assert_eq!(keys.len(), 1);
}

#[test]
fn get_keys_eos_cli_config_gen() {
// Test that get_keys returns the correct keys for eos_cli_config_gen schema
let store = get_test_store();
let keys = store.get_keys(Schema::EosCliConfigGen);

assert!(keys.is_some());
let keys = keys.unwrap();

// The test store has key1 and key2 in eos_cli_config_gen
assert!(keys.contains_key("key1"));
assert!(keys.contains_key("key2"));
assert_eq!(keys.len(), 2);
}

#[test]
fn get_keys_non_dict_schema() {
// Test that get_keys returns None when the schema is not a Dict
use crate::any::AnySchema;

let store = Store {
eos_designs: AnySchema::deserialize(json!({
"type": "str", // Not a dict!
"description": "This is a string schema, not a dict"
}))
.unwrap(),
eos_cli_config_gen: AnySchema::deserialize(json!({
"type": "int", // Not a dict!
"min": 0,
"max": 100
}))
.unwrap(),
};

// Both should return None since they're not Dict schemas
assert!(store.get_keys(Schema::EosDesigns).is_none());
assert!(store.get_keys(Schema::EosCliConfigGen).is_none());
}

#[test]
fn get_keys_dict_without_keys() {
// Test that get_keys returns None when the Dict has no keys defined
use crate::any::AnySchema;

let store = Store {
eos_designs: AnySchema::deserialize(json!({
"type": "dict",
"allow_other_keys": true
// No "keys" field defined
}))
.unwrap(),
eos_cli_config_gen: AnySchema::deserialize(json!({
"type": "dict",
"dynamic_keys": {
"some.path": {
"type": "str"
}
}
// No "keys" field defined
}))
.unwrap(),
};

// Both should return None since they have no keys defined
assert!(store.get_keys(Schema::EosDesigns).is_none());
assert!(store.get_keys(Schema::EosCliConfigGen).is_none());
}
}
79 changes: 77 additions & 2 deletions rust/pyvalidation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ static STORE: OnceLock<Store> = OnceLock::new();
#[pymodule(gil_used = false)]
pub mod validation {
use super::STORE;
use avdschema::{Load as _, Store, any::AnySchema};
use avdschema::{Load as _, Schema, Store, any::AnySchema};
use log::{debug, info};
use pyo3::{Bound, PyResult, exceptions::PyRuntimeError, pyclass, pyfunction, types::PyModule};
use std::path::PathBuf;
Expand Down Expand Up @@ -52,11 +52,19 @@ pub mod validation {
pub url: Option<String>,
}

#[pyclass(frozen, get_all)]
#[derive(Clone)]
pub struct IgnoredEosConfigKey {
pub message: String,
pub key: String,
}

#[pyclass(frozen, get_all)]
#[derive(Clone, Default)]
pub struct ValidationResult {
pub violations: Vec<Violation>,
pub deprecations: Vec<Deprecation>,
pub ignored_eos_config_keys: Vec<IgnoredEosConfigKey>,
}
impl TryFrom<validation::ValidationResult> for ValidationResult {
type Error = pyo3::PyErr;
Expand Down Expand Up @@ -93,6 +101,12 @@ pub mod validation {
url: deprecated.url.to_owned().into(),
})
}
validation::feedback::WarningIssue::IgnoredEosConfigKey(ignored) => {
result.ignored_eos_config_keys.push(IgnoredEosConfigKey {
message: ignored.to_string(),
key: ignored.key.to_owned(),
})
}
});
Ok(result)
}
Expand Down Expand Up @@ -187,7 +201,7 @@ pub mod validation {
let mut data: serde_json::Value = serde_json::from_str(data_as_json)
.map_err(|err| PyRuntimeError::new_err(format!("Invalid JSON in data: {err}")))?;

let mut ctx = Context::new(get_store()?, None);
let mut ctx = Context::new(get_store()?, None, Schema::EosDesigns);
schema.coerce(&mut data, &mut ctx);
schema.validate_value(&data, &mut ctx);

Expand Down Expand Up @@ -506,4 +520,65 @@ mod tests {
}
});
}

#[test]
fn validate_eos_designs_with_ignored_eos_config_key() {
setup();
pyo3::Python::attach(|py| {
let module = py.import("validation").unwrap();
// router_isis is a key from eos_cli_config_gen that should be ignored when validating eos_designs
let data_as_json_str =
serde_json::json!({"fabric_name": "TEST-FABRIC", "router_isis": {"instance": "ISIS_TEST"}}).to_string();
let get_validated_data_result = {
let args = ();
let kwargs = pyo3::types::PyDict::new(py);
kwargs.set_item("data_as_json", data_as_json_str).unwrap();
kwargs.set_item("schema_name", "eos_designs").unwrap();
module
.call_method("get_validated_data", args, Some(&kwargs))
.unwrap()
};
let validation_result = get_validated_data_result
.getattr("validation_result")
.unwrap();

// Should have no violations
let violations = validation_result.getattr("violations").unwrap();
assert!(violations.is_instance_of::<pyo3::types::PyList>());
assert_eq!(violations.len().unwrap(), 0);

// Should have no deprecations
let deprecations = validation_result.getattr("deprecations").unwrap();
assert!(deprecations.is_instance_of::<pyo3::types::PyList>());
assert_eq!(deprecations.len().unwrap(), 0);

// Should have one ignored_eos_config_key
let ignored_keys = validation_result
.getattr("ignored_eos_config_keys")
.unwrap();
assert!(ignored_keys.is_instance_of::<pyo3::types::PyList>());
assert_eq!(ignored_keys.len().unwrap(), 1);

// Check the ignored key details
let ignored_key = ignored_keys.get_item(0).unwrap();
let key = ignored_key
.getattr("key")
.unwrap()
.cast_into_exact::<pyo3::types::PyString>()
.unwrap()
.to_string();
assert_eq!(key, "router_isis");

let message = ignored_key
.getattr("message")
.unwrap()
.cast_into_exact::<pyo3::types::PyString>()
.unwrap()
.to_string();
assert_eq!(
message,
"The 'eos_cli_config_gen' key 'router_isis' is present in the input to 'eos_designs' and will be ignored."
);
});
}
}
3 changes: 2 additions & 1 deletion rust/validation/src/coercion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ impl Coercion for AnySchema {
#[cfg(test)]
mod tests {
use avdschema::base::Base;
use avdschema::Schema;
use ordermap::OrderMap;

use super::*;
Expand Down Expand Up @@ -262,7 +263,7 @@ mod tests {
return_coercion_infos: true,
..Default::default()
};
let mut ctx = Context::new(&store, Some(&configuration));
let mut ctx = Context::new(&store, Some(&configuration), Schema::EosDesigns);
schema.coerce(&mut input, &mut ctx);
schema.validate_value(&input, &mut ctx);
assert!(ctx.result.errors.is_empty());
Expand Down
16 changes: 13 additions & 3 deletions rust/validation/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by the Apache License 2.0
// that can be found in the LICENSE file.

use avdschema::Store;
use avdschema::{Schema, Store};

use crate::feedback::{ErrorIssue, Feedback, InfoIssue, Path, Violation, WarningIssue};

Expand All @@ -18,12 +18,19 @@ pub struct Context<'a> {
}

impl<'a> Context<'a> {
pub fn new(store: &'a Store, configuration: Option<&'a Configuration>) -> Self {
pub fn new(
store: &'a Store,
configuration: Option<&'a Configuration>,
schema_name: Schema,
) -> Self {
Self {
configuration: configuration.cloned().unwrap_or_default(),
store,
result: Default::default(),
state: Default::default(),
state: State {
schema_name: Some(schema_name),
..Default::default()
},
}
}
pub(crate) fn add_error(&mut self, error: impl Into<ErrorIssue>) {
Expand Down Expand Up @@ -77,6 +84,9 @@ pub(crate) struct State {
/// Used for structured_config where we overload other config, and only the final result should be validated for required keys.
pub(crate) relaxed_validation: bool,
pub(crate) path: Path,
/// The schema being validated (EosDesigns or EosCliConfigGen).
/// Used to determine if we should check for eos_cli_config_gen keys in eos_designs input.
pub(crate) schema_name: Option<Schema>,
}

/// Configuration to use during validation.
Expand Down
10 changes: 10 additions & 0 deletions rust/validation/src/feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ pub enum ErrorIssue {
pub enum WarningIssue {
/// Deprecation of data model.
Deprecated(Deprecated),
/// Ignore EOSConfig keys
IgnoredEosConfigKey(IgnoredEosConfigKey),
}

/// InfoIssue is wrapped in Feedback and added to the Context during coercion and validation.
Expand Down Expand Up @@ -331,6 +333,14 @@ impl Removed {
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, derive_more::Display)]
#[display(
"The 'eos_cli_config_gen' key '{key}' is present in the input to 'eos_designs' and will be ignored."
)]
pub struct IgnoredEosConfigKey {
/// The top-level key name that was ignored.
pub key: String,
}

#[cfg(test)]
mod tests {
Expand Down
Loading
Loading