rule page in the validation client

This commit is contained in:
Priec
2026-05-10 09:23:33 +02:00
parent 17a13569d8
commit def75c00b4
9 changed files with 130 additions and 43 deletions

2
client

Submodule client updated: 25a901ff5e...83fedad94e

View File

@@ -68,6 +68,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
".komp_ac.table_validation.ValidationRuleDefinition", ".komp_ac.table_validation.ValidationRuleDefinition",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[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( .type_attribute(
".komp_ac.table_validation.ValidationSetDefinition", ".komp_ac.table_validation.ValidationSetDefinition",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[derive(serde::Serialize, serde::Deserialize)]",

View File

@@ -219,6 +219,7 @@ message ReplaceTableValidationResponse {
} }
message ValidationRuleDefinition { message ValidationRuleDefinition {
optional int64 id = 4;
string name = 1; string name = 1;
optional string description = 2; optional string description = 2;
@@ -226,12 +227,28 @@ message ValidationRuleDefinition {
FieldValidation validation = 3; 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 { message ValidationSetDefinition {
reserved 3;
string name = 1; string name = 1;
optional string description = 2; 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; FieldValidation resolvedValidation = 4;
} }

Binary file not shown.

View File

@@ -209,6 +209,8 @@ pub struct ReplaceTableValidationResponse {
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ValidationRuleDefinition { pub struct ValidationRuleDefinition {
#[prost(int64, optional, tag = "4")]
pub id: ::core::option::Option<i64>,
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String, pub name: ::prost::alloc::string::String,
#[prost(string, optional, tag = "2")] #[prost(string, optional, tag = "2")]
@@ -219,14 +221,40 @@ pub struct ValidationRuleDefinition {
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)] #[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<validation_set_rule_item::Source>,
}
/// 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 { pub struct ValidationSetDefinition {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String, pub name: ::prost::alloc::string::String,
#[prost(string, optional, tag = "2")] #[prost(string, optional, tag = "2")]
pub description: ::core::option::Option<::prost::alloc::string::String>, pub description: ::core::option::Option<::prost::alloc::string::String>,
#[prost(string, repeated, tag = "3")] /// Ordered set items.
pub rule_names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(message, repeated, tag = "5")]
/// Server-resolved snapshot of all rules in ruleNames order. pub rule_items: ::prost::alloc::vec::Vec<ValidationSetRuleItem>,
/// Server-resolved snapshot of all set items in order.
#[prost(message, optional, tag = "4")] #[prost(message, optional, tag = "4")]
pub resolved_validation: ::core::option::Option<FieldValidation>, pub resolved_validation: ::core::option::Option<FieldValidation>,
} }

2
server

Submodule server updated: b178fce273...0f74f9ee27

View File

@@ -136,15 +136,15 @@ profileName: string
set: set:
name: string name: string
description: optional string description: optional string
ruleNames: repeated string ruleItems: repeated ValidationSetRuleItem
``` ```
Frontend rules: Frontend rules:
- `set.name` is required and unique inside a profile. - `set.name` is required and unique inside a profile.
- `ruleNames` must contain at least one rule. - `ruleItems` must contain at least one item.
- `ruleNames` are ordered. - `ruleItems` are ordered.
- Every rule name must already exist. - Every global rule reference must already exist.
- Duplicate rule names in the same set are rejected. - Duplicate rule names in the same set are rejected.
- Conflicting singleton fragments are rejected. - Conflicting singleton fragments are rejected.
@@ -362,7 +362,7 @@ Recommended UI:
```text ```text
name name
description description
ordered rule picker ordered global/inline rule item picker
resolved preview resolved preview
``` ```
@@ -430,11 +430,11 @@ validation:
Create set `phone`: Create set `phone`:
```text ```text
ruleNames: ruleItems:
- required - globalRuleName: required
- phone-length - globalRuleName: phone-length
- digits-only - globalRuleName: digits-only
- phone-mask - globalRuleName: phone-mask
``` ```
Apply set: Apply set:

View File

@@ -11,4 +11,6 @@ pub use rules::{
count_text, CharacterFilter, CharacterLimits, CountMode, DisplayMask, LimitCheckResult, count_text, CharacterFilter, CharacterLimits, CountMode, DisplayMask, LimitCheckResult,
MaskDisplayMode, PatternFilters, PositionFilter, PositionRange, MaskDisplayMode, PatternFilters, PositionFilter, PositionRange,
}; };
pub use set::{AppliedValidation, ValidationRule, ValidationSet}; pub use set::{
AppliedValidation, ValidationRule, ValidationSet, ValidationSetItem, ValidationSetResolveError,
};

View File

@@ -1,5 +1,6 @@
use crate::{ValidationConfig, ValidationMergeError, ValidationSettings}; use crate::{ValidationConfig, ValidationMergeError, ValidationSettings};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationRule { pub struct ValidationRule {
@@ -18,19 +19,52 @@ impl ValidationRule {
pub struct ValidationSet { pub struct ValidationSet {
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub rules: Vec<ValidationRule>, pub items: Vec<ValidationSetItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ValidationSetItem {
GlobalRuleRef(String),
InlineRule {
name: Option<String>,
validation: ValidationSettings,
},
} }
impl ValidationSet { impl ValidationSet {
pub fn resolve_settings(&self) -> Result<ValidationSettings, ValidationMergeError> { pub fn resolve_settings_with_rules<'a>(
ValidationSettings::merge_rules(self.rules.iter().map(|rule| &rule.settings)) &'a self,
rules: impl Fn(&str) -> Option<&'a ValidationRule>,
) -> Result<ValidationSettings, ValidationSetResolveError> {
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::<Result<Vec<_>, _>>()?;
Ok(ValidationSettings::merge_rules(settings)?)
} }
pub fn resolve(&self) -> Result<ValidationConfig, ValidationMergeError> { pub fn resolve_with_rules<'a>(
Ok(self.resolve_settings()?.resolve()) &'a self,
rules: impl Fn(&str) -> Option<&'a ValidationRule>,
) -> Result<ValidationConfig, ValidationSetResolveError> {
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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppliedValidation { pub struct AppliedValidation {
pub set_name: Option<String>, pub set_name: Option<String>,
@@ -56,19 +90,17 @@ mod tests {
let set = ValidationSet { let set = ValidationSet {
name: "phone".to_string(), name: "phone".to_string(),
description: None, description: None,
rules: vec![ items: vec![
ValidationRule { ValidationSetItem::InlineRule {
name: "phone-length".to_string(), name: Some("phone-length".to_string()),
description: None, validation: ValidationSettings {
settings: ValidationSettings {
character_limits: Some(CharacterLimits::new_range(10, 15)), character_limits: Some(CharacterLimits::new_range(10, 15)),
..ValidationSettings::default() ..ValidationSettings::default()
}, },
}, },
ValidationRule { ValidationSetItem::InlineRule {
name: "digits-only".to_string(), name: Some("digits-only".to_string()),
description: None, validation: ValidationSettings {
settings: ValidationSettings {
pattern: Some(PatternSettings { pattern: Some(PatternSettings {
filters: vec![PositionFilterSettings { filters: vec![PositionFilterSettings {
positions: PositionRange::From(0), 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!(settings.character_limits.is_some());
assert_eq!(settings.pattern.expect("pattern").filters.len(), 1); assert_eq!(settings.pattern.expect("pattern").filters.len(), 1);
@@ -93,19 +127,17 @@ mod tests {
let set = ValidationSet { let set = ValidationSet {
name: "conflict".to_string(), name: "conflict".to_string(),
description: None, description: None,
rules: vec![ items: vec![
ValidationRule { ValidationSetItem::InlineRule {
name: "short".to_string(), name: Some("short".to_string()),
description: None, validation: ValidationSettings {
settings: ValidationSettings {
character_limits: Some(CharacterLimits::new(10)), character_limits: Some(CharacterLimits::new(10)),
..ValidationSettings::default() ..ValidationSettings::default()
}, },
}, },
ValidationRule { ValidationSetItem::InlineRule {
name: "long".to_string(), name: Some("long".to_string()),
description: None, validation: ValidationSettings {
settings: ValidationSettings {
character_limits: Some(CharacterLimits::new(20)), character_limits: Some(CharacterLimits::new(20)),
..ValidationSettings::default() ..ValidationSettings::default()
}, },
@@ -113,6 +145,6 @@ mod tests {
], ],
}; };
assert!(set.resolve_settings().is_err()); assert!(set.resolve_settings_with_rules(|_| None).is_err());
} }
} }