From 17a13569d823dcef35bc81a7ff983d3bb204f1c8 Mon Sep 17 00:00:00 2001 From: Priec Date: Wed, 6 May 2026 20:33:53 +0200 Subject: [PATCH] cargo fmt --- canvas | 2 +- client | 2 +- common/src/search.rs | 10 +- search/src/lib.rs | 20 ++- search/src/query_builder.rs | 9 +- server | 2 +- validation-core/src/config.rs | 6 +- validation-core/src/rules/character_limits.rs | 155 ++++++++++-------- validation-core/src/rules/display_mask.rs | 53 +++--- validation-core/src/rules/pattern_rules.rs | 50 +++--- 10 files changed, 174 insertions(+), 135 deletions(-) diff --git a/canvas b/canvas index a4f0216..e6c942d 160000 --- a/canvas +++ b/canvas @@ -1 +1 @@ -Subproject commit a4f0216878c2b1a2fc662b2d93f38d2ffe0907ca +Subproject commit e6c942dd412e7b7ea8ae04412528d225ec794017 diff --git a/client b/client index 14a8b4f..25a901f 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 14a8b4ffbeb30cf04c1cd59a1e8fcd0313306e66 +Subproject commit 25a901ff5e0124485bee95e7133ca641eeca306f diff --git a/common/src/search.rs b/common/src/search.rs index b6f9123..98c40f9 100644 --- a/common/src/search.rs +++ b/common/src/search.rs @@ -1,8 +1,8 @@ use std::path::{Path, PathBuf}; use tantivy::schema::{ - Field, IndexRecordOption, JsonObjectOptions, Schema, TextFieldIndexing, Term, INDEXED, - STORED, STRING, + Field, IndexRecordOption, JsonObjectOptions, Schema, Term, TextFieldIndexing, INDEXED, STORED, + STRING, }; use tantivy::tokenizer::{ AsciiFoldingFilter, LowerCaser, NgramTokenizer, RawTokenizer, RemoveLongFilter, @@ -67,11 +67,7 @@ pub fn create_search_schema() -> Schema { schema_builder.build() } -fn json_options( - tokenizer_name: &str, - with_positions: bool, - stored: bool, -) -> JsonObjectOptions { +fn json_options(tokenizer_name: &str, with_positions: bool, stored: bool) -> JsonObjectOptions { let index_option = if with_positions { IndexRecordOption::WithFreqsAndPositions } else { diff --git a/search/src/lib.rs b/search/src/lib.rs index be0654f..8034594 100644 --- a/search/src/lib.rs +++ b/search/src/lib.rs @@ -5,8 +5,8 @@ use std::path::Path; use std::sync::{Arc, Mutex}; use common::proto::komp_ac::search::searcher_server::Searcher; -use common::proto::komp_ac::search::{search_response::Hit, SearchRequest, SearchResponse}; pub use common::proto::komp_ac::search::searcher_server::SearcherServer; +use common::proto::komp_ac::search::{search_response::Hit, SearchRequest, SearchResponse}; use common::search::{register_tokenizers, search_index_path, SchemaFields}; use query_builder::{build_master_query, ConstraintMode, SearchConstraint}; use sqlx::{PgPool, Row}; @@ -34,7 +34,10 @@ impl SearcherService { } } - async fn run_rpc(&self, request: Request) -> Result, Status> { + async fn run_rpc( + &self, + request: Request, + ) -> Result, Status> { let req = request.into_inner(); let normalized = normalize_request(req)?; @@ -276,7 +279,9 @@ fn normalize_request(req: SearchRequest) -> Result Result, Status> { - let master_query = - build_master_query(&profile.index, &profile.fields, free_query, must, table_filter)?; + let master_query = build_master_query( + &profile.index, + &profile.fields, + free_query, + must, + table_filter, + )?; let searcher = profile.reader.searcher(); let top_docs = searcher diff --git a/search/src/query_builder.rs b/search/src/query_builder.rs index 1f57bfe..43b9f6e 100644 --- a/search/src/query_builder.rs +++ b/search/src/query_builder.rs @@ -34,7 +34,9 @@ pub fn build_master_query( for constraint in must { let predicate = match constraint.mode { - ConstraintMode::Exact => exact_predicate(fields, &constraint.column, &constraint.query)?, + ConstraintMode::Exact => { + exact_predicate(fields, &constraint.column, &constraint.query)? + } ConstraintMode::Fuzzy => { fuzzy_predicate_scoped(fields, &constraint.column, &constraint.query)? } @@ -157,7 +159,10 @@ fn fuzzy_predicate_scoped( .collect(); layers.push(( Occur::Should, - Box::new(BoostQuery::new(Box::new(BooleanQuery::new(ngram_clauses)), 1.0)), + Box::new(BoostQuery::new( + Box::new(BooleanQuery::new(ngram_clauses)), + 1.0, + )), )); } diff --git a/server b/server index 99bc97f..b178fce 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 99bc97f771827f608491d0e18db3575a0c1c40dd +Subproject commit b178fce273424488e78edd7165e2bfc46eeff45f diff --git a/validation-core/src/config.rs b/validation-core/src/config.rs index 78222ad..a841965 100644 --- a/validation-core/src/config.rs +++ b/validation-core/src/config.rs @@ -167,7 +167,11 @@ impl ValidationSettings { &mut self.character_limits, &rule.character_limits, )?; - merge_singleton("allowed_values", &mut self.allowed_values, &rule.allowed_values)?; + merge_singleton( + "allowed_values", + &mut self.allowed_values, + &rule.allowed_values, + )?; merge_singleton("display_mask", &mut self.display_mask, &rule.display_mask)?; merge_singleton("formatter", &mut self.formatter, &rule.formatter)?; diff --git a/validation-core/src/rules/character_limits.rs b/validation-core/src/rules/character_limits.rs index f65950b..0179d1d 100644 --- a/validation-core/src/rules/character_limits.rs +++ b/validation-core/src/rules/character_limits.rs @@ -10,45 +10,43 @@ use unicode_width::UnicodeWidthStr; pub struct CharacterLimits { /// Maximum number of characters allowed (None = unlimited) max_length: Option, - + /// Minimum number of characters required (None = no minimum) min_length: Option, - + /// Warning threshold (warn when approaching max limit) warning_threshold: Option, - + /// Count mode: characters vs display width count_mode: CountMode, } /// How to count characters for limit checking -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -#[derive(Default)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] pub enum CountMode { /// Count actual characters (default) #[default] Characters, - + /// Count display width (useful for CJK characters) DisplayWidth, - + /// Count bytes (rarely used, but available) Bytes, } - /// Result of a character limit check #[derive(Debug, Clone, PartialEq, Eq)] pub enum LimitCheckResult { /// Within limits Ok, - + /// Approaching limit (warning) Warning { current: usize, max: usize }, - + /// At or exceeding limit (error) Exceeded { current: usize, max: usize }, - + /// Below minimum length TooShort { current: usize, min: usize }, } @@ -63,7 +61,7 @@ impl CharacterLimits { count_mode: CountMode::default(), } } - + /// Create new character limits with min and max pub fn new_range(min_length: usize, max_length: usize) -> Self { Self { @@ -93,39 +91,39 @@ impl CharacterLimits { count_mode: CountMode::default(), } } - + /// Set warning threshold (when to show warning before hitting limit) pub fn with_warning_threshold(mut self, threshold: usize) -> Self { self.warning_threshold = Some(threshold); self } - + /// Set count mode (characters vs display width vs bytes) pub fn with_count_mode(mut self, mode: CountMode) -> Self { self.count_mode = mode; self } - + /// Get maximum length pub fn max_length(&self) -> Option { self.max_length } - + /// Get minimum length pub fn min_length(&self) -> Option { self.min_length } - + /// Get warning threshold pub fn warning_threshold(&self) -> Option { self.warning_threshold } - + /// Get count mode pub fn count_mode(&self) -> CountMode { self.count_mode } - + /// Count characters/width/bytes according to the configured mode fn count(&self, text: &str) -> usize { match self.count_mode { @@ -134,7 +132,7 @@ impl CharacterLimits { CountMode::Bytes => text.len(), } } - + /// Check if inserting a character would exceed limits pub fn validate_insertion( &self, @@ -179,7 +177,7 @@ impl CharacterLimits { None // No validation issues } - + /// Validate the current content pub fn validate_content(&self, text: &str) -> Option { let count = self.count(text); @@ -207,52 +205,60 @@ impl CharacterLimits { } } } - + None // No validation issues } - + /// Get the current status of the text against limits pub fn check_limits(&self, text: &str) -> LimitCheckResult { let count = self.count(text); if let Some(max) = self.max_length { if count > max { - return LimitCheckResult::Exceeded { current: count, max }; + return LimitCheckResult::Exceeded { + current: count, + max, + }; } if let Some(warning_threshold) = self.warning_threshold { if count >= warning_threshold { - return LimitCheckResult::Warning { current: count, max }; + return LimitCheckResult::Warning { + current: count, + max, + }; } } } - + // Check min length if let Some(min) = self.min_length { if count < min { - return LimitCheckResult::TooShort { current: count, min }; + return LimitCheckResult::TooShort { + current: count, + min, + }; } } - + LimitCheckResult::Ok } - + /// Get a human-readable status string pub fn status_text(&self, text: &str) -> Option { match self.check_limits(text) { LimitCheckResult::Ok => { // Show current/max if we have a max limit - self.max_length.map(|max| format!("{}/{}", self.count(text), max)) - }, + self.max_length + .map(|max| format!("{}/{}", self.count(text), max)) + } LimitCheckResult::Warning { current, max } => { Some(format!("{current}/{max} (approaching limit)")) - }, + } LimitCheckResult::Exceeded { current, max } => { Some(format!("{current}/{max} (exceeded)")) - }, - LimitCheckResult::TooShort { current, min } => { - Some(format!("{current}/{min} minimum")) - }, + } + LimitCheckResult::TooShort { current, min } => Some(format!("{current}/{min} minimum")), } } pub fn allows_field_switch(&self, text: &str) -> bool { @@ -264,7 +270,7 @@ impl CharacterLimits { true // No minimum requirement, always allow switching } } - + /// Get reason why field switching is not allowed (if any) pub fn field_switch_block_reason(&self, text: &str) -> Option { if let Some(min) = self.min_length { @@ -307,128 +313,139 @@ mod tests { let limits = CharacterLimits::new(10); assert_eq!(limits.max_length(), Some(10)); assert_eq!(limits.min_length(), None); - + let range_limits = CharacterLimits::new_range(5, 15); assert_eq!(range_limits.min_length(), Some(5)); assert_eq!(range_limits.max_length(), Some(15)); } - + #[test] fn test_default_limits() { let limits = CharacterLimits::default(); assert_eq!(limits.max_length(), Some(30)); } - + #[test] fn test_character_counting() { let limits = CharacterLimits::new(5); - + // Test character mode (default) assert_eq!(limits.count("hello"), 5); assert_eq!(limits.count("héllo"), 5); // Accented character counts as 1 - + // Test display width mode let limits = limits.with_count_mode(CountMode::DisplayWidth); assert_eq!(limits.count("hello"), 5); - + // Test bytes mode let limits = limits.with_count_mode(CountMode::Bytes); assert_eq!(limits.count("hello"), 5); assert_eq!(limits.count("héllo"), 6); // é takes 2 bytes in UTF-8 } - + #[test] fn test_insertion_validation() { let limits = CharacterLimits::new(5); - + // Valid insertion let result = limits.validate_insertion("test", 4, 'x'); assert!(result.is_none()); // No validation issues - + // Invalid insertion (would exceed limit) let result = limits.validate_insertion("tests", 5, 'x'); assert!(result.is_some()); assert!(!result.unwrap().is_acceptable()); } - + #[test] fn test_content_validation() { let limits = CharacterLimits::new_range(3, 10); - + // Too short let result = limits.validate_content("hi"); assert!(result.is_some()); assert!(result.unwrap().is_acceptable()); // Warning, not error - + // Just right let result = limits.validate_content("hello"); assert!(result.is_none()); - + // Too long let result = limits.validate_content("hello world!"); assert!(result.is_some()); assert!(!result.unwrap().is_acceptable()); // Error } - + #[test] fn test_warning_threshold() { let limits = CharacterLimits::new(10).with_warning_threshold(8); - + // Below warning threshold let result = limits.validate_insertion("123456", 6, 'x'); assert!(result.is_none()); - + // At warning threshold let result = limits.validate_insertion("1234567", 7, 'x'); assert!(result.is_some()); // This brings us to 8 chars assert!(result.unwrap().is_acceptable()); // Warning, not error - + let result = limits.validate_insertion("12345678", 8, 'x'); assert!(result.is_none()); } - + #[test] fn test_status_text() { let limits = CharacterLimits::new(10); - + assert_eq!(limits.status_text("hello"), Some("5/10".to_string())); - + let limits = limits.with_warning_threshold(8); - assert_eq!(limits.status_text("12345678"), Some("8/10 (approaching limit)".to_string())); - assert_eq!(limits.status_text("1234567890x"), Some("11/10 (exceeded)".to_string())); + assert_eq!( + limits.status_text("12345678"), + Some("8/10 (approaching limit)".to_string()) + ); + assert_eq!( + limits.status_text("1234567890x"), + Some("11/10 (exceeded)".to_string()) + ); } - + #[test] fn test_field_switch_blocking() { let limits = CharacterLimits::new_range(3, 10); - + // Empty field: should allow switching assert!(limits.allows_field_switch("")); assert!(limits.field_switch_block_reason("").is_none()); - + // Field with content below minimum: should block switching assert!(!limits.allows_field_switch("hi")); assert!(limits.field_switch_block_reason("hi").is_some()); - assert!(limits.field_switch_block_reason("hi").unwrap().contains("at least 3 characters")); - + assert!(limits + .field_switch_block_reason("hi") + .unwrap() + .contains("at least 3 characters")); + // Field meeting minimum: should allow switching assert!(limits.allows_field_switch("hello")); assert!(limits.field_switch_block_reason("hello").is_none()); - + // Field exceeding maximum: should still allow switching (validation shows error but doesn't block) assert!(limits.allows_field_switch("this is way too long")); - assert!(limits.field_switch_block_reason("this is way too long").is_none()); + assert!(limits + .field_switch_block_reason("this is way too long") + .is_none()); } - + #[test] fn test_field_switch_no_minimum() { let limits = CharacterLimits::new(10); // Only max, no minimum - + // Should always allow switching when there's no minimum assert!(limits.allows_field_switch("")); assert!(limits.allows_field_switch("a")); assert!(limits.allows_field_switch("hello")); - + assert!(limits.field_switch_block_reason("").is_none()); assert!(limits.field_switch_block_reason("a").is_none()); } diff --git a/validation-core/src/rules/display_mask.rs b/validation-core/src/rules/display_mask.rs index c6870ee..1c43e47 100644 --- a/validation-core/src/rules/display_mask.rs +++ b/validation-core/src/rules/display_mask.rs @@ -3,23 +3,21 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[derive(Default)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum MaskDisplayMode { /// Only show separators as user types /// Example: "" → "", "123" → "123", "12345" → "(123) 45" #[default] Dynamic, - + /// Show full template with placeholders from start /// Example: "" → "(___) ___-____", "123" → "(123) ___-____" - Template { + Template { /// Character to use as placeholder for empty input positions - placeholder: char + placeholder: char, }, } - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DisplayMask { /// Mask pattern like "##-##-####" where # = input position, others are visual separators @@ -32,21 +30,21 @@ pub struct DisplayMask { impl DisplayMask { /// Create a new display mask with dynamic mode (current behavior) - /// + /// /// # Arguments /// * `pattern` - The mask pattern (e.g., "##-##-####", "(###) ###-####") /// * `input_char` - Character representing input positions (usually '#') - /// + /// /// # Examples /// ``` /// use validation_core::DisplayMask; /// /// // Phone number format /// let phone_mask = DisplayMask::new("(###) ###-####", '#'); - /// + /// /// // Date format /// let date_mask = DisplayMask::new("##/##/####", '#'); - /// + /// /// // Custom business format /// let employee_id = DisplayMask::new("EMP-####-##", '#'); /// ``` @@ -59,7 +57,7 @@ impl DisplayMask { } /// Set the display mode for this mask - /// + /// /// # Examples /// ``` /// use validation_core::{DisplayMask, MaskDisplayMode}; @@ -76,7 +74,7 @@ impl DisplayMask { } /// Set template mode with custom placeholder - /// + /// /// # Examples /// ``` /// use validation_core::DisplayMask; @@ -95,7 +93,9 @@ impl DisplayMask { pub fn apply_to_display(&self, raw_input: &str) -> String { match &self.display_mode { MaskDisplayMode::Dynamic => self.apply_dynamic(raw_input), - MaskDisplayMode::Template { placeholder } => self.apply_template(raw_input, *placeholder), + MaskDisplayMode::Template { placeholder } => { + self.apply_template(raw_input, *placeholder) + } } } @@ -158,7 +158,8 @@ impl DisplayMask { /// Check if a display position should accept cursor/input pub fn is_input_position(&self, display_position: usize) -> bool { - self.pattern.chars() + self.pattern + .chars() .nth(display_position) .map(|c| c == self.input_char) .unwrap_or(true) // Beyond pattern = accept input @@ -212,7 +213,7 @@ impl DisplayMask { pub fn prev_input_position(&self, display_pos: usize) -> Option { // Collect pattern chars with indices first, then search backwards let pattern_chars: Vec<(usize, char)> = self.pattern.chars().enumerate().collect(); - + // Search backwards from display_pos for &(i, pattern_char) in pattern_chars.iter().rev() { if i <= display_pos && pattern_char == self.input_char { @@ -268,11 +269,11 @@ mod tests { // User creates their own phone mask let dynamic = DisplayMask::new("(###) ###-####", '#'); let template = DisplayMask::new("(###) ###-####", '#').with_template('_'); - + // Dynamic mode assert_eq!(dynamic.apply_to_display(""), ""); assert_eq!(dynamic.apply_to_display("1234567890"), "(123) 456-7890"); - + // Template mode assert_eq!(template.apply_to_display(""), "(___) ___-____"); assert_eq!(template.apply_to_display("123"), "(123) ___-____"); @@ -284,7 +285,7 @@ mod tests { let us_date = DisplayMask::new("##/##/####", '#'); let eu_date = DisplayMask::new("##.##.####", '#'); let iso_date = DisplayMask::new("####-##-##", '#'); - + assert_eq!(us_date.apply_to_display("12252024"), "12/25/2024"); assert_eq!(eu_date.apply_to_display("25122024"), "25.12.2024"); assert_eq!(iso_date.apply_to_display("20241225"), "2024-12-25"); @@ -296,7 +297,7 @@ mod tests { let employee_id = DisplayMask::new("EMP-####-##", '#'); let product_code = DisplayMask::new("###-###-###", '#'); let invoice = DisplayMask::new("INV####/##", '#'); - + assert_eq!(employee_id.apply_to_display("123456"), "EMP-1234-56"); assert_eq!(product_code.apply_to_display("123456789"), "123-456-789"); assert_eq!(invoice.apply_to_display("123456"), "INV1234/56"); @@ -308,7 +309,7 @@ mod tests { let mask_with_x = DisplayMask::new("XXX-XX-XXXX", 'X'); let mask_with_hash = DisplayMask::new("###-##-####", '#'); let mask_with_n = DisplayMask::new("NNN-NN-NNNN", 'N'); - + assert_eq!(mask_with_x.apply_to_display("123456789"), "123-45-6789"); assert_eq!(mask_with_hash.apply_to_display("123456789"), "123-45-6789"); assert_eq!(mask_with_n.apply_to_display("123456789"), "123-45-6789"); @@ -320,28 +321,28 @@ mod tests { let underscores = DisplayMask::new("##-##", '#').with_template('_'); let dots = DisplayMask::new("##-##", '#').with_template('•'); let dashes = DisplayMask::new("##-##", '#').with_template('-'); - + assert_eq!(underscores.apply_to_display(""), "__-__"); assert_eq!(dots.apply_to_display(""), "••-••"); - assert_eq!(dashes.apply_to_display(""), "-----"); // Note: dashes blend with separator + assert_eq!(dashes.apply_to_display(""), "-----"); // Note: dashes blend with separator } #[test] fn test_position_mapping_user_patterns() { let custom = DisplayMask::new("ABC-###-XYZ", '#'); - + // Position mapping should work correctly with any pattern assert_eq!(custom.raw_pos_to_display_pos(0), 4); // First # at position 4 assert_eq!(custom.raw_pos_to_display_pos(1), 5); // Second # at position 5 assert_eq!(custom.raw_pos_to_display_pos(2), 6); // Third # at position 6 - + assert_eq!(custom.display_pos_to_raw_pos(4), 0); // Position 4 -> first input assert_eq!(custom.display_pos_to_raw_pos(5), 1); // Position 5 -> second input assert_eq!(custom.display_pos_to_raw_pos(6), 2); // Position 6 -> third input - + assert!(!custom.is_input_position(0)); // A assert!(!custom.is_input_position(3)); // - - assert!(custom.is_input_position(4)); // # + assert!(custom.is_input_position(4)); // # assert!(!custom.is_input_position(8)); // Y } } diff --git a/validation-core/src/rules/pattern_rules.rs b/validation-core/src/rules/pattern_rules.rs index 07ae225..2c9f869 100644 --- a/validation-core/src/rules/pattern_rules.rs +++ b/validation-core/src/rules/pattern_rules.rs @@ -84,8 +84,12 @@ impl PositionRange { pub fn positions_up_to(&self, max_length: usize) -> Vec { match self { PositionRange::Single(pos) => { - if *pos < max_length { vec![*pos] } else { vec![] } - }, + if *pos < max_length { + vec![*pos] + } else { + vec![] + } + } PositionRange::Range(start, end) => { let actual_end = (*end).min(max_length.saturating_sub(1)); if *start <= actual_end { @@ -93,20 +97,19 @@ impl PositionRange { } else { vec![] } - }, + } PositionRange::From(start) => { if *start < max_length { (*start..max_length).collect() } else { vec![] } - }, - PositionRange::Multiple(positions) => { - positions.iter() - .filter(|&&pos| pos < max_length) - .copied() - .collect() - }, + } + PositionRange::Multiple(positions) => positions + .iter() + .filter(|&&pos| pos < max_length) + .copied() + .collect(), } } } @@ -134,7 +137,7 @@ impl CharacterFilter { CharacterFilter::OneOf(chars) => { let char_list: String = chars.iter().collect(); format!("one of: {char_list}") - }, + } CharacterFilter::Custom(_) => "custom filter".to_string(), } } @@ -195,7 +198,11 @@ impl PatternFilters { } /// Validate a character at a specific position against all applicable filters - pub fn validate_char_at_position(&self, position: usize, character: char) -> Result<(), String> { + pub fn validate_char_at_position( + &self, + position: usize, + character: char, + ) -> Result<(), String> { for filter in &self.filters { if let Some(error) = filter.error_message(position, character) { return Err(error); @@ -252,7 +259,10 @@ mod tests { assert_eq!(PositionRange::From(2).positions_up_to(5), vec![2, 3, 4]); - assert_eq!(PositionRange::Multiple(vec![0, 2, 5]).positions_up_to(4), vec![0, 2]); + assert_eq!( + PositionRange::Multiple(vec![0, 2, 5]).positions_up_to(4), + vec![0, 2] + ); } #[test] @@ -277,10 +287,7 @@ mod tests { #[test] fn test_position_filter_validation() { - let filter = PositionFilter::new( - PositionRange::Range(0, 1), - CharacterFilter::Alphabetic, - ); + let filter = PositionFilter::new(PositionRange::Range(0, 1), CharacterFilter::Alphabetic); assert!(filter.validate_position(0, 'A')); assert!(filter.validate_position(1, 'b')); @@ -312,11 +319,10 @@ mod tests { #[test] fn test_custom_filter() { - let pattern = PatternFilters::new() - .add_filter(PositionFilter::new( - PositionRange::From(0), - CharacterFilter::Custom(Arc::new(|c| c.is_lowercase())), - )); + let pattern = PatternFilters::new().add_filter(PositionFilter::new( + PositionRange::From(0), + CharacterFilter::Custom(Arc::new(|c| c.is_lowercase())), + )); assert!(pattern.validate_text("hello").is_ok()); assert!(pattern.validate_text("Hello").is_err()); // Uppercase not allowed