Test-Implementation Pair

Implementation: crates/common_utils/src/lib.rs
================================================================================
#![warn(missing_docs, missing_debug_implementations)]
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR" ), "/", "README.md"))]

use masking::{PeekInterface, Secret};

pub mod access_token;
pub mod consts;
pub mod crypto;
pub mod custom_serde;
#[allow(missing_docs)] // Todo: add docs
pub mod encryption;
pub mod errors;
#[allow(missing_docs)] // Todo: add docs
pub mod events;
pub mod ext_traits;
pub mod fp_utils;
/// Used for hashing
pub mod hashing;
pub mod id_type;
#[cfg(feature = "keymanager")]
pub mod keymanager;
pub mod link_utils;
pub mod macros;
#[cfg(feature = "metrics")]
pub mod metrics;
pub mod new_type;
pub mod payout_method_utils;
pub mod pii;
#[allow(missing_docs)] // Todo: add docs
pub mod request;
#[cfg(feature = "signals")]
pub mod signals;
pub mod transformers;
pub mod types;
/// Unified Connector Service (UCS) interface definitions.
///
/// This module defines types and traits for interacting with the Unified Connector Service.
/// It includes reference ID types for payments and refunds, and a trait for extracting
/// UCS reference information from requests.
pub mod ucs_types;
pub mod validation;

pub use base64_serializer::Base64Serializer;

/// Date-time utilities.
pub mod date_time {
    #[cfg(feature = "async_ext")]
    use std::time::Instant;
    use std::{marker::PhantomData, num::NonZeroU8};

    use masking::{Deserialize, Serialize};
    use time::{
        format_description::{
            well_known::iso8601::{Config, EncodedConfig, Iso8601, TimePrecision},
            BorrowedFormatItem,
        },
        OffsetDateTime, PrimitiveDateTime,
    };

    /// Enum to represent date formats
    #[derive(Debug)]
    pub enum DateFormat {
        /// Format the date in 20191105081132 format
        YYYYMMDDHHmmss,
        /// Format the date in 20191105 format
        YYYYMMDD,
        /// Format the date in 201911050811 format
        YYYYMMDDHHmm,
        /// Format the date in 05112019081132 format
        DDMMYYYYHHmmss,
    }

    /// Create a new [`PrimitiveDateTime`] with the current date and time in UTC.
    pub fn now() -> PrimitiveDateTime {
        let utc_date_time = OffsetDateTime::now_utc();
        PrimitiveDateTime::new(utc_date_time.date(), utc_date_time.time())
    }

    /// Convert from OffsetDateTime to PrimitiveDateTime
    pub fn convert_to_pdt(offset_time: OffsetDateTime) -> PrimitiveDateTime {
        PrimitiveDateTime::new(offset_time.date(), offset_time.time())
    }

    /// Return the UNIX timestamp of the current date and time in UTC
    pub fn now_unix_timestamp() -> i64 {
        OffsetDateTime::now_utc().unix_timestamp()
    }

    /// Calculate execution time for a async block in milliseconds
    #[cfg(feature = "async_ext")]
    pub async fn time_it<T, Fut: futures::Future<Output = T>, F: FnOnce() -> Fut>(
        block: F,
    ) -> (T, f64) {
        let start = Instant::now();
        let result = block().await;
        (result, start.elapsed().as_secs_f64() * 1000f64)
    }

    /// Return the given date and time in UTC with the given format Eg: format: YYYYMMDDHHmmss Eg: 20191105081132
    pub fn format_date(
        date: PrimitiveDateTime,
        format: DateFormat,
    ) -> Result<String, time::error::Format> {
        let format = <&[BorrowedFormatItem<'_>]>::from(format);
        date.format(&format)
    }

    /// Return the current date and time in UTC with the format [year]-[month]-[day]T[hour]:[minute]:[second].mmmZ Eg: 2023-02-15T13:33:18.898Z
    pub fn date_as_yyyymmddthhmmssmmmz() -> Result<String, time::error::Format> {
        const ISO_CONFIG: EncodedConfig = Config::DEFAULT
            .set_time_precision(TimePrecision::Second {
                decimal_digits: NonZeroU8::new(3),
            })
            .encode();
        now().assume_utc().format(&Iso8601::<ISO_CONFIG>)
    }

    /// Return the current date and time in UTC formatted as "ddd, DD MMM YYYY HH:mm:ss GMT".
    pub fn now_rfc7231_http_date() -> Result<String, time::error::Format> {
        let now_utc = OffsetDateTime::now_utc();
        // Desired format: ddd, DD MMM YYYY HH:mm:ss GMT
        // Example: Fri, 23 May 2025 06:19:35 GMT
        let format = time::macros::format_description!(
            "[weekday repr:short], [day padding:zero] [month repr:short] [year repr:full] [hour padding:zero repr:24]:[minute padding:zero]:[second padding:zero] GMT"
        );
        now_utc.format(&format)
    }

    impl From<DateFormat> for &[BorrowedFormatItem<'_>] {
        fn from(format: DateFormat) -> Self {
            match format {
                DateFormat::YYYYMMDDHHmmss => time::macros::format_description!("[year repr:full][month padding:zero repr:numerical][day padding:zero][hour padding:zero repr:24][minute padding:zero][second padding:zero]"),
                DateFormat::YYYYMMDD => time::macros::format_description!("[year repr:full][month padding:zero repr:numerical][day padding:zero]"),
                DateFormat::YYYYMMDDHHmm => time::macros::format_description!("[year repr:full][month padding:zero repr:numerical][day padding:zero][hour padding:zero repr:24][minute padding:zero]"),
                DateFormat::DDMMYYYYHHmmss => time::macros::format_description!("[day padding:zero][month padding:zero repr:numerical][year repr:full][hour padding:zero repr:24][minute padding:zero][second padding:zero]"),
            }
        }
    }

    /// Format the date in 05112019 format
    #[derive(Debug, Clone)]
    pub struct DDMMYYYY;
    /// Format the date in 20191105 format
    #[derive(Debug, Clone)]
    pub struct YYYYMMDD;
    /// Format the date in 20191105081132 format
    #[derive(Debug, Clone)]
    pub struct YYYYMMDDHHmmss;

    /// To serialize the date in Dateformats like YYYYMMDDHHmmss, YYYYMMDD, DDMMYYYY
    #[derive(Debug, Deserialize, Clone)]
    pub struct DateTime<T: TimeStrategy> {
        inner: PhantomData<T>,
        value: PrimitiveDateTime,
    }

    impl<T: TimeStrategy> From<PrimitiveDateTime> for DateTime<T> {
        fn from(value: PrimitiveDateTime) -> Self {
            Self {
                inner: PhantomData,
                value,
            }
        }
    }

    /// Time strategy for the Date, Eg: YYYYMMDDHHmmss, YYYYMMDD, DDMMYYYY
    pub trait TimeStrategy {
        /// Stringify the date as per the Time strategy
        fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
    }

    impl<T: TimeStrategy> Serialize for DateTime<T> {
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: serde::Serializer,
        {
            serializer.collect_str(self)
        }
    }

    impl<T: TimeStrategy> std::fmt::Display for DateTime<T> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            T::fmt(&self.value, f)
        }
    }

    impl TimeStrategy for DDMMYYYY {
        fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let year = input.year();
            #[allow(clippy::as_conversions)]
            let month = input.month() as u8;
            let day = input.day();
            let output = format!("{day:02}{month:02}{year}");
            f.write_str(&output)
        }
    }

    impl TimeStrategy for YYYYMMDD {
        fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let year = input.year();
            #[allow(clippy::as_conversions)]
            let month: u8 = input.month() as u8;
            let day = input.day();
            let output = format!("{year}{month:02}{day:02}");
            f.write_str(&output)
        }
    }

    impl TimeStrategy for YYYYMMDDHHmmss {
        fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let year = input.year();
            #[allow(clippy::as_conversions)]
            let month = input.month() as u8;
            let day = input.day();
            let hour = input.hour();
            let minute = input.minute();
            let second = input.second();
            let output = format!("{year}{month:02}{day:02}{hour:02}{minute:02}{second:02}");
            f.write_str(&output)
        }
    }
}

/// Generate a nanoid with the given prefix and length
#[inline]
pub fn generate_id(length: usize, prefix: &str) -> String {
    format!("{}_{}", prefix, nanoid::nanoid!(length, &consts::ALPHABETS))
}

/// Generate a ReferenceId with the default length with the given prefix
fn generate_ref_id_with_default_length<const MAX_LENGTH: u8, const MIN_LENGTH: u8>(
    prefix: &str,
) -> id_type::LengthId<MAX_LENGTH, MIN_LENGTH> {
    id_type::LengthId::<MAX_LENGTH, MIN_LENGTH>::new(prefix)
}

/// Generate a customer id with default length, with prefix as `cus`
pub fn generate_customer_id_of_default_length() -> id_type::CustomerId {
    use id_type::GenerateId;

    id_type::CustomerId::generate()
}

/// Generate a organization id with default length, with prefix as `org`
pub fn generate_organization_id_of_default_length() -> id_type::OrganizationId {
    use id_type::GenerateId;

    id_type::OrganizationId::generate()
}

/// Generate a profile id with default length, with prefix as `pro`
pub fn generate_profile_id_of_default_length() -> id_type::ProfileId {
    use id_type::GenerateId;

    id_type::ProfileId::generate()
}

/// Generate a routing id with default length, with prefix as `routing`
pub fn generate_routing_id_of_default_length() -> id_type::RoutingId {
    use id_type::GenerateId;

    id_type::RoutingId::generate()
}
/// Generate a merchant_connector_account id with default length, with prefix as `mca`
pub fn generate_merchant_connector_account_id_of_default_length(
) -> id_type::MerchantConnectorAccountId {
    use id_type::GenerateId;

    id_type::MerchantConnectorAccountId::generate()
}

/// Generate a profile_acquirer id with default length, with prefix as `mer_acq`
pub fn generate_profile_acquirer_id_of_default_length() -> id_type::ProfileAcquirerId {
    use id_type::GenerateId;

    id_type::ProfileAcquirerId::generate()
}

/// Generate a nanoid with the given prefix and a default length
#[inline]
pub fn generate_id_with_default_len(prefix: &str) -> String {
    let len: usize = consts::ID_LENGTH;
    format!("{}_{}", prefix, nanoid::nanoid!(len, &consts::ALPHABETS))
}

/// Generate a time-ordered (time-sortable) unique identifier using the current time
#[inline]
pub fn generate_time_ordered_id(prefix: &str) -> String {
    format!("{prefix}_{}", uuid::Uuid::now_v7().as_simple())
}

