use hashlink::LinkedHashMap;
use log::{debug, error};
use crate::Error;
use crate::Result;
use crate::Validator;
use crate::YamlSchema;
use crate::schemas::BooleanOrSchema;
use crate::schemas::ObjectSchema;
use crate::utils::{format_marker, format_yaml_data, scalar_to_string};
use crate::validation::Context;
impl Validator for ObjectSchema<'_> {
fn validate(&self, context: &Context, value: &saphyr::MarkedYaml) -> Result<()> {
let data = &value.data;
debug!("Validating object: {}", format_yaml_data(data));
if let saphyr::YamlData::Mapping(mapping) = data {
self.validate_object_mapping(context, value, mapping)
} else {
let error_message = format!(
"[ObjectSchema] {} Expected an object, but got: {data:?}",
format_marker(&value.span.start)
);
error!("{error_message}");
context.add_error(value, error_message);
Ok(())
}
}
}
pub fn try_validate_value_against_properties(
context: &Context,
key: &String,
value: &saphyr::MarkedYaml,
properties: &LinkedHashMap<String, YamlSchema<'_>>,
) -> Result<bool> {
let sub_context = context.append_path(key);
if let Some(schema) = properties.get(key) {
debug!("Validating property '{key}' with schema: {schema}");
let result = schema.validate(&sub_context, value);
return match result {
Ok(_) => Ok(true),
Err(e) => Err(e),
};
}
Ok(false)
}
pub fn try_validate_value_against_additional_properties(
context: &Context,
key: &String,
value: &saphyr::MarkedYaml,
additional_properties: &BooleanOrSchema,
) -> Result<bool> {
let sub_context = context.append_path(key);
match additional_properties {
BooleanOrSchema::Boolean(true) => { }
BooleanOrSchema::Boolean(false) => {
context.add_error(
value,
format!("Additional property '{key}' is not allowed!"),
);
return Ok(false);
}
BooleanOrSchema::Schema(schema) => {
schema.validate(&sub_context, value)?;
}
}
Ok(true)
}
impl ObjectSchema<'_> {
fn validate_object_mapping<'r>(
&self,
context: &Context<'r>,
object: &saphyr::MarkedYaml,
mapping: &saphyr::AnnotatedMapping<'r, saphyr::MarkedYaml<'r>>,
) -> Result<()> {
for (k, value) in mapping {
let key_string = match &k.data {
saphyr::YamlData::Value(scalar) => scalar_to_string(scalar),
v => {
return Err(expected_scalar!(
"[{}] Expected a scalar key, got: {:?}",
format_marker(&k.span.start),
v
));
}
};
let span = &k.span;
debug!("validate_object_mapping: key: \"{key_string}\"");
debug!(
"validate_object_mapping: span.start: {:?}",
format_marker(&span.start)
);
debug!(
"validate_object_mapping: span.end: {:?}",
format_marker(&span.end)
);
if key_string == "$schema" {
continue;
}
if let Some(properties) = &self.properties
&& try_validate_value_against_properties(context, &key_string, value, properties)?
{
continue;
}
let mut matched_pattern_property = false;
if let Some(pattern_properties) = &self.pattern_properties {
for pp in pattern_properties {
log::debug!("pattern: {}", pp.regex.as_str());
if pp.regex.is_match(key_string.as_ref()) {
matched_pattern_property = true;
pp.schema.validate(context, value)?;
}
}
}
if !matched_pattern_property
&& let Some(additional_properties) = &self.additional_properties
{
try_validate_value_against_additional_properties(
context,
&key_string,
value,
additional_properties,
)?;
}
if let Some(property_names) = &self.property_names {
if let Some(re) = &property_names.pattern {
debug!("Regex for property names: {}", re.as_str());
if !re.is_match(key_string.as_ref()) {
context.add_error(
k,
format!(
"Property name '{}' does not match pattern '{}'",
key_string,
re.as_str()
),
);
fail_fast!(context)
}
} else {
return Err(Error::GenericError(
"Expected a pattern for `property_names`".to_string(),
));
}
}
}
if let Some(required) = &self.required {
for required_property in required {
if !mapping
.keys()
.filter_map(|k| k.data.as_str())
.any(|s| s == required_property)
{
context.add_error(
object,
format!("Required property '{required_property}' is missing!"),
);
fail_fast!(context)
}
}
}
if let Some(min_properties) = &self.min_properties
&& mapping.len() < *min_properties
{
context.add_error(
object,
format!("Object has too few properties! Minimum is {min_properties}!"),
);
fail_fast!(context)
}
if let Some(max_properties) = &self.max_properties
&& mapping.len() > *max_properties
{
context.add_error(
object,
format!("Object has too many properties! Maximum is {max_properties}!"),
);
fail_fast!(context)
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::RootSchema;
use crate::YamlSchema;
use crate::engine;
use crate::schemas::NumberSchema;
use crate::schemas::StringSchema;
use hashlink::LinkedHashMap;
use super::*;
#[test]
fn test_should_validate_properties() {
let mut properties = LinkedHashMap::new();
properties.insert(
"foo".to_string(),
YamlSchema::typed_string(StringSchema::default()),
);
properties.insert(
"bar".to_string(),
YamlSchema::typed_number(NumberSchema::default()),
);
let object_schema = ObjectSchema {
properties: Some(properties),
..Default::default()
};
let root_schema = RootSchema::new(YamlSchema::typed_object(object_schema));
let value = r#"
foo: "I'm a string"
bar: 42
"#;
let result = engine::Engine::evaluate(&root_schema, value, true);
assert!(result.is_ok());
let value2 = r#"
foo: 42
baz: "I'm a string"
"#;
let context = engine::Engine::evaluate(&root_schema, value2, true).unwrap();
assert!(context.has_errors());
let errors = context.errors.borrow();
let first_error = errors.first().unwrap();
assert_eq!(first_error.path, "foo");
assert_eq!(
first_error.error,
"Expected a string, but got: Value(Integer(42))"
);
}
}