diff --git a/Cargo.lock b/Cargo.lock index 2112ca49..82037336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2977,6 +2977,7 @@ dependencies = [ "percent-encoding", "regex", "thiserror", + "unicode-segmentation", "url", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index 90726e91..0eb888f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ tower = { version = "0.4.13", features = ["util"] } tower-http = { version = "0.5.1", features = ["cors", "trace"] } tracing = "0.1.40" tracing-subscriber = "0.3.18" +unicode-segmentation = "1.11.0" uuid = { version = "1.7.0", features = ["v4", "serde"] } url = "2.5.0" percent-encoding = "2.3.1" diff --git a/crates/sos24-domain/Cargo.toml b/crates/sos24-domain/Cargo.toml index 8aa2c127..b505f4f9 100644 --- a/crates/sos24-domain/Cargo.toml +++ b/crates/sos24-domain/Cargo.toml @@ -12,6 +12,7 @@ getset.workspace = true mockall.workspace = true regex.workspace = true thiserror.workspace = true +unicode-segmentation.workspace = true uuid.workspace = true url.workspace = true percent-encoding.workspace = true diff --git a/crates/sos24-domain/src/entity/project.rs b/crates/sos24-domain/src/entity/project.rs index 1e2c825c..c10adb76 100644 --- a/crates/sos24-domain/src/entity/project.rs +++ b/crates/sos24-domain/src/entity/project.rs @@ -3,6 +3,7 @@ use std::str::FromStr; use bitflags::bitflags; use getset::Getters; use thiserror::Error; +use unicode_segmentation::UnicodeSegmentation; use crate::{ensure, impl_value_object}; @@ -280,7 +281,7 @@ impl BoundedString { #[derive(Debug, Error)] pub enum BoundedStringError { #[error("Invalid character: `{0}`")] - InvalidCharacter(char), + InvalidCharacter(String), #[error("Empty string is not allowed")] Empty, #[error("Too long (max: {0})")] @@ -293,21 +294,28 @@ impl TryFrom for BoundedString { fn try_from(value: String) -> Result { let mut length = 0; // 文字列長を3倍してカウントする - for c in value.chars() { - if emojis::get(&c.to_string()).is_some() { - return Err(BoundedStringError::InvalidCharacter(c)); + let is_small = |c: char| match c { + '\u{0021}'..='\u{007E}' // 半角英数字・記号 + | '\u{FF10}'..='\u{FF19}' // 全角数字 + | '\u{FF21}'..='\u{FF3A}' // 全角英語(大文字) + | '\u{FF41}'..='\u{FF5A}' // 全角英語(小文字) + => true, + _ => false, + }; + + for grapheme_cluster in value.graphemes(true) { + if emojis::get(grapheme_cluster).is_some() { + return Err(BoundedStringError::InvalidCharacter( + grapheme_cluster.to_string(), + )); } - let char_length = match c { - '\u{0021}'..='\u{007E}' // 半角英数字・記号 - | '\u{FF10}'..='\u{FF19}' // 全角数字 - | '\u{FF21}'..='\u{FF3A}' // 全角英語(大文字) - | '\u{FF41}'..='\u{FF5A}' // 全角英語(小文字) - => 2, - _ => 3, - }; - - length += char_length; + let mut chars = grapheme_cluster.chars(); + let is_small_char = chars + .next() + .map(|c| is_small(c) && chars.next().is_none()) + .unwrap_or(false); + length += if is_small_char { 2 } else { 3 }; } if length == 0 { @@ -422,5 +430,6 @@ mod tests { assert!(ProjectTitle::try_from(format!("{kana18}AAAA")).is_err()); assert!(ProjectTitle::try_from("🙂".to_string()).is_err()); + assert!(ProjectTitle::try_from("企画名#️⃣appare".to_string()).is_err()); } }