/// Generate a time-ordered (time-sortable) unique identifier using the current time without prefix
#[inline]
pub fn generate_time_ordered_id_without_prefix() -> String {
    uuid::Uuid::now_v7().as_simple().to_string()
}

/// Generate a nanoid with the specified length
#[inline]
pub fn generate_id_with_len(length: usize) -> String {
    nanoid::nanoid!(length, &consts::ALPHABETS)
}
#[allow(missing_docs)]
pub trait DbConnectionParams {
    fn get_username(&self) -> &str;
    fn get_password(&self) -> Secret<String>;
    fn get_host(&self) -> &str;
    fn get_port(&self) -> u16;
    fn get_dbname(&self) -> &str;
    fn get_database_url(&self, schema: &str) -> String {
        format!(
            "postgres://{}:{}@{}:{}/{}?application_name={}&options=-c%20search_path%3D{}",
            self.get_username(),
            self.get_password().peek(),
            self.get_host(),
            self.get_port(),
            self.get_dbname(),
            schema,
            schema,
        )
    }
}

// Can't add doc comments for macro invocations, neither does the macro allow it.
#[allow(missing_docs)]
mod base64_serializer {
    use base64_serde::base64_serde_type;

    base64_serde_type!(pub Base64Serializer, crate::consts::BASE64_ENGINE);
}

#[cfg(test)]
mod nanoid_tests {
    use super::*;
    use crate::{
        consts::{
            MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH,
        },
        id_type::AlphaNumericId,
    };

    #[test]
    fn test_generate_id_with_alphanumeric_id() {
        let alphanumeric_id = AlphaNumericId::from(generate_id(10, "def").into());
        assert!(alphanumeric_id.is_ok())
    }

    #[test]
    fn test_generate_merchant_ref_id_with_default_length() {
        let ref_id = id_type::LengthId::<
            MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH,
            MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH,
        >::from(generate_id_with_default_len("def").into());

        assert!(ref_id.is_ok())
    }
}

/// Module for tokenization-related functionality
///
/// This module provides types and functions for handling tokenized payment data,
/// including response structures and token generation utilities.
#[cfg(all(feature = "v2", feature = "tokenization_v2"))]
pub mod tokenization;

Tests: crates/common_utils/src/lib.rs
================================================================================
#![warn(missing_docs, missing_debug_implementations)]
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR" ), "/", "README.md"))]

use masking::{PeekInterface, Secret};

pub mod access_token;
pub mod consts;
pub mod crypto;
pub mod custom_serde;
#[allow(missing_docs)] // Todo: add docs
pub mod encryption;
pub mod errors;
#[allow(missing_docs)] // Todo: add docs
pub mod events;
pub mod ext_traits;
pub mod fp_utils;
/// Used for hashing
pub mod hashing;
pub mod id_type;
#[cfg(feature = "keymanager")]
pub mod keymanager;
pub mod link_utils;
pub mod macros;
#[cfg(feature = "metrics")]
pub mod metrics;
pub mod new_type;
pub mod payout_method_utils;
pub mod pii;
#[allow(missing_docs)] // Todo: add docs
pub mod request;
#[cfg(feature = "signals")]
pub mod signals;
pub mod transformers;
pub mod types;
/// Unified Connector Service (UCS) interface definitions.
///
/// This module defines types and traits for interacting with the Unified Connector Service.
/// It includes reference ID types for payments and refunds, and a trait for extracting
/// UCS reference information from requests.
pub mod ucs_types;
pub mod validation;

pub use base64_serializer::Base64Serializer;

/// Date-time utilities.
pub mod date_time {
    #[cfg(feature = "async_ext")]
    use std::time::Instant;
    use std::{marker::PhantomData, num::NonZeroU8};

    use masking::{Deserialize, Serialize};
    use time::{
        format_description::{
            well_known::iso8601::{Config, EncodedConfig, Iso8601, TimePrecision},
            BorrowedFormatItem,
        },
        OffsetDateTime, PrimitiveDateTime,
    };

    /// Enum to represent date formats
    #[derive(Debug)]
    pub enum DateFormat {
        /// Format the date in 20191105081132 format
        YYYYMMDDHHmmss,
        /// Format the date in 20191105 format
        YYYYMMDD,
        /// Format the date in 201911050811 format
        YYYYMMDDHHmm,
        /// Format the date in 05112019081132 format
        DDMMYYYYHHmmss,
    }

    /// Create a new [`PrimitiveDateTime`] with the current date and time in UTC.
    pub fn now() -> PrimitiveDateTime {
        let utc_date_time = OffsetDateTime::now_utc();
        PrimitiveDateTime::new(utc_date_time.date(), utc_date_time.time())
    }

    /// Convert from OffsetDateTime to PrimitiveDateTime
    pub fn convert_to_pdt(offset_time: OffsetDateTime) -> PrimitiveDateTime {
        PrimitiveDateTime::new(offset_time.date(), offset_time.time())
    }

    /// Return the UNIX timestamp of the current date and time in UTC
    pub fn now_unix_timestamp() -> i64 {
        OffsetDateTime::now_utc().unix_timestamp()
    }

    /// Calculate execution time for a async block in milliseconds
    #[cfg(feature = "async_ext")]
    pub async fn time_it<T, Fut: futures::Future<Output = T>, F: FnOnce() -> Fut>(
        block: F,
    ) -> (T, f64) {
        let start = Instant::now();
        let result = block().await;
        (result, start.elapsed().as_secs_f64() * 1000f64)
    }

    /// Return the given date and time in UTC with the given format Eg: format: YYYYMMDDHHmmss Eg: 20191105081132
    pub fn format_date(
        date: PrimitiveDateTime,
        format: DateFormat,
    ) -> Result<String, time::error::Format> {
        let format = <&[BorrowedFormatItem<'_>]>::from(format);
        date.format(&format)
    }

    /// Return the current date and time in UTC with the format [year]-[month]-[day]T[hour]:[minute]:[second].mmmZ Eg: 2023-02-15T13:33:18.898Z
    pub fn date_as_yyyymmddthhmmssmmmz() -> Result<String, time::error::Format> {
        const ISO_CONFIG: EncodedConfig = Config::DEFAULT
            .set_time_precision(TimePrecision::Second {
                decimal_digits: NonZeroU8::new(3),
            })
            .encode();
        now().assume_utc().format(&Iso8601::<ISO_CONFIG>)
    }

    /// Return the current date and time in UTC formatted as "ddd, DD MMM YYYY HH:mm:ss GMT".
    pub fn now_rfc7231_http_date() -> Result<String, time::error::Format> {
        let now_utc = OffsetDateTime::now_utc();
        // Desired format: ddd, DD MMM YYYY HH:mm:ss GMT
        // Example: Fri, 23 May 2025 06:19:35 GMT
        let format = time::macros::format_description!(
            "[weekday repr:short], [day padding:zero] [month repr:short] [year repr:full] [hour padding:zero repr:24]:[minute padding:zero]:[second padding:zero] GMT"
        );
        now_utc.format(&format)
    }

    impl From<DateFormat> for &[BorrowedFormatItem<'_>] {
        fn from(format: DateFormat) -> Self {
            match format {
                DateFormat::YYYYMMDDHHmmss => time::macros::format_description!("[year repr:full][month padding:zero repr:numerical][day padding:zero][hour padding:zero repr:24][minute padding:zero][second padding:zero]"),
                DateFormat::YYYYMMDD => time::macros::format_description!("[year repr:full][month padding:zero repr:numerical][day padding:zero]"),
                DateFormat::YYYYMMDDHHmm => time::macros::format_description!("[year repr:full][month padding:zero repr:numerical][day padding:zero][hour padding:zero repr:24][minute padding:zero]"),
                DateFormat::DDMMYYYYHHmmss => time::macros::format_description!("[day padding:zero][month padding:zero repr:numerical][year repr:full][hour padding:zero repr:24][minute padding:zero][second padding:zero]"),
            }
        }
    }

    /// Format the date in 05112019 format
    #[derive(Debug, Clone)]
    pub struct DDMMYYYY;
    /// Format the date in 20191105 format
    #[derive(Debug, Clone)]
    pub struct YYYYMMDD;
    /// Format the date in 20191105081132 format
    #[derive(Debug, Clone)]
    pub struct YYYYMMDDHHmmss;

    /// To serialize the date in Dateformats like YYYYMMDDHHmmss, YYYYMMDD, DDMMYYYY
    #[derive(Debug, Deserialize, Clone)]
    pub struct DateTime<T: TimeStrategy> {
        inner: PhantomData<T>,
        value: PrimitiveDateTime,
    }

    impl<T: TimeStrategy> From<PrimitiveDateTime> for DateTime<T> {
        fn from(value: PrimitiveDateTime) -> Self {
            Self {
                inner: PhantomData,
                value,
            }
        }
    }

    /// Time strategy for the Date, Eg: YYYYMMDDHHmmss, YYYYMMDD, DDMMYYYY
    pub trait TimeStrategy {
        /// Stringify the date as per the Time strategy
        fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
    }

    impl<T: TimeStrategy> Serialize for DateTime<T> {
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: serde::Serializer,
        {
            serializer.collect_str(self)
        }
    }

    impl<T: TimeStrategy> std::fmt::Display for DateTime<T> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            T::fmt(&self.value, f)
        }
    }

    impl TimeStrategy for DDMMYYYY {
        fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let year = input.year();
            #[allow(clippy::as_conversions)]
            let month = input.month() as u8;
            let day = input.day();
            let output = format!("{day:02}{month:02}{year}");
            f.write_str(&output)
        }
    }

    impl TimeStrategy for YYYYMMDD {
        fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let year = input.year();
            #[allow(clippy::as_conversions)]
            let month: u8 = input.month() as u8;
            let day = input.day();
            let output = format!("{year}{month:02}{day:02}");
            f.write_str(&output)
        }
    }

    impl TimeStrategy for YYYYMMDDHHmmss {
        fn fmt(input: &PrimitiveDateTime, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let year = input.year();
            #[allow(clippy::as_conversions)]
            let month = input.month() as u8;
            let day = input.day();
            let hour = input.hour();
            let minute = input.minute();
            let second = input.second();
            let output = format!("{year}{month:02}{day:02}{hour:02}{minute:02}{second:02}");
            f.write_str(&output)
        }
    }
}

/// Generate a nanoid with the given prefix and length
#[inline]
pub fn generate_id(length: usize, prefix: &str) -> String {
    format!("{}_{}", prefix, nanoid::nanoid!(length, &consts::ALPHABETS))
}

/// Generate a ReferenceId with the default length with the given prefix
fn generate_ref_id_with_default_length<const MAX_LENGTH: u8, const MIN_LENGTH: u8>(
    prefix: &str,
) -> id_type::LengthId<MAX_LENGTH, MIN_LENGTH> {
    id_type::LengthId::<MAX_LENGTH, MIN_LENGTH>::new(prefix)
}

