1use dex_protocol::dex::{FlowResult as ProtoFlowResult, Value as ProtoValue};
10
11use crate::{FlowErrorType, FlowStatus, SdkError, SdkResult, Value, value_mapper};
12
13#[derive(Clone, Debug)]
14pub struct StepCompletion {
16 pub step_type: String,
18 pub step_execution_id: String,
20 output: ProtoValue,
21}
22
23impl StepCompletion {
24 pub(crate) fn new(step_type: String, step_execution_id: String, output: ProtoValue) -> Self {
25 Self {
26 step_type,
27 step_execution_id,
28 output,
29 }
30 }
31
32 pub fn decode<Output: Value>(&self) -> SdkResult<Output> {
38 value_mapper::decode(&self.output)
39 }
40}
41
42#[derive(Clone, Debug)]
43pub struct FlowResult {
48 status: FlowStatus,
49 error_type: Option<FlowErrorType>,
50 error_message: Option<String>,
51 completions: Vec<StepCompletion>,
52}
53
54impl FlowResult {
55 pub(crate) fn new(
56 status: FlowStatus,
57 error_type: Option<FlowErrorType>,
58 error_message: Option<String>,
59 completions: Vec<StepCompletion>,
60 ) -> Self {
61 Self {
62 status,
63 error_type,
64 error_message,
65 completions,
66 }
67 }
68
69 pub(crate) fn from_proto(result: &ProtoFlowResult) -> SdkResult<Self> {
70 let status = crate::client::map_flow_status(result.flow_status)?;
71 let completions = result
72 .results
73 .iter()
74 .map(|completion| {
75 Ok(StepCompletion::new(
76 completion.completed_step_type.clone(),
77 completion.completed_step_execution_id.clone(),
78 completion.completed_step_output.clone().ok_or_else(|| {
79 SdkError::ValueMapping {
80 message: "Step completion output is required".to_string(),
81 }
82 })?,
83 ))
84 })
85 .collect::<SdkResult<Vec<_>>>()?;
86 Ok(Self::new(
87 status,
88 crate::client::map_flow_error_type(result.error_type),
89 (!result.error_message.is_empty()).then(|| result.error_message.clone()),
90 completions,
91 ))
92 }
93
94 pub fn status(&self) -> FlowStatus {
96 self.status
97 }
98
99 pub fn error_type(&self) -> Option<FlowErrorType> {
101 self.error_type
102 }
103
104 pub fn error_message(&self) -> Option<&str> {
106 self.error_message.as_deref()
107 }
108
109 pub fn is_terminal(&self) -> bool {
111 !matches!(
112 self.status,
113 FlowStatus::Running | FlowStatus::ContinuedAsNew
114 )
115 }
116
117 pub fn completions(&self) -> &[StepCompletion] {
121 &self.completions
122 }
123
124 pub fn single_output<Output: Value>(&self) -> SdkResult<Output> {
131 if !self.is_terminal() {
132 return Err(SdkError::InvalidArgument {
133 message: "Flow result is not terminal".to_string(),
134 });
135 }
136 if self.completions().len() != 1 {
137 return Err(SdkError::InvalidArgument {
138 message: format!(
139 "expected exactly one Step output, found {}",
140 self.completions().len()
141 ),
142 });
143 }
144 self.completions[0].decode()
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use dex_protocol::dex::value;
151
152 use super::*;
153
154 #[test]
155 fn preserves_heterogeneous_completions_and_rejects_ambiguous_single_output() {
156 let result = FlowResult::new(
157 crate::FlowStatus::Completed,
158 None,
159 None,
160 vec![
161 completion(
162 "First",
163 "First-1",
164 value::Kind::StringValue("one".to_string()),
165 ),
166 completion("Second", "Second-2", value::Kind::BoolValue(true)),
167 ],
168 );
169
170 assert_eq!(result.completions()[0].step_type, "First");
171 assert_eq!(result.completions()[1].step_execution_id, "Second-2");
172 assert_eq!(result.completions()[0].decode::<String>().unwrap(), "one");
173 assert!(result.completions()[1].decode::<bool>().unwrap());
174 assert!(matches!(
175 result.single_output::<String>(),
176 Err(SdkError::InvalidArgument { .. })
177 ));
178 }
179
180 #[test]
181 fn single_output_requires_exactly_one_completion() {
182 let single = FlowResult::new(
183 crate::FlowStatus::Completed,
184 None,
185 None,
186 vec![completion("Only", "Only-1", value::Kind::IntValue(7))],
187 );
188 assert_eq!(single.single_output::<i64>().unwrap(), 7);
189
190 let empty = FlowResult::new(crate::FlowStatus::Completed, None, None, Vec::new());
191 assert!(matches!(
192 empty.single_output::<String>(),
193 Err(SdkError::InvalidArgument { .. })
194 ));
195 }
196
197 fn completion(step_type: &str, step_execution_id: &str, kind: value::Kind) -> StepCompletion {
198 StepCompletion::new(
199 step_type.to_string(),
200 step_execution_id.to_string(),
201 ProtoValue { kind: Some(kind) },
202 )
203 }
204}