diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 28c0cf7441..1f4353f25d 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -107,9 +107,18 @@ pub const TENANT_HEADER: &str = "x-tenant-id"; /// Max Length for MerchantReferenceId pub const MAX_ALLOWED_MERCHANT_REFERENCE_ID_LENGTH: u8 = 64; +/// Maximum length allowed for a global id +pub const MIN_GLOBAL_ID_LENGTH: u8 = 32; + +/// Minimum length required for a global id +pub const MAX_GLOBAL_ID_LENGTH: u8 = 64; + /// Minimum allowed length for MerchantReferenceId pub const MIN_REQUIRED_MERCHANT_REFERENCE_ID_LENGTH: u8 = 1; +/// Length of a cell identifier in a distributed system +pub const CELL_IDENTIFIER_LENGTH: u8 = 5; + /// General purpose base64 engine pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD; /// Regex for matching a domain diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index 63f8497cae..55e2e3ffb1 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -7,6 +7,8 @@ mod customer; mod merchant; mod organization; +mod global_id; + pub use customer::CustomerId; use diesel::{ backend::Backend, @@ -87,22 +89,22 @@ impl AlphaNumericId { pub(crate) struct LengthId(AlphaNumericId); /// Error generated from violation of constraints for MerchantReferenceId -#[derive(Debug, Deserialize, Serialize, Error, PartialEq, Eq)] -pub(crate) enum LengthIdError { - #[error("the maximum allowed length for this field is {MAX_LENGTH}")] +#[derive(Debug, Error, PartialEq, Eq)] +pub(crate) enum LengthIdError { + #[error("the maximum allowed length for this field is {0}")] /// Maximum length of string violated - MaxLengthViolated, + MaxLengthViolated(u8), - #[error("the minimum required length for this field is {MIN_LENGTH}")] + #[error("the minimum required length for this field is {0}")] /// Minimum length of string violated - MinLengthViolated, + MinLengthViolated(u8), #[error("{0}")] /// Input contains invalid characters AlphanumericIdError(AlphaNumericIdError), } -impl From for LengthIdError<0, 0> { +impl From for LengthIdError { fn from(alphanumeric_id_error: AlphaNumericIdError) -> Self { Self::AlphanumericIdError(alphanumeric_id_error) } @@ -110,19 +112,17 @@ impl From for LengthIdError<0, 0> { impl LengthId { /// Generates new [MerchantReferenceId] from the given input string - pub fn from( - input_string: Cow<'static, str>, - ) -> Result> { + pub fn from(input_string: Cow<'static, str>) -> Result { let trimmed_input_string = input_string.trim().to_string(); let length_of_input_string = u8::try_from(trimmed_input_string.len()) - .map_err(|_| LengthIdError::MaxLengthViolated)?; + .map_err(|_| LengthIdError::MaxLengthViolated(MAX_LENGTH))?; when(length_of_input_string > MAX_LENGTH, || { - Err(LengthIdError::MaxLengthViolated) + Err(LengthIdError::MaxLengthViolated(MAX_LENGTH)) })?; when(length_of_input_string < MIN_LENGTH, || { - Err(LengthIdError::MinLengthViolated) + Err(LengthIdError::MinLengthViolated(MIN_LENGTH)) })?; let alphanumeric_id = match AlphaNumericId::from(trimmed_input_string.into()) { @@ -142,6 +142,25 @@ impl LengthId Self { Self(alphanumeric_id) } + + /// Create a new LengthId from aplhanumeric id + pub(crate) fn from_alphanumeric_id( + alphanumeric_id: AlphaNumericId, + ) -> Result { + let length_of_input_string = alphanumeric_id.0.len(); + let length_of_input_string = u8::try_from(length_of_input_string) + .map_err(|_| LengthIdError::MaxLengthViolated(MAX_LENGTH))?; + + when(length_of_input_string > MAX_LENGTH, || { + Err(LengthIdError::MaxLengthViolated(MAX_LENGTH)) + })?; + + when(length_of_input_string < MIN_LENGTH, || { + Err(LengthIdError::MinLengthViolated(MIN_LENGTH)) + })?; + + Ok(Self(alphanumeric_id)) + } } impl<'de, const MAX_LENGTH: u8, const MIN_LENGTH: u8> Deserialize<'de> @@ -290,7 +309,11 @@ mod merchant_reference_id_tests { dbg!(&parsed_merchant_reference_id); - assert!(parsed_merchant_reference_id - .is_err_and(|error_type| matches!(error_type, LengthIdError::MaxLengthViolated))); + assert!( + parsed_merchant_reference_id.is_err_and(|error_type| matches!( + error_type, + LengthIdError::MaxLengthViolated(MAX_LENGTH) + )) + ); } } diff --git a/crates/common_utils/src/id_type/global_id.rs b/crates/common_utils/src/id_type/global_id.rs new file mode 100644 index 0000000000..dcfb9998d3 --- /dev/null +++ b/crates/common_utils/src/id_type/global_id.rs @@ -0,0 +1,218 @@ +#![allow(unused)] + +use diesel::{backend::Backend, deserialize::FromSql, serialize::ToSql, sql_types}; +use error_stack::ResultExt; +use serde_json::error; +use thiserror::Error; + +use crate::{ + consts::{CELL_IDENTIFIER_LENGTH, MAX_GLOBAL_ID_LENGTH, MIN_GLOBAL_ID_LENGTH}, + errors, generate_time_ordered_id, + id_type::{AlphaNumericId, AlphaNumericIdError, LengthId, LengthIdError}, +}; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +/// A global id that can be used to identify any entity +/// This id will have information about the entity and cell in a distributed system architecture +pub(crate) struct GlobalId(LengthId); + +#[derive(Clone, Copy)] +/// Entities that can be identified by a global id +pub(crate) enum GlobalEntity { + Customer, + Payment, +} + +impl GlobalEntity { + fn prefix(&self) -> &'static str { + match self { + Self::Customer => "cus", + Self::Payment => "pay", + } + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(crate) struct CellId(LengthId); + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum CellIdError { + #[error("cell id error: {0}")] + InvalidCellLength(LengthIdError), + + #[error("{0}")] + InvalidCellIdFormat(AlphaNumericIdError), +} + +impl From for CellIdError { + fn from(error: LengthIdError) -> Self { + Self::InvalidCellLength(error) + } +} + +impl From for CellIdError { + fn from(error: AlphaNumericIdError) -> Self { + Self::InvalidCellIdFormat(error) + } +} + +impl CellId { + /// Create a new cell id from a string + fn from_str(cell_id_string: &str) -> Result { + let trimmed_input_string = cell_id_string.trim().to_string(); + let alphanumeric_id = AlphaNumericId::from(trimmed_input_string.into())?; + let length_id = LengthId::from_alphanumeric_id(alphanumeric_id)?; + Ok(Self(length_id)) + } + + pub fn from_string(input_string: String) -> error_stack::Result { + Self::from_str(&input_string).change_context( + errors::ValidationError::IncorrectValueProvided { + field_name: "cell_id", + }, + ) + } + + /// Get the string representation of the cell id + fn get_string_repr(&self) -> &str { + &self.0 .0 .0 + } +} + +/// Error generated from violation of constraints for MerchantReferenceId +#[derive(Debug, Error, PartialEq, Eq)] +pub(crate) enum GlobalIdError { + /// The format for the global id is invalid + #[error("The id format is invalid, expected format is {{cell_id:5}}_{{entity_prefix:3}}_{{uuid:32}}_{{random:24}}")] + InvalidIdFormat, + + /// LengthIdError and AlphanumericIdError + #[error("{0}")] + LengthIdError(#[from] LengthIdError), + + /// CellIdError because of invalid cell id format + #[error("{0}")] + CellIdError(#[from] CellIdError), +} + +impl GlobalId { + /// Create a new global id from entity and cell information + /// The entity prefix is used to identify the entity, `cus` for customers, `pay`` for payments etc. + pub fn generate(cell_id: CellId, entity: GlobalEntity) -> Self { + let prefix = format!("{}_{}", cell_id.get_string_repr(), entity.prefix()); + let id = generate_time_ordered_id(&prefix); + let alphanumeric_id = AlphaNumericId::new_unchecked(id); + Self(LengthId::new_unchecked(alphanumeric_id)) + } + + pub fn from_string(input_string: String) -> Result { + let length_id = LengthId::from(input_string.into())?; + let input_string = &length_id.0 .0; + let (cell_id, remaining) = input_string + .split_once("_") + .ok_or(GlobalIdError::InvalidIdFormat)?; + + CellId::from_str(cell_id)?; + + Ok(Self(length_id)) + } +} + +impl ToSql for GlobalId +where + DB: Backend, + String: ToSql, +{ + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, DB>, + ) -> diesel::serialize::Result { + self.0 .0 .0.to_sql(out) + } +} + +impl FromSql for GlobalId +where + DB: Backend, + String: FromSql, +{ + fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { + let string_val = String::from_sql(value)?; + let alphanumeric_id = AlphaNumericId::from(string_val.into())?; + let length_id = LengthId::from_alphanumeric_id(alphanumeric_id)?; + Ok(Self(length_id)) + } +} + +/// Deserialize the global id from string +/// The format should match {cell_id:5}_{entity_prefix:3}_{time_ordered_id:32}_{.*:24} +impl<'de> serde::Deserialize<'de> for GlobalId { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let deserialized_string = String::deserialize(deserializer)?; + Self::from_string(deserialized_string).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod global_id_tests { + #![allow(clippy::unwrap_used)] + use super::*; + + #[test] + fn test_cell_id_from_str() { + let cell_id_string = "12345"; + let cell_id = CellId::from_str(cell_id_string).unwrap(); + assert_eq!(cell_id.get_string_repr(), cell_id_string); + } + + #[test] + fn test_global_id_generate() { + let cell_id_string = "12345"; + let entity = GlobalEntity::Customer; + let cell_id = CellId::from_str(cell_id_string).unwrap(); + let global_id = GlobalId::generate(cell_id, entity); + + /// Generate a regex for globalid + /// Eg - 12abc_cus_abcdefghijklmnopqrstuvwxyz1234567890 + let regex = regex::Regex::new(r"[a-z0-9]{5}_cus_[a-z0-9]{32}").unwrap(); + + assert!(regex.is_match(&global_id.0 .0 .0)); + } + + #[test] + fn test_global_id_from_string() { + let input_string = "12345_cus_abcdefghijklmnopqrstuvwxyz1234567890"; + let global_id = GlobalId::from_string(input_string.to_string()).unwrap(); + assert_eq!(global_id.0 .0 .0, input_string); + } + + #[test] + fn test_global_id_deser() { + let input_string_for_serde_json_conversion = + r#""12345_cus_abcdefghijklmnopqrstuvwxyz1234567890""#; + + let input_string = "12345_cus_abcdefghijklmnopqrstuvwxyz1234567890"; + let global_id = + serde_json::from_str::(input_string_for_serde_json_conversion).unwrap(); + assert_eq!(global_id.0 .0 .0, input_string); + } + + #[test] + fn test_global_id_deser_error() { + let input_string_for_serde_json_conversion = + r#""123_45_cus_abcdefghijklmnopqrstuvwxyz1234567890""#; + + let global_id = serde_json::from_str::(input_string_for_serde_json_conversion); + assert!(global_id.is_err()); + + let expected_error_message = format!( + "cell id error: the minimum required length for this field is {CELL_IDENTIFIER_LENGTH}" + ); + + let error_message = global_id.unwrap_err().to_string(); + assert_eq!(error_message, expected_error_message); + } +}