/// Generate a customer id with default length, with prefix as `cus`
pub fn generate_customer_id_of_default_length() -> id_type::CustomerId {
    use id_type::GenerateId;

    id_type::CustomerId::generate()
}

/// Generate a organization id with default length, with prefix as `org`
pub fn generate_organization_id_of_default_length() -> id_type::OrganizationId {
    use id_type::GenerateId;

    id_type::OrganizationId::generate()
}

/// Generate a profile id with default length, with prefix as `pro`
pub fn generate_profile_id_of_default_length() -> id_type::ProfileId {
    use id_type::GenerateId;

    id_type::ProfileId::generate()
}

/// Generate a routing id with default length, with prefix as `routing`
pub fn generate_routing_id_of_default_length() -> id_type::RoutingId {
    use id_type::GenerateId;

    id_type::RoutingId::generate()
}
/// Generate a merchant_connector_account id with default length, with prefix as `mca`
pub fn generate_merchant_connector_account_id_of_default_length(
) -> id_type::MerchantConnectorAccountId {
    use id_type::GenerateId;

    id_type::MerchantConnectorAccountId::generate()
}

/// Generate a profile_acquirer id with default length, with prefix as `mer_acq`
pub fn generate_profile_acquirer_id_of_default_length() -> id_type::ProfileAcquirerId {
    use id_type::GenerateId;

    id_type::ProfileAcquirerId::generate()
}

/// Generate a nanoid with the given prefix and a default length
#[inline]
pub fn generate_id_with_default_len(prefix: &str) -> String {
    let len: usize = consts::ID_LENGTH;
    format!("{}_{}", prefix, nanoid::nanoid!(len, &consts::ALPHABETS))
}

/// Generate a time-ordered (time-sortable) unique identifier using the current time
#[inline]
pub fn generate_time_ordered_id(prefix: &str) -> String {
    format!("{prefix}_{}", uuid::Uuid::now_v7().as_simple())
}

/// Generate a time-ordered (time-sortable) unique identifier using the current time without prefix
#[inline]
pub fn generate_time_ordered_id_without_prefix() -> String {
    uuid::Uuid::now_v7().as_simple().to_string()
}

/// Generate a nanoid with the specified length
#[inline]
pub fn generate_id_with_len(length: usize) -> String {
    nanoid::nanoid!(length, &consts::ALPHABETS)
}
#[allow(missing_docs)]
pub trait DbConnectionParams {
    fn get_username(&self) -> &str;
    fn get_password(&self) -> Secret<String>;
    fn get_host(&self) -> &str;
    fn get_port(&self) -> u16;
    fn get_dbname(&self) -> &str;
    fn get_database_url(&self, schema: &str) -> String {
        format!(
            "postgres://{}:{}@{}:{}/{}?application_name={}&options=-c%20search_path%3D{}",
            self.get_username(),
            self.get_password().peek(),
            self.get_host(),
            self.get_port(),
            self.get_dbname(),
            schema,
            schema,
        )
    }
}

// Can't add doc comments for macro invocations, neither does the macro allow it.
#[allow(missing_docs)]
mod base64_serializer {
    use base64_serde::base64_serde_type;

    base64_serde_type!(pub Base64Serializer, crate::consts::BASE64_ENGINE);
}

#[cfg(test)]
mod nanoid_tests {
    use super::*;
    use crate::{
        consts::{
            MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH, MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH,
        },
        id_type::AlphaNumericId,
    };

    #[test]
    fn test_generate_id_with_alphanumeric_id() {
        let alphanumeric_id = AlphaNumericId::from(generate_id(10, "def").into());
        assert!(alphanumeric_id.is_ok())
    }

    #[test]
    fn test_generate_merchant_ref_id_with_default_length() {
        let ref_id = id_type::LengthId::<
            MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH,
            MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH,
        >::from(generate_id_with_default_len("def").into());

        assert!(ref_id.is_ok())
    }
}

/// Module for tokenization-related functionality
///
/// This module provides types and functions for handling tokenized payment data,
/// including response structures and token generation utilities.
#[cfg(all(feature = "v2", feature = "tokenization_v2"))]
pub mod tokenization;


Test-Implementation Pair

Implementation: crates/common_utils/src/new_type.rs
================================================================================
//! Contains new types with restrictions
use masking::{ExposeInterface, PeekInterface, Secret};

use crate::{
    consts::MAX_ALLOWED_MERCHANT_NAME_LENGTH,
    pii::{Email, UpiVpaMaskingStrategy},
    transformers::ForeignFrom,
};

#[nutype::nutype(
    derive(Clone, Serialize, Deserialize, Debug),
    validate(len_char_min = 1, len_char_max = MAX_ALLOWED_MERCHANT_NAME_LENGTH)
)]
pub struct MerchantName(String);

impl masking::SerializableSecret for MerchantName {}

/// Function for masking alphanumeric characters in a string.
///
/// # Arguments
///     `val`
///         - holds reference to the string to be masked.
///     `unmasked_char_count`
///         - minimum character count to remain unmasked for identification
///         - this number is for keeping the characters unmasked from
///             both beginning (if feasible) and ending of the string.
///     `min_masked_char_count`
///         - this ensures the minimum number of characters to be masked
///
/// # Behaviour
///     - Returns the original string if its length is less than or equal to `unmasked_char_count`.
///     - If the string length allows, keeps `unmasked_char_count` characters unmasked at both start and end.
///     - Otherwise, keeps `unmasked_char_count` characters unmasked only at the end.
///     - Only alphanumeric characters are masked; other characters remain unchanged.
///
/// # Examples
///     Sort Code
///         (12-34-56, 2, 2) -> 12-**-56
///     Routing number
///         (026009593, 3, 3) -> 026***593
///     CNPJ
///         (12345678901, 4, 4) -> *******8901
///     CNPJ
///         (12345678901, 4, 3) -> 1234***8901
///     Pix key
///         (123e-a452-1243-1244-000, 4, 4) -> 123e-****-****-****-000
///     IBAN
///         (AL35202111090000000001234567, 5, 5) -> AL352******************34567
fn apply_mask(val: &str, unmasked_char_count: usize, min_masked_char_count: usize) -> String {
    let len = val.len();
    if len <= unmasked_char_count {
        return val.to_string();
    }

    let mask_start_index =
    // For showing only last `unmasked_char_count` characters
    if len < (unmasked_char_count * 2 + min_masked_char_count) {
        0
    // For showing first and last `unmasked_char_count` characters
    } else {
        unmasked_char_count
    };
    let mask_end_index = len - unmasked_char_count - 1;
    let range = mask_start_index..=mask_end_index;

    val.chars()
        .enumerate()
        .fold(String::new(), |mut acc, (index, ch)| {
            if ch.is_alphanumeric() && range.contains(&index) {
                acc.push('*');
            } else {
                acc.push(ch);
            }
            acc
        })
}

