Skip to main content

stepflow_flow/workflow/
step_id.rs

1// Copyright 2025 DataStax Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
4// in compliance with the License. You may obtain a copy of the License at
5//
6//     http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software distributed under the License
9// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
10// or implied. See the License for the specific language governing permissions and limitations under
11// the License.
12
13use std::sync::Arc;
14
15use serde::Serialize;
16
17use crate::workflow::Flow;
18
19/// A step identifier that provides access to both the step index and name.
20///
21/// This type has two variants:
22/// - `WithFlow`: Used during execution when you have access to the flow.
23///   More efficient as it doesn't allocate for the name string.
24/// - `Standalone`: Used in storage/serialization contexts where the flow
25///   isn't available.
26///
27/// # Choosing a Constructor
28///
29/// When you have access to `Arc<Flow>`, prefer [`StepId::for_step()`] as it
30/// avoids allocating a string for the step name. Use [`StepId::new()`] only
31/// when the flow reference is not available (e.g., deserializing from storage).
32///
33/// # Note on Flow Lifetime
34///
35/// The `WithFlow` variant keeps the `Arc<Flow>` alive. If you need to store
36/// a `StepId` without keeping the flow alive, use [`StepId::into_standalone()`]
37/// or [`StepId::to_standalone()`].
38#[derive(Clone)]
39pub enum StepId {
40    /// Step with flow reference - borrows name from flow, no allocation.
41    WithFlow {
42        /// Reference to the flow containing this step.
43        flow: Arc<Flow>,
44        /// The step index in the workflow.
45        index: usize,
46    },
47    /// Standalone step - owns the name string.
48    Standalone {
49        /// The step name/ID.
50        name: String,
51        /// The step index in the workflow.
52        index: usize,
53    },
54}
55
56impl StepId {
57    /// Create a StepId from a flow reference.
58    ///
59    /// This is the preferred constructor when you have access to `Arc<Flow>`
60    /// as it avoids allocating a string for the step name.
61    pub fn for_step(flow: Arc<Flow>, index: usize) -> Self {
62        Self::WithFlow { flow, index }
63    }
64
65    /// Create a standalone StepId without a flow reference.
66    ///
67    /// Use this only when the flow reference is not available, such as when
68    /// deserializing from storage. If you have access to `Arc<Flow>`, prefer
69    /// [`StepId::for_step()`] instead to avoid the string allocation.
70    pub fn new(name: String, index: usize) -> Self {
71        Self::Standalone { name, index }
72    }
73
74    /// Get the step index.
75    pub fn index(&self) -> usize {
76        match self {
77            Self::WithFlow { index, .. } => *index,
78            Self::Standalone { index, .. } => *index,
79        }
80    }
81
82    /// Get the step name.
83    pub fn name(&self) -> &str {
84        match self {
85            Self::WithFlow { flow, index } => &flow.steps[*index].id,
86            Self::Standalone { name, .. } => name,
87        }
88    }
89
90    /// Convert to a standalone StepId, cloning if necessary.
91    ///
92    /// This releases the flow reference if held. Useful when you need to
93    /// store the StepId without keeping the flow alive, or when serializing.
94    pub fn to_standalone(&self) -> Self {
95        match self {
96            Self::WithFlow { flow, index } => Self::Standalone {
97                name: flow.steps[*index].id.clone(),
98                index: *index,
99            },
100            Self::Standalone { name, index } => Self::Standalone {
101                name: name.clone(),
102                index: *index,
103            },
104        }
105    }
106
107    /// Convert into a standalone StepId, consuming self.
108    ///
109    /// This releases the flow reference if held. More efficient than
110    /// [`to_standalone()`](Self::to_standalone) when you don't need the original.
111    pub fn into_standalone(self) -> Self {
112        match self {
113            Self::WithFlow { flow, index } => Self::Standalone {
114                name: flow.steps[index].id.clone(),
115                index,
116            },
117            standalone @ Self::Standalone { .. } => standalone,
118        }
119    }
120
121    /// Returns true if this StepId holds a flow reference.
122    pub fn has_flow(&self) -> bool {
123        matches!(self, Self::WithFlow { .. })
124    }
125
126    /// Get the flow reference if this is a WithFlow variant.
127    pub fn flow(&self) -> Option<&Arc<Flow>> {
128        match self {
129            Self::WithFlow { flow, .. } => Some(flow),
130            Self::Standalone { .. } => None,
131        }
132    }
133}
134
135// Equality is based on index and name, not flow identity.
136// Two StepIds are equal if they refer to the same step (same index and name).
137impl PartialEq for StepId {
138    fn eq(&self, other: &Self) -> bool {
139        self.index() == other.index() && self.name() == other.name()
140    }
141}
142
143impl Eq for StepId {}
144
145impl std::hash::Hash for StepId {
146    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
147        self.index().hash(state);
148        self.name().hash(state);
149    }
150}
151
152impl PartialOrd for StepId {
153    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
154        Some(self.cmp(other))
155    }
156}
157
158impl Ord for StepId {
159    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
160        self.index().cmp(&other.index())
161    }
162}
163
164impl std::fmt::Display for StepId {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        write!(f, "{}", self.name())
167    }
168}
169
170impl std::fmt::Debug for StepId {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        f.debug_struct("StepId")
173            .field("index", &self.index())
174            .field("name", &self.name())
175            .finish()
176    }
177}
178
179// Serialization: Serialize as just the step name string.
180// The index is an internal implementation detail not exposed in the API.
181impl Serialize for StepId {
182    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
183    where
184        S: serde::Serializer,
185    {
186        serializer.serialize_str(self.name())
187    }
188}
189
190// Note: StepId intentionally does NOT implement Deserialize.
191// When deserializing, use String for the step name and construct StepId
192// with the proper index from context (flow, database, etc.).
193// This prevents bugs where a deserialized StepId has an incorrect index.
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::ValueExpr;
199    use crate::workflow::builders::{FlowBuilder, StepBuilder};
200    use serde_json::json;
201
202    fn create_test_flow() -> Arc<Flow> {
203        let step_one = StepBuilder::new("step_one")
204            .component("/builtin/eval")
205            .input_literal(json!({"expression": "1 + 1"}))
206            .build();
207        let step_two = StepBuilder::new("step_two")
208            .component("/builtin/eval")
209            .input_literal(json!({"expression": "2 + 2"}))
210            .build();
211
212        let flow = FlowBuilder::new()
213            .step(step_one)
214            .step(step_two)
215            .output(ValueExpr::step_output("step_two"))
216            .build();
217
218        Arc::new(flow)
219    }
220
221    #[test]
222    fn test_for_step_creation() {
223        let flow = create_test_flow();
224        let step_id = StepId::for_step(flow.clone(), 0);
225
226        assert_eq!(step_id.index(), 0);
227        assert_eq!(step_id.name(), "step_one");
228        assert!(step_id.has_flow());
229    }
230
231    #[test]
232    fn test_standalone_creation() {
233        let step_id = StepId::new("my_step".to_string(), 5);
234
235        assert_eq!(step_id.index(), 5);
236        assert_eq!(step_id.name(), "my_step");
237        assert!(!step_id.has_flow());
238    }
239
240    #[test]
241    fn test_to_standalone() {
242        let flow = create_test_flow();
243        let step_id = StepId::for_step(flow, 1);
244        let standalone = step_id.to_standalone();
245
246        assert_eq!(standalone.index(), 1);
247        assert_eq!(standalone.name(), "step_two");
248        assert!(!standalone.has_flow());
249    }
250
251    #[test]
252    fn test_into_standalone() {
253        let flow = create_test_flow();
254        let step_id = StepId::for_step(flow, 0);
255        let standalone = step_id.into_standalone();
256
257        assert_eq!(standalone.index(), 0);
258        assert_eq!(standalone.name(), "step_one");
259        assert!(!standalone.has_flow());
260    }
261
262    #[test]
263    fn test_equality_across_variants() {
264        let flow = create_test_flow();
265        let with_flow = StepId::for_step(flow, 0);
266        let standalone = StepId::new("step_one".to_string(), 0);
267
268        assert_eq!(with_flow, standalone);
269    }
270
271    #[test]
272    fn test_inequality_different_index() {
273        let step1 = StepId::new("step".to_string(), 0);
274        let step2 = StepId::new("step".to_string(), 1);
275
276        assert_ne!(step1, step2);
277    }
278
279    #[test]
280    fn test_inequality_different_name() {
281        let step1 = StepId::new("step_a".to_string(), 0);
282        let step2 = StepId::new("step_b".to_string(), 0);
283
284        assert_ne!(step1, step2);
285    }
286
287    #[test]
288    fn test_ordering() {
289        let step0 = StepId::new("z".to_string(), 0);
290        let step1 = StepId::new("a".to_string(), 1);
291        let step2 = StepId::new("m".to_string(), 2);
292
293        assert!(step0 < step1);
294        assert!(step1 < step2);
295    }
296
297    #[test]
298    fn test_hash_consistency() {
299        use std::collections::hash_map::DefaultHasher;
300        use std::hash::{Hash, Hasher};
301
302        let flow = create_test_flow();
303        let with_flow = StepId::for_step(flow, 0);
304        let standalone = StepId::new("step_one".to_string(), 0);
305
306        let mut hasher1 = DefaultHasher::new();
307        with_flow.hash(&mut hasher1);
308        let hash1 = hasher1.finish();
309
310        let mut hasher2 = DefaultHasher::new();
311        standalone.hash(&mut hasher2);
312        let hash2 = hasher2.finish();
313
314        assert_eq!(hash1, hash2);
315    }
316
317    #[test]
318    fn test_serialize_as_string() {
319        let step_id = StepId::new("my_step".to_string(), 5);
320        let json = serde_json::to_string(&step_id).unwrap();
321        // Should serialize as just the step name string
322        assert_eq!(json, "\"my_step\"");
323    }
324
325    #[test]
326    fn test_serialize_with_flow() {
327        // Verify WithFlow variant also serializes as just the name
328        let flow = create_test_flow();
329        let step_id = StepId::for_step(flow, 1);
330        let json = serde_json::to_string(&step_id).unwrap();
331        assert_eq!(json, "\"step_two\"");
332    }
333
334    // Note: StepId intentionally does NOT implement Deserialize.
335    // This prevents bugs where deserialized StepIds have incorrect indices.
336    // Use String for deserialization and construct StepId with proper index from context.
337
338    #[test]
339    fn test_display() {
340        let step_id = StepId::new("my_step".to_string(), 3);
341        assert_eq!(format!("{}", step_id), "my_step");
342    }
343
344    #[test]
345    fn test_debug() {
346        let step_id = StepId::new("my_step".to_string(), 3);
347        let debug_str = format!("{:?}", step_id);
348        assert!(debug_str.contains("index: 3"));
349        assert!(debug_str.contains("name: \"my_step\""));
350    }
351}