From 0600d3deaa0c66bd37cb371e0e2ad28fdbd367c1 Mon Sep 17 00:00:00 2001 From: Priec Date: Sun, 3 May 2026 10:34:59 +0200 Subject: [PATCH] table validation for the client from the server --- common/build.rs | 32 ++ common/proto/table_validation.proto | 152 +++++++-- common/src/proto/descriptor.bin | Bin 61043 -> 66544 bytes common/src/proto/komp_ac.table_validation.rs | 312 +++++++++++++++++-- server | 2 +- 5 files changed, 437 insertions(+), 61 deletions(-) diff --git a/common/build.rs b/common/build.rs index 41b5b4b..da17cd4 100644 --- a/common/build.rs +++ b/common/build.rs @@ -24,6 +24,14 @@ fn main() -> Result<(), Box> { ".komp_ac.table_validation.PatternRule", "#[derive(serde::Serialize, serde::Deserialize)]", ) + .type_attribute( + ".komp_ac.table_validation.PatternPosition", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_validation.CharacterConstraint", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) .type_attribute( ".komp_ac.table_validation.PatternRules", "#[derive(serde::Serialize, serde::Deserialize)]", @@ -32,6 +40,14 @@ fn main() -> Result<(), Box> { ".komp_ac.table_validation.CustomFormatter", "#[derive(serde::Serialize, serde::Deserialize)]", ) + .type_attribute( + ".komp_ac.table_validation.FormatterOption", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_validation.AllowedValues", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) .type_attribute( ".komp_ac.table_validation.UpdateFieldValidationRequest", "#[derive(serde::Serialize, serde::Deserialize)]", @@ -40,11 +56,27 @@ fn main() -> Result<(), Box> { ".komp_ac.table_validation.UpdateFieldValidationResponse", "#[derive(serde::Serialize, serde::Deserialize)]", ) + .type_attribute( + ".komp_ac.table_validation.ReplaceTableValidationRequest", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) + .type_attribute( + ".komp_ac.table_validation.ReplaceTableValidationResponse", + "#[derive(serde::Serialize, serde::Deserialize)]", + ) // Enum -> readable strings in JSON ("BYTES", "DISPLAY_WIDTH") .type_attribute( ".komp_ac.table_validation.CountMode", "#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]", ) + .type_attribute( + ".komp_ac.table_validation.PatternPositionKind", + "#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]", + ) + .type_attribute( + ".komp_ac.table_validation.CharacterConstraintKind", + "#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]", + ) .type_attribute( ".komp_ac.table_definition.ColumnDefinition", "#[derive(serde::Serialize, serde::Deserialize)]", diff --git a/common/proto/table_validation.proto b/common/proto/table_validation.proto index 1356123..ed13448 100644 --- a/common/proto/table_validation.proto +++ b/common/proto/table_validation.proto @@ -2,31 +2,55 @@ syntax = "proto3"; package komp_ac.table_validation; +// This proto is the canonical server-side storage and distribution contract for +// client validation configuration. +// +// Design goals: +// - The server stores the entire field validation definition in one structured payload. +// - Clients fetch the validation rules for a table in one batch and map them to +// their local validation/runtime system (for example canvas). +// - Common validation must be represented as typed data, not as string mini-languages. +// +// Important split: +// - limits / pattern / allowed_values / required are validation rules. +// - mask / formatter are presentation and input-shaping metadata for clients. + // Request validation rules for a table message GetTableValidationRequest { string profileName = 1; string tableName = 2; } -// Response with field-level validations; if a field is omitted, -// no validation is applied (default unspecified). +// Response with field-level validations for the whole table. +// If a field is omitted, no validation configuration exists for that field. message TableValidationResponse { repeated FieldValidation fields = 1; } -// Field-level validation (extensible for future kinds) +// Field-level validation definition stored on the server and distributed to clients. message FieldValidation { // MUST match your frontend FormState.dataKey for the column string dataKey = 1; - // Current: only CharacterLimits. More rules can be added later. + // Validation 1: length and counting rules. CharacterLimits limits = 10; - // Future expansion: - PatternRules pattern = 11; // Validation 2 - optional CustomFormatter formatter = 14; // Validation 4 – custom formatting logic + + // Validation 2: position-based character constraints. + PatternRules pattern = 11; + + // Exact-value whitelist. + AllowedValues allowed_values = 12; + + // Client-side hint that this field participates in external/asynchronous validation UI. + bool external_validation_enabled = 13; + + // Client-side formatter metadata. This is intentionally data-only, not executable code. + optional CustomFormatter formatter = 14; + + // Client-side display mask metadata. The server stores raw data without mask literals. DisplayMask mask = 3; - // ExternalValidation external = 13; - // CustomFormatter formatter = 14; + + // Field must be provided / treated as required by clients and server enforcement layers. bool required = 4; } @@ -38,7 +62,8 @@ enum CountMode { DISPLAY_WIDTH = 3; } -// Character limit validation (Validation 1) +// Character limit validation (Validation 1). +// These rules map directly to canvas CharacterLimits. message CharacterLimits { // When zero, the field is considered "not set". If both min/max are zero, // the server should avoid sending this FieldValidation (no validation). @@ -51,39 +76,91 @@ message CharacterLimits { CountMode countMode = 4; // defaults to CHARS if unspecified } -// Mask for pretty display +// Mask for pretty display only. +// +// This is not a validation rule by itself. It exists so clients can render and +// navigate masked input while still storing raw values server-side. message DisplayMask { string pattern = 1; // e.g., "(###) ###-####" or "####-##-##" string input_char = 2; // e.g., "#" optional string template_char = 3; // e.g., "_" } -// One position‑based validation rule, similar to CharacterFilter + PositionRange -message PatternRule { - // Range descriptor: how far the rule applies - // Examples: - // - "0" → Single position 0 - // - "0-3" → Range 0..3 inclusive - // - "from:5" → From position 5 onward - // - "0,2,5" → Multiple discrete positions - string range = 1; - - // Character filter type, case‑insensitive keywords: - // "ALPHABETIC", "NUMERIC", "ALPHANUMERIC", - // "ONEOF()", "EXACT(:)", "CUSTOM()" - string filter = 2; +// Which positions a pattern rule applies to. +// This exists instead of a string syntax like "0-3" so the server can validate +// the structure directly and clients do not need to parse a DSL. +message PatternPosition { + PatternPositionKind kind = 1; + uint32 single = 2; + uint32 start = 3; + uint32 end = 4; + repeated uint32 positions = 5; } +enum PatternPositionKind { + PATTERN_POSITION_KIND_UNSPECIFIED = 0; + PATTERN_POSITION_SINGLE = 1; + PATTERN_POSITION_RANGE = 2; + PATTERN_POSITION_FROM = 3; + PATTERN_POSITION_MULTIPLE = 4; +} + +// What type of character constraint a pattern rule applies. +// This mirrors the typed character filters used by canvas. +message CharacterConstraint { + CharacterConstraintKind kind = 1; + + // Used when kind == CHARACTER_CONSTRAINT_EXACT. + optional string exact = 2; + + // Used when kind == CHARACTER_CONSTRAINT_ONE_OF. + repeated string one_of = 3; + + // Used when kind == CHARACTER_CONSTRAINT_REGEX. + optional string regex = 4; +} + +enum CharacterConstraintKind { + CHARACTER_CONSTRAINT_KIND_UNSPECIFIED = 0; + CHARACTER_CONSTRAINT_ALPHABETIC = 1; + CHARACTER_CONSTRAINT_NUMERIC = 2; + CHARACTER_CONSTRAINT_ALPHANUMERIC = 3; + CHARACTER_CONSTRAINT_EXACT = 4; + CHARACTER_CONSTRAINT_ONE_OF = 5; + CHARACTER_CONSTRAINT_REGEX = 6; +} + +// One position-based validation rule, similar to canvas PositionFilter. +message PatternRule { + PatternPosition position = 1; + CharacterConstraint constraint = 2; +} + +// Client-side formatter metadata. +// The formatter "type" is intended to be resolved by a client-side formatter registry. message CustomFormatter { // Formatter type identifier; handled client‑side. // Examples: "PSCFormatter", "PhoneFormatter", "CreditCardFormatter", "DateFormatter" string type = 1; - // Optional free‑text note or parameters (e.g. locale, pattern) - optional string description = 2; + repeated FormatterOption options = 2; + optional string description = 3; } -// Collection of pattern rules for one field +message FormatterOption { + string key = 1; + string value = 2; +} + +// Exact-value whitelist configuration. +// This maps to canvas AllowedValues semantics. +message AllowedValues { + repeated string values = 1; + bool allow_empty = 2; + bool case_insensitive = 3; +} + +// Collection of pattern rules for one field. message PatternRules { // All rules that make up the validation logic repeated PatternRule rules = 1; @@ -92,11 +169,15 @@ message PatternRules { optional string description = 2; } -// Service to fetch validations for a table +// Service for storing and fetching field-validation definitions. service TableValidationService { rpc GetTableValidation(GetTableValidationRequest) returns (TableValidationResponse); + // Upsert a single field validation definition. rpc UpdateFieldValidation(UpdateFieldValidationRequest) returns (UpdateFieldValidationResponse); + + // Replace the full validation definition set for a table in one transaction. + rpc ReplaceTableValidation(ReplaceTableValidationRequest) returns (ReplaceTableValidationResponse); } message UpdateFieldValidationRequest { @@ -110,3 +191,16 @@ message UpdateFieldValidationResponse { bool success = 1; string message = 2; } + +message ReplaceTableValidationRequest { + string profileName = 1; + string tableName = 2; + + // Full replacement set. Fields omitted here are removed from the stored config. + repeated FieldValidation fields = 3; +} + +message ReplaceTableValidationResponse { + bool success = 1; + string message = 2; +} diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 8be89c34a7a130f1524e0725a5f9cefe2cca4b48..73620a79cc6c70998321cc16b5e2921f5b67b653 100644 GIT binary patch delta 8366 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 delta 3234 zcmYjTO>7&-73R(^|3x9@wu z^X9$z*H4W9{8|6<>f7woIdAdpf3m+aZ}Oe5PRc+1llIaL-gDP#x9z5zKdRd101r1U zw`?ahp-si}J}?J)q~ugVN;3Ibq0H;v{(F<&|GcG>ue^^n@6HE#eg0E+Qu+Cq#&7@q zB9EAc7OH#Gt7G2Uha-HRGb3ag;ZV$b$>J;GAFmxl?2 z3B>{gVkABgARTBZsnj5fvLPkXpcd__X(WrAfedQV^Mm|{h$OC=MsKK3OE;S$@vZ!Ze1K(Ri|jRow_TWmarO)s$&<$RM9S3N7c4Ca$60%;FR!jdJbtI5AxF+ zyC?(FD33F2QaS}N2=M?#fO^sag4&tku>k4jzJ6wWg5N@X%ryE#7qs-E*mUe_akgq7 z+tueIo3h_*+iuG_tlFYfZ;H}U`>1J)A2@EYHSI^mV7@;##9u*m8AX-w{`k@DPC}Ij zpFl{&&q@D4C{H3Wz_%qGqCs{c{jyj)YBp`Ry&~#v^|4s1SWT;dX4rOWPP;WHcIxoj zMEA5r!E(i+Ev#Y@<*HgBp*(b&jA_Vwko6Pl5G3nnG7!S?;P5EVNyp68lWZiN5SxBR z_L~h0dDh((p6L@0PS0jOFwS5+$@-Kv$~t*=FhH=KObzoTX&WHbVKbST=j~h-&))Jx z;i%QF*Qk^=+z7`li)y{>6apVQjxnWk<*t1e13j z7OBV+>?|Ml9q3?AAl@b172PzZLkn8^iud%FGbz9Ki9U_q|42hQb&5XgUHR1~R#r7X zK7o;q%YcahXW6soC`2HLX8mp@fV$0g)hM&su9^n5S*U$isv)MFm;L;E%Q@94I%81I z^~lDDDKeZ>eGdfX9P0NisSGgh{o(T$X6IRtJQQ8#Jl}ghK*UIhTY1fiM+hoh8zL(S+oPqTMPqokqLfToIM}BT=%NqFwQ4nHu^nj_YUD8W?UX zT!>kb$z=roI`r?^kKYsbFefm7gYZO7v1S(}&nNlra&vPF!f^}Lqn2}Q`@B-KUR$|F zR5G>+bZY7v=ItY^SyUFY^I1uh%+3z>kb|&9(LoY+`!s}>e{4t>gwcGU;!N-3E0arf#WeQ#INc!+0&A z_Sn7+Y{eSRq1h5sbc3)w3RV@RYxvJAY-Hc@%NjO9UR{TXyj*#89e~K0IjX31x$&7N`tgvGPhWCszsdI(|G z9Hiz08GIkC-X}fCKs;oJN<={(Mur0fwZoC+0730=?Lf9p%rxxK8-DA=pwqEqw1^BN zMwKn8XDI+mP*RNp1ZGJ+OMzgoq@JZQ3ZtwxKLFIqT{Q}-te&NiL9L9%?|Vap6?UG> z`p4wBtaSCrWrj0QdC(9OgeIWAc6Cs09WDHOKVN8;F=VZ@MsX)2N2(e%5#q z>($tVG#6(!vaT_I&c|ieH5Q?|89-pxqW&-eg1s7s355}-2wjBl6Dey~MGq0lRP;bl Mb5Fkc{p6kh1C0mnwEzGB diff --git a/common/src/proto/komp_ac.table_validation.rs b/common/src/proto/komp_ac.table_validation.rs index befe58c..bbf3bce 100644 --- a/common/src/proto/komp_ac.table_validation.rs +++ b/common/src/proto/komp_ac.table_validation.rs @@ -7,40 +7,45 @@ pub struct GetTableValidationRequest { #[prost(string, tag = "2")] pub table_name: ::prost::alloc::string::String, } -/// Response with field-level validations; if a field is omitted, -/// no validation is applied (default unspecified). +/// Response with field-level validations for the whole table. +/// If a field is omitted, no validation configuration exists for that field. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct TableValidationResponse { #[prost(message, repeated, tag = "1")] pub fields: ::prost::alloc::vec::Vec, } -/// Field-level validation (extensible for future kinds) +/// Field-level validation definition stored on the server and distributed to clients. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct FieldValidation { /// MUST match your frontend FormState.dataKey for the column #[prost(string, tag = "1")] pub data_key: ::prost::alloc::string::String, - /// Current: only CharacterLimits. More rules can be added later. + /// Validation 1: length and counting rules. #[prost(message, optional, tag = "10")] pub limits: ::core::option::Option, - /// Future expansion: - /// - /// Validation 2 + /// Validation 2: position-based character constraints. #[prost(message, optional, tag = "11")] pub pattern: ::core::option::Option, - /// Validation 4 – custom formatting logic + /// Exact-value whitelist. + #[prost(message, optional, tag = "12")] + pub allowed_values: ::core::option::Option, + /// Client-side hint that this field participates in external/asynchronous validation UI. + #[prost(bool, tag = "13")] + pub external_validation_enabled: bool, + /// Client-side formatter metadata. This is intentionally data-only, not executable code. #[prost(message, optional, tag = "14")] pub formatter: ::core::option::Option, + /// Client-side display mask metadata. The server stores raw data without mask literals. #[prost(message, optional, tag = "3")] pub mask: ::core::option::Option, - /// ExternalValidation external = 13; - /// CustomFormatter formatter = 14; + /// Field must be provided / treated as required by clients and server enforcement layers. #[prost(bool, tag = "4")] pub required: bool, } -/// Character limit validation (Validation 1) +/// Character limit validation (Validation 1). +/// These rules map directly to canvas CharacterLimits. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, Copy, PartialEq, ::prost::Message)] pub struct CharacterLimits { @@ -57,7 +62,10 @@ pub struct CharacterLimits { #[prost(enumeration = "CountMode", tag = "4")] pub count_mode: i32, } -/// Mask for pretty display +/// Mask for pretty display only. +/// +/// This is not a validation rule by itself. It exists so clients can render and +/// navigate masked input while still storing raw values server-side. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DisplayMask { @@ -71,24 +79,51 @@ pub struct DisplayMask { #[prost(string, optional, tag = "3")] pub template_char: ::core::option::Option<::prost::alloc::string::String>, } -/// One position‑based validation rule, similar to CharacterFilter + PositionRange +/// Which positions a pattern rule applies to. +/// This exists instead of a string syntax like "0-3" so the server can validate +/// the structure directly and clients do not need to parse a DSL. +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PatternPosition { + #[prost(enumeration = "PatternPositionKind", tag = "1")] + pub kind: i32, + #[prost(uint32, tag = "2")] + pub single: u32, + #[prost(uint32, tag = "3")] + pub start: u32, + #[prost(uint32, tag = "4")] + pub end: u32, + #[prost(uint32, repeated, tag = "5")] + pub positions: ::prost::alloc::vec::Vec, +} +/// What type of character constraint a pattern rule applies. +/// This mirrors the typed character filters used by canvas. +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CharacterConstraint { + #[prost(enumeration = "CharacterConstraintKind", tag = "1")] + pub kind: i32, + /// Used when kind == CHARACTER_CONSTRAINT_EXACT. + #[prost(string, optional, tag = "2")] + pub exact: ::core::option::Option<::prost::alloc::string::String>, + /// Used when kind == CHARACTER_CONSTRAINT_ONE_OF. + #[prost(string, repeated, tag = "3")] + pub one_of: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// Used when kind == CHARACTER_CONSTRAINT_REGEX. + #[prost(string, optional, tag = "4")] + pub regex: ::core::option::Option<::prost::alloc::string::String>, +} +/// One position-based validation rule, similar to canvas PositionFilter. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PatternRule { - /// Range descriptor: how far the rule applies - /// Examples: - /// - "0" → Single position 0 - /// - "0-3" → Range 0..3 inclusive - /// - "from:5" → From position 5 onward - /// - "0,2,5" → Multiple discrete positions - #[prost(string, tag = "1")] - pub range: ::prost::alloc::string::String, - /// Character filter type, case‑insensitive keywords: - /// "ALPHABETIC", "NUMERIC", "ALPHANUMERIC", - /// "ONEOF()", "EXACT(:)", "CUSTOM()" - #[prost(string, tag = "2")] - pub filter: ::prost::alloc::string::String, + #[prost(message, optional, tag = "1")] + pub position: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub constraint: ::core::option::Option, } +/// Client-side formatter metadata. +/// The formatter "type" is intended to be resolved by a client-side formatter registry. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct CustomFormatter { @@ -96,11 +131,32 @@ pub struct CustomFormatter { /// Examples: "PSCFormatter", "PhoneFormatter", "CreditCardFormatter", "DateFormatter" #[prost(string, tag = "1")] pub r#type: ::prost::alloc::string::String, - /// Optional free‑text note or parameters (e.g. locale, pattern) - #[prost(string, optional, tag = "2")] + #[prost(message, repeated, tag = "2")] + pub options: ::prost::alloc::vec::Vec, + #[prost(string, optional, tag = "3")] pub description: ::core::option::Option<::prost::alloc::string::String>, } -/// Collection of pattern rules for one field +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FormatterOption { + #[prost(string, tag = "1")] + pub key: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub value: ::prost::alloc::string::String, +} +/// Exact-value whitelist configuration. +/// This maps to canvas AllowedValues semantics. +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AllowedValues { + #[prost(string, repeated, tag = "1")] + pub values: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(bool, tag = "2")] + pub allow_empty: bool, + #[prost(bool, tag = "3")] + pub case_insensitive: bool, +} +/// Collection of pattern rules for one field. #[derive(serde::Serialize, serde::Deserialize)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct PatternRules { @@ -131,6 +187,25 @@ pub struct UpdateFieldValidationResponse { #[prost(string, tag = "2")] pub message: ::prost::alloc::string::String, } +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReplaceTableValidationRequest { + #[prost(string, tag = "1")] + pub profile_name: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub table_name: ::prost::alloc::string::String, + /// Full replacement set. Fields omitted here are removed from the stored config. + #[prost(message, repeated, tag = "3")] + pub fields: ::prost::alloc::vec::Vec, +} +#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReplaceTableValidationResponse { + #[prost(bool, tag = "1")] + pub success: bool, + #[prost(string, tag = "2")] + pub message: ::prost::alloc::string::String, +} /// Character length counting mode #[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] @@ -167,6 +242,90 @@ impl CountMode { } } } +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum PatternPositionKind { + Unspecified = 0, + PatternPositionSingle = 1, + PatternPositionRange = 2, + PatternPositionFrom = 3, + PatternPositionMultiple = 4, +} +impl PatternPositionKind { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "PATTERN_POSITION_KIND_UNSPECIFIED", + Self::PatternPositionSingle => "PATTERN_POSITION_SINGLE", + Self::PatternPositionRange => "PATTERN_POSITION_RANGE", + Self::PatternPositionFrom => "PATTERN_POSITION_FROM", + Self::PatternPositionMultiple => "PATTERN_POSITION_MULTIPLE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "PATTERN_POSITION_KIND_UNSPECIFIED" => Some(Self::Unspecified), + "PATTERN_POSITION_SINGLE" => Some(Self::PatternPositionSingle), + "PATTERN_POSITION_RANGE" => Some(Self::PatternPositionRange), + "PATTERN_POSITION_FROM" => Some(Self::PatternPositionFrom), + "PATTERN_POSITION_MULTIPLE" => Some(Self::PatternPositionMultiple), + _ => None, + } + } +} +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum CharacterConstraintKind { + Unspecified = 0, + CharacterConstraintAlphabetic = 1, + CharacterConstraintNumeric = 2, + CharacterConstraintAlphanumeric = 3, + CharacterConstraintExact = 4, + CharacterConstraintOneOf = 5, + CharacterConstraintRegex = 6, +} +impl CharacterConstraintKind { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "CHARACTER_CONSTRAINT_KIND_UNSPECIFIED", + Self::CharacterConstraintAlphabetic => "CHARACTER_CONSTRAINT_ALPHABETIC", + Self::CharacterConstraintNumeric => "CHARACTER_CONSTRAINT_NUMERIC", + Self::CharacterConstraintAlphanumeric => "CHARACTER_CONSTRAINT_ALPHANUMERIC", + Self::CharacterConstraintExact => "CHARACTER_CONSTRAINT_EXACT", + Self::CharacterConstraintOneOf => "CHARACTER_CONSTRAINT_ONE_OF", + Self::CharacterConstraintRegex => "CHARACTER_CONSTRAINT_REGEX", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "CHARACTER_CONSTRAINT_KIND_UNSPECIFIED" => Some(Self::Unspecified), + "CHARACTER_CONSTRAINT_ALPHABETIC" => { + Some(Self::CharacterConstraintAlphabetic) + } + "CHARACTER_CONSTRAINT_NUMERIC" => Some(Self::CharacterConstraintNumeric), + "CHARACTER_CONSTRAINT_ALPHANUMERIC" => { + Some(Self::CharacterConstraintAlphanumeric) + } + "CHARACTER_CONSTRAINT_EXACT" => Some(Self::CharacterConstraintExact), + "CHARACTER_CONSTRAINT_ONE_OF" => Some(Self::CharacterConstraintOneOf), + "CHARACTER_CONSTRAINT_REGEX" => Some(Self::CharacterConstraintRegex), + _ => None, + } + } +} /// Generated client implementations. pub mod table_validation_service_client { #![allow( @@ -178,7 +337,7 @@ pub mod table_validation_service_client { )] use tonic::codegen::*; use tonic::codegen::http::Uri; - /// Service to fetch validations for a table + /// Service for storing and fetching field-validation definitions. #[derive(Debug, Clone)] pub struct TableValidationServiceClient { inner: tonic::client::Grpc, @@ -290,6 +449,7 @@ pub mod table_validation_service_client { ); self.inner.unary(req, path, codec).await } + /// Upsert a single field validation definition. pub async fn update_field_validation( &mut self, request: impl tonic::IntoRequest, @@ -319,6 +479,36 @@ pub mod table_validation_service_client { ); self.inner.unary(req, path, codec).await } + /// Replace the full validation definition set for a table in one transaction. + pub async fn replace_table_validation( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.table_validation.TableValidationService/ReplaceTableValidation", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "komp_ac.table_validation.TableValidationService", + "ReplaceTableValidation", + ), + ); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -341,6 +531,7 @@ pub mod table_validation_service_server { tonic::Response, tonic::Status, >; + /// Upsert a single field validation definition. async fn update_field_validation( &self, request: tonic::Request, @@ -348,8 +539,16 @@ pub mod table_validation_service_server { tonic::Response, tonic::Status, >; + /// Replace the full validation definition set for a table in one transaction. + async fn replace_table_validation( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } - /// Service to fetch validations for a table + /// Service for storing and fetching field-validation definitions. #[derive(Debug)] pub struct TableValidationServiceServer { inner: Arc, @@ -527,6 +726,57 @@ pub mod table_validation_service_server { }; Box::pin(fut) } + "/komp_ac.table_validation.TableValidationService/ReplaceTableValidation" => { + #[allow(non_camel_case_types)] + struct ReplaceTableValidationSvc( + pub Arc, + ); + impl< + T: TableValidationService, + > tonic::server::UnaryService + for ReplaceTableValidationSvc { + type Response = super::ReplaceTableValidationResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::replace_table_validation( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ReplaceTableValidationSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } _ => { Box::pin(async move { let mut response = http::Response::new( diff --git a/server b/server index 8dbe9cc..8a84218 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 8dbe9cc14c6834ff12195c5a7958f2a593f8cc81 +Subproject commit 8a84218de35d7365a4231ec0c35d768b75ee3ed3