diff --git a/client b/client index 25a901f..83fedad 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 25a901ff5e0124485bee95e7133ca641eeca306f +Subproject commit 83fedad94e454dc1a79a0c0212d19d6d8b719591 diff --git a/common/build.rs b/common/build.rs index 09acd4d..9189ae8 100644 --- a/common/build.rs +++ b/common/build.rs @@ -68,6 +68,14 @@ fn main() -> Result<(), Box> { ".komp_ac.table_validation.ValidationRuleDefinition", "#[derive(serde::Serialize, serde::Deserialize)]", ) + .type_attribute( + ".komp_ac.table_validation.ValidationSetRuleItem", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_validation.ValidationSetRuleItem.Source", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) .type_attribute( ".komp_ac.table_validation.ValidationSetDefinition", "#[derive(serde::Serialize, serde::Deserialize)]", diff --git a/common/proto/table_validation.proto b/common/proto/table_validation.proto index 093027f..bcfb8ab 100644 --- a/common/proto/table_validation.proto +++ b/common/proto/table_validation.proto @@ -219,6 +219,7 @@ message ReplaceTableValidationResponse { } message ValidationRuleDefinition { + optional int64 id = 4; string name = 1; optional string description = 2; @@ -226,12 +227,28 @@ message ValidationRuleDefinition { FieldValidation validation = 3; } +message ValidationSetRuleItem { + int32 position = 1; + optional string name = 2; + optional string description = 3; + + oneof source { + string global_rule_name = 10; + FieldValidation inline_validation = 11; + int64 global_rule_id = 12; + } +} + message ValidationSetDefinition { + reserved 3; + string name = 1; optional string description = 2; - repeated string ruleNames = 3; - // Server-resolved snapshot of all rules in ruleNames order. + // Ordered set items. + repeated ValidationSetRuleItem ruleItems = 5; + + // Server-resolved snapshot of all set items in order. FieldValidation resolvedValidation = 4; } diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index f76e8e5..fae9cd0 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/common/src/proto/komp_ac.table_validation.rs b/common/src/proto/komp_ac.table_validation.rs index 14213a2..51baf6c 100644 --- a/common/src/proto/komp_ac.table_validation.rs +++ b/common/src/proto/komp_ac.table_validation.rs @@ -209,6 +209,8 @@ pub struct ReplaceTableValidationResponse { #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ValidationRuleDefinition { + #[prost(int64, optional, tag = "4")] + pub id: ::core::option::Option, #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, #[prost(string, optional, tag = "2")] @@ -219,14 +221,40 @@ pub struct ValidationRuleDefinition { } #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct ValidationSetRuleItem { + #[prost(int32, tag = "1")] + pub position: i32, + #[prost(string, optional, tag = "2")] + pub name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(string, optional, tag = "3")] + pub description: ::core::option::Option<::prost::alloc::string::String>, + #[prost(oneof = "validation_set_rule_item::Source", tags = "10, 11, 12")] + pub source: ::core::option::Option, +} +/// Nested message and enum types in `ValidationSetRuleItem`. +pub mod validation_set_rule_item { + #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Source { + #[prost(string, tag = "10")] + GlobalRuleName(::prost::alloc::string::String), + #[prost(message, tag = "11")] + InlineValidation(super::FieldValidation), + #[prost(int64, tag = "12")] + GlobalRuleId(i64), + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct ValidationSetDefinition { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, #[prost(string, optional, tag = "2")] pub description: ::core::option::Option<::prost::alloc::string::String>, - #[prost(string, repeated, tag = "3")] - pub rule_names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - /// Server-resolved snapshot of all rules in ruleNames order. + /// Ordered set items. + #[prost(message, repeated, tag = "5")] + pub rule_items: ::prost::alloc::vec::Vec, + /// Server-resolved snapshot of all set items in order. #[prost(message, optional, tag = "4")] pub resolved_validation: ::core::option::Option, } diff --git a/server b/server index b178fce..0f74f9e 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit b178fce273424488e78edd7165e2bfc46eeff45f +Subproject commit 0f74f9ee27133981d21bf52daac5f8b42c08168b diff --git a/validation-core/docs/validation_architecture.md b/validation-core/docs/validation_architecture.md index f064b03..90ca941 100644 --- a/validation-core/docs/validation_architecture.md +++ b/validation-core/docs/validation_architecture.md @@ -136,15 +136,15 @@ profileName: string set: name: string description: optional string - ruleNames: repeated string + ruleItems: repeated ValidationSetRuleItem ``` Frontend rules: - `set.name` is required and unique inside a profile. -- `ruleNames` must contain at least one rule. -- `ruleNames` are ordered. -- Every rule name must already exist. +- `ruleItems` must contain at least one item. +- `ruleItems` are ordered. +- Every global rule reference must already exist. - Duplicate rule names in the same set are rejected. - Conflicting singleton fragments are rejected. @@ -362,7 +362,7 @@ Recommended UI: ```text name description -ordered rule picker +ordered global/inline rule item picker resolved preview ``` @@ -430,11 +430,11 @@ validation: Create set `phone`: ```text -ruleNames: - - required - - phone-length - - digits-only - - phone-mask +ruleItems: + - globalRuleName: required + - globalRuleName: phone-length + - globalRuleName: digits-only + - globalRuleName: phone-mask ``` Apply set: diff --git a/validation-core/src/lib.rs b/validation-core/src/lib.rs index 07c6f87..7165c27 100644 --- a/validation-core/src/lib.rs +++ b/validation-core/src/lib.rs @@ -11,4 +11,6 @@ pub use rules::{ count_text, CharacterFilter, CharacterLimits, CountMode, DisplayMask, LimitCheckResult, MaskDisplayMode, PatternFilters, PositionFilter, PositionRange, }; -pub use set::{AppliedValidation, ValidationRule, ValidationSet}; +pub use set::{ + AppliedValidation, ValidationRule, ValidationSet, ValidationSetItem, ValidationSetResolveError, +}; diff --git a/validation-core/src/set.rs b/validation-core/src/set.rs index a8cb289..6e34625 100644 --- a/validation-core/src/set.rs +++ b/validation-core/src/set.rs @@ -1,5 +1,6 @@ use crate::{ValidationConfig, ValidationMergeError, ValidationSettings}; use serde::{Deserialize, Serialize}; +use thiserror::Error; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ValidationRule { @@ -18,19 +19,52 @@ impl ValidationRule { pub struct ValidationSet { pub name: String, pub description: Option, - pub rules: Vec, + pub items: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ValidationSetItem { + GlobalRuleRef(String), + InlineRule { + name: Option, + validation: ValidationSettings, + }, } impl ValidationSet { - pub fn resolve_settings(&self) -> Result { - ValidationSettings::merge_rules(self.rules.iter().map(|rule| &rule.settings)) + pub fn resolve_settings_with_rules<'a>( + &'a self, + rules: impl Fn(&str) -> Option<&'a ValidationRule>, + ) -> Result { + let settings = self.items.iter().map(|item| match item { + ValidationSetItem::GlobalRuleRef(name) => { + rules(name).map(|rule| &rule.settings).ok_or_else(|| { + ValidationSetResolveError::MissingGlobalRule { name: name.clone() } + }) + } + ValidationSetItem::InlineRule { validation, .. } => Ok(validation), + }); + + let settings = settings.collect::, _>>()?; + Ok(ValidationSettings::merge_rules(settings)?) } - pub fn resolve(&self) -> Result { - Ok(self.resolve_settings()?.resolve()) + pub fn resolve_with_rules<'a>( + &'a self, + rules: impl Fn(&str) -> Option<&'a ValidationRule>, + ) -> Result { + Ok(self.resolve_settings_with_rules(rules)?.resolve()) } } +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum ValidationSetResolveError { + #[error("validation set references missing global rule '{name}'")] + MissingGlobalRule { name: String }, + #[error(transparent)] + Merge(#[from] ValidationMergeError), +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppliedValidation { pub set_name: Option, @@ -56,19 +90,17 @@ mod tests { let set = ValidationSet { name: "phone".to_string(), description: None, - rules: vec![ - ValidationRule { - name: "phone-length".to_string(), - description: None, - settings: ValidationSettings { + items: vec![ + ValidationSetItem::InlineRule { + name: Some("phone-length".to_string()), + validation: ValidationSettings { character_limits: Some(CharacterLimits::new_range(10, 15)), ..ValidationSettings::default() }, }, - ValidationRule { - name: "digits-only".to_string(), - description: None, - settings: ValidationSettings { + ValidationSetItem::InlineRule { + name: Some("digits-only".to_string()), + validation: ValidationSettings { pattern: Some(PatternSettings { filters: vec![PositionFilterSettings { positions: PositionRange::From(0), @@ -82,7 +114,9 @@ mod tests { ], }; - let settings = set.resolve_settings().expect("set should resolve"); + let settings = set + .resolve_settings_with_rules(|_| None) + .expect("set should resolve"); assert!(settings.character_limits.is_some()); assert_eq!(settings.pattern.expect("pattern").filters.len(), 1); @@ -93,19 +127,17 @@ mod tests { let set = ValidationSet { name: "conflict".to_string(), description: None, - rules: vec![ - ValidationRule { - name: "short".to_string(), - description: None, - settings: ValidationSettings { + items: vec![ + ValidationSetItem::InlineRule { + name: Some("short".to_string()), + validation: ValidationSettings { character_limits: Some(CharacterLimits::new(10)), ..ValidationSettings::default() }, }, - ValidationRule { - name: "long".to_string(), - description: None, - settings: ValidationSettings { + ValidationSetItem::InlineRule { + name: Some("long".to_string()), + validation: ValidationSettings { character_limits: Some(CharacterLimits::new(20)), ..ValidationSettings::default() }, @@ -113,6 +145,6 @@ mod tests { ], }; - assert!(set.resolve_settings().is_err()); + assert!(set.resolve_settings_with_rules(|_| None).is_err()); } }