use core::fmt;
use crate::contracts::compatibility::compare_versions;
use crate::contracts::descriptor::Interaction;
use crate::contracts::envelope::MessageEnvelope;
use crate::contracts::event::UniversalEvent;
use crate::contracts::request::UniversalRequest;
use crate::contracts::response::UniversalResponse;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ValidationError {
EmptyPayload,
InteractionMismatch,
EmptyEventName,
EmptyEventType,
EmptyEventScope,
CapabilityRequirementMismatch,
MinimumContractVersionMismatch,
PayloadDescriptorMismatch,
}
impl fmt::Display for ValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::EmptyPayload => "a contract payload must not be empty",
Self::InteractionMismatch => "the envelope interaction does not match its message kind",
Self::EmptyEventName => "an event name must not be empty",
Self::EmptyEventType => "an event type must not be empty",
Self::EmptyEventScope => "an event scope must not be empty",
Self::CapabilityRequirementMismatch => {
"the required capability does not match the contract"
}
Self::MinimumContractVersionMismatch => {
"the contract version does not meet the minimum required version"
}
Self::PayloadDescriptorMismatch => {
"the payload descriptor does not match the contract descriptor"
}
};
formatter.write_str(message)
}
}
impl std::error::Error for ValidationError {}
pub fn validate_envelope(envelope: &MessageEnvelope) -> Result<(), ValidationError> {
if envelope.payload.bytes().is_empty() {
return Err(ValidationError::EmptyPayload);
}
if let Some(capability) = &envelope.metadata.requirements.required_capability
&& capability != &envelope.metadata.descriptor.capability_id
{
return Err(ValidationError::CapabilityRequirementMismatch);
}
if let Some(minimum_version) = &envelope.metadata.requirements.minimum_contract_version
&& compare_versions(minimum_version, &envelope.metadata.descriptor.version)
!= crate::status::Compatibility::Compatible
{
return Err(ValidationError::MinimumContractVersionMismatch);
}
if envelope.payload.descriptor() != &envelope.metadata.descriptor.payload {
return Err(ValidationError::PayloadDescriptorMismatch);
}
Ok(())
}
pub fn validate_request(request: &UniversalRequest) -> Result<(), ValidationError> {
let event = request.universal_event();
if event.envelope.metadata.descriptor.interaction != Interaction::Request {
return Err(ValidationError::InteractionMismatch);
}
validate_wrapper_event_metadata(event, "universal.request", "request")?;
validate_envelope(&event.envelope)
}
pub fn validate_response(response: &UniversalResponse) -> Result<(), ValidationError> {
let event = response.universal_event();
if event.envelope.metadata.descriptor.interaction != Interaction::Response {
return Err(ValidationError::InteractionMismatch);
}
validate_wrapper_event_metadata(event, "universal.response", "response")?;
validate_envelope(&event.envelope)
}
fn validate_wrapper_event_metadata(
event: &UniversalEvent,
expected_event_name: &str,
expected_event_type: &str,
) -> Result<(), ValidationError> {
if event.event_name().as_str().trim().is_empty() {
return Err(ValidationError::EmptyEventName);
}
if event.event_type().trim().is_empty() {
return Err(ValidationError::EmptyEventType);
}
if event.scope().trim().is_empty() {
return Err(ValidationError::EmptyEventScope);
}
if event.event_name().as_str() != expected_event_name
|| event.event_type() != expected_event_type
|| event.scope() != "global"
{
return Err(ValidationError::InteractionMismatch);
}
Ok(())
}
pub fn validate_event(event: &UniversalEvent) -> Result<(), ValidationError> {
if event.envelope.metadata.descriptor.interaction != Interaction::Event {
return Err(ValidationError::InteractionMismatch);
}
if event.event_name().as_str().trim().is_empty() {
return Err(ValidationError::EmptyEventName);
}
if event.event_type().trim().is_empty() {
return Err(ValidationError::EmptyEventType);
}
if event.scope().trim().is_empty() {
return Err(ValidationError::EmptyEventScope);
}
validate_envelope(&event.envelope)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contracts::descriptor::{
ContractDescriptor, EncodedPayload, Interaction, PayloadDescriptor, Version,
};
use crate::contracts::event::UniversalEvent;
use crate::contracts::metadata::{ContractMetadata, Participants};
use crate::identity::{
CapabilityId, ContractId, CorrelationId, EngineId, MessageId, OperationId,
};
use crate::operation::{Operation, OperationContext};
fn make_envelope(interaction: Interaction) -> MessageEnvelope {
let descriptor = ContractDescriptor::new(
ContractId::new("validation.contract").unwrap(),
CapabilityId::new("validation.capability").unwrap(),
Version::new(1, 0, 0),
interaction,
PayloadDescriptor::new("application/octet-stream", Version::new(1, 0, 0)).unwrap(),
);
let metadata = ContractMetadata::new(
descriptor.clone(),
Participants::new(
EngineId::new("sender").unwrap(),
EngineId::new("receiver").unwrap(),
),
);
MessageEnvelope::new(
MessageId::new("message-1").unwrap(),
OperationContext::new(Operation::new(
OperationId::new("operation-1").unwrap(),
CorrelationId::new("correlation-1").unwrap(),
)),
metadata,
EncodedPayload::new(descriptor.payload, b"payload".to_vec()),
)
}
#[test]
fn valid_request_passes_structural_validation() {
let request = UniversalRequest::new(make_envelope(Interaction::Request));
assert_eq!(validate_request(&request), Ok(()));
}
#[test]
fn response_rejects_non_response_interaction() {
let response = UniversalResponse::new(
make_envelope(Interaction::Request),
crate::status::Status::Success,
);
assert_eq!(
validate_response(&response),
Err(ValidationError::InteractionMismatch)
);
}
#[test]
fn request_rejects_contradictory_event_metadata() {
let mut request = UniversalRequest::new(make_envelope(Interaction::Request));
request.event.event_type = "response".into();
assert_eq!(
validate_request(&request),
Err(ValidationError::InteractionMismatch)
);
}
#[test]
fn response_rejects_contradictory_event_metadata() {
let mut response = UniversalResponse::new(
make_envelope(Interaction::Response),
crate::status::Status::Success,
);
response.event.scope = "engine:test".into();
assert_eq!(
validate_response(&response),
Err(ValidationError::InteractionMismatch)
);
}
#[test]
fn valid_event_passes_structural_validation() {
let event = UniversalEvent::new(
make_envelope(Interaction::Event),
"operation.completed",
"operation.completed",
"engine:test",
)
.unwrap();
assert_eq!(validate_event(&event), Ok(()));
}
#[test]
fn envelope_rejects_empty_payload() {
let descriptor = ContractDescriptor::new(
ContractId::new("validation.empty").unwrap(),
CapabilityId::new("validation.capability").unwrap(),
Version::new(1, 0, 0),
Interaction::Request,
PayloadDescriptor::new("application/octet-stream", Version::new(1, 0, 0)).unwrap(),
);
let metadata = ContractMetadata::new(
descriptor.clone(),
Participants::new(
EngineId::new("sender").unwrap(),
EngineId::new("receiver").unwrap(),
),
);
let envelope = MessageEnvelope::new(
MessageId::new("message-empty").unwrap(),
OperationContext::new(Operation::new(
OperationId::new("operation-empty").unwrap(),
CorrelationId::new("correlation-empty").unwrap(),
)),
metadata,
EncodedPayload::new(descriptor.payload, Vec::<u8>::new()),
);
assert_eq!(
validate_envelope(&envelope),
Err(ValidationError::EmptyPayload)
);
}
#[test]
fn validation_error_display_messages() {
assert_eq!(
ValidationError::EmptyPayload.to_string(),
"a contract payload must not be empty"
);
assert_eq!(
ValidationError::InteractionMismatch.to_string(),
"the envelope interaction does not match its message kind"
);
assert_eq!(
ValidationError::EmptyEventName.to_string(),
"an event name must not be empty"
);
assert_eq!(
ValidationError::EmptyEventType.to_string(),
"an event type must not be empty"
);
assert_eq!(
ValidationError::EmptyEventScope.to_string(),
"an event scope must not be empty"
);
assert_eq!(
ValidationError::CapabilityRequirementMismatch.to_string(),
"the required capability does not match the contract"
);
assert_eq!(
ValidationError::MinimumContractVersionMismatch.to_string(),
"the contract version does not meet the minimum required version"
);
assert_eq!(
ValidationError::PayloadDescriptorMismatch.to_string(),
"the payload descriptor does not match the contract descriptor"
);
}
#[test]
fn validation_error_is_error_trait() {
fn assert_error<E: std::error::Error>(_: E) {}
assert_error(ValidationError::EmptyPayload);
assert_error(ValidationError::InteractionMismatch);
}
}