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}