Skip to main content

ruststream_fred/
context.rs

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