lacoctelera/domain/
tag.rsuse once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use validator::{Validate, ValidationError};
static RE_TAG: Lazy<Regex> = Lazy::new(|| Regex::new(r"[a-z_]{2,}$").unwrap());
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate, PartialEq)]
pub struct Tag {
#[validate(custom(function = "validate_identifier"), length(min = 2, max = 20))]
pub identifier: String,
}
impl Tag {
pub fn new(tagname: &str) -> Result<Self, ValidationError> {
let tag = Tag {
identifier: tagname.to_ascii_lowercase(),
};
match tag.validate() {
Ok(_) => Ok(tag),
Err(_) => Err(ValidationError::new("2")),
}
}
}
impl std::fmt::Display for Tag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.identifier)
}
}
fn validate_identifier(value: &str) -> Result<(), ValidationError> {
let haystack = value.to_lowercase();
if RE_TAG.is_match(&haystack) {
Result::Ok(())
} else {
Err(ValidationError::new("2"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use rstest::*;
#[rstest]
#[case("A(tag)")]
#[case("")]
#[case("Averylongtagnamethatshallprovokeanerror")]
#[case("anemoji❌")]
fn wrong_string_fail_to_build_a_tag(#[case] input: &str) {
assert!(Tag::new(input).is_err())
}
#[rstest]
#[case("A_tag")]
#[case("Ta")]
#[case("customTag")]
fn valid_string_succeed_to_build_a_tag(#[case] input: &str) {
assert!(Tag::new(input).is_ok())
}
#[rstest]
#[case("customTag")]
#[case("ALLCAPITALLETTERSTAG")]
fn tags_get_converted_to_lowercase(#[case] input: &str) {
assert_eq!(
Tag::new(input).expect("Failed to build a tag").identifier,
input.to_lowercase()
)
}
}