use super::stable_id::{validate_stable_message_id, StableMessageIdError};
use super::{FailurePolicy, Message};
pub trait InboxHook {
fn consumer_name(&self) -> &str;
}
#[derive(Clone)]
pub enum ConsumerDeliveryMode<I> {
Idempotent,
Inbox(I),
}
#[allow(clippy::derivable_impls)]
impl<I> Default for ConsumerDeliveryMode<I> {
fn default() -> Self {
ConsumerDeliveryMode::Idempotent
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NoInbox {}
#[derive(Clone)]
pub struct RunOptions<I = NoInbox> {
pub delivery_mode: ConsumerDeliveryMode<I>,
pub failure_policy: FailurePolicy,
}
impl<I> Default for RunOptions<I> {
fn default() -> Self {
Self {
delivery_mode: ConsumerDeliveryMode::default(),
failure_policy: FailurePolicy::default(),
}
}
}
impl RunOptions<NoInbox> {
pub fn idempotent() -> Self {
Self::default()
}
}
impl<I> RunOptions<I> {
pub fn inbox(hook: I) -> Self {
Self {
delivery_mode: ConsumerDeliveryMode::Inbox(hook),
failure_policy: FailurePolicy::default(),
}
}
pub fn with_failure_policy(mut self, policy: FailurePolicy) -> Self {
self.failure_policy = policy;
self
}
pub fn is_idempotent(&self) -> bool {
matches!(self.delivery_mode, ConsumerDeliveryMode::Idempotent)
}
pub fn requires_stable_id(&self) -> bool {
matches!(self.delivery_mode, ConsumerDeliveryMode::Inbox(_))
}
pub fn validate_message_id<'m>(
&self,
message: &'m Message,
) -> Result<Option<&'m str>, StableMessageIdError> {
if self.requires_stable_id() {
validate_stable_message_id(message.id()).map(Some)
} else {
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bus::MessageKind;
struct FakeInbox {
consumer: &'static str,
}
impl InboxHook for FakeInbox {
fn consumer_name(&self) -> &str {
self.consumer
}
}
fn message_with_id(id: Option<&str>) -> Message {
let mut message = Message::new("seat.reserved", MessageKind::Event, b"{}".to_vec());
if let Some(id) = id {
message = message.with_id(id);
}
message
}
#[test]
fn default_run_options_are_idempotent_with_default_policy() {
let options = RunOptions::<NoInbox>::default();
assert!(options.is_idempotent());
assert!(!options.requires_stable_id());
assert_eq!(options.failure_policy, FailurePolicy::default());
}
#[test]
fn idempotent_constructor_matches_default() {
let options = RunOptions::idempotent();
assert!(options.is_idempotent());
assert!(!options.requires_stable_id());
}
#[test]
fn inbox_mode_requires_a_stable_id_and_keeps_the_hook() {
let options = RunOptions::inbox(FakeInbox {
consumer: "seat-projection",
});
assert!(!options.is_idempotent());
assert!(options.requires_stable_id());
match &options.delivery_mode {
ConsumerDeliveryMode::Inbox(hook) => {
assert_eq!(hook.consumer_name(), "seat-projection")
}
ConsumerDeliveryMode::Idempotent => panic!("expected inbox mode"),
}
}
#[test]
fn with_failure_policy_overrides_the_default() {
let options = RunOptions::idempotent().with_failure_policy(FailurePolicy::Stop);
assert_eq!(options.failure_policy, FailurePolicy::Stop);
}
#[test]
fn idempotent_mode_needs_no_dedup_key() {
let options = RunOptions::idempotent();
assert_eq!(
options.validate_message_id(&message_with_id(None)),
Ok(None)
);
assert_eq!(
options.validate_message_id(&message_with_id(Some("evt-1"))),
Ok(None)
);
}
#[test]
fn inbox_mode_returns_the_validated_dedup_key() {
let options = RunOptions::inbox(FakeInbox { consumer: "c" });
assert_eq!(
options.validate_message_id(&message_with_id(None)),
Err(StableMessageIdError::Missing)
);
assert_eq!(
options.validate_message_id(&message_with_id(Some(" "))),
Err(StableMessageIdError::Empty)
);
assert_eq!(
options.validate_message_id(&message_with_id(Some("evt-1"))),
Ok(Some("evt-1"))
);
}
}