use std::{
borrow::Cow,
fmt,
future::{Future, ready},
marker::PhantomData,
num::NonZeroU32,
};
#[cfg(feature = "asyncapi")]
use crate::asyncapi::Bindings;
use crate::{ConnectedBroker, DeclareRetryError, Seekable, Seeker, Subscribe, Subscriber};
pub trait CopyPath: copy_path::Sealed {}
pub(crate) mod copy_path {
use crate::DeclareRetryError;
pub trait Sealed {
fn declared_by_name(broker: &'static str) -> Result<(), DeclareRetryError>;
}
impl Sealed for super::AddressedCopies {
fn declared_by_name(_broker: &'static str) -> Result<(), DeclareRetryError> {
Ok(())
}
}
impl Sealed for super::NamedCopies {
fn declared_by_name(_broker: &'static str) -> Result<(), DeclareRetryError> {
Ok(())
}
}
impl Sealed for super::BrokerMoves {
fn declared_by_name(broker: &'static str) -> Result<(), DeclareRetryError> {
Err(DeclareRetryError::Unsupported { broker })
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct AddressedCopies;
impl CopyPath for AddressedCopies {}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct NamedCopies;
impl CopyPath for NamedCopies {}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct BrokerMoves;
impl CopyPath for BrokerMoves {}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct RetryDeclaration {
max_attempts: Option<NonZeroU32>,
dead_letter: Option<Cow<'static, str>>,
}
impl RetryDeclaration {
#[must_use]
pub const fn new() -> Self {
Self {
max_attempts: None,
dead_letter: None,
}
}
#[must_use]
pub fn with_max_attempts(mut self, attempts: NonZeroU32) -> Self {
self.max_attempts = Some(attempts);
self
}
#[must_use]
pub fn with_dead_letter(mut self, destination: impl Into<Cow<'static, str>>) -> Self {
self.dead_letter = Some(destination.into());
self
}
#[must_use]
pub const fn max_attempts(&self) -> Option<NonZeroU32> {
self.max_attempts
}
#[must_use]
pub fn dead_letter(&self) -> Option<&str> {
self.dead_letter.as_deref()
}
#[must_use]
pub const fn declares_nothing(&self) -> bool {
self.max_attempts.is_none() && self.dead_letter.is_none()
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` does not open a subscription on `{C}`",
label = "not a subscription source for this broker",
note = "a subscriber left unnamed carries `Unnamed<..>` until the mount site names it: \
`b.include(handle.name(\"orders\"))`",
note = "otherwise the source belongs to a different broker than the one being mounted on"
)]
pub trait SubscriptionSource<C: ConnectedBroker> {
type Subscriber: Subscriber;
type Copies: CopyPath;
fn name(&self) -> &str;
fn subscribe(
self,
connected: &C,
) -> impl Future<Output = Result<Self::Subscriber, C::Error>> + Send;
#[must_use]
fn declare_retry(self, declaration: &RetryDeclaration) -> Self
where
Self: Sized,
{
let _ = declaration;
self
}
fn declare_retry_on(
&self,
connected: &C,
declaration: &RetryDeclaration,
) -> Result<(), DeclareRetryError> {
let _ = (connected, declaration);
Ok(())
}
#[cfg(feature = "asyncapi")]
#[must_use]
fn channel_bindings(&self) -> Bindings {
Bindings::new()
}
#[cfg(feature = "asyncapi")]
#[must_use]
fn operation_bindings(&self) -> Bindings {
Bindings::new()
}
#[cfg(feature = "asyncapi")]
#[must_use]
fn message_bindings(&self) -> Bindings {
Bindings::new()
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` declares `Copies = AddressedCopies` but says no address",
label = "no redelivery address for this descriptor on `{C}`",
note = "a descriptor whose retry copies this process publishes to one destination implements \
`RedeliveryAddressed` beside `SubscriptionSource`; one that reads many destinations \
declares `Copies = NamedCopies` and lets the mount site name one"
)]
pub trait RedeliveryAddressed<C: ConnectedBroker>:
SubscriptionSource<C, Copies = AddressedCopies>
{
fn redelivery_address(
&self,
connected: &C,
) -> impl Future<Output = Result<RedeliveryAddress, C::Error>> + Send;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RedeliveryAddress(Cow<'static, str>);
impl RedeliveryAddress {
#[must_use]
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self(name.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for RedeliveryAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone)]
pub struct Name(Cow<'static, str>);
impl Name {
#[must_use]
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self(name.into())
}
}
pub trait FromName {
#[must_use]
fn from_name(name: impl Into<Cow<'static, str>>) -> Self;
}
impl FromName for Name {
fn from_name(name: impl Into<Cow<'static, str>>) -> Self {
Self::new(name)
}
}
pub struct Unnamed<S>(PhantomData<fn() -> S>);
impl<S> Unnamed<S> {
#[must_use]
pub const fn new() -> Self {
Self(PhantomData)
}
#[must_use]
pub fn into_named(self, name: impl Into<Cow<'static, str>>) -> S
where
S: FromName,
{
S::from_name(name)
}
}
impl<S> Default for Unnamed<S> {
fn default() -> Self {
Self::new()
}
}
impl<S> Clone for Unnamed<S> {
fn clone(&self) -> Self {
*self
}
}
impl<S> Copy for Unnamed<S> {}
impl<S> fmt::Debug for Unnamed<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Unnamed").finish_non_exhaustive()
}
}
impl<C: Subscribe> SubscriptionSource<C> for Name {
type Subscriber = C::Subscriber;
type Copies = C::Copies;
fn name(&self) -> &str {
&self.0
}
async fn subscribe(self, connected: &C) -> Result<Self::Subscriber, C::Error> {
connected.subscribe(&self.0).await
}
fn declare_retry_on(
&self,
connected: &C,
declaration: &RetryDeclaration,
) -> Result<(), DeclareRetryError> {
connected.declare_retry(&self.0, declaration)
}
}
impl<C: Subscribe<Copies = AddressedCopies>> RedeliveryAddressed<C> for Name {
fn redelivery_address(
&self,
_connected: &C,
) -> impl Future<Output = Result<RedeliveryAddress, C::Error>> + Send {
ready(Ok(RedeliveryAddress::new(self.0.clone())))
}
}
#[derive(Clone)]
pub struct StartAt<S, P> {
inner: S,
position: P,
}
impl<S, P> StartAt<S, P> {
#[must_use]
pub fn new(source: S, position: P) -> Self {
Self {
inner: source,
position,
}
}
#[must_use]
pub fn map_inner<T>(self, f: impl FnOnce(S) -> T) -> StartAt<T, P> {
StartAt {
inner: f(self.inner),
position: self.position,
}
}
}
impl<S, P> fmt::Debug for StartAt<S, P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StartAt").finish_non_exhaustive()
}
}
#[diagnostic::do_not_recommend]
impl<C, S, P> SubscriptionSource<C> for StartAt<S, P>
where
C: ConnectedBroker,
S: SubscriptionSource<C> + Send + Sync,
S::Subscriber: Seekable,
<S::Subscriber as Seekable>::Seeker: Seeker<Position = P, Error = C::Error>,
P: Send,
{
type Subscriber = S::Subscriber;
type Copies = S::Copies;
fn name(&self) -> &str {
self.inner.name()
}
fn declare_retry(self, declaration: &RetryDeclaration) -> Self {
self.map_inner(|inner| inner.declare_retry(declaration))
}
fn declare_retry_on(
&self,
connected: &C,
declaration: &RetryDeclaration,
) -> Result<(), DeclareRetryError> {
self.inner.declare_retry_on(connected, declaration)
}
async fn subscribe(self, connected: &C) -> Result<Self::Subscriber, C::Error> {
let subscriber = self.inner.subscribe(connected).await?;
subscriber.seeker().seek(self.position).await?;
Ok(subscriber)
}
}
impl<C, S, P> RedeliveryAddressed<C> for StartAt<S, P>
where
Self: SubscriptionSource<C, Copies = AddressedCopies>,
C: ConnectedBroker,
S: RedeliveryAddressed<C> + Send + Sync,
{
fn redelivery_address(
&self,
connected: &C,
) -> impl Future<Output = Result<RedeliveryAddress, C::Error>> + Send {
self.inner.redelivery_address(connected)
}
}
#[cfg(all(test, feature = "memory"))]
mod tests {
use super::*;
use crate::memory::{
ConnectedMemoryBroker, MemoryBroker, MemoryPosition, MemorySource, Retaining, Retention,
};
use crate::{Broker, Buffered, nonzero};
fn clone_of<T: Clone>(value: &T) -> T {
value.clone()
}
#[test]
fn an_unnamed_placeholder_builds_its_kind_once_the_name_arrives() {
let placeholder = Unnamed::<Name>::default();
assert!(format!("{placeholder:?}").contains("Unnamed"));
let named: Name = clone_of(&placeholder).into_named("orders");
assert_eq!(
SubscriptionSource::<ConnectedMemoryBroker>::name(&named),
"orders"
);
}
#[test]
fn a_start_position_decorates_the_source_it_wraps() {
let source = StartAt::new(MemorySource::new("orders"), MemoryPosition::start());
assert_eq!(
SubscriptionSource::<ConnectedMemoryBroker<Retaining>>::name(&source),
"orders"
);
assert!(format!("{source:?}").contains("StartAt"));
}
#[tokio::test]
async fn a_source_reports_the_address_its_broker_gives_the_name() {
let connected = MemoryBroker::new()
.connect()
.await
.expect("the in-memory broker connects");
let address = RedeliveryAddress::new("orders");
assert_eq!(address.as_str(), "orders");
assert_eq!(address.to_string(), "orders");
assert_eq!(
Name::new("orders")
.redelivery_address(&connected)
.await
.expect("the in-memory broker answers without a lookup"),
address.clone(),
);
assert_eq!(
Buffered::new(MemorySource::new("orders"))
.redelivery_address(&connected)
.await
.expect("client-side batching changes no address"),
address.clone(),
);
let retaining = MemoryBroker::retaining(Retention::Messages(nonzero!(8)))
.connect()
.await
.expect("the in-memory broker connects");
assert_eq!(
StartAt::new(MemorySource::new("orders"), MemoryPosition::start())
.redelivery_address(&retaining)
.await
.expect("a start position changes no address"),
address,
);
}
#[test]
fn a_start_position_hands_back_the_source_it_wraps() {
let renamed =
StartAt::new(MemorySource::new("orders"), MemoryPosition::start()).map_inner(|inner| {
assert_eq!(
SubscriptionSource::<ConnectedMemoryBroker<Retaining>>::name(&inner),
"orders",
);
MemorySource::new("orders-7")
});
assert_eq!(
SubscriptionSource::<ConnectedMemoryBroker<Retaining>>::name(&renamed),
"orders-7",
);
}
}