mod commands;
mod descriptors;
mod forms;
mod message;
mod rounds;
mod source;
#[cfg(all(test, feature = "testing"))]
mod tests;
mod window;
use std::fmt::{Debug, Formatter};
use std::marker::PhantomData;
use std::ops::Deref;
use fred::bytes_utils::Str;
use fred::clients::{Client, Pipeline};
use fred::error::Error;
use fred::interfaces::ClientLike;
use fred::types::{ClusterHash, CustomCommand, MultipleKeys, MultipleValues, Value};
use ruststream::StartAt;
use ruststream::runtime::{
Declared, ForReply, Outgoing, PublishContext, PublishTransform, Reads, SubscriberBuilder,
SubscriberSettings,
};
use crate::partition::RedisPublishOptions;
pub use descriptors::{
RedisListAtomic, RedisListPipeline, RedisPubSubAtomic, RedisPubSubPatternAtomic,
RedisPubSubPatternPipeline, RedisPubSubPipeline, RedisStreamAtomic, RedisStreamPipeline,
};
pub(crate) use forms::{ListForm, PubSubForm, StreamForm};
pub use message::RoundMessage;
pub(crate) use rounds::Rounds;
pub use source::PipelinedSubscriber;
pub(crate) use window::{Form, Owner, Round, Segment, Window};
#[must_use]
pub struct Pipelined<Descriptor, Mode = Plain> {
descriptor: Descriptor,
mode: PhantomData<fn() -> Mode>,
}
impl<Descriptor: Clone, Mode> Clone for Pipelined<Descriptor, Mode> {
fn clone(&self) -> Self {
Self {
descriptor: self.descriptor.clone(),
mode: PhantomData,
}
}
}
impl<Descriptor: Debug, Mode: WindowMode> Debug for Pipelined<Descriptor, Mode> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pipelined")
.field("descriptor", &self.descriptor)
.field("atomic", &Mode::ATOMIC)
.finish()
}
}
impl<Descriptor, Mode> Pipelined<Descriptor, Mode> {
pub(crate) const fn wrap(descriptor: Descriptor) -> Self {
Self {
descriptor,
mode: PhantomData,
}
}
pub(crate) const fn descriptor(&self) -> &Descriptor {
&self.descriptor
}
pub(crate) fn into_descriptor(self) -> Descriptor {
self.descriptor
}
pub(crate) fn map(self, step: impl FnOnce(Descriptor) -> Descriptor) -> Self {
Self::wrap(step(self.descriptor))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Plain;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Atomic;
mod sealed {
pub trait Sealed {}
impl Sealed for super::Plain {}
impl Sealed for super::Atomic {}
}
pub trait WindowMode: sealed::Sealed + Send + Sync + 'static {
const ATOMIC: bool;
}
impl WindowMode for Plain {
const ATOMIC: bool = false;
}
impl WindowMode for Atomic {
const ATOMIC: bool = true;
}
mod windowable {
#[diagnostic::on_unimplemented(
message = "`.pipeline()` opens a window on a `RedisStream`, `RedisList`, `RedisPubSub` or \
`RedisPubSubPattern` subscription, and this one is `{Self}`",
label = "no window opens here",
note = "a subscription has one window: chain `.pipeline()` once, on the descriptor the \
attribute names"
)]
pub trait Base {}
impl Base for crate::RedisStream {}
impl Base for crate::RedisList {}
impl Base for crate::RedisPubSub {}
impl Base for crate::RedisPubSubPattern {}
}
#[diagnostic::on_unimplemented(
message = "`.pipeline()` opens a window on a subscription of ruststream-fred, and `{Self}` is not one",
label = "no window opens on this source",
note = "name a `RedisStream`, `RedisList`, `RedisPubSub` or `RedisPubSubPattern` in \
`#[subscriber(..)]`, and chain `.pipeline()` once"
)]
pub trait OpensWindow {
type Pipelined;
fn open_window(self) -> Self::Pipelined;
}
impl<Descriptor: windowable::Base> OpensWindow for Descriptor {
type Pipelined = Pipelined<Descriptor, Plain>;
fn open_window(self) -> Self::Pipelined {
Pipelined::wrap(self)
}
}
impl<Source: OpensWindow, Position> OpensWindow for StartAt<Source, Position> {
type Pipelined = StartAt<Source::Pipelined, Position>;
fn open_window(self) -> Self::Pipelined {
self.map_inner(OpensWindow::open_window)
}
}
#[diagnostic::on_unimplemented(
message = "`.atomic()` needs `.pipeline()` first",
label = "this subscription has no pipeline",
note = "write `.pipeline().atomic()`: a transaction is one segment of the window's pipeline, \
and a subscription without a window has no pipeline to put it in"
)]
pub trait Windowed {
type Atomic;
fn into_atomic(self) -> Self::Atomic;
}
impl<Descriptor> Windowed for Pipelined<Descriptor, Plain> {
type Atomic = Pipelined<Descriptor, Atomic>;
fn into_atomic(self) -> Self::Atomic {
Pipelined::wrap(self.descriptor)
}
}
impl<Source: Windowed, Position> Windowed for StartAt<Source, Position> {
type Atomic = StartAt<Source::Atomic, Position>;
fn into_atomic(self) -> Self::Atomic {
self.map_inner(Windowed::into_atomic)
}
}
#[diagnostic::on_unimplemented(
message = "`.pipeline()` opens a window on a subscription of ruststream-fred, and this \
registration is not one",
label = "no window opens on this registration",
note = "name a `RedisStream`, `RedisList`, `RedisPubSub` or `RedisPubSubPattern` in \
`#[subscriber(..)]`, and chain `.pipeline()` once"
)]
pub trait PipelineStep {
type Out;
fn apply_pipeline(self) -> Self::Out;
}
impl<Def, Source, State, DefCodec> PipelineStep for SubscriberBuilder<Def, Source, State, DefCodec>
where
Def: Declared,
Source: OpensWindow,
{
type Out = SubscriberBuilder<Def, Source::Pipelined, State, DefCodec>;
fn apply_pipeline(self) -> Self::Out {
self.map_source(OpensWindow::open_window)
}
}
#[diagnostic::on_unimplemented(
message = "`.atomic()` needs `.pipeline()` first",
label = "this subscription has no pipeline",
note = "write `.pipeline().atomic()`: a transaction is one segment of the window's pipeline, \
and a subscription without a window has no pipeline to put it in"
)]
pub trait AtomicStep {
type Out;
fn apply_atomic(self) -> Self::Out;
}
impl<Def, Source, State, DefCodec> AtomicStep for SubscriberBuilder<Def, Source, State, DefCodec>
where
Def: Declared,
Source: Windowed,
{
type Out = SubscriberBuilder<Def, Source::Atomic, State, DefCodec>;
fn apply_atomic(self) -> Self::Out {
self.map_source(Windowed::into_atomic)
}
}
pub trait RedisPipelineSteps: Declared {
fn pipeline(self) -> <Self::Settings as PipelineStep>::Out
where
Self::Settings: PipelineStep,
{
self.declare().apply_pipeline()
}
fn atomic(self) -> <Self::Settings as AtomicStep>::Out
where
Self::Settings: AtomicStep,
{
self.declare().apply_atomic()
}
}
impl<D: Declared> RedisPipelineSteps for D {}
mod bindable {
pub trait Named {
fn round_name(&self) -> u64;
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a publisher of ruststream-fred, so it cannot join a delivery's round",
label = "this slot publishes through another broker",
note = "bind a slot whose policy is one of this crate's (`stream::Publish`, `list::Publish`, \
`pubsub::Publish`), and bound the slot parameter as `Out<impl Bindable>`"
)]
pub trait Bindable: ruststream::Publisher + bindable::Named {}
impl<T: ruststream::Publisher + bindable::Named> Bindable for T {}
impl bindable::Named for crate::RedisPublisher {
fn round_name(&self) -> u64 {
self.round_name()
}
}
impl bindable::Named for crate::RedisListPublisher {
fn round_name(&self) -> u64 {
self.round_name()
}
}
impl bindable::Named for crate::RedisPubSubPublisher {
fn round_name(&self) -> u64 {
self.round_name()
}
}
impl bindable::Named for crate::RedisDefaultPublisher {
fn round_name(&self) -> u64 {
self.round_name()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct InRound;
impl<C> PublishTransform<ForReply<C>, RedisPublishOptions> for InRound {
type Destination = Reads;
fn apply(
&self,
_out: &mut Outgoing<'_>,
options: &mut Option<RedisPublishOptions>,
_cx: &PublishContext<'_, C>,
) {
options
.get_or_insert_with(RedisPublishOptions::default)
.join_round = true;
}
}
#[derive(Clone)]
pub struct RedisPipeline {
round: Round,
}
impl Debug for RedisPipeline {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RedisPipeline").finish_non_exhaustive()
}
}
impl RedisPipeline {
pub(crate) const fn new(round: Round) -> Self {
Self { round }
}
async fn segment(&self) -> Result<Segment, Error> {
self.round.segment().await
}
pub fn bind<'o, O>(&self, out: &'o O) -> &'o O
where
O: Deref,
O::Target: Bindable,
{
self.round.bind(bindable::Named::round_name(&**out));
out
}
pub async fn evalsha<S, K, V>(&self, hash: S, keys: K, args: V) -> Result<(), Error>
where
S: Into<Str> + Send,
K: Into<MultipleKeys> + Send,
V: TryInto<MultipleValues> + Send,
V::Error: Into<Error> + Send,
{
let args = args.try_into().map_err(Into::into)?;
self.script("EVALSHA", hash.into(), keys.into(), args).await
}
pub async fn eval<S, K, V>(&self, script: S, keys: K, args: V) -> Result<(), Error>
where
S: Into<Str> + Send,
K: Into<MultipleKeys> + Send,
V: TryInto<MultipleValues> + Send,
V::Error: Into<Error> + Send,
{
let args = args.try_into().map_err(Into::into)?;
self.script("EVAL", script.into(), keys.into(), args).await
}
async fn script(
&self,
command: &'static str,
body: Str,
keys: MultipleKeys,
args: Value,
) -> Result<(), Error> {
let keys = keys.inner();
let hash = keys.first().map_or(ClusterHash::Random, |key| {
ClusterHash::Custom(key.cluster_hash())
});
let mut values = Vec::with_capacity(2 + keys.len());
values.push(Value::from(body));
values.push(Value::from(i64::try_from(keys.len()).unwrap_or(i64::MAX)));
values.extend(keys.into_iter().map(Value::from));
match args {
Value::Array(args) => values.extend(args),
Value::Null => {}
arg => values.push(arg),
}
self.custom(CustomCommand::new_static(command, hash, false), values)
.await
}
pub async fn custom<T>(&self, command: CustomCommand, args: Vec<T>) -> Result<(), Error>
where
T: TryInto<Value> + Send,
T::Error: Into<Error> + Send,
{
match self.segment().await? {
Segment::Plain(segment) => segment.custom::<(), T>(command, args).await,
Segment::Atomic(segment) => segment.custom::<(), T>(command, args).await,
}
}
}
pub(crate) fn pipeline_on(client: &Client) -> Pipeline<Client> {
client.pipeline()
}