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:
Narayan Bhat
2024-08-22 17:23:08 +05:30
committed by GitHub
parent bda29cb1b5
commit d14c7887e9
3 changed files with 265 additions and 15 deletions

View File

@ -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

View File

@ -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)
))
);
}
}

View 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);
}
}