/// Masked sort code
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedSortCode(Secret<String>);
impl From<String> for MaskedSortCode {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 2, 2);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedSortCode {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked Routing number
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedRoutingNumber(Secret<String>);
impl From<String> for MaskedRoutingNumber {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 3, 3);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedRoutingNumber {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked bank account
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedBankAccount(Secret<String>);
impl From<String> for MaskedBankAccount {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 4, 4);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedBankAccount {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked IBAN
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedIban(Secret<String>);
impl From<String> for MaskedIban {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 5, 5);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedIban {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked IBAN
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedBic(Secret<String>);
impl From<String> for MaskedBic {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 3, 2);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedBic {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked UPI ID
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedUpiVpaId(Secret<String>);
impl From<String> for MaskedUpiVpaId {
    fn from(src: String) -> Self {
        let unmasked_char_count = 2;
        let masked_value = if let Some((user_identifier, bank_or_psp)) = src.split_once('@') {
            let masked_user_identifier = user_identifier
                .to_string()
                .chars()
                .take(unmasked_char_count)
                .collect::<String>()
                + &"*".repeat(user_identifier.len() - unmasked_char_count);
            format!("{masked_user_identifier}@{bank_or_psp}")
        } else {
            let masked_value = apply_mask(src.as_ref(), unmasked_char_count, 8);
            masked_value
        };

        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String, UpiVpaMaskingStrategy>> for MaskedUpiVpaId {
    fn from(secret: Secret<String, UpiVpaMaskingStrategy>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked Email
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedEmail(Secret<String>);
impl From<String> for MaskedEmail {
    fn from(src: String) -> Self {
        let unmasked_char_count = 2;
        let masked_value = if let Some((user_identifier, domain)) = src.split_once('@') {
            let masked_user_identifier = user_identifier
                .to_string()
                .chars()
                .take(unmasked_char_count)
                .collect::<String>()
                + &"*".repeat(user_identifier.len() - unmasked_char_count);
            format!("{masked_user_identifier}@{domain}")
        } else {
            let masked_value = apply_mask(src.as_ref(), unmasked_char_count, 8);
            masked_value
        };
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedEmail {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}
impl ForeignFrom<Email> for MaskedEmail {
    fn foreign_from(email: Email) -> Self {
        let email_value: String = email.expose().peek().to_owned();
        Self::from(email_value)
    }
}

/// Masked Phone Number
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedPhoneNumber(Secret<String>);
impl From<String> for MaskedPhoneNumber {
    fn from(src: String) -> Self {
        let unmasked_char_count = 2;
        let masked_value = if unmasked_char_count <= src.len() {
            let len = src.len();
            // mask every character except the last 2
            "*".repeat(len - unmasked_char_count).to_string()
                + src
                    .get(len.saturating_sub(unmasked_char_count)..)
                    .unwrap_or("")
        } else {
            src
        };
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedPhoneNumber {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked Psp token
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedPspToken(Secret<String>);
impl From<String> for MaskedPspToken {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 3, 3);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedPspToken {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

#[cfg(test)]
mod apply_mask_fn_test {
    use masking::PeekInterface;

    use crate::new_type::{
        apply_mask, MaskedBankAccount, MaskedIban, MaskedRoutingNumber, MaskedSortCode,
        MaskedUpiVpaId,
    };
    #[test]
    fn test_masked_types() {
        let sort_code = MaskedSortCode::from("110011".to_string());
        let routing_number = MaskedRoutingNumber::from("056008849".to_string());
        let bank_account = MaskedBankAccount::from("12345678901234".to_string());
        let iban = MaskedIban::from("NL02ABNA0123456789".to_string());
        let upi_vpa = MaskedUpiVpaId::from("someusername@okhdfcbank".to_string());

        // Standard masked data tests
        assert_eq!(sort_code.0.peek().to_owned(), "11**11".to_string());
        assert_eq!(routing_number.0.peek().to_owned(), "056***849".to_string());
        assert_eq!(
            bank_account.0.peek().to_owned(),
            "1234******1234".to_string()
        );
        assert_eq!(iban.0.peek().to_owned(), "NL02A********56789".to_string());
        assert_eq!(
            upi_vpa.0.peek().to_owned(),
            "so**********@okhdfcbank".to_string()
        );
    }

    #[test]
    fn test_apply_mask_fn() {
        let value = "12345678901".to_string();

        // Generic masked tests
        assert_eq!(apply_mask(&value, 2, 2), "12*******01".to_string());
        assert_eq!(apply_mask(&value, 3, 2), "123*****901".to_string());
        assert_eq!(apply_mask(&value, 3, 3), "123*****901".to_string());
        assert_eq!(apply_mask(&value, 4, 3), "1234***8901".to_string());
        assert_eq!(apply_mask(&value, 4, 4), "*******8901".to_string());
        assert_eq!(apply_mask(&value, 5, 4), "******78901".to_string());
        assert_eq!(apply_mask(&value, 5, 5), "******78901".to_string());
        assert_eq!(apply_mask(&value, 6, 5), "*****678901".to_string());
        assert_eq!(apply_mask(&value, 6, 6), "*****678901".to_string());
        assert_eq!(apply_mask(&value, 7, 6), "****5678901".to_string());
        assert_eq!(apply_mask(&value, 7, 7), "****5678901".to_string());
        assert_eq!(apply_mask(&value, 8, 7), "***45678901".to_string());
        assert_eq!(apply_mask(&value, 8, 8), "***45678901".to_string());
    }
}

Tests: crates/common_utils/src/new_type.rs
================================================================================
//! Contains new types with restrictions
use masking::{ExposeInterface, PeekInterface, Secret};

use crate::{
    consts::MAX_ALLOWED_MERCHANT_NAME_LENGTH,
    pii::{Email, UpiVpaMaskingStrategy},
    transformers::ForeignFrom,
};

#[nutype::nutype(
    derive(Clone, Serialize, Deserialize, Debug),
    validate(len_char_min = 1, len_char_max = MAX_ALLOWED_MERCHANT_NAME_LENGTH)
)]
pub struct MerchantName(String);

impl masking::SerializableSecret for MerchantName {}

/// Function for masking alphanumeric characters in a string.
///
/// # Arguments
///     `val`
///         - holds reference to the string to be masked.
///     `unmasked_char_count`
///         - minimum character count to remain unmasked for identification
///         - this number is for keeping the characters unmasked from
///             both beginning (if feasible) and ending of the string.
///     `min_masked_char_count`
///         - this ensures the minimum number of characters to be masked
///
/// # Behaviour
///     - Returns the original string if its length is less than or equal to `unmasked_char_count`.
///     - If the string length allows, keeps `unmasked_char_count` characters unmasked at both start and end.
///     - Otherwise, keeps `unmasked_char_count` characters unmasked only at the end.
///     - Only alphanumeric characters are masked; other characters remain unchanged.
///
/// # Examples
///     Sort Code
///         (12-34-56, 2, 2) -> 12-**-56
///     Routing number
///         (026009593, 3, 3) -> 026***593
///     CNPJ
///         (12345678901, 4, 4) -> *******8901
///     CNPJ
///         (12345678901, 4, 3) -> 1234***8901
///     Pix key
///         (123e-a452-1243-1244-000, 4, 4) -> 123e-****-****-****-000
///     IBAN
///         (AL35202111090000000001234567, 5, 5) -> AL352******************34567
fn apply_mask(val: &str, unmasked_char_count: usize, min_masked_char_count: usize) -> String {
    let len = val.len();
    if len <= unmasked_char_count {
        return val.to_string();
    }

    let mask_start_index =
    // For showing only last `unmasked_char_count` characters
    if len < (unmasked_char_count * 2 + min_masked_char_count) {
        0
    // For showing first and last `unmasked_char_count` characters
    } else {
        unmasked_char_count
    };
    let mask_end_index = len - unmasked_char_count - 1;
    let range = mask_start_index..=mask_end_index;

    val.chars()
        .enumerate()
        .fold(String::new(), |mut acc, (index, ch)| {
            if ch.is_alphanumeric() && range.contains(&index) {
                acc.push('*');
            } else {
                acc.push(ch);
            }
            acc
        })
}

/// Masked sort code
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedSortCode(Secret<String>);
impl From<String> for MaskedSortCode {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 2, 2);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedSortCode {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked Routing number
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedRoutingNumber(Secret<String>);
impl From<String> for MaskedRoutingNumber {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 3, 3);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedRoutingNumber {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked bank account
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedBankAccount(Secret<String>);
impl From<String> for MaskedBankAccount {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 4, 4);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedBankAccount {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked IBAN
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedIban(Secret<String>);
impl From<String> for MaskedIban {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 5, 5);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedIban {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked IBAN
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedBic(Secret<String>);
impl From<String> for MaskedBic {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 3, 2);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedBic {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked UPI ID
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedUpiVpaId(Secret<String>);
impl From<String> for MaskedUpiVpaId {
    fn from(src: String) -> Self {
        let unmasked_char_count = 2;
        let masked_value = if let Some((user_identifier, bank_or_psp)) = src.split_once('@') {
            let masked_user_identifier = user_identifier
                .to_string()
                .chars()
                .take(unmasked_char_count)
                .collect::<String>()
                + &"*".repeat(user_identifier.len() - unmasked_char_count);
            format!("{masked_user_identifier}@{bank_or_psp}")
        } else {
            let masked_value = apply_mask(src.as_ref(), unmasked_char_count, 8);
            masked_value
        };

        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String, UpiVpaMaskingStrategy>> for MaskedUpiVpaId {
    fn from(secret: Secret<String, UpiVpaMaskingStrategy>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked Email
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedEmail(Secret<String>);
impl From<String> for MaskedEmail {
    fn from(src: String) -> Self {
        let unmasked_char_count = 2;
        let masked_value = if let Some((user_identifier, domain)) = src.split_once('@') {
            let masked_user_identifier = user_identifier
                .to_string()
                .chars()
                .take(unmasked_char_count)
                .collect::<String>()
                + &"*".repeat(user_identifier.len() - unmasked_char_count);
            format!("{masked_user_identifier}@{domain}")
        } else {
            let masked_value = apply_mask(src.as_ref(), unmasked_char_count, 8);
            masked_value
        };
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedEmail {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}
impl ForeignFrom<Email> for MaskedEmail {
    fn foreign_from(email: Email) -> Self {
        let email_value: String = email.expose().peek().to_owned();
        Self::from(email_value)
    }
}

/// Masked Phone Number
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedPhoneNumber(Secret<String>);
impl From<String> for MaskedPhoneNumber {
    fn from(src: String) -> Self {
        let unmasked_char_count = 2;
        let masked_value = if unmasked_char_count <= src.len() {
            let len = src.len();
            // mask every character except the last 2
            "*".repeat(len - unmasked_char_count).to_string()
                + src
                    .get(len.saturating_sub(unmasked_char_count)..)
                    .unwrap_or("")
        } else {
            src
        };
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedPhoneNumber {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

/// Masked Psp token
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MaskedPspToken(Secret<String>);
impl From<String> for MaskedPspToken {
    fn from(src: String) -> Self {
        let masked_value = apply_mask(src.as_ref(), 3, 3);
        Self(Secret::from(masked_value))
    }
}
impl From<Secret<String>> for MaskedPspToken {
    fn from(secret: Secret<String>) -> Self {
        Self::from(secret.expose())
    }
}

#[cfg(test)]
mod apply_mask_fn_test {
    use masking::PeekInterface;

    use crate::new_type::{
        apply_mask, MaskedBankAccount, MaskedIban, MaskedRoutingNumber, MaskedSortCode,
        MaskedUpiVpaId,
    };
    #[test]
    fn test_masked_types() {
        let sort_code = MaskedSortCode::from("110011".to_string());
        let routing_number = MaskedRoutingNumber::from("056008849".to_string());
        let bank_account = MaskedBankAccount::from("12345678901234".to_string());
        let iban = MaskedIban::from("NL02ABNA0123456789".to_string());
        let upi_vpa = MaskedUpiVpaId::from("someusername@okhdfcbank".to_string());

        // Standard masked data tests
        assert_eq!(sort_code.0.peek().to_owned(), "11**11".to_string());
        assert_eq!(routing_number.0.peek().to_owned(), "056***849".to_string());
        assert_eq!(
            bank_account.0.peek().to_owned(),
            "1234******1234".to_string()
        );
        assert_eq!(iban.0.peek().to_owned(), "NL02A********56789".to_string());
        assert_eq!(
            upi_vpa.0.peek().to_owned(),
            "so**********@okhdfcbank".to_string()
        );
    }

    #[test]
    fn test_apply_mask_fn() {
        let value = "12345678901".to_string();

        // Generic masked tests
        assert_eq!(apply_mask(&value, 2, 2), "12*******01".to_string());
        assert_eq!(apply_mask(&value, 3, 2), "123*****901".to_string());
        assert_eq!(apply_mask(&value, 3, 3), "123*****901".to_string());
        assert_eq!(apply_mask(&value, 4, 3), "1234***8901".to_string());
        assert_eq!(apply_mask(&value, 4, 4), "*******8901".to_string());
        assert_eq!(apply_mask(&value, 5, 4), "******78901".to_string());
        assert_eq!(apply_mask(&value, 5, 5), "******78901".to_string());
        assert_eq!(apply_mask(&value, 6, 5), "*****678901".to_string());
        assert_eq!(apply_mask(&value, 6, 6), "*****678901".to_string());
        assert_eq!(apply_mask(&value, 7, 6), "****5678901".to_string());
        assert_eq!(apply_mask(&value, 7, 7), "****5678901".to_string());
        assert_eq!(apply_mask(&value, 8, 7), "***45678901".to_string());
        assert_eq!(apply_mask(&value, 8, 8), "***45678901".to_string());
    }
}


Test-Implementation Pair

Implementation: crates/common_utils/src/validation.rs
================================================================================
//! Custom validations for some shared types.

#![deny(clippy::invalid_regex)]

use std::{collections::HashSet, sync::LazyLock};

use error_stack::report;
use globset::Glob;
use regex::Regex;
#[cfg(feature = "logs")]
use router_env::logger;

use crate::errors::{CustomResult, ValidationError};

/// Validates a given phone number using the [phonenumber] crate
///
/// It returns a [ValidationError::InvalidValue] in case it could not parse the phone number
pub fn validate_phone_number(phone_number: &str) -> Result<(), ValidationError> {
    let _ = phonenumber::parse(None, phone_number).map_err(|e| ValidationError::InvalidValue {
        message: format!("Could not parse phone number: {phone_number}, because: {e:?}"),
    })?;

    Ok(())
}

/// Performs a simple validation against a provided email address.
pub fn validate_email(email: &str) -> CustomResult<(), ValidationError> {
    #[deny(clippy::invalid_regex)]
    static EMAIL_REGEX: LazyLock<Option<Regex>> = LazyLock::new(|| {
        #[allow(unknown_lints)]
        #[allow(clippy::manual_ok_err)]
        match Regex::new(
            r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
        ) {
            Ok(regex) => Some(regex),
            Err(_error) => {
                #[cfg(feature = "logs")]
                logger::error!(?_error);
                None
            }
        }
    });
    let email_regex = match EMAIL_REGEX.as_ref() {
        Some(regex) => Ok(regex),
        None => Err(report!(ValidationError::InvalidValue {
            message: "Invalid regex expression".into()
        })),
    }?;

    const EMAIL_MAX_LENGTH: usize = 319;
    if email.is_empty() || email.chars().count() > EMAIL_MAX_LENGTH {
        return Err(report!(ValidationError::InvalidValue {
            message: "Email address is either empty or exceeds maximum allowed length".into()
        }));
    }

    if !email_regex.is_match(email) {
        return Err(report!(ValidationError::InvalidValue {
            message: "Invalid email address format".into()
        }));
    }

    Ok(())
}

/// Checks whether a given domain matches against a list of valid domain glob patterns
pub fn validate_domain_against_allowed_domains(
    domain: &str,
    allowed_domains: HashSet<String>,
) -> bool {
    allowed_domains.iter().any(|allowed_domain| {
        Glob::new(allowed_domain)
            .map(|glob| glob.compile_matcher().is_match(domain))
            .map_err(|err| {
                let err_msg = format!(
                    "Invalid glob pattern for configured allowed_domain [{allowed_domain:?}]! - {err:?}",

                );
                #[cfg(feature = "logs")]
                logger::error!(err_msg);
                err_msg
            })
            .unwrap_or(false)
    })
}

/// checks whether the input string contains potential XSS or SQL injection attack vectors
pub fn contains_potential_xss_or_sqli(input: &str) -> bool {
    let decoded = urlencoding::decode(input).unwrap_or_else(|_| input.into());

    // Check for suspicious percent-encoded patterns
    static PERCENT_ENCODED: LazyLock<Option<Regex>> = LazyLock::new(|| {
        Regex::new(r"%[0-9A-Fa-f]{2}")
            .map_err(|_err| {
                #[cfg(feature = "logs")]
                logger::error!(?_err);
            })
            .ok()
    });

    if decoded.contains('%') {
        match PERCENT_ENCODED.as_ref() {
            Some(regex) => {
                if regex.is_match(&decoded) {
                    return true;
                }
            }
            None => return true,
        }
    }

    if ammonia::is_html(&decoded) {
        return true;
    }

    static XSS: LazyLock<Option<Regex>> = LazyLock::new(|| {
        Regex::new(
            r"(?is)\bon[a-z]+\s*=|\bjavascript\s*:|\bdata\s*:\s*text/html|\b(alert|prompt|confirm|eval)\s*\(",
        )
        .map_err(|_err| {
            #[cfg(feature = "logs")]
             logger::error!(?_err);
        })
        .ok()
    });

    static SQLI: LazyLock<Option<Regex>> = LazyLock::new(|| {
        Regex::new(
            r"(?is)(?:')\s*or\s*'?\d+'?=?\d*|union\s+select|insert\s+into|drop\s+table|information_schema|sleep\s*\(|--|;",
        )
        .map_err(|_err| {
            #[cfg(feature = "logs")]
             logger::error!(?_err);
        })
        .ok()
    });

    match XSS.as_ref() {
        Some(regex) => {
            if regex.is_match(&decoded) {
                return true;
            }
        }
        None => return true,
    }

    match SQLI.as_ref() {
        Some(regex) => {
            if regex.is_match(&decoded) {
                return true;
            }
        }
        None => return true,
    }

    false
}

#[cfg(test)]
mod tests {
    use fake::{faker::internet::en::SafeEmail, Fake};
    use proptest::{
        prop_assert,
        strategy::{Just, NewTree, Strategy},
        test_runner::TestRunner,
    };
    use test_case::test_case;

    use super::*;

    #[derive(Debug)]
    struct ValidEmail;

    impl Strategy for ValidEmail {
        type Tree = Just<String>;
        type Value = String;

        fn new_tree(&self, _runner: &mut TestRunner) -> NewTree<Self> {
            Ok(Just(SafeEmail().fake()))
        }
    }

    #[test]
    fn test_validate_email() {
        let result = validate_email("abc@example.com");
        assert!(result.is_ok());

        let result = validate_email("abc+123@example.com");
        assert!(result.is_ok());

        let result = validate_email("");
        assert!(result.is_err());
    }

    #[test_case("+40745323456" ; "Romanian valid phone number")]
    #[test_case("+34912345678" ; "Spanish valid phone number")]
    #[test_case("+41 79 123 45 67" ; "Swiss valid phone number")]
    #[test_case("+66 81 234 5678" ; "Thailand valid phone number")]
    fn test_validate_phone_number(phone_number: &str) {
        assert!(validate_phone_number(phone_number).is_ok());
    }

    #[test_case("9123456789" ; "Romanian invalid phone number")]
    fn test_invalid_phone_number(phone_number: &str) {
        let res = validate_phone_number(phone_number);
        assert!(res.is_err());
    }

    proptest::proptest! {
        /// Example of unit test
        #[test]
        fn proptest_valid_fake_email(email in ValidEmail) {
            prop_assert!(validate_email(&email).is_ok());
        }

        /// Example of unit test
        #[test]
        fn proptest_invalid_data_email(email in "\\PC*") {
            prop_assert!(validate_email(&email).is_err());
        }

        #[test]
        fn proptest_invalid_email(email in "[.+]@(.+)") {
            prop_assert!(validate_email(&email).is_err());
        }
    }

    #[test]
    fn detects_basic_script_tags() {
        assert!(contains_potential_xss_or_sqli(
            ""
        ));
    }

    #[test]
    fn detects_event_handlers() {
        assert!(contains_potential_xss_or_sqli(
            "onload=alert('xss') onclick=alert('xss') onmouseover=alert('xss')",
        ));
    }

    #[test]
    fn detects_data_url_payload() {
        assert!(contains_potential_xss_or_sqli(
            "data:text/html,",
        ));
    }

    #[test]
    fn detects_iframe_javascript_src() {
        assert!(contains_potential_xss_or_sqli(
            "",
        ));
    }

    #[test]
    fn detects_svg_with_script() {
        assert!(contains_potential_xss_or_sqli(
            "<svg></svg>",
        ));
    }

    #[test]
    fn detects_object_with_js() {
        assert!(contains_potential_xss_or_sqli(
            "<object data=javascript:alert('xss')></object>",
        ));
    }

    #[test]
    fn detects_mixed_case_tags() {
        assert!(contains_potential_xss_or_sqli(
            ""
        ));
    }

    #[test]
    fn detects_embedded_script_in_text() {
        assert!(contains_potential_xss_or_sqli(
            "TestCompany",
        ));
    }

    #[test]
    fn detects_math_with_script() {
        assert!(contains_potential_xss_or_sqli(
            "<math></math>",
        ));
    }

    #[test]
    fn detects_basic_sql_tautology() {
        assert!(contains_potential_xss_or_sqli("' OR '1'='1"));
    }

    #[test]
    fn detects_time_based_sqli() {
        assert!(contains_potential_xss_or_sqli("' OR SLEEP(5) --"));
    }

    #[test]
    fn detects_percent_encoded_sqli() {
        // %27 OR %271%27=%271 is a typical encoded variant
        assert!(contains_potential_xss_or_sqli("%27%20OR%20%271%27%3D%271"));
    }

    #[test]
    fn detects_benign_html_as_suspicious() {
        assert!(contains_potential_xss_or_sqli("Hello"));
    }

    #[test]
    fn allows_legitimate_plain_text() {
        assert!(!contains_potential_xss_or_sqli("My Test Company Ltd."));
    }

    #[test]
    fn allows_normal_url() {
        assert!(!contains_potential_xss_or_sqli("https://example.com"));
    }

    #[test]
    fn allows_percent_char_without_encoding() {
        assert!(!contains_potential_xss_or_sqli("Get 50% off today"));
    }
}

Tests: crates/common_utils/src/validation.rs
================================================================================
//! Custom validations for some shared types.

#![deny(clippy::invalid_regex)]

use std::{collections::HashSet, sync::LazyLock};

use error_stack::report;
use globset::Glob;
use regex::Regex;
#[cfg(feature = "logs")]
use router_env::logger;

use crate::errors::{CustomResult, ValidationError};

/// Validates a given phone number using the [phonenumber] crate
///
/// It returns a [ValidationError::InvalidValue] in case it could not parse the phone number
pub fn validate_phone_number(phone_number: &str) -> Result<(), ValidationError> {
    let _ = phonenumber::parse(None, phone_number).map_err(|e| ValidationError::InvalidValue {
        message: format!("Could not parse phone number: {phone_number}, because: {e:?}"),
    })?;

    Ok(())
}

/// Performs a simple validation against a provided email address.
pub fn validate_email(email: &str) -> CustomResult<(), ValidationError> {
    #[deny(clippy::invalid_regex)]
    static EMAIL_REGEX: LazyLock<Option<Regex>> = LazyLock::new(|| {
        #[allow(unknown_lints)]
        #[allow(clippy::manual_ok_err)]
        match Regex::new(
            r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$",
        ) {
            Ok(regex) => Some(regex),
            Err(_error) => {
                #[cfg(feature = "logs")]
                logger::error!(?_error);
                None
            }
        }
    });
    let email_regex = match EMAIL_REGEX.as_ref() {
        Some(regex) => Ok(regex),
        None => Err(report!(ValidationError::InvalidValue {
            message: "Invalid regex expression".into()
        })),
    }?;

    const EMAIL_MAX_LENGTH: usize = 319;
    if email.is_empty() || email.chars().count() > EMAIL_MAX_LENGTH {
        return Err(report!(ValidationError::InvalidValue {
            message: "Email address is either empty or exceeds maximum allowed length".into()
        }));
    }

    if !email_regex.is_match(email) {
        return Err(report!(ValidationError::InvalidValue {
            message: "Invalid email address format".into()
        }));
    }

    Ok(())
}

/// Checks whether a given domain matches against a list of valid domain glob patterns
pub fn validate_domain_against_allowed_domains(
    domain: &str,
    allowed_domains: HashSet<String>,
) -> bool {
    allowed_domains.iter().any(|allowed_domain| {
        Glob::new(allowed_domain)
            .map(|glob| glob.compile_matcher().is_match(domain))
            .map_err(|err| {
                let err_msg = format!(
                    "Invalid glob pattern for configured allowed_domain [{allowed_domain:?}]! - {err:?}",

                );
                #[cfg(feature = "logs")]
                logger::error!(err_msg);
                err_msg
            })
            .unwrap_or(false)
    })
}

/// checks whether the input string contains potential XSS or SQL injection attack vectors
pub fn contains_potential_xss_or_sqli(input: &str) -> bool {
    let decoded = urlencoding::decode(input).unwrap_or_else(|_| input.into());

    // Check for suspicious percent-encoded patterns
    static PERCENT_ENCODED: LazyLock<Option<Regex>> = LazyLock::new(|| {
        Regex::new(r"%[0-9A-Fa-f]{2}")
            .map_err(|_err| {
                #[cfg(feature = "logs")]
                logger::error!(?_err);
            })
            .ok()
    });

    if decoded.contains('%') {
        match PERCENT_ENCODED.as_ref() {
            Some(regex) => {
                if regex.is_match(&decoded) {
                    return true;
                }
            }
            None => return true,
        }
    }

    if ammonia::is_html(&decoded) {
        return true;
    }

    static XSS: LazyLock<Option<Regex>> = LazyLock::new(|| {
        Regex::new(
            r"(?is)\bon[a-z]+\s*=|\bjavascript\s*:|\bdata\s*:\s*text/html|\b(alert|prompt|confirm|eval)\s*\(",
        )
        .map_err(|_err| {
            #[cfg(feature = "logs")]
             logger::error!(?_err);
        })
        .ok()
    });

    static SQLI: LazyLock<Option<Regex>> = LazyLock::new(|| {
        Regex::new(
            r"(?is)(?:')\s*or\s*'?\d+'?=?\d*|union\s+select|insert\s+into|drop\s+table|information_schema|sleep\s*\(|--|;",
        )
        .map_err(|_err| {
            #[cfg(feature = "logs")]
             logger::error!(?_err);
        })
        .ok()
    });

    match XSS.as_ref() {
        Some(regex) => {
            if regex.is_match(&decoded) {
                return true;
            }
        }
        None => return true,
    }

    match SQLI.as_ref() {
        Some(regex) => {
            if regex.is_match(&decoded) {
                return true;
            }
        }
        None => return true,
    }

    false
}

#[cfg(test)]
mod tests {
    use fake::{faker::internet::en::SafeEmail, Fake};
    use proptest::{
        prop_assert,
        strategy::{Just, NewTree, Strategy},
        test_runner::TestRunner,
    };
    use test_case::test_case;

    use super::*;

    #[derive(Debug)]
    struct ValidEmail;

    impl Strategy for ValidEmail {
        type Tree = Just<String>;
        type Value = String;

        fn new_tree(&self, _runner: &mut TestRunner) -> NewTree<Self> {
            Ok(Just(SafeEmail().fake()))
        }
    }

    #[test]
    fn test_validate_email() {
        let result = validate_email("abc@example.com");
        assert!(result.is_ok());

        let result = validate_email("abc+123@example.com");
        assert!(result.is_ok());

        let result = validate_email("");
        assert!(result.is_err());
    }

    #[test_case("+40745323456" ; "Romanian valid phone number")]
    #[test_case("+34912345678" ; "Spanish valid phone number")]
    #[test_case("+41 79 123 45 67" ; "Swiss valid phone number")]
    #[test_case("+66 81 234 5678" ; "Thailand valid phone number")]
    fn test_validate_phone_number(phone_number: &str) {
        assert!(validate_phone_number(phone_number).is_ok());
    }

    #[test_case("9123456789" ; "Romanian invalid phone number")]
    fn test_invalid_phone_number(phone_number: &str) {
        let res = validate_phone_number(phone_number);
        assert!(res.is_err());
    }

    proptest::proptest! {
        /// Example of unit test
        #[test]
        fn proptest_valid_fake_email(email in ValidEmail) {
            prop_assert!(validate_email(&email).is_ok());
        }

        /// Example of unit test
        #[test]
        fn proptest_invalid_data_email(email in "\\PC*") {
            prop_assert!(validate_email(&email).is_err());
        }

        #[test]
        fn proptest_invalid_email(email in "[.+]@(.+)") {
            prop_assert!(validate_email(&email).is_err());
        }
    }

    #[test]
    fn detects_basic_script_tags() {
        assert!(contains_potential_xss_or_sqli(
            ""
        ));
    }

    #[test]
    fn detects_event_handlers() {
        assert!(contains_potential_xss_or_sqli(
            "onload=alert('xss') onclick=alert('xss') onmouseover=alert('xss')",
        ));
    }

    #[test]
    fn detects_data_url_payload() {
        assert!(contains_potential_xss_or_sqli(
            "data:text/html,",
        ));
    }

    #[test]
    fn detects_iframe_javascript_src() {
        assert!(contains_potential_xss_or_sqli(
            "",
        ));
    }

    #[test]
    fn detects_svg_with_script() {
        assert!(contains_potential_xss_or_sqli(
            "<svg></svg>",
        ));
    }

    #[test]
    fn detects_object_with_js() {
        assert!(contains_potential_xss_or_sqli(
            "<object data=javascript:alert('xss')></object>",
        ));
    }

    #[test]
    fn detects_mixed_case_tags() {
        assert!(contains_potential_xss_or_sqli(
            ""
        ));
    }

    #[test]
    fn detects_embedded_script_in_text() {
        assert!(contains_potential_xss_or_sqli(
            "TestCompany",
        ));
    }

    #[test]
    fn detects_math_with_script() {
        assert!(contains_potential_xss_or_sqli(
            "<math></math>",
        ));
    }

    #[test]
    fn detects_basic_sql_tautology() {
        assert!(contains_potential_xss_or_sqli("' OR '1'='1"));
    }

    #[test]
    fn detects_time_based_sqli() {
        assert!(contains_potential_xss_or_sqli("' OR SLEEP(5) --"));
    }

    #[test]
    fn detects_percent_encoded_sqli() {
        // %27 OR %271%27=%271 is a typical encoded variant
        assert!(contains_potential_xss_or_sqli("%27%20OR%20%271%27%3D%271"));
    }

    #[test]
    fn detects_benign_html_as_suspicious() {
        assert!(contains_potential_xss_or_sqli("Hello"));
    }

    #[test]
    fn allows_legitimate_plain_text() {
        assert!(!contains_potential_xss_or_sqli("My Test Company Ltd."));
    }

    #[test]
    fn allows_normal_url() {
        assert!(!contains_potential_xss_or_sqli("https://example.com"));
    }

    #[test]
    fn allows_percent_char_without_encoding() {
        assert!(!contains_potential_xss_or_sqli("Get 50% off today"));
    }
}


Test-Implementation Pair

Implementation: crates/common_utils/src/pii.rs
================================================================================
//! Personal Identifiable Information protection.

use std::{convert::AsRef, fmt, ops, str::FromStr};

use diesel::{
    backend::Backend,
    deserialize,
    deserialize::FromSql,
    prelude::*,
    serialize::{Output, ToSql},
    sql_types, AsExpression,
};
use error_stack::ResultExt;
use masking::{ExposeInterface, Secret, Strategy, WithType};
#[cfg(feature = "logs")]
use router_env::logger;
use serde::Deserialize;

use crate::{
    crypto::Encryptable,
    errors::{self, ValidationError},
    validation::{validate_email, validate_phone_number},
};

/// A string constant representing a redacted or masked value.
pub const REDACTED: &str = "Redacted";

/// Type alias for serde_json value which has Secret Information
pub type SecretSerdeValue = Secret<serde_json::Value>;

/// Strategy for masking a PhoneNumber
#[derive(Debug)]
pub enum PhoneNumberStrategy {}

/// Phone Number
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(try_from = "String")]
pub struct PhoneNumber(Secret<String, PhoneNumberStrategy>);

impl<T> Strategy<T> for PhoneNumberStrategy
where
    T: AsRef<str> + fmt::Debug,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let val_str: &str = val.as_ref();

        if let Some(val_str) = val_str.get(val_str.len() - 4..) {
            // masks everything but the last 4 digits
            write!(f, "{}{}", "*".repeat(val_str.len() - 4), val_str)
        } else {
            #[cfg(feature = "logs")]
            logger::error!("Invalid phone number: {val_str}");
            WithType::fmt(val, f)
        }
    }
}

impl FromStr for PhoneNumber {
    type Err = error_stack::Report<ValidationError>;
    fn from_str(phone_number: &str) -> Result<Self, Self::Err> {
        validate_phone_number(phone_number)?;
        let secret = Secret::<String, PhoneNumberStrategy>::new(phone_number.to_string());
        Ok(Self(secret))
    }
}

impl TryFrom<String> for PhoneNumber {
    type Error = error_stack::Report<errors::ParsingError>;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::from_str(&value).change_context(errors::ParsingError::PhoneNumberParsingError)
    }
}

impl ops::Deref for PhoneNumber {
    type Target = Secret<String, PhoneNumberStrategy>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl ops::DerefMut for PhoneNumber {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl<DB> Queryable<sql_types::Text, DB> for PhoneNumber
where
    DB: Backend,
    Self: FromSql<sql_types::Text, DB>,
{
    type Row = Self;

    fn build(row: Self::Row) -> deserialize::Result<Self> {
        Ok(row)
    }
}

impl<DB> FromSql<sql_types::Text, DB> for PhoneNumber
where
    DB: Backend,
    String: FromSql<sql_types::Text, DB>,
{
    fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
        let val = String::from_sql(bytes)?;
        Ok(Self::from_str(val.as_str())?)
    }
}

impl<DB> ToSql<sql_types::Text, DB> for PhoneNumber
where
    DB: Backend,
    String: ToSql<sql_types::Text, DB>,
{
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
        self.0.to_sql(out)
    }
}

/*
/// Phone number
#[derive(Debug)]
pub struct PhoneNumber;

impl<T> Strategy<T> for PhoneNumber
where
    T: AsRef<str>,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let val_str: &str = val.as_ref();

        if val_str.len() < 10 || val_str.len() > 12 {
            return WithType::fmt(val, f);
        }

        write!(
            f,
            "{}{}{}",
            &val_str[..2],
            "*".repeat(val_str.len() - 5),
            &val_str[(val_str.len() - 3)..]
        )
    }
}
*/

/// Strategy for Encryption
#[derive(Debug)]
pub enum EncryptionStrategy {}

impl<T> Strategy<T> for EncryptionStrategy
where
    T: AsRef<[u8]>,
{
    fn fmt(value: &T, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            fmt,
            "*** Encrypted data of length {} bytes ***",
            value.as_ref().len()
        )
    }
}

/// Client secret
#[derive(Debug)]
pub enum ClientSecret {}

impl<T> Strategy<T> for ClientSecret
where
    T: AsRef<str>,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let val_str: &str = val.as_ref();

        let client_secret_segments: Vec<&str> = val_str.split('_').collect();

        if client_secret_segments.len() != 4
            || !client_secret_segments.contains(&"pay")
            || !client_secret_segments.contains(&"secret")
        {
            return WithType::fmt(val, f);
        }

        if let Some((client_secret_segments_0, client_secret_segments_1)) = client_secret_segments
            .first()
            .zip(client_secret_segments.get(1))
        {
            write!(
                f,
                "{}_{}_{}",
                client_secret_segments_0,
                client_secret_segments_1,
                "*".repeat(
                    val_str.len()
                        - (client_secret_segments_0.len() + client_secret_segments_1.len() + 2)
                )
            )
        } else {
            #[cfg(feature = "logs")]
            logger::error!("Invalid client secret: {val_str}");
            WithType::fmt(val, f)
        }
    }
}

/// Strategy for masking Email
#[derive(Debug, Copy, Clone, Deserialize)]
pub enum EmailStrategy {}

impl<T> Strategy<T> for EmailStrategy
where
    T: AsRef<str> + fmt::Debug,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let val_str: &str = val.as_ref();
        match val_str.split_once('@') {
            Some((a, b)) => write!(f, "{}@{}", "*".repeat(a.len()), b),
            None => WithType::fmt(val, f),
        }
    }
}
/// Email address
#[derive(
    serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Default, AsExpression,
)]
#[diesel(sql_type = sql_types::Text)]
#[serde(try_from = "String")]
pub struct Email(Secret<String, EmailStrategy>);

impl From<Encryptable<Secret<String, EmailStrategy>>> for Email {
    fn from(item: Encryptable<Secret<String, EmailStrategy>>) -> Self {
        Self(item.into_inner())
    }
}

impl ExposeInterface<Secret<String, EmailStrategy>> for Email {
    fn expose(self) -> Secret<String, EmailStrategy> {
        self.0
    }
}

impl TryFrom<String> for Email {
    type Error = error_stack::Report<errors::ParsingError>;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::from_str(&value).change_context(errors::ParsingError::EmailParsingError)
    }
}

impl ops::Deref for Email {
    type Target = Secret<String, EmailStrategy>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl ops::DerefMut for Email {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl<DB> Queryable<sql_types::Text, DB> for Email
where
    DB: Backend,
    Self: FromSql<sql_types::Text, DB>,
{
    type Row = Self;

    fn build(row: Self::Row) -> deserialize::Result<Self> {
        Ok(row)
    }
}

impl<DB> FromSql<sql_types::Text, DB> for Email
where
    DB: Backend,
    String: FromSql<sql_types::Text, DB>,
{
    fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
        let val = String::from_sql(bytes)?;
        Ok(Self::from_str(val.as_str())?)
    }
}

impl<DB> ToSql<sql_types::Text, DB> for Email
where
    DB: Backend,
    String: ToSql<sql_types::Text, DB>,
{
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
        self.0.to_sql(out)
    }
}

impl FromStr for Email {
    type Err = error_stack::Report<ValidationError>;
    fn from_str(email: &str) -> Result<Self, Self::Err> {
        if email.eq(REDACTED) {
            return Ok(Self(Secret::new(email.to_string())));
        }
        match validate_email(email) {
            Ok(_) => {
                let secret = Secret::<String, EmailStrategy>::new(email.to_string());
                Ok(Self(secret))
            }
            Err(_) => Err(ValidationError::InvalidValue {
                message: "Invalid email address format".into(),
            }
            .into()),
        }
    }
}

/// IP address
#[derive(Debug)]
pub enum IpAddress {}

impl<T> Strategy<T> for IpAddress
where
    T: AsRef<str>,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let val_str: &str = val.as_ref();
        let segments: Vec<&str> = val_str.split('.').collect();

        if segments.len() != 4 {
            return WithType::fmt(val, f);
        }

        for seg in segments.iter() {
            if seg.is_empty() || seg.len() > 3 {
                return WithType::fmt(val, f);
            }
        }

        if let Some(segments) = segments.first() {
            write!(f, "{segments}.**.**.**")
        } else {
            #[cfg(feature = "logs")]
            logger::error!("Invalid IP address: {val_str}");
            WithType::fmt(val, f)
        }
    }
}

/// Strategy for masking UPI VPA's
#[derive(Debug)]
pub enum UpiVpaMaskingStrategy {}

impl<T> Strategy<T> for UpiVpaMaskingStrategy
where
    T: AsRef<str> + fmt::Debug,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let vpa_str: &str = val.as_ref();
        if let Some((user_identifier, bank_or_psp)) = vpa_str.split_once('@') {
            let masked_user_identifier = "*".repeat(user_identifier.len());
            write!(f, "{masked_user_identifier}@{bank_or_psp}")
        } else {
            WithType::fmt(val, f)
        }
    }
}

#[cfg(test)]
mod pii_masking_strategy_tests {
    use std::str::FromStr;

    use masking::{ExposeInterface, Secret};

    use super::{ClientSecret, Email, IpAddress, UpiVpaMaskingStrategy};
    use crate::pii::{EmailStrategy, REDACTED};

    /*
    #[test]
    fn test_valid_phone_number_masking() {
        let secret: Secret<String, PhoneNumber> = Secret::new("9123456789".to_string());
        assert_eq!("99*****299", format!("{}", secret));
    }

    #[test]
    fn test_invalid_phone_number_masking() {
        let secret: Secret<String, PhoneNumber> = Secret::new("99229922".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{}", secret));

        let secret: Secret<String, PhoneNumber> = Secret::new("9922992299229922".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{}", secret));
    }
    */

    #[test]
    fn test_valid_email_masking() {
        let secret: Secret<String, EmailStrategy> = Secret::new("example@test.com".to_string());
        assert_eq!("*******@test.com", format!("{secret:?}"));

        let secret: Secret<String, EmailStrategy> = Secret::new("username@gmail.com".to_string());
        assert_eq!("********@gmail.com", format!("{secret:?}"));
    }

    #[test]
    fn test_invalid_email_masking() {
        let secret: Secret<String, EmailStrategy> = Secret::new("myemailgmail.com".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));

        let secret: Secret<String, EmailStrategy> = Secret::new("myemail$gmail.com".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
    }

    #[test]
    fn test_valid_newtype_email() {
        let email_check = Email::from_str("example@abc.com");
        assert!(email_check.is_ok());
    }

    #[test]
    fn test_invalid_newtype_email() {
        let email_check = Email::from_str("example@abc@com");
        assert!(email_check.is_err());
    }

    #[test]
    fn test_redacted_email() {
        let email_result = Email::from_str(REDACTED);
        assert!(email_result.is_ok());
        if let Ok(email) = email_result {
            let secret_value = email.0.expose();
            assert_eq!(secret_value.as_str(), REDACTED);
        }
    }

    #[test]
    fn test_valid_ip_addr_masking() {
        let secret: Secret<String, IpAddress> = Secret::new("123.23.1.78".to_string());
        assert_eq!("123.**.**.**", format!("{secret:?}"));
    }

    #[test]
    fn test_invalid_ip_addr_masking() {
        let secret: Secret<String, IpAddress> = Secret::new("123.4.56".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));

        let secret: Secret<String, IpAddress> = Secret::new("123.4567.12.4".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));

        let secret: Secret<String, IpAddress> = Secret::new("123..4.56".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
    }

    #[test]
    fn test_valid_client_secret_masking() {
        let secret: Secret<String, ClientSecret> =
            Secret::new("pay_uszFB2QGe9MmLY65ojhT_secret_tLjTz9tAQxUVEFqfmOIP".to_string());
        assert_eq!(
            "pay_uszFB2QGe9MmLY65ojhT_***************************",
            format!("{secret:?}")
        );
    }

    #[test]
    fn test_invalid_client_secret_masking() {
        let secret: Secret<String, IpAddress> =
            Secret::new("pay_uszFB2QGe9MmLY65ojhT_secret".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
    }

    #[test]
    fn test_valid_phone_number_default_masking() {
        let secret: Secret<String> = Secret::new("+40712345678".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
    }

    #[test]
    fn test_valid_upi_vpa_masking() {
        let secret: Secret<String, UpiVpaMaskingStrategy> = Secret::new("my_name@upi".to_string());
        assert_eq!("*******@upi", format!("{secret:?}"));
    }

    #[test]
    fn test_invalid_upi_vpa_masking() {
        let secret: Secret<String, UpiVpaMaskingStrategy> = Secret::new("my_name_upi".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
    }
}

Tests: crates/common_utils/src/pii.rs
================================================================================
//! Personal Identifiable Information protection.

use std::{convert::AsRef, fmt, ops, str::FromStr};

use diesel::{
    backend::Backend,
    deserialize,
    deserialize::FromSql,
    prelude::*,
    serialize::{Output, ToSql},
    sql_types, AsExpression,
};
use error_stack::ResultExt;
use masking::{ExposeInterface, Secret, Strategy, WithType};
#[cfg(feature = "logs")]
use router_env::logger;
use serde::Deserialize;

use crate::{
    crypto::Encryptable,
    errors::{self, ValidationError},
    validation::{validate_email, validate_phone_number},
};

/// A string constant representing a redacted or masked value.
pub const REDACTED: &str = "Redacted";

/// Type alias for serde_json value which has Secret Information
pub type SecretSerdeValue = Secret<serde_json::Value>;

/// Strategy for masking a PhoneNumber
#[derive(Debug)]
pub enum PhoneNumberStrategy {}

/// Phone Number
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(try_from = "String")]
pub struct PhoneNumber(Secret<String, PhoneNumberStrategy>);

impl<T> Strategy<T> for PhoneNumberStrategy
where
    T: AsRef<str> + fmt::Debug,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let val_str: &str = val.as_ref();

        if let Some(val_str) = val_str.get(val_str.len() - 4..) {
            // masks everything but the last 4 digits
            write!(f, "{}{}", "*".repeat(val_str.len() - 4), val_str)
        } else {
            #[cfg(feature = "logs")]
            logger::error!("Invalid phone number: {val_str}");
            WithType::fmt(val, f)
        }
    }
}

impl FromStr for PhoneNumber {
    type Err = error_stack::Report<ValidationError>;
    fn from_str(phone_number: &str) -> Result<Self, Self::Err> {
        validate_phone_number(phone_number)?;
        let secret = Secret::<String, PhoneNumberStrategy>::new(phone_number.to_string());
        Ok(Self(secret))
    }
}

impl TryFrom<String> for PhoneNumber {
    type Error = error_stack::Report<errors::ParsingError>;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::from_str(&value).change_context(errors::ParsingError::PhoneNumberParsingError)
    }
}

impl ops::Deref for PhoneNumber {
    type Target = Secret<String, PhoneNumberStrategy>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl ops::DerefMut for PhoneNumber {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl<DB> Queryable<sql_types::Text, DB> for PhoneNumber
where
    DB: Backend,
    Self: FromSql<sql_types::Text, DB>,
{
    type Row = Self;

    fn build(row: Self::Row) -> deserialize::Result<Self> {
        Ok(row)
    }
}

impl<DB> FromSql<sql_types::Text, DB> for PhoneNumber
where
    DB: Backend,
    String: FromSql<sql_types::Text, DB>,
{
    fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
        let val = String::from_sql(bytes)?;
        Ok(Self::from_str(val.as_str())?)
    }
}

impl<DB> ToSql<sql_types::Text, DB> for PhoneNumber
where
    DB: Backend,
    String: ToSql<sql_types::Text, DB>,
{
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
        self.0.to_sql(out)
    }
}

/*
/// Phone number
#[derive(Debug)]
pub struct PhoneNumber;

impl<T> Strategy<T> for PhoneNumber
where
    T: AsRef<str>,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let val_str: &str = val.as_ref();

        if val_str.len() < 10 || val_str.len() > 12 {
            return WithType::fmt(val, f);
        }

        write!(
            f,
            "{}{}{}",
            &val_str[..2],
            "*".repeat(val_str.len() - 5),
            &val_str[(val_str.len() - 3)..]
        )
    }
}
*/

/// Strategy for Encryption
#[derive(Debug)]
pub enum EncryptionStrategy {}

impl<T> Strategy<T> for EncryptionStrategy
where
    T: AsRef<[u8]>,
{
    fn fmt(value: &T, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            fmt,
            "*** Encrypted data of length {} bytes ***",
            value.as_ref().len()
        )
    }
}

/// Client secret
#[derive(Debug)]
pub enum ClientSecret {}

impl<T> Strategy<T> for ClientSecret
where
    T: AsRef<str>,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let val_str: &str = val.as_ref();

        let client_secret_segments: Vec<&str> = val_str.split('_').collect();

        if client_secret_segments.len() != 4
            || !client_secret_segments.contains(&"pay")
            || !client_secret_segments.contains(&"secret")
        {
            return WithType::fmt(val, f);
        }

        if let Some((client_secret_segments_0, client_secret_segments_1)) = client_secret_segments
            .first()
            .zip(client_secret_segments.get(1))
        {
            write!(
                f,
                "{}_{}_{}",
                client_secret_segments_0,
                client_secret_segments_1,
                "*".repeat(
                    val_str.len()
                        - (client_secret_segments_0.len() + client_secret_segments_1.len() + 2)
                )
            )
        } else {
            #[cfg(feature = "logs")]
            logger::error!("Invalid client secret: {val_str}");
            WithType::fmt(val, f)
        }
    }
}

/// Strategy for masking Email
#[derive(Debug, Copy, Clone, Deserialize)]
pub enum EmailStrategy {}

impl<T> Strategy<T> for EmailStrategy
where
    T: AsRef<str> + fmt::Debug,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let val_str: &str = val.as_ref();
        match val_str.split_once('@') {
            Some((a, b)) => write!(f, "{}@{}", "*".repeat(a.len()), b),
            None => WithType::fmt(val, f),
        }
    }
}
/// Email address
#[derive(
    serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Default, AsExpression,
)]
#[diesel(sql_type = sql_types::Text)]
#[serde(try_from = "String")]
pub struct Email(Secret<String, EmailStrategy>);

impl From<Encryptable<Secret<String, EmailStrategy>>> for Email {
    fn from(item: Encryptable<Secret<String, EmailStrategy>>) -> Self {
        Self(item.into_inner())
    }
}

impl ExposeInterface<Secret<String, EmailStrategy>> for Email {
    fn expose(self) -> Secret<String, EmailStrategy> {
        self.0
    }
}

impl TryFrom<String> for Email {
    type Error = error_stack::Report<errors::ParsingError>;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Self::from_str(&value).change_context(errors::ParsingError::EmailParsingError)
    }
}

impl ops::Deref for Email {
    type Target = Secret<String, EmailStrategy>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl ops::DerefMut for Email {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl<DB> Queryable<sql_types::Text, DB> for Email
where
    DB: Backend,
    Self: FromSql<sql_types::Text, DB>,
{
    type Row = Self;

    fn build(row: Self::Row) -> deserialize::Result<Self> {
        Ok(row)
    }
}

impl<DB> FromSql<sql_types::Text, DB> for Email
where
    DB: Backend,
    String: FromSql<sql_types::Text, DB>,
{
    fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
        let val = String::from_sql(bytes)?;
        Ok(Self::from_str(val.as_str())?)
    }
}

impl<DB> ToSql<sql_types::Text, DB> for Email
where
    DB: Backend,
    String: ToSql<sql_types::Text, DB>,
{
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
        self.0.to_sql(out)
    }
}

impl FromStr for Email {
    type Err = error_stack::Report<ValidationError>;
    fn from_str(email: &str) -> Result<Self, Self::Err> {
        if email.eq(REDACTED) {
            return Ok(Self(Secret::new(email.to_string())));
        }
        match validate_email(email) {
            Ok(_) => {
                let secret = Secret::<String, EmailStrategy>::new(email.to_string());
                Ok(Self(secret))
            }
            Err(_) => Err(ValidationError::InvalidValue {
                message: "Invalid email address format".into(),
            }
            .into()),
        }
    }
}

/// IP address
#[derive(Debug)]
pub enum IpAddress {}

impl<T> Strategy<T> for IpAddress
where
    T: AsRef<str>,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let val_str: &str = val.as_ref();
        let segments: Vec<&str> = val_str.split('.').collect();

