mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat(global_id): create a GlobalId domain type (#5644)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -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
|
||||
|
||||
@ -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<const MAX_LENGTH: u8, const MIN_LENGTH: u8>(AlphaNumericId);
|
||||
|
||||
/// Error generated from violation of constraints for MerchantReferenceId
|
||||
#[derive(Debug, Deserialize, Serialize, Error, PartialEq, Eq)]
|
||||
pub(crate) enum LengthIdError<const MAX_LENGTH: u8, const MIN_LENGTH: u8> {
|
||||
#[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<AlphaNumericIdError> for LengthIdError<0, 0> {
|
||||
impl From<AlphaNumericIdError> for LengthIdError {
|
||||
fn from(alphanumeric_id_error: AlphaNumericIdError) -> Self {
|
||||
Self::AlphanumericIdError(alphanumeric_id_error)
|
||||
}
|
||||
@ -110,19 +112,17 @@ impl From<AlphaNumericIdError> for LengthIdError<0, 0> {
|
||||
|
||||
impl<const MAX_LENGTH: u8, const MIN_LENGTH: u8> LengthId<MAX_LENGTH, MIN_LENGTH> {
|
||||
/// Generates new [MerchantReferenceId] from the given input string
|
||||
pub fn from(
|
||||
input_string: Cow<'static, str>,
|
||||
) -> Result<Self, LengthIdError<MAX_LENGTH, MIN_LENGTH>> {
|
||||
pub fn from(input_string: Cow<'static, str>) -> Result<Self, LengthIdError> {
|
||||
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<const MAX_LENGTH: u8, const MIN_LENGTH: u8> LengthId<MAX_LENGTH, MIN_LENGTH
|
||||
pub(crate) fn new_unchecked(alphanumeric_id: AlphaNumericId) -> Self {
|
||||
Self(alphanumeric_id)
|
||||
}
|
||||
|
||||
/// Create a new LengthId from aplhanumeric id
|
||||
pub(crate) fn from_alphanumeric_id(
|
||||
alphanumeric_id: AlphaNumericId,
|
||||
) -> Result<Self, LengthIdError> {
|
||||
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)
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
218
crates/common_utils/src/id_type/global_id.rs
Normal file
218
crates/common_utils/src/id_type/global_id.rs
Normal file
@ -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<MAX_GLOBAL_ID_LENGTH, MIN_GLOBAL_ID_LENGTH>);
|
||||
|
||||
#[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<CELL_IDENTIFIER_LENGTH, CELL_IDENTIFIER_LENGTH>);
|
||||
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum CellIdError {
|
||||
#[error("cell id error: {0}")]
|
||||
InvalidCellLength(LengthIdError),
|
||||
|
||||
#[error("{0}")]
|
||||
InvalidCellIdFormat(AlphaNumericIdError),
|
||||
}
|
||||
|
||||
impl From<LengthIdError> for CellIdError {
|
||||
fn from(error: LengthIdError) -> Self {
|
||||
Self::InvalidCellLength(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AlphaNumericIdError> 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<Self, CellIdError> {
|
||||
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, errors::ValidationError> {
|
||||
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<Self, GlobalIdError> {
|
||||
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<DB> ToSql<sql_types::Text, DB> for GlobalId
|
||||
where
|
||||
DB: Backend,
|
||||
String: ToSql<sql_types::Text, DB>,
|
||||
{
|
||||
fn to_sql<'b>(
|
||||
&'b self,
|
||||
out: &mut diesel::serialize::Output<'b, '_, DB>,
|
||||
) -> diesel::serialize::Result {
|
||||
self.0 .0 .0.to_sql(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DB> FromSql<sql_types::Text, DB> for GlobalId
|
||||
where
|
||||
DB: Backend,
|
||||
String: FromSql<sql_types::Text, DB>,
|
||||
{
|
||||
fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {
|
||||
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
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::<GlobalId>(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::<GlobalId>(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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user