1use std::any::TypeId;
10use std::collections::{BTreeMap, BTreeSet};
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, AttributeStoreNames, FlowAlreadyStartedOptions,
17 FlowConfig as ProtoFlowConfig, FlowResetStepMethod, FlowResetType, FlowRetryPolicy,
18 FlowStartOptions, FlowStatus as ProtoFlowStatus, FlowTimeoutPolicy as ProtoFlowTimeoutPolicy,
19 GetFlowSummaryRequest, IdReusePolicy as ProtoIdReusePolicy, InvokeRpcRequest,
20 ListStreamMessagesRequest, ReadStreamRequest, ResetFlowRequest, SearchFlowsRequest,
21 SkipTimerRequest, StartFlowRequest, StepDurability as ProtoStepDurability, StopFlowRequest,
22 StopType as ProtoStopType, TriggerContinueAsNewRequest, UpdateFlowConfigRequest,
23 WaitForAttributeRequest, WaitForFlowRequest, WaitForStepCompletionRequest,
24 WorkerTarget as ProtoWorkerTarget, WriteStreamRequest,
25};
26use tokio::runtime::Runtime;
27use tonic::{Code, Request};
28use uuid::Uuid;
29
30mod attribute_reads;
31pub(crate) mod flow_service_tls;
32
33pub use flow_service_tls::FlowServiceTlsOptions;
34use flow_service_tls::flow_service_endpoint;
35
36pub use attribute_reads::{AttributeRead, AttributeReads};
37
38use crate::credential::{CredentialChannel, CredentialInterceptor};
39use crate::sdk_error::{FlowTargetRequirement, ServiceError};
40use crate::server_protocol::{NegotiatedServerProtocolVersion, negotiate_server_protocol};
41use crate::stop_flow_options::StopType;
42use crate::time_travel_options::{TimeTravelPoint, TimeTravelStepMethod};
43use crate::value_hydrator::ValueHydrator;
44use crate::value_mapper;
45use crate::wait_options::ClientRequestBudget;
46use crate::worker_dispatcher::{map_flow_timeout_handler_options, map_step_options};
47use crate::{
48 ActiveStepSearchMode, Attribute, AttributeMap, AttributeMatch, BlobCache, ClientOptions, Flow,
49 FlowConfig, FlowErrorType, FlowInfo, FlowResult, FlowStatus, FlowTimeoutPolicy, IdReusePolicy,
50 Registry, RetryPolicy, Rpc, RpcInvokeOptions, SdkError, SdkResult, SearchFlowEntry,
51 SearchFlowsOptions, SearchFlowsPage, StartFlowOptions, StepCompletion, StepDurability,
52 StepExecutionId, StopFlowOptions, Stream, StreamMessage, StreamMessagesPage, TimeTravelOptions,
53 TimerId, Value, WaitForAttributeOptions, WaitForStepCompletionOptions,
54};
55
56pub struct Client {
92 runtime: Runtime,
93 registry: Registry,
94 service: FlowServiceClient<CredentialChannel>,
95 hydrator: ValueHydrator,
96 negotiated_protocol_version: NegotiatedServerProtocolVersion,
97 protocol_negotiation: tokio::sync::Mutex<Option<Result<u32, String>>>,
98}
99
100impl Client {
101 pub fn new(registry: Registry, blob_cache: Arc<BlobCache>, options: ClientOptions) -> Self {
109 Self::try_new(registry, blob_cache, options)
110 .unwrap_or_else(|error| panic!("cannot create Dex Client: {error}"))
111 }
112
113 pub fn try_new(
121 registry: Registry,
122 blob_cache: Arc<BlobCache>,
123 options: ClientOptions,
124 ) -> SdkResult<Self> {
125 let runtime = Runtime::new().map_err(service_error)?;
126 let endpoint = flow_service_endpoint(
127 options.server_address_value(),
128 options.flow_service_tls_value(),
129 "client",
130 )?;
131 let service = {
132 let _runtime_guard = runtime.enter();
133 FlowServiceClient::new(CredentialChannel::new(
134 endpoint.connect_lazy(),
135 CredentialInterceptor::new(options.credential_value()),
136 ))
137 };
138 let hydrator = ValueHydrator::new(service.clone(), blob_cache);
139 Ok(Self {
140 runtime,
141 registry,
142 service,
143 hydrator,
144 negotiated_protocol_version: NegotiatedServerProtocolVersion::default(),
145 protocol_negotiation: tokio::sync::Mutex::new(None),
146 })
147 }
148
149 pub fn start_flow<SomeFlow: Flow>(
174 &self,
175 flow: &SomeFlow,
176 flow_id: &str,
177 input: SomeFlow::StartInput,
178 ) -> SdkResult<String> {
179 self.start_flow_with_options(flow, flow_id, input, StartFlowOptions::new())
180 }
181
182 pub fn start_flow_with_options<SomeFlow: Flow>(
197 &self,
198 flow: &SomeFlow,
199 flow_id: &str,
200 input: SomeFlow::StartInput,
201 options: StartFlowOptions,
202 ) -> SdkResult<String> {
203 require_name(flow_id, "Flow ID")?;
204 let registered = self.registry.flow(flow.flow_type())?;
205 if registered.type_id != TypeId::of::<SomeFlow>() {
206 return Err(SdkError::FlowDefinition {
207 message: format!(
208 "Flow {} does not match the registered Rust type",
209 flow.flow_type()
210 ),
211 });
212 }
213 let (start_step_type, step_input, step_options) = match ®istered.start_step {
214 Some(step) => (
215 step.name.to_string(),
216 Some(value_mapper::encode(&input)?),
217 Some(
218 map_step_options(
219 registered,
220 registered
221 .handler
222 .step_options(step.name)
223 .map_err(sdk_handler_error)?,
224 )
225 .map_err(sdk_handler_error)?,
226 ),
227 ),
228 None => (String::new(), None, None),
229 };
230 let flow_timeout_seconds = optional_seconds(options.timeout)?;
231 let flow_timeout_policy =
232 map_flow_timeout_policy(registered, flow_timeout_seconds, options.timeout_policy)?;
233 let request = StartFlowRequest {
234 flow_id: flow_id.to_string(),
235 flow_type: registered.name.to_string(),
236 flow_timeout_seconds,
237 flow_timeout_policy,
238 start_step_type,
239 step_input,
240 step_options,
241 flow_start_options: Some(self.map_start_options(
242 registered,
243 &options,
244 flow_timeout_seconds,
245 flow_timeout_policy,
246 )?),
247 request_id: options
248 .request_id
249 .clone()
250 .unwrap_or_else(|| Uuid::new_v4().to_string()),
251 };
252 self.require_server_protocol("start_flow")?;
253 let mut service = self.service.clone();
254 self.runtime.block_on(async {
255 service
256 .start_flow(request)
257 .await
258 .map(|response| response.into_inner().run_id)
259 .map_err(|status| {
260 SdkError::from_status(
261 status,
262 "start_flow",
263 Some(flow_id),
264 FlowTargetRequirement::None,
265 )
266 })
267 })
268 }
269
270 pub fn invoke_rpc<Input: Value, Output: Value>(
298 &self,
299 flow_id: &str,
300 rpc: Rpc<Input, Output>,
301 input: Input,
302 ) -> SdkResult<Output> {
303 self.do_invoke_rpc(flow_id, rpc.name(), &input, &RpcInvokeOptions::new())
304 }
305
306 pub fn invoke_rpc_with_options<Input: Value, Output: Value>(
313 &self,
314 flow_id: &str,
315 rpc: Rpc<Input, Output>,
316 input: Input,
317 options: RpcInvokeOptions,
318 ) -> SdkResult<Output> {
319 self.do_invoke_rpc(flow_id, rpc.name(), &input, &options)
320 }
321
322 pub fn invoke_rpc_without_input<Output: Value>(
326 &self,
327 flow_id: &str,
328 rpc: Rpc<(), Output>,
329 ) -> SdkResult<Output> {
330 self.do_invoke_rpc(flow_id, rpc.name(), &(), &RpcInvokeOptions::new())
331 }
332
333 pub fn invoke_rpc_without_input_with_options<Output: Value>(
339 &self,
340 flow_id: &str,
341 rpc: Rpc<(), Output>,
342 options: RpcInvokeOptions,
343 ) -> SdkResult<Output> {
344 self.do_invoke_rpc(flow_id, rpc.name(), &(), &options)
345 }
346
347 pub fn write_stream<T: Value>(
357 &self,
358 flow_id: &str,
359 stream: &Stream<T>,
360 source: &str,
361 value: T,
362 ) -> SdkResult<()> {
363 require_name(flow_id, "Flow ID")?;
364 require_name(source, "Stream source")?;
365 let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
366 let request = WriteStreamRequest {
367 flow_id: flow_id.to_string(),
368 flow_type,
369 stream_name: stream.name().to_string(),
370 stream_capacity_bytes: stream.stream_capacity_bytes(),
371 value: Some(value_mapper::encode(&value)?),
372 source: source.to_string(),
373 };
374 self.call_empty(
375 "write_stream",
376 Some(flow_id),
377 FlowTargetRequirement::None,
378 |mut service| async move { service.write_stream(request).await },
379 )
380 }
381
382 pub fn read_stream<T: Value>(
386 &self,
387 flow_id: &str,
388 stream: &Stream<T>,
389 resume_token: &str,
390 ) -> SdkResult<StreamMessage<T>> {
391 self.read_stream_result(flow_id, stream, resume_token, None)
392 }
393
394 pub fn read_stream_with_timeout<T: Value>(
401 &self,
402 flow_id: &str,
403 stream: &Stream<T>,
404 resume_token: &str,
405 timeout: Duration,
406 ) -> SdkResult<StreamMessage<T>> {
407 self.read_stream_result(flow_id, stream, resume_token, Some(timeout))
408 }
409
410 pub fn list_stream_messages<T: Value>(
430 &self,
431 flow_id: &str,
432 stream: &Stream<T>,
433 page_size: i32,
434 before_page_token: &str,
435 ) -> SdkResult<StreamMessagesPage<T>> {
436 require_name(flow_id, "Flow ID")?;
437 if page_size < 1 {
438 return Err(invalid("Stream page size must be positive"));
439 }
440 let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
441 let request = ListStreamMessagesRequest {
442 flow_id: flow_id.to_string(),
443 flow_type,
444 stream_name: stream.name().to_string(),
445 page_size,
446 before_page_token: before_page_token.to_string(),
447 };
448 self.require_server_protocol("list_stream_messages")?;
449 let mut service = self.service.clone();
450 let response = self.runtime.block_on(async {
451 service
452 .list_stream_messages(request)
453 .await
454 .map(|response| response.into_inner())
455 .map_err(|status| {
456 SdkError::from_status(
457 status,
458 "list_stream_messages",
459 Some(flow_id),
460 FlowTargetRequirement::None,
461 )
462 })
463 })?;
464 let messages = response
465 .messages
466 .into_iter()
467 .map(|message| {
468 if message.resume_token.is_empty() {
469 return Err(invalid("ListStreamMessages returned an empty resume token"));
470 }
471 let value = message
472 .value
473 .ok_or_else(|| invalid("ListStreamMessages omitted a Stream message Value"))?;
474 let created_time = message.created_time.map(timestamp).ok_or_else(|| {
475 invalid("ListStreamMessages omitted a Stream message creation time")
476 })?;
477 Ok(StreamMessage {
478 value: value_mapper::decode(&value)?,
479 resume_token: message.resume_token,
480 created_time,
481 source: message.source,
482 })
483 })
484 .collect::<SdkResult<Vec<_>>>()?;
485 Ok(StreamMessagesPage {
486 messages,
487 next_page_token: response.next_page_token,
488 })
489 }
490
491 pub fn wait_for_flow(&self, flow_id: &str) -> SdkResult<FlowResult> {
498 self.wait_for_flow_result(flow_id, None)
499 }
500
501 pub fn wait_for_flow_with_timeout(
512 &self,
513 flow_id: &str,
514 timeout: Duration,
515 ) -> SdkResult<FlowResult> {
516 self.wait_for_flow_result(flow_id, Some(timeout))
517 }
518
519 pub fn describe_flow(&self, flow_id: &str) -> SdkResult<FlowInfo> {
525 self.require_server_protocol("describe_flow")?;
526 let mut service = self.service.clone();
527 let response = self.runtime.block_on(async {
528 service
529 .get_flow_summary(GetFlowSummaryRequest {
530 flow_id: flow_id.to_string(),
531 run_id: String::new(),
532 })
533 .await
534 .map(|response| response.into_inner())
535 .map_err(|status| {
536 SdkError::from_status(
537 status,
538 "describe_flow",
539 Some(flow_id),
540 FlowTargetRequirement::Existing,
541 )
542 })
543 })?;
544 let execution = response
545 .flow_execution_id
546 .ok_or_else(|| service_error("GetFlowSummary omitted FlowExecutionID"))?;
547 Ok(FlowInfo {
548 flow_id: execution.flow_id,
549 run_id: execution.run_id,
550 flow_type: response.flow_type,
551 status: map_flow_status(response.flow_status)?,
552 started_at: response
553 .start_time
554 .map_or(SystemTime::UNIX_EPOCH, timestamp),
555 })
556 }
557
558 pub fn search_flows(&self, query: &str, page_size: i32) -> SdkResult<SearchFlowsPage> {
563 self.search_flows_page(query, page_size, "")
564 }
565
566 pub fn search_flows_page(
571 &self,
572 query: &str,
573 page_size: i32,
574 next_page_token: &str,
575 ) -> SdkResult<SearchFlowsPage> {
576 self.search_flows_with_options(query, page_size, next_page_token, SearchFlowsOptions::new())
577 }
578
579 pub fn search_flows_with_options(
581 &self,
582 query: &str,
583 page_size: i32,
584 next_page_token: &str,
585 options: SearchFlowsOptions,
586 ) -> SdkResult<SearchFlowsPage> {
587 if page_size < 0 {
588 return Err(invalid("search page size must not be negative"));
589 }
590 self.require_server_protocol("search_flows")?;
591 let mut service = self.service.clone();
592 self.runtime.block_on(async {
593 let response = service
594 .search_flows(SearchFlowsRequest {
595 query: query.to_string(),
596 page_size,
597 next_page_token: next_page_token.to_string(),
598 include_continued_as_new: options.include_continued_as_new,
599 })
600 .await
601 .map(|response| response.into_inner())
602 .map_err(|status| {
603 SdkError::from_status(status, "search_flows", None, FlowTargetRequirement::None)
604 })?;
605 let values = response
606 .flow_runs
607 .iter()
608 .flat_map(|entry| {
609 entry.indexed_attributes.iter().map(|attribute| {
610 attribute
611 .value
612 .clone()
613 .map(|value| (entry.flow_id.clone(), value))
614 .ok_or_else(|| invalid("Indexed Attribute has no Value"))
615 })
616 })
617 .collect::<SdkResult<Vec<_>>>()?;
618 let mut values = self.hydrator.hydrate_for_flows(values).await?.into_iter();
619 let mut flows = Vec::with_capacity(response.flow_runs.len());
620 for entry in response.flow_runs {
621 let mut indexed_attributes = BTreeMap::new();
622 for attribute in entry.indexed_attributes {
623 let value = values
624 .next()
625 .ok_or_else(|| invalid("Indexed Attribute hydration count mismatch"))?;
626 indexed_attributes.insert(attribute.key, value_mapper::decode_untyped(&value)?);
627 }
628 flows.push(SearchFlowEntry {
629 flow_id: entry.flow_id,
630 run_id: entry.run_id,
631 flow_type: entry.flow_type,
632 status: map_flow_status(entry.flow_status)?,
633 started_at: entry.start_time.map(timestamp),
634 closed_at: entry.close_time.map(timestamp),
635 indexed_attributes,
636 });
637 }
638 Ok(SearchFlowsPage {
639 flows,
640 next_page_token: response.next_page_token,
641 })
642 })
643 }
644
645 pub fn stop_flow(&self, flow_id: &str, options: StopFlowOptions) -> SdkResult<()> {
654 let stop_type = match options.stop_type {
655 StopType::Cancel => ProtoStopType::Cancel,
656 StopType::Terminate => ProtoStopType::Terminate,
657 StopType::Fail => ProtoStopType::Fail,
658 };
659 self.call_empty(
660 "stop_flow",
661 Some(flow_id),
662 FlowTargetRequirement::Active,
663 |mut service| async move {
664 service
665 .stop_flow(StopFlowRequest {
666 flow_id: flow_id.to_string(),
667 run_id: String::new(),
668 reason: options.reason.unwrap_or_default(),
669 stop_type: stop_type as i32,
670 })
671 .await
672 },
673 )
674 }
675
676 pub fn time_travel(&self, flow_id: &str, options: TimeTravelOptions) -> SdkResult<String> {
690 let mut request = ResetFlowRequest {
691 flow_id: flow_id.to_string(),
692 reset_type: FlowResetType::Unspecified as i32,
693 reason: options.reason.unwrap_or_default(),
694 skip_writes_reapply: options.skip_writes_reapply,
695 ..Default::default()
696 };
697 match options.point {
698 TimeTravelPoint::Beginning => request.reset_type = FlowResetType::Beginning as i32,
699 TimeTravelPoint::HistoryEventTime(time) => {
700 request.reset_type = FlowResetType::HistoryEventTime as i32;
701 request.history_event_time = rfc3339(time)?;
702 }
703 TimeTravelPoint::StepType(step_type) => {
704 request.reset_type = FlowResetType::StepType as i32;
705 request.step_type = step_type.to_string();
706 }
707 TimeTravelPoint::StepExecution(execution, method) => {
708 request.reset_type = FlowResetType::StepExecutionId as i32;
709 request.step_execution_id =
710 format!("{}-{}", execution.step_type, execution.execution_number);
711 request.step_method = match method {
712 TimeTravelStepMethod::WaitFor => FlowResetStepMethod::WaitFor as i32,
713 TimeTravelStepMethod::Execute => FlowResetStepMethod::Execute as i32,
714 };
715 }
716 }
717 self.require_server_protocol("time_travel")?;
718 let mut service = self.service.clone();
719 self.runtime.block_on(async {
720 service
721 .reset_flow(request)
722 .await
723 .map(|response| response.into_inner().run_id)
724 .map_err(|status| {
725 SdkError::from_status(
726 status,
727 "time_travel",
728 Some(flow_id),
729 FlowTargetRequirement::Existing,
730 )
731 })
732 })
733 }
734
735 pub fn skip_timer(
742 &self,
743 flow_id: &str,
744 step_execution: StepExecutionId,
745 timer: TimerId,
746 ) -> SdkResult<()> {
747 let (timer_condition_id, timer_condition_index) = match timer {
748 TimerId::ConditionId(id) => (id, None),
749 TimerId::ConditionIndex(index) => (
750 String::new(),
751 Some(i32::try_from(index).map_err(|_| invalid("timer index exceeds int32"))?),
752 ),
753 };
754 self.call_empty(
755 "skip_timer",
756 Some(flow_id),
757 FlowTargetRequirement::Active,
758 |mut service| async move {
759 service
760 .skip_timer(SkipTimerRequest {
761 flow_id: flow_id.to_string(),
762 run_id: String::new(),
763 step_execution_id: format!(
764 "{}-{}",
765 step_execution.step_type, step_execution.execution_number
766 ),
767 timer_condition_id,
768 timer_condition_index,
769 })
770 .await
771 },
772 )
773 }
774
775 pub fn wait_for_step_completion(
785 &self,
786 flow_id: &str,
787 step_execution: StepExecutionId,
788 options: WaitForStepCompletionOptions,
789 ) -> SdkResult<()> {
790 let request_budget = ClientRequestBudget::new(options.request_timeout)?;
791 let internal_handler_timeout_seconds = seconds32(options.internal_handler_timeout)?;
792 self.require_server_protocol_within_budget(
793 "wait_for_step_completion",
794 flow_id,
795 &request_budget,
796 )?;
797 loop {
798 let request_attempt =
799 request_budget.next_attempt("wait_for_step_completion", flow_id)?;
800 let request_flow_id = flow_id.to_string();
801 let request_step_type = step_execution.step_type.to_string();
802 let request_step_execution_number = step_execution.execution_number.to_string();
803 let request_id = options.request_id.clone();
804 let result = self.call_empty(
805 "wait_for_step_completion",
806 Some(flow_id),
807 FlowTargetRequirement::Active,
808 |mut service| async move {
809 let mut request = Request::new(WaitForStepCompletionRequest {
810 flow_id: request_flow_id,
811 step_type: request_step_type,
812 step_execution_number: request_step_execution_number,
813 request_timeout_seconds: request_attempt.request_timeout_seconds,
814 internal_handler_timeout_seconds,
815 request_id,
816 });
817 if let Some(transport_timeout) = request_attempt.transport_timeout {
818 request.set_timeout(transport_timeout);
819 }
820 service.wait_for_step_completion(request).await
821 },
822 );
823 match result {
824 Err(SdkError::LongPollTimeout { .. }) => continue,
825 Err(error)
826 if error.service_error().is_some_and(|service| {
827 matches!(service.code(), Code::DeadlineExceeded | Code::Cancelled)
828 }) && request_budget.has_deadline() =>
829 {
830 return Err(SdkError::request_timeout(
831 "wait_for_step_completion",
832 flow_id,
833 ));
834 }
835 result => return result,
836 }
837 }
838 }
839
840 pub fn wait_for_attribute_match<T: Value>(
849 &self,
850 flow_id: &str,
851 attribute: &Attribute<T>,
852 attribute_match: AttributeMatch<T>,
853 options: WaitForAttributeOptions,
854 ) -> SdkResult<T> {
855 require_waitable_attribute(attribute.name(), attribute.is_hard_delete_on_null())?;
856 self.wait_for_attribute_value(flow_id, attribute.name(), &attribute_match, options)
857 }
858
859 pub fn wait_for_attribute_map_instance_match<T: Value>(
866 &self,
867 flow_id: &str,
868 attribute: &AttributeMap<T>,
869 instance: &str,
870 attribute_match: AttributeMatch<T>,
871 options: WaitForAttributeOptions,
872 ) -> SdkResult<T> {
873 require_waitable_attribute(attribute.name(), attribute.is_hard_delete_on_null())?;
874 self.wait_for_attribute_value(
875 flow_id,
876 &map_physical_name(attribute.name(), instance)?,
877 &attribute_match,
878 options,
879 )
880 }
881
882 fn wait_for_attribute_value<T: Value>(
883 &self,
884 flow_id: &str,
885 key: &str,
886 attribute_match: &AttributeMatch<T>,
887 options: WaitForAttributeOptions,
888 ) -> SdkResult<T> {
889 let mut encoded_match = attribute_match.encode()?;
890 encoded_match.key = key.to_string();
891 let request_budget = ClientRequestBudget::new(options.request_timeout)?;
892 let internal_handler_timeout_seconds = seconds32(options.internal_handler_timeout)?;
893 self.require_server_protocol_within_budget(
894 "wait_for_attribute_match",
895 flow_id,
896 &request_budget,
897 )?;
898 let response = loop {
899 let request_attempt =
900 request_budget.next_attempt("wait_for_attribute_match", flow_id)?;
901 let mut service = self.service.clone();
902 let mut request = Request::new(WaitForAttributeRequest {
903 flow_id: flow_id.to_string(),
904 r#match: Some(encoded_match.clone()),
905 request_timeout_seconds: request_attempt.request_timeout_seconds,
906 internal_handler_timeout_seconds,
907 request_id: options.request_id.clone(),
908 });
909 if let Some(transport_timeout) = request_attempt.transport_timeout {
910 request.set_timeout(transport_timeout);
911 }
912 let result = self
913 .runtime
914 .block_on(service.wait_for_attribute(request))
915 .map_err(|status| {
916 SdkError::from_status(
917 status,
918 "wait_for_attribute_match",
919 Some(flow_id),
920 FlowTargetRequirement::Active,
921 )
922 });
923 match result {
924 Ok(response) => break response.into_inner(),
925 Err(SdkError::LongPollTimeout { .. }) => continue,
926 Err(error)
927 if error.service_error().is_some_and(|service| {
928 matches!(service.code(), Code::DeadlineExceeded | Code::Cancelled)
929 }) && request_budget.has_deadline() =>
930 {
931 return Err(SdkError::request_timeout(
932 "wait_for_attribute_match",
933 flow_id,
934 ));
935 }
936 Err(error) => return Err(error),
937 }
938 };
939 let matched_value = response.matched_value.ok_or_else(|| SdkError::Service {
940 service: ServiceError::local(
941 "wait_for_attribute_match",
942 "WaitForAttribute response is incomplete",
943 ),
944 })?;
945 value_mapper::decode(&matched_value)
946 }
947
948 pub fn update_flow_config(&self, flow_id: &str, config: FlowConfig) -> SdkResult<()> {
953 let flow_config = self.map_flow_config(Some(&config))?;
954 self.call_empty(
955 "update_flow_config",
956 Some(flow_id),
957 FlowTargetRequirement::Active,
958 |mut service| async move {
959 service
960 .update_flow_config(UpdateFlowConfigRequest {
961 flow_id: flow_id.to_string(),
962 run_id: String::new(),
963 flow_config: Some(flow_config),
964 })
965 .await
966 },
967 )
968 }
969
970 pub fn trigger_continue_as_new(&self, flow_id: &str) -> SdkResult<()> {
972 self.call_empty(
973 "trigger_continue_as_new",
974 Some(flow_id),
975 FlowTargetRequirement::Active,
976 |mut service| async move {
977 service
978 .trigger_continue_as_new(TriggerContinueAsNewRequest {
979 flow_id: flow_id.to_string(),
980 run_id: String::new(),
981 })
982 .await
983 },
984 )
985 }
986
987 pub fn health_check(&self) -> SdkResult<()> {
991 self.call_empty(
992 "health_check",
993 None,
994 FlowTargetRequirement::None,
995 |mut service| async move { service.health_check(()).await },
996 )
997 }
998
999 fn do_invoke_rpc<Input: Value, Output: Value>(
1000 &self,
1001 flow_id: &str,
1002 rpc_name: &str,
1003 input: &Input,
1004 invoke_options: &RpcInvokeOptions,
1005 ) -> SdkResult<Output> {
1006 let (flow, rpc) = self.registry.rpc_with_flow(rpc_name)?;
1007 let mut lock_attribute_keys = rpc
1008 .locks
1009 .iter()
1010 .map(|lock| lock.physical_name())
1011 .collect::<BTreeSet<_>>();
1012 for lock in &invoke_options.lock_attribute_map_instances {
1013 let Some(instance) = lock.instance() else {
1014 return Err(SdkError::FlowDefinition {
1015 message: "RpcInvokeOptions lock must target an AttributeMap instance".into(),
1016 });
1017 };
1018 require_rpc_invoke_definition(
1019 flow,
1020 lock.attribute_name(),
1021 crate::persistence::PersistenceKind::AttributeMap,
1022 )?;
1023 crate::registry::validate_map_instance(instance).map_err(invalid)?;
1024 lock_attribute_keys.insert(lock.physical_name());
1025 }
1026 let mut load_attribute_map_instances = rpc
1027 .load_attribute_maps
1028 .iter()
1029 .map(|load| map_load_name(&load.name, load.instance.as_deref()))
1030 .collect::<BTreeSet<_>>();
1031 for load in &invoke_options.load_attribute_map_instances {
1032 let instance = require_rpc_invoke_map_load(
1033 flow,
1034 &load.name,
1035 load.instance.as_deref(),
1036 crate::persistence::PersistenceKind::AttributeMap,
1037 )?;
1038 load_attribute_map_instances
1039 .insert(crate::registry::physical_name(&load.name, instance));
1040 }
1041 let mut load_channel_names = rpc
1042 .load_channels
1043 .iter()
1044 .map(|load| load.name.clone())
1045 .collect::<Vec<_>>();
1046 load_channel_names.sort();
1047 let mut load_channel_map_instances = rpc
1048 .load_channel_maps
1049 .iter()
1050 .map(|load| map_load_name(&load.name, load.instance.as_deref()))
1051 .collect::<BTreeSet<_>>();
1052 for load in &invoke_options.load_channel_map_instances {
1053 let instance = require_rpc_invoke_map_load(
1054 flow,
1055 &load.name,
1056 load.instance.as_deref(),
1057 crate::persistence::PersistenceKind::ChannelMap,
1058 )?;
1059 load_channel_map_instances.insert(crate::registry::physical_name(&load.name, instance));
1060 }
1061 let request = InvokeRpcRequest {
1062 flow_id: flow_id.to_string(),
1063 run_id: String::new(),
1064 rpc_name: rpc_name.to_string(),
1065 input: Some(value_mapper::encode(input)?),
1066 timeout_seconds: optional_seconds(rpc.timeout)?,
1067 lock_attribute_keys: lock_attribute_keys.into_iter().collect(),
1068 request_id: Uuid::new_v4().to_string(),
1069 is_transactional: rpc.is_transactional,
1070 load_attribute_map_instances: load_attribute_map_instances.into_iter().collect(),
1071 load_channel_names,
1072 load_channel_map_instances: load_channel_map_instances.into_iter().collect(),
1073 };
1074 self.require_server_protocol("invoke_rpc")?;
1075 let mut service = self.service.clone();
1076 let output = self.runtime.block_on(async {
1077 service
1078 .invoke_rpc(request)
1079 .await
1080 .map(|response| response.into_inner().output)
1081 .map_err(|status| {
1082 SdkError::from_status(
1083 status,
1084 "invoke_rpc",
1085 Some(flow_id),
1086 FlowTargetRequirement::Active,
1087 )
1088 })
1089 })?;
1090 let output = output.ok_or_else(|| invalid("InvokeRPC omitted output"))?;
1091 let output = self
1092 .runtime
1093 .block_on(self.hydrator.hydrate(flow_id, output))?;
1094 value_mapper::decode(&output)
1095 }
1096
1097 fn read_stream_result<T: Value>(
1098 &self,
1099 flow_id: &str,
1100 stream: &Stream<T>,
1101 resume_token: &str,
1102 timeout: Option<Duration>,
1103 ) -> SdkResult<StreamMessage<T>> {
1104 require_name(flow_id, "Flow ID")?;
1105 let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
1106 let wait_time_seconds = optional_seconds(timeout)?;
1107 let request = ReadStreamRequest {
1108 flow_id: flow_id.to_string(),
1109 flow_type,
1110 stream_name: stream.name().to_string(),
1111 resume_token: resume_token.to_string(),
1112 wait_time_seconds,
1113 };
1114 self.require_server_protocol("read_stream")?;
1115 let mut service = self.service.clone();
1116 let response = self.runtime.block_on(async {
1117 service
1118 .read_stream(request)
1119 .await
1120 .map(|response| response.into_inner())
1121 .map_err(|status| {
1122 SdkError::from_status(
1123 status,
1124 "read_stream",
1125 Some(flow_id),
1126 FlowTargetRequirement::None,
1127 )
1128 })
1129 })?;
1130 let message = response
1131 .message
1132 .ok_or_else(|| invalid("ReadStream omitted its message"))?;
1133 if message.resume_token.is_empty() {
1134 return Err(invalid("ReadStream returned an empty resume token"));
1135 }
1136 let value = message
1137 .value
1138 .ok_or_else(|| invalid("ReadStream omitted its Value"))?;
1139 let created_time = message
1140 .created_time
1141 .map(timestamp)
1142 .ok_or_else(|| invalid("ReadStream omitted its creation time"))?;
1143 Ok(StreamMessage {
1144 value: value_mapper::decode(&value)?,
1145 resume_token: message.resume_token,
1146 created_time,
1147 source: message.source,
1148 })
1149 }
1150
1151 fn wait_for_flow_result(
1152 &self,
1153 flow_id: &str,
1154 timeout: Option<Duration>,
1155 ) -> SdkResult<FlowResult> {
1156 self.require_server_protocol("wait_for_flow")?;
1157 let mut service = self.service.clone();
1158 let response = self.runtime.block_on(async {
1159 service
1160 .wait_for_flow(WaitForFlowRequest {
1161 flow_id: flow_id.to_string(),
1162 run_id: String::new(),
1163 needs_results: true,
1164 wait_time_seconds: optional_seconds(timeout)?,
1165 })
1166 .await
1167 .map(|response| response.into_inner())
1168 .map_err(|status| {
1169 SdkError::from_status(
1170 status,
1171 "wait_for_flow",
1172 Some(flow_id),
1173 FlowTargetRequirement::Existing,
1174 )
1175 })
1176 });
1177 let response = response?;
1178 let mut outputs = Vec::with_capacity(response.results.len());
1179 for result in &response.results {
1180 outputs.push(result.completed_step_output.clone().ok_or_else(|| {
1181 SdkError::ValueMapping {
1182 message: "Step completion output is required".to_string(),
1183 }
1184 })?);
1185 }
1186 let outputs = self
1187 .runtime
1188 .block_on(self.hydrator.hydrate_all(flow_id, outputs))?;
1189 let completions = response
1190 .results
1191 .into_iter()
1192 .zip(outputs)
1193 .map(|(result, output)| {
1194 StepCompletion::new(
1195 result.completed_step_type,
1196 result.completed_step_execution_id,
1197 output,
1198 )
1199 })
1200 .collect();
1201 Ok(FlowResult::new(
1202 map_flow_status(response.flow_status)?,
1203 map_flow_error_type(response.error_type),
1204 (!response.error_message.is_empty()).then_some(response.error_message),
1205 completions,
1206 ))
1207 }
1208
1209 fn map_start_options(
1210 &self,
1211 flow: &crate::registry::RegisteredFlow,
1212 options: &StartFlowOptions,
1213 flow_timeout_seconds: i32,
1214 flow_timeout_policy: i32,
1215 ) -> SdkResult<FlowStartOptions> {
1216 let attributes = options
1217 .attributes
1218 .iter()
1219 .map(|attribute| {
1220 let definition =
1221 flow.persistence
1222 .get(&attribute.attribute_name)
1223 .ok_or_else(|| {
1224 invalid(format!(
1225 "initial Attribute is not registered: {}",
1226 attribute.key
1227 ))
1228 })?;
1229 attribute.encode_attribute_write(definition)
1230 })
1231 .collect::<SdkResult<Vec<_>>>()?;
1232 if let Some(write) = attributes.iter().find(|write| write.is_hard_delete_on_null) {
1233 self.require_hard_delete_attribute_support("start_flow", &write.key)?;
1234 }
1235 let config = options.config_override.as_ref();
1236 let flow_config_override = Some(map_pull_flow_config(config)?);
1237 #[expect(
1238 clippy::needless_update,
1239 reason = "The test preserves forward-compatible construction of SDK options."
1240 )]
1241 let flow_start_options = FlowStartOptions {
1242 id_reuse_policy: match options.id_reuse_policy {
1243 IdReusePolicy::Default => ProtoIdReusePolicy::Unspecified,
1244 IdReusePolicy::AllowIfPreviousFailed => {
1245 ProtoIdReusePolicy::AllowIfPreviousExistsAbnormally
1246 }
1247 IdReusePolicy::AllowIfNotRunning => ProtoIdReusePolicy::AllowIfNoRunning,
1248 IdReusePolicy::Disallow => ProtoIdReusePolicy::DisallowReuse,
1249 IdReusePolicy::TerminateIfRunning => ProtoIdReusePolicy::AllowTerminateIfRunning,
1250 } as i32,
1251 flow_start_delay_seconds: optional_seconds(options.start_delay)?,
1252 retry_policy: options
1253 .retry_policy
1254 .clone()
1255 .map(map_flow_retry)
1256 .transpose()?,
1257 attributes,
1258 flow_config_override,
1259 flow_already_started_options: Some(FlowAlreadyStartedOptions {
1260 ignore_already_started_error: options.ignore_already_started,
1261 }),
1262 timeout_handler_options: map_flow_timeout_handler_options(
1263 flow,
1264 flow_timeout_seconds,
1265 flow_timeout_policy,
1266 options.timeout_handler_options.as_ref(),
1267 )
1268 .map_err(sdk_handler_error)?,
1269 ..Default::default()
1270 };
1271 Ok(flow_start_options)
1272 }
1273
1274 fn require_server_protocol(&self, operation: &'static str) -> SdkResult<()> {
1275 self.runtime
1276 .block_on(self.negotiate_server_protocol_once(operation))
1277 }
1278
1279 fn require_server_protocol_within_budget(
1282 &self,
1283 operation: &'static str,
1284 flow_id: &str,
1285 request_budget: &ClientRequestBudget,
1286 ) -> SdkResult<()> {
1287 let Some(deadline) = request_budget.deadline() else {
1288 return self.require_server_protocol(operation);
1289 };
1290 self.runtime
1291 .block_on(async {
1292 tokio::time::timeout_at(
1293 tokio::time::Instant::from_std(deadline),
1294 self.negotiate_server_protocol_once(operation),
1295 )
1296 .await
1297 })
1298 .unwrap_or_else(|_| Err(SdkError::request_timeout(operation, flow_id)))
1299 }
1300
1301 async fn negotiate_server_protocol_once(&self, operation: &'static str) -> SdkResult<()> {
1302 let mut cached = self.protocol_negotiation.lock().await;
1303 if cached.is_none() {
1304 let mut service = self.service.clone();
1305 let server_info = service
1306 .get_server_info(())
1307 .await
1308 .map_err(|status| {
1309 SdkError::from_status(status, operation, None, FlowTargetRequirement::None)
1310 })?
1311 .into_inner();
1312 *cached = Some(negotiate_server_protocol(&server_info));
1313 }
1314 match cached.as_ref().expect("Server negotiation was cached") {
1315 Ok(version) => {
1316 self.negotiated_protocol_version.store(*version);
1317 Ok(())
1318 }
1319 Err(message) => Err(SdkError::Service {
1320 service: ServiceError::local("server_protocol", message.clone()),
1321 }),
1322 }
1323 }
1324
1325 fn require_hard_delete_attribute_support(
1326 &self,
1327 operation: &'static str,
1328 attribute_key: &str,
1329 ) -> SdkResult<()> {
1330 self.require_server_protocol(operation)?;
1331 self.negotiated_protocol_version
1332 .require_hard_delete_attribute_support(attribute_key)
1333 .map_err(|error| SdkError::Service {
1334 service: ServiceError::local(operation, error),
1335 })
1336 }
1337
1338 fn map_flow_config(&self, config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
1339 map_flow_config(config)
1340 }
1341
1342 fn call_empty<Response, Future, Call>(
1343 &self,
1344 operation: &'static str,
1345 flow_id: Option<&str>,
1346 requirement: FlowTargetRequirement,
1347 call: Call,
1348 ) -> SdkResult<()>
1349 where
1350 Call: FnOnce(FlowServiceClient<CredentialChannel>) -> Future,
1351 Future: std::future::Future<Output = Result<tonic::Response<Response>, tonic::Status>>,
1352 {
1353 self.require_server_protocol(operation)?;
1354 let service = self.service.clone();
1355 self.runtime
1356 .block_on(call(service))
1357 .map(|_| ())
1358 .map_err(|status| SdkError::from_status(status, operation, flow_id, requirement))
1359 }
1360}
1361
1362pub(crate) fn map_pull_flow_config(config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
1363 Ok(ProtoFlowConfig {
1364 worker_target: Some(ProtoWorkerTarget { is_pull_mode: true }),
1365 ..map_flow_config(config)?
1366 })
1367}
1368
1369pub(crate) fn map_flow_config(config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
1370 Ok(ProtoFlowConfig {
1371 active_step_search_mode: config
1372 .and_then(|config| config.active_step_search_mode)
1373 .map(|mode| match mode {
1374 ActiveStepSearchMode::All => ProtoSearchMode::EnabledForAll as i32,
1375 ActiveStepSearchMode::WithWaitFor => {
1376 ProtoSearchMode::EnabledForStepsWithWaitFor as i32
1377 }
1378 ActiveStepSearchMode::Disabled => ProtoSearchMode::Disabled as i32,
1379 }),
1380 continue_as_new_threshold: config
1381 .and_then(|config| config.continue_as_new_threshold)
1382 .map(i32::try_from)
1383 .transpose()
1384 .map_err(|_| invalid("continue-as-new threshold exceeds int32"))?,
1385 continue_as_new_page_size_in_bytes: config
1386 .and_then(|config| config.continue_as_new_page_size_bytes)
1387 .map(i32::try_from)
1388 .transpose()
1389 .map_err(|_| invalid("continue-as-new page size exceeds int32"))?,
1390 step_durability: config
1391 .and_then(|config| config.step_durability)
1392 .map(|durability| {
1393 (match durability {
1394 StepDurability::Default => ProtoStepDurability::Unspecified,
1395 StepDurability::Sync => ProtoStepDurability::Sync,
1396 StepDurability::Async => ProtoStepDurability::Async,
1397 }) as i32
1398 }),
1399 worker_target: None,
1400 attribute_store_names: config.and_then(|config| {
1401 config
1402 .attribute_store_names
1403 .as_ref()
1404 .map(|names| AttributeStoreNames {
1405 names: names.clone(),
1406 })
1407 }),
1408 })
1409}
1410
1411pub(crate) fn map_flow_retry(retry: RetryPolicy) -> SdkResult<FlowRetryPolicy> {
1412 Ok(FlowRetryPolicy {
1413 initial_interval_seconds: optional_seconds(retry.initial_interval)?,
1414 backoff_coefficient: retry.backoff_coefficient.unwrap_or_default() as f32,
1415 maximum_interval_seconds: optional_seconds(retry.maximum_interval)?,
1416 maximum_attempts: i32::try_from(retry.maximum_attempts.unwrap_or_default())
1417 .map_err(|_| invalid("maximum attempts exceed int32"))?,
1418 })
1419}
1420
1421pub(crate) fn map_flow_status(status: i32) -> SdkResult<FlowStatus> {
1422 match ProtoFlowStatus::try_from(status).ok() {
1423 Some(ProtoFlowStatus::Running) => Ok(FlowStatus::Running),
1424 Some(ProtoFlowStatus::Completed) => Ok(FlowStatus::Completed),
1425 Some(ProtoFlowStatus::Failed) => Ok(FlowStatus::Failed),
1426 Some(ProtoFlowStatus::ServerSideTimeoutInternalOnly) => {
1427 Ok(FlowStatus::ServerSideTimeoutInternalOnly)
1428 }
1429 Some(ProtoFlowStatus::Terminated) => Ok(FlowStatus::Terminated),
1430 Some(ProtoFlowStatus::Canceled) => Ok(FlowStatus::Canceled),
1431 Some(ProtoFlowStatus::ContinuedAsNew) => Ok(FlowStatus::ContinuedAsNew),
1432 _ => Err(invalid(format!("unknown Flow status {status}"))),
1433 }
1434}
1435
1436pub(crate) fn map_flow_error_type(error_type: i32) -> Option<FlowErrorType> {
1437 use dex_protocol::dex::FlowErrorType as ProtoFlowErrorType;
1438
1439 match ProtoFlowErrorType::try_from(error_type).ok() {
1440 Some(ProtoFlowErrorType::StepDecisionFailingFlow) => {
1441 Some(FlowErrorType::StepDecisionFailed)
1442 }
1443 Some(ProtoFlowErrorType::ClientApiFailingFlow) => Some(FlowErrorType::ClientApiFailed),
1444 Some(ProtoFlowErrorType::WorkerApiFail) => Some(FlowErrorType::WorkerApiFailed),
1445 Some(ProtoFlowErrorType::InvalidUserFlowCode) => Some(FlowErrorType::InvalidUserFlowCode),
1446 Some(ProtoFlowErrorType::Internal) => Some(FlowErrorType::Internal),
1447 Some(ProtoFlowErrorType::FlowTimeout) => Some(FlowErrorType::FlowTimeout),
1448 _ => None,
1449 }
1450}
1451
1452pub(crate) fn map_flow_timeout_policy(
1453 flow: &crate::registry::RegisteredFlow,
1454 timeout_seconds: i32,
1455 policy: FlowTimeoutPolicy,
1456) -> SdkResult<i32> {
1457 if timeout_seconds == 0 {
1458 if policy != FlowTimeoutPolicy::Default {
1459 return Err(invalid("Flow timeout policy requires a positive timeout"));
1460 }
1461 return Ok(ProtoFlowTimeoutPolicy::Unspecified as i32);
1462 }
1463 let resolved = match policy {
1464 FlowTimeoutPolicy::Default if flow.handler.has_timeout_handler() => {
1465 FlowTimeoutPolicy::Handler
1466 }
1467 FlowTimeoutPolicy::Default => FlowTimeoutPolicy::Fail,
1468 configured => configured,
1469 };
1470 if resolved == FlowTimeoutPolicy::Handler && !flow.handler.has_timeout_handler() {
1471 return Err(invalid(format!(
1472 "Flow {} has no timeout handler",
1473 flow.name
1474 )));
1475 }
1476 Ok(match resolved {
1477 FlowTimeoutPolicy::Default => ProtoFlowTimeoutPolicy::Unspecified,
1478 FlowTimeoutPolicy::Fail => ProtoFlowTimeoutPolicy::Fail,
1479 FlowTimeoutPolicy::Cancel => ProtoFlowTimeoutPolicy::Cancel,
1480 FlowTimeoutPolicy::Handler => ProtoFlowTimeoutPolicy::Handler,
1481 } as i32)
1482}
1483
1484fn timestamp(timestamp: prost_types::Timestamp) -> SystemTime {
1485 if timestamp.seconds >= 0 {
1486 SystemTime::UNIX_EPOCH
1487 + Duration::from_secs(timestamp.seconds as u64)
1488 + Duration::from_nanos(timestamp.nanos.max(0) as u64)
1489 } else {
1490 SystemTime::UNIX_EPOCH - Duration::from_secs(timestamp.seconds.unsigned_abs())
1491 }
1492}
1493
1494fn rfc3339(time: SystemTime) -> SdkResult<String> {
1495 let duration = time
1496 .duration_since(SystemTime::UNIX_EPOCH)
1497 .map_err(service_error)?;
1498 let days = (duration.as_secs() / 86_400) as i64;
1499 let seconds = duration.as_secs() % 86_400;
1500 let (year, month, day) = civil_from_days(days);
1501 let hour = seconds / 3_600;
1502 let minute = seconds % 3_600 / 60;
1503 let second = seconds % 60;
1504 Ok(format!(
1505 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
1506 ))
1507}
1508
1509fn civil_from_days(days: i64) -> (i64, i64, i64) {
1510 let days = days + 719_468;
1511 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
1512 let day_of_era = days - era * 146_097;
1513 let year_of_era =
1514 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
1515 let mut year = year_of_era + era * 400;
1516 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
1517 let month_prime = (5 * day_of_year + 2) / 153;
1518 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
1519 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
1520 year += i64::from(month <= 2);
1521 (year, month, day)
1522}
1523
1524fn map_load_name(name: &str, instance: Option<&str>) -> String {
1525 match instance {
1526 Some(instance) => crate::registry::physical_name(name, instance),
1527 None => format!("{name}/"),
1528 }
1529}
1530
1531fn require_rpc_invoke_map_load<'a>(
1532 flow: &crate::registry::RegisteredFlow,
1533 name: &str,
1534 instance: Option<&'a str>,
1535 expected_kind: crate::persistence::PersistenceKind,
1536) -> SdkResult<&'a str> {
1537 require_rpc_invoke_definition(flow, name, expected_kind)?;
1538 let instance = instance.ok_or_else(|| SdkError::FlowDefinition {
1539 message: "RpcInvokeOptions requires an exact map instance".to_string(),
1540 })?;
1541 crate::registry::validate_map_instance(instance).map_err(invalid)?;
1542 Ok(instance)
1543}
1544
1545fn require_rpc_invoke_definition(
1546 flow: &crate::registry::RegisteredFlow,
1547 name: &str,
1548 expected_kind: crate::persistence::PersistenceKind,
1549) -> SdkResult<()> {
1550 flow.persistence
1551 .get(name)
1552 .filter(|definition| definition.kind == expected_kind)
1553 .ok_or_else(|| SdkError::FlowDefinition {
1554 message: format!(
1555 "Flow {} does not register the selected map {name}",
1556 flow.name
1557 ),
1558 })?;
1559 Ok(())
1560}
1561
1562fn optional_seconds(duration: Option<Duration>) -> SdkResult<i32> {
1563 duration
1564 .map(seconds32)
1565 .transpose()
1566 .map(Option::unwrap_or_default)
1567}
1568
1569pub(crate) fn seconds32(duration: Duration) -> SdkResult<i32> {
1570 if duration.subsec_nanos() != 0 {
1571 return Err(invalid("Duration must use whole seconds"));
1572 }
1573 i32::try_from(duration.as_secs()).map_err(|_| invalid("Duration exceeds int32"))
1574}
1575
1576fn require_name(value: &str, kind: &str) -> SdkResult<()> {
1577 if value.is_empty() {
1578 Err(invalid(format!("{kind} is required")))
1579 } else {
1580 Ok(())
1581 }
1582}
1583
1584fn map_physical_name(name: &str, instance: &str) -> SdkResult<String> {
1585 crate::registry::validate_map_instance(instance).map_err(invalid)?;
1586 Ok(crate::registry::physical_name(name, instance))
1587}
1588
1589fn require_waitable_attribute(attribute_name: &str, is_hard_delete_on_null: bool) -> SdkResult<()> {
1590 if is_hard_delete_on_null {
1591 return Err(invalid(format!(
1592 "wait_for_attribute_match does not support hard-delete Attribute {attribute_name}"
1593 )));
1594 }
1595 Ok(())
1596}
1597
1598fn sdk_handler_error(error: impl std::fmt::Display) -> SdkError {
1599 invalid(error.to_string())
1600}
1601
1602pub(crate) fn invalid(message: impl Into<String>) -> SdkError {
1603 SdkError::InvalidArgument {
1604 message: message.into(),
1605 }
1606}
1607
1608fn service_error(error: impl std::fmt::Display) -> SdkError {
1609 SdkError::Service {
1610 service: ServiceError::local("client", error.to_string()),
1611 }
1612}
1613
1614#[cfg(test)]
1615mod tests;
1616
1617#[cfg(test)]
1618#[path = "client_pull_routing_tests.rs"]
1619mod pull_routing_tests;