        if segments.len() != 4 {
            return WithType::fmt(val, f);
        }

        for seg in segments.iter() {
            if seg.is_empty() || seg.len() > 3 {
                return WithType::fmt(val, f);
            }
        }

        if let Some(segments) = segments.first() {
            write!(f, "{segments}.**.**.**")
        } else {
            #[cfg(feature = "logs")]
            logger::error!("Invalid IP address: {val_str}");
            WithType::fmt(val, f)
        }
    }
}

/// Strategy for masking UPI VPA's
#[derive(Debug)]
pub enum UpiVpaMaskingStrategy {}

impl<T> Strategy<T> for UpiVpaMaskingStrategy
where
    T: AsRef<str> + fmt::Debug,
{
    fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let vpa_str: &str = val.as_ref();
        if let Some((user_identifier, bank_or_psp)) = vpa_str.split_once('@') {
            let masked_user_identifier = "*".repeat(user_identifier.len());
            write!(f, "{masked_user_identifier}@{bank_or_psp}")
        } else {
            WithType::fmt(val, f)
        }
    }
}

#[cfg(test)]
mod pii_masking_strategy_tests {
    use std::str::FromStr;

    use masking::{ExposeInterface, Secret};

    use super::{ClientSecret, Email, IpAddress, UpiVpaMaskingStrategy};
    use crate::pii::{EmailStrategy, REDACTED};

