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