nizaam-core 1.0.1

Domain-agnostic Rust infrastructure core for building secure, observable, reliable, and extensible Nizaam engines.
Documentation
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;

/// A structural contract validation failurae.
#[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(())
}

/// Validates the structural invariants of a universal Event.
///
/// Event-specific semantics are limited to the Event interaction marker, the
/// non-empty Event type, and the non-empty Event scope. Generic envelope rules
/// continue to be enforced by [`validate_envelope`].
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);
    }
}