use crate::contracts::descriptor::{
ContractDescriptor, EncodedPayload, Interaction, PayloadDescriptor,
};
use crate::contracts::metadata::{ContractMetadata, Participants};
use crate::contracts::{MessageEnvelope, UniversalEvent, Version};
use crate::identity::{CapabilityId, ContractId, EngineId, EventId, MessageId};
use crate::operation::OperationContext;
use crate::status::Retryability;
use std::sync::atomic::{AtomicU64, Ordering};
use super::{ErrorClass, ErrorCode, ErrorOwner, ErrorReference, Severity};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DiagnosticDetail {
pub key: String,
pub value: String,
}
impl DiagnosticDetail {
pub fn new(key: impl Into<String>, value: impl Into<String>) -> Option<Self> {
let key = key.into();
let value = value.into();
(!key.trim().is_empty() && !value.trim().is_empty()).then_some(Self { key, value })
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ErrorContext {
pub operation: OperationContext,
pub engine_id: Option<EngineId>,
pub capability_id: Option<CapabilityId>,
}
impl ErrorContext {
pub fn new(operation: OperationContext) -> Self {
Self {
operation,
engine_id: None,
capability_id: None,
}
}
pub fn from_engine(mut self, engine_id: EngineId) -> Self {
self.engine_id = Some(engine_id);
self
}
pub fn for_capability(mut self, capability_id: CapabilityId) -> Self {
self.capability_id = Some(capability_id);
self
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GlobalError {
pub code: ErrorCode,
pub owner: ErrorOwner,
pub version: Version,
pub class: ErrorClass,
pub severity: Severity,
pub retryability: Retryability,
pub message: String,
pub details: Vec<DiagnosticDetail>,
pub solution_reference: Option<String>,
pub context: ErrorContext,
pub cause: Option<ErrorReference>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ErrorEvent {
pub event: UniversalEvent,
pub error: GlobalError,
}
static NEXT_ERROR_EVENT_ID: AtomicU64 = AtomicU64::new(1);
impl ErrorEvent {
pub fn new(error: GlobalError) -> Self {
let sequence = NEXT_ERROR_EVENT_ID.fetch_add(1, Ordering::Relaxed);
let event_id = EventId::new(format!("error-{sequence}-{}", error.code.as_str()))
.expect("validated error codes produce valid event ids");
let message_id = MessageId::new(format!("error-message-{event_id}"))
.expect("derived error message ids are non-empty");
let sender = error
.context
.engine_id
.clone()
.unwrap_or_else(|| EngineId::new("core").expect("static core engine id is valid"));
let descriptor = ContractDescriptor::new(
ContractId::new("error.event").expect("static error contract id is valid"),
CapabilityId::new("error.report").expect("static error capability id is valid"),
Version::new(1, 0, 0),
Interaction::Event,
PayloadDescriptor::new("text/plain", Version::new(1, 0, 0))
.expect("static error payload descriptor is valid"),
);
let payload_descriptor = descriptor.payload.clone();
let metadata = ContractMetadata::new(
descriptor,
Participants::new(
sender,
EngineId::new("error-system").expect("static error-system engine id is valid"),
),
);
let envelope = MessageEnvelope::new(
message_id,
error.context.operation.clone(),
metadata,
EncodedPayload::new(payload_descriptor, error.message.as_bytes().to_vec()),
);
let scope = error
.context
.engine_id
.as_ref()
.map(|engine| format!("engine:{}", engine.as_str()))
.unwrap_or_else(|| "global".to_owned());
let event =
UniversalEvent::from_parts(envelope, event_id, "error.occurred", "error", scope)
.expect("internal ErrorEvent construction must produce a valid universal Event");
Self { event, error }
}
pub fn reference(&self) -> ErrorReference {
ErrorReference::new(self.error.code.as_str().to_owned())
.expect("validated error codes produce valid references")
}
pub fn event_id(&self) -> &EventId {
self.event.event_id()
}
pub fn message_id(&self) -> &MessageId {
self.event.message_id()
}
pub fn event_name(&self) -> &crate::events::EventName {
self.event.event_name()
}
pub fn event_type(&self) -> &str {
self.event.event_type()
}
pub fn event_scope(&self) -> &str {
self.event.scope()
}
pub fn universal_event(&self) -> &UniversalEvent {
&self.event
}
pub fn error(&self) -> &GlobalError {
&self.error
}
}
impl GlobalError {
pub(crate) fn from_definition(
definition: &super::ErrorDefinition,
context: ErrorContext,
message: Option<String>,
) -> Self {
Self {
code: definition.code.clone(),
owner: definition.owner.clone(),
version: definition.version.clone(),
class: definition.class,
severity: definition.severity,
retryability: definition.retryability,
message: message.unwrap_or_else(|| definition.default_message().to_owned()),
details: Vec::new(),
solution_reference: definition.solution_reference.clone(),
context,
cause: None,
}
}
pub fn with_detail(mut self, detail: DiagnosticDetail) -> Self {
self.details.push(detail);
self
}
pub fn details(&self) -> &[DiagnosticDetail] {
&self.details
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn caused_by(mut self, cause: ErrorReference) -> Self {
self.cause = Some(cause);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contracts::Version;
use crate::contracts::descriptor::Interaction;
use crate::error::{ErrorClass, ErrorCode, ErrorDefinition, ErrorOwner, Severity};
use crate::identity::{CorrelationId, OperationId};
use crate::operation::Operation;
use crate::operation::OperationContext;
use crate::status::Retryability;
fn operation_context() -> OperationContext {
OperationContext::new(Operation::new(
OperationId::new("test-op").unwrap(),
CorrelationId::new("test-corr").unwrap(),
))
}
fn global_error() -> GlobalError {
GlobalError {
code: ErrorCode::new("CORE.TEST.001").unwrap(),
owner: ErrorOwner::new("CORE").unwrap(),
version: Version::new(1, 0, 0),
class: ErrorClass::Contract,
severity: Severity::Error,
retryability: Retryability::NonRetryable,
message: "Test error".to_string(),
details: Vec::new(),
solution_reference: None,
context: ErrorContext::new(operation_context()),
cause: None,
}
}
#[test]
fn diagnostic_detail_rejects_empty_key() {
let detail = DiagnosticDetail::new("", "value");
assert!(detail.is_none());
}
#[test]
fn diagnostic_detail_rejects_empty_value() {
let detail = DiagnosticDetail::new("key", "");
assert!(detail.is_none());
}
#[test]
fn diagnostic_detail_accepts_valid_fields() {
let detail = DiagnosticDetail::new("key", "value").unwrap();
assert_eq!(detail.key, "key");
assert_eq!(detail.value, "value");
}
#[test]
fn error_context_starts_without_engine_or_capability() {
let context = ErrorContext::new(operation_context());
assert!(context.engine_id.is_none());
assert!(context.capability_id.is_none());
}
#[test]
fn error_context_builder_methods_set_fields() {
let context = ErrorContext::new(operation_context())
.from_engine(EngineId::new("engine-1").unwrap())
.for_capability(CapabilityId::new("cap-1").unwrap());
assert!(context.engine_id.is_some());
assert!(context.capability_id.is_some());
}
#[test]
fn global_error_from_definition_preserves_fields() {
let definition = ErrorDefinition::new(
ErrorCode::new("CORE.TEST.002").unwrap(),
ErrorOwner::new("CORE").unwrap(),
Version::new(1, 0, 0),
ErrorClass::Contract,
Severity::Warning,
"Warning message",
Retryability::Retryable,
)
.unwrap();
let error =
GlobalError::from_definition(&definition, ErrorContext::new(operation_context()), None);
assert_eq!(error.code.as_str(), "CORE.TEST.002");
assert_eq!(error.severity, Severity::Warning);
assert_eq!(error.retryability, Retryability::Retryable);
assert!(error.cause.is_none());
assert!(error.details.is_empty());
}
#[test]
fn global_error_with_message_overrides_default() {
let mut error = global_error();
error = error.with_message("Custom message".to_string());
assert_eq!(error.message, "Custom message");
}
#[test]
fn global_error_with_detail_adds_to_details() {
let mut error = global_error();
error = error.with_detail(DiagnosticDetail::new("key", "value").unwrap());
assert_eq!(error.details.len(), 1);
assert_eq!(error.details[0].key, "key");
}
#[test]
fn global_error_caused_by_sets_cause() {
let mut error = global_error();
let cause = ErrorReference::new("CORE.CAUSE.001").unwrap();
error = error.caused_by(cause.clone());
assert_eq!(error.cause, Some(cause));
}
#[test]
fn error_event_constructs_universal_event_internally() {
let event = ErrorEvent::new(global_error());
assert!(!event.event_id().as_str().is_empty());
assert!(!event.message_id().as_str().is_empty());
assert_eq!(event.event_name().as_str(), "error.occurred");
assert_eq!(event.event_type(), "error");
assert_eq!(event.event_scope(), "global");
assert_eq!(event.reference().as_str(), "CORE.TEST.001");
assert_eq!(event.error().message, "Test error");
}
#[test]
fn error_event_preserves_operation_context_in_universal_envelope() {
let error = global_error();
let operation = error.context.operation.clone();
let event = ErrorEvent::new(error);
assert_eq!(
event.universal_event().envelope.operation_context,
operation
);
assert_eq!(
event
.universal_event()
.envelope
.metadata
.descriptor
.interaction,
Interaction::Event
);
}
}