1use alloc::vec::Vec;
2use bytes::Bytes;
3use core::future::Future;
4
5use crate::pipe::SendPipe;
6pub use crate::pipe::capabilities::ApplyOps;
7use crate::pipe::primitives::Pipe;
8
9use crate::pipe::handler::{PipeHandle, ThreadLocalPipeHandle};
10use crate::pipe::request::{Request, Response};
11use proxima_core::ProximaError;
12
13#[cfg(feature = "std")]
14use crate::pipe::handler::into_handle;
15#[cfg(feature = "std")]
16use crate::pipe::mutate::MutateOp;
17#[cfg(feature = "std")]
18use crate::pipe::pipe_factory::PipeFactory;
19#[cfg(feature = "std")]
20use alloc::string::String;
21#[cfg(feature = "std")]
22use regex::Regex;
23#[cfg(feature = "std")]
24use serde::{Deserialize, Serialize};
25#[cfg(feature = "std")]
26use serde_json::Value;
27#[cfg(feature = "std")]
28use std::pin::Pin;
29
30#[cfg(feature = "std")]
38pub struct Transform<Inner = PipeHandle, InOp = RequestOp, OutOp = ResponseOp> {
39 pub inner: Inner,
40 pub(crate) in_ops: Vec<InOp>,
41 pub(crate) out_ops: Vec<OutOp>,
42}
43
44#[cfg(not(feature = "std"))]
45pub struct Transform<Inner = PipeHandle, InOp = (), OutOp = ()> {
46 pub inner: Inner,
47 pub(crate) in_ops: Vec<InOp>,
48 pub(crate) out_ops: Vec<OutOp>,
49}
50
51impl<Inner, InOp, OutOp> Transform<Inner, InOp, OutOp> {
52 #[must_use]
53 pub fn new(inner: Inner) -> Self {
54 Self {
55 inner,
56 in_ops: Vec::new(),
57 out_ops: Vec::new(),
58 }
59 }
60
61 #[must_use]
62 pub fn with_request_op(mut self, op: InOp) -> Self {
63 self.in_ops.push(op);
64 self
65 }
66
67 #[must_use]
68 pub fn with_response_op(mut self, op: OutOp) -> Self {
69 self.out_ops.push(op);
70 self
71 }
72}
73
74impl<Inner, InOp, OutOp> SendPipe for Transform<Inner, InOp, OutOp>
75where
76 Inner: SendPipe + Clone,
77 Inner::In: ApplyOps<InOp> + Send,
78 Inner::Out: ApplyOps<OutOp>,
79 Inner::Err: Send,
80 InOp: Clone + Send + Sync + 'static,
81 OutOp: Clone + Send + Sync + 'static,
82{
83 type In = Inner::In;
84 type Out = Inner::Out;
85 type Err = Inner::Err;
86
87 fn call(
88 &self,
89 input: Inner::In,
90 ) -> impl Future<Output = Result<Inner::Out, Inner::Err>> + Send {
91 let mapped = input.apply(&self.in_ops);
92 let out_ops = self.out_ops.clone();
93 let inner = self.inner.clone();
94 async move {
95 let output = SendPipe::call(&inner, mapped).await?;
96 Ok(output.apply(&out_ops))
97 }
98 }
99}
100
101impl<InOp, OutOp> Pipe for Transform<ThreadLocalPipeHandle, InOp, OutOp>
102where
103 Request<Bytes>: ApplyOps<InOp>,
104 Response<Bytes>: ApplyOps<OutOp>,
105 InOp: Clone + Send + Sync + 'static,
106 OutOp: Clone + Send + Sync + 'static,
107{
108 type In = Request<Bytes>;
109 type Out = Response<Bytes>;
110 type Err = ProximaError;
111
112 fn call(
113 &self,
114 input: Request<Bytes>,
115 ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> {
116 let mapped = input.apply(&self.in_ops);
117 let out_ops = self.out_ops.clone();
118 let inner = self.inner.clone();
119 async move {
120 let output = Pipe::call(&inner, mapped).await?;
121 Ok(output.apply(&out_ops))
122 }
123 }
124}
125
126#[cfg(feature = "std")]
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(tag = "op", rename_all = "snake_case")]
136pub enum RequestOpConfig {
137 SetHeader {
138 name: String,
139 value: String,
140 },
141 RemoveHeader {
142 name: String,
143 },
144 RewritePath {
145 pattern: String,
146 template: String,
147 },
148 Mutate {
149 #[serde(flatten)]
150 mutate: MutateOp,
151 },
152 Body,
153 SetBody,
154 RewriteBody,
155 Callback,
156}
157
158#[cfg(feature = "std")]
159impl RequestOpConfig {
160 fn into_op(self) -> Result<RequestOp, ProximaError> {
161 match self {
162 RequestOpConfig::SetHeader { name, value } => Ok(RequestOp::SetHeader { name, value }),
163 RequestOpConfig::RemoveHeader { name } => Ok(RequestOp::RemoveHeader { name }),
164 RequestOpConfig::RewritePath { pattern, template } => {
165 let pattern = Regex::new(&pattern)
166 .map_err(|err| ProximaError::Config(format!("rewrite_path pattern: {err}")))?;
167 Ok(RequestOp::RewritePath { pattern, template })
168 }
169 RequestOpConfig::Mutate { mutate } => Ok(RequestOp::Mutate(mutate)),
170 RequestOpConfig::Body | RequestOpConfig::SetBody | RequestOpConfig::RewriteBody => {
171 Err(ProximaError::Config(
172 "transform body ops not implemented in v1; use a downstream pipe for body transforms"
173 .into(),
174 ))
175 }
176 RequestOpConfig::Callback => Err(ProximaError::Config(
177 "transform callbacks land in chunk 2 alongside the callback upstream registry".into(),
178 )),
179 }
180 }
181}
182
183#[cfg(feature = "std")]
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(tag = "op", rename_all = "snake_case")]
187pub enum ResponseOpConfig {
188 SetHeader {
189 name: String,
190 value: String,
191 },
192 RemoveHeader {
193 name: String,
194 },
195 Mutate {
196 #[serde(flatten)]
197 mutate: MutateOp,
198 },
199}
200
201#[cfg(feature = "std")]
202impl ResponseOpConfig {
203 fn into_op(self) -> ResponseOp {
204 match self {
205 ResponseOpConfig::SetHeader { name, value } => ResponseOp::SetHeader { name, value },
206 ResponseOpConfig::RemoveHeader { name } => ResponseOp::RemoveHeader { name },
207 ResponseOpConfig::Mutate { mutate } => ResponseOp::Mutate(mutate),
208 }
209 }
210}
211
212#[cfg(feature = "std")]
215#[derive(Debug, Clone, Default, Serialize, Deserialize)]
216pub struct TransformConfig {
217 #[serde(default)]
218 pub request_pipeline: alloc::vec::Vec<RequestOpConfig>,
219 #[serde(default)]
220 pub response_pipeline: alloc::vec::Vec<ResponseOpConfig>,
221}
222
223#[cfg(feature = "std")]
224impl TransformConfig {
225 #[must_use]
227 pub fn new() -> Self {
228 Self::default()
229 }
230
231 #[must_use]
233 pub fn with_request_op(mut self, op: RequestOpConfig) -> Self {
234 self.request_pipeline.push(op);
235 self
236 }
237
238 #[must_use]
240 pub fn with_response_op(mut self, op: ResponseOpConfig) -> Self {
241 self.response_pipeline.push(op);
242 self
243 }
244
245 pub fn into_transform(self, inner: PipeHandle) -> Result<Transform<PipeHandle>, ProximaError> {
248 let mut transform = Transform::new(inner);
249 for op in self.request_pipeline {
250 transform.in_ops.push(op.into_op()?);
251 }
252 for op in self.response_pipeline {
253 transform.out_ops.push(op.into_op());
254 }
255 Ok(transform)
256 }
257}
258
259#[cfg(feature = "std")]
260impl Transform<PipeHandle> {
261 pub fn from_spec(inner: PipeHandle, value: &Value) -> Result<Self, ProximaError> {
262 let config: TransformConfig = serde_json::from_value(value.clone())
263 .map_err(|err| ProximaError::Config(format!("transform config: {err}")))?;
264 config.into_transform(inner)
265 }
266}
267
268#[cfg(feature = "std")]
269pub struct TransformFactory;
270
271#[cfg(feature = "std")]
272impl PipeFactory for TransformFactory {
273 fn name(&self) -> &str {
274 "transform"
275 }
276
277 fn build(
278 &self,
279 spec: &Value,
280 inner: Option<PipeHandle>,
281 ) -> Pin<Box<dyn Future<Output = Result<PipeHandle, ProximaError>> + Send + '_>> {
282 let spec = spec.clone();
283 Box::pin(async move {
284 let inner = inner
285 .ok_or_else(|| ProximaError::Config("transform requires an inner pipe".into()))?;
286 let transform = Transform::from_spec(inner, &spec)?;
287 Ok(into_handle(transform))
288 })
289 }
290}
291
292#[cfg(feature = "std")]
299#[derive(Debug, Clone)]
300pub enum RequestOp {
301 SetHeader { name: String, value: String },
302 RemoveHeader { name: String },
303 RewritePath { pattern: Regex, template: String },
304 Mutate(MutateOp),
305}
306
307#[cfg(feature = "std")]
308#[derive(Debug, Clone)]
309pub enum ResponseOp {
310 SetHeader { name: String, value: String },
311 RemoveHeader { name: String },
312 Mutate(MutateOp),
313}
314
315#[cfg(feature = "std")]
316impl ApplyOps<RequestOp> for Request<Bytes> {
317 fn apply(mut self, ops: &[RequestOp]) -> Self {
318 for op in ops {
319 match op {
320 RequestOp::SetHeader { name, value } => {
321 self.metadata.insert(name.clone(), value.clone());
322 }
323 RequestOp::RemoveHeader { name } => {
324 self.metadata.remove(name);
325 }
326 RequestOp::RewritePath { pattern, template } => {
327 let path_view = std::str::from_utf8(&self.path).unwrap_or("");
328 let rewritten = pattern.replace(path_view, template.as_str()).into_owned();
329 self.path = bytes::Bytes::from(rewritten);
330 }
331 RequestOp::Mutate(mutate) => {
332 self = mutate.apply_to(self);
333 }
334 }
335 }
336 self
337 }
338}
339
340#[cfg(feature = "std")]
341impl ApplyOps<ResponseOp> for Response<Bytes> {
342 fn apply(mut self, ops: &[ResponseOp]) -> Self {
343 for op in ops {
344 match op {
345 ResponseOp::SetHeader { name, value } => {
346 self.metadata.insert(name.clone(), value.clone());
347 }
348 ResponseOp::RemoveHeader { name } => {
349 self.metadata.remove(name);
350 }
351 ResponseOp::Mutate(mutate) => {
352 self = mutate.apply_to(self);
353 }
354 }
355 }
356 self
357 }
358}
359
360#[cfg(all(test, not(loom)))]
365#[allow(clippy::unwrap_used, clippy::expect_used)]
366mod tests {
367 use std::future::Future;
368 use std::sync::{Arc, Mutex};
369
370 use super::*;
371 use crate::pipe::handler::into_handle;
372
373 struct Capture {
374 captured: Arc<Mutex<Option<Request<Bytes>>>>,
375 }
376
377 impl Capture {
378 fn new() -> (Self, Arc<Mutex<Option<Request<Bytes>>>>) {
379 let captured: Arc<Mutex<Option<Request<Bytes>>>> = Arc::new(Mutex::new(None));
380 (
381 Self {
382 captured: captured.clone(),
383 },
384 captured,
385 )
386 }
387 }
388
389 impl SendPipe for Capture {
390 type In = Request<Bytes>;
391 type Out = Response<Bytes>;
392 type Err = ProximaError;
393
394 fn call(
395 &self,
396 request: Request<Bytes>,
397 ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
398 *self.captured.lock().expect("capture lock") = Some(Request {
399 method: request.method.clone(),
400 path: request.path.clone(),
401 query: request.query.clone(),
402 metadata: request.metadata.clone(),
403 payload: bytes::Bytes::new(),
404 stream: None,
405 context: request.context.clone(),
406 });
407 async move {
408 let mut response = Response::ok("ok");
409 response.metadata.insert("server", "test/1.0");
410 Ok(response)
411 }
412 }
413 }
414
415 fn build_request() -> Request<Bytes> {
416 Request::builder()
417 .method("GET")
418 .path("/v1/items/42")
419 .header("user-agent", "rstest")
420 .build()
421 .expect("builder")
422 }
423
424 fn dummy() -> PipeHandle {
425 let (capture, _) = Capture::new();
426 into_handle(capture)
427 }
428
429 #[proxima::test]
430 async fn set_header_adds_header_to_request() {
431 let (capture, captured) = Capture::new();
432 let stack = Transform::new(into_handle(capture)).with_request_op(RequestOp::SetHeader {
433 name: "x-trace".into(),
434 value: "abc".into(),
435 });
436 let _ = SendPipe::call(&stack, build_request()).await.expect("call");
437 let guard = captured.lock().expect("captured");
438 let request = guard.as_ref().expect("captured request");
439 assert_eq!(request.metadata.get_str("x-trace"), Some("abc"));
440 }
441
442 #[proxima::test]
443 async fn remove_header_drops_header_from_request() {
444 let (capture, captured) = Capture::new();
445 let stack = Transform::new(into_handle(capture)).with_request_op(RequestOp::RemoveHeader {
446 name: "user-agent".into(),
447 });
448 let _ = SendPipe::call(&stack, build_request()).await.expect("call");
449 let guard = captured.lock().expect("captured");
450 let request = guard.as_ref().expect("captured request");
451 assert!(!request.metadata.contains_key("user-agent"));
452 }
453
454 #[proxima::test]
455 async fn rewrite_path_swaps_v1_to_v2() {
456 let (capture, captured) = Capture::new();
457 let pattern = Regex::new("^/v1/(.*)").expect("regex");
458 let stack = Transform::new(into_handle(capture)).with_request_op(RequestOp::RewritePath {
459 pattern,
460 template: "/v2/$1".into(),
461 });
462 let _ = SendPipe::call(&stack, build_request()).await.expect("call");
463 let guard = captured.lock().expect("captured");
464 let request = guard.as_ref().expect("captured request");
465 assert_eq!(request.path, "/v2/items/42");
466 }
467
468 #[proxima::test]
469 async fn response_remove_header_strips_header_from_response() {
470 let (capture, _) = Capture::new();
471 let stack =
472 Transform::new(into_handle(capture)).with_response_op(ResponseOp::RemoveHeader {
473 name: "server".into(),
474 });
475 let response = SendPipe::call(&stack, build_request()).await.expect("call");
476 assert!(!response.metadata.contains_key("server"));
477 }
478
479 #[test]
480 fn body_op_is_a_config_error_in_v1() {
481 let value = serde_json::json!({
482 "request_pipeline": [{"op": "body", "value": "x"}],
483 });
484 let outcome = Transform::from_spec(dummy(), &value);
485 assert!(matches!(outcome, Err(ProximaError::Config(_))));
486 }
487
488 #[test]
489 fn callback_op_is_a_config_error_until_chunk_2() {
490 let value = serde_json::json!({
491 "request_pipeline": [{"op": "callback", "name": "redact"}],
492 });
493 let outcome = Transform::from_spec(dummy(), &value);
494 assert!(matches!(outcome, Err(ProximaError::Config(_))));
495 }
496
497 #[test]
498 fn from_spec_parses_set_remove_rewrite_pipeline() {
499 let value = serde_json::json!({
500 "request_pipeline": [
501 {"op": "set_header", "name": "x-corr", "value": "abc"},
502 {"op": "remove_header", "name": "cookie"},
503 {"op": "rewrite_path", "pattern": "^/old/(.*)", "template": "/new/$1"},
504 ],
505 "response_pipeline": [
506 {"op": "set_header", "name": "x-served-by", "value": "proxima"},
507 ],
508 });
509 let transform = Transform::from_spec(dummy(), &value).expect("parse");
510 assert_eq!(transform.in_ops.len(), 3);
511 assert_eq!(transform.out_ops.len(), 1);
512 }
513
514 fn echo_body_pipe() -> PipeHandle {
515 struct EchoBody;
516 impl SendPipe for EchoBody {
517 type In = Request<Bytes>;
518 type Out = Response<Bytes>;
519 type Err = ProximaError;
520
521 fn call(
522 &self,
523 request: Request<Bytes>,
524 ) -> impl Future<Output = Result<Response<Bytes>, ProximaError>> + Send {
525 async move {
526 let (_, body) = request.body_bytes().await?;
527 Ok(Response::new(200).with_body(body))
528 }
529 }
530 }
531 into_handle(EchoBody)
532 }
533
534 fn body_request(body: &'static [u8]) -> Request<Bytes> {
535 Request::builder()
536 .method("POST")
537 .path("/")
538 .body(bytes::Bytes::from_static(body))
539 .build()
540 .expect("builder")
541 }
542
543 #[proxima::test]
544 async fn response_mutate_corrupts_the_body_deterministically() {
545 use crate::pipe::mutate::{MutateOp, Mutation};
546 const PAYLOAD: &[u8] = b"the quick brown fox jumps over the lazy dog";
547 let stack = Transform::new(echo_body_pipe()).with_response_op(ResponseOp::Mutate(
548 MutateOp::new(Mutation::BitFlip { bits: 3 }, 0x5EED),
549 ));
550
551 let response = SendPipe::call(&stack, body_request(PAYLOAD))
552 .await
553 .expect("call");
554 let corrupted = response.collect_body().await.expect("body");
555
556 let expected = Mutation::BitFlip { bits: 3 }.mutate(0x5EED, 0, PAYLOAD);
557 assert_eq!(
558 &corrupted[..],
559 &expected[..],
560 "transform corrupts with the seeded mutation"
561 );
562 assert_ne!(&corrupted[..], PAYLOAD, "body was actually corrupted");
563 }
564
565 #[test]
568 fn parity_fluent_builder_and_config_value_match() {
569 use crate::pipe::mutate::{MutateOp, Mutation};
570 let from_value: TransformConfig = serde_json::from_value(serde_json::json!({
571 "request_pipeline": [
572 {"op": "set_header", "name": "x-corr", "value": "abc"},
573 {"op": "rewrite_path", "pattern": "^/old/(.*)", "template": "/new/$1"},
574 ],
575 "response_pipeline": [
576 {"op": "mutate", "kind": "truncate", "seed": 11},
577 ],
578 }))
579 .expect("from_value");
580 let from_value = from_value
581 .into_transform(dummy())
582 .expect("into_transform value");
583
584 let from_builder = TransformConfig::new()
585 .with_request_op(RequestOpConfig::SetHeader {
586 name: "x-corr".into(),
587 value: "abc".into(),
588 })
589 .with_request_op(RequestOpConfig::RewritePath {
590 pattern: "^/old/(.*)".into(),
591 template: "/new/$1".into(),
592 })
593 .with_response_op(ResponseOpConfig::Mutate {
594 mutate: MutateOp::new(Mutation::Truncate, 11),
595 })
596 .into_transform(dummy())
597 .expect("into_transform builder");
598
599 assert_eq!(from_value.in_ops.len(), from_builder.in_ops.len());
600 assert_eq!(from_value.out_ops.len(), from_builder.out_ops.len());
601 match (&from_value.in_ops[1], &from_builder.in_ops[1]) {
602 (
603 RequestOp::RewritePath {
604 pattern: left_pattern,
605 template: left_template,
606 },
607 RequestOp::RewritePath {
608 pattern: right_pattern,
609 template: right_template,
610 },
611 ) => {
612 assert_eq!(left_pattern.as_str(), right_pattern.as_str());
613 assert_eq!(left_template, right_template);
614 }
615 other => panic!("expected matching rewrite_path ops, got {other:?}"),
616 }
617 match (&from_value.out_ops[0], &from_builder.out_ops[0]) {
618 (ResponseOp::Mutate(left), ResponseOp::Mutate(right)) => assert_eq!(left, right),
619 other => panic!("expected matching mutate ops, got {other:?}"),
620 }
621 }
622
623 #[test]
624 fn from_spec_parses_mutate_op_into_the_pipeline() {
625 use crate::pipe::mutate::{MutateOp, Mutation};
626 let value = serde_json::json!({
627 "response_pipeline": [
628 {"op": "mutate", "kind": "truncate", "seed": 11},
629 ],
630 });
631 let transform = Transform::from_spec(dummy(), &value).expect("parse");
632 assert_eq!(transform.out_ops.len(), 1);
633 match &transform.out_ops[0] {
634 ResponseOp::Mutate(op) => {
635 assert_eq!(
636 op,
637 &MutateOp::new(Mutation::Truncate, 11),
638 "parses the seeded mutate op"
639 );
640 }
641 other => panic!("expected a mutate op, got {other:?}"),
642 }
643 }
644
645 #[derive(Clone, PartialEq, Debug)]
646 struct CounterMsg(u64);
647
648 #[derive(Clone)]
649 enum CounterMsgOp {
650 Double,
651 AddConst(u64),
652 }
653
654 impl ApplyOps<CounterMsgOp> for CounterMsg {
655 fn apply(mut self, ops: &[CounterMsgOp]) -> Self {
656 for op in ops {
657 match op {
658 CounterMsgOp::Double => self.0 *= 2,
659 CounterMsgOp::AddConst(constant) => self.0 += constant,
660 }
661 }
662 self
663 }
664 }
665
666 #[derive(Clone)]
667 struct CounterSink;
668
669 impl SendPipe for CounterSink {
670 type In = CounterMsg;
671 type Out = CounterMsg;
672 type Err = ProximaError;
673
674 fn call(
675 &self,
676 input: CounterMsg,
677 ) -> impl Future<Output = Result<CounterMsg, ProximaError>> + Send {
678 async move { Ok(CounterMsg(input.0 + 1)) }
679 }
680 }
681
682 #[proxima::test]
683 async fn transform_is_generic_over_a_non_http_payload() {
684 let stack = Transform::new(CounterSink)
685 .with_request_op(CounterMsgOp::Double)
686 .with_response_op(CounterMsgOp::AddConst(10));
687 let out = SendPipe::call(&stack, CounterMsg(3)).await.expect("call");
688 assert_eq!(
689 out,
690 CounterMsg(17),
691 "Double(3)=6 -> sink=7 -> AddConst(10)=17"
692 );
693 }
694}