Skip to main content

zrx_stream/stream/operator/
product.rs

1// Copyright (c) 2025-2026 Zensical and contributors
2
3// SPDX-License-Identifier: MIT
4// All contributions are certified under the DCO
5
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to
8// deal in the Software without restriction, including without limitation the
9// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10// sell copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12
13// The above copyright notice and this permission notice shall be included in
14// all copies or substantial portions of the Software.
15
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22// IN THE SOFTWARE.
23
24// ----------------------------------------------------------------------------
25
26//! Product operator.
27
28use std::marker::PhantomData;
29
30use zrx_scheduler::action::context::Binding;
31use zrx_scheduler::action::{Action, Context};
32use zrx_scheduler::step::{IntoSteps, Scoped};
33use zrx_scheduler::{Id, Value};
34
35use crate::stream::combinator::convert::IntoStreamTupleCons;
36use crate::stream::Stream;
37
38use super::Operator;
39
40// ----------------------------------------------------------------------------
41// Structs
42// ----------------------------------------------------------------------------
43
44/// Product operator.
45pub struct Product<T, U> {
46    /// Capture types.
47    marker: PhantomData<(T, U)>,
48}
49
50// ----------------------------------------------------------------------------
51// Implementations
52// ----------------------------------------------------------------------------
53
54impl<I, T> Stream<I, T>
55where
56    I: Id,
57    T: Value,
58{
59    /// Maps the stream using the provided function.
60    #[inline]
61    pub fn product<U>(&self, stream: &Stream<I, U>) -> Stream<I, (T, U)>
62    where
63        U: Value,
64    {
65        stream
66            .into_stream_tuple_cons(self.clone())
67            .subscribe(Product { marker: PhantomData })
68    }
69}
70
71// ----------------------------------------------------------------------------
72// Trait implementations
73// ----------------------------------------------------------------------------
74
75impl<I, T, U> Action<I> for Product<T, U>
76where
77    I: Id,
78    T: Value,
79    U: Value,
80{
81    type Inputs = (T, U);
82    type Output<'a> = (T, U);
83
84    /// Executes the operator.
85    fn execute(&mut self, ctx: Context<I, Self>) -> impl IntoSteps<I, Self> {
86        let Binding { scopes, inputs, mut output, .. } = ctx.bind();
87        scopes.into_iter().flat_map(move |scope| {
88            let (left, right) = *inputs;
89            let mut scoped = vec![];
90
91            // If the key exists in the left scope,
92            if let Some(l_value) = left.get(&scope) {
93                for (r_scope, r_value) in right.iter() {
94                    let combined = scope.concat(r_scope);
95                    output.insert(
96                        combined.clone(),
97                        (l_value.clone(), r_value.clone()),
98                    );
99                    scoped.push(Scoped::from(combined).done());
100                }
101            } else {
102                for r_scope in right.keys() {
103                    let combined = scope.concat(r_scope);
104                    output.remove(&combined);
105                    scoped.push(Scoped::from(combined).done());
106                }
107            }
108
109            // If the key exists in the left scope,
110            if let Some(r_value) = right.get(&scope) {
111                // omit double-emit
112                for (l_scope, l_value) in
113                    left.iter().filter(|(k, _)| *k != &*scope)
114                {
115                    let combined = scope.concat(l_scope);
116                    output.insert(
117                        combined.clone(),
118                        (l_value.clone(), r_value.clone()),
119                    );
120                    scoped.push(Scoped::from(combined).done());
121                }
122            } else {
123                for l_scope in left.keys() {
124                    let combined = l_scope.concat(&scope);
125                    output.remove(&combined);
126                    scoped.push(Scoped::from(combined).done());
127                }
128            }
129            scoped
130        })
131    }
132}