1use std::any::TypeId;
10use std::collections::BTreeMap;
11use std::sync::Arc;
12use std::time::{Duration, SystemTime};
13
14use dex_protocol::dex::flow_service_client::FlowServiceClient;
15use dex_protocol::dex::{
16 ActiveStepSearchMode as ProtoSearchMode, AttributeWrite, FlowAlreadyStartedOptions,
17 FlowConfig as ProtoFlowConfig, FlowResetType, FlowRetryPolicy, FlowStartOptions,
18 FlowStatus as ProtoFlowStatus, GetAttributesRequest, GetFlowSummaryRequest,
19 IdReusePolicy as ProtoIdReusePolicy, InvokeRpcRequest, PublishToChannelRequest,
20 ResetFlowRequest, SearchFlowsRequest, SetAttributesRequest, SkipTimerRequest, StartFlowRequest,
21 StepDurability as ProtoStepDurability, StopFlowRequest, StopType as ProtoStopType,
22 TriggerContinueAsNewRequest, UpdateFlowConfigRequest, WaitForAttributeCondition,
23 WaitForAttributeEqual, WaitForAttributeRequest, WaitForFlowRequest, WaitForFlowResponse,
24 WaitForStepCompletionRequest, WorkerTarget as ProtoWorkerTarget, wait_for_attribute_condition,
25};
26use tokio::runtime::Runtime;
27use tonic::transport::Endpoint;
28use uuid::Uuid;
29
30use crate::reset_flow_options::ResetPoint;
31use crate::sdk_error::{FlowTargetRequirement, ServiceError};
32use crate::stop_flow_options::StopType;
33use crate::value_hydrator::ValueHydrator;
34use crate::value_mapper;
35use crate::worker_dispatcher::map_step_options;
36use crate::{
37 ActiveStepSearchMode, Attribute, AttributeMap, BlobCache, Channel, ChannelMap, ClientOptions,
38 Flow, FlowConfig, FlowErrorType, FlowInfo, FlowStatus, IdReusePolicy, Registry,
39 ResetFlowOptions, RetryPolicy, Rpc, SdkError, SdkResult, SearchFlowEntry, SearchFlowsPage,
40 StartFlowOptions, StepDurability, StepExecutionId, StopFlowOptions, TimerId, Value,
41 WorkerTarget,
42};
43
44pub struct Client {
64 runtime: Runtime,
65 registry: Registry,
66 service: FlowServiceClient<tonic::transport::Channel>,
67 hydrator: ValueHydrator,
68 options: ClientOptions,
69}
70
71impl Client {
72 pub fn new(registry: Registry, blob_cache: Arc<BlobCache>, options: ClientOptions) -> Self {
80 Self::try_new(registry, blob_cache, options)
81 .unwrap_or_else(|error| panic!("cannot create Dex Client: {error}"))
82 }
83
84 pub fn try_new(
91 registry: Registry,
92 blob_cache: Arc<BlobCache>,
93 options: ClientOptions,
94 ) -> SdkResult<Self> {
95 let runtime = Runtime::new().map_err(service_error)?;
96 let endpoint = Endpoint::from_shared(endpoint_address(options.server_address_value()))
97 .map_err(service_error)?;
98 let service = {
99 let _runtime_guard = runtime.enter();
100 FlowServiceClient::new(endpoint.connect_lazy())
101 };
102 let hydrator = ValueHydrator::new(service.clone(), blob_cache);
103 Ok(Self {
104 runtime,
105 registry,
106 service,
107 hydrator,
108 options,
109 })
110 }
111
112 pub fn start_flow<SomeFlow: Flow>(
121 &self,
122 flow: &SomeFlow,
123 flow_id: &str,
124 input: SomeFlow::StartInput,
125 ) -> SdkResult<String> {
126 self.start_flow_with_options(flow, flow_id, input, StartFlowOptions::new())
127 }
128
129 pub fn start_flow_with_options<SomeFlow: Flow>(
138 &self,
139 flow: &SomeFlow,
140 flow_id: &str,
141 input: SomeFlow::StartInput,
142 options: StartFlowOptions,
143 ) -> SdkResult<String> {
144 require_name(flow_id, "Flow ID")?;
145 let registered = self.registry.flow(flow.flow_type())?;
146 if registered.type_id != TypeId::of::<SomeFlow>() {
147 return Err(SdkError::FlowDefinition {
148 message: format!(
149 "Flow {} does not match the registered Rust type",
150 flow.flow_type()
151 ),
152 });
153 }
154 let (start_step_type, step_input, step_options) = match ®istered.start_step {
155 Some(step) => (
156 step.name.to_string(),
157 Some(value_mapper::encode(&input)?),
158 Some(
159 map_step_options(
160 registered,
161 registered
162 .handler
163 .step_options(step.name)
164 .map_err(sdk_handler_error)?,
165 )
166 .map_err(sdk_handler_error)?,
167 ),
168 ),
169 None => (String::new(), None, None),
170 };
171 let request = StartFlowRequest {
172 flow_id: flow_id.to_string(),
173 flow_type: registered.name.to_string(),
174 flow_timeout_seconds: optional_seconds(options.timeout)?,
175 start_step_type,
176 step_input,
177 step_options,
178 flow_start_options: Some(self.map_start_options(registered, &options)?),
179 request_id: options
180 .request_id
181 .clone()
182 .unwrap_or_else(|| Uuid::new_v4().to_string()),
183 };
184 let mut service = self.service.clone();
185 self.runtime.block_on(async {
186 service
187 .start_flow(request)
188 .await
189 .map(|response| response.into_inner().run_id)
190 .map_err(|status| {
191 SdkError::from_status(
192 status,
193 "start_flow",
194 Some(flow_id),
195 FlowTargetRequirement::None,
196 )
197 })
198 })
199 }
200
201 pub fn invoke_rpc<Input: Value, Output: Value>(
208 &self,
209 flow_id: &str,
210 rpc: Rpc<Input, Output>,
211 input: Input,
212 ) -> SdkResult<Output> {
213 self.do_invoke_rpc(flow_id, rpc.name(), &input)
214 }
215
216 pub fn invoke_rpc_without_input<Output: Value>(
220 &self,
221 flow_id: &str,
222 rpc: Rpc<(), Output>,
223 ) -> SdkResult<Output> {
224 self.do_invoke_rpc(flow_id, rpc.name(), &())
225 }
226
227 pub fn get_attribute<T: Value>(
231 &self,
232 flow_id: &str,
233 attribute: &Attribute<T>,
234 ) -> SdkResult<Option<T>> {
235 self.get_attribute_value(flow_id, attribute.name())
236 }
237
238 pub fn get_attribute_map<T: Value>(
242 &self,
243 flow_id: &str,
244 attribute: &AttributeMap<T>,
245 instance: &str,
246 ) -> SdkResult<Option<T>> {
247 self.get_attribute_value(
248 flow_id,
249 &crate::registry::physical_name(attribute.name(), instance),
250 )
251 }
252
253 pub fn set_attribute<T: Value>(
259 &self,
260 flow_id: &str,
261 attribute: &Attribute<T>,
262 value: T,
263 ) -> SdkResult<()> {
264 self.set_attribute_value(
265 flow_id,
266 attribute.name(),
267 &value,
268 attribute.index().map(|index| index.proto_config(false)),
269 attribute.sync_config(),
270 )
271 }
272
273 pub fn set_attribute_map<T: Value>(
279 &self,
280 flow_id: &str,
281 attribute: &AttributeMap<T>,
282 instance: &str,
283 value: T,
284 ) -> SdkResult<()> {
285 self.set_attribute_value(
286 flow_id,
287 &crate::registry::physical_name(attribute.name(), instance),
288 &value,
289 attribute.index().map(|index| index.proto_config(true)),
290 attribute.sync_config(),
291 )
292 }
293
294 pub fn publish<T: Value>(
296 &self,
297 flow_id: &str,
298 channel: &Channel<T>,
299 value: T,
300 ) -> SdkResult<()> {
301 self.publish_values(flow_id, channel.name(), [value])
302 }
303
304 pub fn publish_many<T: Value>(
306 &self,
307 flow_id: &str,
308 channel: &Channel<T>,
309 values: impl IntoIterator<Item = T>,
310 ) -> SdkResult<()> {
311 self.publish_values(flow_id, channel.name(), values)
312 }
313
314 pub fn publish_map<T: Value>(
316 &self,
317 flow_id: &str,
318 channel: &ChannelMap<T>,
319 instance: &str,
320 values: impl IntoIterator<Item = T>,
321 ) -> SdkResult<()> {
322 self.publish_values(
323 flow_id,
324 &crate::registry::physical_name(channel.name(), instance),
325 values,
326 )
327 }
328
329 pub fn wait_for_flow<Output: Value>(&self, flow_id: &str) -> SdkResult<Output> {
336 self.wait_for_flow_response(flow_id, None)
337 .and_then(|response| self.decode_flow_output(response))
338 }
339
340 pub fn wait_for_flow_with_timeout<Output: Value>(
347 &self,
348 flow_id: &str,
349 timeout: Duration,
350 ) -> SdkResult<Output> {
351 self.wait_for_flow_response(flow_id, Some(timeout))
352 .and_then(|response| self.decode_flow_output(response))
353 }
354
355 pub fn describe_flow(&self, flow_id: &str) -> SdkResult<FlowInfo> {
361 let mut service = self.service.clone();
362 let response = self.runtime.block_on(async {
363 service
364 .get_flow_summary(GetFlowSummaryRequest {
365 flow_id: flow_id.to_string(),
366 run_id: String::new(),
367 })
368 .await
369 .map(|response| response.into_inner())
370 .map_err(|status| {
371 SdkError::from_status(
372 status,
373 "describe_flow",
374 Some(flow_id),
375 FlowTargetRequirement::Existing,
376 )
377 })
378 })?;
379 let execution = response
380 .flow_execution_id
381 .ok_or_else(|| service_error("GetFlowSummary omitted FlowExecutionID"))?;
382 Ok(FlowInfo {
383 flow_id: execution.flow_id,
384 run_id: execution.run_id,
385 flow_type: response.flow_type,
386 status: map_flow_status(response.flow_status)?,
387 started_at: response
388 .start_time
389 .map_or(SystemTime::UNIX_EPOCH, timestamp),
390 })
391 }
392
393 pub fn search_flows(&self, query: &str, page_size: i32) -> SdkResult<SearchFlowsPage> {
398 self.search_flows_page(query, page_size, "")
399 }
400
401 pub fn search_flows_page(
406 &self,
407 query: &str,
408 page_size: i32,
409 next_page_token: &str,
410 ) -> SdkResult<SearchFlowsPage> {
411 if page_size < 0 {
412 return Err(invalid("search page size must not be negative"));
413 }
414 let mut service = self.service.clone();
415 self.runtime.block_on(async {
416 let response = service
417 .search_flows(SearchFlowsRequest {
418 query: query.to_string(),
419 page_size,
420 next_page_token: next_page_token.to_string(),
421 })
422 .await
423 .map(|response| response.into_inner())
424 .map_err(|status| {
425 SdkError::from_status(status, "search_flows", None, FlowTargetRequirement::None)
426 })?;
427 let values = response
428 .flow_runs
429 .iter()
430 .flat_map(|entry| entry.indexed_attributes.iter())
431 .map(|attribute| {
432 attribute
433 .value
434 .clone()
435 .ok_or_else(|| invalid("Indexed Attribute has no Value"))
436 })
437 .collect::<SdkResult<Vec<_>>>()?;
438 let mut values = self.hydrator.hydrate_all(values).await?.into_iter();
439 let mut flows = Vec::with_capacity(response.flow_runs.len());
440 for entry in response.flow_runs {
441 let mut indexed_attributes = BTreeMap::new();
442 for attribute in entry.indexed_attributes {
443 let value = values
444 .next()
445 .ok_or_else(|| invalid("Indexed Attribute hydration count mismatch"))?;
446 indexed_attributes.insert(attribute.key, value_mapper::decode_untyped(&value)?);
447 }
448 flows.push(SearchFlowEntry {
449 flow_id: entry.flow_id,
450 run_id: entry.run_id,
451 flow_type: entry.flow_type,
452 status: map_flow_status(entry.flow_status)?,
453 started_at: entry.start_time.map(timestamp),
454 closed_at: entry.close_time.map(timestamp),
455 indexed_attributes,
456 });
457 }
458 Ok(SearchFlowsPage {
459 flows,
460 next_page_token: response.next_page_token,
461 })
462 })
463 }
464
465 pub fn stop_flow(&self, flow_id: &str, options: StopFlowOptions) -> SdkResult<()> {
471 let stop_type = match options.stop_type {
472 StopType::Cancel => ProtoStopType::Cancel,
473 StopType::Terminate => ProtoStopType::Terminate,
474 StopType::Fail => ProtoStopType::Fail,
475 };
476 self.call_empty(
477 "stop_flow",
478 Some(flow_id),
479 FlowTargetRequirement::Active,
480 |mut service| async move {
481 service
482 .stop_flow(StopFlowRequest {
483 flow_id: flow_id.to_string(),
484 run_id: String::new(),
485 reason: options.reason.unwrap_or_default(),
486 stop_type: stop_type as i32,
487 })
488 .await
489 },
490 )
491 }
492
493 pub fn reset_flow(&self, flow_id: &str, options: ResetFlowOptions) -> SdkResult<String> {
502 let mut request = ResetFlowRequest {
503 flow_id: flow_id.to_string(),
504 run_id: String::new(),
505 reset_type: FlowResetType::Unspecified as i32,
506 history_event_id: 0,
507 reason: options.reason.unwrap_or_default(),
508 history_event_time: String::new(),
509 step_type: String::new(),
510 step_execution_id: String::new(),
511 skip_channel_messages_reapply: options.skip_channel_messages_reapply,
512 skip_locking_rpc_reapply: options.skip_locking_rpc_reapply,
513 };
514 match options.point {
515 ResetPoint::Beginning => request.reset_type = FlowResetType::Beginning as i32,
516 ResetPoint::HistoryEventId(event_id) => {
517 request.reset_type = FlowResetType::HistoryEventId as i32;
518 request.history_event_id = i32::try_from(event_id)
519 .map_err(|_| invalid("history event ID exceeds int32"))?;
520 }
521 ResetPoint::HistoryEventTime(time) => {
522 request.reset_type = FlowResetType::HistoryEventTime as i32;
523 request.history_event_time = rfc3339(time)?;
524 }
525 ResetPoint::StepType(step_type) => {
526 request.reset_type = FlowResetType::StepType as i32;
527 request.step_type = step_type.to_string();
528 }
529 ResetPoint::StepExecution(execution) => {
530 request.reset_type = FlowResetType::StepExecutionId as i32;
531 request.step_execution_id =
532 format!("{}-{}", execution.step_type, execution.execution_number);
533 }
534 }
535 let mut service = self.service.clone();
536 self.runtime.block_on(async {
537 service
538 .reset_flow(request)
539 .await
540 .map(|response| response.into_inner().run_id)
541 .map_err(|status| {
542 SdkError::from_status(
543 status,
544 "reset_flow",
545 Some(flow_id),
546 FlowTargetRequirement::Existing,
547 )
548 })
549 })
550 }
551
552 pub fn skip_timer(
559 &self,
560 flow_id: &str,
561 step_execution: StepExecutionId,
562 timer: TimerId,
563 ) -> SdkResult<()> {
564 let (timer_condition_id, timer_condition_index) = match timer {
565 TimerId::ConditionId(id) => (id, None),
566 TimerId::ConditionIndex(index) => (
567 String::new(),
568 Some(i32::try_from(index).map_err(|_| invalid("timer index exceeds int32"))?),
569 ),
570 };
571 self.call_empty(
572 "skip_timer",
573 Some(flow_id),
574 FlowTargetRequirement::Active,
575 |mut service| async move {
576 service
577 .skip_timer(SkipTimerRequest {
578 flow_id: flow_id.to_string(),
579 run_id: String::new(),
580 step_execution_id: format!(
581 "{}-{}",
582 step_execution.step_type, step_execution.execution_number
583 ),
584 timer_condition_id,
585 timer_condition_index,
586 })
587 .await
588 },
589 )
590 }
591
592 pub fn wait_for_step_completion(
599 &self,
600 flow_id: &str,
601 step_execution: StepExecutionId,
602 timeout: Duration,
603 ) -> SdkResult<()> {
604 let wait_time_seconds = seconds32(timeout)?;
605 self.call_empty(
606 "wait_for_step_completion",
607 Some(flow_id),
608 FlowTargetRequirement::Active,
609 |mut service| async move {
610 service
611 .wait_for_step_completion(WaitForStepCompletionRequest {
612 flow_id: flow_id.to_string(),
613 step_type: step_execution.step_type.to_string(),
614 step_execution_number: step_execution.execution_number.to_string(),
615 wait_time_seconds,
616 request_id: Uuid::new_v4().to_string(),
617 })
618 .await
619 },
620 )
621 }
622
623 pub fn wait_for_attribute_equal<T: Value>(
629 &self,
630 flow_id: &str,
631 attribute: &Attribute<T>,
632 expected: T,
633 timeout: Duration,
634 ) -> SdkResult<()> {
635 self.wait_for_attribute_value(flow_id, attribute.name(), &expected, timeout)
636 }
637
638 pub fn wait_for_attribute_map_equal<T: Value>(
643 &self,
644 flow_id: &str,
645 attribute: &AttributeMap<T>,
646 instance: &str,
647 expected: T,
648 timeout: Duration,
649 ) -> SdkResult<()> {
650 self.wait_for_attribute_value(
651 flow_id,
652 &crate::registry::physical_name(attribute.name(), instance),
653 &expected,
654 timeout,
655 )
656 }
657
658 fn wait_for_attribute_value<T: Value>(
659 &self,
660 flow_id: &str,
661 key: &str,
662 expected: &T,
663 timeout: Duration,
664 ) -> SdkResult<()> {
665 let value = value_mapper::encode(expected)?;
666 if !matches!(
667 value.kind,
668 Some(dex_protocol::dex::value::Kind::StringValue(_))
669 | Some(dex_protocol::dex::value::Kind::BoolValue(_))
670 | Some(dex_protocol::dex::value::Kind::IntValue(_))
671 | Some(dex_protocol::dex::value::Kind::DoubleValue(_))
672 ) {
673 return Err(invalid(
674 "wait_for_attribute_equal supports only scalar values",
675 ));
676 }
677 let wait_time_seconds = seconds32(timeout)?;
678 self.call_empty(
679 "wait_for_attribute_equal",
680 Some(flow_id),
681 FlowTargetRequirement::Active,
682 |mut service| async move {
683 service
684 .wait_for_attribute(WaitForAttributeRequest {
685 flow_id: flow_id.to_string(),
686 run_id: String::new(),
687 condition: Some(WaitForAttributeCondition {
688 kind: Some(wait_for_attribute_condition::Kind::Equal(
689 WaitForAttributeEqual {
690 key: key.to_string(),
691 value: Some(value),
692 },
693 )),
694 }),
695 wait_time_seconds,
696 request_id: Uuid::new_v4().to_string(),
697 })
698 .await
699 },
700 )
701 }
702
703 pub fn update_flow_config(&self, flow_id: &str, config: FlowConfig) -> SdkResult<()> {
707 let flow_config = self.map_flow_config(Some(&config))?;
708 self.call_empty(
709 "update_flow_config",
710 Some(flow_id),
711 FlowTargetRequirement::Active,
712 |mut service| async move {
713 service
714 .update_flow_config(UpdateFlowConfigRequest {
715 flow_id: flow_id.to_string(),
716 run_id: String::new(),
717 flow_config: Some(flow_config),
718 })
719 .await
720 },
721 )
722 }
723
724 pub fn trigger_continue_as_new(&self, flow_id: &str) -> SdkResult<()> {
726 self.call_empty(
727 "trigger_continue_as_new",
728 Some(flow_id),
729 FlowTargetRequirement::Active,
730 |mut service| async move {
731 service
732 .trigger_continue_as_new(TriggerContinueAsNewRequest {
733 flow_id: flow_id.to_string(),
734 run_id: String::new(),
735 })
736 .await
737 },
738 )
739 }
740
741 pub fn health_check(&self) -> SdkResult<()> {
745 self.call_empty(
746 "health_check",
747 None,
748 FlowTargetRequirement::None,
749 |mut service| async move { service.health_check(()).await },
750 )
751 }
752
753 fn do_invoke_rpc<Input: Value, Output: Value>(
754 &self,
755 flow_id: &str,
756 rpc_name: &str,
757 input: &Input,
758 ) -> SdkResult<Output> {
759 let rpc = self.registry.rpc(rpc_name)?;
760 let request = InvokeRpcRequest {
761 flow_id: flow_id.to_string(),
762 run_id: String::new(),
763 rpc_name: rpc_name.to_string(),
764 input: Some(value_mapper::encode(input)?),
765 timeout_seconds: optional_seconds(rpc.timeout)?,
766 lock_attribute_keys: rpc.locks.iter().map(|lock| lock.physical_name()).collect(),
767 request_id: Uuid::new_v4().to_string(),
768 };
769 let mut service = self.service.clone();
770 let output = self.runtime.block_on(async {
771 service
772 .invoke_rpc(request)
773 .await
774 .map(|response| response.into_inner().output)
775 .map_err(|status| {
776 SdkError::from_status(
777 status,
778 "invoke_rpc",
779 Some(flow_id),
780 FlowTargetRequirement::Active,
781 )
782 })
783 })?;
784 let output = output.ok_or_else(|| invalid("InvokeRPC omitted output"))?;
785 let output = self.runtime.block_on(self.hydrator.hydrate(output))?;
786 value_mapper::decode(&output)
787 }
788
789 fn get_attribute_value<T: Value>(&self, flow_id: &str, key: &str) -> SdkResult<Option<T>> {
790 let mut service = self.service.clone();
791 let response = self.runtime.block_on(async {
792 service
793 .get_attributes(GetAttributesRequest {
794 flow_id: flow_id.to_string(),
795 run_id: String::new(),
796 keys: vec![key.to_string()],
797 all_keys: false,
798 })
799 .await
800 .map(|response| response.into_inner())
801 .map_err(|status| {
802 SdkError::from_status(
803 status,
804 "get_attribute",
805 Some(flow_id),
806 FlowTargetRequirement::Existing,
807 )
808 })
809 })?;
810 let Some(entry) = response.attributes.into_iter().next() else {
811 return Ok(None);
812 };
813 let value = entry
814 .value
815 .ok_or_else(|| invalid("GetAttributes returned an empty Value"))?;
816 let value = self.runtime.block_on(self.hydrator.hydrate(value))?;
817 value_mapper::decode(&value).map(Some)
818 }
819
820 fn set_attribute_value<T: Value>(
821 &self,
822 flow_id: &str,
823 key: &str,
824 value: &T,
825 index_config: Option<dex_protocol::dex::IndexConfig>,
826 sync_config: Option<dex_protocol::dex::AttributeSyncConfig>,
827 ) -> SdkResult<()> {
828 let write = AttributeWrite {
829 key: key.to_string(),
830 value: Some(value_mapper::encode(value)?),
831 index_config,
832 sync_config,
833 };
834 self.call_empty(
835 "set_attribute",
836 Some(flow_id),
837 FlowTargetRequirement::Active,
838 |mut service| async move {
839 service
840 .set_attributes(SetAttributesRequest {
841 flow_id: flow_id.to_string(),
842 run_id: String::new(),
843 attributes: vec![write],
844 request_id: Uuid::new_v4().to_string(),
845 })
846 .await
847 },
848 )
849 }
850
851 fn publish_values<T: Value>(
852 &self,
853 flow_id: &str,
854 channel_name: &str,
855 values: impl IntoIterator<Item = T>,
856 ) -> SdkResult<()> {
857 let messages = values
858 .into_iter()
859 .map(|value| {
860 Ok(dex_protocol::dex::ChannelMessage {
861 channel_name: channel_name.to_string(),
862 value: Some(value_mapper::encode(&value)?),
863 })
864 })
865 .collect::<SdkResult<Vec<_>>>()?;
866 self.call_empty(
867 "publish",
868 Some(flow_id),
869 FlowTargetRequirement::Active,
870 |mut service| async move {
871 service
872 .publish_to_channel(PublishToChannelRequest {
873 flow_id: flow_id.to_string(),
874 run_id: String::new(),
875 messages,
876 })
877 .await
878 },
879 )
880 }
881
882 fn wait_for_flow_response(
883 &self,
884 flow_id: &str,
885 timeout: Option<Duration>,
886 ) -> SdkResult<WaitForFlowResponse> {
887 let mut service = self.service.clone();
888 let response = self.runtime.block_on(async {
889 service
890 .wait_for_flow(WaitForFlowRequest {
891 flow_id: flow_id.to_string(),
892 run_id: String::new(),
893 needs_results: true,
894 wait_time_seconds: optional_seconds(timeout)?,
895 })
896 .await
897 .map(|response| response.into_inner())
898 .map_err(|status| {
899 SdkError::from_status(
900 status,
901 "wait_for_flow",
902 Some(flow_id),
903 FlowTargetRequirement::Existing,
904 )
905 })
906 });
907 let response = response?;
908 let status = map_flow_status(response.flow_status)?;
909 if status != FlowStatus::Completed {
910 let flow = self.describe_flow(flow_id)?;
911 return Err(SdkError::FlowUncompleted {
912 run_id: flow.run_id,
913 status,
914 error_type: map_flow_error_type(response.error_type),
915 message: (!response.error_message.is_empty()).then_some(response.error_message),
916 result_count: response.results.len(),
917 });
918 }
919 Ok(response)
920 }
921
922 fn decode_flow_output<Output: Value>(
923 &self,
924 response: WaitForFlowResponse,
925 ) -> SdkResult<Output> {
926 let output = response
927 .results
928 .into_iter()
929 .rev()
930 .find_map(|result| result.completed_step_output)
931 .ok_or_else(|| invalid("completed Flow has no Step output"))?;
932 let output = self.runtime.block_on(self.hydrator.hydrate(output))?;
933 value_mapper::decode(&output)
934 }
935
936 fn map_start_options(
937 &self,
938 flow: &crate::registry::RegisteredFlow,
939 options: &StartFlowOptions,
940 ) -> SdkResult<FlowStartOptions> {
941 let attributes = options
942 .attributes
943 .iter()
944 .map(|attribute| {
945 let logical_name = attribute.key.split('/').next().unwrap_or(&attribute.key);
946 if !flow.persistence.contains_key(logical_name) {
947 return Err(invalid(format!(
948 "initial Attribute is not registered: {}",
949 attribute.key
950 )));
951 }
952 Ok(AttributeWrite {
953 key: attribute.key.clone(),
954 value: Some(attribute.value.encode()?),
955 index_config: attribute.index_config.clone(),
956 sync_config: attribute.sync_config,
957 })
958 })
959 .collect::<SdkResult<Vec<_>>>()?;
960 let config = options.config_override.as_ref();
961 let flow_config_override =
962 if config.is_some() || self.options.worker_target_value().is_some() {
963 Some(self.map_flow_config(config)?)
964 } else {
965 None
966 };
967 Ok(FlowStartOptions {
968 id_reuse_policy: match options.id_reuse_policy {
969 IdReusePolicy::Default => ProtoIdReusePolicy::Unspecified,
970 IdReusePolicy::AllowIfPreviousFailed => {
971 ProtoIdReusePolicy::AllowIfPreviousExistsAbnormally
972 }
973 IdReusePolicy::AllowIfNotRunning => ProtoIdReusePolicy::AllowIfNoRunning,
974 IdReusePolicy::Disallow => ProtoIdReusePolicy::DisallowReuse,
975 IdReusePolicy::TerminateIfRunning => ProtoIdReusePolicy::AllowTerminateIfRunning,
976 } as i32,
977 cron_schedule: options.cron_schedule.clone().unwrap_or_default(),
978 flow_start_delay_seconds: optional_seconds(options.start_delay)?,
979 retry_policy: options
980 .retry_policy
981 .clone()
982 .map(map_flow_retry)
983 .transpose()?,
984 attributes,
985 flow_config_override,
986 flow_already_started_options: Some(FlowAlreadyStartedOptions {
987 ignore_already_started_error: options.ignore_already_started,
988 }),
989 })
990 }
991
992 fn map_flow_config(&self, config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
993 map_flow_config(config, self.options.worker_target_value())
994 }
995
996 fn call_empty<Response, Future, Call>(
997 &self,
998 operation: &'static str,
999 flow_id: Option<&str>,
1000 requirement: FlowTargetRequirement,
1001 call: Call,
1002 ) -> SdkResult<()>
1003 where
1004 Call: FnOnce(FlowServiceClient<tonic::transport::Channel>) -> Future,
1005 Future: std::future::Future<Output = Result<tonic::Response<Response>, tonic::Status>>,
1006 {
1007 let service = self.service.clone();
1008 self.runtime
1009 .block_on(call(service))
1010 .map(|_| ())
1011 .map_err(|status| SdkError::from_status(status, operation, flow_id, requirement))
1012 }
1013}
1014
1015fn map_flow_config(
1016 config: Option<&FlowConfig>,
1017 default_target: Option<&WorkerTarget>,
1018) -> SdkResult<ProtoFlowConfig> {
1019 let target = config
1020 .and_then(|config| config.worker_target.as_ref())
1021 .or(default_target);
1022 Ok(ProtoFlowConfig {
1023 active_step_search_mode: config
1024 .and_then(|config| config.active_step_search_mode)
1025 .map(|mode| match mode {
1026 ActiveStepSearchMode::All => ProtoSearchMode::EnabledForAll as i32,
1027 ActiveStepSearchMode::WithWaitFor => {
1028 ProtoSearchMode::EnabledForStepsWithWaitFor as i32
1029 }
1030 ActiveStepSearchMode::Disabled => ProtoSearchMode::Disabled as i32,
1031 }),
1032 continue_as_new_threshold: config
1033 .and_then(|config| config.continue_as_new_threshold)
1034 .map(i32::try_from)
1035 .transpose()
1036 .map_err(|_| invalid("continue-as-new threshold exceeds int32"))?,
1037 continue_as_new_page_size_in_bytes: config
1038 .and_then(|config| config.continue_as_new_page_size_bytes)
1039 .map(i32::try_from)
1040 .transpose()
1041 .map_err(|_| invalid("continue-as-new page size exceeds int32"))?,
1042 step_durability: config
1043 .and_then(|config| config.step_durability)
1044 .map(|durability| {
1045 (match durability {
1046 StepDurability::Default => ProtoStepDurability::Unspecified,
1047 StepDurability::Sync => ProtoStepDurability::Sync,
1048 StepDurability::Async => ProtoStepDurability::Async,
1049 }) as i32
1050 }),
1051 worker_target: target.map(map_worker_target),
1052 attribute_sync_config_name: config.and_then(|config| config.attribute_store_name.clone()),
1053 })
1054}
1055
1056fn map_flow_retry(retry: RetryPolicy) -> SdkResult<FlowRetryPolicy> {
1057 Ok(FlowRetryPolicy {
1058 initial_interval_seconds: optional_seconds(retry.initial_interval)?,
1059 backoff_coefficient: retry.backoff_coefficient.unwrap_or_default() as f32,
1060 maximum_interval_seconds: optional_seconds(retry.maximum_interval)?,
1061 maximum_attempts: i32::try_from(retry.maximum_attempts.unwrap_or_default())
1062 .map_err(|_| invalid("maximum attempts exceed int32"))?,
1063 })
1064}
1065
1066fn map_worker_target(target: &WorkerTarget) -> ProtoWorkerTarget {
1067 ProtoWorkerTarget {
1068 address: target.address().to_string(),
1069 is_headless_address: target.is_headless(),
1070 }
1071}
1072
1073fn map_flow_status(status: i32) -> SdkResult<FlowStatus> {
1074 match ProtoFlowStatus::try_from(status).ok() {
1075 Some(ProtoFlowStatus::Running) => Ok(FlowStatus::Running),
1076 Some(ProtoFlowStatus::Completed) => Ok(FlowStatus::Completed),
1077 Some(ProtoFlowStatus::Failed) => Ok(FlowStatus::Failed),
1078 Some(ProtoFlowStatus::Timeout) => Ok(FlowStatus::TimedOut),
1079 Some(ProtoFlowStatus::Terminated) => Ok(FlowStatus::Terminated),
1080 Some(ProtoFlowStatus::Canceled) => Ok(FlowStatus::Canceled),
1081 Some(ProtoFlowStatus::ContinuedAsNew) => Ok(FlowStatus::ContinuedAsNew),
1082 _ => Err(invalid(format!("unknown Flow status {status}"))),
1083 }
1084}
1085
1086fn map_flow_error_type(error_type: i32) -> Option<FlowErrorType> {
1087 use dex_protocol::dex::FlowErrorType as ProtoFlowErrorType;
1088
1089 match ProtoFlowErrorType::try_from(error_type).ok() {
1090 Some(ProtoFlowErrorType::StepDecisionFailingFlow) => {
1091 Some(FlowErrorType::StepDecisionFailed)
1092 }
1093 Some(ProtoFlowErrorType::ClientApiFailingFlow) => Some(FlowErrorType::ClientApiFailed),
1094 Some(ProtoFlowErrorType::WorkerApiFail) => Some(FlowErrorType::WorkerApiFailed),
1095 Some(ProtoFlowErrorType::InvalidUserFlowCode) => Some(FlowErrorType::InvalidUserFlowCode),
1096 Some(ProtoFlowErrorType::Internal) => Some(FlowErrorType::Internal),
1097 _ => None,
1098 }
1099}
1100
1101fn timestamp(timestamp: prost_types::Timestamp) -> SystemTime {
1102 if timestamp.seconds >= 0 {
1103 SystemTime::UNIX_EPOCH
1104 + Duration::from_secs(timestamp.seconds as u64)
1105 + Duration::from_nanos(timestamp.nanos.max(0) as u64)
1106 } else {
1107 SystemTime::UNIX_EPOCH - Duration::from_secs(timestamp.seconds.unsigned_abs())
1108 }
1109}
1110
1111fn rfc3339(time: SystemTime) -> SdkResult<String> {
1112 let duration = time
1113 .duration_since(SystemTime::UNIX_EPOCH)
1114 .map_err(service_error)?;
1115 let days = (duration.as_secs() / 86_400) as i64;
1116 let seconds = duration.as_secs() % 86_400;
1117 let (year, month, day) = civil_from_days(days);
1118 let hour = seconds / 3_600;
1119 let minute = seconds % 3_600 / 60;
1120 let second = seconds % 60;
1121 Ok(format!(
1122 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
1123 ))
1124}
1125
1126fn civil_from_days(days: i64) -> (i64, i64, i64) {
1127 let days = days + 719_468;
1128 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
1129 let day_of_era = days - era * 146_097;
1130 let year_of_era =
1131 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
1132 let mut year = year_of_era + era * 400;
1133 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
1134 let month_prime = (5 * day_of_year + 2) / 153;
1135 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
1136 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
1137 year += i64::from(month <= 2);
1138 (year, month, day)
1139}
1140
1141fn optional_seconds(duration: Option<Duration>) -> SdkResult<i32> {
1142 duration
1143 .map(seconds32)
1144 .transpose()
1145 .map(Option::unwrap_or_default)
1146}
1147
1148fn seconds32(duration: Duration) -> SdkResult<i32> {
1149 if duration.subsec_nanos() != 0 {
1150 return Err(invalid("Duration must use whole seconds"));
1151 }
1152 i32::try_from(duration.as_secs()).map_err(|_| invalid("Duration exceeds int32"))
1153}
1154
1155fn endpoint_address(address: &str) -> String {
1156 if address.contains("://") {
1157 address.to_string()
1158 } else {
1159 format!("http://{address}")
1160 }
1161}
1162
1163fn require_name(value: &str, kind: &str) -> SdkResult<()> {
1164 if value.is_empty() {
1165 Err(invalid(format!("{kind} is required")))
1166 } else {
1167 Ok(())
1168 }
1169}
1170
1171fn sdk_handler_error(error: impl std::fmt::Display) -> SdkError {
1172 invalid(error.to_string())
1173}
1174
1175fn invalid(message: impl Into<String>) -> SdkError {
1176 SdkError::InvalidArgument {
1177 message: message.into(),
1178 }
1179}
1180
1181fn service_error(error: impl std::fmt::Display) -> SdkError {
1182 SdkError::Service {
1183 service: ServiceError::local("client", error.to_string()),
1184 }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189 use super::map_flow_config;
1190 use crate::FlowConfig;
1191
1192 #[test]
1193 fn attribute_store_name_preserves_protocol_presence() {
1194 let absent = map_flow_config(Some(&FlowConfig::new()), None).expect("map absent config");
1195 let named = map_flow_config(
1196 Some(&FlowConfig::new().attribute_store_name("profiles")),
1197 None,
1198 )
1199 .expect("map named config");
1200 let disabled = map_flow_config(Some(&FlowConfig::new().attribute_store_name("")), None)
1201 .expect("map disabled config");
1202
1203 assert_eq!(absent.attribute_sync_config_name, None);
1204 assert_eq!(
1205 named.attribute_sync_config_name.as_deref(),
1206 Some("profiles")
1207 );
1208 assert_eq!(disabled.attribute_sync_config_name.as_deref(), Some(""));
1209 }
1210}