#![warn(missing_docs)]
#![warn(clippy::all)]
#[cfg(feature = "outbox")]
pub mod outbox;
pub mod policy;
pub mod prelude;
#[cfg(feature = "streams")]
pub mod stream;
pub mod transaction;
pub use async_trait::async_trait;
#[must_use]
pub const fn const_str_eq(a: &str, b: &str) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
pub trait Repository: Send + Sync {
type Error: std::error::Error + Send + Sync;
type Pool;
fn pool(&self) -> &Self::Pool;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pagination {
pub limit: i64,
pub offset: i64
}
impl Pagination {
#[must_use]
pub const fn new(limit: i64, offset: i64) -> Self {
Self {
limit,
offset
}
}
#[must_use]
pub const fn page(page: i64, per_page: i64) -> Self {
Self {
limit: per_page,
offset: page * per_page
}
}
}
impl Default for Pagination {
fn default() -> Self {
Self {
limit: 100,
offset: 0
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortDirection {
#[default]
Asc,
Desc
}
impl SortDirection {
#[must_use]
pub const fn as_sql(&self) -> &'static str {
match self {
Self::Asc => "ASC",
Self::Desc => "DESC"
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EventKind {
Created,
Updated,
SoftDeleted,
HardDeleted,
Restored
}
impl EventKind {
#[must_use]
pub const fn is_delete(&self) -> bool {
matches!(self, Self::SoftDeleted | Self::HardDeleted)
}
#[must_use]
pub const fn is_mutation(&self) -> bool {
!matches!(self, Self::Restored)
}
}
pub trait EntityEvent: Send + Sync + std::fmt::Debug {
type Id;
fn kind(&self) -> EventKind;
fn entity_id(&self) -> &Self::Id;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CommandKind {
Create,
Update,
Delete,
Custom
}
impl CommandKind {
#[must_use]
pub const fn is_create(&self) -> bool {
matches!(self, Self::Create)
}
#[must_use]
pub const fn is_mutation(&self) -> bool {
!matches!(self, Self::Custom)
}
}
pub trait EntityCommand: Send + Sync + std::fmt::Debug {
fn kind(&self) -> CommandKind;
fn name(&self) -> &'static str;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constraint_error_display_unique_field() {
let err = ConstraintError {
kind: ConstraintKind::Unique,
constraint: "users_email_key".to_string(),
field: Some("email")
};
assert_eq!(err.to_string(), "duplicate value for unique field `email`");
}
#[test]
fn constraint_error_display_fk_field() {
let err = ConstraintError {
kind: ConstraintKind::ForeignKey,
constraint: "orders_user_id_fkey".to_string(),
field: Some("user_id")
};
assert_eq!(
err.to_string(),
"referenced row missing for field `user_id`"
);
}
#[test]
fn constraint_error_display_unknown_field() {
let err = ConstraintError {
kind: ConstraintKind::Check,
constraint: "orders_amount_check".to_string(),
field: None
};
assert_eq!(
err.to_string(),
"Check constraint `orders_amount_check` violated"
);
}
#[test]
fn pagination_new() {
let p = Pagination::new(50, 100);
assert_eq!(p.limit, 50);
assert_eq!(p.offset, 100);
}
#[test]
fn pagination_page() {
let p = Pagination::page(2, 25);
assert_eq!(p.limit, 25);
assert_eq!(p.offset, 50);
}
#[test]
fn pagination_default() {
let p = Pagination::default();
assert_eq!(p.limit, 100);
assert_eq!(p.offset, 0);
}
#[test]
fn sort_direction_sql() {
assert_eq!(SortDirection::Asc.as_sql(), "ASC");
assert_eq!(SortDirection::Desc.as_sql(), "DESC");
}
#[test]
fn sort_direction_default() {
assert_eq!(SortDirection::default(), SortDirection::Asc);
}
#[test]
fn event_kind_is_delete() {
assert!(!EventKind::Created.is_delete());
assert!(!EventKind::Updated.is_delete());
assert!(EventKind::SoftDeleted.is_delete());
assert!(EventKind::HardDeleted.is_delete());
assert!(!EventKind::Restored.is_delete());
}
#[test]
fn event_kind_is_mutation() {
assert!(EventKind::Created.is_mutation());
assert!(EventKind::Updated.is_mutation());
assert!(EventKind::SoftDeleted.is_mutation());
assert!(EventKind::HardDeleted.is_mutation());
assert!(!EventKind::Restored.is_mutation());
}
#[test]
fn command_kind_is_create() {
assert!(CommandKind::Create.is_create());
assert!(!CommandKind::Update.is_create());
assert!(!CommandKind::Delete.is_create());
assert!(!CommandKind::Custom.is_create());
}
#[test]
fn command_kind_is_mutation() {
assert!(CommandKind::Create.is_mutation());
assert!(CommandKind::Update.is_mutation());
assert!(CommandKind::Delete.is_mutation());
assert!(!CommandKind::Custom.is_mutation());
}
#[test]
fn const_str_eq_equal_strings() {
assert!(const_str_eq("order_status", "order_status"));
assert!(const_str_eq("", ""));
}
#[test]
fn const_str_eq_different_lengths() {
assert!(!const_str_eq("order", "order_status"));
assert!(!const_str_eq("order_status", ""));
}
#[test]
fn const_str_eq_same_length_different_content() {
assert!(!const_str_eq("order_status", "order_states"));
assert!(!const_str_eq("abc", "abd"));
}
#[test]
fn const_str_eq_in_const_context() {
const OK: bool = const_str_eq("user_role", "user_role");
const MISMATCH: bool = const_str_eq("user_role", "user_rank");
assert_eq!((OK, MISMATCH), (true, false));
}
}
#[cfg(feature = "serde")]
pub mod serde_helpers {
pub mod double_option {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
where
T: Deserialize<'de>,
D: Deserializer<'de>
{
Option::<T>::deserialize(deserializer).map(Some)
}
pub fn serialize<T, S>(value: &Option<Option<T>>, serializer: S) -> Result<S::Ok, S::Error>
where
T: Serialize,
S: Serializer
{
match value {
Some(inner) => inner.serialize(serializer),
None => serializer.serialize_none()
}
}
}
#[cfg(all(test, feature = "serde_json"))]
mod tests {
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)]
struct Patch {
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "super::double_option"
)]
nick: Option<Option<String>>
}
#[test]
fn absent_field_is_outer_none() {
let patch: Patch = serde_json::from_str("{}").unwrap();
assert_eq!(patch.nick, None);
}
#[test]
fn null_field_is_some_none() {
let patch: Patch = serde_json::from_str(r#"{"nick": null}"#).unwrap();
assert_eq!(patch.nick, Some(None));
}
#[test]
fn value_field_is_some_some() {
let patch: Patch = serde_json::from_str(r#"{"nick": "neo"}"#).unwrap();
assert_eq!(patch.nick, Some(Some("neo".to_string())));
}
#[test]
fn outer_none_skipped_on_serialize() {
let json = serde_json::to_string(&Patch {
nick: None
})
.unwrap();
assert_eq!(json, "{}");
}
#[test]
fn some_none_serializes_null() {
let json = serde_json::to_string(&Patch {
nick: Some(None)
})
.unwrap();
assert_eq!(json, r#"{"nick":null}"#);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConstraintKind {
Unique,
ForeignKey,
Check
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstraintError {
pub kind: ConstraintKind,
pub constraint: String,
pub field: Option<&'static str>
}
impl std::fmt::Display for ConstraintError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (self.kind, self.field) {
(ConstraintKind::Unique, Some(field)) => {
write!(f, "duplicate value for unique field `{field}`")
}
(ConstraintKind::ForeignKey, Some(field)) => {
write!(f, "referenced row missing for field `{field}`")
}
(kind, _) => write!(f, "{kind:?} constraint `{}` violated", self.constraint)
}
}
}
impl std::error::Error for ConstraintError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransitionError {
pub entity: &'static str,
pub from: String,
pub to: &'static str
}
impl std::fmt::Display for TransitionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} cannot transition from `{}` to `{}`",
self.entity, self.from, self.to
)
}
}
impl std::error::Error for TransitionError {}