scaffold workspace + domain crate (errors, ids, uuid_id macro)

This commit is contained in:
2026-07-12 01:00:50 +02:00
parent e3a65d8052
commit 3ff146615f
7 changed files with 1215 additions and 0 deletions

22
crates/domain/Cargo.toml Normal file
View 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 }

View 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
View File

@@ -0,0 +1,5 @@
pub mod errors;
pub mod value_objects;
pub use errors::{DomainError, DomainResult};
pub use value_objects::*;

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

View File

@@ -0,0 +1,3 @@
pub mod ids;
pub use ids::*;