From def75c00b4fe2f4f99a52f1bc078c871d256683d Mon Sep 17 00:00:00 2001 From: Priec Date: Sun, 10 May 2026 09:23:33 +0200 Subject: [PATCH] rule page in the validation client --- client | 2 +- common/build.rs | 8 ++ common/proto/table_validation.proto | 21 ++++- common/src/proto/descriptor.bin | Bin 72490 -> 73489 bytes common/src/proto/komp_ac.table_validation.rs | 34 +++++++- server | 2 +- .../docs/validation_architecture.md | 20 ++--- validation-core/src/lib.rs | 4 +- validation-core/src/set.rs | 82 ++++++++++++------ 9 files changed, 130 insertions(+), 43 deletions(-) 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 f76e8e5e8a27b5d49623d715ba8936fe5e101c90..fae9cd08cb06abf422896dc7148d26086aaadf8d 100644 GIT binary patch delta 3623 zcmZXWZHQD=7{~8<=FHtYcUIXsGqW>0`!aV|OS96Tmrs2NgXo1UHi)2L*_t5+w%yp( zK6Z^PG_=yO-N+&fjSRJN!?$Qti272{*P>b|Qb{cm1;NPv&pGGW75e7S^F06aKmX^P zJNwaLy?su9a@$s#du{fPt(91P+g>-_S1QKTYE*TiR%Y%EhVk#sO0_?^wcNgCri_2> zoPHwB#+i{;W9`zobEMIndTO%u?E2Qk_|R7rmmeJ+Yc)n0A8D>1-Z0j@Z+-I_Q;VZ# zC#G8b-&F*J(K4czAJ7u3Hs%Jv}z@ zU8|9BU>VV$y#4>wD5nXjerk1EpV;>GMh%+gs;kNEb}_y9)mDp%qa1yNdd)-2#Vp zN>SPNf#46=A&A;jDsrJ=6{@FHv4qxC>jGi*w4z>LtZ-DIJ1Q0-by|)Jw8f@diXnAc z_0+rzgmy+kD-eeU6eSMr%%T!+dj>Obsi7KDXE?Q`NUabBCj3LupDN>aiUO4Xp&~q9 zUgqMnN)LJ)8AtI;ZrFwlxJ}0$_KAfapD^O4oU1jox!= zeJ_+Y61hE#q~Hix^R*o0M(V8#qIDw;@wTK(CVY{Cm%XDBDqo~%xm7K( zqfJD7OVg|d!V);NIzSX}qK>74kljQD<9o6A68Voq$&ME2P6M>7Irnh@qV*-R9|zK9 z6K1G>e#e}a@(Du7jQ2Dy-QsG^; zi+2#b1D$suYPXAbAXK+I?^;)ge>ZlJ|5>TnIDyj3aqb}h=&*|39b|u1NLNkx1_f_= zM^#k5Ap@?mqiHwbtOmgm(9IW!+G#)El$RkpEd#FdLI!WHk_$c1uV=6z`Tt0EFyr$*9JT z-jj?#Xa%~AfOgeq1Vrn5l2NY-_fYVGchsv=qxVq6Znf@ZOMB&HAjkrpED*ALB_kkY z_sS#aGvS96eB@>OQ2C)m-^Z%^h;Fji8iYpxbdR79-yg)ekMi!16D*^5A1zxU)!1bp zt-8_1*>A%A6nyM==||;$*`=R%IUwFa@D6m|fv7zo-hog(V7;d}cR$B@P#ysYy+G;Z zI1kDrz$$tV$|D#s;inXQ<{b^7@>3b`06RJ)j{pQ!pnC*B)E=Ua)P3c^kUb;=9$-ha zGGGvTfzA=ouKEE3(K;&wuAA^M1)qCIbyOaf`PSLd5y=Py)fDVx!6-f=Aps$KL>@t% z9UUdA*tvkv3zSxNbd+-LuT!j|^(d9Q?X{{SS$#|n141j%wWeV79wUDmAbO9v(`a31 zI=1L5Y^!R`khjNTLE|6T^7_rz@G I`xOuT2Lv#}z5oCK delta 2652 zcmYM0%WG9v6voe9_vCo*iHdh8xq02(dlN0u);cJK4od%kB3LN2gHkA^W)QGiVjXmJ zwD`aWJ~&n*YEg^vfiDQO;GokMI_#h$1w{>5)M6}JOlyDpv2>PS_FCWm_P5U2=N|ew zz4&8#?d^SCf9x`OqdYj%nEhbie>HQhTz8~dZh5P>ET_lI>qpkwrTOB|)=zByVrufM zuO~Nc9J^u4iRq7;&Ac|XWy8d}&%d15JW%OWR@Odz`HynZu?@4|9DBStpZz#rWV=jq zP0Cg8jm{pK`Ke=Wm+O9M7B*j!ccsfD>9Xk!BUMG?u(m|8;&53#U!12Dvv||USX?xz z$6OJmo2I+2=!@-$B1$(+XEguRE2U!7Tje{yeo{!rUq-ae?QzT4W^5DMZkcum1CrKl zcE==t8k2YDm9$Rf9aC+4Z*@s*de=xe7K9lv91EoOt{w|S^=>#;k*0OG=RG|a#JRw5 zE)c1EdM?lx$8&*5-E-%13Dt#T%^B%<_Yg{;DhWz+uaw-jb7RI1Yc*1Hrq#~h_fpV( zBddnqEwBGJTDW0v?o4j%yqtPx)71KjsWmEIAxG*%j&-RZe&5&?QM=;R##j0rt<4^o zdKHm3oG*y#R$;z{d%PR&iQUFC z?)QY~1+~`=c$>t#gH?LBiN8A}?YG%>Np?j?{Zww3IN*NgXotl4La2gaz96+bq=*BC zk=-G2!2QnAPKg7C=mmo#(67b;gS76HIN$-B?UrO;bTmNaZi(|9aE|t9Mo^b690ysL z;yoG?i0mF0lFMkoIod1nl7yHV7~Ut)uMUM<5u|mmblmMB>7dQ_OL8bW8l-Z+#Mg4L zaFz~eNP|8kFoXnBd_ZDGAhHKEqe18Bpk@S-5f}~w`qh{bNb5n(Xvk)zB;Vnva5wG{ zrKMEET{q+$9oCaU$bvx@r1r381R{GlWF+aZ&5lZPEFN^2%A*?nuv48DS?wP&h?fA& z!~Fvj=d^T(PY_7&v@9RjYId2H*Wd7QHf?rXlJDa#O)8J;E={+~3H1))9Sq(AbVdgZ(F+DgpkIvx25CK`18&*utRxqsqZXBCb-pdXax)Hb;{=@ zo`z+5&xg~bY0J@tSG^T_L2Y%P#0#%l-6!#a2GO?JCE;gMxG~#QUebr9?Sd#S>uf+| z!7v+;;>$W35ZTK*nzplhMIAx3g255!SL0zoTCeC~+AfHzBBMUT*FYY9IDFtidap|S Pp#;), #[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()); } }