use super::scope::Scope;
use crate::{identity::EventId, operation::OperationContext, security::SecurityContext};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EventName(Box<str>);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InvalidEventName;
impl std::fmt::Display for InvalidEventName {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("an event name must not be empty")
}
}
impl std::error::Error for InvalidEventName {}
impl EventName {
pub fn new(value: impl Into<String>) -> Result<Self, InvalidEventName> {
let value = value.into();
if value.trim().is_empty() {
return Err(InvalidEventName);
}
Ok(Self(value.into_boxed_str()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for EventName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::fmt::Display for EventName {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
impl std::str::FromStr for EventName {
type Err = InvalidEventName;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl TryFrom<String> for EventName {
type Error = InvalidEventName;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct EventContext {
operation_context: Option<OperationContext>,
security_context: Option<SecurityContext>,
}
impl EventContext {
pub const fn empty() -> Self {
Self {
operation_context: None,
security_context: None,
}
}
pub const fn new(
operation_context: Option<OperationContext>,
security_context: Option<SecurityContext>,
) -> Self {
Self {
operation_context,
security_context,
}
}
pub fn operation_context(&self) -> Option<&OperationContext> {
self.operation_context.as_ref()
}
pub fn security_context(&self) -> Option<&SecurityContext> {
self.security_context.as_ref()
}
pub fn with_operation_context(mut self, operation_context: OperationContext) -> Self {
self.operation_context = Some(operation_context);
self
}
pub fn with_security_context(mut self, security_context: SecurityContext) -> Self {
self.security_context = Some(security_context);
self
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EventCreationError {
EmptyEventType,
}
impl std::fmt::Display for EventCreationError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyEventType => formatter.write_str("event type must not be empty"),
}
}
}
impl std::error::Error for EventCreationError {}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Event {
event_id: EventId,
event_name: EventName,
event_type: Box<str>,
scope: Scope,
context: EventContext,
}
impl Event {
pub fn new(
event_id: EventId,
event_name: EventName,
event_type: impl Into<String>,
scope: Scope,
) -> Result<Self, EventCreationError> {
Self::new_with_context(
event_id,
event_name,
event_type,
scope,
EventContext::empty(),
)
}
pub fn new_with_context(
event_id: EventId,
event_name: EventName,
event_type: impl Into<String>,
scope: Scope,
context: EventContext,
) -> Result<Self, EventCreationError> {
let event_type = event_type.into();
if event_type.trim().is_empty() {
return Err(EventCreationError::EmptyEventType);
}
Ok(Self {
event_id,
event_name,
event_type: event_type.into_boxed_str(),
scope,
context,
})
}
pub fn event_id(&self) -> &EventId {
&self.event_id
}
pub fn event_name(&self) -> &EventName {
&self.event_name
}
pub fn event_type(&self) -> &str {
&self.event_type
}
pub fn scope(&self) -> &Scope {
&self.scope
}
pub fn context(&self) -> &EventContext {
&self.context
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::EventId;
fn event_name() -> EventName {
EventName::new("engine.started").unwrap()
}
fn scope() -> Scope {
Scope::new("engine:test").unwrap()
}
#[test]
fn event_name_constructs_and_rejects_empty_values() {
let name = EventName::new("engine.started").unwrap();
assert_eq!(name.as_str(), "engine.started");
assert_eq!(name.to_string(), "engine.started");
assert!(EventName::new("").is_err());
assert!(EventName::new(" ").is_err());
}
#[test]
fn event_name_supports_standard_string_conversions() {
use std::str::FromStr;
let name = EventName::from_str("engine.started").unwrap();
assert_eq!(name.as_ref(), "engine.started");
assert_eq!(
EventName::try_from(String::from("engine.started"))
.unwrap()
.as_str(),
"engine.started"
);
}
#[test]
fn event_name_error_has_stable_message() {
assert_eq!(
InvalidEventName.to_string(),
"an event name must not be empty"
);
}
#[test]
fn event_constructs_with_identity_name_type_and_scope() {
let event = Event::new(
EventId::new("event-1").unwrap(),
event_name(),
"lifecycle",
scope(),
)
.unwrap();
assert_eq!(event.event_id().as_str(), "event-1");
assert_eq!(event.event_name().as_str(), "engine.started");
assert_eq!(event.event_type(), "lifecycle");
assert_eq!(event.scope().as_str(), "engine:test");
}
#[test]
fn event_rejects_empty_event_type() {
assert_eq!(
Event::new(EventId::new("event-1").unwrap(), event_name(), "", scope(),).unwrap_err(),
EventCreationError::EmptyEventType
);
assert_eq!(
Event::new(
EventId::new("event-2").unwrap(),
event_name(),
" ",
scope(),
)
.unwrap_err(),
EventCreationError::EmptyEventType
);
}
#[test]
fn event_name_and_event_id_remain_distinct() {
let first = Event::new(
EventId::new("event-1").unwrap(),
EventName::new("engine.started").unwrap(),
"lifecycle",
scope(),
)
.unwrap();
let second = Event::new(
EventId::new("event-2").unwrap(),
EventName::new("engine.started").unwrap(),
"lifecycle",
scope(),
)
.unwrap();
assert_ne!(first.event_id(), second.event_id());
assert_eq!(first.event_name(), second.event_name());
}
#[test]
fn event_is_immutable_and_clone_preserves_metadata() {
let event = Event::new(
EventId::new("event-1").unwrap(),
event_name(),
"lifecycle",
scope(),
)
.unwrap();
let cloned = event.clone();
assert_eq!(cloned, event);
}
#[test]
fn event_context_preserves_operation_and_security_context() {
use crate::{
identity::{CorrelationId, OperationId},
operation::Operation,
security::identity::{PrincipalId, PrincipalIdentity, PrincipalType},
};
let operation_context = OperationContext::new(Operation::new(
OperationId::new("operation-1").unwrap(),
CorrelationId::new("correlation-1").unwrap(),
));
let principal = PrincipalIdentity::new(
PrincipalType::Engine,
PrincipalId::new("producer-engine").unwrap(),
);
let security_context = SecurityContext::new(principal, None);
let context = EventContext::empty()
.with_operation_context(operation_context.clone())
.with_security_context(security_context.clone());
assert_eq!(context.operation_context(), Some(&operation_context));
assert_eq!(context.security_context(), Some(&security_context));
let event = Event::new_with_context(
EventId::new("event-ctx-1").unwrap(),
event_name(),
"lifecycle",
scope(),
context,
)
.unwrap();
assert_eq!(
event.context().operation_context(),
Some(&operation_context)
);
assert_eq!(event.context().security_context(), Some(&security_context));
}
}