reliar-inbox 0.1.0

Transactional inbox deduplication: InboxStore/InboxHandler contracts, claim/complete/fail/purge semantics (no storage or transport dependency).
Documentation
//! [`InboxMessage`]: what the inbox copies off an inbound envelope (ADR 0042 A.2.2).

use reliar_core::{ConversationId, CorrelationId, Envelope, MessageId, MessageType};

/// What the inbox copies off an inbound envelope: its identity, its type, and the trace fields
/// that let an operator follow it and let the handler's outbound events inherit the
/// conversation. **No payload** — the inbox deduplicates, it does not archive.
///
/// Borrowed and `Copy`, so passing it by value costs nothing. Taken by both
/// [`crate::InboxStore::claim`] and [`crate::InboxStore::fail`]: `fail` **creates** the row when
/// the handler's rollback removed it, so it must supply every `NOT NULL` column — and the row it
/// creates is the dead row an operator most needs to identify by type.
///
/// There is deliberately **no `From<&Envelope<T>>` impl and no `impl Into<InboxMessage<'_>>`
/// parameter bound** — ADR 0037 Amendment A settled that family for `enqueue`: a generic on a
/// required trait method taxes every implementor and the inference reads as magic. One named
/// conversion, [`Self::from_envelope`], called explicitly.
///
/// `Message` requires `Serialize + DeserializeOwned` unconditionally (`reliar-core`); this
/// crate's own `serde` feature only gates the *library's* optional dependency, so the doctest
/// below draws `serde` from the dev-dependency instead and compiles under every feature
/// combination, including `--no-default-features`.
/// ```
/// use reliar_core::{Envelope, Message};
/// use reliar_inbox::InboxMessage;
///
/// #[derive(serde::Serialize, serde::Deserialize)]
/// struct OrderCreated;
/// impl Message for OrderCreated {
///     const TYPE: &'static str = "orders.created";
///     const VERSION: u16 = 1;
/// }
///
/// let envelope = Envelope::builder(OrderCreated).build();
/// let message = InboxMessage::from_envelope(&envelope);
///
/// assert_eq!(message.id, envelope.id);
/// assert_eq!(message.conversation_id, envelope.metadata.correlation.conversation_id);
/// ```
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct InboxMessage<'a> {
    /// The message's own id — the deduplication key's second half.
    pub id: MessageId,

    /// Stored split as `message_type` + `message_version`, exactly as the outbox stores it, so
    /// [`crate::InboxStore::find`] can rehydrate through [`MessageType::from_parts`].
    pub message_type: &'a MessageType,

    /// Stored `NOT NULL`. A message that arrived without one carries the
    /// [`ConversationId::UNSET`] nil sentinel and the sentinel is what is stored — Reliar never
    /// mints a conversation id here (ADR 0038).
    pub conversation_id: ConversationId,

    /// The caller's business correlation, if the sender set one.
    pub correlation_id: Option<&'a CorrelationId>,

    /// **The inbound envelope's own causation** — what caused the message being handled — not
    /// this row's `message_id`. An outbound event's causation is set by the handler
    /// (`.causation(inbound.id)`), not read off this column; a column definitionally equal to
    /// `message_id` would mislead every operator who read both (ADR 0042 A.2.3).
    pub causation_id: Option<MessageId>,
}

impl<'a> InboxMessage<'a> {
    /// The minimum: identity and type. Conversation defaults to [`ConversationId::UNSET`], the
    /// two optional ids to `None`.
    ///
    /// ```
    /// use reliar_core::{MessageId, MessageType};
    /// use reliar_inbox::InboxMessage;
    ///
    /// let message_type = MessageType::new("orders.created", 1);
    /// let message = InboxMessage::new(MessageId::new(), &message_type);
    /// assert!(message.correlation_id.is_none());
    /// ```
    #[must_use]
    pub const fn new(id: MessageId, message_type: &'a MessageType) -> Self {
        Self {
            id,
            message_type,
            conversation_id: ConversationId::UNSET,
            correlation_id: None,
            causation_id: None,
        }
    }

    /// The common path — reads the five values off an envelope's `id`, `message_type` and
    /// `metadata.correlation`. Works for `Envelope<T>` and for `SerializedEnvelope` alike.
    ///
    /// `serde` here comes from the dev-dependency — see [`Self`]'s own doctest for why.
    /// ```
    /// use reliar_core::{Envelope, Message};
    /// use reliar_inbox::InboxMessage;
    ///
    /// # #[derive(serde::Serialize, serde::Deserialize)]
    /// # struct Ping;
    /// # impl Message for Ping { const TYPE: &'static str = "ping"; const VERSION: u16 = 1; }
    /// let envelope = Envelope::builder(Ping).build();
    /// let message = InboxMessage::from_envelope(&envelope);
    /// assert_eq!(message.id, envelope.id);
    /// ```
    #[must_use]
    pub fn from_envelope<T>(envelope: &'a Envelope<T>) -> Self {
        Self {
            id: envelope.id,
            message_type: &envelope.message_type,
            conversation_id: envelope.metadata.correlation.conversation_id,
            correlation_id: envelope.metadata.correlation.correlation_id.as_ref(),
            causation_id: envelope.metadata.correlation.causation_id,
        }
    }

    /// Sets [`Self::conversation_id`].
    ///
    /// ```
    /// use reliar_core::{ConversationId, MessageId, MessageType};
    /// use reliar_core::uuid::Uuid;
    /// use reliar_inbox::InboxMessage;
    ///
    /// let message_type = MessageType::new("orders.created", 1);
    /// let conversation_id = ConversationId::from_uuid(Uuid::now_v7());
    /// let message = InboxMessage::new(MessageId::new(), &message_type).conversation(conversation_id);
    /// assert_eq!(message.conversation_id, conversation_id);
    /// ```
    #[must_use]
    pub const fn conversation(mut self, conversation_id: ConversationId) -> Self {
        self.conversation_id = conversation_id;

        self
    }

    /// Sets [`Self::correlation_id`].
    ///
    /// ```
    /// use reliar_core::{CorrelationId, MessageId, MessageType};
    /// use reliar_inbox::InboxMessage;
    ///
    /// let message_type = MessageType::new("orders.created", 1);
    /// let correlation_id = CorrelationId::parse("checkout-42").unwrap();
    /// let message = InboxMessage::new(MessageId::new(), &message_type).correlation(&correlation_id);
    /// assert_eq!(message.correlation_id, Some(&correlation_id));
    /// ```
    #[must_use]
    pub const fn correlation(mut self, correlation_id: &'a CorrelationId) -> Self {
        self.correlation_id = Some(correlation_id);

        self
    }

    /// Sets [`Self::causation_id`].
    ///
    /// ```
    /// use reliar_core::{MessageId, MessageType};
    /// use reliar_inbox::InboxMessage;
    ///
    /// let message_type = MessageType::new("orders.created", 1);
    /// let causation_id = MessageId::new();
    /// let message = InboxMessage::new(MessageId::new(), &message_type).causation(causation_id);
    /// assert_eq!(message.causation_id, Some(causation_id));
    /// ```
    #[must_use]
    pub const fn causation(mut self, causation_id: MessageId) -> Self {
        self.causation_id = Some(causation_id);

        self
    }
}