evidentsource_client/conversions/
constraints.rs

1//! AppendCondition type conversions (DCB specification).
2//!
3//! See: <https://dcb.events/specification/>
4
5use evidentsource_core::domain::{AppendCondition, EventSelector, Range};
6
7use crate::com::evidentsource as proto;
8
9use super::error::ConversionError;
10
11// =============================================================================
12// Domain -> Proto (infallible)
13// =============================================================================
14
15impl From<AppendCondition> for proto::AppendCondition {
16    fn from(condition: AppendCondition) -> Self {
17        use proto::append_condition::Condition;
18
19        let condition_variant = match condition {
20            AppendCondition::Min(selector, revision) => {
21                Condition::Min(proto::append_condition::Min {
22                    selector: Some(selector.into()),
23                    revision,
24                })
25            }
26            AppendCondition::Max(selector, revision) => {
27                Condition::Max(proto::append_condition::Max {
28                    selector: Some(selector.into()),
29                    revision,
30                })
31            }
32            AppendCondition::Range(selector, range) => {
33                Condition::Range(proto::append_condition::Range {
34                    selector: Some(selector.into()),
35                    min: range.min(),
36                    max: range.max(),
37                })
38            }
39        };
40
41        proto::AppendCondition {
42            condition: Some(condition_variant),
43        }
44    }
45}
46
47// =============================================================================
48// Proto -> Domain (fallible)
49// =============================================================================
50
51impl TryFrom<proto::AppendCondition> for AppendCondition {
52    type Error = ConversionError;
53
54    fn try_from(proto: proto::AppendCondition) -> Result<Self, Self::Error> {
55        use proto::append_condition::Condition;
56
57        let condition = proto
58            .condition
59            .ok_or_else(|| ConversionError::missing_oneof("AppendCondition", "condition"))?;
60
61        match condition {
62            Condition::Min(min) => {
63                let selector_proto = min.selector.ok_or_else(|| {
64                    ConversionError::missing_field("AppendCondition.Min", "selector")
65                })?;
66                let selector = EventSelector::try_from(selector_proto)
67                    .map_err(|e| ConversionError::nested("AppendCondition.Min.selector", e))?;
68                Ok(AppendCondition::Min(selector, min.revision))
69            }
70            Condition::Max(max) => {
71                let selector_proto = max.selector.ok_or_else(|| {
72                    ConversionError::missing_field("AppendCondition.Max", "selector")
73                })?;
74                let selector = EventSelector::try_from(selector_proto)
75                    .map_err(|e| ConversionError::nested("AppendCondition.Max.selector", e))?;
76                Ok(AppendCondition::Max(selector, max.revision))
77            }
78            Condition::Range(range) => {
79                let selector_proto = range.selector.ok_or_else(|| {
80                    ConversionError::missing_field("AppendCondition.Range", "selector")
81                })?;
82                let selector = EventSelector::try_from(selector_proto)
83                    .map_err(|e| ConversionError::nested("AppendCondition.Range.selector", e))?;
84
85                let domain_range = Range::new(range.min, range.max).map_err(|_| {
86                    ConversionError::InvalidRange {
87                        min: range.min,
88                        max: range.max,
89                    }
90                })?;
91
92                Ok(AppendCondition::Range(selector, domain_range))
93            }
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    fn make_selector(name: &str) -> EventSelector {
103        EventSelector::event_type_equals(name).unwrap()
104    }
105
106    #[test]
107    fn test_roundtrip_min_condition() {
108        let selector = make_selector("test-type");
109        let domain = AppendCondition::min(selector, 42);
110        let proto: proto::AppendCondition = domain.clone().into();
111        let back: AppendCondition = proto.try_into().unwrap();
112        assert_eq!(domain, back);
113    }
114
115    #[test]
116    fn test_roundtrip_max_condition() {
117        let selector = make_selector("test-type");
118        let domain = AppendCondition::max(selector, 100);
119        let proto: proto::AppendCondition = domain.clone().into();
120        let back: AppendCondition = proto.try_into().unwrap();
121        assert_eq!(domain, back);
122    }
123
124    #[test]
125    fn test_roundtrip_range_condition() {
126        let selector = make_selector("test-type");
127        let range = Range::new(10, 50).unwrap();
128        let domain = AppendCondition::range(selector, range);
129        let proto: proto::AppendCondition = domain.clone().into();
130        let back: AppendCondition = proto.try_into().unwrap();
131        assert_eq!(domain, back);
132    }
133
134    #[test]
135    fn test_invalid_range_returns_error() {
136        let proto = proto::AppendCondition {
137            condition: Some(proto::append_condition::Condition::Range(
138                proto::append_condition::Range {
139                    selector: Some(proto::EventSelector {
140                        selector: Some(proto::event_selector::Selector::Equals(
141                            proto::EventAttribute {
142                                attribute: Some(proto::event_attribute::Attribute::Stream(
143                                    "my-stream".to_string(),
144                                )),
145                            },
146                        )),
147                    }),
148                    min: 100,
149                    max: 50, // Invalid: min > max
150                },
151            )),
152        };
153
154        let result: Result<AppendCondition, _> = proto.try_into();
155        assert!(matches!(result, Err(ConversionError::InvalidRange { .. })));
156    }
157}