1use alloc::sync::Arc;
2use bytes::Bytes;
3use core::fmt::Debug;
4use core::future::Future;
5use core::marker::PhantomData;
6use portable_atomic::{AtomicU64, Ordering};
7
8use crate::pipe::SendPipe;
9use crate::pipe::ext::PipeExt;
10use crate::pipe::handler::{PipeHandle, into_handle};
11use crate::pipe::primitives::{Pipe, UnpinPipe, UnpinSendPipe};
12use crate::pipe::when::When;
13use serde::{Deserialize, Serialize};
14
15use crate::pipe::request::Request;
16use proxima_core::ProximaError;
17
18#[cfg(feature = "std")]
19use crate::pipe::pipe_factory::PipeFactory;
20#[cfg(feature = "std")]
21use serde_json::Value;
22#[cfg(feature = "std")]
23use std::pin::Pin;
24
25impl From<RejectMode> for ProximaError {
51 fn from(mode: RejectMode) -> Self {
52 match mode {
53 RejectMode::Drop => ProximaError::Forbidden("forbidden".into()),
54 RejectMode::Error => ProximaError::Config("filter: predicate rejected request".into()),
55 }
56 }
57}
58
59#[derive(Serialize, Deserialize)]
69#[serde(tag = "kind", rename_all = "snake_case", bound = "")]
70pub enum Predicate<In = Request<Bytes>, Err = ProximaError> {
71 Always {
72 #[serde(skip)]
73 _marker: PhantomData<fn(In) -> Err>,
74 },
75 Never {
76 #[serde(skip)]
77 _marker: PhantomData<fn(In) -> Err>,
78 },
79 When {
80 #[serde(flatten)]
81 gate: When,
82 #[serde(skip)]
83 calls: Arc<AtomicU64>,
84 #[serde(skip)]
85 _marker: PhantomData<fn(In) -> Err>,
86 },
87 Unless {
88 #[serde(flatten)]
89 gate: When,
90 #[serde(skip)]
91 calls: Arc<AtomicU64>,
92 #[serde(skip)]
93 _marker: PhantomData<fn(In) -> Err>,
94 },
95}
96
97impl<In, Err> Debug for Predicate<In, Err> {
105 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
106 match self {
107 Predicate::Always { .. } => formatter.debug_struct("Always").finish(),
108 Predicate::Never { .. } => formatter.debug_struct("Never").finish(),
109 Predicate::When { gate, calls, .. } => formatter
110 .debug_struct("When")
111 .field("gate", gate)
112 .field("calls", &calls.load(Ordering::Relaxed))
113 .finish(),
114 Predicate::Unless { gate, calls, .. } => formatter
115 .debug_struct("Unless")
116 .field("gate", gate)
117 .field("calls", &calls.load(Ordering::Relaxed))
118 .finish(),
119 }
120 }
121}
122
123impl<In, Err> Clone for Predicate<In, Err> {
124 fn clone(&self) -> Self {
125 match self {
126 Predicate::Always { .. } => Predicate::Always {
127 _marker: PhantomData,
128 },
129 Predicate::Never { .. } => Predicate::Never {
130 _marker: PhantomData,
131 },
132 Predicate::When { gate, calls, .. } => Predicate::When {
133 gate: *gate,
134 calls: Arc::clone(calls),
135 _marker: PhantomData,
136 },
137 Predicate::Unless { gate, calls, .. } => Predicate::Unless {
138 gate: *gate,
139 calls: Arc::clone(calls),
140 _marker: PhantomData,
141 },
142 }
143 }
144}
145
146impl<In, Err> PartialEq for Predicate<In, Err> {
147 fn eq(&self, other: &Self) -> bool {
148 match (self, other) {
149 (Predicate::Always { .. }, Predicate::Always { .. }) => true,
150 (Predicate::Never { .. }, Predicate::Never { .. }) => true,
151 (Predicate::When { gate: left, .. }, Predicate::When { gate: right, .. }) => {
152 left == right
153 }
154 (Predicate::Unless { gate: left, .. }, Predicate::Unless { gate: right, .. }) => {
155 left == right
156 }
157 _ => false,
158 }
159 }
160}
161
162impl<In, Err> Predicate<In, Err> {
163 #[must_use]
164 pub fn always() -> Self {
165 Predicate::Always {
166 _marker: PhantomData,
167 }
168 }
169
170 #[must_use]
171 pub fn never() -> Self {
172 Predicate::Never {
173 _marker: PhantomData,
174 }
175 }
176
177 #[must_use]
178 pub fn when(gate: When) -> Self {
179 Predicate::When {
180 gate,
181 calls: Arc::new(AtomicU64::new(0)),
182 _marker: PhantomData,
183 }
184 }
185
186 #[must_use]
187 pub fn unless(gate: When) -> Self {
188 Predicate::Unless {
189 gate,
190 calls: Arc::new(AtomicU64::new(0)),
191 _marker: PhantomData,
192 }
193 }
194
195 fn admits(&self) -> bool {
198 match self {
199 Predicate::Always { .. } => true,
200 Predicate::Never { .. } => false,
201 Predicate::When { gate, calls, .. } => {
202 let index = calls.fetch_add(1, Ordering::Relaxed);
203 gate.fires(index)
204 }
205 Predicate::Unless { gate, calls, .. } => {
206 let index = calls.fetch_add(1, Ordering::Relaxed);
207 !gate.fires(index)
208 }
209 }
210 }
211}
212
213impl<In, Err> SendPipe for Predicate<In, Err>
214where
215 In: Send + 'static,
216 Err: From<RejectMode> + Debug + Send + 'static,
217{
218 type In = In;
219 type Out = In;
220 type Err = Err;
221
222 fn call(&self, input: In) -> impl Future<Output = Result<In, Err>> + Send {
223 let admits = self.admits();
224 async move {
225 if admits {
226 Ok(input)
227 } else {
228 Err(Err::from(RejectMode::Drop))
229 }
230 }
231 }
232}
233
234impl<In, Err> Pipe for Predicate<In, Err>
238where
239 In: 'static,
240 Err: From<RejectMode> + Debug + 'static,
241{
242 type In = In;
243 type Out = In;
244 type Err = Err;
245
246 fn call(&self, input: In) -> impl Future<Output = Result<In, Err>> {
247 let admits = self.admits();
248 async move {
249 if admits {
250 Ok(input)
251 } else {
252 Err(Err::from(RejectMode::Drop))
253 }
254 }
255 }
256}
257
258impl<In, Err> UnpinPipe for Predicate<In, Err>
263where
264 In: 'static,
265 Err: From<RejectMode> + Debug + 'static,
266{
267 type In = In;
268 type Out = In;
269 type Err = Err;
270
271 fn call(&self, input: In) -> impl Future<Output = Result<In, Err>> + Unpin {
272 let admits = self.admits();
273 core::future::ready(if admits {
274 Ok(input)
275 } else {
276 Err(Err::from(RejectMode::Drop))
277 })
278 }
279}
280
281impl<In, Err> UnpinSendPipe for Predicate<In, Err>
282where
283 In: Send + 'static,
284 Err: From<RejectMode> + Debug + Send + 'static,
285{
286 type In = In;
287 type Out = In;
288 type Err = Err;
289
290 fn call(&self, input: In) -> impl Future<Output = Result<In, Err>> + Send + Unpin {
291 let admits = self.admits();
292 core::future::ready(if admits {
293 Ok(input)
294 } else {
295 Err(Err::from(RejectMode::Drop))
296 })
297 }
298}
299
300#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
306#[serde(rename_all = "snake_case")]
307pub enum RejectMode {
308 Error,
309 Drop,
310}
311
312#[derive(Serialize, Deserialize)]
326#[serde(bound = "")]
327pub struct FilterConfig<In = Request<Bytes>, Err = ProximaError> {
328 pub predicate: Predicate<In, Err>,
329 pub on_reject: RejectMode,
330}
331
332impl<In, Err> Debug for FilterConfig<In, Err> {
336 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
337 formatter
338 .debug_struct("FilterConfig")
339 .field("predicate", &self.predicate)
340 .field("on_reject", &self.on_reject)
341 .finish()
342 }
343}
344
345impl<In, Err> Clone for FilterConfig<In, Err> {
346 fn clone(&self) -> Self {
347 Self {
348 predicate: self.predicate.clone(),
349 on_reject: self.on_reject,
350 }
351 }
352}
353
354impl<In, Err> PartialEq for FilterConfig<In, Err> {
355 fn eq(&self, other: &Self) -> bool {
356 self.predicate == other.predicate && self.on_reject == other.on_reject
357 }
358}
359
360impl<In, Err> Default for FilterConfig<In, Err> {
361 fn default() -> Self {
362 Self {
363 predicate: Predicate::always(),
364 on_reject: RejectMode::Drop,
365 }
366 }
367}
368
369impl<In, Err> SendPipe for FilterConfig<In, Err>
370where
371 In: Send + 'static,
372 Err: From<RejectMode> + Debug + Send + 'static,
373{
374 type In = In;
375 type Out = In;
376 type Err = Err;
377
378 fn call(&self, input: In) -> impl Future<Output = Result<In, Err>> + Send {
379 let admits = self.predicate.admits();
380 let on_reject = self.on_reject;
381 async move {
382 if admits {
383 Ok(input)
384 } else {
385 Err(Err::from(on_reject))
386 }
387 }
388 }
389}
390
391impl<In, Err> Pipe for FilterConfig<In, Err>
392where
393 In: 'static,
394 Err: From<RejectMode> + Debug + 'static,
395{
396 type In = In;
397 type Out = In;
398 type Err = Err;
399
400 fn call(&self, input: In) -> impl Future<Output = Result<In, Err>> {
401 let admits = self.predicate.admits();
402 let on_reject = self.on_reject;
403 async move {
404 if admits {
405 Ok(input)
406 } else {
407 Err(Err::from(on_reject))
408 }
409 }
410 }
411}
412
413impl<In, Err> UnpinPipe for FilterConfig<In, Err>
417where
418 In: 'static,
419 Err: From<RejectMode> + Debug + 'static,
420{
421 type In = In;
422 type Out = In;
423 type Err = Err;
424
425 fn call(&self, input: In) -> impl Future<Output = Result<In, Err>> + Unpin {
426 let admits = self.predicate.admits();
427 let on_reject = self.on_reject;
428 core::future::ready(if admits {
429 Ok(input)
430 } else {
431 Err(Err::from(on_reject))
432 })
433 }
434}
435
436impl<In, Err> UnpinSendPipe for FilterConfig<In, Err>
437where
438 In: Send + 'static,
439 Err: From<RejectMode> + Debug + Send + 'static,
440{
441 type In = In;
442 type Out = In;
443 type Err = Err;
444
445 fn call(&self, input: In) -> impl Future<Output = Result<In, Err>> + Send + Unpin {
446 let admits = self.predicate.admits();
447 let on_reject = self.on_reject;
448 core::future::ready(if admits {
449 Ok(input)
450 } else {
451 Err(Err::from(on_reject))
452 })
453 }
454}
455
456impl FilterConfig<Request<Bytes>, ProximaError> {
465 #[must_use]
470 pub fn into_filter(self, inner: PipeHandle) -> PipeHandle {
471 into_handle(PipeExt::and_then(self, inner))
472 }
473
474 #[cfg(feature = "std")]
475 pub fn from_spec(inner: PipeHandle, value: &Value) -> Result<PipeHandle, ProximaError> {
476 let config: FilterConfig<Request<Bytes>, ProximaError> =
477 serde_json::from_value(value.clone())
478 .map_err(|err| ProximaError::Config(format!("filter config: {err}")))?;
479 Ok(config.into_filter(inner))
480 }
481}
482
483#[cfg(feature = "std")]
484pub struct FilterFactory;
485
486#[cfg(feature = "std")]
487impl PipeFactory for FilterFactory {
488 fn name(&self) -> &str {
489 "filter"
490 }
491
492 fn build(
493 &self,
494 spec: &Value,
495 inner: Option<PipeHandle>,
496 ) -> Pin<Box<dyn Future<Output = Result<PipeHandle, ProximaError>> + Send + '_>> {
497 let spec = spec.clone();
498 Box::pin(async move {
499 let inner = inner
500 .ok_or_else(|| ProximaError::Config("filter requires an inner pipe".into()))?;
501 FilterConfig::from_spec(inner, &spec)
502 })
503 }
504}
505
506#[cfg(all(test, not(loom)))]
511#[allow(clippy::unwrap_used, clippy::expect_used)]
512mod tests {
513 use std::future::Future;
514 use std::sync::atomic::{AtomicUsize, Ordering as StdOrdering};
515
516 use super::*;
517 use crate::pipe::handler::into_handle;
518 use crate::pipe::request::{Request, Response};
519
520 fn echo_pipe() -> PipeHandle {
521 struct EchoPipe;
522 impl SendPipe for EchoPipe {
523 type In = Request<Bytes>;
524 type Out = Response<Bytes>;
525 type Err = ProximaError;
526
527 fn call(
528 &self,
529 request: Request<Bytes>,
530 ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
531 async move {
532 let (_, body) = request.body_bytes().await?;
533 Ok(Response::new(200).with_body(body))
534 }
535 }
536 }
537 into_handle(EchoPipe)
538 }
539
540 fn counting_echo_pipe() -> (PipeHandle, Arc<AtomicUsize>) {
545 struct CountingEcho(Arc<AtomicUsize>);
546 impl SendPipe for CountingEcho {
547 type In = Request<Bytes>;
548 type Out = Response<Bytes>;
549 type Err = ProximaError;
550
551 fn call(
552 &self,
553 request: Request<Bytes>,
554 ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
555 self.0.fetch_add(1, StdOrdering::Relaxed);
556 async move {
557 let (_, body) = request.body_bytes().await?;
558 Ok(Response::new(200).with_body(body))
559 }
560 }
561 }
562 let calls = Arc::new(AtomicUsize::new(0));
563 (into_handle(CountingEcho(calls.clone())), calls)
564 }
565
566 fn build_request() -> Request<Bytes> {
567 Request::builder()
568 .method("POST")
569 .path("/")
570 .body(bytes::Bytes::from_static(b"hello"))
571 .build()
572 .expect("builder")
573 }
574
575 #[proxima::test]
576 async fn passes_through_when_predicate_is_true() {
577 let stack = FilterConfig {
578 predicate: Predicate::always(),
579 on_reject: RejectMode::Drop,
580 }
581 .into_filter(echo_pipe());
582 let response = SendPipe::call(&stack, build_request()).await.expect("call");
583 assert_eq!(response.status, 200);
584 let body = response.collect_body().await.expect("body");
585 assert_eq!(
586 &body[..],
587 b"hello",
588 "passing request reaches the inner echo pipe"
589 );
590 }
591
592 #[proxima::test]
593 async fn reject_with_error_returns_err() {
594 let stack = FilterConfig {
595 predicate: Predicate::never(),
596 on_reject: RejectMode::Error,
597 }
598 .into_filter(echo_pipe());
599 let outcome = SendPipe::call(&stack, build_request()).await;
600 assert!(
601 matches!(outcome, Err(ProximaError::Config(_))),
602 "RejectMode::Error surfaces the config error"
603 );
604 }
605
606 #[proxima::test]
607 async fn reject_with_drop_produces_a_forbidden_error() {
608 let (inner, calls) = counting_echo_pipe();
609 let stack = FilterConfig {
610 predicate: Predicate::never(),
611 on_reject: RejectMode::Drop,
612 }
613 .into_filter(inner);
614 let outcome = SendPipe::call(&stack, build_request()).await;
615 match outcome {
616 Err(ProximaError::Forbidden(payload)) => {
617 assert_eq!(
618 payload, "forbidden",
619 "RejectMode::Drop's payload is the edge's 403 body verbatim"
620 );
621 }
622 other => panic!("expected Forbidden, got {other:?}"),
623 }
624 assert_eq!(
625 calls.load(StdOrdering::Relaxed),
626 0,
627 "a rejected call never reaches the inner pipe"
628 );
629 }
630
631 #[test]
632 fn config_clone_and_serde_round_trip_are_lossless() {
633 let config = FilterConfig {
634 predicate: Predicate::never(),
635 on_reject: RejectMode::Error,
636 };
637
638 assert_eq!(
639 config.clone(),
640 config,
641 "the config's own Clone/PartialEq round-trips"
642 );
643 let json = serde_json::to_value(&config).expect("serialize");
644 let parsed: FilterConfig = serde_json::from_value(json).expect("deserialize");
645 assert_eq!(parsed, config, "serde round-trip is lossless");
646 }
647
648 #[proxima::test]
649 async fn from_spec_parses_serde_config() {
650 let value = serde_json::json!({
651 "predicate": {"kind": "never"},
652 "on_reject": "drop",
653 });
654 let stack = FilterConfig::from_spec(echo_pipe(), &value).expect("from spec");
655 let outcome = SendPipe::call(&stack, build_request()).await;
656 assert!(
657 matches!(outcome, Err(ProximaError::Forbidden(_))),
658 "never + drop rejects with Forbidden, proving the parsed config actually runs"
659 );
660 }
661
662 #[proxima::test]
663 async fn when_gated_filter_drops_and_passes_by_seed() {
664 let gate = When::prob(0.5).seed(0x5EED);
665 let stack = FilterConfig {
666 predicate: Predicate::when(gate),
667 on_reject: RejectMode::Drop,
668 }
669 .into_filter(echo_pipe());
670
671 let mut observed = Vec::new();
672 for _ in 0..32_u64 {
673 let outcome = SendPipe::call(&stack, build_request()).await;
674 observed.push(outcome.is_ok());
675 }
676
677 let expected: Vec<bool> = (0..32_u64).map(|index| gate.fires(index)).collect();
678 assert_eq!(
679 observed, expected,
680 "filter pass/drop tracks the gate's deterministic sequence"
681 );
682 }
683
684 #[proxima::test]
685 async fn when_prob_one_always_passes_and_prob_zero_always_drops() {
686 let always = FilterConfig {
687 predicate: Predicate::when(When::prob(1.0).seed(3)),
688 on_reject: RejectMode::Drop,
689 }
690 .into_filter(echo_pipe());
691 let never = FilterConfig {
692 predicate: Predicate::when(When::prob(0.0).seed(3)),
693 on_reject: RejectMode::Drop,
694 }
695 .into_filter(echo_pipe());
696 for _ in 0..16 {
697 let passed = SendPipe::call(&always, build_request()).await;
698 assert!(passed.is_ok(), "prob 1.0 always passes");
699 let dropped = SendPipe::call(&never, build_request()).await;
700 assert!(dropped.is_err(), "prob 0.0 always drops");
701 }
702 }
703
704 #[test]
705 fn when_config_round_trips_and_flattens_to_a_single_tagged_object() {
706 let config = FilterConfig {
707 predicate: Predicate::when(When::prob(0.3).seed(0xC0FFEE)),
708 on_reject: RejectMode::Drop,
709 };
710
711 let json = serde_json::to_value(&config).expect("serialize");
712 let parsed: FilterConfig = serde_json::from_value(json.clone()).expect("deserialize");
713
714 assert_eq!(parsed, config, "serde round-trip is lossless");
715 assert_eq!(
716 json,
717 serde_json::json!({
718 "predicate": {"kind": "when", "prob": 0.3, "seed": 0xC0FFEE_u64},
719 "on_reject": "drop",
720 }),
721 "When flattens to a single tagged object"
722 );
723 }
724
725 #[proxima::test]
726 async fn unless_gated_filter_rejects_on_a_fire() {
727 let gate = When::prob(0.5).seed(0x5EED);
728 let stack = FilterConfig {
729 predicate: Predicate::unless(gate),
730 on_reject: RejectMode::Drop,
731 }
732 .into_filter(echo_pipe());
733
734 let mut observed = Vec::new();
735 for _ in 0..32_u64 {
736 let outcome = SendPipe::call(&stack, build_request()).await;
737 observed.push(outcome.is_err());
738 }
739
740 let expected: Vec<bool> = (0..32_u64).map(|index| gate.fires(index)).collect();
741 assert_eq!(
742 observed, expected,
743 "Unless drops exactly when the gate fires"
744 );
745 }
746
747 #[test]
748 fn unless_config_round_trips_with_its_own_tag() {
749 let config = FilterConfig {
750 predicate: Predicate::unless(When::prob(0.3).seed(7)),
751 on_reject: RejectMode::Drop,
752 };
753 let json = serde_json::to_value(&config).expect("serialize");
754 let parsed: FilterConfig = serde_json::from_value(json.clone()).expect("deserialize");
755 assert_eq!(parsed, config, "Unless round-trips through config");
756 assert_eq!(
757 json,
758 serde_json::json!({
759 "predicate": {"kind": "unless", "prob": 0.3, "seed": 7},
760 "on_reject": "drop",
761 }),
762 "Unless carries the `unless` tag, distinct from When"
763 );
764 }
765
766 #[proxima::test]
767 async fn from_spec_parses_when_predicate() {
768 let value = serde_json::json!({
769 "predicate": {"kind": "when", "prob": 0.25, "seed": 11},
770 "on_reject": "error",
771 });
772 let stack = FilterConfig::from_spec(echo_pipe(), &value).expect("from spec");
773
774 let gate = When::prob(0.25).seed(11);
775 let mut observed = Vec::new();
776 for _ in 0..16_u64 {
777 let outcome = SendPipe::call(&stack, build_request()).await;
778 observed.push(outcome.is_ok());
779 if let Err(error) = outcome {
780 assert!(
781 matches!(error, ProximaError::Config(_)),
782 "on_reject: error surfaces ProximaError::Config, got {error:?}"
783 );
784 }
785 }
786
787 let expected: Vec<bool> = (0..16_u64).map(|index| gate.fires(index)).collect();
788 assert_eq!(
789 observed, expected,
790 "from_spec's when predicate matches the gate's deterministic sequence"
791 );
792 }
793
794 #[derive(Clone, PartialEq, Debug)]
795 struct SensorReading {
796 celsius: i32,
797 }
798
799 impl From<RejectMode> for SensorReading {
804 fn from(_mode: RejectMode) -> Self {
805 SensorReading { celsius: i32::MIN }
806 }
807 }
808
809 #[derive(Clone)]
810 struct Threshold {
811 max_celsius: i32,
812 }
813
814 impl SendPipe for Threshold {
818 type In = SensorReading;
819 type Out = SensorReading;
820 type Err = SensorReading;
821
822 fn call(
823 &self,
824 reading: SensorReading,
825 ) -> impl Future<Output = Result<SensorReading, SensorReading>> + Send {
826 let admits = reading.celsius <= self.max_celsius;
827 async move {
828 if admits {
829 Ok(reading)
830 } else {
831 Err(SensorReading { celsius: i32::MIN })
832 }
833 }
834 }
835 }
836
837 impl Pipe for Threshold {
840 type In = SensorReading;
841 type Out = SensorReading;
842 type Err = SensorReading;
843
844 fn call(
845 &self,
846 reading: SensorReading,
847 ) -> impl Future<Output = Result<SensorReading, SensorReading>> {
848 SendPipe::call(self, reading)
849 }
850 }
851
852 #[derive(Clone)]
853 struct ReadingSink;
854
855 impl SendPipe for ReadingSink {
856 type In = SensorReading;
857 type Out = SensorReading;
858 type Err = SensorReading;
859
860 fn call(
861 &self,
862 input: SensorReading,
863 ) -> impl Future<Output = Result<SensorReading, SensorReading>> + Send {
864 async move {
865 Ok(SensorReading {
866 celsius: input.celsius + 1,
867 })
868 }
869 }
870 }
871
872 impl Pipe for ReadingSink {
873 type In = SensorReading;
874 type Out = SensorReading;
875 type Err = SensorReading;
876
877 fn call(
878 &self,
879 input: SensorReading,
880 ) -> impl Future<Output = Result<SensorReading, SensorReading>> {
881 SendPipe::call(self, input)
882 }
883 }
884
885 #[proxima::test]
886 async fn filter_is_generic_over_a_non_http_payload() {
887 let stack = Threshold { max_celsius: 100 }.and_then(ReadingSink);
888
889 let admitted = SendPipe::call(&stack, SensorReading { celsius: 20 }).await;
890 assert_eq!(admitted, Ok(SensorReading { celsius: 21 }));
891
892 let dropped = SendPipe::call(&stack, SensorReading { celsius: 250 }).await;
893 assert_eq!(dropped, Err(SensorReading { celsius: i32::MIN }));
894 }
895
896 #[proxima::test]
904 async fn predicate_and_filter_config_are_generic_over_a_non_http_payload() {
905 let always_admits =
906 Predicate::<SensorReading, SensorReading>::when(When::prob(1.0).seed(1))
907 .and_then(ReadingSink);
908 let admitted = SendPipe::call(&always_admits, SensorReading { celsius: 20 }).await;
909 assert_eq!(admitted, Ok(SensorReading { celsius: 21 }));
910
911 let stack = FilterConfig::<SensorReading, SensorReading> {
912 predicate: Predicate::never(),
913 on_reject: RejectMode::Drop,
914 }
915 .and_then(ReadingSink);
916 let dropped = SendPipe::call(&stack, SensorReading { celsius: 20 }).await;
917 assert_eq!(dropped, Err(SensorReading { celsius: i32::MIN }));
918
919 let error_mode = FilterConfig::<SensorReading, SensorReading> {
920 predicate: Predicate::never(),
921 on_reject: RejectMode::Error,
922 }
923 .and_then(ReadingSink);
924 let errored = SendPipe::call(&error_mode, SensorReading { celsius: 20 }).await;
925 assert_eq!(
926 errored,
927 Err(SensorReading { celsius: i32::MIN }),
928 "RejectMode::Error still routes through From<RejectMode> for a non-HTTP Err"
929 );
930 }
931
932 fn poll_ready<F: Future + core::marker::Unpin>(mut future: F) -> F::Output {
935 let waker = core::task::Waker::noop();
936 let mut cx = core::task::Context::from_waker(waker);
937 match Pin::new(&mut future).poll(&mut cx) {
938 core::task::Poll::Ready(output) => output,
939 core::task::Poll::Pending => {
940 panic!("Predicate/FilterConfig never suspend; expected Ready")
941 }
942 }
943 }
944
945 #[test]
946 fn predicate_unpin_pipe_matches_send_pipe_on_admit_and_reject() {
947 let admits: Predicate = Predicate::always();
948 let rejects: Predicate = Predicate::never();
949
950 assert!(
951 poll_ready(UnpinPipe::call(&admits, build_request())).is_ok(),
952 "Always admits"
953 );
954 assert!(
955 matches!(
956 poll_ready(UnpinPipe::call(&rejects, build_request())),
957 Err(ProximaError::Forbidden(_))
958 ),
959 "Never rejects with Forbidden"
960 );
961 }
962
963 #[test]
964 fn predicate_unpin_send_pipe_is_send_and_unpin() {
965 fn needs_send_unpin<F: Future + Send + Unpin>(_: &F) {}
966 let predicate: Predicate = Predicate::always();
967 let call = UnpinSendPipe::call(&predicate, build_request());
968 needs_send_unpin(&call);
969 }
970
971 #[test]
972 fn filter_config_unpin_pipe_matches_send_pipe_on_admit_and_reject() {
973 let admits: FilterConfig = FilterConfig {
974 predicate: Predicate::always(),
975 on_reject: RejectMode::Drop,
976 };
977 let rejects: FilterConfig = FilterConfig {
978 predicate: Predicate::never(),
979 on_reject: RejectMode::Error,
980 };
981
982 assert!(poll_ready(UnpinPipe::call(&admits, build_request())).is_ok());
983 assert!(matches!(
984 poll_ready(UnpinPipe::call(&rejects, build_request())),
985 Err(ProximaError::Config(_))
986 ));
987 }
988
989 #[test]
990 fn filter_config_unpin_send_pipe_is_send_and_unpin() {
991 fn needs_send_unpin<F: Future + Send + Unpin>(_: &F) {}
992 let config: FilterConfig = FilterConfig::default();
993 let call = UnpinSendPipe::call(&config, build_request());
994 needs_send_unpin(&call);
995 }
996}