cargo fmt

This commit is contained in:
Priec
2026-05-06 20:33:53 +02:00
parent 14f88e6a40
commit 17a13569d8
10 changed files with 174 additions and 135 deletions

2
canvas

Submodule canvas updated: a4f0216878...e6c942dd41

2
client

Submodule client updated: 14a8b4ffbe...25a901ff5e

View File

@@ -1,8 +1,8 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tantivy::schema::{ use tantivy::schema::{
Field, IndexRecordOption, JsonObjectOptions, Schema, TextFieldIndexing, Term, INDEXED, Field, IndexRecordOption, JsonObjectOptions, Schema, Term, TextFieldIndexing, INDEXED, STORED,
STORED, STRING, STRING,
}; };
use tantivy::tokenizer::{ use tantivy::tokenizer::{
AsciiFoldingFilter, LowerCaser, NgramTokenizer, RawTokenizer, RemoveLongFilter, AsciiFoldingFilter, LowerCaser, NgramTokenizer, RawTokenizer, RemoveLongFilter,
@@ -67,11 +67,7 @@ pub fn create_search_schema() -> Schema {
schema_builder.build() schema_builder.build()
} }
fn json_options( fn json_options(tokenizer_name: &str, with_positions: bool, stored: bool) -> JsonObjectOptions {
tokenizer_name: &str,
with_positions: bool,
stored: bool,
) -> JsonObjectOptions {
let index_option = if with_positions { let index_option = if with_positions {
IndexRecordOption::WithFreqsAndPositions IndexRecordOption::WithFreqsAndPositions
} else { } else {

View File

@@ -5,8 +5,8 @@ use std::path::Path;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use common::proto::komp_ac::search::searcher_server::Searcher; 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; 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 common::search::{register_tokenizers, search_index_path, SchemaFields};
use query_builder::{build_master_query, ConstraintMode, SearchConstraint}; use query_builder::{build_master_query, ConstraintMode, SearchConstraint};
use sqlx::{PgPool, Row}; use sqlx::{PgPool, Row};
@@ -34,7 +34,10 @@ impl SearcherService {
} }
} }
async fn run_rpc(&self, request: Request<SearchRequest>) -> Result<Response<SearchResponse>, Status> { async fn run_rpc(
&self,
request: Request<SearchRequest>,
) -> Result<Response<SearchResponse>, Status> {
let req = request.into_inner(); let req = request.into_inner();
let normalized = normalize_request(req)?; let normalized = normalize_request(req)?;
@@ -276,7 +279,9 @@ fn normalize_request(req: SearchRequest) -> Result<NormalizedSearchRequest, Stat
}); });
} }
let limit = req.limit.map(|value| (value as usize).min(HARD_RESULT_LIMIT)); let limit = req
.limit
.map(|value| (value as usize).min(HARD_RESULT_LIMIT));
Ok(NormalizedSearchRequest { Ok(NormalizedSearchRequest {
profile_name: profile_name.to_string(), profile_name: profile_name.to_string(),
@@ -335,8 +340,13 @@ async fn run_search(
must: &[SearchConstraint], must: &[SearchConstraint],
limit: usize, limit: usize,
) -> Result<Vec<Hit>, Status> { ) -> Result<Vec<Hit>, Status> {
let master_query = let master_query = build_master_query(
build_master_query(&profile.index, &profile.fields, free_query, must, table_filter)?; &profile.index,
&profile.fields,
free_query,
must,
table_filter,
)?;
let searcher = profile.reader.searcher(); let searcher = profile.reader.searcher();
let top_docs = searcher let top_docs = searcher

View File

@@ -34,7 +34,9 @@ pub fn build_master_query(
for constraint in must { for constraint in must {
let predicate = match constraint.mode { 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 => { ConstraintMode::Fuzzy => {
fuzzy_predicate_scoped(fields, &constraint.column, &constraint.query)? fuzzy_predicate_scoped(fields, &constraint.column, &constraint.query)?
} }
@@ -157,7 +159,10 @@ fn fuzzy_predicate_scoped(
.collect(); .collect();
layers.push(( layers.push((
Occur::Should, 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,
)),
)); ));
} }

2
server

Submodule server updated: 99bc97f771...b178fce273

View File

@@ -167,7 +167,11 @@ impl ValidationSettings {
&mut self.character_limits, &mut self.character_limits,
&rule.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("display_mask", &mut self.display_mask, &rule.display_mask)?;
merge_singleton("formatter", &mut self.formatter, &rule.formatter)?; merge_singleton("formatter", &mut self.formatter, &rule.formatter)?;

View File

@@ -10,45 +10,43 @@ use unicode_width::UnicodeWidthStr;
pub struct CharacterLimits { pub struct CharacterLimits {
/// Maximum number of characters allowed (None = unlimited) /// Maximum number of characters allowed (None = unlimited)
max_length: Option<usize>, max_length: Option<usize>,
/// Minimum number of characters required (None = no minimum) /// Minimum number of characters required (None = no minimum)
min_length: Option<usize>, min_length: Option<usize>,
/// Warning threshold (warn when approaching max limit) /// Warning threshold (warn when approaching max limit)
warning_threshold: Option<usize>, warning_threshold: Option<usize>,
/// Count mode: characters vs display width /// Count mode: characters vs display width
count_mode: CountMode, count_mode: CountMode,
} }
/// How to count characters for limit checking /// How to count characters for limit checking
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
#[derive(Default)]
pub enum CountMode { pub enum CountMode {
/// Count actual characters (default) /// Count actual characters (default)
#[default] #[default]
Characters, Characters,
/// Count display width (useful for CJK characters) /// Count display width (useful for CJK characters)
DisplayWidth, DisplayWidth,
/// Count bytes (rarely used, but available) /// Count bytes (rarely used, but available)
Bytes, Bytes,
} }
/// Result of a character limit check /// Result of a character limit check
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum LimitCheckResult { pub enum LimitCheckResult {
/// Within limits /// Within limits
Ok, Ok,
/// Approaching limit (warning) /// Approaching limit (warning)
Warning { current: usize, max: usize }, Warning { current: usize, max: usize },
/// At or exceeding limit (error) /// At or exceeding limit (error)
Exceeded { current: usize, max: usize }, Exceeded { current: usize, max: usize },
/// Below minimum length /// Below minimum length
TooShort { current: usize, min: usize }, TooShort { current: usize, min: usize },
} }
@@ -63,7 +61,7 @@ impl CharacterLimits {
count_mode: CountMode::default(), count_mode: CountMode::default(),
} }
} }
/// Create new character limits with min and max /// Create new character limits with min and max
pub fn new_range(min_length: usize, max_length: usize) -> Self { pub fn new_range(min_length: usize, max_length: usize) -> Self {
Self { Self {
@@ -93,39 +91,39 @@ impl CharacterLimits {
count_mode: CountMode::default(), count_mode: CountMode::default(),
} }
} }
/// Set warning threshold (when to show warning before hitting limit) /// Set warning threshold (when to show warning before hitting limit)
pub fn with_warning_threshold(mut self, threshold: usize) -> Self { pub fn with_warning_threshold(mut self, threshold: usize) -> Self {
self.warning_threshold = Some(threshold); self.warning_threshold = Some(threshold);
self self
} }
/// Set count mode (characters vs display width vs bytes) /// Set count mode (characters vs display width vs bytes)
pub fn with_count_mode(mut self, mode: CountMode) -> Self { pub fn with_count_mode(mut self, mode: CountMode) -> Self {
self.count_mode = mode; self.count_mode = mode;
self self
} }
/// Get maximum length /// Get maximum length
pub fn max_length(&self) -> Option<usize> { pub fn max_length(&self) -> Option<usize> {
self.max_length self.max_length
} }
/// Get minimum length /// Get minimum length
pub fn min_length(&self) -> Option<usize> { pub fn min_length(&self) -> Option<usize> {
self.min_length self.min_length
} }
/// Get warning threshold /// Get warning threshold
pub fn warning_threshold(&self) -> Option<usize> { pub fn warning_threshold(&self) -> Option<usize> {
self.warning_threshold self.warning_threshold
} }
/// Get count mode /// Get count mode
pub fn count_mode(&self) -> CountMode { pub fn count_mode(&self) -> CountMode {
self.count_mode self.count_mode
} }
/// Count characters/width/bytes according to the configured mode /// Count characters/width/bytes according to the configured mode
fn count(&self, text: &str) -> usize { fn count(&self, text: &str) -> usize {
match self.count_mode { match self.count_mode {
@@ -134,7 +132,7 @@ impl CharacterLimits {
CountMode::Bytes => text.len(), CountMode::Bytes => text.len(),
} }
} }
/// Check if inserting a character would exceed limits /// Check if inserting a character would exceed limits
pub fn validate_insertion( pub fn validate_insertion(
&self, &self,
@@ -179,7 +177,7 @@ impl CharacterLimits {
None // No validation issues None // No validation issues
} }
/// Validate the current content /// Validate the current content
pub fn validate_content(&self, text: &str) -> Option<ValidationResult> { pub fn validate_content(&self, text: &str) -> Option<ValidationResult> {
let count = self.count(text); let count = self.count(text);
@@ -207,52 +205,60 @@ impl CharacterLimits {
} }
} }
} }
None // No validation issues None // No validation issues
} }
/// Get the current status of the text against limits /// Get the current status of the text against limits
pub fn check_limits(&self, text: &str) -> LimitCheckResult { pub fn check_limits(&self, text: &str) -> LimitCheckResult {
let count = self.count(text); let count = self.count(text);
if let Some(max) = self.max_length { if let Some(max) = self.max_length {
if count > max { if count > max {
return LimitCheckResult::Exceeded { current: count, max }; return LimitCheckResult::Exceeded {
current: count,
max,
};
} }
if let Some(warning_threshold) = self.warning_threshold { if let Some(warning_threshold) = self.warning_threshold {
if count >= warning_threshold { if count >= warning_threshold {
return LimitCheckResult::Warning { current: count, max }; return LimitCheckResult::Warning {
current: count,
max,
};
} }
} }
} }
// Check min length // Check min length
if let Some(min) = self.min_length { if let Some(min) = self.min_length {
if count < min { if count < min {
return LimitCheckResult::TooShort { current: count, min }; return LimitCheckResult::TooShort {
current: count,
min,
};
} }
} }
LimitCheckResult::Ok LimitCheckResult::Ok
} }
/// Get a human-readable status string /// Get a human-readable status string
pub fn status_text(&self, text: &str) -> Option<String> { pub fn status_text(&self, text: &str) -> Option<String> {
match self.check_limits(text) { match self.check_limits(text) {
LimitCheckResult::Ok => { LimitCheckResult::Ok => {
// Show current/max if we have a max limit // 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 } => { LimitCheckResult::Warning { current, max } => {
Some(format!("{current}/{max} (approaching limit)")) Some(format!("{current}/{max} (approaching limit)"))
}, }
LimitCheckResult::Exceeded { current, max } => { LimitCheckResult::Exceeded { current, max } => {
Some(format!("{current}/{max} (exceeded)")) Some(format!("{current}/{max} (exceeded)"))
}, }
LimitCheckResult::TooShort { current, min } => { LimitCheckResult::TooShort { current, min } => Some(format!("{current}/{min} minimum")),
Some(format!("{current}/{min} minimum"))
},
} }
} }
pub fn allows_field_switch(&self, text: &str) -> bool { pub fn allows_field_switch(&self, text: &str) -> bool {
@@ -264,7 +270,7 @@ impl CharacterLimits {
true // No minimum requirement, always allow switching true // No minimum requirement, always allow switching
} }
} }
/// Get reason why field switching is not allowed (if any) /// Get reason why field switching is not allowed (if any)
pub fn field_switch_block_reason(&self, text: &str) -> Option<String> { pub fn field_switch_block_reason(&self, text: &str) -> Option<String> {
if let Some(min) = self.min_length { if let Some(min) = self.min_length {
@@ -307,128 +313,139 @@ mod tests {
let limits = CharacterLimits::new(10); let limits = CharacterLimits::new(10);
assert_eq!(limits.max_length(), Some(10)); assert_eq!(limits.max_length(), Some(10));
assert_eq!(limits.min_length(), None); assert_eq!(limits.min_length(), None);
let range_limits = CharacterLimits::new_range(5, 15); let range_limits = CharacterLimits::new_range(5, 15);
assert_eq!(range_limits.min_length(), Some(5)); assert_eq!(range_limits.min_length(), Some(5));
assert_eq!(range_limits.max_length(), Some(15)); assert_eq!(range_limits.max_length(), Some(15));
} }
#[test] #[test]
fn test_default_limits() { fn test_default_limits() {
let limits = CharacterLimits::default(); let limits = CharacterLimits::default();
assert_eq!(limits.max_length(), Some(30)); assert_eq!(limits.max_length(), Some(30));
} }
#[test] #[test]
fn test_character_counting() { fn test_character_counting() {
let limits = CharacterLimits::new(5); let limits = CharacterLimits::new(5);
// Test character mode (default) // Test character mode (default)
assert_eq!(limits.count("hello"), 5); assert_eq!(limits.count("hello"), 5);
assert_eq!(limits.count("héllo"), 5); // Accented character counts as 1 assert_eq!(limits.count("héllo"), 5); // Accented character counts as 1
// Test display width mode // Test display width mode
let limits = limits.with_count_mode(CountMode::DisplayWidth); let limits = limits.with_count_mode(CountMode::DisplayWidth);
assert_eq!(limits.count("hello"), 5); assert_eq!(limits.count("hello"), 5);
// Test bytes mode // Test bytes mode
let limits = limits.with_count_mode(CountMode::Bytes); let limits = limits.with_count_mode(CountMode::Bytes);
assert_eq!(limits.count("hello"), 5); assert_eq!(limits.count("hello"), 5);
assert_eq!(limits.count("héllo"), 6); // é takes 2 bytes in UTF-8 assert_eq!(limits.count("héllo"), 6); // é takes 2 bytes in UTF-8
} }
#[test] #[test]
fn test_insertion_validation() { fn test_insertion_validation() {
let limits = CharacterLimits::new(5); let limits = CharacterLimits::new(5);
// Valid insertion // Valid insertion
let result = limits.validate_insertion("test", 4, 'x'); let result = limits.validate_insertion("test", 4, 'x');
assert!(result.is_none()); // No validation issues assert!(result.is_none()); // No validation issues
// Invalid insertion (would exceed limit) // Invalid insertion (would exceed limit)
let result = limits.validate_insertion("tests", 5, 'x'); let result = limits.validate_insertion("tests", 5, 'x');
assert!(result.is_some()); assert!(result.is_some());
assert!(!result.unwrap().is_acceptable()); assert!(!result.unwrap().is_acceptable());
} }
#[test] #[test]
fn test_content_validation() { fn test_content_validation() {
let limits = CharacterLimits::new_range(3, 10); let limits = CharacterLimits::new_range(3, 10);
// Too short // Too short
let result = limits.validate_content("hi"); let result = limits.validate_content("hi");
assert!(result.is_some()); assert!(result.is_some());
assert!(result.unwrap().is_acceptable()); // Warning, not error assert!(result.unwrap().is_acceptable()); // Warning, not error
// Just right // Just right
let result = limits.validate_content("hello"); let result = limits.validate_content("hello");
assert!(result.is_none()); assert!(result.is_none());
// Too long // Too long
let result = limits.validate_content("hello world!"); let result = limits.validate_content("hello world!");
assert!(result.is_some()); assert!(result.is_some());
assert!(!result.unwrap().is_acceptable()); // Error assert!(!result.unwrap().is_acceptable()); // Error
} }
#[test] #[test]
fn test_warning_threshold() { fn test_warning_threshold() {
let limits = CharacterLimits::new(10).with_warning_threshold(8); let limits = CharacterLimits::new(10).with_warning_threshold(8);
// Below warning threshold // Below warning threshold
let result = limits.validate_insertion("123456", 6, 'x'); let result = limits.validate_insertion("123456", 6, 'x');
assert!(result.is_none()); assert!(result.is_none());
// At warning threshold // At warning threshold
let result = limits.validate_insertion("1234567", 7, 'x'); let result = limits.validate_insertion("1234567", 7, 'x');
assert!(result.is_some()); // This brings us to 8 chars assert!(result.is_some()); // This brings us to 8 chars
assert!(result.unwrap().is_acceptable()); // Warning, not error assert!(result.unwrap().is_acceptable()); // Warning, not error
let result = limits.validate_insertion("12345678", 8, 'x'); let result = limits.validate_insertion("12345678", 8, 'x');
assert!(result.is_none()); assert!(result.is_none());
} }
#[test] #[test]
fn test_status_text() { fn test_status_text() {
let limits = CharacterLimits::new(10); let limits = CharacterLimits::new(10);
assert_eq!(limits.status_text("hello"), Some("5/10".to_string())); assert_eq!(limits.status_text("hello"), Some("5/10".to_string()));
let limits = limits.with_warning_threshold(8); let limits = limits.with_warning_threshold(8);
assert_eq!(limits.status_text("12345678"), Some("8/10 (approaching limit)".to_string())); assert_eq!(
assert_eq!(limits.status_text("1234567890x"), Some("11/10 (exceeded)".to_string())); limits.status_text("12345678"),
Some("8/10 (approaching limit)".to_string())
);
assert_eq!(
limits.status_text("1234567890x"),
Some("11/10 (exceeded)".to_string())
);
} }
#[test] #[test]
fn test_field_switch_blocking() { fn test_field_switch_blocking() {
let limits = CharacterLimits::new_range(3, 10); let limits = CharacterLimits::new_range(3, 10);
// Empty field: should allow switching // Empty field: should allow switching
assert!(limits.allows_field_switch("")); assert!(limits.allows_field_switch(""));
assert!(limits.field_switch_block_reason("").is_none()); assert!(limits.field_switch_block_reason("").is_none());
// Field with content below minimum: should block switching // Field with content below minimum: should block switching
assert!(!limits.allows_field_switch("hi")); assert!(!limits.allows_field_switch("hi"));
assert!(limits.field_switch_block_reason("hi").is_some()); 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 // Field meeting minimum: should allow switching
assert!(limits.allows_field_switch("hello")); assert!(limits.allows_field_switch("hello"));
assert!(limits.field_switch_block_reason("hello").is_none()); assert!(limits.field_switch_block_reason("hello").is_none());
// Field exceeding maximum: should still allow switching (validation shows error but doesn't block) // 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.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] #[test]
fn test_field_switch_no_minimum() { fn test_field_switch_no_minimum() {
let limits = CharacterLimits::new(10); // Only max, no minimum let limits = CharacterLimits::new(10); // Only max, no minimum
// Should always allow switching when there's no minimum // Should always allow switching when there's no minimum
assert!(limits.allows_field_switch("")); assert!(limits.allows_field_switch(""));
assert!(limits.allows_field_switch("a")); assert!(limits.allows_field_switch("a"));
assert!(limits.allows_field_switch("hello")); assert!(limits.allows_field_switch("hello"));
assert!(limits.field_switch_block_reason("").is_none()); assert!(limits.field_switch_block_reason("").is_none());
assert!(limits.field_switch_block_reason("a").is_none()); assert!(limits.field_switch_block_reason("a").is_none());
} }

View File

@@ -3,23 +3,21 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Default)]
pub enum MaskDisplayMode { pub enum MaskDisplayMode {
/// Only show separators as user types /// Only show separators as user types
/// Example: "" → "", "123" → "123", "12345" → "(123) 45" /// Example: "" → "", "123" → "123", "12345" → "(123) 45"
#[default] #[default]
Dynamic, Dynamic,
/// Show full template with placeholders from start /// Show full template with placeholders from start
/// Example: "" → "(___) ___-____", "123" → "(123) ___-____" /// Example: "" → "(___) ___-____", "123" → "(123) ___-____"
Template { Template {
/// Character to use as placeholder for empty input positions /// Character to use as placeholder for empty input positions
placeholder: char placeholder: char,
}, },
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DisplayMask { pub struct DisplayMask {
/// Mask pattern like "##-##-####" where # = input position, others are visual separators /// Mask pattern like "##-##-####" where # = input position, others are visual separators
@@ -32,21 +30,21 @@ pub struct DisplayMask {
impl DisplayMask { impl DisplayMask {
/// Create a new display mask with dynamic mode (current behavior) /// Create a new display mask with dynamic mode (current behavior)
/// ///
/// # Arguments /// # Arguments
/// * `pattern` - The mask pattern (e.g., "##-##-####", "(###) ###-####") /// * `pattern` - The mask pattern (e.g., "##-##-####", "(###) ###-####")
/// * `input_char` - Character representing input positions (usually '#') /// * `input_char` - Character representing input positions (usually '#')
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use validation_core::DisplayMask; /// use validation_core::DisplayMask;
/// ///
/// // Phone number format /// // Phone number format
/// let phone_mask = DisplayMask::new("(###) ###-####", '#'); /// let phone_mask = DisplayMask::new("(###) ###-####", '#');
/// ///
/// // Date format /// // Date format
/// let date_mask = DisplayMask::new("##/##/####", '#'); /// let date_mask = DisplayMask::new("##/##/####", '#');
/// ///
/// // Custom business format /// // Custom business format
/// let employee_id = DisplayMask::new("EMP-####-##", '#'); /// let employee_id = DisplayMask::new("EMP-####-##", '#');
/// ``` /// ```
@@ -59,7 +57,7 @@ impl DisplayMask {
} }
/// Set the display mode for this mask /// Set the display mode for this mask
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use validation_core::{DisplayMask, MaskDisplayMode}; /// use validation_core::{DisplayMask, MaskDisplayMode};
@@ -76,7 +74,7 @@ impl DisplayMask {
} }
/// Set template mode with custom placeholder /// Set template mode with custom placeholder
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use validation_core::DisplayMask; /// use validation_core::DisplayMask;
@@ -95,7 +93,9 @@ impl DisplayMask {
pub fn apply_to_display(&self, raw_input: &str) -> String { pub fn apply_to_display(&self, raw_input: &str) -> String {
match &self.display_mode { match &self.display_mode {
MaskDisplayMode::Dynamic => self.apply_dynamic(raw_input), 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 /// Check if a display position should accept cursor/input
pub fn is_input_position(&self, display_position: usize) -> bool { pub fn is_input_position(&self, display_position: usize) -> bool {
self.pattern.chars() self.pattern
.chars()
.nth(display_position) .nth(display_position)
.map(|c| c == self.input_char) .map(|c| c == self.input_char)
.unwrap_or(true) // Beyond pattern = accept input .unwrap_or(true) // Beyond pattern = accept input
@@ -212,7 +213,7 @@ impl DisplayMask {
pub fn prev_input_position(&self, display_pos: usize) -> Option<usize> { pub fn prev_input_position(&self, display_pos: usize) -> Option<usize> {
// Collect pattern chars with indices first, then search backwards // Collect pattern chars with indices first, then search backwards
let pattern_chars: Vec<(usize, char)> = self.pattern.chars().enumerate().collect(); let pattern_chars: Vec<(usize, char)> = self.pattern.chars().enumerate().collect();
// Search backwards from display_pos // Search backwards from display_pos
for &(i, pattern_char) in pattern_chars.iter().rev() { for &(i, pattern_char) in pattern_chars.iter().rev() {
if i <= display_pos && pattern_char == self.input_char { if i <= display_pos && pattern_char == self.input_char {
@@ -268,11 +269,11 @@ mod tests {
// User creates their own phone mask // User creates their own phone mask
let dynamic = DisplayMask::new("(###) ###-####", '#'); let dynamic = DisplayMask::new("(###) ###-####", '#');
let template = DisplayMask::new("(###) ###-####", '#').with_template('_'); let template = DisplayMask::new("(###) ###-####", '#').with_template('_');
// Dynamic mode // Dynamic mode
assert_eq!(dynamic.apply_to_display(""), ""); assert_eq!(dynamic.apply_to_display(""), "");
assert_eq!(dynamic.apply_to_display("1234567890"), "(123) 456-7890"); assert_eq!(dynamic.apply_to_display("1234567890"), "(123) 456-7890");
// Template mode // Template mode
assert_eq!(template.apply_to_display(""), "(___) ___-____"); assert_eq!(template.apply_to_display(""), "(___) ___-____");
assert_eq!(template.apply_to_display("123"), "(123) ___-____"); assert_eq!(template.apply_to_display("123"), "(123) ___-____");
@@ -284,7 +285,7 @@ mod tests {
let us_date = DisplayMask::new("##/##/####", '#'); let us_date = DisplayMask::new("##/##/####", '#');
let eu_date = DisplayMask::new("##.##.####", '#'); let eu_date = DisplayMask::new("##.##.####", '#');
let iso_date = DisplayMask::new("####-##-##", '#'); let iso_date = DisplayMask::new("####-##-##", '#');
assert_eq!(us_date.apply_to_display("12252024"), "12/25/2024"); assert_eq!(us_date.apply_to_display("12252024"), "12/25/2024");
assert_eq!(eu_date.apply_to_display("25122024"), "25.12.2024"); assert_eq!(eu_date.apply_to_display("25122024"), "25.12.2024");
assert_eq!(iso_date.apply_to_display("20241225"), "2024-12-25"); assert_eq!(iso_date.apply_to_display("20241225"), "2024-12-25");
@@ -296,7 +297,7 @@ mod tests {
let employee_id = DisplayMask::new("EMP-####-##", '#'); let employee_id = DisplayMask::new("EMP-####-##", '#');
let product_code = DisplayMask::new("###-###-###", '#'); let product_code = DisplayMask::new("###-###-###", '#');
let invoice = DisplayMask::new("INV####/##", '#'); let invoice = DisplayMask::new("INV####/##", '#');
assert_eq!(employee_id.apply_to_display("123456"), "EMP-1234-56"); assert_eq!(employee_id.apply_to_display("123456"), "EMP-1234-56");
assert_eq!(product_code.apply_to_display("123456789"), "123-456-789"); assert_eq!(product_code.apply_to_display("123456789"), "123-456-789");
assert_eq!(invoice.apply_to_display("123456"), "INV1234/56"); 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_x = DisplayMask::new("XXX-XX-XXXX", 'X');
let mask_with_hash = DisplayMask::new("###-##-####", '#'); let mask_with_hash = DisplayMask::new("###-##-####", '#');
let mask_with_n = DisplayMask::new("NNN-NN-NNNN", 'N'); 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_x.apply_to_display("123456789"), "123-45-6789");
assert_eq!(mask_with_hash.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"); 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 underscores = DisplayMask::new("##-##", '#').with_template('_');
let dots = DisplayMask::new("##-##", '#').with_template('•'); let dots = DisplayMask::new("##-##", '#').with_template('•');
let dashes = DisplayMask::new("##-##", '#').with_template('-'); let dashes = DisplayMask::new("##-##", '#').with_template('-');
assert_eq!(underscores.apply_to_display(""), "__-__"); assert_eq!(underscores.apply_to_display(""), "__-__");
assert_eq!(dots.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] #[test]
fn test_position_mapping_user_patterns() { fn test_position_mapping_user_patterns() {
let custom = DisplayMask::new("ABC-###-XYZ", '#'); let custom = DisplayMask::new("ABC-###-XYZ", '#');
// Position mapping should work correctly with any pattern // 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(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(1), 5); // Second # at position 5
assert_eq!(custom.raw_pos_to_display_pos(2), 6); // Third # at position 6 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(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(5), 1); // Position 5 -> second input
assert_eq!(custom.display_pos_to_raw_pos(6), 2); // Position 6 -> third 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(0)); // A
assert!(!custom.is_input_position(3)); // - 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 assert!(!custom.is_input_position(8)); // Y
} }
} }

View File

@@ -84,8 +84,12 @@ impl PositionRange {
pub fn positions_up_to(&self, max_length: usize) -> Vec<usize> { pub fn positions_up_to(&self, max_length: usize) -> Vec<usize> {
match self { match self {
PositionRange::Single(pos) => { PositionRange::Single(pos) => {
if *pos < max_length { vec![*pos] } else { vec![] } if *pos < max_length {
}, vec![*pos]
} else {
vec![]
}
}
PositionRange::Range(start, end) => { PositionRange::Range(start, end) => {
let actual_end = (*end).min(max_length.saturating_sub(1)); let actual_end = (*end).min(max_length.saturating_sub(1));
if *start <= actual_end { if *start <= actual_end {
@@ -93,20 +97,19 @@ impl PositionRange {
} else { } else {
vec![] vec![]
} }
}, }
PositionRange::From(start) => { PositionRange::From(start) => {
if *start < max_length { if *start < max_length {
(*start..max_length).collect() (*start..max_length).collect()
} else { } else {
vec![] vec![]
} }
}, }
PositionRange::Multiple(positions) => { PositionRange::Multiple(positions) => positions
positions.iter() .iter()
.filter(|&&pos| pos < max_length) .filter(|&&pos| pos < max_length)
.copied() .copied()
.collect() .collect(),
},
} }
} }
} }
@@ -134,7 +137,7 @@ impl CharacterFilter {
CharacterFilter::OneOf(chars) => { CharacterFilter::OneOf(chars) => {
let char_list: String = chars.iter().collect(); let char_list: String = chars.iter().collect();
format!("one of: {char_list}") format!("one of: {char_list}")
}, }
CharacterFilter::Custom(_) => "custom filter".to_string(), CharacterFilter::Custom(_) => "custom filter".to_string(),
} }
} }
@@ -195,7 +198,11 @@ impl PatternFilters {
} }
/// Validate a character at a specific position against all applicable filters /// 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 { for filter in &self.filters {
if let Some(error) = filter.error_message(position, character) { if let Some(error) = filter.error_message(position, character) {
return Err(error); 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::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] #[test]
@@ -277,10 +287,7 @@ mod tests {
#[test] #[test]
fn test_position_filter_validation() { fn test_position_filter_validation() {
let filter = PositionFilter::new( let filter = PositionFilter::new(PositionRange::Range(0, 1), CharacterFilter::Alphabetic);
PositionRange::Range(0, 1),
CharacterFilter::Alphabetic,
);
assert!(filter.validate_position(0, 'A')); assert!(filter.validate_position(0, 'A'));
assert!(filter.validate_position(1, 'b')); assert!(filter.validate_position(1, 'b'));
@@ -312,11 +319,10 @@ mod tests {
#[test] #[test]
fn test_custom_filter() { fn test_custom_filter() {
let pattern = PatternFilters::new() let pattern = PatternFilters::new().add_filter(PositionFilter::new(
.add_filter(PositionFilter::new( PositionRange::From(0),
PositionRange::From(0), CharacterFilter::Custom(Arc::new(|c| c.is_lowercase())),
CharacterFilter::Custom(Arc::new(|c| c.is_lowercase())), ));
));
assert!(pattern.validate_text("hello").is_ok()); assert!(pattern.validate_text("hello").is_ok());
assert!(pattern.validate_text("Hello").is_err()); // Uppercase not allowed assert!(pattern.validate_text("Hello").is_err()); // Uppercase not allowed