    /*
    #[test]
    fn test_valid_phone_number_masking() {
        let secret: Secret<String, PhoneNumber> = Secret::new("9123456789".to_string());
        assert_eq!("99*****299", format!("{}", secret));
    }

    #[test]
    fn test_invalid_phone_number_masking() {
        let secret: Secret<String, PhoneNumber> = Secret::new("99229922".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{}", secret));

        let secret: Secret<String, PhoneNumber> = Secret::new("9922992299229922".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{}", secret));
    }
    */

    #[test]
    fn test_valid_email_masking() {
        let secret: Secret<String, EmailStrategy> = Secret::new("example@test.com".to_string());
        assert_eq!("*******@test.com", format!("{secret:?}"));

        let secret: Secret<String, EmailStrategy> = Secret::new("username@gmail.com".to_string());
        assert_eq!("********@gmail.com", format!("{secret:?}"));
    }

    #[test]
    fn test_invalid_email_masking() {
        let secret: Secret<String, EmailStrategy> = Secret::new("myemailgmail.com".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));

        let secret: Secret<String, EmailStrategy> = Secret::new("myemail$gmail.com".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
    }

    #[test]
    fn test_valid_newtype_email() {
        let email_check = Email::from_str("example@abc.com");
        assert!(email_check.is_ok());
    }

