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}