scaffold workspace + domain crate (errors, ids, uuid_id macro)
This commit is contained in:
22
crates/domain/Cargo.toml
Normal file
22
crates/domain/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "domain"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
test-helpers = []
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
email_address = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
63
crates/domain/src/errors/mod.rs
Normal file
63
crates/domain/src/errors/mod.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum DomainError {
|
||||
#[error("User not found: {0}")]
|
||||
UserNotFound(Uuid),
|
||||
|
||||
#[error("User already exists: {0}")]
|
||||
UserAlreadyExists(String),
|
||||
|
||||
#[error("Channel not found: {0}")]
|
||||
ChannelNotFound(Uuid),
|
||||
|
||||
#[error("No active schedule for channel: {0}")]
|
||||
NoActiveSchedule(Uuid),
|
||||
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
|
||||
#[error("Invalid timezone: {0}")]
|
||||
TimezoneError(String),
|
||||
|
||||
#[error("Unauthenticated: {0}")]
|
||||
Unauthenticated(String),
|
||||
|
||||
#[error("Forbidden: {0}")]
|
||||
Forbidden(String),
|
||||
|
||||
#[error("Repository error: {0}")]
|
||||
RepositoryError(String),
|
||||
|
||||
#[error("Infrastructure error: {0}")]
|
||||
InfrastructureError(String),
|
||||
}
|
||||
|
||||
impl DomainError {
|
||||
pub fn validation(message: impl Into<String>) -> Self {
|
||||
Self::ValidationError(message.into())
|
||||
}
|
||||
|
||||
pub fn unauthenticated(message: impl Into<String>) -> Self {
|
||||
Self::Unauthenticated(message.into())
|
||||
}
|
||||
|
||||
pub fn forbidden(message: impl Into<String>) -> Self {
|
||||
Self::Forbidden(message.into())
|
||||
}
|
||||
|
||||
pub fn is_not_found(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
DomainError::UserNotFound(_) | DomainError::ChannelNotFound(_)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_conflict(&self) -> bool {
|
||||
matches!(self, DomainError::UserAlreadyExists(_))
|
||||
}
|
||||
}
|
||||
|
||||
pub type DomainResult<T> = Result<T, DomainError>;
|
||||
5
crates/domain/src/lib.rs
Normal file
5
crates/domain/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod errors;
|
||||
pub mod value_objects;
|
||||
|
||||
pub use errors::{DomainError, DomainResult};
|
||||
pub use value_objects::*;
|
||||
93
crates/domain/src/value_objects/ids.rs
Normal file
93
crates/domain/src/value_objects/ids.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
macro_rules! uuid_id {
|
||||
($name:ident) => {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct $name(Uuid);
|
||||
|
||||
impl $name {
|
||||
pub fn generate() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
|
||||
pub fn from_uuid(uuid: Uuid) -> Self {
|
||||
Self(uuid)
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Uuid {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for $name {
|
||||
type Err = uuid::Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self(s.parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Uuid> for $name {
|
||||
fn from(uuid: Uuid) -> Self {
|
||||
Self(uuid)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use uuid_id;
|
||||
|
||||
uuid_id!(UserId);
|
||||
uuid_id!(ChannelId);
|
||||
uuid_id!(SlotId);
|
||||
uuid_id!(BlockId);
|
||||
uuid_id!(ScheduleId);
|
||||
|
||||
/// Opaque media item identifier -- format is provider-specific.
|
||||
/// The domain never inspects the string; it just passes it back to the provider.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct MediaItemId(String);
|
||||
|
||||
impl MediaItemId {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for MediaItemId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MediaItemId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for MediaItemId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for MediaItemId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
3
crates/domain/src/value_objects/mod.rs
Normal file
3
crates/domain/src/value_objects/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod ids;
|
||||
|
||||
pub use ids::*;
|
||||
Reference in New Issue
Block a user