evidentsource-client 1.0.0-rc1

Rust client for the EvidentSource event sourcing platform
Documentation
//! EventSelector type conversions.

use evidentsource_core::domain::{
    EventAttribute, EventAttributePrefix, EventSelector, EventSubject, EventType, StreamName,
};

use crate::com::evidentsource as proto;

use super::error::ConversionError;

// =============================================================================
// Domain -> Proto (infallible)
// =============================================================================

impl From<EventSelector> for proto::EventSelector {
    fn from(selector: EventSelector) -> Self {
        use proto::event_selector::Selector;

        let selector_variant = match selector {
            EventSelector::Equals(attr) => Selector::Equals(attr.into()),
            EventSelector::StartsWith(prefix) => Selector::StartsWith(prefix.into()),
            EventSelector::And { left, right } => {
                Selector::And(Box::new(proto::event_selector::LogicalAnd {
                    left: Some(Box::new((*left).into())),
                    right: Some(Box::new((*right).into())),
                }))
            }
            EventSelector::Or { left, right } => {
                Selector::Or(Box::new(proto::event_selector::LogicalOr {
                    left: Some(Box::new((*left).into())),
                    right: Some(Box::new((*right).into())),
                }))
            }
        };

        proto::EventSelector {
            selector: Some(selector_variant),
        }
    }
}

impl From<EventAttribute> for proto::EventAttribute {
    fn from(attr: EventAttribute) -> Self {
        use proto::event_attribute::Attribute;

        let attribute = match attr {
            EventAttribute::Stream(name) => Attribute::Stream(name.to_string()),
            EventAttribute::Subject(subject) => {
                Attribute::Subject(proto::event_attribute::SubjectValue {
                    has_value: subject.is_some(),
                    value: subject.map(|s| s.to_string()).unwrap_or_default(),
                })
            }
            EventAttribute::EventType(et) => Attribute::EventType(et.to_string()),
        };

        proto::EventAttribute {
            attribute: Some(attribute),
        }
    }
}

impl From<EventAttributePrefix> for proto::event_selector::StartsWith {
    fn from(prefix: EventAttributePrefix) -> Self {
        use proto::event_selector::starts_with::Attribute;

        let attribute = match prefix {
            EventAttributePrefix::Stream(name) => Attribute::Stream(name.to_string()),
            EventAttributePrefix::Subject(subject) => Attribute::Subject(subject.to_string()),
            EventAttributePrefix::EventType(et) => Attribute::EventType(et.to_string()),
        };

        proto::event_selector::StartsWith {
            attribute: Some(attribute),
        }
    }
}

// =============================================================================
// Proto -> Domain (fallible)
// =============================================================================

impl TryFrom<proto::EventSelector> for EventSelector {
    type Error = ConversionError;

    fn try_from(proto: proto::EventSelector) -> Result<Self, Self::Error> {
        use proto::event_selector::Selector;

        let selector = proto
            .selector
            .ok_or_else(|| ConversionError::missing_oneof("EventSelector", "selector"))?;

        match selector {
            Selector::Equals(attr) => {
                let domain_attr = EventAttribute::try_from(attr)?;
                Ok(EventSelector::Equals(domain_attr))
            }
            Selector::StartsWith(starts_with) => {
                let domain_prefix = EventAttributePrefix::try_from(starts_with)?;
                Ok(EventSelector::StartsWith(domain_prefix))
            }
            Selector::And(logical_and) => {
                let left = logical_and.left.ok_or_else(|| {
                    ConversionError::missing_field("EventSelector.LogicalAnd", "left")
                })?;
                let right = logical_and.right.ok_or_else(|| {
                    ConversionError::missing_field("EventSelector.LogicalAnd", "right")
                })?;

                let left_domain = EventSelector::try_from(*left)
                    .map_err(|e| ConversionError::nested("And.left", e))?;
                let right_domain = EventSelector::try_from(*right)
                    .map_err(|e| ConversionError::nested("And.right", e))?;

                Ok(EventSelector::And {
                    left: Box::new(left_domain),
                    right: Box::new(right_domain),
                })
            }
            Selector::Or(logical_or) => {
                let left = logical_or.left.ok_or_else(|| {
                    ConversionError::missing_field("EventSelector.LogicalOr", "left")
                })?;
                let right = logical_or.right.ok_or_else(|| {
                    ConversionError::missing_field("EventSelector.LogicalOr", "right")
                })?;

                let left_domain = EventSelector::try_from(*left)
                    .map_err(|e| ConversionError::nested("Or.left", e))?;
                let right_domain = EventSelector::try_from(*right)
                    .map_err(|e| ConversionError::nested("Or.right", e))?;

                Ok(EventSelector::Or {
                    left: Box::new(left_domain),
                    right: Box::new(right_domain),
                })
            }
        }
    }
}

