use crate::enums::{AttributionType, EventPayload, EventType, HexacoPath};
use crate::event::event_spec::EventSpec;
use crate::event::Event;
use crate::types::{Duration, EntityId, EventId, MicrosystemId};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EventBuildError {
pub reason: String,
}
impl fmt::Display for EventBuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Event build error: {}", self.reason)
}
}
impl std::error::Error for EventBuildError {}
#[derive(Debug, Clone)]
pub struct EventBuilder {
event_type: EventType,
id: Option<EventId>,
source: Option<EntityId>,
target: Option<EntityId>,
severity: f64,
payload: Option<EventPayload>,
trust_attribution: Option<AttributionType>,
timestamp: Duration,
microsystem_context: Option<MicrosystemId>,
base_shifts: Vec<(HexacoPath, f32)>,
custom_spec: Option<EventSpec>,
}
impl EventBuilder {
#[must_use]
pub fn new(event_type: EventType) -> Self {
EventBuilder {
event_type,
id: None,
source: None,
target: None,
severity: 0.5,
payload: None,
trust_attribution: None,
timestamp: Duration::zero(),
microsystem_context: None,
base_shifts: Vec::new(),
custom_spec: None,
}
}
#[must_use]
pub fn custom(spec: EventSpec) -> Self {
EventBuilder {
event_type: EventType::Custom,
id: None,
source: None,
target: None,
severity: 1.0,
payload: None,
trust_attribution: None,
timestamp: Duration::zero(),
microsystem_context: None,
base_shifts: Vec::new(),
custom_spec: Some(spec),
}
}
#[must_use]
pub fn id(mut self, id: EventId) -> Self {
self.id = Some(id);
self
}
#[must_use]
pub fn source(mut self, entity_id: EntityId) -> Self {
self.source = Some(entity_id);
self
}
#[must_use]
pub fn target(mut self, entity_id: EntityId) -> Self {
self.target = Some(entity_id);
self
}
#[must_use]
pub fn severity(mut self, severity: f64) -> Self {
self.severity = severity.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn payload(mut self, payload: EventPayload) -> Self {
self.payload = Some(payload);
self
}
#[must_use]
pub fn trust_attribution(mut self, attribution: AttributionType) -> Self {
self.trust_attribution = Some(attribution);
self
}
#[must_use]
pub fn timestamp(mut self, timestamp: Duration) -> Self {
self.timestamp = timestamp;
self
}
#[must_use]
pub fn context(mut self, microsystem: MicrosystemId) -> Self {
self.microsystem_context = Some(microsystem);
self
}
#[must_use]
pub fn with_base_shift(mut self, trait_path: HexacoPath, amount: f32) -> Self {
let clamped = amount.clamp(-1.0, 1.0);
self.base_shifts.push((trait_path, clamped));
self
}
pub fn build(self) -> Result<Event, EventBuildError> {
let mut event = if let Some(id) = self.id {
Event::with_id(id, self.event_type)
} else {
Event::new(self.event_type)
};
event.set_source(self.source);
event.set_target(self.target);
event.set_severity(self.severity);
event.set_payload(self.payload.unwrap_or(EventPayload::Empty));
event.set_trust_attribution(self.trust_attribution);
event.set_timestamp(self.timestamp);
event.set_microsystem_context(self.microsystem_context);
event.set_base_shifts(self.base_shifts);
if let Some(spec) = self.custom_spec {
event.set_custom_spec(spec);
}
Ok(event)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::event_spec::{ChronicFlags, EventImpact, PermanenceValues};
#[test]
fn event_builder_minimal() {
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.build()
.unwrap();
assert_eq!(event.event_type(), EventType::ExperienceCombatMilitary);
assert!(event.source().is_none());
assert!(event.target().is_none());
assert!((event.severity() - 0.5).abs() < f64::EPSILON);
assert_eq!(event.payload(), &EventPayload::Empty);
assert!(!event.has_payload_data());
assert!(event.trust_attribution().is_none());
}
#[test]
fn event_builder_full() {
let source = EntityId::new("source_001").unwrap();
let target = EntityId::new("target_001").unwrap();
let context = MicrosystemId::new("work_001").unwrap();
let custom_id = EventId::new("custom_evt").unwrap();
let event = EventBuilder::new(EventType::AchieveGoalMajor)
.id(custom_id.clone())
.source(source.clone())
.target(target.clone())
.severity(0.9)
.trust_attribution(AttributionType::Ability)
.timestamp(Duration::days(50))
.context(context.clone())
.build()
.unwrap();
assert_eq!(event.id(), &custom_id);
assert_eq!(event.event_type(), EventType::AchieveGoalMajor);
assert_eq!(event.source(), Some(&source));
assert_eq!(event.target(), Some(&target));
assert!((event.severity() - 0.9).abs() < f64::EPSILON);
assert_eq!(event.timestamp().as_days(), 50);
assert_eq!(event.microsystem_context(), Some(&context));
assert_eq!(event.trust_attribution(), Some(AttributionType::Ability));
}
#[test]
fn event_builder_fluent_chain() {
let event = EventBuilder::new(EventType::ExperienceBetrayalTrust)
.severity(0.7)
.timestamp(Duration::hours(24))
.build()
.unwrap();
assert_eq!(event.event_type(), EventType::ExperienceBetrayalTrust);
assert!((event.severity() - 0.7).abs() < f64::EPSILON);
}
#[test]
fn event_builder_severity_clamped() {
let event_high = EventBuilder::new(EventType::ExperienceCombatMilitary)
.severity(1.5)
.build()
.unwrap();
assert!((event_high.severity() - 1.0).abs() < f64::EPSILON);
let event_low = EventBuilder::new(EventType::ExperienceCombatMilitary)
.severity(-0.5)
.build()
.unwrap();
assert!(event_low.severity().abs() < f64::EPSILON);
}
#[test]
fn event_builder_auto_generates_id() {
let event1 = EventBuilder::new(EventType::AchieveGoalMajor)
.build()
.unwrap();
let event2 = EventBuilder::new(EventType::AchieveGoalMajor)
.build()
.unwrap();
assert_ne!(event1.id(), event2.id());
}
#[test]
fn event_build_error_display() {
let error = EventBuildError {
reason: "test error".to_string(),
};
let display = format!("{}", error);
assert!(display.contains("test error"));
}
#[test]
fn event_builder_clone() {
let builder = EventBuilder::new(EventType::ExperienceCombatMilitary).severity(0.8);
let cloned = builder.clone();
let event1 = builder.build().unwrap();
let event2 = cloned.build().unwrap();
assert_eq!(event1.event_type(), event2.event_type());
assert!((event1.severity() - event2.severity()).abs() < f64::EPSILON);
}
#[test]
fn event_builder_debug_format() {
let builder = EventBuilder::new(EventType::ExperienceCombatMilitary);
let debug = format!("{:?}", builder);
assert!(debug.contains("EventBuilder"));
assert!(debug.contains("ExperienceCombatMilitary"));
}
#[test]
fn event_builder_with_base_shift() {
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.severity(0.9)
.with_base_shift(HexacoPath::Neuroticism, 0.25)
.with_base_shift(HexacoPath::Agreeableness, -0.15)
.build()
.unwrap();
assert!(event.has_base_shifts());
assert_eq!(event.base_shifts().len(), 2);
assert_eq!(event.base_shifts()[0], (HexacoPath::Neuroticism, 0.25));
assert_eq!(event.base_shifts()[1], (HexacoPath::Agreeableness, -0.15));
}
#[test]
fn event_builder_no_base_shifts_by_default() {
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.build()
.unwrap();
assert!(!event.has_base_shifts());
}
#[test]
fn event_builder_base_shift_clamps_values() {
let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
.with_base_shift(HexacoPath::Openness, 2.0)
.with_base_shift(HexacoPath::Extraversion, -2.0)
.build()
.unwrap();
assert!((event.base_shifts()[0].1 - 1.0).abs() < f32::EPSILON);
assert!((event.base_shifts()[1].1 - (-1.0)).abs() < f32::EPSILON);
}
#[test]
fn event_builder_custom_with_spec() {
let custom_spec = EventSpec {
impact: EventImpact {
valence: 0.88,
..Default::default()
},
chronic: ChronicFlags::default(),
permanence: PermanenceValues::default(),
};
let event = EventBuilder::custom(custom_spec).severity(0.7).build().unwrap();
assert_eq!(event.event_type(), EventType::Custom);
let spec = event.spec();
assert!((spec.impact.valence - 0.88).abs() < f32::EPSILON);
}
#[test]
fn event_build_error_implements_error_trait() {
let error = EventBuildError {
reason: "test error".to_string(),
};
let error_ref: &dyn std::error::Error = &error;
assert!(error_ref.to_string().contains("test error"));
assert!(error_ref.source().is_none());
}
}