evidentsource-client 1.0.0-rc1

Rust client for the EvidentSource event sourcing platform
Documentation
//! AppendCondition type conversions (DCB specification).
//!
//! See: <https://dcb.events/specification/>

use evidentsource_core::domain::{AppendCondition, EventSelector, Range};

use crate::com::evidentsource as proto;

use super::error::ConversionError;

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

impl From<AppendCondition> for proto::AppendCondition {
    fn from(condition: AppendCondition) -> Self {
        use proto::append_condition::Condition;

        let condition_variant = match condition {
            AppendCondition::Min(selector, revision) => {
                Condition::Min(proto::append_condition::Min {
                    selector: Some(selector.into()),
                    revision,
                })
            }
            AppendCondition::Max(selector, revision) => {
                Condition::Max(proto::append_condition::Max {
                    selector: Some(selector.into()),
                    revision,
                })
            }
            AppendCondition::Range(selector, range) => {
                Condition::Range(proto::append_condition::Range {
                    selector: Some(selector.into()),
                    min: range.min(),
                    max: range.max(),
                })
            }
        };

        proto::AppendCondition {
            condition: Some(condition_variant),
        }
    }
}

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

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

    fn try_from(proto: proto::AppendCondition) -> Result<Self, Self::Error> {
        use proto::append_condition::Condition;

        let condition = proto
            .condition
            .ok_or_else(|| ConversionError::missing_oneof("AppendCondition", "condition"))?;

        match condition {
            Condition::Min(min) => {
                let selector_proto = min.selector.ok_or_else(|| {
                    ConversionError::missing_field("AppendCondition.Min", "selector")
                })?;
                let selector = EventSelector::try_from(selector_proto)
                    .map_err(|e| ConversionError::nested("AppendCondition.Min.selector", e))?;
                Ok(AppendCondition::Min(selector, min.revision))
            }
            Condition::Max(max) => {
                let selector_proto = max.selector.ok_or_else(|| {
                    ConversionError::missing_field("AppendCondition.Max", "selector")
                })?;
                let selector = EventSelector::try_from(selector_proto)
                    .map_err(|e| ConversionError::nested("AppendCondition.Max.selector", e))?;
                Ok(AppendCondition::Max(selector, max.revision))
            }
            Condition::Range(range) => {
                let selector_proto = range.selector.ok_or_else(|| {
                    ConversionError::missing_field("AppendCondition.Range", "selector")
                })?;
                let selector = EventSelector::try_from(selector_proto)
                    .map_err(|e| ConversionError::nested("AppendCondition.Range.selector", e))?;

                let domain_range = Range::new(range.min, range.max).map_err(|_| {
                    ConversionError::InvalidRange {
                        min: range.min,
                        max: range.max,
                    }
                })?;

                Ok(AppendCondition::Range(selector, domain_range))
            }
        }
    }
}

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

    fn make_selector(name: &str) -> EventSelector {
        EventSelector::event_type_equals(name).unwrap()
    }

    #[test]
    fn test_roundtrip_min_condition() {
        let selector = make_selector("test-type");
        let domain = AppendCondition::min(selector, 42);
        let proto: proto::AppendCondition = domain.clone().into();
        let back: AppendCondition = proto.try_into().unwrap();
        assert_eq!(domain, back);
    }

    #[test]
    fn test_roundtrip_max_condition() {
        let selector = make_selector("test-type");
        let domain = AppendCondition::max(selector, 100);
        let proto: proto::AppendCondition = domain.clone().into();
        let back: AppendCondition = proto.try_into().unwrap();
        assert_eq!(domain, back);
    }

    #[test]
    fn test_roundtrip_range_condition() {
        let selector = make_selector("test-type");
        let range = Range::new(10, 50).unwrap();
        let domain = AppendCondition::range(selector, range);
        let proto: proto::AppendCondition = domain.clone().into();
        let back: AppendCondition = proto.try_into().unwrap();
        assert_eq!(domain, back);
    }

    #[test]
    fn test_invalid_range_returns_error() {
        let proto = proto::AppendCondition {
            condition: Some(proto::append_condition::Condition::Range(
                proto::append_condition::Range {
                    selector: Some(proto::EventSelector {
                        selector: Some(proto::event_selector::Selector::Equals(
                            proto::EventAttribute {
                                attribute: Some(proto::event_attribute::Attribute::Stream(
                                    "my-stream".to_string(),
                                )),
                            },
                        )),
                    }),
                    min: 100,
                    max: 50, // Invalid: min > max
                },
            )),
        };

        let result: Result<AppendCondition, _> = proto.try_into();
        assert!(matches!(result, Err(ConversionError::InvalidRange { .. })));
    }
}