diff --git a/Cargo.lock b/Cargo.lock index a09235b..57b77a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,8 @@ dependencies = [ "dotenv", "futures-core", "log", + "regex", + "serde_json", "tonic", "tonic-health", "tucana", @@ -1155,9 +1157,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -1362,9 +1364,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 9d56440..2bebcc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ code0-definition-reader = "0.0.10" tonic-health = "0.14.1" async-nats = "0.42.0" futures-core = "0.3.31" +regex = "1.11.2" +serde_json = "1.0.143" [lib] doctest = true @@ -26,4 +28,5 @@ default = ["all"] flow_definition = [] flow_config = [] flow_health = [] -all = ["flow_definition", "flow_config", "flow_health"] +flow_validator = [] +all = ["flow_definition", "flow_config", "flow_health", "flow_validator"] diff --git a/src/flow_validator/mod.rs b/src/flow_validator/mod.rs new file mode 100644 index 0000000..e1db221 --- /dev/null +++ b/src/flow_validator/mod.rs @@ -0,0 +1,118 @@ +mod rule; + +use rule::{ + contains_key::apply_contains_key, + contains_type::apply_contains_type, + item_of_collection::apply_item_of_collection, + number_range::apply_number_range, + regex::apply_regex, + violation::{DataTypeNotFoundRuleViolation, DataTypeRuleError, DataTypeRuleViolation}, +}; + +use tucana::shared::{ExecutionDataType, ValidationFlow, Value, execution_data_type_rule::Config}; +pub struct VerificationResult; + +pub fn verify_flow(flow: ValidationFlow, body: Value) -> Result<(), DataTypeRuleError> { + let input_type = match &flow.input_type_identifier { + Some(r) => r.clone(), + None => return Ok(()), //Returns directly because no rule is given. The body is ok and will not be concidered + }; + + let data_type = match flow + .data_types + .iter() + .find(|dt| dt.identifier == input_type) + { + Some(dt) => dt.clone(), + None => { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::DataTypeNotFound( + DataTypeNotFoundRuleViolation { + data_type: input_type, + }, + )], + }); + } + }; + + verify_data_type_rules(body, data_type, &flow.data_types) +} + +//Verifies the rules on the datatype of the body thats given +fn verify_data_type_rules( + body: Value, + data_type: ExecutionDataType, + availabe_data_types: &Vec, +) -> Result<(), DataTypeRuleError> { + let mut violations: Vec = Vec::new(); + for rule in data_type.rules { + let rule_config = match rule.config { + None => continue, + Some(config) => config, + }; + + match rule_config { + Config::NumberRange(config) => { + match apply_number_range(config, &body, &String::from("value")) { + Ok(_) => continue, + Err(violation) => { + violations.extend(violation.violations); + continue; + } + } + } + Config::ItemOfCollection(config) => { + match apply_item_of_collection(config, &body, "key") { + Ok(_) => continue, + Err(violation) => { + violations.extend(violation.violations); + continue; + } + } + } + Config::ContainsType(config) => { + match apply_contains_type(config, &availabe_data_types, &body) { + Ok(_) => continue, + Err(violation) => { + violations.extend(violation.violations); + continue; + } + } + } + Config::Regex(config) => { + match apply_regex(config, &body) { + Ok(_) => continue, + Err(violation) => { + violations.extend(violation.violations); + continue; + } + }; + } + Config::ContainsKey(config) => { + match apply_contains_key(config, &body, &availabe_data_types) { + Ok(_) => continue, + Err(violation) => { + violations.extend(violation.violations); + continue; + } + }; + } + } + } + + if violations.is_empty() { + Ok(()) + } else { + Err(DataTypeRuleError { violations }) + } +} + +fn get_data_type_by_id( + data_types: &Vec, + identifier: &String, +) -> Option { + data_types + .iter() + .find(|data_type| &data_type.identifier == identifier) + .cloned() +} diff --git a/src/flow_validator/rule/contains_key.rs b/src/flow_validator/rule/contains_key.rs new file mode 100644 index 0000000..f95538b --- /dev/null +++ b/src/flow_validator/rule/contains_key.rs @@ -0,0 +1,72 @@ +use super::violation::ContainsKeyRuleViolation; +use super::violation::DataTypeRuleError; +use super::violation::DataTypeRuleViolation; +use super::violation::MissingDataTypeRuleDefinition; +use tucana::shared::ExecutionDataType; +use tucana::shared::ExecutionDataTypeContainsKeyRuleConfig; +use tucana::shared::Value; +use tucana::shared::helper::path::expect_kind; +use tucana::shared::value::Kind; +use crate::flow_validator::{get_data_type_by_id, verify_data_type_rules}; + +/// # Data Type Validation Behavior +/// +/// This function checks if a specific key exists in the JSON body and validates +/// if its value matches the expected data type. +/// +/// ## Process: +/// 1. Searches for the specified key in the JSON body +/// 2. If the key is found, retrieves the associated data type definition from the flow +/// 3. Validates that the value matches the expected data type +/// +/// ## Error Handling: +/// - Returns a `ContainsKeyRuleViolation` if the specified key is not found in the body +/// - Returns a `MissingDataTypeRuleDefinition` if the referenced data type doesn't exist +/// - Returns validation errors if the value doesn't match the expected data type +pub fn apply_contains_key( + rule: ExecutionDataTypeContainsKeyRuleConfig, + body: &Value, + available_data_types: &Vec, +) -> Result<(), DataTypeRuleError> { + let identifier = rule.data_type_identifier; + + if let Some(Kind::StructValue(_)) = &body.kind { + let value = match expect_kind(&identifier, &body) { + Some(value) => Value { + kind: Some(value.to_owned()), + }, + None => { + let error = ContainsKeyRuleViolation { + missing_key: identifier, + }; + + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::ContainsKey(error)], + }); + } + }; + + let data_type = match get_data_type_by_id(&available_data_types, &identifier) { + Some(data_type) => data_type, + None => { + let error = MissingDataTypeRuleDefinition { + missing_type: identifier, + }; + + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::MissingDataType(error)], + }); + } + }; + + return verify_data_type_rules(value, data_type, available_data_types); + } else { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::ContainsKey( + ContainsKeyRuleViolation { + missing_key: identifier, + }, + )], + }); + } +} diff --git a/src/flow_validator/rule/contains_type.rs b/src/flow_validator/rule/contains_type.rs new file mode 100644 index 0000000..8b98245 --- /dev/null +++ b/src/flow_validator/rule/contains_type.rs @@ -0,0 +1,73 @@ +use super::violation::{DataTypeRuleError, DataTypeRuleViolation, InvalidFormatRuleViolation}; +use tucana::shared::{ + ExecutionDataType, ExecutionDataTypeContainsTypeRuleConfig, Value, value::Kind, +}; +use crate::flow_validator::{get_data_type_by_id, verify_data_type_rules}; + +/// # Item of Collection Validation +/// +/// This function validates if a value is contained within a predefined collection of allowed items. +/// +/// ## Process: +/// 1. Checks if the provided value is present in the collection of allowed items +/// +/// ## Error Handling: +/// - Returns an `ItemOfCollectionRuleViolation` if the value is not found in the collection +/// +pub fn apply_contains_type( + rule: ExecutionDataTypeContainsTypeRuleConfig, + available_data_types: &Vec, + body: &Value, +) -> Result<(), DataTypeRuleError> { + let identifier = rule.data_type_identifier; + let real_body = match &body.kind { + Some(body) => body.clone(), + None => { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::InvalidFormat( + InvalidFormatRuleViolation { + expected_format: identifier, + value: String::from("other"), + }, + )], + }); + } + }; + + match real_body { + Kind::ListValue(list) => { + let real_data_type = get_data_type_by_id(available_data_types, &identifier); + + if let Some(data_type) = real_data_type { + let mut rule_errors: Option = None; + + for value in list.values { + match verify_data_type_rules(value, data_type.clone(), &available_data_types) { + Ok(_) => {} + Err(errors) => { + rule_errors = Some(errors); + } + } + } + + if let Some(errors) = rule_errors { + return Err(errors); + } else { + return Ok(()); + } + } + } + _ => { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::InvalidFormat( + InvalidFormatRuleViolation { + expected_format: identifier, + value: String::from("other"), + }, + )], + }); + } + } + + Ok(()) +} diff --git a/src/flow_validator/rule/item_of_collection.rs b/src/flow_validator/rule/item_of_collection.rs new file mode 100644 index 0000000..2aa48f5 --- /dev/null +++ b/src/flow_validator/rule/item_of_collection.rs @@ -0,0 +1,187 @@ +use super::violation::{DataTypeRuleError, DataTypeRuleViolation, ItemOfCollectionRuleViolation}; +use tucana::shared::{DataTypeItemOfCollectionRuleConfig, Value}; + +/// # Item of Collection Validation +/// +/// This function validates if a value is contained within a predefined collection of allowed items. +/// +/// ## Process: +/// 1. Checks if the provided value is present in the collection of allowed items +/// +/// ## Error Handling: +/// - Returns an `ItemOfCollectionRuleViolation` if the value is not found in the collection +/// +pub fn apply_item_of_collection( + rule: DataTypeItemOfCollectionRuleConfig, + body: &Value, + key: &str, +) -> Result<(), DataTypeRuleError> { + if !rule.items.contains(body) { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::ItemOfCollection( + ItemOfCollectionRuleViolation { + collection_name: String::from(key), + }, + )], + }); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use tucana::shared::{ListValue, NullValue, Struct, Value}; + + #[test] + fn test_apply_item_of_collection_success() { + let value = Value { + kind: Some(tucana::shared::value::Kind::StringValue( + "allowed_value".to_string(), + )), + }; + let items = vec![ + Value { + kind: Some(tucana::shared::value::Kind::StringValue( + "allowed_value".to_string(), + )), + }, + Value { + kind: Some(tucana::shared::value::Kind::StringValue( + "another_allowed_value".to_string(), + )), + }, + ]; + + let rule = DataTypeItemOfCollectionRuleConfig { items }; + let result = apply_item_of_collection(rule, &value, "test_field"); + + assert!(result.is_ok()); + } + + #[test] + fn test_apply_item_of_collection_failure() { + let value = Value { + kind: Some(tucana::shared::value::Kind::StringValue( + "disallowed_value".to_string(), + )), + }; + let items = vec![ + Value { + kind: Some(tucana::shared::value::Kind::StringValue( + "allowed_value".to_string(), + )), + }, + Value { + kind: Some(tucana::shared::value::Kind::StringValue( + "another_allowed_value".to_string(), + )), + }, + ]; + + let rule = DataTypeItemOfCollectionRuleConfig { items }; + let result = apply_item_of_collection(rule, &value, "test_field"); + + assert!(result.is_err()); + if let Err(error) = result { + assert_eq!(error.violations.len(), 1); + match &error.violations[0] { + DataTypeRuleViolation::ItemOfCollection(violation) => { + assert_eq!(violation.collection_name, "test_field"); + } + _ => panic!("Expected ItemOfCollection violation"), + } + } + } + + #[test] + fn test_apply_item_of_collection_with_different_value_types() { + let value = Value { + kind: Some(tucana::shared::value::Kind::NumberValue(42.0)), + }; + let items = vec![ + Value { + kind: Some(tucana::shared::value::Kind::StringValue( + "allowed_value".to_string(), + )), + }, + Value { + kind: Some(tucana::shared::value::Kind::NumberValue(42.0)), + }, + ]; + + let rule = DataTypeItemOfCollectionRuleConfig { items }; + let result = apply_item_of_collection(rule, &value, "test_field"); + + assert!(result.is_ok()); + } + + #[test] + fn test_apply_item_of_collection_with_complex_values() { + let mut fields = HashMap::new(); + fields.insert( + "name".to_string(), + Value { + kind: Some(tucana::shared::value::Kind::StringValue("test".to_string())), + }, + ); + fields.insert( + "age".to_string(), + Value { + kind: Some(tucana::shared::value::Kind::NumberValue(30.0)), + }, + ); + let struct_value = Value { + kind: Some(tucana::shared::value::Kind::StructValue(Struct { fields })), + }; + + let list_values = vec![ + Value { + kind: Some(tucana::shared::value::Kind::StringValue("one".to_string())), + }, + Value { + kind: Some(tucana::shared::value::Kind::NumberValue(2.0)), + }, + ]; + let list_value = Value { + kind: Some(tucana::shared::value::Kind::ListValue(ListValue { + values: list_values, + })), + }; + + let null_value = Value { + kind: Some(tucana::shared::value::Kind::NullValue( + NullValue::NullValue as i32, + )), + }; + + let items = vec![struct_value.clone(), list_value.clone(), null_value.clone()]; + let rule = DataTypeItemOfCollectionRuleConfig { items }; + + assert!(apply_item_of_collection(rule.clone(), &struct_value, "test_field").is_ok()); + assert!(apply_item_of_collection(rule.clone(), &list_value, "test_field").is_ok()); + assert!(apply_item_of_collection(rule, &null_value, "test_field").is_ok()); + + let mut different_fields = HashMap::new(); + different_fields.insert( + "different".to_string(), + Value { + kind: Some(tucana::shared::value::Kind::BoolValue(true)), + }, + ); + let different_struct = Value { + kind: Some(tucana::shared::value::Kind::StructValue(Struct { + fields: different_fields, + })), + }; + + let rule_for_failure = DataTypeItemOfCollectionRuleConfig { + items: vec![struct_value], + }; + assert!( + apply_item_of_collection(rule_for_failure, &different_struct, "test_field").is_err() + ); + } +} diff --git a/src/flow_validator/rule/mod.rs b/src/flow_validator/rule/mod.rs new file mode 100644 index 0000000..e793609 --- /dev/null +++ b/src/flow_validator/rule/mod.rs @@ -0,0 +1,6 @@ +pub mod contains_key; +pub mod contains_type; +pub mod item_of_collection; +pub mod number_range; +pub mod regex; +pub mod violation; \ No newline at end of file diff --git a/src/flow_validator/rule/number_range.rs b/src/flow_validator/rule/number_range.rs new file mode 100644 index 0000000..8af4fca --- /dev/null +++ b/src/flow_validator/rule/number_range.rs @@ -0,0 +1,134 @@ +use tucana::shared::{DataTypeNumberRangeRuleConfig, Value, value::Kind}; + +use super::violation::{ + DataTypeRuleError, DataTypeRuleViolation, NumberInRangeRuleViolation, + RegexRuleTypeNotAcceptedViolation, +}; + +/// # Number Range Validation +/// +/// This function validates if a numeric value falls within a specified range and follows step constraints. +/// +/// ## Process: +/// 1. Extracts the numeric value from the input (if it is a number) +/// 2. Checks if the number is within the specified range (from/to) +/// 3. If steps are specified, verifies the number is divisible by the step value +/// +/// ## Error Handling: +/// - Returns a `RegexRuleTypeNotAcceptedViolation` if the value is not a number +/// - Returns a `NumberInRangeRuleViolation` if the number is outside the specified range +/// - Returns a `NumberInRangeRuleViolation` if the number doesn't conform to the step constraint +/// +pub fn apply_number_range( + rule: DataTypeNumberRangeRuleConfig, + body: &Value, + key: &str, +) -> Result<(), DataTypeRuleError> { + let kind = match &body.kind { + Some(kind) => kind, + None => return Ok(()), + }; + + let result = match kind { + Kind::NumberValue(n) => n.clone(), + _ => { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::RegexTypeNotAccepted( + RegexRuleTypeNotAcceptedViolation { + type_not_accepted: format!("{:?}", kind), + }, + )], + }); + } + }; + + if result < rule.from as f64 || result > rule.to as f64 { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::NumberInRange( + NumberInRangeRuleViolation { + key: String::from(key), + }, + )], + }); + } + + if let Some(modulo) = rule.steps { + if modulo == 0 { + return Ok(()); + } + + if result % modulo as f64 != 0.0 { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::NumberInRange( + NumberInRangeRuleViolation { + key: String::from(key), + }, + )], + }); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn number_as_value(number: f64) -> Value { + Value { + kind: Some(Kind::NumberValue(number)), + } + } + + #[test] + fn test_apply_number_range() { + let rule = DataTypeNumberRangeRuleConfig { + from: 1, + to: 10, + steps: None, + }; + + assert!(apply_number_range(rule, &number_as_value(-2.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(2.0), "test").is_ok()); + assert!(apply_number_range(rule, &number_as_value(3.0), "test").is_ok()); + assert!(apply_number_range(rule, &number_as_value(11.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(12.0), "test").is_err()); + } + + #[test] + fn test_apply_number_range_with_steps() { + let rule = DataTypeNumberRangeRuleConfig { + from: 1, + to: 10, + steps: Some(2), + }; + + assert!(apply_number_range(rule, &number_as_value(2.0), "test").is_ok()); + assert!(apply_number_range(rule, &number_as_value(4.0), "test").is_ok()); + assert!(apply_number_range(rule, &number_as_value(6.0), "test").is_ok()); + assert!(apply_number_range(rule, &number_as_value(8.0), "test").is_ok()); + assert!(apply_number_range(rule, &number_as_value(10.0), "test").is_ok()); + assert!(apply_number_range(rule, &number_as_value(1.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(3.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(5.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(7.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(9.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(11.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(12.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(-12.0), "test").is_err()); + } + + #[test] + fn test_apply_number_range_with_falty_steps() { + let rule = DataTypeNumberRangeRuleConfig { + from: 1, + to: 10, + steps: Some(0), + }; + + assert!(apply_number_range(rule, &number_as_value(-12.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(12.0), "test").is_err()); + assert!(apply_number_range(rule, &number_as_value(6.0), "test").is_ok()); + } +} diff --git a/src/flow_validator/rule/regex.rs b/src/flow_validator/rule/regex.rs new file mode 100644 index 0000000..26ddd51 --- /dev/null +++ b/src/flow_validator/rule/regex.rs @@ -0,0 +1,245 @@ +use tucana::shared::{DataTypeRegexRuleConfig, Value, value::Kind}; +use super::violation::{ + DataTypeRuleError, DataTypeRuleViolation, RegexRuleTypeNotAcceptedViolation, RegexRuleViolation, +}; + +/// # Regex Pattern Validation +/// +/// This function validates if a value matches a specified regex pattern. +/// +/// ## Process: +/// 1. Converts the input value to a string representation (if possible) +/// 2. Compiles the regex pattern from the rule +/// 3. Checks if the string representation matches the regex pattern +/// +/// ## Error Handling: +/// - Returns a `RegexRuleTypeNotAcceptedViolation` if the value type cannot be converted to a string +/// (e.g., arrays, objects) +/// - Returns a `RegexRuleViolation` if the string representation does not match the specified pattern +/// +pub fn apply_regex(rule: DataTypeRegexRuleConfig, body: &Value) -> Result<(), DataTypeRuleError> { + let kind = match &body.kind { + Some(kind) => kind, + None => return Ok(()), + }; + + let result = match kind { + Kind::BoolValue(b) => b.to_string(), + Kind::NumberValue(n) => n.to_string(), + Kind::StringValue(s) => s.clone(), + Kind::NullValue(_) => "null".to_string(), + Kind::StructValue(s) => { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::RegexTypeNotAccepted( + RegexRuleTypeNotAcceptedViolation { + type_not_accepted: format!("StructValue({:?})", s), + }, + )], + }); + } + Kind::ListValue(l) => { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::RegexTypeNotAccepted( + RegexRuleTypeNotAcceptedViolation { + type_not_accepted: format!("ListValue({:?})", l), + }, + )], + }); + } + }; + + let regex = regex::Regex::new(rule.pattern.as_str()).unwrap(); + + if !regex.is_match(&result) { + return Err(DataTypeRuleError { + violations: vec![DataTypeRuleViolation::Regex(RegexRuleViolation { + missing_regex: rule.pattern.clone(), + })], + }); + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tucana::shared::{ListValue, Struct}; + + #[test] + fn test_apply_regex_with_matching_string() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from("^[a-z]+$"), + }; + let value = Value { + kind: Some(Kind::StringValue(String::from("abcde"))), + }; + + assert!(apply_regex(rule, &value).is_ok()); + } + + #[test] + fn test_apply_regex_with_non_matching_string() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from("^[a-z]+$"), + }; + let value = Value { + kind: Some(Kind::StringValue(String::from("123"))), + }; + + let result = apply_regex(rule, &value); + assert!(result.is_err()); + + if let Err(DataTypeRuleError { violations }) = result { + assert_eq!(violations.len(), 1); + match &violations[0] { + DataTypeRuleViolation::Regex(violation) => { + assert_eq!(violation.missing_regex, "^[a-z]+$"); + } + _ => panic!("Expected RegexRuleViolation"), + } + } + } + + #[test] + fn test_apply_regex_with_matching_boolean() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from("^true$"), + }; + let value = Value { + kind: Some(Kind::BoolValue(true)), + }; + + assert!(apply_regex(rule, &value).is_ok()); + } + + #[test] + fn test_apply_regex_with_non_matching_boolean() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from("^false$"), + }; + let value = Value { + kind: Some(Kind::BoolValue(true)), + }; + + assert!(apply_regex(rule, &value).is_err()); + } + + #[test] + fn test_apply_regex_with_matching_number() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from("^42$"), + }; + let value = Value { + kind: Some(Kind::NumberValue(42.0)), + }; + + assert!(apply_regex(rule, &value).is_ok()); + } + + #[test] + fn test_apply_regex_with_non_matching_number() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from("^[0-9]+$"), + }; + let value = Value { + kind: Some(Kind::NumberValue(3.14)), + }; + + assert!(apply_regex(rule, &value).is_err()); + } + + #[test] + fn test_apply_regex_with_array() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from(".*"), + }; + let value = Value { + kind: Some(Kind::ListValue(ListValue { values: vec![] })), + }; + + let result = apply_regex(rule, &value); + assert!(result.is_err()); + + if let Err(DataTypeRuleError { violations }) = result { + assert_eq!(violations.len(), 1); + match &violations[0] { + DataTypeRuleViolation::RegexTypeNotAccepted(violation) => { + assert!(violation.type_not_accepted.contains("ListValue")); + } + _ => panic!("Expected RegexRuleTypeNotAcceptedViolation"), + } + } + } + + #[test] + fn test_apply_regex_with_object() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from(".*"), + }; + let value = Value { + kind: Some(Kind::StructValue(Struct { + fields: Default::default(), + })), + }; + + let result = apply_regex(rule, &value); + assert!(result.is_err()); + + if let Err(DataTypeRuleError { violations }) = result { + assert_eq!(violations.len(), 1); + match &violations[0] { + DataTypeRuleViolation::RegexTypeNotAccepted(violation) => { + assert!(violation.type_not_accepted.contains("StructValue")); + } + _ => panic!("Expected RegexRuleTypeNotAcceptedViolation"), + } + } + } + + #[test] + fn test_apply_regex_with_null_kind() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from(".*"), + }; + let value = Value { kind: None }; + + assert!(apply_regex(rule, &value).is_ok()); + } + + #[test] + fn test_apply_regex_complex_pattern() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from(r"^\d{3}-\d{2}-\d{4}$"), // SSN pattern + }; + let value = Value { + kind: Some(Kind::StringValue(String::from("123-45-6789"))), + }; + + assert!(apply_regex(rule.clone(), &value).is_ok()); + + let invalid_value = Value { + kind: Some(Kind::StringValue(String::from("123-456-789"))), + }; + + assert!(apply_regex(rule, &invalid_value).is_err()); + } + + #[test] + fn test_apply_regex_email_pattern() { + let rule = DataTypeRegexRuleConfig { + pattern: String::from(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"), + }; + let value = Value { + kind: Some(Kind::StringValue(String::from("test@example.com"))), + }; + + assert!(apply_regex(rule.clone(), &value).is_ok()); + + let invalid_value = Value { + kind: Some(Kind::StringValue(String::from("invalid-email"))), + }; + + assert!(apply_regex(rule, &invalid_value).is_err()); + } +} diff --git a/src/flow_validator/rule/violation.rs b/src/flow_validator/rule/violation.rs new file mode 100644 index 0000000..4257e89 --- /dev/null +++ b/src/flow_validator/rule/violation.rs @@ -0,0 +1,166 @@ +pub struct DataTypeRuleError { + pub violations: Vec, +} + +pub enum DataTypeRuleViolation { + MissingDataType(MissingDataTypeRuleDefinition), + ContainsKey(ContainsKeyRuleViolation), + Regex(RegexRuleViolation), + RegexTypeNotAccepted(RegexRuleTypeNotAcceptedViolation), + DataTypeNotFound(DataTypeNotFoundRuleViolation), + NumberInRange(NumberInRangeRuleViolation), + ItemOfCollection(ItemOfCollectionRuleViolation), + InvalidFormat(InvalidFormatRuleViolation), + GenericKeyNotAllowed(GenericKeyNotAllowedRuleViolation), + DataTypeIdentifierNotPresent(DataTypeIdentifierNotPresentRuleViolation), +} + +pub struct MissingDataTypeRuleDefinition { + pub missing_type: String, +} + +pub struct ContainsKeyRuleViolation { + pub missing_key: String, +} + +pub struct RegexRuleViolation { + pub missing_regex: String, +} + +pub struct RegexRuleTypeNotAcceptedViolation { + pub type_not_accepted: String, +} + +pub struct DataTypeNotFoundRuleViolation { + pub data_type: String, +} + +pub struct NumberInRangeRuleViolation { + pub key: String, +} + +pub struct ItemOfCollectionRuleViolation { + pub collection_name: String, +} + +pub struct InvalidFormatRuleViolation { + pub expected_format: String, + pub value: String, +} + +pub struct GenericKeyNotAllowedRuleViolation { + pub key: String, +} + +pub struct DataTypeIdentifierNotPresentRuleViolation { + pub identifier: String, +} + +impl DataTypeRuleError { + pub fn to_string(&self) -> String { + let mut violations = Vec::new(); + + for violation in &self.violations { + match violation { + DataTypeRuleViolation::ContainsKey(v) => { + violations.push(serde_json::json!({ + "type": "ContainsKey", + "explanation": format!("Missing required key: '{}'", v.missing_key), + "details": { + "missing_key": v.missing_key + } + })); + } + DataTypeRuleViolation::Regex(v) => { + violations.push(serde_json::json!({ + "type": "Regex", + "explanation": format!("Failed to match regex pattern: '{}'", v.missing_regex), + "details": { + "missing_regex": v.missing_regex + } + })); + } + DataTypeRuleViolation::MissingDataType(v) => { + violations.push(serde_json::json!({ + "type": "MissingDataType", + "explanation": format!("Missing required data type: '{}'", v.missing_type), + "details": { + "missing_type": v.missing_type + } + })); + } + DataTypeRuleViolation::RegexTypeNotAccepted(v) => { + violations.push(serde_json::json!({ + "type": "RegexTypeNotAccepted", + "explanation": format!("Regex pattern does not match data type: '{}'", v.type_not_accepted), + "details": { + "type_not_accepted": v.type_not_accepted + } + })); + } + DataTypeRuleViolation::DataTypeNotFound(v) => { + violations.push(serde_json::json!({ + "type": "DataTypeNotFound", + "explanation": format!("Data type not found: '{}'", v.data_type), + "details": { + "data_type": v.data_type + } + })); + } + DataTypeRuleViolation::NumberInRange(v) => { + violations.push(serde_json::json!({ + "type": "NumberInRange", + "explanation": format!("Number not in valid range for key: '{}'", v.key), + "details": { + "key": v.key + } + })); + } + DataTypeRuleViolation::ItemOfCollection(v) => { + violations.push(serde_json::json!({ + "type": "ItemOfCollection", + "explanation": format!("Item is not a valid member of collection: '{}'", v.collection_name), + "details": { + "collection_name": v.collection_name + } + })); + } + DataTypeRuleViolation::InvalidFormat(v) => { + violations.push(serde_json::json!({ + "type": "InvalidFormat", + "explanation": format!("Invalid format. Expected: '{}', Got: '{}'", v.expected_format, v.value), + "details": { + "expected_format": v.expected_format, + "value": v.value + } + })); + } + DataTypeRuleViolation::GenericKeyNotAllowed(v) => { + violations.push(serde_json::json!({ + "type": "GenericKeyNotAllowed", + "explanation": format!("Generic key not allowed: '{}'", v.key), + "details": { + "key": v.key + } + })); + } + DataTypeRuleViolation::DataTypeIdentifierNotPresent(v) => { + violations.push(serde_json::json!({ + "type": "DataTypeIdentifierNotPresent", + "explanation": format!("Data type identifier not present: '{}'", v.identifier), + "details": { + "identifier": v.identifier + } + })); + } + } + } + + serde_json::json!({ + "error": "DataTypeRuleError", + "violation_count": self.violations.len(), + "violations": violations + }) + .to_string() + } +} diff --git a/src/lib.rs b/src/lib.rs index f29c878..cb80057 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,3 +6,6 @@ pub mod flow_config; #[cfg(feature = "flow_health")] pub mod flow_health; + +#[cfg(feature = "flow_validator")] +mod flow_validator; \ No newline at end of file