impl TryFrom<proto::EventAttribute> for EventAttribute {
    type Error = ConversionError;

    fn try_from(proto: proto::EventAttribute) -> Result<Self, Self::Error> {
        use proto::event_attribute::Attribute;

        let attr = proto
            .attribute
            .ok_or_else(|| ConversionError::missing_oneof("EventAttribute", "attribute"))?;

        match attr {
            Attribute::Stream(s) => {
                let name = StreamName::new(s)?;
                Ok(EventAttribute::Stream(name))
            }
            Attribute::Subject(subj) => {
                if subj.has_value {
                    let subject = EventSubject::new(subj.value)?;
                    Ok(EventAttribute::Subject(Some(subject)))
                } else {
                    Ok(EventAttribute::Subject(None))
                }
            }
            Attribute::EventType(et) => {
                let event_type = EventType::new(et)?;
                Ok(EventAttribute::EventType(event_type))
            }
        }
    }
}

impl TryFrom<proto::event_selector::StartsWith> for EventAttributePrefix {
    type Error = ConversionError;

    fn try_from(proto: proto::event_selector::StartsWith) -> Result<Self, Self::Error> {
        use proto::event_selector::starts_with::Attribute;

        let attr = proto
            .attribute
            .ok_or_else(|| ConversionError::missing_oneof("StartsWith", "attribute"))?;

        match attr {
            Attribute::Stream(s) => {
                let name = StreamName::new(s)?;
                Ok(EventAttributePrefix::Stream(name))
            }
            Attribute::Subject(s) => {
                let subject = EventSubject::new(s)?;
                Ok(EventAttributePrefix::Subject(subject))
            }
            Attribute::EventType(et) => {
                let event_type = EventType::new(et)?;
                Ok(EventAttributePrefix::EventType(event_type))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_roundtrip_simple_equals() {
        let domain = EventSelector::stream_equals("my-stream").unwrap();
        let proto: proto::EventSelector = domain.clone().into();
        let back: EventSelector = proto.try_into().unwrap();
        assert_eq!(domain, back);
    }

    #[test]
    fn test_roundtrip_nested_and_or() {
        let domain = EventSelector::stream_equals("stream1")
            .unwrap()
            .and(EventSelector::event_type_equals("type1").unwrap())
            .or(EventSelector::subject_equals("subject1").unwrap());

        let proto: proto::EventSelector = domain.clone().into();
        let back: EventSelector = proto.try_into().unwrap();
        assert_eq!(domain, back);
    }

    #[test]
    fn test_missing_oneof_returns_error() {
        let proto = proto::EventSelector { selector: None };
        let result: Result<EventSelector, _> = proto.try_into();
        assert!(matches!(result, Err(ConversionError::MissingOneof { .. })));
    }

    #[test]
    fn test_no_subject_roundtrip() {
        let domain = EventSelector::no_subject();
        let proto: proto::EventSelector = domain.clone().into();
        let back: EventSelector = proto.try_into().unwrap();
        assert_eq!(domain, back);
    }
}