Skip to main content

temporalio_client/
lib.rs

1#![warn(missing_docs)] // error if there are missing docs
2
3//! This crate contains client implementations that can be used to contact the Temporal service.
4//!
5//! It implements auto-retry behavior and metrics collection.
6
7#[macro_use]
8extern crate tracing;
9
10mod activity;
11mod async_activity_handle;
12pub mod callback_based;
13mod dns;
14/// Configuration loading from environment variables and TOML files.
15#[cfg(feature = "envconfig")]
16pub mod envconfig;
17pub mod errors;
18pub mod grpc;
19/// Interceptors for high-level client operations.
20pub mod interceptors;
21mod metrics;
22mod options_structs;
23/// Experimental APIs for configuring clients with reusable plugins.
24pub mod plugins;
25/// Visible only for tests
26#[doc(hidden)]
27pub mod proxy;
28mod replaceable;
29pub mod request_extensions;
30mod retry;
31mod rpc_options;
32/// Schedule operations: create, describe, update, pause, trigger, backfill, list, and delete.
33pub mod schedules;
34#[cfg(test)]
35mod test_helpers;
36pub mod worker;
37mod workflow_handle;
38mod workflow_status;
39
40pub use crate::{
41    proxy::HttpConnectProxyOptions,
42    request_extensions::PayloadErrorLimits,
43    retry::{CallType, RETRYABLE_ERROR_CODES},
44};
45pub use activity::*;
46pub use async_activity_handle::{
47    ActivityHeartbeatResponse, ActivityIdentifier, AsyncActivityHandle,
48};
49#[doc(hidden)]
50pub use retry::jittered;
51
52pub use interceptors::{
53    BackfillScheduleInput, CancelWorkflowInput, ClientInterceptor, CompleteAsyncActivityInput,
54    CountWorkflowsInput, CountWorkflowsOutput, CreateScheduleInput, CreateScheduleOutput,
55    DeleteScheduleInput, DescribeScheduleInput, DescribeScheduleOutput, DescribeWorkflowInput,
56    DescribeWorkflowOutput, FailAsyncActivityInput, FetchWorkflowHistoryPageInput,
57    FetchWorkflowHistoryPageOutput, HasArgs, HeartbeatAsyncActivityInput, ListSchedulesPageInput,
58    ListSchedulesPageOutput, ListWorkflowsPageInput, ListWorkflowsPageOutput, Next,
59    PauseScheduleInput, PollWorkflowUpdateInput, PollWorkflowUpdateOutput, QueryWorkflowInput,
60    QueryWorkflowOutput, ReportAsyncActivityCancellationInput, SendScheduleUpdateInput,
61    SignalWorkflowInput, StartWorkflowInput, StartWorkflowOutput, StartWorkflowUpdateInput,
62    StartWorkflowUpdateOutput, TemporalClientValue, TerminateWorkflowInput, TriggerScheduleInput,
63    UnpauseScheduleInput, UpdateScheduleInput,
64};
65pub use metrics::{LONG_REQUEST_LATENCY_HISTOGRAM_NAME, REQUEST_LATENCY_HISTOGRAM_NAME};
66pub use options_structs::*;
67pub use plugins::{
68    ClientPlugin, ErasedClientPlugin, PluginApplyError, PluginError, PluginTarget, WorkerPluginData,
69};
70pub use replaceable::SharedReplaceableClient;
71pub use retry::RetryOptions;
72pub use rpc_options::{RpcMetadata, RpcMetadataError, RpcOptions};
73pub use temporalio_common::{Memo, RetryPolicy};
74pub use url::Url;
75/// Potentially dangerous TLS related functionality.
76pub mod danger {
77    /// Re-export the `ServerCertVerifier` trait so that users can implement custom TLS
78    /// server certificate verification without depending on `tokio-rustls` directly,
79    /// while explicitly acknowledging the danger in the import path.
80    pub use tokio_rustls::rustls::client::danger::ServerCertVerifier;
81}
82#[cfg(feature = "dynamic-tls")]
83/// Re-export of [`tokio_rustls::rustls::SignatureScheme`] — parameter type
84/// of [`ResolvesClientCert::resolve`].
85pub use tokio_rustls::rustls::SignatureScheme;
86#[cfg(feature = "dynamic-tls")]
87/// Re-export the `ResolvesClientCert` trait and supporting types so that users
88/// can implement dynamic client certificate resolution without depending on
89/// `tokio-rustls` directly.
90///
91/// This enables transparent certificate rotation for mTLS connections (e.g.,
92/// short-lived certs issued by Vault and rotated on disk by a sidecar).
93///
94/// Implementors will also need [`CertifiedKey`] and [`SignatureScheme`].
95pub use tokio_rustls::rustls::client::ResolvesClientCert;
96#[cfg(feature = "dynamic-tls")]
97/// Re-export of [`tokio_rustls::rustls::sign::CertifiedKey`] — the return type
98/// of [`ResolvesClientCert::resolve`].
99pub use tokio_rustls::rustls::sign::CertifiedKey;
100pub use tonic;
101pub use workflow_handle::{
102    UntypedQuery, UntypedSignal, UntypedUpdate, UntypedWorkflow, UntypedWorkflowHandle,
103    WorkflowExecutionDescription, WorkflowExecutionInfo, WorkflowExecutionResult, WorkflowHandle,
104    WorkflowHistory, WorkflowHistoryJsonError, WorkflowResultDetails, WorkflowUpdateHandle,
105};
106pub use workflow_status::WorkflowExecutionStatus;
107
108use crate::{
109    grpc::{
110        AttachMetricLabels, CloudService, HealthService, OperatorService, TestService,
111        WorkflowService,
112    },
113    metrics::{ChannelOrGrpcOverride, GrpcMetricSvc, MetricsContext},
114    request_extensions::RequestExt,
115    worker::ClientWorkerSet,
116};
117use errors::*;
118use futures_util::{future::BoxFuture, stream, stream::Stream};
119use http::Uri;
120use parking_lot::RwLock;
121use std::{
122    collections::{HashMap, VecDeque},
123    error::Error,
124    fmt::Debug,
125    pin::Pin,
126    str::FromStr,
127    sync::{Arc, OnceLock},
128    task::{Context, Poll},
129    time::{Duration, SystemTime},
130};
131use temporalio_common::{
132    ActivityDefinition, HasWorkflowDefinition, UntypedActivity,
133    data_converters::{
134        DataConverter, GenericPayloadConverter, PayloadConverter, SerializationContext,
135        SerializationContextData,
136    },
137    payload_visitor::decode_payloads,
138    protos::{
139        coresdk::IntoPayloadsExt,
140        grpc::health::v1::health_client::HealthClient,
141        proto_ts_to_system_time,
142        temporal::api::{
143            cloud::cloudservice::v1::cloud_service_client::CloudServiceClient,
144            common::v1::{ActivityType, WorkflowType},
145            enums::v1::{
146                ActivityIdConflictPolicy as ProtoActivityIdConflictPolicy,
147                ActivityIdReusePolicy as ProtoActivityIdReusePolicy, TaskQueueKind,
148            },
149            errordetails::v1::WorkflowExecutionAlreadyStartedFailure,
150            operatorservice::v1::operator_service_client::OperatorServiceClient,
151            sdk::v1::UserMetadata,
152            taskqueue::v1::TaskQueue,
153            testservice::v1::test_service_client::TestServiceClient,
154            workflow::v1 as workflow,
155            workflowservice::v1::{
156                count_workflow_executions_response, workflow_service_client::WorkflowServiceClient,
157                *,
158            },
159        },
160        utilities::decode_status_detail,
161    },
162    search_attributes::{SearchAttributeError, SearchAttributeValue, SearchAttributes},
163};
164use tonic::{
165    Code, IntoRequest,
166    body::Body,
167    client::GrpcService,
168    codec::CompressionEncoding,
169    codegen::InterceptedService,
170    metadata::{
171        AsciiMetadataKey, AsciiMetadataValue, BinaryMetadataKey, BinaryMetadataValue, MetadataMap,
172        MetadataValue,
173    },
174    service::Interceptor,
175    transport::{Certificate, Endpoint, Identity},
176};
177use tower::ServiceBuilder;
178use uuid::Uuid;
179
180static CLIENT_NAME_HEADER_KEY: &str = "client-name";
181static CLIENT_VERSION_HEADER_KEY: &str = "client-version";
182static TEMPORAL_NAMESPACE_HEADER_KEY: &str = "temporal-namespace";
183
184#[doc(hidden)]
185/// Key used to communicate when a GRPC message is too large
186pub static MESSAGE_TOO_LARGE_KEY: &str = "message-too-large";
187#[doc(hidden)]
188/// Returns the violation, if `status` is the client proactively rejecting an outbound request for exceeding a
189/// payload/memo error size limit.
190pub fn payload_limit_violation_from(
191    status: &tonic::Status,
192) -> Option<&temporalio_common::payload_limits::PayloadLimitViolation> {
193    std::error::Error::source(status).and_then(|src| src.downcast_ref())
194}
195#[doc(hidden)]
196/// Key used to indicate a error was returned by the retryer because of the short-circuit predicate
197pub static ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT: &str = "short-circuit";
198
199/// The server times out polls after 60 seconds. Set our timeout to be slightly beyond that.
200const LONG_POLL_TIMEOUT: Duration = Duration::from_secs(70);
201const OTHER_CALL_TIMEOUT: Duration = Duration::from_secs(30);
202const VERSION: &str = env!("CARGO_PKG_VERSION");
203
204/// A connection to the Temporal service.
205///
206/// Cloning a connection is cheap (single Arc increment). The underlying connection is shared
207/// between clones.
208#[derive(Clone, Debug)]
209pub struct Connection {
210    inner: Arc<ConnectionInner>,
211}
212
213#[derive(Clone, derive_more::Debug)]
214struct ConnectionInner {
215    #[debug(skip)]
216    service: TemporalServiceClient,
217    retry_options: RetryOptions,
218    identity: String,
219    headers: Arc<RwLock<ClientHeaders>>,
220    client_name: String,
221    client_version: String,
222    /// Capabilities as read from the `get_system_info` RPC call made on client connection
223    capabilities: Option<get_system_info_response::Capabilities>,
224    workers: Arc<ClientWorkerSet>,
225    _dns_task: Option<Arc<dns::DnsReresolutionHandle>>,
226    /// Configured payload/memo size warning thresholds (bytes); `0` disables that warning.
227    payloads_warn_size: usize,
228    memo_warn_size: usize,
229}
230
231/// Resolve a user-configured warning threshold (bytes) into the internal representation. `0`
232/// disables the warning (`None`); so does a value that doesn't fit in `usize` on this platform (a
233/// threshold larger than any addressable payload could never fire anyway), with a warning logged.
234/// `option` names the configured field, for diagnostics.
235fn resolve_warn_threshold(option: &'static str, bytes: u64) -> usize {
236    usize::try_from(bytes).unwrap_or_else(|_| {
237        warn!(
238            option,
239            configured_bytes = bytes,
240            "Configured payload size warning threshold exceeds the maximum addressable size on this \
241             platform; disabling this warning"
242        );
243        0
244    })
245}
246
247impl Connection {
248    /// Connect to a Temporal service.
249    pub async fn connect(mut options: ConnectionOptions) -> Result<Self, ClientConnectError> {
250        if options.service_override.is_some() {
251            options.grpc_compression = GrpcCompression::None;
252        }
253
254        let first_result = Self::connect_once(&options).await;
255        if options.grpc_compression == GrpcCompression::Gzip
256            && let Err(ClientConnectError::SystemInfoCallError(status)) = &first_result
257            && status.code() == Code::Unimplemented
258            && {
259                let msg = status.message().to_lowercase();
260                msg.contains("decompress")
261                    || msg.contains("grpc-encoding")
262                    || msg.contains("compressor")
263            }
264        {
265            options.grpc_compression = GrpcCompression::None;
266            return Self::connect_once(&options).await;
267        }
268        first_result
269    }
270
271    async fn connect_once(options: &ConnectionOptions) -> Result<Self, ClientConnectError> {
272        let dns_lb_opts = dns::validate_and_get_dns_lb(options)?.cloned();
273        let (service, dns_task) = if let Some(service_override) = options.service_override.clone() {
274            (
275                GrpcMetricSvc {
276                    inner: ChannelOrGrpcOverride::GrpcOverride(service_override),
277                    metrics: options.metrics_meter.clone().map(MetricsContext::new),
278                    disable_errcode_label: options.disable_error_code_metric_tags,
279                },
280                None,
281            )
282        } else if let Some(dns_opts) = &dns_lb_opts {
283            let (channel, sender) = dns::create_balanced_channel(options).await?;
284            let handle = dns::spawn_dns_reresolution(
285                sender,
286                options.target.clone(),
287                options.tls_options.clone(),
288                options.keep_alive.clone(),
289                options.override_origin.clone(),
290                dns_opts.resolution_interval,
291                options.connect_timeout,
292            );
293            (
294                ServiceBuilder::new()
295                    .layer_fn(move |channel| GrpcMetricSvc {
296                        inner: ChannelOrGrpcOverride::Channel(channel),
297                        metrics: options.metrics_meter.clone().map(MetricsContext::new),
298                        disable_errcode_label: options.disable_error_code_metric_tags,
299                    })
300                    .service(channel),
301                Some(handle),
302            )
303        } else {
304            let endpoint = Endpoint::from_shared(options.target.to_string())?;
305            let endpoint = if let Some(timeout) = options.connect_timeout {
306                endpoint.connect_timeout(timeout)
307            } else {
308                endpoint
309            };
310            let tls_result = add_tls_to_channel(options.tls_options.as_ref(), endpoint).await?;
311
312            #[cfg(feature = "dynamic-tls")]
313            let (channel, custom_connector_info) = match tls_result {
314                TlsConfigResult::Standard(ep) => (
315                    ep,
316                    None::<(Arc<tokio_rustls::rustls::ClientConfig>, String)>,
317                ),
318                TlsConfigResult::CustomConnector {
319                    endpoint: ep,
320                    rustls_config,
321                    domain,
322                } => (ep, Some((rustls_config, domain))),
323            };
324            #[cfg(not(feature = "dynamic-tls"))]
325            let channel = match tls_result {
326                TlsConfigResult::Standard(ep) => ep,
327            };
328
329            let channel = if let Some(keep_alive) = options.keep_alive.as_ref() {
330                channel
331                    .keep_alive_while_idle(true)
332                    .http2_keep_alive_interval(keep_alive.interval)
333                    .keep_alive_timeout(keep_alive.timeout)
334            } else {
335                channel
336            };
337            let channel = if let Some(origin) = options.override_origin.clone() {
338                channel.origin(origin)
339            } else {
340                channel
341            };
342            // Validate that proxy and dynamic cert resolver aren't combined
343            #[cfg(feature = "dynamic-tls")]
344            if options.http_connect_proxy.is_some() && custom_connector_info.is_some() {
345                return Err(ClientConnectError::InvalidConfig(
346                    "client_cert_resolver is not yet supported with http_connect_proxy. \
347                     Use static client_tls_options when using a proxy, or remove the proxy."
348                        .to_owned(),
349                ));
350            }
351            // Connect, using a custom TLS connector if dynamic cert resolution is needed
352            let channel = if let Some(proxy) = options.http_connect_proxy.as_ref() {
353                proxy.connect_endpoint(&channel).await?
354            } else {
355                #[cfg(feature = "dynamic-tls")]
356                if let Some((rustls_config, domain)) = custom_connector_info {
357                    let server_name =
358                        tokio_rustls::rustls::pki_types::ServerName::try_from(domain.as_str())
359                            .map_err(|e| {
360                                ClientConnectError::InvalidConfig(format!(
361                                    "Invalid TLS domain name '{domain}': {e}"
362                                ))
363                            })?
364                            .to_owned();
365                    let connector = DynamicTlsConnector {
366                        tls: tokio_rustls::TlsConnector::from(rustls_config),
367                        domain: Arc::new(server_name),
368                    };
369                    channel.connect_with_connector(connector).await?
370                } else {
371                    channel.connect().await?
372                }
373                #[cfg(not(feature = "dynamic-tls"))]
374                channel.connect().await?
375            };
376            (
377                ServiceBuilder::new()
378                    .layer_fn(move |channel| GrpcMetricSvc {
379                        inner: ChannelOrGrpcOverride::Channel(channel),
380                        metrics: options.metrics_meter.clone().map(MetricsContext::new),
381                        disable_errcode_label: options.disable_error_code_metric_tags,
382                    })
383                    .service(channel),
384                None,
385            )
386        };
387
388        let headers = Arc::new(RwLock::new(ClientHeaders {
389            user_headers: parse_ascii_headers(options.headers.clone().unwrap_or_default())?,
390            user_binary_headers: parse_binary_headers(
391                options.binary_headers.clone().unwrap_or_default(),
392            )?,
393            api_key: options.api_key.clone(),
394        }));
395        let interceptor = ServiceCallInterceptor {
396            client_name: options.client_name.clone(),
397            client_version: options.client_version.clone(),
398            headers: headers.clone(),
399        };
400        let svc = InterceptedService::new(service, interceptor);
401        let mut svc_client = TemporalServiceClient::new(svc, options.grpc_compression);
402
403        let capabilities = if !options.skip_get_system_info {
404            match svc_client
405                .get_system_info(GetSystemInfoRequest::default().into_request())
406                .await
407            {
408                Ok(sysinfo) => sysinfo.into_inner().capabilities,
409                Err(status) => match status.code() {
410                    Code::Unimplemented
411                        if {
412                            let msg = status.message().to_lowercase();
413                            msg.contains("unknown method")
414                                || msg.contains("unknown service")
415                                || msg.contains("method not found")
416                                || (msg.contains("getsysteminfo")
417                                    && (msg.contains("is unimplemented")
418                                        || msg.contains("not implement")))
419                        } =>
420                    {
421                        None
422                    }
423                    _ => return Err(ClientConnectError::SystemInfoCallError(status)),
424                },
425            }
426        } else {
427            None
428        };
429        Ok(Self {
430            inner: Arc::new(ConnectionInner {
431                service: svc_client,
432                retry_options: options.retry_options.clone(),
433                identity: options.identity.clone(),
434                headers,
435                client_name: options.client_name.clone(),
436                client_version: options.client_version.clone(),
437                capabilities,
438                workers: Arc::new(ClientWorkerSet::new()),
439                _dns_task: dns_task,
440                payloads_warn_size: resolve_warn_threshold(
441                    "payloads_warn_size",
442                    options.payload_limits.payloads_warn_size,
443                ),
444                memo_warn_size: resolve_warn_threshold(
445                    "memo_warn_size",
446                    options.payload_limits.memo_warn_size,
447                ),
448            }),
449        })
450    }
451
452    /// Set API key, overwriting any previous one.
453    pub fn set_api_key(&self, api_key: Option<String>) {
454        self.inner.headers.write().api_key = api_key;
455    }
456
457    /// Set HTTP request headers overwriting previous headers.
458    ///
459    /// This will not affect headers set via [ConnectionOptions::binary_headers].
460    ///
461    /// # Errors
462    ///
463    /// Will return an error if any of the provided keys or values are not valid gRPC metadata.
464    /// If an error is returned, the previous headers will remain unchanged.
465    pub fn set_headers(&self, headers: HashMap<String, String>) -> Result<(), InvalidHeaderError> {
466        self.inner.headers.write().user_headers = parse_ascii_headers(headers)?;
467        Ok(())
468    }
469
470    /// Set binary HTTP request headers overwriting previous headers.
471    ///
472    /// This will not affect headers set via [ConnectionOptions::headers].
473    ///
474    /// # Errors
475    ///
476    /// Will return an error if any of the provided keys are not valid gRPC binary metadata keys.
477    /// If an error is returned, the previous headers will remain unchanged.
478    pub fn set_binary_headers(
479        &self,
480        binary_headers: HashMap<String, Vec<u8>>,
481    ) -> Result<(), InvalidHeaderError> {
482        self.inner.headers.write().user_binary_headers = parse_binary_headers(binary_headers)?;
483        Ok(())
484    }
485
486    /// Returns the value used for the `client-name` header by this connection.
487    pub fn client_name(&self) -> &str {
488        &self.inner.client_name
489    }
490
491    /// Returns the value used for the `client-version` header by this connection.
492    pub fn client_version(&self) -> &str {
493        &self.inner.client_version
494    }
495
496    /// Returns the server capabilities we (may have) learned about when establishing an initial
497    /// connection
498    pub fn capabilities(&self) -> Option<&get_system_info_response::Capabilities> {
499        self.inner.capabilities.as_ref()
500    }
501
502    /// Get a mutable reference to the retry options.
503    ///
504    /// Note: If this connection has been cloned, this will copy-on-write to avoid
505    /// affecting other clones.
506    pub fn retry_options_mut(&mut self) -> &mut RetryOptions {
507        &mut Arc::make_mut(&mut self.inner).retry_options
508    }
509
510    /// Get a reference to the connection identity.
511    pub fn identity(&self) -> &str {
512        &self.inner.identity
513    }
514
515    /// Get a mutable reference to the connection identity.
516    ///
517    /// Note: If this connection has been cloned, this will copy-on-write to avoid
518    /// affecting other clones.
519    pub fn identity_mut(&mut self) -> &mut String {
520        &mut Arc::make_mut(&mut self.inner).identity
521    }
522
523    /// Returns a reference to a registry with workers using this client instance.
524    pub fn workers(&self) -> Arc<ClientWorkerSet> {
525        self.inner.workers.clone()
526    }
527
528    /// Returns the client-wide key.
529    pub fn worker_grouping_key(&self) -> Uuid {
530        self.inner.workers.worker_grouping_key()
531    }
532
533    /// Get the underlying workflow service client for making raw gRPC calls.
534    pub fn workflow_service(&self) -> Box<dyn WorkflowService> {
535        self.inner.service.workflow_service()
536    }
537
538    /// Get the underlying operator service client for making raw gRPC calls.
539    pub fn operator_service(&self) -> Box<dyn OperatorService> {
540        self.inner.service.operator_service()
541    }
542
543    /// Get the underlying cloud service client for making raw gRPC calls.
544    pub fn cloud_service(&self) -> Box<dyn CloudService> {
545        self.inner.service.cloud_service()
546    }
547
548    /// Get the underlying test service client for making raw gRPC calls.
549    pub fn test_service(&self) -> Box<dyn TestService> {
550        self.inner.service.test_service()
551    }
552
553    /// Get the underlying health service client for making raw gRPC calls.
554    pub fn health_service(&self) -> Box<dyn HealthService> {
555        self.inner.service.health_service()
556    }
557}
558
559#[derive(Debug)]
560struct ClientHeaders {
561    user_headers: HashMap<AsciiMetadataKey, AsciiMetadataValue>,
562    user_binary_headers: HashMap<BinaryMetadataKey, BinaryMetadataValue>,
563    api_key: Option<String>,
564}
565
566impl ClientHeaders {
567    fn apply_to_metadata(&self, metadata: &mut MetadataMap) {
568        for (key, val) in self.user_headers.iter() {
569            // Only if not already present
570            if !metadata.contains_key(key) {
571                metadata.insert(key, val.clone());
572            }
573        }
574        for (key, val) in self.user_binary_headers.iter() {
575            // Only if not already present
576            if !metadata.contains_key(key) {
577                metadata.insert_bin(key, val.clone());
578            }
579        }
580        if let Some(api_key) = &self.api_key {
581            // Only if not already present
582            if !metadata.contains_key("authorization")
583                && let Ok(val) = format!("Bearer {api_key}").parse()
584            {
585                metadata.insert("authorization", val);
586            }
587        }
588    }
589}
590
591/// Result of TLS configuration: either standard tonic TLS was applied to the endpoint,
592/// or a custom rustls config is needed for dynamic certificate resolution.
593#[derive(Debug)]
594enum TlsConfigResult {
595    /// Standard tonic TLS was applied, endpoint is ready to connect normally.
596    Standard(Endpoint),
597    /// A custom rustls::ClientConfig is needed. The endpoint has no TLS configured;
598    /// the caller must use `connect_with_connector` with a custom TLS connector.
599    ///
600    /// Experimental API subject to change
601    #[cfg(feature = "dynamic-tls")]
602    CustomConnector {
603        endpoint: Endpoint,
604        rustls_config: Arc<tokio_rustls::rustls::ClientConfig>,
605        domain: String,
606    },
607}
608
609/// If TLS is configured, set the appropriate options on the provided channel and return it.
610/// Passes it through if TLS options not set.
611///
612/// When `client_cert_resolver` is set, tonic's built-in TLS cannot be used (it only supports
613/// static client certificates). In that case, we return `TlsConfigResult::CustomConnector`
614/// with a manually-built `rustls::ClientConfig` that the caller must use with
615/// `connect_with_connector`.
616async fn add_tls_to_channel(
617    tls_options: Option<&TlsOptions>,
618    mut channel: Endpoint,
619) -> Result<TlsConfigResult, ClientConnectError> {
620    if let Some(tls_cfg) = tls_options {
621        if tls_cfg.server_cert_verifier.is_some() && tls_cfg.server_root_ca_cert.is_some() {
622            return Err(ClientConnectError::InvalidConfig(
623                "Cannot set both `server_root_ca_cert` and `server_cert_verifier`".to_owned(),
624            ));
625        }
626
627        #[cfg(feature = "dynamic-tls")]
628        if tls_cfg.client_tls_options.is_some() && tls_cfg.client_cert_resolver.is_some() {
629            return Err(ClientConnectError::InvalidConfig(
630                "Cannot set both `client_tls_options` and `client_cert_resolver`. \
631                 Use `client_tls_options` for static certificates or \
632                 `client_cert_resolver` for dynamic certificate resolution, but not both."
633                    .to_owned(),
634            ));
635        }
636
637        // Extract the domain for SNI / :authority header
638        let domain_override = tls_cfg.domain.clone();
639        if let Some(domain) = &domain_override {
640            let uri: Uri = format!("https://{domain}").parse()?;
641            channel = channel.origin(uri);
642        }
643
644        // Dynamic certificate resolver path: build rustls::ClientConfig manually
645        #[cfg(feature = "dynamic-tls")]
646        if let Some(resolver) = &tls_cfg.client_cert_resolver {
647            let rustls_config = build_custom_rustls_config(tls_cfg, Some(resolver.clone()))?;
648            // Strip brackets from IPv6 literals (e.g. "[::1]" -> "::1")
649            // since ServerName::try_from expects raw IP addresses
650            let sni_domain = domain_override
651                .or_else(|| {
652                    channel
653                        .uri()
654                        .host()
655                        .map(|h| h.trim_matches(|c| c == '[' || c == ']').to_owned())
656                })
657                .ok_or_else(|| {
658                    ClientConnectError::InvalidConfig(
659                        "Cannot determine TLS server name for dynamic cert resolution: \
660                         set 'domain' in TlsOptions or use a URL with a hostname"
661                            .to_owned(),
662                    )
663                })?;
664            return Ok(TlsConfigResult::CustomConnector {
665                endpoint: channel,
666                rustls_config: Arc::new(rustls_config),
667                domain: sni_domain,
668            });
669        }
670
671        // Standard tonic TLS path
672        let mut tls = tonic::transport::ClientTlsConfig::new();
673
674        if tls_cfg.server_cert_verifier.is_none() {
675            if let Some(root_cert) = &tls_cfg.server_root_ca_cert {
676                let server_root_ca_cert = Certificate::from_pem(root_cert);
677                tls = tls.ca_certificate(server_root_ca_cert);
678            } else {
679                tls = tls.with_native_roots();
680            }
681        }
682
683        if let Some(domain) = &tls_cfg.domain {
684            tls = tls.domain_name(domain);
685        }
686
687        if let Some(client_opts) = &tls_cfg.client_tls_options {
688            let client_identity =
689                Identity::from_pem(&client_opts.client_cert, &client_opts.client_private_key);
690            tls = tls.identity(client_identity);
691        }
692
693        let endpoint = if let Some(verifier) = &tls_cfg.server_cert_verifier {
694            channel
695                .tls_config_with_verifier(tls, verifier.clone())
696                .map_err(ClientConnectError::from)?
697        } else {
698            channel.tls_config(tls).map_err(ClientConnectError::from)?
699        };
700        return Ok(TlsConfigResult::Standard(endpoint));
701    }
702    Ok(TlsConfigResult::Standard(channel))
703}
704
705#[cfg(feature = "dynamic-tls")]
706/// Build a `rustls::ClientConfig` manually for the dynamic certificate resolver path.
707///
708/// This replicates the logic that tonic normally handles internally but uses
709/// `with_client_cert_resolver` instead of `with_client_auth_cert`.
710fn build_custom_rustls_config(
711    tls_cfg: &TlsOptions,
712    client_cert_resolver: Option<Arc<dyn tokio_rustls::rustls::client::ResolvesClientCert>>,
713) -> Result<tokio_rustls::rustls::ClientConfig, ClientConnectError> {
714    use tokio_rustls::rustls::{ClientConfig, RootCertStore, crypto};
715
716    // Get or install a crypto provider
717    let provider = crypto::CryptoProvider::get_default()
718        .cloned()
719        .or_else(|| {
720            // Try ring first, then aws-lc, matching tonic's behavior
721            #[cfg(feature = "tls-ring")]
722            {
723                return Some(Arc::new(crypto::ring::default_provider()));
724            }
725            #[cfg(feature = "tls-aws-lc")]
726            #[allow(unreachable_code)]
727            {
728                return Some(Arc::new(crypto::aws_lc_rs::default_provider()));
729            }
730            #[allow(unreachable_code)]
731            None
732        })
733        .ok_or_else(|| {
734            ClientConnectError::InvalidConfig(
735                "No TLS crypto provider available. Enable the `tls-ring` or `tls-aws-lc` feature."
736                    .to_owned(),
737            )
738        })?;
739
740    let builder = ClientConfig::builder_with_provider(provider)
741        .with_safe_default_protocol_versions()
742        .map_err(|e| {
743            ClientConnectError::InvalidConfig(format!("Failed to configure TLS protocols: {e}"))
744        })?;
745
746    // Configure server certificate verification
747    let builder = if let Some(verifier) = &tls_cfg.server_cert_verifier {
748        builder
749            .dangerous()
750            .with_custom_certificate_verifier(verifier.clone())
751    } else {
752        use std::io::Cursor;
753        use tokio_rustls::rustls::pki_types::{CertificateDer, pem::PemObject as _};
754
755        let mut roots = RootCertStore::empty();
756        if let Some(ca_cert) = &tls_cfg.server_root_ca_cert {
757            let certs: Vec<CertificateDer<'static>> =
758                CertificateDer::pem_reader_iter(&mut Cursor::new(ca_cert))
759                    .collect::<Result<Vec<_>, _>>()
760                    .map_err(|e| {
761                        ClientConnectError::InvalidConfig(format!(
762                            "Failed to parse CA certificate PEM: {e}"
763                        ))
764                    })?;
765            roots.add_parsable_certificates(certs);
766            if roots.is_empty() {
767                return Err(ClientConnectError::InvalidConfig(
768                    "None of the provided CA certificates could be parsed. \
769                     Ensure the PEM data contains valid X.509 certificates."
770                        .to_owned(),
771                ));
772            }
773        } else {
774            // Use native OS root certificates (same logic as tonic's with_native_roots)
775            let native_result = rustls_native_certs::load_native_certs();
776            if !native_result.errors.is_empty() {
777                warn!(
778                    "errors occurred when loading native certs: {:?}",
779                    native_result.errors
780                );
781            }
782            if native_result.certs.is_empty() {
783                return Err(ClientConnectError::InvalidConfig(
784                    "No native TLS root certificates found".to_owned(),
785                ));
786            }
787            roots.add_parsable_certificates(native_result.certs);
788            if roots.is_empty() {
789                return Err(ClientConnectError::InvalidConfig(
790                    "Native TLS root certificates were found but none could be parsed".to_owned(),
791                ));
792            }
793        }
794        builder.with_root_certificates(roots)
795    };
796
797    // Configure client authentication
798    let mut config = if let Some(resolver) = client_cert_resolver {
799        builder.with_client_cert_resolver(resolver)
800    } else {
801        builder.with_no_client_auth()
802    };
803
804    // Set ALPN to h2 for HTTP/2 (required by gRPC)
805    config.alpn_protocols.push(b"h2".to_vec());
806
807    Ok(config)
808}
809
810#[cfg(feature = "dynamic-tls")]
811/// Default TCP connect timeout for the dynamic TLS connector.
812/// Matches a reasonable timeout for production use; the built-in tonic connector
813/// uses `Endpoint::connect_timeout()` which we cannot access from a custom connector.
814const DYNAMIC_TLS_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
815
816#[cfg(feature = "dynamic-tls")]
817/// A custom connector that wraps a TCP connector with TLS using a custom
818/// `rustls::ClientConfig` (needed for dynamic cert resolution).
819#[derive(Clone)]
820struct DynamicTlsConnector {
821    tls: tokio_rustls::TlsConnector,
822    domain: Arc<tokio_rustls::rustls::pki_types::ServerName<'static>>,
823}
824
825#[cfg(feature = "dynamic-tls")]
826impl std::fmt::Debug for DynamicTlsConnector {
827    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
828        f.debug_struct("DynamicTlsConnector")
829            .field("domain", &self.domain)
830            .finish()
831    }
832}
833
834#[cfg(feature = "dynamic-tls")]
835impl tower::Service<Uri> for DynamicTlsConnector {
836    type Response = hyper_util::rt::TokioIo<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>;
837    type Error = Box<dyn std::error::Error + Send + Sync>;
838    type Future =
839        Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
840
841    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
842        Poll::Ready(Ok(()))
843    }
844
845    fn call(&mut self, uri: Uri) -> Self::Future {
846        let tls = self.tls.clone();
847        let domain = self.domain.clone();
848
849        Box::pin(async move {
850            let host = uri
851                .host()
852                .ok_or_else(|| -> Box<dyn std::error::Error + Send + Sync> {
853                    format!("URI has no host for TLS connection: {uri}").into()
854                })?;
855            let port = uri.port_u16().unwrap_or(443);
856            // Use (host, port) tuple to correctly handle IPv6 addresses
857            // (e.g. "::1" would break if formatted as "::1:443")
858            let addr_display = format!("{}:{}", host, port);
859
860            debug!(target: "temporal_client", %uri, addr = %addr_display, "DynamicTlsConnector: establishing TCP+TLS connection");
861
862            // Use a timeout to prevent hanging on unreachable hosts.
863            // Tonic's built-in connector respects Endpoint::connect_timeout(),
864            // but custom connectors must handle timeouts themselves.
865            let tcp = tokio::time::timeout(
866                DYNAMIC_TLS_CONNECT_TIMEOUT,
867                tokio::net::TcpStream::connect((host, port)),
868            )
869            .await
870            .map_err(|_| -> Box<dyn std::error::Error + Send + Sync> {
871                format!(
872                    "TCP connect to {addr_display} timed out after {}s",
873                    DYNAMIC_TLS_CONNECT_TIMEOUT.as_secs()
874                )
875                .into()
876            })?
877            .map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
878                format!("TCP connect to {addr_display} failed: {e}").into()
879            })?;
880
881            // Disable Nagle's algorithm for low-latency gRPC messaging
882            tcp.set_nodelay(true)?;
883
884            let tls_stream = tls.connect(domain.as_ref().to_owned(), tcp).await?;
885            debug!(target: "temporal_client", addr = %addr_display, "DynamicTlsConnector: TLS handshake complete");
886            Ok(hyper_util::rt::TokioIo::new(tls_stream))
887        })
888    }
889}
890
891fn parse_ascii_headers(
892    headers: HashMap<String, String>,
893) -> Result<HashMap<AsciiMetadataKey, AsciiMetadataValue>, InvalidHeaderError> {
894    let mut parsed_headers = HashMap::with_capacity(headers.len());
895    for (k, v) in headers.into_iter() {
896        let key = match AsciiMetadataKey::from_str(&k) {
897            Ok(key) => key,
898            Err(err) => {
899                return Err(InvalidHeaderError::InvalidAsciiHeaderKey {
900                    key: k,
901                    source: err,
902                });
903            }
904        };
905        let value = match MetadataValue::from_str(&v) {
906            Ok(value) => value,
907            Err(err) => {
908                return Err(InvalidHeaderError::InvalidAsciiHeaderValue {
909                    key: k,
910                    value: v,
911                    source: err,
912                });
913            }
914        };
915        parsed_headers.insert(key, value);
916    }
917
918    Ok(parsed_headers)
919}
920
921fn parse_binary_headers(
922    headers: HashMap<String, Vec<u8>>,
923) -> Result<HashMap<BinaryMetadataKey, BinaryMetadataValue>, InvalidHeaderError> {
924    let mut parsed_headers = HashMap::with_capacity(headers.len());
925    for (k, v) in headers.into_iter() {
926        let key = match BinaryMetadataKey::from_str(&k) {
927            Ok(key) => key,
928            Err(err) => {
929                return Err(InvalidHeaderError::InvalidBinaryHeaderKey {
930                    key: k,
931                    source: err,
932                });
933            }
934        };
935        let value = BinaryMetadataValue::from_bytes(&v);
936        parsed_headers.insert(key, value);
937    }
938
939    Ok(parsed_headers)
940}
941
942/// Interceptor which attaches common metadata (like "client-name") to every outgoing call
943#[derive(Clone)]
944pub struct ServiceCallInterceptor {
945    client_name: String,
946    client_version: String,
947    /// Only accessed as a reader
948    headers: Arc<RwLock<ClientHeaders>>,
949}
950
951impl Interceptor for ServiceCallInterceptor {
952    /// This function will get called on each outbound request. Returning a `Status` here will
953    /// cancel the request and have that status returned to the caller.
954    fn call(
955        &mut self,
956        mut request: tonic::Request<()>,
957    ) -> Result<tonic::Request<()>, tonic::Status> {
958        let metadata = request.metadata_mut();
959        if !metadata.contains_key(CLIENT_NAME_HEADER_KEY) {
960            metadata.insert(
961                CLIENT_NAME_HEADER_KEY,
962                self.client_name
963                    .parse()
964                    .unwrap_or_else(|_| MetadataValue::from_static("")),
965            );
966        }
967        if !metadata.contains_key(CLIENT_VERSION_HEADER_KEY) {
968            metadata.insert(
969                CLIENT_VERSION_HEADER_KEY,
970                self.client_version
971                    .parse()
972                    .unwrap_or_else(|_| MetadataValue::from_static("")),
973            );
974        }
975        self.headers.read().apply_to_metadata(metadata);
976        request.set_default_timeout(OTHER_CALL_TIMEOUT);
977
978        Ok(request)
979    }
980}
981
982/// Aggregates various services exposed by the Temporal server
983#[derive(Clone)]
984pub struct TemporalServiceClient {
985    workflow_svc_client: Box<dyn WorkflowService>,
986    operator_svc_client: Box<dyn OperatorService>,
987    cloud_svc_client: Box<dyn CloudService>,
988    test_svc_client: Box<dyn TestService>,
989    health_svc_client: Box<dyn HealthService>,
990}
991
992/// We up the limit on incoming messages from server from the 4Mb default to 128Mb. If for
993/// whatever reason this needs to be changed by the user, we support overriding it via env var.
994fn get_decode_max_size() -> usize {
995    static _DECODE_MAX_SIZE: OnceLock<usize> = OnceLock::new();
996    *_DECODE_MAX_SIZE.get_or_init(|| {
997        std::env::var("TEMPORAL_MAX_INCOMING_GRPC_BYTES")
998            .ok()
999            .and_then(|s| s.parse().ok())
1000            .unwrap_or(128 * 1024 * 1024)
1001    })
1002}
1003
1004impl TemporalServiceClient {
1005    fn new<T>(svc: T, compression: GrpcCompression) -> Self
1006    where
1007        T: GrpcService<Body> + Send + Sync + Clone + 'static,
1008        T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + 'static,
1009        T::Error: Into<tonic::codegen::StdError>,
1010        <T::ResponseBody as tonic::codegen::Body>::Error: Into<tonic::codegen::StdError> + Send,
1011        <T as GrpcService<Body>>::Future: Send,
1012    {
1013        // The generated service clients don't share a trait exposing the compression setters, so
1014        // a macro applies the same configuration to each concrete client type.
1015        macro_rules! configure {
1016            ($client:expr) => {{
1017                let client = $client.max_decoding_message_size(get_decode_max_size());
1018                match compression {
1019                    GrpcCompression::Gzip => client
1020                        .send_compressed(CompressionEncoding::Gzip)
1021                        .accept_compressed(CompressionEncoding::Gzip),
1022                    GrpcCompression::None => client,
1023                }
1024            }};
1025        }
1026
1027        let workflow_svc_client = Box::new(configure!(WorkflowServiceClient::new(svc.clone())));
1028        let operator_svc_client = Box::new(configure!(OperatorServiceClient::new(svc.clone())));
1029        let cloud_svc_client = Box::new(configure!(CloudServiceClient::new(svc.clone())));
1030        let test_svc_client = Box::new(configure!(TestServiceClient::new(svc.clone())));
1031        let health_svc_client = Box::new(configure!(HealthClient::new(svc.clone())));
1032
1033        Self {
1034            workflow_svc_client,
1035            operator_svc_client,
1036            cloud_svc_client,
1037            test_svc_client,
1038            health_svc_client,
1039        }
1040    }
1041
1042    /// Create a service client from implementations of the individual underlying services. Useful
1043    /// for mocking out service implementations.
1044    pub fn from_services(
1045        workflow: Box<dyn WorkflowService>,
1046        operator: Box<dyn OperatorService>,
1047        cloud: Box<dyn CloudService>,
1048        test: Box<dyn TestService>,
1049        health: Box<dyn HealthService>,
1050    ) -> Self {
1051        Self {
1052            workflow_svc_client: workflow,
1053            operator_svc_client: operator,
1054            cloud_svc_client: cloud,
1055            test_svc_client: test,
1056            health_svc_client: health,
1057        }
1058    }
1059
1060    /// Get the underlying workflow service client
1061    pub fn workflow_service(&self) -> Box<dyn WorkflowService> {
1062        self.workflow_svc_client.clone()
1063    }
1064    /// Get the underlying operator service client
1065    pub fn operator_service(&self) -> Box<dyn OperatorService> {
1066        self.operator_svc_client.clone()
1067    }
1068    /// Get the underlying cloud service client
1069    pub fn cloud_service(&self) -> Box<dyn CloudService> {
1070        self.cloud_svc_client.clone()
1071    }
1072    /// Get the underlying test service client
1073    pub fn test_service(&self) -> Box<dyn TestService> {
1074        self.test_svc_client.clone()
1075    }
1076    /// Get the underlying health service client
1077    pub fn health_service(&self) -> Box<dyn HealthService> {
1078        self.health_svc_client.clone()
1079    }
1080}
1081
1082/// Contains an instance of a namespace-bound client for interacting with the Temporal server.
1083/// Cheap to clone.
1084#[derive(Clone, Debug)]
1085pub struct Client {
1086    connection: Connection,
1087    options: Arc<ClientOptions>,
1088}
1089
1090impl Client {
1091    /// Connect to a Temporal service and create a namespace-bound client, applying registered
1092    /// plugins to connection and client options in registration order.
1093    pub async fn connect(
1094        mut connection_options: ConnectionOptions,
1095        client_options: ClientOptions,
1096    ) -> Result<Self, ClientConnectError> {
1097        plugins::apply_connection_plugins(&client_options, &mut connection_options)?;
1098        let connection = Connection::connect(connection_options).await?;
1099        Ok(Self::new(connection, client_options)?)
1100    }
1101
1102    /// Create a new client from a connection and options.
1103    ///
1104    /// Registered client plugins are applied here. Connection plugin hooks only run when using
1105    /// [`Client::connect`].
1106    pub fn new(connection: Connection, mut options: ClientOptions) -> Result<Self, ClientNewError> {
1107        plugins::apply_client_plugins(&mut options)?;
1108        Ok(Client {
1109            connection,
1110            options: Arc::new(options),
1111        })
1112    }
1113
1114    /// Return the options this client was initialized with
1115    pub fn options(&self) -> &ClientOptions {
1116        &self.options
1117    }
1118
1119    /// Return this client's options mutably.
1120    ///
1121    /// Note: If this client has been cloned, this will copy-on-write to avoid affecting other
1122    /// clones.
1123    pub fn options_mut(&mut self) -> &mut ClientOptions {
1124        Arc::make_mut(&mut self.options)
1125    }
1126
1127    /// Returns a reference to the underlying connection
1128    pub fn connection(&self) -> &Connection {
1129        &self.connection
1130    }
1131
1132    /// Returns a mutable reference to the underlying connection
1133    pub fn connection_mut(&mut self) -> &mut Connection {
1134        &mut self.connection
1135    }
1136}
1137
1138// High-level workflow operations on Client.
1139// These forward to the internal WorkflowClientTrait blanket impl which is
1140// available because Client implements WorkflowService + NamespacedClient + Clone.
1141impl Client {
1142    /// Start a workflow execution.
1143    ///
1144    /// Returns a [`WorkflowHandle`] that can be used to interact with the workflow
1145    /// (e.g., get its result, send signals, query, etc.).
1146    pub async fn start_workflow<W>(
1147        &self,
1148        workflow: W,
1149        input: W::Input,
1150        options: WorkflowStartOptions,
1151    ) -> Result<WorkflowHandle<Self, W>, WorkflowStartError>
1152    where
1153        W: HasWorkflowDefinition,
1154        W::Input: Send,
1155    {
1156        WorkflowClientTrait::start_workflow(self, workflow, input, options).await
1157    }
1158
1159    /// Get a handle to an existing workflow.
1160    ///
1161    /// For untyped access, use `get_workflow_handle::<UntypedWorkflow>(...)`.
1162    pub fn get_workflow_handle<W: HasWorkflowDefinition>(
1163        &self,
1164        workflow_id: impl Into<String>,
1165    ) -> WorkflowHandle<Self, W> {
1166        WorkflowClientTrait::get_workflow_handle(self, workflow_id)
1167    }
1168
1169    /// List workflows matching a query.
1170    ///
1171    /// Returns a stream that lazily paginates through results.
1172    /// Use `limit` in options to cap the number of results returned.
1173    pub fn list_workflows(
1174        &self,
1175        query: impl Into<String>,
1176        opts: WorkflowListOptions,
1177    ) -> ListWorkflowsStream {
1178        WorkflowClientTrait::list_workflows(self, query, opts)
1179    }
1180
1181    /// Count workflows matching a query.
1182    pub async fn count_workflows(
1183        &self,
1184        query: impl Into<String>,
1185        opts: WorkflowCountOptions,
1186    ) -> Result<WorkflowExecutionCount, ClientError> {
1187        WorkflowClientTrait::count_workflows(self, query, opts).await
1188    }
1189
1190    /// Get a handle to complete an activity asynchronously.
1191    ///
1192    /// An activity returning `ActivityError::WillCompleteAsync` can be completed with this handle.
1193    ///
1194    /// To get a handle to a standalone activity that can be used to wait for result and manage
1195    /// the execution, see [`get_activity_handle`](Self::get_activity_handle).
1196    pub fn get_async_activity_handle(
1197        &self,
1198        identifier: ActivityIdentifier,
1199    ) -> AsyncActivityHandle<Self> {
1200        WorkflowClientTrait::get_async_activity_handle(self, identifier)
1201    }
1202
1203    /// Start a standalone activity.
1204    ///
1205    /// Returns [`ActivityHandle`] that can be used to wait for result or to perform other
1206    /// operations on the activity.
1207    pub async fn start_activity<A>(
1208        &self,
1209        activity: A,
1210        input: A::Input,
1211        options: ActivityStartOptions,
1212    ) -> Result<ActivityHandle<Self, A>, StartActivityError>
1213    where
1214        A: ActivityDefinition,
1215    {
1216        WorkflowClientTrait::start_activity(self, activity, input, options).await
1217    }
1218
1219    /// Get a handle to an existing standalone activity execution. If `run_id` is not specified,
1220    /// the handle always targets the latest execution with matching ID.
1221    ///
1222    /// Note that the validity of the handle is not checked until a method is called on it.
1223    /// If invalid ID or run ID is used, the method will return `NotFound` error.
1224    ///
1225    /// To get an untyped handle, use [`get_untyped_activity_handle`](Self::get_untyped_activity_handle).
1226    ///
1227    /// To get a handle that can be used to complete an activity asynchronously,
1228    /// see [`get_async_activity_handle`](Self::get_async_activity_handle).
1229    pub fn get_activity_handle<A>(
1230        &self,
1231        activity: A,
1232        id: impl Into<String>,
1233        run_id: Option<String>,
1234    ) -> ActivityHandle<Self, A>
1235    where
1236        Self: Sized,
1237        A: ActivityDefinition,
1238    {
1239        WorkflowClientTrait::get_activity_handle(self, activity, id, run_id)
1240    }
1241
1242    /// Get an untyped handle to an existing standalone activity execution. If `run_id` is not
1243    /// specified, the handle always targets the latest execution with matching ID.
1244    ///
1245    /// Note that the validity of the handle is not checked until a method is called on it.
1246    /// If invalid ID or run ID is used, the method will return `NotFound` error.
1247    ///
1248    /// To get a typed handle, use [`get_activity_handle`](Self::get_activity_handle).
1249    ///
1250    /// To get a handle that can be used to complete an activity asynchronously,
1251    /// see [`get_async_activity_handle`](Self::get_async_activity_handle).
1252    pub fn get_untyped_activity_handle(
1253        &self,
1254        id: impl Into<String>,
1255        run_id: Option<String>,
1256    ) -> ActivityHandle<Self, UntypedActivity>
1257    where
1258        Self: Sized,
1259    {
1260        WorkflowClientTrait::get_untyped_activity_handle(self, id, run_id)
1261    }
1262
1263    /// List activities matching a query. Returns a stream that lazily paginates through results.
1264    pub fn list_activities(
1265        &self,
1266        query: impl Into<String>,
1267        options: ActivityListOptions,
1268    ) -> ListActivitiesStream {
1269        WorkflowClientTrait::list_activities(self, query, options)
1270    }
1271
1272    /// Count activities matching a query.
1273    pub async fn count_activities(
1274        &self,
1275        query: impl Into<String>,
1276        options: ActivityCountOptions,
1277    ) -> Result<ActivityExecutionCount, ClientError> {
1278        WorkflowClientTrait::count_activities(self, query, options).await
1279    }
1280}
1281
1282impl NamespacedClient for Client {
1283    fn namespace(&self) -> String {
1284        self.options.namespace.clone()
1285    }
1286
1287    fn identity(&self) -> String {
1288        self.connection.identity().to_owned()
1289    }
1290
1291    fn data_converter(&self) -> &DataConverter {
1292        &self.options.data_converter
1293    }
1294
1295    fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
1296        &self.options.client_interceptors
1297    }
1298}
1299
1300/// Enum to help reference a namespace by either the namespace name or the namespace id
1301#[derive(Clone)]
1302pub enum Namespace {
1303    /// Namespace name
1304    Name(String),
1305    /// Namespace id
1306    Id(String),
1307}
1308
1309/// This trait provides higher-level friendlier interaction with the server.
1310/// See the [WorkflowService] trait for a lower-level client.
1311pub(crate) trait WorkflowClientTrait: NamespacedClient {
1312    /// Start a workflow execution.
1313    fn start_workflow<W>(
1314        &self,
1315        workflow: W,
1316        input: W::Input,
1317        options: WorkflowStartOptions,
1318    ) -> impl Future<Output = Result<WorkflowHandle<Self, W>, WorkflowStartError>>
1319    where
1320        Self: Sized,
1321        W: HasWorkflowDefinition,
1322        W::Input: Send;
1323
1324    /// Get a handle to an existing workflow. `run_id` may be left blank to specify the most recent
1325    /// execution having the provided `workflow_id`.
1326    ///
1327    /// For untyped access, use `get_workflow_handle::<UntypedWorkflow>(...)`.
1328    ///
1329    /// See also [WorkflowHandle::new], for specifying namespace or first_execution_run_id.
1330    fn get_workflow_handle<W: HasWorkflowDefinition>(
1331        &self,
1332        workflow_id: impl Into<String>,
1333    ) -> WorkflowHandle<Self, W>
1334    where
1335        Self: Sized;
1336
1337    /// List workflows matching a query.
1338    /// Returns a stream that lazily paginates through results.
1339    /// Use `limit` in options to cap the number of results returned.
1340    fn list_workflows(
1341        &self,
1342        query: impl Into<String>,
1343        opts: WorkflowListOptions,
1344    ) -> ListWorkflowsStream;
1345
1346    /// Count workflows matching a query.
1347    fn count_workflows(
1348        &self,
1349        query: impl Into<String>,
1350        opts: WorkflowCountOptions,
1351    ) -> impl Future<Output = Result<WorkflowExecutionCount, ClientError>>;
1352
1353    /// Get a handle to complete an activity asynchronously.
1354    ///
1355    /// An activity returning `ActivityError::WillCompleteAsync` can be completed with this handle.
1356    fn get_async_activity_handle(
1357        &self,
1358        identifier: ActivityIdentifier,
1359    ) -> AsyncActivityHandle<Self>
1360    where
1361        Self: Sized;
1362
1363    /// Start a standalone activity.
1364    fn start_activity<A>(
1365        &self,
1366        activity: A,
1367        input: A::Input,
1368        options: ActivityStartOptions,
1369    ) -> impl Future<Output = Result<ActivityHandle<Self, A>, StartActivityError>>
1370    where
1371        Self: Sized,
1372        A: ActivityDefinition;
1373
1374    /// Get a handle to a previously started standalone activity.
1375    fn get_activity_handle<A>(
1376        &self,
1377        activity: A,
1378        id: impl Into<String>,
1379        run_id: Option<String>,
1380    ) -> ActivityHandle<Self, A>
1381    where
1382        Self: Sized,
1383        A: ActivityDefinition;
1384
1385    /// Get an untyped handle to a previously started standalone activity.
1386    fn get_untyped_activity_handle(
1387        &self,
1388        id: impl Into<String>,
1389        run_id: Option<String>,
1390    ) -> ActivityHandle<Self, UntypedActivity>
1391    where
1392        Self: Sized;
1393
1394    /// List activities matching a query. Returns a stream that lazily paginates through results.
1395    fn list_activities(
1396        &self,
1397        query: impl Into<String>,
1398        _options: ActivityListOptions,
1399    ) -> ListActivitiesStream;
1400
1401    /// Count activities matching a query.
1402    fn count_activities(
1403        &self,
1404        query: impl Into<String>,
1405        _options: ActivityCountOptions,
1406    ) -> impl Future<Output = Result<ActivityExecutionCount, ClientError>>;
1407}
1408
1409/// A client that is bound to a namespace
1410pub trait NamespacedClient {
1411    /// Returns the namespace this client is bound to
1412    fn namespace(&self) -> String;
1413    /// Returns the client identity
1414    fn identity(&self) -> String;
1415    /// Returns the data converter for serializing/deserializing payloads.
1416    /// Default implementation returns a static default converter.
1417    fn data_converter(&self) -> &DataConverter {
1418        static DEFAULT: OnceLock<DataConverter> = OnceLock::new();
1419        DEFAULT.get_or_init(DataConverter::default)
1420    }
1421    /// Returns the interceptors used for high-level client operations.
1422    ///
1423    /// # Warning
1424    ///
1425    /// This provider exists so SDK-owned client handles can carry interceptor configuration
1426    /// through the high-level client blanket implementation. Custom client implementations should
1427    /// normally retain the default empty chain unless they deliberately provide the same plumbing.
1428    fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
1429        &[]
1430    }
1431}
1432
1433/// A workflow execution returned from list operations.
1434/// This represents information about a workflow present in visibility.
1435#[derive(Debug, Clone)]
1436pub struct WorkflowExecution {
1437    raw: workflow::WorkflowExecutionInfo,
1438    data_converter: DataConverter,
1439}
1440
1441impl WorkflowExecution {
1442    fn new_with_data_converter(
1443        raw: workflow::WorkflowExecutionInfo,
1444        data_converter: DataConverter,
1445    ) -> Self {
1446        Self {
1447            raw,
1448            data_converter,
1449        }
1450    }
1451
1452    /// The workflow ID.
1453    pub fn id(&self) -> &str {
1454        self.raw
1455            .execution
1456            .as_ref()
1457            .map(|e| e.workflow_id.as_str())
1458            .unwrap_or("")
1459    }
1460
1461    /// The run ID.
1462    pub fn run_id(&self) -> &str {
1463        self.raw
1464            .execution
1465            .as_ref()
1466            .map(|e| e.run_id.as_str())
1467            .unwrap_or("")
1468    }
1469
1470    /// The workflow type name.
1471    pub fn workflow_type(&self) -> &str {
1472        self.raw
1473            .r#type
1474            .as_ref()
1475            .map(|t| t.name.as_str())
1476            .unwrap_or("")
1477    }
1478
1479    /// The current status of the workflow execution.
1480    pub fn status(&self) -> WorkflowExecutionStatus {
1481        WorkflowExecutionStatus::from_raw(self.raw.status)
1482    }
1483
1484    /// When the workflow was created.
1485    pub fn start_time(&self) -> Option<SystemTime> {
1486        self.raw
1487            .start_time
1488            .as_ref()
1489            .and_then(proto_ts_to_system_time)
1490    }
1491
1492    /// When the workflow run started or should start.
1493    pub fn execution_time(&self) -> Option<SystemTime> {
1494        self.raw
1495            .execution_time
1496            .as_ref()
1497            .and_then(proto_ts_to_system_time)
1498    }
1499
1500    /// When the workflow was closed, if closed.
1501    pub fn close_time(&self) -> Option<SystemTime> {
1502        self.raw
1503            .close_time
1504            .as_ref()
1505            .and_then(proto_ts_to_system_time)
1506    }
1507
1508    /// The task queue the workflow runs on.
1509    pub fn task_queue(&self) -> &str {
1510        &self.raw.task_queue
1511    }
1512
1513    /// Number of events in history.
1514    pub fn history_length(&self) -> i64 {
1515        self.raw.history_length
1516    }
1517
1518    /// Workflow memo decoded with the client's payload converter.
1519    pub fn memo(&self) -> Memo {
1520        Memo::from_raw(
1521            self.raw.memo.clone(),
1522            self.data_converter.payload_converter().clone(),
1523            SerializationContextData::Workflow,
1524        )
1525    }
1526
1527    /// Parent workflow ID, if this is a child workflow.
1528    pub fn parent_id(&self) -> Option<&str> {
1529        self.raw
1530            .parent_execution
1531            .as_ref()
1532            .map(|e| e.workflow_id.as_str())
1533    }
1534
1535    /// Parent run ID, if this is a child workflow.
1536    pub fn parent_run_id(&self) -> Option<&str> {
1537        self.raw
1538            .parent_execution
1539            .as_ref()
1540            .map(|e| e.run_id.as_str())
1541    }
1542
1543    /// Search attributes on the workflow.
1544    pub fn search_attributes(&self) -> SearchAttributes {
1545        self.raw
1546            .search_attributes
1547            .as_ref()
1548            .map(SearchAttributes::from_proto)
1549            .unwrap_or_default()
1550    }
1551
1552    /// Access the raw proto for additional fields not exposed via accessors.
1553    pub fn raw(&self) -> &workflow::WorkflowExecutionInfo {
1554        &self.raw
1555    }
1556
1557    /// Consume the wrapper and return the raw proto.
1558    pub fn into_raw(self) -> workflow::WorkflowExecutionInfo {
1559        self.raw
1560    }
1561}
1562
1563/// A stream of workflow executions from a list query.
1564/// Internally paginates through results from the server.
1565pub struct ListWorkflowsStream {
1566    inner: Pin<Box<dyn Stream<Item = Result<WorkflowExecution, ClientError>> + Send>>,
1567}
1568
1569impl ListWorkflowsStream {
1570    fn new(
1571        inner: Pin<Box<dyn Stream<Item = Result<WorkflowExecution, ClientError>> + Send>>,
1572    ) -> Self {
1573        Self { inner }
1574    }
1575}
1576
1577impl Stream for ListWorkflowsStream {
1578    type Item = Result<WorkflowExecution, ClientError>;
1579
1580    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1581        self.inner.as_mut().poll_next(cx)
1582    }
1583}
1584
1585/// Result of a workflow count operation.
1586///
1587/// If the query includes a group-by clause, `groups` will contain the aggregated
1588/// counts and `count` will be the sum of all group counts.
1589#[derive(Debug, Clone)]
1590pub struct WorkflowExecutionCount {
1591    count: usize,
1592    groups: Vec<WorkflowCountAggregationGroup>,
1593}
1594
1595impl WorkflowExecutionCount {
1596    pub(crate) fn from_response(resp: CountWorkflowExecutionsResponse) -> Self {
1597        Self {
1598            count: resp.count as usize,
1599            groups: resp
1600                .groups
1601                .into_iter()
1602                .map(WorkflowCountAggregationGroup::from_proto)
1603                .collect(),
1604        }
1605    }
1606
1607    /// The approximate number of workflows matching the query.
1608    /// If grouping was applied, this is the sum of all group counts.
1609    pub fn count(&self) -> usize {
1610        self.count
1611    }
1612
1613    /// The groups if the query had a group-by clause, or empty if not.
1614    pub fn groups(&self) -> &[WorkflowCountAggregationGroup] {
1615        &self.groups
1616    }
1617}
1618
1619/// Aggregation group from a workflow count query with a group-by clause.
1620#[derive(Debug, Clone)]
1621pub struct WorkflowCountAggregationGroup {
1622    raw: count_workflow_executions_response::AggregationGroup,
1623}
1624
1625impl WorkflowCountAggregationGroup {
1626    fn from_proto(proto: count_workflow_executions_response::AggregationGroup) -> Self {
1627        Self { raw: proto }
1628    }
1629
1630    /// Retrieve a typed group value at `index`.
1631    ///
1632    ///  Returns `None` if the index is out of bounds or deserialization fails.
1633    ///  Use [`Self::try_get`] for explicit error handling.
1634    pub fn get<T: SearchAttributeValue>(&self, index: usize) -> Option<T> {
1635        self.try_get(index).ok().flatten()
1636    }
1637
1638    /// Retrieve a typed group value at `index`, preserving deserialization
1639    /// errors.
1640    ///
1641    /// Returns `Ok(None)` if the index is out of bounds and `Err` if the
1642    /// payload cannot be deserialized.
1643    pub fn try_get<T: SearchAttributeValue>(
1644        &self,
1645        index: usize,
1646    ) -> Result<Option<T>, SearchAttributeError> {
1647        match self.raw.group_values.get(index) {
1648            Some(payload) => T::from_search_attribute_payload(payload).map(Some),
1649            None => Ok(None),
1650        }
1651    }
1652
1653    /// The approximate number of workflows matching for this group.
1654    pub fn count(&self) -> usize {
1655        self.raw.count as usize
1656    }
1657}
1658
1659impl<T> WorkflowClientTrait for T
1660where
1661    T: WorkflowService + NamespacedClient + Clone + Send + Sync + 'static,
1662{
1663    async fn start_workflow<W>(
1664        &self,
1665        workflow: W,
1666        input: W::Input,
1667        options: WorkflowStartOptions,
1668    ) -> Result<WorkflowHandle<Self, W>, WorkflowStartError>
1669    where
1670        W: HasWorkflowDefinition,
1671        W::Input: Send,
1672    {
1673        let namespace = self.namespace();
1674        let interceptor_output = interceptors::call_start_workflow(
1675            self.client_interceptors(),
1676            StartWorkflowInput::new(workflow.name().to_owned(), input, options),
1677            Next::new({
1678                let client = (*self).clone();
1679                move |input: StartWorkflowInput| -> BoxFuture<
1680                    '_,
1681                    Result<StartWorkflowOutput, WorkflowStartError>,
1682                > {
1683                    let mut client = client;
1684                    Box::pin(async move {
1685                        let (workflow_type, args, options, rpc_options) = input.into_parts();
1686                        let data_converter = client.data_converter().clone();
1687                        let unencoded_payloads = {
1688                            let payload_converter = data_converter.payload_converter();
1689                            let context = SerializationContext {
1690                                data: &SerializationContextData::Workflow,
1691                                converter: payload_converter,
1692                            };
1693                            args.serialize_payloads(&context)
1694                        };
1695                        drop(args);
1696
1697                        let payloads = data_converter
1698                            .codec()
1699                            .encode(&SerializationContextData::Workflow, unencoded_payloads?)
1700                            .await?;
1701                        let namespace = client.namespace();
1702                        let workflow_id = options.workflow_id.clone();
1703                        let task_queue_name = options.task_queue.clone();
1704
1705                        let user_metadata = if options.static_summary.is_some()
1706                            || options.static_details.is_some()
1707                        {
1708                            let payload_converter = PayloadConverter::default();
1709                            let context = SerializationContext {
1710                                data: &SerializationContextData::Workflow,
1711                                converter: &payload_converter,
1712                            };
1713                            Some(UserMetadata {
1714                                summary: options.static_summary.map(|summary| {
1715                                    payload_converter.to_payload(&context, &summary).expect(
1716                                        "String-to-JSON payload serialization is infallible",
1717                                    )
1718                                }),
1719                                details: options.static_details.map(|details| {
1720                                    payload_converter.to_payload(&context, &details).expect(
1721                                        "String-to-JSON payload serialization is infallible",
1722                                    )
1723                                }),
1724                            })
1725                        } else {
1726                            None
1727                        };
1728
1729                        let run_id = if let Some(start_signal) = options.start_signal {
1730                            let mut request = SignalWithStartWorkflowExecutionRequest {
1731                                namespace,
1732                                workflow_id: workflow_id.clone(),
1733                                workflow_type: Some(WorkflowType {
1734                                    name: workflow_type,
1735                                }),
1736                                task_queue: Some(TaskQueue {
1737                                    name: task_queue_name,
1738                                    kind: TaskQueueKind::Normal as i32,
1739                                    normal_name: String::new(),
1740                                }),
1741                                input: payloads.into_payloads(),
1742                                signal_name: start_signal.signal_name,
1743                                signal_input: start_signal.input,
1744                                identity: client.identity(),
1745                                request_id: Uuid::new_v4().to_string(),
1746                                workflow_id_reuse_policy: options.id_reuse_policy as i32,
1747                                workflow_id_conflict_policy: options.id_conflict_policy as i32,
1748                                workflow_execution_timeout: options
1749                                    .execution_timeout
1750                                    .and_then(|duration| duration.try_into().ok()),
1751                                workflow_run_timeout: options
1752                                    .run_timeout
1753                                    .and_then(|duration| duration.try_into().ok()),
1754                                workflow_task_timeout: options
1755                                    .task_timeout
1756                                    .and_then(|duration| duration.try_into().ok()),
1757                                search_attributes: options
1758                                    .search_attributes
1759                                    .map(|attributes| attributes.into_proto()),
1760                                cron_schedule: options.cron_schedule.unwrap_or_default(),
1761                                retry_policy: options.retry_policy.map(Into::into),
1762                                header: options.header.or(start_signal.header),
1763                                user_metadata,
1764                                ..Default::default()
1765                            }
1766                            .into_request();
1767                            rpc_options.apply_to(&mut request);
1768                            WorkflowService::signal_with_start_workflow_execution(
1769                                &mut client,
1770                                request,
1771                            )
1772                            .await?
1773                            .into_inner()
1774                            .run_id
1775                        } else {
1776                            let mut request = StartWorkflowExecutionRequest {
1777                                namespace,
1778                                input: payloads.into_payloads(),
1779                                workflow_id: workflow_id.clone(),
1780                                workflow_type: Some(WorkflowType {
1781                                    name: workflow_type,
1782                                }),
1783                                task_queue: Some(TaskQueue {
1784                                    name: task_queue_name,
1785                                    kind: TaskQueueKind::Unspecified as i32,
1786                                    normal_name: String::new(),
1787                                }),
1788                                request_id: Uuid::new_v4().to_string(),
1789                                workflow_id_reuse_policy: options.id_reuse_policy as i32,
1790                                workflow_id_conflict_policy: options.id_conflict_policy as i32,
1791                                workflow_execution_timeout: options
1792                                    .execution_timeout
1793                                    .and_then(|duration| duration.try_into().ok()),
1794                                workflow_run_timeout: options
1795                                    .run_timeout
1796                                    .and_then(|duration| duration.try_into().ok()),
1797                                workflow_task_timeout: options
1798                                    .task_timeout
1799                                    .and_then(|duration| duration.try_into().ok()),
1800                                search_attributes: options
1801                                    .search_attributes
1802                                    .map(|attributes| attributes.into_proto()),
1803                                cron_schedule: options.cron_schedule.unwrap_or_default(),
1804                                request_eager_execution: options.enable_eager_workflow_start,
1805                                retry_policy: options.retry_policy.map(Into::into),
1806                                links: options.links,
1807                                completion_callbacks: options.completion_callbacks,
1808                                priority: Some(options.priority.into()),
1809                                header: options.header,
1810                                user_metadata,
1811                                ..Default::default()
1812                            }
1813                            .into_request();
1814                            rpc_options.apply_to(&mut request);
1815                            client
1816                                .start_workflow_execution(request)
1817                                .await
1818                                .map_err(|status| {
1819                                    if status.code() == Code::AlreadyExists {
1820                                        let run_id = decode_status_detail::<
1821                                            WorkflowExecutionAlreadyStartedFailure,
1822                                        >(
1823                                            status.details()
1824                                        )
1825                                        .map(|failure| failure.run_id);
1826                                        WorkflowStartError::AlreadyStarted {
1827                                            run_id,
1828                                            source: status,
1829                                        }
1830                                    } else {
1831                                        WorkflowStartError::Rpc(status)
1832                                    }
1833                                })?
1834                                .into_inner()
1835                                .run_id
1836                        };
1837
1838                        Ok(StartWorkflowOutput::new(workflow_id, run_id))
1839                    })
1840                }
1841            }),
1842        )
1843        .await?;
1844        let StartWorkflowOutput {
1845            workflow_id,
1846            run_id,
1847        } = interceptor_output;
1848
1849        Ok(WorkflowHandle::new(
1850            self.clone(),
1851            WorkflowExecutionInfo {
1852                namespace,
1853                workflow_id,
1854                run_id: Some(run_id.clone()),
1855                first_execution_run_id: Some(run_id),
1856            },
1857        ))
1858    }
1859
1860    fn get_workflow_handle<W: HasWorkflowDefinition>(
1861        &self,
1862        workflow_id: impl Into<String>,
1863    ) -> WorkflowHandle<Self, W>
1864    where
1865        Self: Sized,
1866    {
1867        WorkflowHandle::new(
1868            self.clone(),
1869            WorkflowExecutionInfo {
1870                namespace: self.namespace(),
1871                workflow_id: workflow_id.into(),
1872                run_id: None,
1873                first_execution_run_id: None,
1874            },
1875        )
1876    }
1877
1878    fn list_workflows(
1879        &self,
1880        query: impl Into<String>,
1881        opts: WorkflowListOptions,
1882    ) -> ListWorkflowsStream {
1883        let client = self.clone();
1884        let namespace = self.namespace();
1885        let query = query.into();
1886        let limit = opts.limit;
1887        let rpc_options = opts.rpc_options;
1888
1889        // State: (next_page_token, buffer, yielded_count, exhausted)
1890        let initial_state = (Vec::new(), VecDeque::new(), 0, false);
1891
1892        let stream = stream::unfold(
1893            initial_state,
1894            move |(next_page_token, mut buffer, mut yielded, exhausted)| {
1895                let client = client.clone();
1896                let namespace = namespace.clone();
1897                let query = query.clone();
1898                let rpc_options = rpc_options.clone();
1899
1900                async move {
1901                    if let Some(l) = limit
1902                        && yielded >= l
1903                    {
1904                        return None;
1905                    }
1906
1907                    if let Some(exec) = buffer.pop_front() {
1908                        yielded += 1;
1909                        return Some((Ok(exec), (next_page_token, buffer, yielded, exhausted)));
1910                    }
1911
1912                    if exhausted {
1913                        return None;
1914                    }
1915
1916                    let response = interceptors::call_list_workflows_page(
1917                        client.client_interceptors(),
1918                        ListWorkflowsPageInput {
1919                            query,
1920                            next_page_token: next_page_token.clone(),
1921                            rpc_options,
1922                        },
1923                        Next::new({
1924                            let mut rpc_client = client.clone();
1925                            move |input: ListWorkflowsPageInput| -> BoxFuture<
1926                                '_,
1927                                Result<ListWorkflowsPageOutput, ClientError>,
1928                            > {
1929                                Box::pin(async move {
1930                                    let mut request = ListWorkflowExecutionsRequest {
1931                                        namespace,
1932                                        page_size: 0,
1933                                        next_page_token: input.next_page_token,
1934                                        query: input.query,
1935                                    }
1936                                    .into_request();
1937                                    input.rpc_options.apply_to(&mut request);
1938                                    let response = WorkflowService::list_workflow_executions(
1939                                        &mut rpc_client,
1940                                        request,
1941                                    )
1942                                    .await?
1943                                    .into_inner();
1944                                    Ok(ListWorkflowsPageOutput::new(
1945                                        response.executions,
1946                                        response.next_page_token,
1947                                    ))
1948                                })
1949                            }
1950                        }),
1951                    )
1952                    .await;
1953
1954                    match response {
1955                        Ok(mut output) => {
1956                            let new_exhausted = output.next_page_token.is_empty();
1957                            let new_token = output.next_page_token;
1958
1959                            let data_converter = client.data_converter().clone();
1960                            for execution in &mut output.executions {
1961                                if let Some(memo) = execution.memo.as_mut()
1962                                    && let Err(err) = decode_payloads(
1963                                        memo,
1964                                        data_converter.codec(),
1965                                        &SerializationContextData::Workflow,
1966                                    )
1967                                    .await
1968                                {
1969                                    return Some((
1970                                        Err(ClientError::from(err)),
1971                                        (new_token, buffer, yielded, true),
1972                                    ));
1973                                }
1974                            }
1975                            buffer = output
1976                                .executions
1977                                .into_iter()
1978                                .map(|raw| {
1979                                    WorkflowExecution::new_with_data_converter(
1980                                        raw,
1981                                        data_converter.clone(),
1982                                    )
1983                                })
1984                                .collect();
1985
1986                            if let Some(exec) = buffer.pop_front() {
1987                                yielded += 1;
1988                                Some((Ok(exec), (new_token, buffer, yielded, new_exhausted)))
1989                            } else {
1990                                None
1991                            }
1992                        }
1993                        Err(e) => Some((Err(e), (next_page_token, buffer, yielded, true))),
1994                    }
1995                }
1996            },
1997        );
1998
1999        ListWorkflowsStream::new(Box::pin(stream))
2000    }
2001
2002    async fn count_workflows(
2003        &self,
2004        query: impl Into<String>,
2005        opts: WorkflowCountOptions,
2006    ) -> Result<WorkflowExecutionCount, ClientError> {
2007        let output = interceptors::call_count_workflows(
2008            self.client_interceptors(),
2009            CountWorkflowsInput {
2010                query: query.into(),
2011                options: opts,
2012            },
2013            Next::new({
2014                let mut client = (*self).clone();
2015                move |input: CountWorkflowsInput| -> BoxFuture<
2016                    '_,
2017                    Result<CountWorkflowsOutput, ClientError>,
2018                > {
2019                    Box::pin(async move {
2020                        let mut request = CountWorkflowExecutionsRequest {
2021                            namespace: client.namespace(),
2022                            query: input.query,
2023                        }
2024                        .into_request();
2025                        input.options.rpc_options.apply_to(&mut request);
2026                        let response = WorkflowService::count_workflow_executions(
2027                            &mut client,
2028                            request,
2029                        )
2030                        .await?
2031                        .into_inner();
2032                        Ok(CountWorkflowsOutput::new(response))
2033                    })
2034                }
2035            }),
2036        )
2037        .await?;
2038
2039        Ok(WorkflowExecutionCount::from_response(output.response))
2040    }
2041
2042    fn get_async_activity_handle(&self, identifier: ActivityIdentifier) -> AsyncActivityHandle<Self>
2043    where
2044        Self: Sized,
2045    {
2046        AsyncActivityHandle::new(self.clone(), identifier)
2047    }
2048
2049    async fn start_activity<A>(
2050        &self,
2051        activity: A,
2052        input: A::Input,
2053        options: ActivityStartOptions,
2054    ) -> Result<ActivityHandle<Self, A>, StartActivityError>
2055    where
2056        Self: Sized,
2057        A: ActivityDefinition,
2058    {
2059        let mut client = self.clone();
2060        let dc = client.data_converter();
2061        let sc = &SerializationContextData::Activity;
2062
2063        let user_metadata = {
2064            let summary = match &options.summary {
2065                Some(summary) => Some(dc.to_payload(sc, summary).await?),
2066                None => None,
2067            };
2068            let details = match &options.static_details {
2069                Some(details) => Some(dc.to_payload(sc, details).await?),
2070                None => None,
2071            };
2072            (summary.is_some() || details.is_some()).then_some(UserMetadata { summary, details })
2073        };
2074
2075        let resp = client
2076            .start_activity_execution(
2077                StartActivityExecutionRequest {
2078                    namespace: client.namespace(),
2079                    identity: client.identity(),
2080                    request_id: Uuid::new_v4().to_string(),
2081                    activity_id: options.id.clone(),
2082                    activity_type: Some(ActivityType {
2083                        name: activity.name().to_string(),
2084                    }),
2085                    task_queue: Some(TaskQueue {
2086                        name: options.task_queue,
2087                        kind: TaskQueueKind::Normal.into(),
2088                        normal_name: "".to_string(),
2089                    }),
2090                    schedule_to_close_timeout: try_into_or_box_err(
2091                        options.close_timeouts.schedule_to_close(),
2092                        StartActivityError::Other,
2093                    )?,
2094                    schedule_to_start_timeout: try_into_or_box_err(
2095                        options.schedule_to_start_timeout,
2096                        StartActivityError::Other,
2097                    )?,
2098                    start_to_close_timeout: try_into_or_box_err(
2099                        options.close_timeouts.start_to_close(),
2100                        StartActivityError::Other,
2101                    )?,
2102                    heartbeat_timeout: try_into_or_box_err(
2103                        options.heartbeat_timeout,
2104                        StartActivityError::Other,
2105                    )?,
2106                    retry_policy: options.retry_policy.map(Into::into),
2107                    input: dc.to_payloads(sc, &input).await?.into_payloads(),
2108                    id_reuse_policy: ProtoActivityIdReusePolicy::from(options.id_reuse_policy)
2109                        .into(),
2110                    id_conflict_policy: ProtoActivityIdConflictPolicy::from(
2111                        options.id_conflict_policy,
2112                    )
2113                    .into(),
2114                    search_attributes: options.search_attributes.map(SearchAttributes::into_proto),
2115                    header: options.header,
2116                    user_metadata,
2117                    priority: Some(options.priority.into()),
2118                    start_delay: try_into_or_box_err(
2119                        options.start_delay,
2120                        StartActivityError::Other,
2121                    )?,
2122                    ..Default::default()
2123                }
2124                .into_request(),
2125            )
2126            .await?
2127            .into_inner();
2128
2129        Ok(ActivityHandle::new(
2130            client,
2131            options.id,
2132            (!resp.run_id.is_empty()).then_some(resp.run_id),
2133        ))
2134    }
2135
2136    fn get_activity_handle<A>(
2137        &self,
2138        _activity: A,
2139        id: impl Into<String>,
2140        run_id: Option<String>,
2141    ) -> ActivityHandle<Self, A>
2142    where
2143        Self: Sized,
2144        A: ActivityDefinition,
2145    {
2146        ActivityHandle::new(self.clone(), id.into(), run_id)
2147    }
2148
2149    fn get_untyped_activity_handle(
2150        &self,
2151        id: impl Into<String>,
2152        run_id: Option<String>,
2153    ) -> ActivityHandle<Self, UntypedActivity>
2154    where
2155        Self: Sized,
2156    {
2157        ActivityHandle::new(self.clone(), id.into(), run_id)
2158    }
2159
2160    fn list_activities(
2161        &self,
2162        query: impl Into<String>,
2163        _options: ActivityListOptions,
2164    ) -> ListActivitiesStream {
2165        let client = self.clone();
2166        let namespace = client.namespace();
2167        let query = query.into();
2168
2169        ListActivitiesStream::new(stream::unfold(
2170            Some(vec![]), // empty token for initial query, None if done
2171            move |next_page_token| {
2172                let mut client = client.clone();
2173                let namespace = namespace.clone();
2174                let query = query.clone();
2175
2176                async move {
2177                    // making it more visible that we're terminating stream here
2178                    #[allow(clippy::question_mark)]
2179                    let Some(token): Option<Vec<u8>> = next_page_token else {
2180                        return None;
2181                    };
2182
2183                    match WorkflowService::list_activity_executions(
2184                        &mut client,
2185                        ListActivityExecutionsRequest {
2186                            namespace,
2187                            page_size: 0, // Use server default
2188                            next_page_token: token.clone(),
2189                            query,
2190                        }
2191                        .into_request(),
2192                    )
2193                    .await
2194                    .map(|r| r.into_inner())
2195                    {
2196                        Ok(resp) => Some((
2197                            Ok(resp.executions),
2198                            (!resp.next_page_token.is_empty()).then_some(resp.next_page_token),
2199                        )),
2200                        Err(e) => Some((Err(e.into()), Some(token))),
2201                    }
2202                }
2203            },
2204        ))
2205    }
2206
2207    async fn count_activities(
2208        &self,
2209        query: impl Into<String>,
2210        _options: ActivityCountOptions,
2211    ) -> Result<ActivityExecutionCount, ClientError> {
2212        let mut client = self.clone();
2213        let resp = client
2214            .count_activity_executions(
2215                CountActivityExecutionsRequest {
2216                    namespace: client.namespace(),
2217                    query: query.into(),
2218                }
2219                .into_request(),
2220            )
2221            .await?
2222            .into_inner();
2223        Ok(ActivityExecutionCount::from_response(resp))
2224    }
2225}
2226
2227macro_rules! dbg_panic {
2228  ($($arg:tt)*) => {
2229      use tracing::error;
2230      error!($($arg)*);
2231      debug_assert!(false, $($arg)*);
2232  };
2233}
2234pub(crate) use dbg_panic;
2235
2236fn try_into_or_box_err<A, B, E, MapErr>(val: Option<A>, map_err: MapErr) -> Result<Option<B>, E>
2237where
2238    A: TryInto<B>,
2239    <A as TryInto<B>>::Error: Error + Send + Sync + 'static,
2240    MapErr: FnOnce(Box<dyn Error + Send + Sync + 'static>) -> E,
2241{
2242    val.map(TryInto::try_into)
2243        .transpose()
2244        .map_err(|e| map_err(Box::from(e)))
2245}
2246
2247#[cfg(test)]
2248mod tests {
2249    use super::*;
2250    use crate::callback_based::CallbackBasedGrpcService;
2251    use std::{
2252        sync::atomic::{AtomicUsize, Ordering},
2253        time::Instant,
2254    };
2255    use temporalio_common::search_attributes::SearchAttributeKey;
2256    use tonic::{Status, metadata::Ascii};
2257    use url::Url;
2258
2259    #[test]
2260    fn count_aggregation_group_gets_typed_value() {
2261        let attrs = SearchAttributes::new([SearchAttributeKey::int("group").value_set(42)]);
2262        let group = WorkflowCountAggregationGroup {
2263            raw: count_workflow_executions_response::AggregationGroup {
2264                group_values: vec![attrs.raw_payload("group").unwrap().clone()],
2265                count: 1,
2266            },
2267        };
2268
2269        assert_eq!(group.get::<i64>(0), Some(42));
2270        assert_eq!(group.get::<i64>(1), None);
2271        assert!(group.try_get::<String>(0).is_err());
2272        assert_eq!(group.try_get::<i64>(1).unwrap(), None);
2273    }
2274
2275    fn connection_options_for_system_info_test(
2276        service_override: CallbackBasedGrpcService,
2277    ) -> ConnectionOptions {
2278        ConnectionOptions::new(Url::parse("http://localhost:7233").unwrap())
2279            .service_override(service_override)
2280            .dns_load_balancing(None)
2281            .build()
2282    }
2283
2284    #[test]
2285    fn applies_headers() {
2286        // Initial header set
2287        let headers = Arc::new(RwLock::new(ClientHeaders {
2288            user_headers: HashMap::new(),
2289            user_binary_headers: HashMap::new(),
2290            api_key: Some("my-api-key".to_owned()),
2291        }));
2292        headers.clone().write().user_headers.insert(
2293            "my-meta-key".parse().unwrap(),
2294            "my-meta-val".parse().unwrap(),
2295        );
2296        headers.clone().write().user_binary_headers.insert(
2297            "my-bin-meta-key-bin".parse().unwrap(),
2298            vec![1, 2, 3].try_into().unwrap(),
2299        );
2300        let mut interceptor = ServiceCallInterceptor {
2301            client_name: "cute-kitty".to_string(),
2302            client_version: "0.1.0".to_string(),
2303            headers: headers.clone(),
2304        };
2305
2306        // Confirm on metadata
2307        let req = interceptor.call(tonic::Request::new(())).unwrap();
2308        assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val");
2309        assert_eq!(
2310            req.metadata().get("authorization").unwrap(),
2311            "Bearer my-api-key"
2312        );
2313        assert_eq!(
2314            req.metadata().get_bin("my-bin-meta-key-bin").unwrap(),
2315            vec![1, 2, 3].as_slice()
2316        );
2317
2318        // Overwrite at request time
2319        let mut req = tonic::Request::new(());
2320        req.metadata_mut()
2321            .insert("my-meta-key", "my-meta-val2".parse().unwrap());
2322        req.metadata_mut()
2323            .insert("authorization", "my-api-key2".parse().unwrap());
2324        req.metadata_mut()
2325            .insert_bin("my-bin-meta-key-bin", vec![4, 5, 6].try_into().unwrap());
2326        let req = interceptor.call(req).unwrap();
2327        assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val2");
2328        assert_eq!(req.metadata().get("authorization").unwrap(), "my-api-key2");
2329        assert_eq!(
2330            req.metadata().get_bin("my-bin-meta-key-bin").unwrap(),
2331            vec![4, 5, 6].as_slice()
2332        );
2333
2334        // Overwrite auth on header
2335        headers.clone().write().user_headers.insert(
2336            "authorization".parse().unwrap(),
2337            "my-api-key3".parse().unwrap(),
2338        );
2339        let req = interceptor.call(tonic::Request::new(())).unwrap();
2340        assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val");
2341        assert_eq!(req.metadata().get("authorization").unwrap(), "my-api-key3");
2342
2343        // Remove headers and auth and confirm gone
2344        headers.clone().write().user_headers.clear();
2345        headers.clone().write().user_binary_headers.clear();
2346        headers.clone().write().api_key.take();
2347        let req = interceptor.call(tonic::Request::new(())).unwrap();
2348        assert!(!req.metadata().contains_key("my-meta-key"));
2349        assert!(!req.metadata().contains_key("authorization"));
2350        assert!(!req.metadata().contains_key("my-bin-meta-key-bin"));
2351
2352        // Timeout header not overriden
2353        let mut req = tonic::Request::new(());
2354        req.metadata_mut()
2355            .insert("grpc-timeout", "1S".parse().unwrap());
2356        let req = interceptor.call(req).unwrap();
2357        assert_eq!(
2358            req.metadata().get("grpc-timeout").unwrap(),
2359            "1S".parse::<MetadataValue<Ascii>>().unwrap()
2360        );
2361    }
2362
2363    #[test]
2364    fn invalid_ascii_header_key() {
2365        let invalid_headers = {
2366            let mut h = HashMap::new();
2367            h.insert("x-binary-key-bin".to_owned(), "value".to_owned());
2368            h
2369        };
2370
2371        let result = parse_ascii_headers(invalid_headers);
2372        assert!(result.is_err());
2373        assert_eq!(
2374            result.err().unwrap().to_string(),
2375            "Invalid ASCII header key 'x-binary-key-bin': invalid gRPC metadata key name"
2376        );
2377    }
2378
2379    #[test]
2380    fn invalid_ascii_header_value() {
2381        let invalid_headers = {
2382            let mut h = HashMap::new();
2383            // Nul bytes are valid UTF-8, but not valid ascii gRPC headers:
2384            h.insert("x-ascii-key".to_owned(), "\x00value".to_owned());
2385            h
2386        };
2387
2388        let result = parse_ascii_headers(invalid_headers);
2389        assert!(result.is_err());
2390        assert_eq!(
2391            result.err().unwrap().to_string(),
2392            "Invalid ASCII header value for key 'x-ascii-key': failed to parse metadata value"
2393        );
2394    }
2395
2396    #[test]
2397    fn invalid_binary_header_key() {
2398        let invalid_headers = {
2399            let mut h = HashMap::new();
2400            h.insert("x-ascii-key".to_owned(), vec![1, 2, 3]);
2401            h
2402        };
2403
2404        let result = parse_binary_headers(invalid_headers);
2405        assert!(result.is_err());
2406        assert_eq!(
2407            result.err().unwrap().to_string(),
2408            "Invalid binary header key 'x-ascii-key': invalid gRPC metadata key name"
2409        );
2410    }
2411
2412    #[test]
2413    fn keep_alive_defaults() {
2414        let opts = ConnectionOptions::new(Url::parse("https://smolkitty").unwrap())
2415            .identity("enchicat".to_string())
2416            .client_name("cute-kitty".to_string())
2417            .client_version("0.1.0".to_string())
2418            .build();
2419        assert_eq!(
2420            opts.keep_alive.clone().unwrap().interval,
2421            ClientKeepAliveOptions::default().interval
2422        );
2423        assert_eq!(
2424            opts.keep_alive.clone().unwrap().timeout,
2425            ClientKeepAliveOptions::default().timeout
2426        );
2427
2428        // Can be explicitly set to None
2429        let opts = ConnectionOptions::new(Url::parse("https://smolkitty").unwrap())
2430            .identity("enchicat".to_string())
2431            .client_name("cute-kitty".to_string())
2432            .client_version("0.1.0".to_string())
2433            .keep_alive(None)
2434            .build();
2435        dbg!(&opts.keep_alive);
2436        assert!(opts.keep_alive.is_none());
2437    }
2438
2439    #[rstest::rstest]
2440    #[case(
2441        "unknown method GetSystemInfo for service temporal.api.workflowservice.v1.WorkflowService"
2442    )]
2443    #[case("Method temporal.api.workflowservice.v1.WorkflowService/GetSystemInfo is unimplemented")]
2444    #[case(
2445        "The server does not implement the method /temporal.api.workflowservice.v1.WorkflowService/GetSystemInfo"
2446    )]
2447    #[tokio::test]
2448    async fn get_system_info_missing_method_falls_back_to_empty_capabilities(
2449        #[case] message: &'static str,
2450    ) {
2451        let attempts = Arc::new(AtomicUsize::new(0));
2452        let attempts_clone = attempts.clone();
2453        let service_override = CallbackBasedGrpcService {
2454            callback: Arc::new(move |req| {
2455                let attempts = attempts_clone.clone();
2456                Box::pin(async move {
2457                    assert_eq!(req.rpc, "GetSystemInfo");
2458                    attempts.fetch_add(1, Ordering::SeqCst);
2459                    Err(Status::unimplemented(message))
2460                })
2461            }),
2462        };
2463
2464        let connection =
2465            Connection::connect(connection_options_for_system_info_test(service_override))
2466                .await
2467                .unwrap();
2468
2469        assert!(connection.capabilities().is_none());
2470        assert_eq!(attempts.load(Ordering::SeqCst), 1);
2471    }
2472
2473    #[tokio::test]
2474    async fn get_system_info_non_missing_unimplemented_fails_connect() {
2475        let attempts = Arc::new(AtomicUsize::new(0));
2476        let attempts_clone = attempts.clone();
2477        let service_override = CallbackBasedGrpcService {
2478            callback: Arc::new(move |req| {
2479                let attempts = attempts_clone.clone();
2480                Box::pin(async move {
2481                    assert_eq!(req.rpc, "GetSystemInfo");
2482                    attempts.fetch_add(1, Ordering::SeqCst);
2483                    Err(Status::unimplemented("backend temporarily unimplemented"))
2484                })
2485            }),
2486        };
2487
2488        let err =
2489            match Connection::connect(connection_options_for_system_info_test(service_override))
2490                .await
2491            {
2492                Ok(_) => panic!("connection should fail"),
2493                Err(err) => err,
2494            };
2495
2496        assert!(matches!(
2497            err,
2498            ClientConnectError::SystemInfoCallError(status)
2499                if status.code() == Code::Unimplemented
2500                    && status.message() == "backend temporarily unimplemented"
2501        ));
2502        assert_eq!(attempts.load(Ordering::SeqCst), 1);
2503    }
2504
2505    #[tokio::test]
2506    async fn connect_timeout_bounds_connection_attempt() {
2507        let url = Url::parse("http://10.255.255.1:7233").unwrap();
2508        let opts = ConnectionOptions::new(url)
2509            .connect_timeout(Duration::from_millis(500))
2510            .build();
2511        let start = Instant::now();
2512        let result = Connection::connect(opts).await;
2513        assert!(result.is_err(), "connection should fail");
2514        assert!(start.elapsed() < Duration::from_secs(2));
2515    }
2516
2517    mod tls_custom_verifier_tests {
2518        use super::*;
2519        use tokio_rustls::rustls::{
2520            DigitallySignedStruct, Error as RustlsError, SignatureScheme,
2521            client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
2522            pki_types::{CertificateDer, ServerName, UnixTime},
2523        };
2524
2525        /// A minimal mock verifier for testing. In production, users would
2526        /// implement real certificate pinning or custom validation here.
2527        #[derive(Debug)]
2528        struct MockVerifier;
2529
2530        impl ServerCertVerifier for MockVerifier {
2531            fn verify_server_cert(
2532                &self,
2533                _end_entity: &CertificateDer<'_>,
2534                _intermediates: &[CertificateDer<'_>],
2535                _server_name: &ServerName<'_>,
2536                _ocsp_response: &[u8],
2537                _now: UnixTime,
2538            ) -> Result<ServerCertVerified, RustlsError> {
2539                Ok(ServerCertVerified::assertion())
2540            }
2541
2542            fn verify_tls12_signature(
2543                &self,
2544                _message: &[u8],
2545                _cert: &CertificateDer<'_>,
2546                _dss: &DigitallySignedStruct,
2547            ) -> Result<HandshakeSignatureValid, RustlsError> {
2548                Ok(HandshakeSignatureValid::assertion())
2549            }
2550
2551            fn verify_tls13_signature(
2552                &self,
2553                _message: &[u8],
2554                _cert: &CertificateDer<'_>,
2555                _dss: &DigitallySignedStruct,
2556            ) -> Result<HandshakeSignatureValid, RustlsError> {
2557                Ok(HandshakeSignatureValid::assertion())
2558            }
2559
2560            fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
2561                vec![
2562                    SignatureScheme::ECDSA_NISTP256_SHA256,
2563                    SignatureScheme::RSA_PSS_SHA256,
2564                ]
2565            }
2566        }
2567
2568        #[tokio::test]
2569        async fn add_tls_to_channel_with_custom_verifier() {
2570            let tls_opts = TlsOptions::builder()
2571                .server_cert_verifier(Arc::new(MockVerifier))
2572                .domain("test.temporal.io".to_string())
2573                .build();
2574            let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2575            let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2576            assert!(
2577                matches!(&result, Ok(TlsConfigResult::Standard(_))),
2578                "add_tls_to_channel should succeed with a custom verifier: {:?}",
2579                result.err()
2580            );
2581        }
2582
2583        #[tokio::test]
2584        async fn add_tls_to_channel_with_verifier_and_ca_cert_fails() {
2585            // When both server_cert_verifier and server_root_ca_cert are set,
2586            // add_tls_to_channel should fail with InvalidConfig.
2587            let tls_opts = TlsOptions::builder()
2588                .server_root_ca_cert(b"some-ca-cert-bytes".to_vec())
2589                .server_cert_verifier(Arc::new(MockVerifier))
2590                .domain("test.temporal.io".to_string())
2591                .build();
2592            let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2593            let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2594            assert!(
2595                matches!(result, Err(ClientConnectError::InvalidConfig(_))),
2596                "add_tls_to_channel should fail with InvalidConfig when both CA cert and verifier are set: {:?}",
2597                result
2598            );
2599        }
2600
2601        #[tokio::test]
2602        async fn add_tls_to_channel_without_verifier_still_works() {
2603            // Regression test: the original PEM path must still work.
2604            let tls_opts = TlsOptions::builder()
2605                .domain("test.temporal.io".to_string())
2606                .build();
2607            let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2608            let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2609            assert!(
2610                matches!(&result, Ok(TlsConfigResult::Standard(_))),
2611                "add_tls_to_channel should succeed without a verifier (native roots): {:?}",
2612                result.err()
2613            );
2614        }
2615
2616        // --- Dynamic client cert resolver tests ---
2617
2618        #[cfg(feature = "dynamic-tls")]
2619        mod dynamic_cert_tests {
2620            use super::*;
2621
2622            /// A mock `ResolvesClientCert` that always returns None (no client cert).
2623            /// Used to test the plumbing without requiring real certificates.
2624            #[derive(Debug)]
2625            struct MockClientCertResolver;
2626
2627            impl tokio_rustls::rustls::client::ResolvesClientCert for MockClientCertResolver {
2628                fn resolve(
2629                    &self,
2630                    _acceptable_issuers: &[&[u8]],
2631                    _sigschemes: &[tokio_rustls::rustls::SignatureScheme],
2632                ) -> Option<Arc<tokio_rustls::rustls::sign::CertifiedKey>> {
2633                    None // No client cert available — server may reject, but plumbing works
2634                }
2635
2636                fn has_certs(&self) -> bool {
2637                    false
2638                }
2639            }
2640
2641            #[tokio::test]
2642            async fn add_tls_with_client_cert_resolver_returns_custom_connector() {
2643                let resolver = Arc::new(MockClientCertResolver);
2644                let tls_opts = TlsOptions {
2645                    client_cert_resolver: Some(resolver),
2646                    domain: Some("test.temporal.io".to_string()),
2647                    ..Default::default()
2648                };
2649                let endpoint =
2650                    tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2651                let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2652                match result {
2653                    Ok(TlsConfigResult::CustomConnector {
2654                        domain,
2655                        rustls_config,
2656                        ..
2657                    }) => {
2658                        assert_eq!(domain, "test.temporal.io");
2659                        // Verify ALPN is set to h2
2660                        assert_eq!(rustls_config.alpn_protocols, vec![b"h2".to_vec()]);
2661                    }
2662                    other => panic!(
2663                        "Expected TlsConfigResult::CustomConnector, got {:?}",
2664                        other.err()
2665                    ),
2666                }
2667            }
2668
2669            #[tokio::test]
2670            async fn add_tls_with_client_cert_resolver_inherits_domain_from_endpoint() {
2671                let resolver = Arc::new(MockClientCertResolver);
2672                let tls_opts = TlsOptions {
2673                    client_cert_resolver: Some(resolver),
2674                    // No explicit domain — should be derived from the endpoint URI
2675                    ..Default::default()
2676                };
2677                let endpoint =
2678                    tonic::transport::Channel::from_static("https://my-server.example.com:7233");
2679                let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2680                match result {
2681                    Ok(TlsConfigResult::CustomConnector { domain, .. }) => {
2682                        assert_eq!(domain, "my-server.example.com");
2683                    }
2684                    other => panic!(
2685                        "Expected TlsConfigResult::CustomConnector, got {:?}",
2686                        other.err()
2687                    ),
2688                }
2689            }
2690
2691            #[tokio::test]
2692            async fn add_tls_with_resolver_and_custom_verifier() {
2693                let resolver = Arc::new(MockClientCertResolver);
2694                let tls_opts = TlsOptions {
2695                    client_cert_resolver: Some(resolver),
2696                    server_cert_verifier: Some(Arc::new(MockVerifier)),
2697                    domain: Some("test.temporal.io".to_string()),
2698                    ..Default::default()
2699                };
2700                let endpoint =
2701                    tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2702                let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2703                assert!(
2704                    matches!(&result, Ok(TlsConfigResult::CustomConnector { .. })),
2705                    "Should succeed when combining cert resolver with custom server verifier: {:?}",
2706                    result.err()
2707                );
2708            }
2709
2710            #[tokio::test]
2711            async fn add_tls_with_resolver_and_custom_ca_cert() {
2712                // Use a valid PEM-formatted CA certificate
2713                let ca_pem = include_bytes!("../tests/testdata/ca.pem");
2714                let resolver = Arc::new(MockClientCertResolver);
2715                let tls_opts = TlsOptions {
2716                    client_cert_resolver: Some(resolver),
2717                    server_root_ca_cert: Some(ca_pem.to_vec()),
2718                    domain: Some("test.temporal.io".to_string()),
2719                    ..Default::default()
2720                };
2721                let endpoint =
2722                    tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2723                let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2724                assert!(
2725                    matches!(&result, Ok(TlsConfigResult::CustomConnector { .. })),
2726                    "Should succeed when combining cert resolver with custom CA cert: {:?}",
2727                    result.err()
2728                );
2729            }
2730
2731            #[tokio::test]
2732            async fn add_tls_both_static_and_dynamic_client_cert_fails() {
2733                let resolver = Arc::new(MockClientCertResolver);
2734                let tls_opts = TlsOptions {
2735                    client_tls_options: Some(ClientTlsOptions {
2736                        client_cert: b"some-cert".to_vec(),
2737                        client_private_key: b"some-key".to_vec(),
2738                    }),
2739                    client_cert_resolver: Some(resolver),
2740                    domain: Some("test.temporal.io".to_string()),
2741                    ..Default::default()
2742                };
2743                let endpoint =
2744                    tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2745                let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2746                assert!(
2747                    matches!(result, Err(ClientConnectError::InvalidConfig(msg)) if msg.contains("client_tls_options") && msg.contains("client_cert_resolver")),
2748                    "Should fail with InvalidConfig when both static and dynamic client certs are set"
2749                );
2750            }
2751
2752            #[tokio::test]
2753            async fn add_tls_no_options_returns_standard_passthrough() {
2754                let endpoint = tonic::transport::Channel::from_static("http://localhost:7233");
2755                let result = add_tls_to_channel(None, endpoint).await;
2756                assert!(
2757                    matches!(&result, Ok(TlsConfigResult::Standard(_))),
2758                    "Should return Standard when no TLS options are set"
2759                );
2760            }
2761
2762            #[test]
2763            fn build_custom_rustls_config_with_resolver() {
2764                let resolver = Arc::new(MockClientCertResolver);
2765                let tls_opts = TlsOptions {
2766                    domain: Some("test.temporal.io".to_string()),
2767                    ..Default::default()
2768                };
2769                let config = build_custom_rustls_config(&tls_opts, Some(resolver));
2770                assert!(config.is_ok(), "Should build config: {:?}", config.err());
2771                let config = config.unwrap();
2772                assert_eq!(config.alpn_protocols, vec![b"h2".to_vec()]);
2773            }
2774
2775            #[test]
2776            fn build_custom_rustls_config_without_resolver() {
2777                let tls_opts = TlsOptions {
2778                    domain: Some("test.temporal.io".to_string()),
2779                    ..Default::default()
2780                };
2781                let config = build_custom_rustls_config(&tls_opts, None);
2782                assert!(config.is_ok(), "Should build config: {:?}", config.err());
2783            }
2784
2785            #[test]
2786            fn build_custom_rustls_config_with_custom_verifier_and_resolver() {
2787                let resolver = Arc::new(MockClientCertResolver);
2788                let tls_opts = TlsOptions {
2789                    server_cert_verifier: Some(Arc::new(MockVerifier)),
2790                    domain: Some("test.temporal.io".to_string()),
2791                    ..Default::default()
2792                };
2793                let config = build_custom_rustls_config(&tls_opts, Some(resolver));
2794                assert!(
2795                    config.is_ok(),
2796                    "Should build config with custom verifier + resolver: {:?}",
2797                    config.err()
2798                );
2799            }
2800
2801            #[test]
2802            fn tls_options_debug_shows_custom_for_resolver() {
2803                let resolver = Arc::new(MockClientCertResolver);
2804                let tls_opts = TlsOptions {
2805                    client_cert_resolver: Some(resolver),
2806                    ..Default::default()
2807                };
2808                let debug_str = format!("{:?}", tls_opts);
2809                assert!(
2810                    debug_str.contains("\"<custom>\""),
2811                    "Debug should show <custom> for client_cert_resolver: {debug_str}"
2812                );
2813                assert!(
2814                    debug_str.contains("client_cert_resolver"),
2815                    "Debug should contain field name: {debug_str}"
2816                );
2817            }
2818
2819            #[test]
2820            fn tls_options_default_has_no_resolver() {
2821                let tls_opts = TlsOptions::default();
2822                assert!(tls_opts.client_cert_resolver.is_none());
2823                assert!(tls_opts.client_tls_options.is_none());
2824                assert!(tls_opts.server_cert_verifier.is_none());
2825            }
2826
2827            #[tokio::test]
2828            async fn add_tls_resolver_with_ip_host_uses_ip_as_domain() {
2829                // When no explicit domain is set, the host from the URI is used for SNI.
2830                // This verifies the .or_else() fallback works correctly.
2831                let resolver = Arc::new(MockClientCertResolver);
2832                let tls_opts = TlsOptions {
2833                    client_cert_resolver: Some(resolver),
2834                    // No domain set — should fall back to URI host
2835                    ..Default::default()
2836                };
2837                let endpoint = tonic::transport::Channel::from_static("https://192.168.1.100:7233");
2838                let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2839                match result {
2840                    Ok(TlsConfigResult::CustomConnector { domain, .. }) => {
2841                        assert_eq!(domain, "192.168.1.100");
2842                    }
2843                    other => panic!(
2844                        "Expected CustomConnector with IP domain, got {:?}",
2845                        other.err()
2846                    ),
2847                }
2848            }
2849        }
2850    }
2851
2852    mod start_workflow_interceptor_tests {
2853        use super::*;
2854        use crate::request_extensions::RetryConfigForCall;
2855        use parking_lot::Mutex;
2856        use std::sync::atomic::{AtomicUsize, Ordering};
2857        use temporalio_common::{
2858            HasWorkflowDefinition, WorkflowDefinition,
2859            data_converters::{
2860                DefaultFailureConverter, PayloadCodec, PayloadConversionError,
2861                SerializationContext, SerializationContextData, TemporalSerializable,
2862            },
2863            protos::temporal::api::common::v1::Payload,
2864        };
2865        use tonic::{Request, Response};
2866
2867        struct TestWorkflow;
2868
2869        impl WorkflowDefinition for TestWorkflow {
2870            type Input = Vec<String>;
2871            type Output = ();
2872
2873            fn name(&self) -> &str {
2874                "test-workflow"
2875            }
2876        }
2877
2878        impl HasWorkflowDefinition for TestWorkflow {
2879            type Run = Self;
2880        }
2881
2882        #[derive(Default)]
2883        struct RecordedStart {
2884            calls: usize,
2885            workflow_type: String,
2886            payloads: Vec<Payload>,
2887            ascii_metadata: Option<String>,
2888            binary_metadata: Option<Vec<u8>>,
2889            grpc_timeout: Option<String>,
2890            retry_options: Option<RetryOptions>,
2891        }
2892
2893        struct CountingCodec {
2894            encode_calls: Arc<AtomicUsize>,
2895        }
2896
2897        impl PayloadCodec for CountingCodec {
2898            fn encode(
2899                &self,
2900                _context: &SerializationContextData,
2901                payloads: Vec<Payload>,
2902            ) -> futures_util::future::BoxFuture<
2903                'static,
2904                Result<Vec<Payload>, PayloadConversionError>,
2905            > {
2906                self.encode_calls.fetch_add(1, Ordering::SeqCst);
2907                Box::pin(async move { Ok(payloads) })
2908            }
2909
2910            fn decode(
2911                &self,
2912                _context: &SerializationContextData,
2913                payloads: Vec<Payload>,
2914            ) -> futures_util::future::BoxFuture<
2915                'static,
2916                Result<Vec<Payload>, PayloadConversionError>,
2917            > {
2918                Box::pin(async move { Ok(payloads) })
2919            }
2920        }
2921
2922        #[derive(Clone)]
2923        struct MockStartWorkflowClient {
2924            recorded: Arc<Mutex<RecordedStart>>,
2925            data_converter: DataConverter,
2926        }
2927
2928        impl NamespacedClient for MockStartWorkflowClient {
2929            fn namespace(&self) -> String {
2930                "test-namespace".to_owned()
2931            }
2932
2933            fn identity(&self) -> String {
2934                "test-identity".to_owned()
2935            }
2936
2937            fn data_converter(&self) -> &DataConverter {
2938                &self.data_converter
2939            }
2940        }
2941
2942        impl WorkflowService for MockStartWorkflowClient {
2943            fn start_workflow_execution(
2944                &mut self,
2945                request: Request<StartWorkflowExecutionRequest>,
2946            ) -> futures_util::future::BoxFuture<
2947                '_,
2948                Result<Response<StartWorkflowExecutionResponse>, tonic::Status>,
2949            > {
2950                let ascii_metadata = request
2951                    .metadata()
2952                    .get("call-meta")
2953                    .map(|value| value.to_str().unwrap().to_owned());
2954                let binary_metadata = request
2955                    .metadata()
2956                    .get_bin("call-meta-bin")
2957                    .map(|value| value.to_bytes().unwrap().to_vec());
2958                let grpc_timeout = request
2959                    .metadata()
2960                    .get("grpc-timeout")
2961                    .map(|value| value.to_str().unwrap().to_owned());
2962                let retry_options = request
2963                    .extensions()
2964                    .get::<RetryConfigForCall>()
2965                    .map(|config| config.0.clone());
2966                let request = request.into_inner();
2967                let mut recorded = self.recorded.lock();
2968                recorded.calls += 1;
2969                recorded.workflow_type = request.workflow_type.unwrap().name;
2970                recorded.payloads = request.input.unwrap_or_default().payloads;
2971                recorded.ascii_metadata = ascii_metadata;
2972                recorded.binary_metadata = binary_metadata;
2973                recorded.grpc_timeout = grpc_timeout;
2974                recorded.retry_options = retry_options;
2975
2976                Box::pin(async {
2977                    Ok(Response::new(StartWorkflowExecutionResponse {
2978                        run_id: "server-run-id".to_owned(),
2979                        ..Default::default()
2980                    }))
2981                })
2982            }
2983
2984            fn signal_with_start_workflow_execution(
2985                &mut self,
2986                request: Request<SignalWithStartWorkflowExecutionRequest>,
2987            ) -> futures_util::future::BoxFuture<
2988                '_,
2989                Result<Response<SignalWithStartWorkflowExecutionResponse>, tonic::Status>,
2990            > {
2991                let ascii_metadata = request
2992                    .metadata()
2993                    .get("call-meta")
2994                    .map(|value| value.to_str().unwrap().to_owned());
2995                let binary_metadata = request
2996                    .metadata()
2997                    .get_bin("call-meta-bin")
2998                    .map(|value| value.to_bytes().unwrap().to_vec());
2999                let grpc_timeout = request
3000                    .metadata()
3001                    .get("grpc-timeout")
3002                    .map(|value| value.to_str().unwrap().to_owned());
3003                let retry_options = request
3004                    .extensions()
3005                    .get::<RetryConfigForCall>()
3006                    .map(|config| config.0.clone());
3007                let request = request.into_inner();
3008                let mut recorded = self.recorded.lock();
3009                recorded.calls += 1;
3010                recorded.workflow_type = request.workflow_type.unwrap().name;
3011                recorded.payloads = request.input.unwrap_or_default().payloads;
3012                recorded.ascii_metadata = ascii_metadata;
3013                recorded.binary_metadata = binary_metadata;
3014                recorded.grpc_timeout = grpc_timeout;
3015                recorded.retry_options = retry_options;
3016
3017                Box::pin(async {
3018                    Ok(Response::new(SignalWithStartWorkflowExecutionResponse {
3019                        run_id: "signal-server-run-id".to_owned(),
3020                        ..Default::default()
3021                    }))
3022                })
3023            }
3024        }
3025
3026        #[derive(Clone)]
3027        struct InterceptedClient {
3028            inner: MockStartWorkflowClient,
3029            interceptors: Vec<Arc<dyn ClientInterceptor>>,
3030        }
3031
3032        impl NamespacedClient for InterceptedClient {
3033            fn namespace(&self) -> String {
3034                self.inner.namespace()
3035            }
3036
3037            fn identity(&self) -> String {
3038                self.inner.identity()
3039            }
3040
3041            fn data_converter(&self) -> &DataConverter {
3042                self.inner.data_converter()
3043            }
3044
3045            fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
3046                &self.interceptors
3047            }
3048        }
3049
3050        impl WorkflowService for InterceptedClient {
3051            fn start_workflow_execution(
3052                &mut self,
3053                request: Request<StartWorkflowExecutionRequest>,
3054            ) -> futures_util::future::BoxFuture<
3055                '_,
3056                Result<Response<StartWorkflowExecutionResponse>, tonic::Status>,
3057            > {
3058                self.inner.start_workflow_execution(request)
3059            }
3060
3061            fn signal_with_start_workflow_execution(
3062                &mut self,
3063                request: Request<SignalWithStartWorkflowExecutionRequest>,
3064            ) -> futures_util::future::BoxFuture<
3065                '_,
3066                Result<Response<SignalWithStartWorkflowExecutionResponse>, tonic::Status>,
3067            > {
3068                self.inner.signal_with_start_workflow_execution(request)
3069            }
3070        }
3071
3072        struct OrderedInterceptor {
3073            name: &'static str,
3074            events: Arc<Mutex<Vec<String>>>,
3075            encode_calls: Arc<AtomicUsize>,
3076        }
3077
3078        impl ClientInterceptor for OrderedInterceptor {
3079            fn start_workflow<'a>(
3080                &'a self,
3081                mut input: StartWorkflowInput,
3082                next: Next<
3083                    'a,
3084                    StartWorkflowInput,
3085                    BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
3086                >,
3087            ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
3088                Box::pin(async move {
3089                    assert_eq!(self.encode_calls.load(Ordering::SeqCst), 0);
3090                    self.events.lock().push(format!("{}-pre", self.name));
3091                    tokio::task::yield_now().await;
3092                    if self.name == "outer" {
3093                        input
3094                            .args_mut::<Vec<String>>()
3095                            .unwrap()
3096                            .push("mutated".to_owned());
3097                    } else {
3098                        assert_eq!(
3099                            input.args_ref::<Vec<String>>().unwrap(),
3100                            &["initial".to_owned(), "mutated".to_owned()]
3101                        );
3102                        input.replace_args("replacement".to_owned());
3103                        input.workflow_type = "replacement-workflow".to_owned();
3104                    }
3105                    let result = next.run(input).await;
3106                    tokio::task::yield_now().await;
3107                    self.events.lock().push(format!("{}-post", self.name));
3108                    result
3109                })
3110            }
3111        }
3112
3113        struct ShortCircuitInterceptor;
3114
3115        impl ClientInterceptor for ShortCircuitInterceptor {
3116            fn start_workflow<'a>(
3117                &'a self,
3118                input: StartWorkflowInput,
3119                _next: Next<
3120                    'a,
3121                    StartWorkflowInput,
3122                    BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
3123                >,
3124            ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
3125                assert_eq!(
3126                    input.args_ref::<Vec<String>>().unwrap(),
3127                    &["initial".to_owned()]
3128                );
3129                Box::pin(async {
3130                    Ok(StartWorkflowOutput::new(
3131                        "short-circuit-workflow-id",
3132                        "short-circuit-run-id",
3133                    ))
3134                })
3135            }
3136        }
3137
3138        struct CountingInput {
3139            conversion_calls: Arc<AtomicUsize>,
3140        }
3141
3142        impl TemporalSerializable for CountingInput {
3143            fn to_payloads(
3144                &self,
3145                _context: &SerializationContext<'_>,
3146            ) -> Result<Vec<Payload>, PayloadConversionError> {
3147                self.conversion_calls.fetch_add(1, Ordering::SeqCst);
3148                Ok(vec![Payload::default()])
3149            }
3150        }
3151
3152        struct ConversionTimingInterceptor {
3153            conversion_calls: Arc<AtomicUsize>,
3154        }
3155
3156        impl ClientInterceptor for ConversionTimingInterceptor {
3157            fn start_workflow<'a>(
3158                &'a self,
3159                mut input: StartWorkflowInput,
3160                next: Next<
3161                    'a,
3162                    StartWorkflowInput,
3163                    BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
3164                >,
3165            ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
3166                input.replace_args(CountingInput {
3167                    conversion_calls: self.conversion_calls.clone(),
3168                });
3169                let future = next.run(input);
3170                assert_eq!(self.conversion_calls.load(Ordering::SeqCst), 0);
3171                future
3172            }
3173        }
3174
3175        fn mock_client(
3176            interceptors: Vec<Arc<dyn ClientInterceptor>>,
3177            encode_calls: Arc<AtomicUsize>,
3178        ) -> (InterceptedClient, Arc<Mutex<RecordedStart>>) {
3179            let recorded = Arc::new(Mutex::new(RecordedStart::default()));
3180            let data_converter = DataConverter::new(
3181                PayloadConverter::default(),
3182                DefaultFailureConverter,
3183                CountingCodec {
3184                    encode_calls: encode_calls.clone(),
3185                },
3186            );
3187            (
3188                InterceptedClient {
3189                    inner: MockStartWorkflowClient {
3190                        recorded: recorded.clone(),
3191                        data_converter,
3192                    },
3193                    interceptors,
3194                },
3195                recorded,
3196            )
3197        }
3198
3199        #[tokio::test]
3200        async fn interceptors_order_mutate_replace_and_defer_conversion() {
3201            let events = Arc::new(Mutex::new(Vec::new()));
3202            let encode_calls = Arc::new(AtomicUsize::new(0));
3203            let interceptors: Vec<Arc<dyn ClientInterceptor>> = vec![
3204                Arc::new(OrderedInterceptor {
3205                    name: "outer",
3206                    events: events.clone(),
3207                    encode_calls: encode_calls.clone(),
3208                }),
3209                Arc::new(OrderedInterceptor {
3210                    name: "inner",
3211                    events: events.clone(),
3212                    encode_calls: encode_calls.clone(),
3213                }),
3214            ];
3215            let (client, recorded) = mock_client(interceptors, encode_calls.clone());
3216
3217            let handle = client
3218                .start_workflow(
3219                    TestWorkflow,
3220                    vec!["initial".to_owned()],
3221                    WorkflowStartOptions::new("task-queue", "workflow-id").build(),
3222                )
3223                .await
3224                .unwrap();
3225
3226            assert_eq!(
3227                events.lock().as_slice(),
3228                ["outer-pre", "inner-pre", "inner-post", "outer-post"]
3229            );
3230            assert_eq!(encode_calls.load(Ordering::SeqCst), 1);
3231            assert_eq!(handle.run_id(), Some("server-run-id"));
3232            let payloads = {
3233                let recorded = recorded.lock();
3234                assert_eq!(recorded.calls, 1);
3235                assert_eq!(recorded.workflow_type, "replacement-workflow");
3236                recorded.payloads.clone()
3237            };
3238            let replacement: String = client
3239                .data_converter()
3240                .from_payloads(&SerializationContextData::Workflow, payloads)
3241                .await
3242                .unwrap();
3243            assert_eq!(replacement, "replacement");
3244        }
3245
3246        #[tokio::test]
3247        async fn interceptor_can_short_circuit() {
3248            let encode_calls = Arc::new(AtomicUsize::new(0));
3249            let (client, recorded) = mock_client(
3250                vec![Arc::new(ShortCircuitInterceptor)],
3251                encode_calls.clone(),
3252            );
3253            let handle = client
3254                .start_workflow(
3255                    TestWorkflow,
3256                    vec!["initial".to_owned()],
3257                    WorkflowStartOptions::new("task-queue", "ignored-workflow-id").build(),
3258                )
3259                .await
3260                .unwrap();
3261
3262            assert_eq!(handle.info().workflow_id, "short-circuit-workflow-id");
3263            assert_eq!(handle.run_id(), Some("short-circuit-run-id"));
3264            assert_eq!(recorded.lock().calls, 0);
3265            assert_eq!(encode_calls.load(Ordering::SeqCst), 0);
3266        }
3267
3268        #[tokio::test]
3269        async fn payload_conversion_waits_for_next_future_poll() {
3270            let conversion_calls = Arc::new(AtomicUsize::new(0));
3271            let encode_calls = Arc::new(AtomicUsize::new(0));
3272            let recorded = Arc::new(Mutex::new(RecordedStart::default()));
3273            let data_converter = DataConverter::new(
3274                PayloadConverter::UseWrappers,
3275                DefaultFailureConverter,
3276                CountingCodec {
3277                    encode_calls: encode_calls.clone(),
3278                },
3279            );
3280            let client = InterceptedClient {
3281                inner: MockStartWorkflowClient {
3282                    recorded: recorded.clone(),
3283                    data_converter,
3284                },
3285                interceptors: vec![Arc::new(ConversionTimingInterceptor {
3286                    conversion_calls: conversion_calls.clone(),
3287                })],
3288            };
3289
3290            client
3291                .start_workflow(
3292                    TestWorkflow,
3293                    vec!["initial".to_owned()],
3294                    WorkflowStartOptions::new("task-queue", "workflow-id").build(),
3295                )
3296                .await
3297                .unwrap();
3298
3299            assert_eq!(conversion_calls.load(Ordering::SeqCst), 1);
3300            assert_eq!(encode_calls.load(Ordering::SeqCst), 1);
3301            assert_eq!(recorded.lock().calls, 1);
3302        }
3303
3304        #[tokio::test]
3305        async fn custom_client_defaults_to_empty_chain() {
3306            let recorded = Arc::new(Mutex::new(RecordedStart::default()));
3307            let client = MockStartWorkflowClient {
3308                recorded: recorded.clone(),
3309                data_converter: DataConverter::default(),
3310            };
3311            assert!(client.client_interceptors().is_empty());
3312
3313            client
3314                .start_workflow(
3315                    TestWorkflow,
3316                    vec!["initial".to_owned()],
3317                    WorkflowStartOptions::new("task-queue", "workflow-id").build(),
3318                )
3319                .await
3320                .unwrap();
3321            assert_eq!(recorded.lock().calls, 1);
3322        }
3323
3324        #[tokio::test]
3325        async fn rpc_options_reach_the_request() {
3326            let (client, recorded) = mock_client(Vec::new(), Arc::new(AtomicUsize::new(0)));
3327            let mut metadata = RpcMetadata::new();
3328            metadata.insert("call-meta", "call-value").unwrap();
3329            metadata
3330                .insert_binary("call-meta-bin", vec![0, 255])
3331                .unwrap();
3332            let rpc_options = RpcOptions::builder()
3333                .metadata(metadata)
3334                .timeout(Duration::from_millis(250))
3335                .retry_options(RetryOptions::no_retries())
3336                .build();
3337            let mut options = WorkflowStartOptions::new("task-queue", "workflow-id").build();
3338            options.rpc_options = rpc_options.clone();
3339
3340            client
3341                .start_workflow(TestWorkflow, vec!["initial".to_owned()], options)
3342                .await
3343                .unwrap();
3344
3345            {
3346                let recorded = recorded.lock();
3347                assert_eq!(recorded.ascii_metadata.as_deref(), Some("call-value"));
3348                assert_eq!(recorded.binary_metadata.as_deref(), Some(&[0, 255][..]));
3349                assert_eq!(recorded.grpc_timeout.as_deref(), Some("250000u"));
3350                assert_eq!(recorded.retry_options, Some(RetryOptions::no_retries()));
3351            }
3352
3353            let mut options = WorkflowStartOptions::new("task-queue", "signal-workflow-id").build();
3354            options.start_signal = Some(WorkflowStartSignal::new("signal-name").build());
3355            options.rpc_options = rpc_options;
3356            let handle = client
3357                .start_workflow(TestWorkflow, vec!["initial".to_owned()], options)
3358                .await
3359                .unwrap();
3360
3361            let recorded = recorded.lock();
3362            assert_eq!(recorded.calls, 2);
3363            assert_eq!(recorded.ascii_metadata.as_deref(), Some("call-value"));
3364            assert_eq!(recorded.binary_metadata.as_deref(), Some(&[0, 255][..]));
3365            assert_eq!(recorded.grpc_timeout.as_deref(), Some("250000u"));
3366            assert_eq!(recorded.retry_options, Some(RetryOptions::no_retries()));
3367            assert_eq!(handle.run_id(), Some("signal-server-run-id"));
3368        }
3369
3370        #[test]
3371        fn rpc_metadata_combines_with_and_overrides_connection_defaults() {
3372            let headers = Arc::new(RwLock::new(ClientHeaders {
3373                user_headers: HashMap::from([
3374                    (
3375                        "shared-meta".parse().unwrap(),
3376                        "connection-value".parse().unwrap(),
3377                    ),
3378                    (
3379                        "connection-meta".parse().unwrap(),
3380                        "connection-only".parse().unwrap(),
3381                    ),
3382                ]),
3383                user_binary_headers: HashMap::from([
3384                    (
3385                        "shared-meta-bin".parse().unwrap(),
3386                        BinaryMetadataValue::from_bytes(&[1]),
3387                    ),
3388                    (
3389                        "connection-meta-bin".parse().unwrap(),
3390                        BinaryMetadataValue::from_bytes(&[2]),
3391                    ),
3392                ]),
3393                api_key: None,
3394            }));
3395            let mut service_interceptor = ServiceCallInterceptor {
3396                client_name: "test-client".to_owned(),
3397                client_version: "test-version".to_owned(),
3398                headers,
3399            };
3400            let mut rpc_options = RpcOptions::default();
3401            rpc_options
3402                .metadata
3403                .insert("shared-meta", "call-value")
3404                .unwrap();
3405            rpc_options
3406                .metadata
3407                .insert("call-meta", "call-only")
3408                .unwrap();
3409            rpc_options
3410                .metadata
3411                .insert_binary("shared-meta-bin", vec![3])
3412                .unwrap();
3413            rpc_options
3414                .metadata
3415                .insert_binary("call-meta-bin", vec![4])
3416                .unwrap();
3417            let mut request = Request::new(());
3418            rpc_options.apply_to(&mut request);
3419
3420            let request = service_interceptor.call(request).unwrap();
3421            assert_eq!(request.metadata().get("shared-meta").unwrap(), "call-value");
3422            assert_eq!(request.metadata().get("call-meta").unwrap(), "call-only");
3423            assert_eq!(
3424                request.metadata().get("connection-meta").unwrap(),
3425                "connection-only"
3426            );
3427            assert_eq!(
3428                request.metadata().get_bin("shared-meta-bin").unwrap(),
3429                &[3][..]
3430            );
3431            assert_eq!(
3432                request.metadata().get_bin("call-meta-bin").unwrap(),
3433                &[4][..]
3434            );
3435            assert_eq!(
3436                request.metadata().get_bin("connection-meta-bin").unwrap(),
3437                &[2][..]
3438            );
3439        }
3440    }
3441
3442    mod list_workflows_tests {
3443        use super::*;
3444        use crate::test_helpers::{FailingCodec, XorCodec};
3445        use futures_util::{FutureExt, StreamExt};
3446        use std::sync::atomic::{AtomicUsize, Ordering};
3447        use temporalio_common::{
3448            data_converters::DefaultFailureConverter,
3449            protos::temporal::api::common::v1::{
3450                Memo as ProtoMemo, Payload, WorkflowExecution as ProtoWorkflowExecution,
3451            },
3452        };
3453        use tonic::{Request, Response};
3454
3455        #[derive(Clone)]
3456        struct MockListWorkflowsClient {
3457            call_count: Arc<AtomicUsize>,
3458            // Returns this many workflows per page
3459            page_size: usize,
3460            // Total workflows available
3461            total_workflows: usize,
3462            data_converter: DataConverter,
3463            memo_payload: Option<Payload>,
3464            interceptors: Vec<Arc<dyn ClientInterceptor>>,
3465        }
3466
3467        impl NamespacedClient for MockListWorkflowsClient {
3468            fn namespace(&self) -> String {
3469                "test-namespace".to_string()
3470            }
3471            fn identity(&self) -> String {
3472                "test-identity".to_string()
3473            }
3474            fn data_converter(&self) -> &DataConverter {
3475                &self.data_converter
3476            }
3477            fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
3478                &self.interceptors
3479            }
3480        }
3481
3482        struct CountingListInterceptor {
3483            calls: Arc<AtomicUsize>,
3484        }
3485
3486        impl ClientInterceptor for CountingListInterceptor {
3487            fn list_workflows_page<'a>(
3488                &'a self,
3489                input: ListWorkflowsPageInput,
3490                next: Next<
3491                    'a,
3492                    ListWorkflowsPageInput,
3493                    BoxFuture<'a, Result<ListWorkflowsPageOutput, ClientError>>,
3494                >,
3495            ) -> BoxFuture<'a, Result<ListWorkflowsPageOutput, ClientError>> {
3496                self.calls.fetch_add(1, Ordering::SeqCst);
3497                next.run(input)
3498            }
3499        }
3500
3501        impl WorkflowService for MockListWorkflowsClient {
3502            fn list_workflow_executions(
3503                &mut self,
3504                request: Request<ListWorkflowExecutionsRequest>,
3505            ) -> futures_util::future::BoxFuture<
3506                '_,
3507                Result<Response<ListWorkflowExecutionsResponse>, tonic::Status>,
3508            > {
3509                self.call_count.fetch_add(1, Ordering::SeqCst);
3510                let req = request.into_inner();
3511
3512                // Determine offset from page token
3513                let offset: usize = if req.next_page_token.is_empty() {
3514                    0
3515                } else {
3516                    String::from_utf8(req.next_page_token)
3517                        .unwrap()
3518                        .parse()
3519                        .unwrap()
3520                };
3521
3522                let remaining = self.total_workflows.saturating_sub(offset);
3523                let count = remaining.min(self.page_size);
3524                let new_offset = offset + count;
3525
3526                let executions: Vec<_> = (offset..offset + count)
3527                    .map(|i| workflow::WorkflowExecutionInfo {
3528                        execution: Some(ProtoWorkflowExecution {
3529                            workflow_id: format!("wf-{i}"),
3530                            run_id: format!("run-{i}"),
3531                        }),
3532                        r#type: Some(WorkflowType {
3533                            name: "TestWorkflow".to_string(),
3534                        }),
3535                        task_queue: "test-queue".to_string(),
3536                        memo: self.memo_payload.clone().map(|payload| ProtoMemo {
3537                            fields: HashMap::from([("memo-key".to_owned(), payload)]),
3538                        }),
3539                        ..Default::default()
3540                    })
3541                    .collect();
3542
3543                let next_page_token = if new_offset < self.total_workflows {
3544                    new_offset.to_string().into_bytes()
3545                } else {
3546                    vec![]
3547                };
3548
3549                async move {
3550                    Ok(Response::new(ListWorkflowExecutionsResponse {
3551                        executions,
3552                        next_page_token,
3553                    }))
3554                }
3555                .boxed()
3556            }
3557        }
3558
3559        #[tokio::test]
3560        async fn list_workflows_paginates_through_all_results() {
3561            let call_count = Arc::new(AtomicUsize::new(0));
3562            let interceptor_calls = Arc::new(AtomicUsize::new(0));
3563            let client = MockListWorkflowsClient {
3564                call_count: call_count.clone(),
3565                page_size: 3,
3566                total_workflows: 10,
3567                data_converter: DataConverter::default(),
3568                memo_payload: None,
3569                interceptors: vec![Arc::new(CountingListInterceptor {
3570                    calls: interceptor_calls.clone(),
3571                })],
3572            };
3573
3574            let stream = client.list_workflows("", WorkflowListOptions::default());
3575            let results: Vec<_> = stream.collect().await;
3576
3577            assert_eq!(results.len(), 10);
3578            for (i, result) in results.iter().enumerate() {
3579                let wf = result.as_ref().unwrap();
3580                assert_eq!(wf.id(), format!("wf-{i}"));
3581                assert_eq!(wf.run_id(), format!("run-{i}"));
3582            }
3583            // Should have made 4 calls: pages of 3, 3, 3, 1
3584            assert_eq!(call_count.load(Ordering::SeqCst), 4);
3585            assert_eq!(interceptor_calls.load(Ordering::SeqCst), 4);
3586        }
3587
3588        #[tokio::test]
3589        async fn list_workflows_respects_limit() {
3590            let call_count = Arc::new(AtomicUsize::new(0));
3591            let client = MockListWorkflowsClient {
3592                call_count: call_count.clone(),
3593                page_size: 3,
3594                total_workflows: 10,
3595                data_converter: DataConverter::default(),
3596                memo_payload: None,
3597                interceptors: Vec::new(),
3598            };
3599
3600            let opts = WorkflowListOptions::builder().limit(5).build();
3601            let stream = client.list_workflows("", opts);
3602            let results: Vec<_> = stream.collect().await;
3603
3604            assert_eq!(results.len(), 5);
3605            for (i, result) in results.iter().enumerate() {
3606                let wf = result.as_ref().unwrap();
3607                assert_eq!(wf.id(), format!("wf-{i}"));
3608            }
3609            // Should have made 2 calls: 1 page of 3, then 2 more from next page
3610            assert_eq!(call_count.load(Ordering::SeqCst), 2);
3611        }
3612
3613        #[tokio::test]
3614        async fn list_workflows_limit_less_than_page_size() {
3615            let call_count = Arc::new(AtomicUsize::new(0));
3616            let client = MockListWorkflowsClient {
3617                call_count: call_count.clone(),
3618                page_size: 10,
3619                total_workflows: 100,
3620                data_converter: DataConverter::default(),
3621                memo_payload: None,
3622                interceptors: Vec::new(),
3623            };
3624
3625            let opts = WorkflowListOptions::builder().limit(3).build();
3626            let stream = client.list_workflows("", opts);
3627            let results: Vec<_> = stream.collect().await;
3628
3629            assert_eq!(results.len(), 3);
3630            // Only 1 call needed since limit < page_size
3631            assert_eq!(call_count.load(Ordering::SeqCst), 1);
3632        }
3633
3634        #[tokio::test]
3635        async fn list_workflows_empty_results() {
3636            let call_count = Arc::new(AtomicUsize::new(0));
3637            let client = MockListWorkflowsClient {
3638                call_count: call_count.clone(),
3639                page_size: 10,
3640                total_workflows: 0,
3641                data_converter: DataConverter::default(),
3642                memo_payload: None,
3643                interceptors: Vec::new(),
3644            };
3645
3646            let stream = client.list_workflows("", WorkflowListOptions::default());
3647            let results: Vec<_> = stream.collect().await;
3648
3649            assert_eq!(results.len(), 0);
3650            assert_eq!(call_count.load(Ordering::SeqCst), 1);
3651        }
3652
3653        #[tokio::test]
3654        async fn list_workflows_exposes_typed_memo() {
3655            let data_converter = DataConverter::new(
3656                PayloadConverter::default(),
3657                DefaultFailureConverter,
3658                XorCodec,
3659            );
3660            let memo_payload = data_converter
3661                .to_payload(
3662                    &SerializationContextData::Workflow,
3663                    &"memo-value".to_owned(),
3664                )
3665                .await
3666                .unwrap();
3667            let client = MockListWorkflowsClient {
3668                call_count: Arc::new(AtomicUsize::new(0)),
3669                page_size: 1,
3670                total_workflows: 1,
3671                data_converter,
3672                memo_payload: Some(memo_payload),
3673                interceptors: Vec::new(),
3674            };
3675
3676            let workflow = client
3677                .list_workflows("", WorkflowListOptions::default())
3678                .next()
3679                .await
3680                .unwrap()
3681                .unwrap();
3682
3683            assert_eq!(
3684                workflow.memo().get::<String>("memo-key").unwrap(),
3685                Some("memo-value".to_owned())
3686            );
3687        }
3688
3689        #[tokio::test]
3690        async fn list_workflows_yields_codec_error_then_ends() {
3691            let client = MockListWorkflowsClient {
3692                call_count: Arc::new(AtomicUsize::new(0)),
3693                page_size: 1,
3694                total_workflows: 1,
3695                data_converter: DataConverter::new(
3696                    PayloadConverter::default(),
3697                    DefaultFailureConverter,
3698                    FailingCodec,
3699                ),
3700                memo_payload: Some(Payload::default()),
3701                interceptors: Vec::new(),
3702            };
3703            let mut stream = client.list_workflows("", WorkflowListOptions::default());
3704
3705            let err = stream.next().await.unwrap().unwrap_err();
3706
3707            assert!(matches!(err, ClientError::PayloadConversion(_)));
3708            assert!(stream.next().await.is_none());
3709        }
3710    }
3711}