Skip to main content

ruststream_fred/
context.rs

1//! Per-delivery and per-batch context fields exposed to handlers.
2//!
3//! A handler types its context parameter as `Context<'_, StreamContext>`, or binds one field as a
4//! parameter with the core's `Ctx<K>` extractor and a key from [`keys`].
5//!
6//! - [StreamContext][context]: [`StreamContext`], a stream delivery's entry id, position,
7//!   consumer group and seek handle, under [`keys::EntryId`], [`keys::Position`],
8//!   [`keys::ConsumerGroup`] and [`keys::SeekHandle`].
9//! - [ConsumerGroup][group]: [`keys::ConsumerGroup`], the group a stream subscription reads
10//!   through.
11//! - [StreamBatchContext][batch]: [`StreamBatchContext`], a stream batch handler's context.
12//! - [PubSubContext][pubsub]: [`PubSubContext`], the channel a message arrived on, under
13//!   [`keys::Channel`] and [`keys::FromPattern`].
14//! - [FredPool][pool]: [`keys::FredPool`] and [`PoolContext`], the connection pool on every form.
15//! - [Commands][pipeline]: [`keys::Pipeline`] and [`PipelineContext`], the round of a
16//!   `.pipeline()` subscription.
17//! - [Counts][counts]: the delivery count and idle time of the claiming modes ride the
18//!   [`DELIVERY_COUNT_HEADER`](crate::DELIVERY_COUNT_HEADER) and
19//!   [`IDLE_MS_HEADER`](crate::IDLE_MS_HEADER) headers.
20//!
21//! [context]: https://powersemmi.github.io/ruststream-fred/latest/components/context/
22//! [group]: https://powersemmi.github.io/ruststream-fred/latest/components/context/consumer-group/
23//! [batch]: https://powersemmi.github.io/ruststream-fred/latest/components/context/batch-context/
24//! [pubsub]: https://powersemmi.github.io/ruststream-fred/latest/components/context/pubsub-context/
25//! [pool]: https://powersemmi.github.io/ruststream-fred/latest/components/context/fred-pool/
26//! [pipeline]: https://powersemmi.github.io/ruststream-fred/latest/components/pipelining/commands/
27//! [counts]: https://powersemmi.github.io/ruststream-fred/latest/components/acknowledgement/counts/
28
29use std::convert::Infallible;
30
31use fred::clients::Pool;
32use ruststream::runtime::{Context, Ctx, FromContext};
33use ruststream::{BuildBatchContext, BuildContext, Field};
34
35use crate::list::RedisListMessage;
36use crate::message::RedisMessage;
37use crate::pipeline::{Form, RedisPipeline, RoundMessage};
38use crate::pubsub::RedisPubSubMessage;
39use crate::seek::{EntryId, RedisGroupPosition, RedisGroupSeeker};
40
41/// Per-delivery context for a Redis Streams delivery ([`RedisMessage`]).
42///
43/// Built once per delivery from the message. Read its fields by [`keys`] key off a
44/// [`ruststream::runtime::Context`], or bind one as a handler parameter with the core
45/// `Ctx<K>` extractor. A body that repositions its group names this type as its context and needs
46/// nothing else: the [`keys::SeekHandle`] key carries the live handle.
47///
48/// # Examples
49///
50/// ```
51/// # mod demo {
52/// use ruststream_fred::stream::prelude::*;
53/// # use serde::Deserialize;
54/// # #[derive(Deserialize)]
55/// # struct Order {
56/// #     id: u64,
57/// # }
58///
59/// /// Skips the group past a region the producer marked poisoned, and logs where it was.
60/// #[subscriber(RedisStream::new("orders").group("workers"))]
61/// async fn work(order: &Order, ctx: &mut Context<'_, StreamContext>) -> HandlerOutcome {
62///     if order.id != 0 {
63///         return HandlerOutcome::ack();
64///     }
65///     tracing::warn!(entry = %ctx.context(keys::EntryId), "poison marker");
66///     match ctx.context(keys::SeekHandle).seek(RedisGroupPosition::end()).await {
67///         Ok(()) => HandlerOutcome::ack(),
68///         Err(_) => HandlerOutcome::retry(),
69///     }
70/// }
71/// # }
72/// ```
73#[derive(Debug, Clone)]
74pub struct StreamContext {
75    entry: EntryId,
76    position: RedisGroupPosition,
77    // Carries the stream key and the consumer group too, so the group needs no second copy.
78    seeker: RedisGroupSeeker,
79}
80
81impl StreamContext {
82    /// The stream entry id (for example `1700000000000-0`) this delivery was read at.
83    #[must_use]
84    pub const fn entry_id(&self) -> EntryId {
85        self.entry
86    }
87
88    /// The group cursor that redelivers this entry, as [`Positioned`](ruststream::Positioned)
89    /// reports it: the cursor is exclusive, so it sits one id below the entry's own.
90    #[must_use]
91    pub const fn position(&self) -> RedisGroupPosition {
92        self.position
93    }
94
95    /// The consumer group this delivery was read through.
96    #[must_use]
97    pub fn consumer_group(&self) -> &str {
98        self.seeker.group()
99    }
100
101    /// The handle repositioning this delivery's consumer group.
102    ///
103    /// The cursor belongs to the group, so a seek through it moves every consumer of that group;
104    /// see [`RedisGroupSeeker`].
105    #[must_use]
106    pub const fn seeker(&self) -> &RedisGroupSeeker {
107        &self.seeker
108    }
109
110    /// The broker's connection pool, for a command whose answer the handler needs now.
111    #[must_use]
112    pub const fn pool(&self) -> &Pool {
113        self.seeker.pool()
114    }
115}
116
117impl BuildContext<RedisMessage> for StreamContext {
118    fn build(msg: &RedisMessage) -> Self {
119        Self {
120            entry: msg.entry_id(),
121            position: ruststream::Positioned::position(msg),
122            // A clone of the subscription's pre-minted handle: reference-count bumps only,
123            // nothing allocated per delivery.
124            seeker: msg.seeker().clone(),
125        }
126    }
127}
128
129/// Batch context for a batched Redis Streams subscription: what the whole subscription shares.
130///
131/// The runtime builds one per dispatched batch from the batch's first delivery, and a batch body
132/// reads it by [`keys`] key with `ctx.context(..)`. Per-delivery data (the entry id, the position
133/// that redelivers one entry) has no place here, because a batch spans many deliveries: it rides
134/// the batch's own elements instead, read off each element's typed header contract. Keeping this a
135/// separate type from [`StreamContext`] is what rejects a batch body asking for per-delivery fields
136/// at compile time.
137///
138/// # Examples
139///
140/// ```
141/// # mod demo {
142/// use ruststream_fred::stream::prelude::*;
143/// # use serde::Deserialize;
144/// # #[derive(Deserialize)]
145/// # struct Order {
146/// #     id: u64,
147/// # }
148///
149/// /// A batch that saw the poison marker rewinds the group once the batch is settled.
150/// #[subscriber(RedisStream::new("orders").group("workers"))]
151/// async fn work(
152///     batch: &[Order],
153///     ctx: &mut Context<'_, StreamBatchContext>,
154/// ) -> HandlerOutcome {
155///     if batch.iter().any(|order| order.id == 0)
156///         && ctx
157///             .context(keys::SeekHandle)
158///             .seek(RedisGroupPosition::beginning())
159///             .await
160///             .is_err()
161///     {
162///         return HandlerOutcome::retry();
163///     }
164///     HandlerOutcome::ack()
165/// }
166/// # }
167/// ```
168#[derive(Debug, Clone)]
169pub struct StreamBatchContext {
170    seeker: RedisGroupSeeker,
171}
172
173impl StreamBatchContext {
174    /// The consumer group every delivery of this batch was read through.
175    #[must_use]
176    pub fn consumer_group(&self) -> &str {
177        self.seeker.group()
178    }
179
180    /// The handle repositioning this subscription's consumer group.
181    #[must_use]
182    pub const fn seeker(&self) -> &RedisGroupSeeker {
183        &self.seeker
184    }
185
186    /// The broker's connection pool, for a command whose answer the batch needs now.
187    #[must_use]
188    pub const fn pool(&self) -> &Pool {
189        self.seeker.pool()
190    }
191}
192
193impl BuildBatchContext<RedisMessage> for StreamBatchContext {
194    fn build(first: &RedisMessage) -> Self {
195        Self {
196            seeker: first.seeker().clone(),
197        }
198    }
199}
200
201/// Per-delivery context for a Redis Pub/Sub delivery ([`RedisPubSubMessage`]).
202///
203/// Pub/Sub keeps no history, so there is nothing to reposition and no position to report: the
204/// fields are the delivery's own channel and how it matched.
205#[derive(Debug, Clone)]
206pub struct PubSubContext {
207    channel: String,
208    from_pattern: bool,
209    pool: Pool,
210}
211
212impl PubSubContext {
213    /// Constructs a context directly from its native fields (mainly for tests).
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// use std::error::Error;
219    ///
220    /// use fred::clients::Pool;
221    /// use fred::types::config::Config;
222    /// use ruststream_fred::context::PubSubContext;
223    ///
224    /// /// The region a fan-out delivery belongs to, read off the channel it arrived on.
225    /// fn region(cx: &PubSubContext) -> &str {
226    ///     cx.channel().rsplit_once('.').map_or("", |(_, region)| region)
227    /// }
228    ///
229    /// # fn main() -> Result<(), Box<dyn Error>> {
230    /// // An unconnected pool: `Pool::new` opens no socket.
231    /// let pool = Pool::new(Config::default(), None, None, None, 1)?;
232    /// let cx = PubSubContext::new("events.eu", true, pool);
233    /// assert_eq!(region(&cx), "eu");
234    /// # Ok(())
235    /// # }
236    /// ```
237    #[must_use]
238    pub fn new(channel: impl Into<String>, from_pattern: bool, pool: Pool) -> Self {
239        Self {
240            channel: channel.into(),
241            from_pattern,
242            pool,
243        }
244    }
245
246    /// The broker's connection pool, for a command whose answer the handler needs now.
247    #[must_use]
248    pub const fn pool(&self) -> &Pool {
249        &self.pool
250    }
251
252    /// The concrete channel this message arrived on (the matched channel, not the subscription
253    /// glob, for a pattern subscription).
254    #[must_use]
255    pub fn channel(&self) -> &str {
256        &self.channel
257    }
258
259    /// Whether the delivery matched through a `PSUBSCRIBE` pattern rather than an exact subscribe.
260    #[must_use]
261    pub const fn from_pattern(&self) -> bool {
262        self.from_pattern
263    }
264}
265
266impl BuildContext<RedisPubSubMessage> for PubSubContext {
267    fn build(msg: &RedisPubSubMessage) -> Self {
268        Self {
269            channel: msg.channel().to_owned(),
270            from_pattern: msg.from_pattern(),
271            pool: msg.pool().clone(),
272        }
273    }
274}
275
276/// The per-delivery context that carries the broker's connection pool and nothing else: what a
277/// handler taking `Ctx<keys::FredPool>` alone reads, on every form.
278///
279/// # Examples
280///
281/// ```
282/// # mod demo {
283/// use fred::interfaces::KeysInterface;
284/// use ruststream::prelude::*;
285/// use ruststream::subscriber;
286/// use ruststream_fred::RedisList;
287/// use ruststream_fred::context::keys;
288/// # #[derive(serde::Deserialize)]
289/// # struct Job { id: u64 }
290///
291/// /// Runs a job once: a delivery of a job seen before counts past one and is dropped.
292/// #[subscriber(RedisList::new("jobs").reliable())]
293/// async fn work(job: &Job, Ctx(pool): Ctx<keys::FredPool>) -> HandlerOutcome {
294///     match pool.incr::<i64, _>(format!("jobs.seen:{}", job.id)).await {
295///         Ok(1) => HandlerOutcome::ack(),
296///         Ok(_) => HandlerOutcome::drop(),
297///         Err(_) => HandlerOutcome::retry(),
298///     }
299/// }
300/// # }
301/// ```
302#[derive(Debug, Clone)]
303pub struct PoolContext {
304    pool: Pool,
305}
306
307impl PoolContext {
308    /// The broker's connection pool.
309    #[must_use]
310    pub const fn pool(&self) -> &Pool {
311        &self.pool
312    }
313
314    pub(crate) const fn from_pool(pool: Pool) -> Self {
315        Self { pool }
316    }
317}
318
319impl BuildContext<RedisMessage> for PoolContext {
320    fn build(msg: &RedisMessage) -> Self {
321        Self::from_pool(msg.seeker().pool().clone())
322    }
323}
324
325impl BuildContext<RedisListMessage> for PoolContext {
326    fn build(msg: &RedisListMessage) -> Self {
327        Self::from_pool(msg.pool().clone())
328    }
329}
330
331impl BuildContext<RedisPubSubMessage> for PoolContext {
332    fn build(msg: &RedisPubSubMessage) -> Self {
333        Self::from_pool(msg.pool().clone())
334    }
335}
336
337/// The per-delivery context of a `.pipeline()` subscription: the delivery's round and the
338/// broker's connection pool.
339///
340/// Only a pipelined subscription's delivery builds it, which is what makes `Ctx<keys::Pipeline>`
341/// a compile error on a mount site without `.pipeline()`.
342///
343/// # Examples
344///
345/// ```
346/// # mod demo {
347/// use ruststream_fred::stream::prelude::*;
348/// # #[derive(serde::Deserialize)]
349/// # struct Order { id: u64 }
350///
351/// #[subscriber(RedisStream::new("orders").group("workers"))]
352/// async fn work(order: &Order, ctx: &mut Context<'_, PipelineContext>) -> HandlerOutcome {
353///     let key = format!("orders.seen:{}", order.id);
354///     if ctx.context(keys::Pipeline).incr(key).await.is_err() {
355///         return HandlerOutcome::retry();
356///     }
357///     HandlerOutcome::ack()
358/// }
359///
360/// fn app() -> RustStream {
361///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
362///         RedisBroker::standalone("redis://localhost:6379"),
363///         |b| {
364///             b.include(work.pipeline());
365///         },
366///     )
367/// }
368/// # }
369/// ```
370#[derive(Debug, Clone)]
371pub struct PipelineContext {
372    pipeline: RedisPipeline,
373    pool: Pool,
374}
375
376impl PipelineContext {
377    /// The delivery's round: what the handler queues its commands into.
378    #[must_use]
379    pub const fn pipeline(&self) -> &RedisPipeline {
380        &self.pipeline
381    }
382
383    /// The broker's connection pool, for a command whose answer the handler needs now.
384    #[must_use]
385    pub const fn pool(&self) -> &Pool {
386        &self.pool
387    }
388}
389
390impl<F: Form> BuildContext<RoundMessage<F>> for PipelineContext {
391    fn build(msg: &RoundMessage<F>) -> Self {
392        Self {
393            pipeline: RedisPipeline::new(msg.round().clone()),
394            pool: F::pool(msg.inner()).clone(),
395        }
396    }
397}
398
399/// A batch of a `.pipeline()` subscription is one segment: its context hands the batch body the
400/// round every delivery of the batch shares.
401impl<F: Form> BuildBatchContext<RoundMessage<F>> for PipelineContext {
402    fn build(first: &RoundMessage<F>) -> Self {
403        <Self as BuildContext<RoundMessage<F>>>::build(first)
404    }
405}
406
407impl<F: Form> BuildContext<RoundMessage<F>> for PoolContext {
408    fn build(msg: &RoundMessage<F>) -> Self {
409        Self::from_pool(F::pool(msg.inner()).clone())
410    }
411}
412
413/// `Ctx<keys::FredPool>` beside `Ctx<keys::Pipeline>`: the handler's context is
414/// [`PipelineContext`] there.
415impl<State: Sync> FromContext<PipelineContext, State> for Ctx<keys::FredPool> {
416    type Rejection = Infallible;
417
418    fn from_context(
419        ctx: &mut Context<'_, PipelineContext, State>,
420    ) -> impl Future<Output = Result<Self, Infallible>> + Send {
421        let pool = ctx.context(keys::FredPool).clone();
422        async move { Ok(Self(pool)) }
423    }
424}
425
426/// `Ctx<keys::FredPool>` beside a stream key: the handler's context is [`StreamContext`] there.
427impl<State: Sync> FromContext<StreamContext, State> for Ctx<keys::FredPool> {
428    type Rejection = Infallible;
429
430    fn from_context(
431        ctx: &mut Context<'_, StreamContext, State>,
432    ) -> impl Future<Output = Result<Self, Infallible>> + Send {
433        let pool = ctx.context(keys::FredPool).clone();
434        async move { Ok(Self(pool)) }
435    }
436}
437
438/// `Ctx<keys::FredPool>` beside a Pub/Sub key: the handler's context is [`PubSubContext`] there.
439impl<State: Sync> FromContext<PubSubContext, State> for Ctx<keys::FredPool> {
440    type Rejection = Infallible;
441
442    fn from_context(
443        ctx: &mut Context<'_, PubSubContext, State>,
444    ) -> impl Future<Output = Result<Self, Infallible>> + Send {
445        let pool = ctx.context(keys::FredPool).clone();
446        async move { Ok(Self(pool)) }
447    }
448}
449
450/// Compile-time [`Field`] keys, one per native field, read with `ctx.context(key)`.
451///
452/// Each key is a zero-sized selector implementing [`Field`] only for the context types that carry
453/// its field, so applying a key to the wrong transport's context is a compile error. A key that
454/// also implements [`ContextField`](ruststream::ContextField) can be bound as a `Ctx<K>` handler
455/// parameter; those read the per-delivery context, so a batch body reaches its own fields through
456/// `ctx.context(..)` instead.
457pub mod keys {
458    use ruststream::ContextField;
459
460    use fred::clients::Pool;
461
462    use super::{
463        Field, PipelineContext, PoolContext, PubSubContext, RedisGroupPosition, RedisGroupSeeker,
464        RedisPipeline, StreamBatchContext, StreamContext,
465    };
466
467    /// Reads the delivery's round off the context of a `.pipeline()` subscription: the
468    /// [`RedisPipeline`] its handler queues commands into.
469    ///
470    /// The key names [`PipelineContext`], which only a pipelined subscription's delivery builds,
471    /// so taking it on a subscription without a window does not compile.
472    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
473    pub struct Pipeline;
474
475    impl ContextField for Pipeline {
476        type Context = PipelineContext;
477        type Value = RedisPipeline;
478        fn read(self, src: &PipelineContext) -> RedisPipeline {
479            src.pipeline().clone()
480        }
481    }
482
483    impl Field<PipelineContext> for Pipeline {
484        type Value<'a> = &'a RedisPipeline;
485        fn get(self, src: &PipelineContext) -> &RedisPipeline {
486            src.pipeline()
487        }
488    }
489
490    impl Field<PipelineContext> for FredPool {
491        type Value<'a> = &'a Pool;
492        fn get(self, src: &PipelineContext) -> &Pool {
493            src.pool()
494        }
495    }
496
497    /// Reads the broker's connection pool, `fred::clients::Pool`, off the context of any
498    /// subscription.
499    ///
500    /// For a command whose answer the handler needs now, or one that must leave at once. As the
501    /// only `Ctx` key of a handler it names [`PoolContext`]; beside a form's own key it reads
502    /// that form's context, and goes after that key in the signature.
503    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
504    pub struct FredPool;
505
506    impl ContextField for FredPool {
507        type Context = PoolContext;
508        type Value = Pool;
509        fn read(self, src: &PoolContext) -> Pool {
510            src.pool().clone()
511        }
512    }
513
514    impl Field<PoolContext> for FredPool {
515        type Value<'a> = &'a Pool;
516        fn get(self, src: &PoolContext) -> &Pool {
517            src.pool()
518        }
519    }
520
521    impl Field<StreamContext> for FredPool {
522        type Value<'a> = &'a Pool;
523        fn get(self, src: &StreamContext) -> &Pool {
524            src.pool()
525        }
526    }
527
528    impl Field<StreamBatchContext> for FredPool {
529        type Value<'a> = &'a Pool;
530        fn get(self, src: &StreamBatchContext) -> &Pool {
531            src.pool()
532        }
533    }
534
535    impl Field<PubSubContext> for FredPool {
536        type Value<'a> = &'a Pool;
537        fn get(self, src: &PubSubContext) -> &Pool {
538            src.pool()
539        }
540    }
541
542    /// Reads the stream entry id this delivery was read at off a [`StreamContext`].
543    ///
544    /// The value is the parsed [`EntryId`](crate::EntryId), so it compares and orders the way the
545    /// stream does; render it with `to_string` for the wire spelling.
546    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
547    pub struct EntryId;
548
549    impl Field<StreamContext> for EntryId {
550        type Value<'a> = crate::EntryId;
551        fn get(self, src: &StreamContext) -> crate::EntryId {
552            src.entry_id()
553        }
554    }
555
556    impl ContextField for EntryId {
557        type Context = StreamContext;
558        type Value = crate::EntryId;
559        fn read(self, src: &StreamContext) -> crate::EntryId {
560            src.entry_id()
561        }
562    }
563
564    /// Reads the group cursor that redelivers this entry off a [`StreamContext`].
565    ///
566    /// Seeking to it delivers this message again, followed by the entries after it.
567    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
568    pub struct Position;
569
570    impl Field<StreamContext> for Position {
571        type Value<'a> = RedisGroupPosition;
572        fn get(self, src: &StreamContext) -> RedisGroupPosition {
573            src.position()
574        }
575    }
576
577    impl ContextField for Position {
578        type Context = StreamContext;
579        type Value = RedisGroupPosition;
580        fn read(self, src: &StreamContext) -> RedisGroupPosition {
581            src.position()
582        }
583    }
584
585    /// Reads the group's reposition handle off a stream context, per delivery or per batch.
586    ///
587    /// The handle is subscription-scoped (resolved once, when the subscription opens), which is
588    /// why it is the one field both context types carry. As a `Ctx<SeekHandle>` parameter it binds
589    /// the per-delivery context; a batch body reads it with `ctx.context(SeekHandle)` off
590    /// [`StreamBatchContext`].
591    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
592    pub struct SeekHandle;
593
594    impl Field<StreamContext> for SeekHandle {
595        type Value<'a> = &'a RedisGroupSeeker;
596        fn get(self, src: &StreamContext) -> &RedisGroupSeeker {
597            src.seeker()
598        }
599    }
600
601    impl Field<StreamBatchContext> for SeekHandle {
602        type Value<'a> = &'a RedisGroupSeeker;
603        fn get(self, src: &StreamBatchContext) -> &RedisGroupSeeker {
604            src.seeker()
605        }
606    }
607
608    impl ContextField for SeekHandle {
609        type Context = StreamContext;
610        type Value = RedisGroupSeeker;
611        fn read(self, src: &StreamContext) -> RedisGroupSeeker {
612            src.seeker().clone()
613        }
614    }
615
616    /// Reads the consumer group off a stream context, per delivery or per batch.
617    ///
618    /// # Examples
619    ///
620    /// ```
621    /// # mod demo {
622    /// use ruststream_fred::stream::prelude::*;
623    /// use serde::Deserialize;
624    ///
625    /// #[derive(Deserialize)]
626    /// struct Order {
627    ///     id: u64,
628    /// }
629    ///
630    /// #[subscriber(RedisStream::new("orders").group("workers"))]
631    /// async fn handle(order: &Order, ctx: &mut Context<'_, StreamContext>) -> HandlerOutcome {
632    ///     // The entry id reads like `1700000000000-0`.
633    ///     tracing::info!(
634    ///         order.id,
635    ///         entry = %ctx.context(keys::EntryId),
636    ///         group = %ctx.context(keys::ConsumerGroup),
637    ///         "read"
638    ///     );
639    ///     HandlerOutcome::ack()
640    /// }
641    ///
642    /// #[ruststream::app]
643    /// fn app() -> impl App {
644    ///     RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
645    ///         RedisBroker::standalone("redis://localhost:6379"),
646    ///         |b| {
647    ///             b.include(handle);
648    ///         },
649    ///     )
650    /// }
651    /// # }
652    /// # fn main() {}
653    /// ```
654    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
655    pub struct ConsumerGroup;
656
657    impl Field<StreamContext> for ConsumerGroup {
658        type Value<'a> = &'a str;
659        fn get(self, src: &StreamContext) -> &str {
660            src.consumer_group()
661        }
662    }
663
664    impl Field<StreamBatchContext> for ConsumerGroup {
665        type Value<'a> = &'a str;
666        fn get(self, src: &StreamBatchContext) -> &str {
667            src.consumer_group()
668        }
669    }
670
671    impl ContextField for ConsumerGroup {
672        type Context = StreamContext;
673        type Value = String;
674        fn read(self, src: &StreamContext) -> String {
675            src.consumer_group().to_owned()
676        }
677    }
678
679    /// Reads the concrete channel off a [`PubSubContext`].
680    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
681    pub struct Channel;
682
683    impl Field<PubSubContext> for Channel {
684        type Value<'a> = &'a str;
685        fn get(self, src: &PubSubContext) -> &str {
686            src.channel()
687        }
688    }
689
690    impl ContextField for Channel {
691        type Context = PubSubContext;
692        type Value = String;
693        fn read(self, src: &PubSubContext) -> String {
694            src.channel().to_owned()
695        }
696    }
697
698    /// Reads whether a [`PubSubContext`] delivery matched through a `PSUBSCRIBE` pattern.
699    #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
700    pub struct FromPattern;
701
702    impl Field<PubSubContext> for FromPattern {
703        type Value<'a> = bool;
704        fn get(self, src: &PubSubContext) -> bool {
705            src.from_pattern()
706        }
707    }
708
709    impl ContextField for FromPattern {
710        type Context = PubSubContext;
711        type Value = bool;
712        fn read(self, src: &PubSubContext) -> bool {
713            src.from_pattern()
714        }
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::PubSubContext;
721    use super::keys::{Channel, FromPattern};
722    use fred::clients::Pool;
723    use fred::types::config::Config;
724    use ruststream::{ContextField, Field};
725
726    /// An unconnected pool (just client structs); `Pool::new` opens no sockets.
727    fn offline_pool() -> Pool {
728        Pool::new(Config::default(), None, None, None, 1).expect("offline pool")
729    }
730
731    #[test]
732    fn pubsub_keys_read_channel_and_pattern_flag() {
733        let exact = PubSubContext::new("events", false, offline_pool());
734        assert_eq!(Channel.get(&exact), "events");
735        assert!(!FromPattern.get(&exact));
736
737        let matched = PubSubContext::new("events.user", true, offline_pool());
738        assert_eq!(Channel.get(&matched), "events.user");
739        assert!(FromPattern.get(&matched));
740    }
741
742    #[test]
743    fn pubsub_context_field_keys_yield_owned_values() {
744        let pubsub = PubSubContext::new("orders.eu", true, offline_pool());
745        assert_eq!(
746            <Channel as ContextField>::read(Channel, &pubsub),
747            "orders.eu".to_owned()
748        );
749        assert!(<FromPattern as ContextField>::read(FromPattern, &pubsub));
750    }
751}