#[macro_export]
macro_rules! impl_entity_multi_tenant {
($type:ident) => {
impl $type {
#[allow(dead_code)]
pub fn get_tenant_id(&self) -> ::uuid::Uuid {
self.tenant_id
}
}
};
}
#[macro_export]
macro_rules! entity_fields {
() => {
pub id: ::uuid::Uuid,
#[serde(rename = "type")]
pub entity_type: String,
pub created_at: ::chrono::DateTime<::chrono::Utc>,
pub updated_at: ::chrono::DateTime<::chrono::Utc>,
pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
pub status: String,
};
}
#[macro_export]
macro_rules! data_fields {
() => {
pub id: ::uuid::Uuid,
#[serde(rename = "type")]
pub entity_type: String,
pub created_at: ::chrono::DateTime<::chrono::Utc>,
pub updated_at: ::chrono::DateTime<::chrono::Utc>,
pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
pub status: String,
pub name: String,
};
}
#[macro_export]
macro_rules! link_fields {
() => {
pub id: ::uuid::Uuid,
#[serde(rename = "type")]
pub entity_type: String,
pub created_at: ::chrono::DateTime<::chrono::Utc>,
pub updated_at: ::chrono::DateTime<::chrono::Utc>,
pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
pub status: String,
pub link_type: String,
pub source_id: ::uuid::Uuid,
pub target_id: ::uuid::Uuid,
};
}
#[macro_export]
macro_rules! impl_data_entity {
(
$type:ident,
$type_name:expr,
[ $( $indexed_field:expr ),* $(,)? ],
{
$( $specific_field:ident : $specific_type:ty ),* $(,)?
}
) => {
#[derive(Debug, Clone, ::serde::Serialize, ::serde::Deserialize)]
pub struct $type {
/// Unique identifier for this entity
pub id: ::uuid::Uuid,
#[serde(rename = "type")]
pub entity_type: String,
pub created_at: ::chrono::DateTime<::chrono::Utc>,
pub updated_at: ::chrono::DateTime<::chrono::Utc>,
pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
pub status: String,
pub name: String,
$( pub $specific_field : $specific_type ),*
}
impl $crate::core::entity::Entity for $type {
type Service = ();
fn resource_name() -> &'static str {
use std::sync::OnceLock;
static PLURAL: OnceLock<&'static str> = OnceLock::new();
PLURAL.get_or_init(|| {
Box::leak(
$crate::core::pluralize::Pluralizer::pluralize($type_name)
.into_boxed_str()
)
})
}
fn resource_name_singular() -> &'static str {
$type_name
}
fn service_from_host(
_host: &::std::sync::Arc<dyn ::std::any::Any + Send + Sync>
) -> ::anyhow::Result<::std::sync::Arc<Self::Service>> {
unimplemented!("service_from_host must be implemented by user")
}
fn id(&self) -> ::uuid::Uuid {
self.id
}
fn entity_type(&self) -> &str {
&self.entity_type
}
fn created_at(&self) -> ::chrono::DateTime<::chrono::Utc> {
self.created_at
}
fn updated_at(&self) -> ::chrono::DateTime<::chrono::Utc> {
self.updated_at
}
fn deleted_at(&self) -> Option<::chrono::DateTime<::chrono::Utc>> {
self.deleted_at
}
fn status(&self) -> &str {
&self.status
}
}
impl $crate::core::entity::Data for $type {
fn name(&self) -> &str {
&self.name
}
fn indexed_fields() -> &'static [&'static str] {
&[ $( $indexed_field ),* ]
}
fn field_value(&self, field: &str) -> Option<$crate::core::field::FieldValue> {
match field {
"name" => Some($crate::core::field::FieldValue::String(self.name.clone())),
"status" => Some($crate::core::field::FieldValue::String(self.status.clone())),
_ => None,
}
}
}
impl $type {
pub fn new(
name: String,
status: String,
$( $specific_field: $specific_type ),*
) -> Self {
Self {
id: ::uuid::Uuid::new_v4(),
entity_type: $type_name.to_string(),
created_at: ::chrono::Utc::now(),
updated_at: ::chrono::Utc::now(),
deleted_at: None,
status,
name,
$( $specific_field ),*
}
}
pub fn soft_delete(&mut self) {
self.deleted_at = Some(::chrono::Utc::now());
self.updated_at = ::chrono::Utc::now();
}
pub fn restore(&mut self) {
self.deleted_at = None;
self.updated_at = ::chrono::Utc::now();
}
pub fn touch(&mut self) {
self.updated_at = ::chrono::Utc::now();
}
pub fn set_status(&mut self, status: String) {
self.status = status;
self.touch();
}
}
};
}
#[macro_export]
macro_rules! impl_link_entity {
(
$type:ident,
$type_name:expr,
{
$( $specific_field:ident : $specific_type:ty ),* $(,)?
}
) => {
#[derive(Debug, Clone, ::serde::Serialize, ::serde::Deserialize)]
pub struct $type {
/// Unique identifier for this entity
pub id: ::uuid::Uuid,
#[serde(rename = "type")]
pub entity_type: String,
pub created_at: ::chrono::DateTime<::chrono::Utc>,
pub updated_at: ::chrono::DateTime<::chrono::Utc>,
pub deleted_at: Option<::chrono::DateTime<::chrono::Utc>>,
pub status: String,
pub link_type: String,
pub source_id: ::uuid::Uuid,
pub target_id: ::uuid::Uuid,
$( pub $specific_field : $specific_type ),*
}
impl $crate::core::entity::Entity for $type {
type Service = ();
fn resource_name() -> &'static str {
use std::sync::OnceLock;
static PLURAL: OnceLock<&'static str> = OnceLock::new();
PLURAL.get_or_init(|| {
Box::leak(
$crate::core::pluralize::Pluralizer::pluralize($type_name)
.into_boxed_str()
)
})
}
fn resource_name_singular() -> &'static str {
$type_name
}
fn service_from_host(
_host: &::std::sync::Arc<dyn ::std::any::Any + Send + Sync>
) -> ::anyhow::Result<::std::sync::Arc<Self::Service>> {
unimplemented!("service_from_host must be implemented by user")
}
fn id(&self) -> ::uuid::Uuid {
self.id
}
fn entity_type(&self) -> &str {
&self.entity_type
}
fn created_at(&self) -> ::chrono::DateTime<::chrono::Utc> {
self.created_at
}
fn updated_at(&self) -> ::chrono::DateTime<::chrono::Utc> {
self.updated_at
}
fn deleted_at(&self) -> Option<::chrono::DateTime<::chrono::Utc>> {
self.deleted_at
}
fn status(&self) -> &str {
&self.status
}
}
impl $crate::core::entity::Link for $type {
fn source_id(&self) -> ::uuid::Uuid {
self.source_id
}
fn target_id(&self) -> ::uuid::Uuid {
self.target_id
}
fn link_type(&self) -> &str {
&self.link_type
}
}
impl $type {
pub fn new(
link_type: String,
source_id: ::uuid::Uuid,
target_id: ::uuid::Uuid,
status: String,
$( $specific_field: $specific_type ),*
) -> Self {
Self {
id: ::uuid::Uuid::new_v4(),
entity_type: $type_name.to_string(),
created_at: ::chrono::Utc::now(),
updated_at: ::chrono::Utc::now(),
deleted_at: None,
status,
link_type,
source_id,
target_id,
$( $specific_field ),*
}
}
pub fn soft_delete(&mut self) {
self.deleted_at = Some(::chrono::Utc::now());
self.updated_at = ::chrono::Utc::now();
}
#[allow(dead_code)]
pub fn restore(&mut self) {
self.deleted_at = None;
self.updated_at = ::chrono::Utc::now();
}
#[allow(dead_code)]
pub fn touch(&mut self) {
self.updated_at = ::chrono::Utc::now();
}
#[allow(dead_code)]
pub fn set_status(&mut self, status: String) {
self.status = status;
self.touch();
}
}
};
}
#[macro_export]
macro_rules! impl_data_entity_validated {
(
$type:ident,
$type_name:expr,
[ $( $indexed_field:expr ),* $(,)? ],
{
$( $specific_field:ident : $specific_type:ty ),* $(,)?
}
$(,)?
validate: {
$(
$op:ident: {
$(
$val_field:ident: [ $( $validator:tt )* ]
),* $(,)?
}
),* $(,)?
}
$(,)?
filters: {
$(
$fop:ident: {
$(
$fil_field:ident: [ $( $filter:tt )* ]
),* $(,)?
}
),* $(,)?
}
$(,)?
) => {
$crate::impl_data_entity!(
$type,
$type_name,
[ $( $indexed_field ),* ],
{
$( $specific_field : $specific_type ),*
}
);
impl $crate::core::validation::extractor::ValidatableEntity for $type {
fn validation_config(operation: &str) -> $crate::core::validation::EntityValidationConfig {
use $crate::core::validation::*;
let mut config = EntityValidationConfig::new($type_name);
$(
if operation == stringify!($op) {
$(
$crate::add_validators_for_field!(config, stringify!($val_field), $( $validator )*);
)*
}
)*
$(
if operation == stringify!($fop) {
$(
$crate::add_filters_for_field!(config, stringify!($fil_field), $( $filter )*);
)*
}
)*
config
}
}
};
}
#[macro_export]
macro_rules! add_validators_for_field {
($config:expr, $field:expr,) => {};
($config:expr, $field:expr, required $( $rest:tt )*) => {
$config.add_validator($field, $crate::core::validation::validators::required());
$crate::add_validators_for_field!($config, $field, $( $rest )*);
};
($config:expr, $field:expr, optional $( $rest:tt )*) => {
$config.add_validator($field, $crate::core::validation::validators::optional());
$crate::add_validators_for_field!($config, $field, $( $rest )*);
};
($config:expr, $field:expr, positive $( $rest:tt )*) => {
$config.add_validator($field, $crate::core::validation::validators::positive());
$crate::add_validators_for_field!($config, $field, $( $rest )*);
};
($config:expr, $field:expr, string_length($min:expr, $max:expr) $( $rest:tt )*) => {
$config.add_validator($field, $crate::core::validation::validators::string_length($min, $max));
$crate::add_validators_for_field!($config, $field, $( $rest )*);
};
($config:expr, $field:expr, max_value($max:expr) $( $rest:tt )*) => {
$config.add_validator($field, $crate::core::validation::validators::max_value($max));
$crate::add_validators_for_field!($config, $field, $( $rest )*);
};
($config:expr, $field:expr, in_list($( $value:expr ),* $(,)?) $( $rest:tt )*) => {
$config.add_validator($field, $crate::core::validation::validators::in_list(vec![$( $value.to_string() ),*]));
$crate::add_validators_for_field!($config, $field, $( $rest )*);
};
($config:expr, $field:expr, date_format($format:expr) $( $rest:tt )*) => {
$config.add_validator($field, $crate::core::validation::validators::date_format($format));
$crate::add_validators_for_field!($config, $field, $( $rest )*);
};
}
#[macro_export]
macro_rules! add_filters_for_field {
($config:expr, $field:expr,) => {};
($config:expr, $field:expr, trim $( $rest:tt )*) => {
$config.add_filter($field, $crate::core::validation::filters::trim());
$crate::add_filters_for_field!($config, $field, $( $rest )*);
};
($config:expr, $field:expr, uppercase $( $rest:tt )*) => {
$config.add_filter($field, $crate::core::validation::filters::uppercase());
$crate::add_filters_for_field!($config, $field, $( $rest )*);
};
($config:expr, $field:expr, lowercase $( $rest:tt )*) => {
$config.add_filter($field, $crate::core::validation::filters::lowercase());
$crate::add_filters_for_field!($config, $field, $( $rest )*);
};
($config:expr, $field:expr, round_decimals($decimals:expr) $( $rest:tt )*) => {
$config.add_filter($field, $crate::core::validation::filters::round_decimals($decimals));
$crate::add_filters_for_field!($config, $field, $( $rest )*);
};
}
#[cfg(test)]
mod tests {
use crate::prelude::*;
impl_data_entity!(
TestUser,
"test_user",
["name", "email"],
{
email: String,
}
);
impl_link_entity!(
TestOwnerLink,
"test_owner_link",
{
since: DateTime<Utc>,
}
);
#[test]
fn test_data_entity_creation() {
let user = TestUser::new(
"John Doe".to_string(),
"active".to_string(),
"john@example.com".to_string(),
);
assert_eq!(user.name(), "John Doe");
assert_eq!(user.status(), "active");
assert_eq!(user.email, "john@example.com");
assert!(!user.is_deleted());
assert!(user.is_active());
}
#[test]
fn test_data_entity_soft_delete() {
let mut user = TestUser::new(
"John Doe".to_string(),
"active".to_string(),
"john@example.com".to_string(),
);
assert!(!user.is_deleted());
user.soft_delete();
assert!(user.is_deleted());
assert!(!user.is_active());
}
#[test]
fn test_data_entity_restore() {
let mut user = TestUser::new(
"John Doe".to_string(),
"active".to_string(),
"john@example.com".to_string(),
);
user.soft_delete();
assert!(user.is_deleted());
user.restore();
assert!(!user.is_deleted());
assert!(user.is_active());
}
#[test]
fn test_link_entity_creation() {
let user_id = Uuid::new_v4();
let car_id = Uuid::new_v4();
let link = TestOwnerLink::new(
"owner".to_string(),
user_id,
car_id,
"active".to_string(),
Utc::now(),
);
assert_eq!(link.source_id(), user_id);
assert_eq!(link.target_id(), car_id);
assert_eq!(link.link_type(), "owner");
assert_eq!(link.status(), "active");
assert!(!link.is_deleted());
}
#[test]
fn test_link_entity_soft_delete() {
let link = TestOwnerLink::new(
"owner".to_string(),
Uuid::new_v4(),
Uuid::new_v4(),
"active".to_string(),
Utc::now(),
);
let mut link = link;
assert!(!link.is_deleted());
link.soft_delete();
assert!(link.is_deleted());
}
#[test]
fn test_entity_set_status() {
let mut user = TestUser::new(
"John Doe".to_string(),
"active".to_string(),
"john@example.com".to_string(),
);
assert_eq!(user.status(), "active");
user.set_status("inactive".to_string());
assert_eq!(user.status(), "inactive");
}
#[test]
fn test_data_entity_field_value_name() {
let user = TestUser::new(
"Alice".to_string(),
"active".to_string(),
"alice@example.com".to_string(),
);
let name_val = user
.field_value("name")
.expect("field_value('name') should return Some");
assert_eq!(
name_val,
crate::core::field::FieldValue::String("Alice".to_string())
);
}
#[test]
fn test_data_entity_field_value_status() {
let user = TestUser::new(
"Bob".to_string(),
"pending".to_string(),
"bob@example.com".to_string(),
);
let status_val = user
.field_value("status")
.expect("field_value('status') should return Some");
assert_eq!(
status_val,
crate::core::field::FieldValue::String("pending".to_string())
);
}
#[test]
fn test_data_entity_field_value_unknown_returns_none() {
let user = TestUser::new(
"Charlie".to_string(),
"active".to_string(),
"charlie@example.com".to_string(),
);
assert!(user.field_value("email").is_none());
assert!(user.field_value("nonexistent").is_none());
}
#[test]
fn test_data_entity_resource_name() {
assert_eq!(TestUser::resource_name(), "test_users");
}
#[test]
fn test_data_entity_resource_name_singular() {
assert_eq!(TestUser::resource_name_singular(), "test_user");
}
#[test]
fn test_link_entity_resource_name() {
assert_eq!(TestOwnerLink::resource_name(), "test_owner_links");
assert_eq!(TestOwnerLink::resource_name_singular(), "test_owner_link");
}
#[test]
fn test_data_entity_indexed_fields() {
let fields = TestUser::indexed_fields();
assert!(fields.contains(&"name"));
assert!(fields.contains(&"email"));
assert_eq!(fields.len(), 2);
}
}