use crate::{
ast::{Command, CommandType, NestedAttrType, NestedValue, OCAAst, ObjectKind, OverlayContent},
errors::Error,
utils::is_valid_language_code,
};
use indexmap::{IndexMap, IndexSet, indexmap};
use log::debug;
use overlay_file::{ElementType, KeyType};
type CaptureAttributes = IndexMap<String, NestedAttrType>;
pub trait Validator {
fn validate(&self, ast: &OCAAst, command: Command) -> Result<bool, Error>;
}
pub struct OCAValidator {}
impl Validator for OCAValidator {
fn validate(&self, ast: &OCAAst, command: Command) -> Result<bool, Error> {
let mut errors = Vec::new();
let mut valid = true;
match ast.version.as_str() {
"2.0.0" | "1.0.0" => {
let version_validator = validate(ast, command);
if version_validator.is_err() {
valid = false;
errors.push(version_validator.err().unwrap());
}
}
"" => {
valid = false;
errors.push(Error::MissingVersion());
}
_ => {
valid = false;
errors.push(Error::InvalidVersion(ast.version.to_string()));
}
}
if valid {
Ok(true)
} else {
Err(Error::Validation(errors))
}
}
}
fn validate(ast: &OCAAst, command: Command) -> Result<bool, Error> {
let mut valid = true;
let mut errors = Vec::new();
match (&command.kind, &command.object_kind) {
(CommandType::Add, ObjectKind::CaptureBase(_)) => {
match rule_add_attr_if_not_exist(ast, command) {
Ok(result) => {
if !result {
valid = result;
}
}
Err(error) => {
valid = false;
errors.push(error);
}
}
}
(CommandType::Remove, ObjectKind::CaptureBase(_)) => {
match rule_remove_attr_if_exist(ast, command) {
Ok(result) => {
if !result {
valid = result;
}
}
Err(error) => {
valid = false;
errors.push(error);
}
}
}
(CommandType::Add, ObjectKind::Overlay(_)) => {
match validate_against_overlay_def(ast, &command) {
Ok(result) => {
if !result {
valid = result;
}
}
Err(error) => {
valid = false;
errors.push(error);
}
}
}
_ => {
}
}
if valid {
Ok(true)
} else {
Err(Error::Validation(errors))
}
}
fn validate_against_overlay_def(ast: &OCAAst, command: &Command) -> Result<bool, Error> {
let mut errors = Vec::new();
if let ObjectKind::Overlay(overlay_content) = &command.object_kind {
let cb_attrs = extract_attributes(ast);
validate_overlay(overlay_content, &cb_attrs, &mut errors);
}
if errors.is_empty() {
Ok(true)
} else {
Err(Error::Validation(errors))
}
}
fn validate_overlay(
overlay_content: &OverlayContent,
capture_base_attrs: &CaptureAttributes,
errors: &mut Vec<Error>,
) {
let new_properties = IndexMap::new();
let properties = overlay_content
.properties
.as_ref()
.unwrap_or(&new_properties);
let mut found_elements = IndexSet::new();
for (prop_name, prop_value) in properties.iter() {
if let Some(element) = overlay_content
.overlay_def
.elements
.iter()
.find(|e| e.name == *prop_name)
.or_else(|| {
overlay_content
.overlay_def
.elements
.iter()
.find(|e| e.name.is_empty())
})
{
found_elements.insert(prop_name.clone());
match is_valid_property_type(prop_value, &element.values) {
Ok(true) => {
if element.keys == KeyType::AttrNames {
validate_attr_names(prop_value, capture_base_attrs, prop_name, errors);
}
}
Ok(false) => {
errors.push(Error::InvalidPropertyValue(format!(
"Property '{}' in {} has an invalid value type",
prop_name,
overlay_content.overlay_def.get_full_name(),
)));
}
Err(err_msg) => {
errors.push(Error::InvalidPropertyValue(format!(
"Property '{}': {} in {}",
prop_name,
err_msg,
overlay_content.overlay_def.get_full_name(),
)));
}
}
} else {
errors.push(Error::InvalidProperty(format!(
"Property '{}' is not allowed by the overlay definition {}",
prop_name,
overlay_content.overlay_def.get_full_name(),
)));
}
}
for element in &overlay_content.overlay_def.elements {
if !element.name.is_empty() && !found_elements.contains(&element.name) {
errors.push(Error::MissingRequiredAttribute(
element.name.clone(),
overlay_content.overlay_def.get_full_name(),
));
}
}
for key in &overlay_content.overlay_def.unique_keys {
if !properties.contains_key(key) {
errors.push(Error::MissingRequiredAttribute(
key.clone(),
overlay_content.overlay_def.get_full_name(),
));
}
}
}
fn validate_attr_names(
prop_value: &NestedValue,
capture_base_attributes: &CaptureAttributes,
prop_name: &str,
errors: &mut Vec<Error>,
) {
if let NestedValue::Object(attr_names) = prop_value {
for attr_name in attr_names.keys() {
if !capture_base_attributes.contains_key(attr_name) {
errors.push(Error::InvalidProperty(format!(
"Attribute '{}' in '{}' is not present in the capture base",
attr_name, prop_name
)));
}
}
} else {
errors.push(Error::InvalidPropertyValue(format!(
"Property '{}' should be an object for AttrNames type",
prop_name
)));
}
}
fn is_valid_property_type(
value: &NestedValue,
expected_type: &ElementType,
) -> Result<bool, String> {
match (value, expected_type) {
(NestedValue::Array(_), ElementType::Array(_)) => Ok(true),
(NestedValue::Reference(_), ElementType::Ref) => Ok(true),
(NestedValue::Value(_), ElementType::Any) => Ok(true),
(NestedValue::Value(_), ElementType::Text) => Ok(true),
(NestedValue::Value(_), ElementType::Binary) => Ok(true),
(NestedValue::Value(s), ElementType::Lang) => {
if is_valid_language_code(s) {
Ok(true)
} else {
Err(format!("Invalid language code: '{}'", s))
}
}
(NestedValue::Object(object), ElementType::Complex(types)) => {
for (key, v) in object {
let mut any_valid = false;
let mut last_error = String::new();
for t in types {
debug!(
"Checking value {:?} for key '{}' against type {:?}",
v, key, t
);
match is_valid_property_type(v, t) {
Ok(true) => {
any_valid = true;
break;
}
Err(e) => {
last_error = e;
continue;
}
_ => continue,
}
}
if !any_valid {
return Err(format!(
"No valid type found for value {:?} (key: '{}') in complex element: {:?}. Last error: {}",
v, key, types, last_error
));
}
}
Ok(true)
}
(_, ElementType::Complex(types)) => {
let mut any_valid = false;
let mut last_error = String::new();
for t in types {
debug!("Checking value {:?} against type {:?}", value, t);
match is_valid_property_type(value, t) {
Ok(true) => {
any_valid = true;
break;
}
Err(e) => {
last_error = e;
continue;
}
_ => continue,
}
}
if any_valid {
Ok(true)
} else {
Err(format!(
"No valid type found for value {:?} in complex element: {:?}. Last error: {}",
value, types, last_error
))
}
}
(NestedValue::Object(object), ElementType::Object(inner)) => {
if let Some(inner_def) = inner {
for (_, v) in object {
is_valid_property_type(v, &inner_def.values)?;
}
Ok(true)
} else {
Err(format!(
"No valid inner definition of the object {:?} found for complex type",
value
))
}
}
(NestedValue::Object(object), _) => {
for (_, v) in object {
is_valid_property_type(v, expected_type)?;
}
Ok(true)
}
_ => Err(format!(
"Mismatched value type: expected {:?}, got {:?}",
expected_type, value
)),
}
}
fn rule_remove_attr_if_exist(ast: &OCAAst, command_to_validate: Command) -> Result<bool, Error> {
let mut errors = Vec::new();
let attributes = extract_attributes(ast);
let content = command_to_validate.object_kind.capture_content();
match (
content,
content.as_ref().and_then(|c| c.attributes.as_ref()),
) {
(Some(_content), Some(attrs_to_remove)) => {
let valid = attrs_to_remove
.keys()
.all(|key| attributes.contains_key(key));
if !valid {
errors.push(Error::InvalidOperation(
"Cannot remove attribute if does not exists".to_string(),
));
}
}
(None, None) => (),
(None, Some(_)) => (),
(Some(_), None) => (),
}
if errors.is_empty() {
Ok(true)
} else {
Err(Error::Validation(errors))
}
}
fn rule_add_attr_if_not_exist(ast: &OCAAst, command_to_validate: Command) -> Result<bool, Error> {
let mut errors = Vec::new();
let default_attrs: IndexMap<String, NestedAttrType> = indexmap! {};
let attributes = extract_attributes(ast);
let content = command_to_validate.object_kind.capture_content();
match content {
Some(content) => {
let attrs_to_add = content.attributes.clone().unwrap_or(default_attrs);
debug!("attrs_to_add: {:?}", attrs_to_add);
let existing_keys: Vec<_> = attrs_to_add
.keys()
.filter(|key| attributes.contains_key(*key))
.collect();
if !existing_keys.is_empty() {
errors.push(Error::InvalidOperation(format!(
"Cannot add attribute if already exists: {:?}",
existing_keys
)));
Err(Error::Validation(errors))
} else {
Ok(true)
}
}
None => {
errors.push(Error::InvalidOperation(
"No attribtues specify to be added".to_string(),
));
Err(Error::Validation(errors))
}
}
}
fn extract_attributes(ast: &OCAAst) -> CaptureAttributes {
let default_attrs: IndexMap<String, NestedAttrType> = indexmap! {};
let mut attributes: CaptureAttributes = indexmap! {};
for instruction in &ast.commands {
match (instruction.kind.clone(), instruction.object_kind.clone()) {
(CommandType::Remove, ObjectKind::CaptureBase(capture_content)) => {
let attrs = capture_content
.attributes
.as_ref()
.unwrap_or(&default_attrs);
attributes.retain(|key, _value| !attrs.contains_key(key));
}
(CommandType::Add, ObjectKind::CaptureBase(capture_content)) => {
let attrs = capture_content
.attributes
.as_ref()
.unwrap_or(&default_attrs);
attributes.extend(attrs.iter().map(|(k, v)| (k.clone(), v.clone())));
}
_ => {}
}
}
attributes
}
#[cfg(test)]
mod tests {
use indexmap::indexmap;
use overlay_file::KeyType;
use super::*;
use crate::ast::{
AttributeType, CaptureContent, Command, CommandType, OCAAst, ObjectKind, RefValue,
};
#[test]
fn test_rule_remove_if_exist() {
let command = Command {
kind: CommandType::Add,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"name".to_string() => NestedAttrType::Value(AttributeType::Text),
"documentType".to_string() => NestedAttrType::Value(AttributeType::Text),
"photo".to_string() => NestedAttrType::Value(AttributeType::Binary),
}),
}),
};
let command2 = Command {
kind: CommandType::Add,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"issuer".to_string() => NestedAttrType::Value(AttributeType::Text),
"last_name".to_string() => NestedAttrType::Value(AttributeType::Binary),
}),
}),
};
let remove_command = Command {
kind: CommandType::Remove,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"name".to_string() => NestedAttrType::Null,
"documentType".to_string() => NestedAttrType::Null,
}),
}),
};
let add_command = Command {
kind: CommandType::Add,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"name".to_string() => NestedAttrType::Value(AttributeType::Text),
}),
}),
};
let valid_command = Command {
kind: CommandType::Remove,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"name".to_string() => NestedAttrType::Null,
"issuer".to_string() => NestedAttrType::Null,
}),
}),
};
let invalid_command = Command {
kind: CommandType::Remove,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"documentType".to_string() => NestedAttrType::Null,
}),
}),
};
let mut ocaast = OCAAst::new();
ocaast.commands.push(command);
ocaast.commands.push(command2);
ocaast.commands.push(remove_command);
ocaast.commands.push(add_command);
let mut result = rule_remove_attr_if_exist(&ocaast, valid_command.clone());
assert!(result.is_ok());
ocaast.commands.push(invalid_command.clone());
result = rule_remove_attr_if_exist(&ocaast, invalid_command);
assert!(result.is_err());
}
#[test]
fn test_rule_add_if_not_exist() {
let command = Command {
kind: CommandType::Add,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"name".to_string() => NestedAttrType::Value(AttributeType::Text),
"documentType".to_string() => NestedAttrType::Value(AttributeType::Text),
"photo".to_string() => NestedAttrType::Value(AttributeType::Binary),
}),
}),
};
let command2 = Command {
kind: CommandType::Add,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"issuer".to_string() => NestedAttrType::Value(AttributeType::Text),
"last_name".to_string() => NestedAttrType::Value(AttributeType::Binary),
}),
}),
};
let valid_command = Command {
kind: CommandType::Add,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"first_name".to_string() => NestedAttrType::Value(AttributeType::Text),
"address".to_string() => NestedAttrType::Value(AttributeType::Text),
}),
}),
};
let invalid_command = Command {
kind: CommandType::Add,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"name".to_string() => NestedAttrType::Value(AttributeType::Text),
"phone".to_string() => NestedAttrType::Value(AttributeType::Text),
}),
}),
};
let mut ocaast = OCAAst::new();
ocaast.commands.push(command);
ocaast.commands.push(command2);
let mut result = rule_add_attr_if_not_exist(&ocaast, valid_command.clone());
assert!(result.is_ok());
ocaast.commands.push(invalid_command.clone());
result = rule_add_attr_if_not_exist(&ocaast, invalid_command.clone());
assert!(result.is_err());
}
#[test]
fn test_validate_overlay_against_definition() {
use overlay_file::{ElementType, OverlayDef, OverlayElementDef};
let label_overlay_def = OverlayDef {
name: "Label".to_string(),
elements: vec![
OverlayElementDef {
name: "attr_labels".to_string(),
values: ElementType::Text,
keys: KeyType::AttrNames,
},
OverlayElementDef {
name: "language".to_string(),
values: ElementType::Lang,
keys: KeyType::None,
},
],
namespace: Some("hcf".to_string()),
version: "2.0.0".to_string(),
unique_keys: Vec::new(),
};
let meta_overlay_def = OverlayDef {
name: "meta".to_string(),
elements: vec![
OverlayElementDef {
name: "language".to_string(),
values: ElementType::Lang,
keys: KeyType::None,
},
OverlayElementDef {
name: "description".to_string(),
values: ElementType::Text,
keys: KeyType::None,
},
OverlayElementDef {
name: "name".to_string(),
values: ElementType::Text,
keys: KeyType::None,
},
OverlayElementDef {
name: "".to_string(),
values: ElementType::Text,
keys: KeyType::None,
},
],
namespace: Some("hcf".to_string()),
version: "2.0.0".to_string(),
unique_keys: Vec::new(),
};
let entry_overlay_def = OverlayDef {
name: "Entry".to_string(),
elements: vec![
OverlayElementDef {
name: "attribute_entries".to_string(),
values: ElementType::Complex(vec![
ElementType::Ref,
ElementType::Object(Some(Box::new(OverlayElementDef {
name: "".to_string(),
values: ElementType::Any,
keys: KeyType::Text,
}))),
]),
keys: KeyType::AttrNames,
},
OverlayElementDef {
name: "language".to_string(),
values: ElementType::Lang,
keys: KeyType::None,
},
],
namespace: Some("hcf".to_string()),
version: "2.0.0".to_string(),
unique_keys: Vec::new(),
};
let entry_code_overlay_def = OverlayDef {
name: "Entry_Code".to_string(),
elements: vec![OverlayElementDef {
name: "attribute_entry_codes".to_string(),
values: ElementType::Complex(vec![ElementType::Ref, ElementType::Array(None)]),
keys: KeyType::AttrNames,
}],
namespace: Some("hcf".to_string()),
version: "2.0.0".to_string(),
unique_keys: Vec::new(),
};
let capture_base = Command {
kind: CommandType::Add,
object_kind: ObjectKind::CaptureBase(CaptureContent {
attributes: Some(indexmap! {
"first_name".to_string() => NestedAttrType::Value(AttributeType::Text),
"last_name".to_string() => NestedAttrType::Value(AttributeType::Text),
"address".to_string() => NestedAttrType::Value(AttributeType::Text),
"sex".to_string() => NestedAttrType::Value(AttributeType::Text),
}),
}),
};
let mut ocaast = OCAAst::new();
ocaast.commands.push(capture_base.clone());
let valid_overlay = Command {
kind: CommandType::Add,
object_kind: ObjectKind::Overlay(OverlayContent {
properties: Some(indexmap! {
"attr_labels".to_string() => NestedValue::Object(indexmap! {
"first_name".to_string() => NestedValue::Value("First name".to_string()),
"last_name".to_string() => NestedValue::Value("Last name".to_string()),
}),
"language".to_string() => NestedValue::Value("en-UK".to_string()),
}),
overlay_def: label_overlay_def.clone(),
}),
};
match validate_against_overlay_def(&ocaast, &valid_overlay) {
Ok(_) => {} Err(Error::Validation(errors)) => {
panic!(
"Valid overlay should pass validation, got errors: {:?}",
errors
);
}
Err(e) => {
panic!("Unexpected error during validation: {:?}", e);
}
}
ocaast.commands.push(valid_overlay.clone());
let invalid_overlay_missing_field = Command {
kind: CommandType::Add,
object_kind: ObjectKind::Overlay(OverlayContent {
overlay_def: label_overlay_def.clone(),
properties: Some(indexmap! {
"attr_labels".to_string() => NestedValue::Object(indexmap! {
"address".to_string() => NestedValue::Reference(RefValue::Name("passport".to_string())),
}),
"lang".to_string() => NestedValue::Value("pl".to_string()),
}),
}),
};
let result = validate_against_overlay_def(&ocaast, &invalid_overlay_missing_field);
match result {
Ok(is_valid) => assert!(
!is_valid,
"Overlay with missing required field should fail validation"
),
Err(Error::Validation(errors)) => {
assert_eq!(errors.len(), 3);
assert_eq!(
errors[2].to_string(),
"Missing required attribute language in Overlay: hcf:Label/2.0.0"
);
assert_eq!(
errors[0].to_string(),
"Invalid Property Value: Property 'attr_labels': Mismatched value type: expected Text, got Reference(Name(\"passport\")) in hcf:Label/2.0.0"
);
assert_eq!(
errors[1].to_string(),
"Invalid Property: Property 'lang' is not allowed by the overlay definition hcf:Label/2.0.0"
);
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
let meta_overlay = Command {
kind: CommandType::Add,
object_kind: ObjectKind::Overlay(OverlayContent {
overlay_def: meta_overlay_def.clone(),
properties: Some(indexmap! {
"language".to_string() => NestedValue::Value("en-UK".to_string()),
"description".to_string() => NestedValue::Value("Some description".to_string()),
"name".to_string() => NestedValue::Value("Some name".to_string()),
"custom1".to_string() => NestedValue::Value("Custom value 1".to_string()),
"custom2".to_string() => NestedValue::Array(vec![NestedValue::Value("Custom value 2".to_string()), NestedValue::Value("Custom value 3".to_string())]),
}),
}),
};
match validate_against_overlay_def(&ocaast, &meta_overlay) {
Ok(_) => {} Err(Error::Validation(errors)) => {
assert_eq!(
errors[0].to_string(),
"Invalid Property Value: Property 'custom2': Mismatched value type: expected Text, got Array([Value(\"Custom value 2\"), Value(\"Custom value 3\")]) in hcf:meta/2.0.0"
);
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
let entry_code_overlay = Command {
kind: CommandType::Add,
object_kind: ObjectKind::Overlay(OverlayContent {
overlay_def: entry_code_overlay_def.clone(),
properties: Some(indexmap! {
"attribute_entry_codes".to_string() => NestedValue::Object(indexmap! {
"sex".to_string() => NestedValue::Array(vec![NestedValue::Value("Male".to_string()), NestedValue::Value("Female".to_string())]),
"address".to_string() => NestedValue::Reference(RefValue::Name("adres".to_string())),
}),
}),
}),
};
match validate_against_overlay_def(&ocaast, &entry_code_overlay) {
Ok(result) => assert!(result, "Entry code overlay should pass validation"),
Err(e) => panic!("Unexpected error: {:?}", e),
}
let entry_code_overlay = Command {
kind: CommandType::Add,
object_kind: ObjectKind::Overlay(OverlayContent {
overlay_def: entry_code_overlay_def.clone(),
properties: Some(indexmap! {
"attribute_entry_codes".to_string() => NestedValue::Object(indexmap! {
"sex".to_string() => NestedValue::Reference(RefValue::Name("nazwa".to_string())),
}),
}),
}),
};
let result = validate_against_overlay_def(&ocaast, &entry_code_overlay);
match result {
Ok(is_valid) => assert!(is_valid, "Entry code overlay should pass validation"),
Err(e) => panic!("Unexpected error: {:?}", e),
}
let entry_overlay = Command {
kind: CommandType::Add,
object_kind: ObjectKind::Overlay(OverlayContent {
overlay_def: entry_overlay_def.clone(),
properties: Some(indexmap! {
"attribute_entries".to_string() => NestedValue::Object(indexmap! {
"sex".to_string() => NestedValue::Object(indexmap! {
"M".to_string() => NestedValue::Value("male".to_string()),
"F".to_string() => NestedValue::Value("female".to_string()),
}),
}),
"language".to_string() => NestedValue::Value("en-UK".to_string()),
}),
}),
};
let result = validate_against_overlay_def(&ocaast, &entry_overlay);
match result {
Ok(is_valid) => assert!(is_valid, "Entry overlay should pass validation"),
Err(Error::Validation(errors)) => {
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].to_string(),
"Invalid Property Value: Property 'attribute_entries"
);
}
Err(e) => panic!("Unexpected error: {:?}", e),
}
}
}