    #[test]
    fn test_invalid_newtype_email() {
        let email_check = Email::from_str("example@abc@com");
        assert!(email_check.is_err());
    }

    #[test]
    fn test_redacted_email() {
        let email_result = Email::from_str(REDACTED);
        assert!(email_result.is_ok());
        if let Ok(email) = email_result {
            let secret_value = email.0.expose();
            assert_eq!(secret_value.as_str(), REDACTED);
        }
    }

    #[test]
    fn test_valid_ip_addr_masking() {
        let secret: Secret<String, IpAddress> = Secret::new("123.23.1.78".to_string());
        assert_eq!("123.**.**.**", format!("{secret:?}"));
    }

    #[test]
    fn test_invalid_ip_addr_masking() {
        let secret: Secret<String, IpAddress> = Secret::new("123.4.56".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));

        let secret: Secret<String, IpAddress> = Secret::new("123.4567.12.4".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));

        let secret: Secret<String, IpAddress> = Secret::new("123..4.56".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
    }

    #[test]
    fn test_valid_client_secret_masking() {
        let secret: Secret<String, ClientSecret> =
            Secret::new("pay_uszFB2QGe9MmLY65ojhT_secret_tLjTz9tAQxUVEFqfmOIP".to_string());
        assert_eq!(
            "pay_uszFB2QGe9MmLY65ojhT_***************************",
            format!("{secret:?}")
        );
    }

    #[test]
    fn test_invalid_client_secret_masking() {
        let secret: Secret<String, IpAddress> =
            Secret::new("pay_uszFB2QGe9MmLY65ojhT_secret".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
    }

    #[test]
    fn test_valid_phone_number_default_masking() {
        let secret: Secret<String> = Secret::new("+40712345678".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
    }

    #[test]
    fn test_valid_upi_vpa_masking() {
        let secret: Secret<String, UpiVpaMaskingStrategy> = Secret::new("my_name@upi".to_string());
        assert_eq!("*******@upi", format!("{secret:?}"));
    }

    #[test]
    fn test_invalid_upi_vpa_masking() {
        let secret: Secret<String, UpiVpaMaskingStrategy> = Secret::new("my_name_upi".to_string());
        assert_eq!("*** alloc::string::String ***", format!("{secret:?}"));
    }
}