@temporalio/core-bridge 1.11.8 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/Cargo.lock +219 -193
  2. package/Cargo.toml +27 -8
  3. package/README.md +5 -0
  4. package/index.js +72 -12
  5. package/lib/errors.d.ts +25 -0
  6. package/lib/errors.js +76 -1
  7. package/lib/errors.js.map +1 -1
  8. package/lib/index.d.ts +11 -478
  9. package/lib/index.js +28 -5
  10. package/lib/index.js.map +1 -1
  11. package/lib/native.d.ts +330 -0
  12. package/lib/{worker-tuner.js → native.js} +1 -1
  13. package/lib/native.js.map +1 -0
  14. package/package.json +7 -3
  15. package/releases/aarch64-apple-darwin/index.node +0 -0
  16. package/releases/aarch64-unknown-linux-gnu/index.node +0 -0
  17. package/releases/x86_64-apple-darwin/index.node +0 -0
  18. package/releases/x86_64-pc-windows-msvc/index.node +0 -0
  19. package/releases/x86_64-unknown-linux-gnu/index.node +0 -0
  20. package/sdk-core/.cargo/config.toml +8 -2
  21. package/sdk-core/.cargo/multi-worker-manual-test +15 -0
  22. package/sdk-core/.github/workflows/per-pr.yml +40 -11
  23. package/sdk-core/AGENTS.md +73 -0
  24. package/sdk-core/ARCHITECTURE.md +71 -23
  25. package/sdk-core/Cargo.toml +1 -1
  26. package/sdk-core/README.md +4 -4
  27. package/sdk-core/arch_docs/workflow_task_chunking.md +51 -0
  28. package/sdk-core/client/Cargo.toml +1 -1
  29. package/sdk-core/client/src/lib.rs +49 -13
  30. package/sdk-core/client/src/metrics.rs +15 -16
  31. package/sdk-core/client/src/proxy.rs +2 -2
  32. package/sdk-core/client/src/raw.rs +54 -8
  33. package/sdk-core/client/src/retry.rs +109 -13
  34. package/sdk-core/client/src/worker_registry/mod.rs +4 -4
  35. package/sdk-core/client/src/workflow_handle/mod.rs +1 -1
  36. package/sdk-core/core/Cargo.toml +28 -8
  37. package/sdk-core/core/src/abstractions.rs +62 -10
  38. package/sdk-core/core/src/core_tests/activity_tasks.rs +180 -8
  39. package/sdk-core/core/src/core_tests/mod.rs +4 -4
  40. package/sdk-core/core/src/core_tests/queries.rs +18 -4
  41. package/sdk-core/core/src/core_tests/workers.rs +3 -3
  42. package/sdk-core/core/src/core_tests/workflow_tasks.rs +191 -25
  43. package/sdk-core/core/src/ephemeral_server/mod.rs +10 -3
  44. package/sdk-core/core/src/internal_flags.rs +14 -14
  45. package/sdk-core/core/src/lib.rs +5 -2
  46. package/sdk-core/core/src/pollers/mod.rs +1 -1
  47. package/sdk-core/core/src/pollers/poll_buffer.rs +495 -164
  48. package/sdk-core/core/src/protosext/mod.rs +3 -3
  49. package/sdk-core/core/src/replay/mod.rs +3 -3
  50. package/sdk-core/core/src/telemetry/metrics.rs +13 -4
  51. package/sdk-core/core/src/telemetry/mod.rs +72 -70
  52. package/sdk-core/core/src/telemetry/otel.rs +51 -54
  53. package/sdk-core/core/src/test_help/mod.rs +9 -3
  54. package/sdk-core/core/src/worker/activities/activity_heartbeat_manager.rs +31 -11
  55. package/sdk-core/core/src/worker/activities/local_activities.rs +35 -28
  56. package/sdk-core/core/src/worker/activities.rs +58 -30
  57. package/sdk-core/core/src/worker/client/mocks.rs +3 -3
  58. package/sdk-core/core/src/worker/client.rs +155 -53
  59. package/sdk-core/core/src/worker/mod.rs +103 -95
  60. package/sdk-core/core/src/worker/nexus.rs +100 -73
  61. package/sdk-core/core/src/worker/tuner/resource_based.rs +14 -6
  62. package/sdk-core/core/src/worker/tuner.rs +4 -13
  63. package/sdk-core/core/src/worker/workflow/history_update.rs +47 -51
  64. package/sdk-core/core/src/worker/workflow/machines/child_workflow_state_machine.rs +1 -4
  65. package/sdk-core/core/src/worker/workflow/machines/nexus_operation_state_machine.rs +127 -32
  66. package/sdk-core/core/src/worker/workflow/machines/transition_coverage.rs +1 -2
  67. package/sdk-core/core/src/worker/workflow/machines/update_state_machine.rs +1 -1
  68. package/sdk-core/core/src/worker/workflow/machines/workflow_machines.rs +55 -42
  69. package/sdk-core/core/src/worker/workflow/managed_run.rs +45 -35
  70. package/sdk-core/core/src/worker/workflow/mod.rs +200 -97
  71. package/sdk-core/core/src/worker/workflow/wft_poller.rs +175 -4
  72. package/sdk-core/core/src/worker/workflow/workflow_stream.rs +38 -36
  73. package/sdk-core/core-api/Cargo.toml +8 -0
  74. package/sdk-core/core-api/src/envconfig.rs +1544 -0
  75. package/sdk-core/core-api/src/lib.rs +2 -0
  76. package/sdk-core/core-api/src/telemetry/metrics.rs +8 -0
  77. package/sdk-core/core-api/src/telemetry.rs +36 -3
  78. package/sdk-core/core-api/src/worker.rs +301 -75
  79. package/sdk-core/docker/docker-compose-telem.yaml +1 -0
  80. package/sdk-core/etc/prometheus.yaml +6 -2
  81. package/sdk-core/histories/long_local_activity_with_update-0_history.bin +0 -0
  82. package/sdk-core/histories/long_local_activity_with_update-1_history.bin +0 -0
  83. package/sdk-core/histories/long_local_activity_with_update-2_history.bin +0 -0
  84. package/sdk-core/histories/long_local_activity_with_update-3_history.bin +0 -0
  85. package/sdk-core/sdk/src/activity_context.rs +5 -0
  86. package/sdk-core/sdk/src/interceptors.rs +73 -3
  87. package/sdk-core/sdk/src/lib.rs +15 -16
  88. package/sdk-core/sdk/src/workflow_context/options.rs +10 -0
  89. package/sdk-core/sdk/src/workflow_context.rs +48 -29
  90. package/sdk-core/sdk/src/workflow_future.rs +5 -6
  91. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/.github/workflows/push-to-buf.yml +20 -0
  92. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/CODEOWNERS +6 -0
  93. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/README.md +17 -6
  94. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/VERSION +1 -1
  95. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/buf.lock +7 -2
  96. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/buf.yaml +2 -0
  97. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/cloudservice/v1/request_response.proto +78 -0
  98. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/cloudservice/v1/service.proto +29 -0
  99. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/identity/v1/message.proto +74 -32
  100. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/namespace/v1/message.proto +45 -15
  101. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/nexus/v1/message.proto +7 -1
  102. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/operation/v1/message.proto +3 -3
  103. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/region/v1/message.proto +3 -3
  104. package/sdk-core/sdk-core-protos/protos/api_upstream/LICENSE +1 -1
  105. package/sdk-core/sdk-core-protos/protos/api_upstream/buf.yaml +2 -0
  106. package/sdk-core/sdk-core-protos/protos/api_upstream/openapi/openapiv2.json +1103 -88
  107. package/sdk-core/sdk-core-protos/protos/api_upstream/openapi/openapiv3.yaml +1233 -151
  108. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/activity/v1/message.proto +0 -22
  109. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/batch/v1/message.proto +19 -24
  110. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/command/v1/message.proto +0 -22
  111. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/common/v1/message.proto +12 -22
  112. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/deployment/v1/message.proto +45 -45
  113. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/batch_operation.proto +0 -22
  114. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/command_type.proto +0 -22
  115. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/common.proto +15 -22
  116. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/deployment.proto +0 -22
  117. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/event_type.proto +4 -22
  118. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/failed_cause.proto +0 -22
  119. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/namespace.proto +0 -22
  120. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/nexus.proto +0 -20
  121. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/query.proto +0 -22
  122. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/reset.proto +0 -22
  123. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/schedule.proto +0 -22
  124. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/task_queue.proto +0 -22
  125. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/update.proto +0 -22
  126. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/workflow.proto +4 -22
  127. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/errordetails/v1/message.proto +0 -22
  128. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/export/v1/message.proto +0 -22
  129. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/failure/v1/message.proto +2 -22
  130. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/filter/v1/message.proto +0 -22
  131. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/history/v1/message.proto +75 -49
  132. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/namespace/v1/message.proto +0 -22
  133. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/nexus/v1/message.proto +0 -20
  134. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/operatorservice/v1/request_response.proto +0 -22
  135. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/operatorservice/v1/service.proto +0 -22
  136. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/protocol/v1/message.proto +0 -22
  137. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/query/v1/message.proto +0 -22
  138. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/replication/v1/message.proto +0 -22
  139. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/rules/v1/message.proto +90 -0
  140. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/schedule/v1/message.proto +0 -22
  141. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/sdk/v1/enhanced_stack_trace.proto +0 -22
  142. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/sdk/v1/task_complete_metadata.proto +0 -22
  143. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/sdk/v1/user_metadata.proto +0 -22
  144. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/sdk/v1/workflow_metadata.proto +0 -22
  145. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/taskqueue/v1/message.proto +17 -38
  146. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/update/v1/message.proto +0 -22
  147. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/version/v1/message.proto +0 -22
  148. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflow/v1/message.proto +151 -44
  149. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/request_response.proto +172 -65
  150. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/service.proto +69 -28
  151. package/sdk-core/sdk-core-protos/protos/local/temporal/sdk/core/activity_task/activity_task.proto +18 -0
  152. package/sdk-core/sdk-core-protos/protos/local/temporal/sdk/core/common/common.proto +5 -0
  153. package/sdk-core/sdk-core-protos/protos/local/temporal/sdk/core/nexus/nexus.proto +16 -1
  154. package/sdk-core/sdk-core-protos/protos/local/temporal/sdk/core/workflow_activation/workflow_activation.proto +21 -15
  155. package/sdk-core/sdk-core-protos/protos/local/temporal/sdk/core/workflow_commands/workflow_commands.proto +3 -0
  156. package/sdk-core/sdk-core-protos/protos/local/temporal/sdk/core/workflow_completion/workflow_completion.proto +3 -0
  157. package/sdk-core/sdk-core-protos/src/lib.rs +60 -16
  158. package/sdk-core/test-utils/src/lib.rs +157 -39
  159. package/sdk-core/tests/cloud_tests.rs +86 -0
  160. package/sdk-core/tests/fuzzy_workflow.rs +23 -26
  161. package/sdk-core/tests/global_metric_tests.rs +116 -0
  162. package/sdk-core/tests/heavy_tests.rs +127 -7
  163. package/sdk-core/tests/integ_tests/client_tests.rs +2 -8
  164. package/sdk-core/tests/integ_tests/metrics_tests.rs +100 -106
  165. package/sdk-core/tests/integ_tests/polling_tests.rs +94 -8
  166. package/sdk-core/tests/integ_tests/update_tests.rs +75 -6
  167. package/sdk-core/tests/integ_tests/worker_tests.rs +54 -5
  168. package/sdk-core/tests/integ_tests/worker_versioning_tests.rs +240 -0
  169. package/sdk-core/tests/integ_tests/workflow_tests/determinism.rs +41 -3
  170. package/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +168 -8
  171. package/sdk-core/tests/integ_tests/workflow_tests/nexus.rs +285 -15
  172. package/sdk-core/tests/integ_tests/workflow_tests/priority.rs +12 -4
  173. package/sdk-core/tests/integ_tests/workflow_tests/stickyness.rs +3 -2
  174. package/sdk-core/tests/integ_tests/workflow_tests.rs +124 -74
  175. package/sdk-core/tests/main.rs +3 -51
  176. package/sdk-core/tests/manual_tests.rs +430 -0
  177. package/sdk-core/tests/runner.rs +28 -2
  178. package/src/client.rs +565 -0
  179. package/src/helpers/abort_controller.rs +204 -0
  180. package/src/helpers/callbacks.rs +299 -0
  181. package/src/helpers/errors.rs +302 -0
  182. package/src/helpers/future.rs +44 -0
  183. package/src/helpers/handles.rs +191 -0
  184. package/src/helpers/inspect.rs +18 -0
  185. package/src/helpers/json_string.rs +58 -0
  186. package/src/helpers/mod.rs +20 -0
  187. package/src/helpers/properties.rs +71 -0
  188. package/src/helpers/try_from_js.rs +213 -0
  189. package/src/helpers/try_into_js.rs +129 -0
  190. package/src/lib.rs +28 -40
  191. package/src/logs.rs +111 -0
  192. package/src/metrics.rs +325 -0
  193. package/src/runtime.rs +409 -498
  194. package/src/testing.rs +315 -57
  195. package/src/worker.rs +907 -378
  196. package/ts/errors.ts +57 -0
  197. package/ts/index.ts +10 -596
  198. package/ts/native.ts +496 -0
  199. package/lib/worker-tuner.d.ts +0 -167
  200. package/lib/worker-tuner.js.map +0 -1
  201. package/src/conversions/slot_supplier_bridge.rs +0 -291
  202. package/src/conversions.rs +0 -618
  203. package/src/errors.rs +0 -38
  204. package/src/helpers.rs +0 -297
  205. package/ts/worker-tuner.ts +0 -193
@@ -0,0 +1,1544 @@
1
+ //! Environment and file-based configuration for Temporal clients.
2
+ //!
3
+ //! This module provides utilities to load Temporal client configuration from TOML files
4
+ //! and environment variables. The configuration supports multiple profiles and various
5
+ //! connection settings including TLS, authentication, and codec configuration.
6
+ //!
7
+ //! ## Environment Variables
8
+ //!
9
+ //! The following environment variables are supported:
10
+ //! - `TEMPORAL_CONFIG_FILE`: Path to the TOML configuration file
11
+ //! - `TEMPORAL_PROFILE`: Profile name to use from the configuration file
12
+ //! - `TEMPORAL_ADDRESS`: Temporal server address
13
+ //! - `TEMPORAL_NAMESPACE`: Temporal namespace
14
+ //! - `TEMPORAL_API_KEY`: API key for authentication
15
+ //! - `TEMPORAL_TLS`: A boolean (`true`/`false`) string to enable/disable TLS.
16
+ //! - `TEMPORAL_TLS_CLIENT_CERT_PATH`: Path to a client certificate file. Mutually exclusive with TEMPORAL_TLS_CLIENT_CERT_DATA, only supply one.
17
+ //! - `TEMPORAL_TLS_CLIENT_CERT_DATA`: The raw client certificate data. Mutually exclusive with TEMPORAL_TLS_CLIENT_CERT_PATH, only supply one.
18
+ //! - `TEMPORAL_TLS_CLIENT_KEY_PATH`: Path to a client key file. Mutually exclusive with TEMPORAL_TLS_CLIENT_KEY_DATA, only supply one.
19
+ //! - `TEMPORAL_TLS_CLIENT_KEY_DATA`: The raw client key data. Mutually exclusive with TEMPORAL_TLS_CLIENT_KEY_PATH, only supply one.
20
+ //! - `TEMPORAL_TLS_SERVER_CA_CERT_PATH`: Path to a server CA certificate file. Mutually exclusive with TEMPORAL_TLS_SERVER_CA_CERT_DATA, only supply one.
21
+ //! - `TEMPORAL_TLS_SERVER_CA_CERT_DATA`: The raw server CA certificate data. Mutually exclusive with TEMPORAL_TLS_SERVER_CA_CERT_PATH, only supply one.
22
+ //! - `TEMPORAL_TLS_SERVER_NAME`: The server name to use for SNI.
23
+ //! - `TEMPORAL_TLS_DISABLE_HOST_VERIFICATION`: A boolean (`true`/`false`) string to disable host verification.
24
+ //! - `TEMPORAL_CODEC_ENDPOINT`: The endpoint for a remote data converter.
25
+ //! - `TEMPORAL_CODEC_AUTH`: The authorization header value for a remote data converter.
26
+ //! - `TEMPORAL_GRPC_META_*`: gRPC metadata headers. Any variables with this prefix will be
27
+ //! converted to gRPC headers. The part of the name after the prefix is converted to the header
28
+ //! name by lowercasing it and replacing underscores with hyphens. For example
29
+ //! `TEMPORAL_GRPC_META_SOME_KEY` becomes `some-key`.
30
+ //!
31
+ //! ## TOML Configuration Format
32
+ //!
33
+ //! ```toml
34
+ //! [profile.default]
35
+ //! address = "localhost:7233"
36
+ //! namespace = "default"
37
+ //! api_key = "your-api-key"
38
+ //!
39
+ //! [profile.default.tls]
40
+ //! disabled = false
41
+ //! client_cert_path = "/path/to/cert.pem"
42
+ //! client_key_path = "/path/to/key.pem"
43
+ //!
44
+ //! [profile.default.codec]
45
+ //! endpoint = "http://localhost:8080"
46
+ //! auth = "Bearer token"
47
+ //!
48
+ //! [profile.default.grpc_meta]
49
+ //! custom_header = "value"
50
+ //! ```
51
+
52
+ use serde::{Deserialize, Serialize};
53
+ use std::{collections::HashMap, fs, path::Path};
54
+ use thiserror::Error;
55
+
56
+ /// Default profile name when none is specified
57
+ pub const DEFAULT_PROFILE: &str = "default";
58
+
59
+ /// Default configuration file name
60
+ pub const DEFAULT_CONFIG_FILE: &str = "temporal.toml";
61
+
62
+ /// Errors that can occur during configuration loading
63
+ #[derive(Debug, Error)]
64
+ pub enum ConfigError {
65
+ #[error("Profile '{0}' not found")]
66
+ ProfileNotFound(String),
67
+
68
+ #[error("Invalid configuration: {0}")]
69
+ InvalidConfig(String),
70
+
71
+ #[error("Configuration loading error: {0}")]
72
+ LoadError(anyhow::Error),
73
+ }
74
+
75
+ impl From<std::str::Utf8Error> for ConfigError {
76
+ fn from(e: std::str::Utf8Error) -> Self {
77
+ Self::LoadError(e.into())
78
+ }
79
+ }
80
+
81
+ impl From<toml::de::Error> for ConfigError {
82
+ fn from(e: toml::de::Error) -> Self {
83
+ Self::LoadError(e.into())
84
+ }
85
+ }
86
+
87
+ impl From<toml::ser::Error> for ConfigError {
88
+ fn from(e: toml::ser::Error) -> Self {
89
+ Self::LoadError(e.into())
90
+ }
91
+ }
92
+
93
+ /// A source for configuration or a TLS certificate/key, from a path or raw data.
94
+ #[derive(Debug, Clone, PartialEq)]
95
+ pub enum DataSource {
96
+ Path(String),
97
+ Data(Vec<u8>),
98
+ }
99
+
100
+ /// ClientConfig represents a client config file.
101
+ #[derive(Debug, Clone, PartialEq, Default)]
102
+ pub struct ClientConfig {
103
+ /// Profiles, keyed by profile name
104
+ pub profiles: HashMap<String, ClientConfigProfile>,
105
+ }
106
+
107
+ /// ClientConfigProfile is profile-level configuration for a client.
108
+ #[derive(Debug, Clone, PartialEq, Default)]
109
+ pub struct ClientConfigProfile {
110
+ /// Client address
111
+ pub address: Option<String>,
112
+
113
+ /// Client namespace
114
+ pub namespace: Option<String>,
115
+
116
+ /// Client API key. If present and TLS field is None or present and not disabled (i.e. without Disabled as true),
117
+ /// TLS is defaulted to enabled.
118
+ pub api_key: Option<String>,
119
+
120
+ /// Optional client TLS config.
121
+ pub tls: Option<ClientConfigTLS>,
122
+
123
+ /// Optional client codec config.
124
+ pub codec: Option<ClientConfigCodec>,
125
+
126
+ /// Client gRPC metadata (aka headers). When loading from TOML and env var, or writing to TOML, the keys are
127
+ /// lowercased and underscores are replaced with hyphens. This is used for deduplicating/overriding too, so manually
128
+ /// set values that are not normalized may not get overridden with [ClientConfigProfile::apply_env_vars].
129
+ pub grpc_meta: HashMap<String, String>,
130
+ }
131
+
132
+ /// ClientConfigTLS is TLS configuration for a client.
133
+ #[derive(Debug, Clone, PartialEq, Default)]
134
+ pub struct ClientConfigTLS {
135
+ /// If true, TLS is explicitly disabled. If false/unset, whether TLS is enabled or not depends on other factors such
136
+ /// as whether this struct is present or None, and whether API key exists (which enables TLS by default).
137
+ pub disabled: bool,
138
+
139
+ /// Client certificate source.
140
+ pub client_cert: Option<DataSource>,
141
+
142
+ /// Client key source.
143
+ pub client_key: Option<DataSource>,
144
+
145
+ /// Server CA certificate source.
146
+ pub server_ca_cert: Option<DataSource>,
147
+
148
+ /// SNI override
149
+ pub server_name: Option<String>,
150
+
151
+ /// True if host verification should be skipped
152
+ pub disable_host_verification: bool,
153
+ }
154
+
155
+ /// Codec configuration for a client
156
+ #[derive(Debug, Clone, PartialEq, Default)]
157
+ pub struct ClientConfigCodec {
158
+ /// Remote endpoint for the codec
159
+ pub endpoint: Option<String>,
160
+
161
+ /// Auth for the codec
162
+ pub auth: Option<String>,
163
+ }
164
+
165
+ /// Options for loading client configuration
166
+ #[derive(Debug, Default)]
167
+ pub struct LoadClientConfigOptions {
168
+ /// Where to load config from. If unset, will try env vars then default path.
169
+ pub config_source: Option<DataSource>,
170
+
171
+ /// If true, will error if there are unrecognized keys
172
+ pub config_file_strict: bool,
173
+ }
174
+
175
+ /// Options for loading a client configuration profile
176
+ #[derive(Debug, Default)]
177
+ pub struct LoadClientConfigProfileOptions {
178
+ /// Where to load config from. If unset, will try env vars then default path.
179
+ pub config_source: Option<DataSource>,
180
+
181
+ /// Specific profile to use
182
+ pub config_file_profile: Option<String>,
183
+
184
+ /// If true, will error if there are unrecognized keys.
185
+ pub config_file_strict: bool,
186
+
187
+ /// Disable loading from file
188
+ pub disable_file: bool,
189
+
190
+ /// Disable loading from environment variables
191
+ pub disable_env: bool,
192
+ }
193
+
194
+ /// Options for parsing TOML configuration
195
+ #[derive(Debug, Default)]
196
+ pub struct ClientConfigFromTOMLOptions {
197
+ /// If true, will error if there are unrecognized keys.
198
+ pub strict: bool,
199
+ }
200
+
201
+ /// Read bytes from a file path, returning Ok(None) if it doesn't exist
202
+ fn read_path_bytes(path: &str) -> Result<Option<Vec<u8>>, ConfigError> {
203
+ if !Path::new(path).exists() {
204
+ return Ok(None);
205
+ }
206
+ match fs::read(path) {
207
+ Ok(data) => Ok(Some(data)),
208
+ Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
209
+ Err(e) => Err(ConfigError::LoadError(e.into())),
210
+ }
211
+ }
212
+
213
+ /// Load client configuration from TOML. Does not load values from environment variables
214
+ /// (but may use environment variables to get which config file to load). This will not fail
215
+ /// if the file does not exist.
216
+ pub fn load_client_config(
217
+ options: LoadClientConfigOptions,
218
+ env_vars: Option<&HashMap<String, String>>,
219
+ ) -> Result<ClientConfig, ConfigError> {
220
+ // Get which bytes to load from TOML
221
+ let toml_data = match options.config_source {
222
+ Some(DataSource::Data(d)) => Some(d),
223
+ Some(DataSource::Path(p)) => read_path_bytes(&p)?,
224
+ None => {
225
+ let file_path = env_vars
226
+ .and_then(|vars| vars.get("TEMPORAL_CONFIG_FILE"))
227
+ .filter(|p| !p.is_empty())
228
+ .cloned()
229
+ .map(Ok)
230
+ .unwrap_or_else(get_default_config_file_path)?;
231
+ read_path_bytes(&file_path)?
232
+ }
233
+ };
234
+
235
+ if let Some(data) = toml_data {
236
+ ClientConfig::from_toml(
237
+ &data,
238
+ ClientConfigFromTOMLOptions {
239
+ strict: options.config_file_strict,
240
+ },
241
+ )
242
+ } else {
243
+ Ok(ClientConfig::default())
244
+ }
245
+ }
246
+
247
+ /// Load a specific client configuration profile
248
+ pub fn load_client_config_profile(
249
+ options: LoadClientConfigProfileOptions,
250
+ env_vars: Option<&HashMap<String, String>>,
251
+ ) -> Result<ClientConfigProfile, ConfigError> {
252
+ if options.disable_file && options.disable_env {
253
+ return Err(ConfigError::InvalidConfig(
254
+ "Cannot disable both file and environment loading".to_string(),
255
+ ));
256
+ }
257
+
258
+ let mut profile = if options.disable_file {
259
+ ClientConfigProfile::default()
260
+ } else {
261
+ // Load the full config
262
+ let config = load_client_config(
263
+ LoadClientConfigOptions {
264
+ config_source: options.config_source,
265
+ config_file_strict: options.config_file_strict,
266
+ },
267
+ env_vars,
268
+ )?;
269
+
270
+ // Determine profile name
271
+ let (profile_name, profile_unset) = match &options.config_file_profile {
272
+ Some(profile) => (profile.clone(), false),
273
+ None => env_vars
274
+ .and_then(|vars| vars.get("TEMPORAL_PROFILE"))
275
+ .filter(|p| !p.is_empty())
276
+ .map(|p| (p.clone(), false))
277
+ .unwrap_or((DEFAULT_PROFILE.to_string(), true)),
278
+ };
279
+
280
+ if let Some(prof) = config.profiles.get(&profile_name) {
281
+ Ok(prof.clone())
282
+ } else if !profile_unset {
283
+ Err(ConfigError::ProfileNotFound(profile_name))
284
+ } else {
285
+ Ok(ClientConfigProfile::default())
286
+ }?
287
+ };
288
+
289
+ // Apply environment variables if not disabled
290
+ if !options.disable_env {
291
+ if let Some(vars) = env_vars {
292
+ profile.apply_env_vars(vars)?;
293
+ }
294
+ }
295
+
296
+ // Apply API key → TLS auto-enabling logic
297
+ profile.apply_api_key_tls_logic();
298
+
299
+ Ok(profile)
300
+ }
301
+
302
+ impl ClientConfig {
303
+ /// Load configuration from a TOML string with options. This will replace all profiles within,
304
+ /// it does not do any form of merging.
305
+ pub fn from_toml(
306
+ toml_bytes: &[u8],
307
+ options: ClientConfigFromTOMLOptions,
308
+ ) -> Result<Self, ConfigError> {
309
+ use strict::StrictTomlClientConfig;
310
+ let toml_str = std::str::from_utf8(toml_bytes)?;
311
+ let mut conf = ClientConfig::default();
312
+ if toml_str.trim().is_empty() {
313
+ return Ok(conf);
314
+ }
315
+
316
+ if options.strict {
317
+ let toml_conf: StrictTomlClientConfig = toml::from_str(toml_str)?;
318
+ toml_conf.apply_to_client_config(&mut conf)?;
319
+ } else {
320
+ let toml_conf: TomlClientConfig = toml::from_str(toml_str)?;
321
+ toml_conf.apply_to_client_config(&mut conf)?;
322
+ }
323
+ Ok(conf)
324
+ }
325
+
326
+ /// Convert configuration to TOML string.
327
+ pub fn to_toml(&self) -> Result<Vec<u8>, ConfigError> {
328
+ let mut toml_conf = TomlClientConfig::new();
329
+ toml_conf.populate_from_client_config(self);
330
+ Ok(toml::to_string_pretty(&toml_conf)?.into_bytes())
331
+ }
332
+ }
333
+
334
+ impl ClientConfigProfile {
335
+ /// Apply environment variable overrides to this profile
336
+ pub fn apply_env_vars(
337
+ &mut self,
338
+ env_vars: &HashMap<String, String>,
339
+ ) -> Result<(), ConfigError> {
340
+ // Apply basic settings
341
+ if let Some(address) = env_vars.get("TEMPORAL_ADDRESS") {
342
+ self.address = Some(address.clone());
343
+ }
344
+ if let Some(namespace) = env_vars.get("TEMPORAL_NAMESPACE") {
345
+ self.namespace = Some(namespace.clone());
346
+ }
347
+ if let Some(api_key) = env_vars.get("TEMPORAL_API_KEY") {
348
+ self.api_key = Some(api_key.clone());
349
+ }
350
+
351
+ self.apply_tls_env_vars(env_vars)?;
352
+ self.apply_codec_env_vars(env_vars)?;
353
+ self.apply_grpc_meta_env_vars(env_vars)?;
354
+
355
+ Ok(())
356
+ }
357
+
358
+ fn apply_tls_env_vars(
359
+ &mut self,
360
+ env_vars: &HashMap<String, String>,
361
+ ) -> Result<(), ConfigError> {
362
+ const TLS_ENV_VARS: &[&str] = &[
363
+ "TEMPORAL_TLS",
364
+ "TEMPORAL_TLS_CLIENT_CERT_PATH",
365
+ "TEMPORAL_TLS_CLIENT_CERT_DATA",
366
+ "TEMPORAL_TLS_CLIENT_KEY_PATH",
367
+ "TEMPORAL_TLS_CLIENT_KEY_DATA",
368
+ "TEMPORAL_TLS_SERVER_CA_CERT_PATH",
369
+ "TEMPORAL_TLS_SERVER_CA_CERT_DATA",
370
+ "TEMPORAL_TLS_SERVER_NAME",
371
+ "TEMPORAL_TLS_DISABLE_HOST_VERIFICATION",
372
+ ];
373
+
374
+ if TLS_ENV_VARS.iter().any(|&k| env_vars.contains_key(k)) && self.tls.is_none() {
375
+ self.tls = Some(ClientConfigTLS::default());
376
+ }
377
+
378
+ if let Some(ref mut tls) = self.tls {
379
+ if let Some(disabled_str) = env_vars.get("TEMPORAL_TLS") {
380
+ if let Some(disabled) = env_var_to_bool(disabled_str) {
381
+ tls.disabled = !disabled;
382
+ }
383
+ }
384
+
385
+ apply_data_source_env_var(
386
+ env_vars,
387
+ "cert",
388
+ "TEMPORAL_TLS_CLIENT_CERT_PATH",
389
+ "TEMPORAL_TLS_CLIENT_CERT_DATA",
390
+ &mut tls.client_cert,
391
+ )?;
392
+ apply_data_source_env_var(
393
+ env_vars,
394
+ "key",
395
+ "TEMPORAL_TLS_CLIENT_KEY_PATH",
396
+ "TEMPORAL_TLS_CLIENT_KEY_DATA",
397
+ &mut tls.client_key,
398
+ )?;
399
+ apply_data_source_env_var(
400
+ env_vars,
401
+ "server CA cert",
402
+ "TEMPORAL_TLS_SERVER_CA_CERT_PATH",
403
+ "TEMPORAL_TLS_SERVER_CA_CERT_DATA",
404
+ &mut tls.server_ca_cert,
405
+ )?;
406
+
407
+ if let Some(v) = env_vars.get("TEMPORAL_TLS_SERVER_NAME") {
408
+ tls.server_name = Some(v.clone());
409
+ }
410
+ if let Some(v) = env_vars.get("TEMPORAL_TLS_DISABLE_HOST_VERIFICATION") {
411
+ if let Some(b) = env_var_to_bool(v) {
412
+ tls.disable_host_verification = b;
413
+ }
414
+ }
415
+ }
416
+ Ok(())
417
+ }
418
+
419
+ fn apply_codec_env_vars(
420
+ &mut self,
421
+ env_vars: &HashMap<String, String>,
422
+ ) -> Result<(), ConfigError> {
423
+ const CODEC_ENV_VARS: &[&str] = &["TEMPORAL_CODEC_ENDPOINT", "TEMPORAL_CODEC_AUTH"];
424
+ if CODEC_ENV_VARS.iter().any(|&k| env_vars.contains_key(k)) && self.codec.is_none() {
425
+ self.codec = Some(ClientConfigCodec::default());
426
+ }
427
+
428
+ if let Some(ref mut codec) = self.codec {
429
+ if let Some(endpoint) = env_vars.get("TEMPORAL_CODEC_ENDPOINT") {
430
+ codec.endpoint = Some(endpoint.clone());
431
+ }
432
+ if let Some(auth) = env_vars.get("TEMPORAL_CODEC_AUTH") {
433
+ codec.auth = Some(auth.clone());
434
+ }
435
+ }
436
+ Ok(())
437
+ }
438
+
439
+ fn apply_grpc_meta_env_vars(
440
+ &mut self,
441
+ env_vars: &HashMap<String, String>,
442
+ ) -> Result<(), ConfigError> {
443
+ for (key, value) in env_vars {
444
+ if let Some(header_name) = key.strip_prefix("TEMPORAL_GRPC_META_") {
445
+ let normalized_name = normalize_grpc_meta_key(header_name);
446
+ if value.is_empty() {
447
+ self.grpc_meta.remove(&normalized_name);
448
+ } else {
449
+ self.grpc_meta.insert(normalized_name, value.clone());
450
+ }
451
+ }
452
+ }
453
+ Ok(())
454
+ }
455
+
456
+ /// Apply automatic TLS enabling when API key is present
457
+ pub fn apply_api_key_tls_logic(&mut self) {
458
+ if self.api_key.is_some() && self.tls.is_none() {
459
+ // If API key is present but no TLS config exists, create one with TLS enabled
460
+ self.tls = Some(ClientConfigTLS::default());
461
+ }
462
+ }
463
+ }
464
+
465
+ /// Helper for applying env vars to a data source.
466
+ fn apply_data_source_env_var(
467
+ env_vars: &HashMap<String, String>,
468
+ name: &str,
469
+ path_var: &str,
470
+ data_var: &str,
471
+ dest: &mut Option<DataSource>,
472
+ ) -> Result<(), ConfigError> {
473
+ let has_path_env = env_vars.contains_key(path_var);
474
+ let has_data_env = env_vars.contains_key(data_var);
475
+
476
+ if has_path_env && has_data_env {
477
+ return Err(ConfigError::InvalidConfig(format!(
478
+ "Cannot specify both {path_var} and {data_var}"
479
+ )));
480
+ }
481
+
482
+ if has_data_env {
483
+ if dest
484
+ .as_ref()
485
+ .is_some_and(|s| matches!(s, DataSource::Path(_)))
486
+ {
487
+ return Err(ConfigError::InvalidConfig(format!(
488
+ "Cannot specify {name} data via {data_var} when {name} path is already specified"
489
+ )));
490
+ }
491
+ *dest = Some(DataSource::Data(
492
+ env_vars.get(data_var).unwrap().clone().into_bytes(),
493
+ ));
494
+ } else if has_path_env {
495
+ if dest
496
+ .as_ref()
497
+ .is_some_and(|s| matches!(s, DataSource::Data(_)))
498
+ {
499
+ return Err(ConfigError::InvalidConfig(format!(
500
+ "Cannot specify {name} path via {path_var} when {name} data is already specified"
501
+ )));
502
+ }
503
+ *dest = Some(DataSource::Path(env_vars.get(path_var).unwrap().clone()));
504
+ }
505
+ Ok(())
506
+ }
507
+
508
+ /// Parse a boolean value from string (supports "true", "false", "1", "0")
509
+ fn env_var_to_bool(s: &str) -> Option<bool> {
510
+ match s.to_lowercase().as_str() {
511
+ "true" | "1" => Some(true),
512
+ "false" | "0" => Some(false),
513
+ _ => None,
514
+ }
515
+ }
516
+
517
+ /// Normalize gRPC metadata key (lowercase and replace underscores with hyphens)
518
+ fn normalize_grpc_meta_key(key: &str) -> String {
519
+ key.to_lowercase().replace('_', "-")
520
+ }
521
+
522
+ /// Get the default configuration file path
523
+ fn get_default_config_file_path() -> Result<String, ConfigError> {
524
+ // Try to get user config directory
525
+ let config_dir = dirs::config_dir()
526
+ .ok_or_else(|| ConfigError::InvalidConfig("failed getting user config dir".to_string()))?;
527
+
528
+ let path = config_dir.join("temporalio").join(DEFAULT_CONFIG_FILE);
529
+ Ok(path.to_string_lossy().to_string())
530
+ }
531
+
532
+ #[derive(Debug, Clone, Serialize, Deserialize)]
533
+ struct TomlClientConfig {
534
+ #[serde(default, rename = "profile")]
535
+ profiles: HashMap<String, TomlClientConfigProfile>,
536
+ }
537
+
538
+ impl TomlClientConfig {
539
+ fn new() -> Self {
540
+ Self {
541
+ profiles: HashMap::new(),
542
+ }
543
+ }
544
+
545
+ fn apply_to_client_config(&self, conf: &mut ClientConfig) -> Result<(), ConfigError> {
546
+ conf.profiles = HashMap::with_capacity(self.profiles.len());
547
+ for (k, v) in &self.profiles {
548
+ conf.profiles.insert(k.clone(), v.to_client_config()?);
549
+ }
550
+ Ok(())
551
+ }
552
+
553
+ fn populate_from_client_config(&mut self, conf: &ClientConfig) {
554
+ self.profiles = HashMap::with_capacity(conf.profiles.len());
555
+ for (k, v) in &conf.profiles {
556
+ let mut prof = TomlClientConfigProfile::new();
557
+ prof.populate_from_client_config(v);
558
+ self.profiles.insert(k.clone(), prof);
559
+ }
560
+ }
561
+ }
562
+
563
+ #[derive(Debug, Clone, Serialize, Deserialize)]
564
+ struct TomlClientConfigProfile {
565
+ #[serde(skip_serializing_if = "Option::is_none")]
566
+ address: Option<String>,
567
+
568
+ #[serde(skip_serializing_if = "Option::is_none")]
569
+ namespace: Option<String>,
570
+
571
+ #[serde(skip_serializing_if = "Option::is_none")]
572
+ api_key: Option<String>,
573
+
574
+ #[serde(skip_serializing_if = "Option::is_none")]
575
+ tls: Option<TomlClientConfigTLS>,
576
+
577
+ #[serde(skip_serializing_if = "Option::is_none")]
578
+ codec: Option<TomlClientConfigCodec>,
579
+
580
+ #[serde(default, skip_serializing_if = "HashMap::is_empty")]
581
+ grpc_meta: HashMap<String, String>,
582
+ }
583
+
584
+ impl TomlClientConfigProfile {
585
+ fn new() -> Self {
586
+ Self {
587
+ address: None,
588
+ namespace: None,
589
+ api_key: None,
590
+ tls: None,
591
+ codec: None,
592
+ grpc_meta: HashMap::new(),
593
+ }
594
+ }
595
+ fn to_client_config(&self) -> Result<ClientConfigProfile, ConfigError> {
596
+ let mut ret = ClientConfigProfile {
597
+ address: self.address.clone(),
598
+ namespace: self.namespace.clone(),
599
+ api_key: self.api_key.clone(),
600
+ tls: self
601
+ .tls
602
+ .as_ref()
603
+ .map(|tls| tls.to_client_config())
604
+ .transpose()?,
605
+ codec: self.codec.as_ref().map(|codec| codec.to_client_config()),
606
+ grpc_meta: HashMap::new(),
607
+ };
608
+
609
+ if !self.grpc_meta.is_empty() {
610
+ ret.grpc_meta = HashMap::with_capacity(self.grpc_meta.len());
611
+ for (k, v) in &self.grpc_meta {
612
+ ret.grpc_meta.insert(normalize_grpc_meta_key(k), v.clone());
613
+ }
614
+ }
615
+ Ok(ret)
616
+ }
617
+
618
+ fn populate_from_client_config(&mut self, conf: &ClientConfigProfile) {
619
+ self.address = conf.address.clone();
620
+ self.namespace = conf.namespace.clone();
621
+ self.api_key = conf.api_key.clone();
622
+
623
+ if let Some(ref tls_conf) = conf.tls {
624
+ let mut toml_tls = TomlClientConfigTLS::new();
625
+ toml_tls.populate_from_client_config(tls_conf);
626
+ self.tls = Some(toml_tls);
627
+ } else {
628
+ self.tls = None;
629
+ }
630
+
631
+ if let Some(ref codec_conf) = conf.codec {
632
+ let mut toml_codec = TomlClientConfigCodec::new();
633
+ toml_codec.populate_from_client_config(codec_conf);
634
+ self.codec = Some(toml_codec);
635
+ } else {
636
+ self.codec = None;
637
+ }
638
+
639
+ if !conf.grpc_meta.is_empty() {
640
+ self.grpc_meta = HashMap::with_capacity(conf.grpc_meta.len());
641
+ for (k, v) in &conf.grpc_meta {
642
+ self.grpc_meta.insert(normalize_grpc_meta_key(k), v.clone());
643
+ }
644
+ } else {
645
+ self.grpc_meta.clear();
646
+ }
647
+ }
648
+ }
649
+
650
+ #[derive(Debug, Clone, Serialize, Deserialize)]
651
+ struct TomlClientConfigTLS {
652
+ #[serde(default, skip_serializing_if = "std::ops::Not::not")]
653
+ disabled: bool,
654
+
655
+ #[serde(skip_serializing_if = "Option::is_none")]
656
+ client_cert_path: Option<String>,
657
+
658
+ #[serde(skip_serializing_if = "Option::is_none")]
659
+ client_cert_data: Option<String>, // String in TOML, not Vec<u8>
660
+
661
+ #[serde(skip_serializing_if = "Option::is_none")]
662
+ client_key_path: Option<String>,
663
+
664
+ #[serde(skip_serializing_if = "Option::is_none")]
665
+ client_key_data: Option<String>, // String in TOML, not Vec<u8>
666
+
667
+ #[serde(skip_serializing_if = "Option::is_none")]
668
+ server_ca_cert_path: Option<String>,
669
+
670
+ #[serde(skip_serializing_if = "Option::is_none")]
671
+ server_ca_cert_data: Option<String>, // String in TOML, not Vec<u8>
672
+
673
+ #[serde(skip_serializing_if = "Option::is_none")]
674
+ server_name: Option<String>,
675
+
676
+ #[serde(default, skip_serializing_if = "std::ops::Not::not")]
677
+ disable_host_verification: bool,
678
+ }
679
+
680
+ impl TomlClientConfigTLS {
681
+ fn new() -> Self {
682
+ Self {
683
+ disabled: false,
684
+ client_cert_path: None,
685
+ client_cert_data: None,
686
+ client_key_path: None,
687
+ client_key_data: None,
688
+ server_ca_cert_path: None,
689
+ server_ca_cert_data: None,
690
+ server_name: None,
691
+ disable_host_verification: false,
692
+ }
693
+ }
694
+
695
+ fn to_client_config(&self) -> Result<ClientConfigTLS, ConfigError> {
696
+ if self.client_cert_path.is_some() && self.client_cert_data.is_some() {
697
+ return Err(ConfigError::InvalidConfig(
698
+ "Cannot specify both client_cert_path and client_cert_data".to_string(),
699
+ ));
700
+ }
701
+ if self.client_key_path.is_some() && self.client_key_data.is_some() {
702
+ return Err(ConfigError::InvalidConfig(
703
+ "Cannot specify both client_key_path and client_key_data".to_string(),
704
+ ));
705
+ }
706
+ if self.server_ca_cert_path.is_some() && self.server_ca_cert_data.is_some() {
707
+ return Err(ConfigError::InvalidConfig(
708
+ "Cannot specify both server_ca_cert_path and server_ca_cert_data".to_string(),
709
+ ));
710
+ }
711
+
712
+ let string_to_bytes = |s: &Option<String>| {
713
+ s.as_ref().and_then(|val| {
714
+ if val.is_empty() {
715
+ None
716
+ } else {
717
+ Some(val.as_bytes().to_vec())
718
+ }
719
+ })
720
+ };
721
+
722
+ Ok(ClientConfigTLS {
723
+ disabled: self.disabled,
724
+ client_cert: self
725
+ .client_cert_path
726
+ .clone()
727
+ .map(DataSource::Path)
728
+ .or_else(|| string_to_bytes(&self.client_cert_data).map(DataSource::Data)),
729
+ client_key: self
730
+ .client_key_path
731
+ .clone()
732
+ .map(DataSource::Path)
733
+ .or_else(|| string_to_bytes(&self.client_key_data).map(DataSource::Data)),
734
+ server_ca_cert: self
735
+ .server_ca_cert_path
736
+ .clone()
737
+ .map(DataSource::Path)
738
+ .or_else(|| string_to_bytes(&self.server_ca_cert_data).map(DataSource::Data)),
739
+ server_name: self.server_name.clone(),
740
+ disable_host_verification: self.disable_host_verification,
741
+ })
742
+ }
743
+
744
+ fn populate_from_client_config(&mut self, conf: &ClientConfigTLS) {
745
+ self.disabled = conf.disabled;
746
+ if let Some(ref cert_source) = conf.client_cert {
747
+ match cert_source {
748
+ DataSource::Path(p) => self.client_cert_path = Some(p.clone()),
749
+ DataSource::Data(d) => {
750
+ self.client_cert_data = Some(String::from_utf8_lossy(d).into_owned())
751
+ }
752
+ }
753
+ }
754
+ if let Some(ref key_source) = conf.client_key {
755
+ match key_source {
756
+ DataSource::Path(p) => self.client_key_path = Some(p.clone()),
757
+ DataSource::Data(d) => {
758
+ self.client_key_data = Some(String::from_utf8_lossy(d).into_owned())
759
+ }
760
+ }
761
+ }
762
+ if let Some(ref ca_source) = conf.server_ca_cert {
763
+ match ca_source {
764
+ DataSource::Path(p) => self.server_ca_cert_path = Some(p.clone()),
765
+ DataSource::Data(d) => {
766
+ self.server_ca_cert_data = Some(String::from_utf8_lossy(d).into_owned())
767
+ }
768
+ }
769
+ }
770
+ self.server_name = conf.server_name.clone();
771
+ self.disable_host_verification = conf.disable_host_verification;
772
+ }
773
+ }
774
+
775
+ #[derive(Debug, Clone, Serialize, Deserialize)]
776
+ struct TomlClientConfigCodec {
777
+ #[serde(skip_serializing_if = "Option::is_none")]
778
+ endpoint: Option<String>,
779
+
780
+ #[serde(skip_serializing_if = "Option::is_none")]
781
+ auth: Option<String>,
782
+ }
783
+
784
+ impl TomlClientConfigCodec {
785
+ fn new() -> Self {
786
+ Self {
787
+ endpoint: None,
788
+ auth: None,
789
+ }
790
+ }
791
+ fn to_client_config(&self) -> ClientConfigCodec {
792
+ ClientConfigCodec {
793
+ endpoint: self.endpoint.clone(),
794
+ auth: self.auth.clone(),
795
+ }
796
+ }
797
+
798
+ fn populate_from_client_config(&mut self, conf: &ClientConfigCodec) {
799
+ self.endpoint = conf.endpoint.clone();
800
+ self.auth = conf.auth.clone();
801
+ }
802
+ }
803
+
804
+ mod strict {
805
+ use super::{
806
+ ClientConfig, ClientConfigCodec, ClientConfigProfile, ClientConfigTLS, ConfigError,
807
+ DataSource, normalize_grpc_meta_key,
808
+ };
809
+ use serde::Deserialize;
810
+ use std::collections::HashMap;
811
+
812
+ #[derive(Debug, Deserialize)]
813
+ #[serde(deny_unknown_fields)]
814
+ pub(crate) struct StrictTomlClientConfig {
815
+ #[serde(default, rename = "profile")]
816
+ profiles: HashMap<String, StrictTomlClientConfigProfile>,
817
+ }
818
+
819
+ impl StrictTomlClientConfig {
820
+ pub(crate) fn apply_to_client_config(
821
+ self,
822
+ conf: &mut ClientConfig,
823
+ ) -> Result<(), ConfigError> {
824
+ conf.profiles = HashMap::with_capacity(self.profiles.len());
825
+ for (k, v) in self.profiles {
826
+ conf.profiles.insert(k, v.into_client_config()?);
827
+ }
828
+ Ok(())
829
+ }
830
+ }
831
+
832
+ #[derive(Debug, Deserialize)]
833
+ #[serde(deny_unknown_fields)]
834
+ struct StrictTomlClientConfigProfile {
835
+ #[serde(default)]
836
+ address: Option<String>,
837
+ #[serde(default)]
838
+ namespace: Option<String>,
839
+ #[serde(default)]
840
+ api_key: Option<String>,
841
+ #[serde(default)]
842
+ tls: Option<StrictTomlClientConfigTLS>,
843
+ #[serde(default)]
844
+ codec: Option<StrictTomlClientConfigCodec>,
845
+ #[serde(default)]
846
+ grpc_meta: HashMap<String, String>,
847
+ }
848
+
849
+ impl StrictTomlClientConfigProfile {
850
+ fn into_client_config(self) -> Result<ClientConfigProfile, ConfigError> {
851
+ let mut ret = ClientConfigProfile {
852
+ address: self.address,
853
+ namespace: self.namespace,
854
+ api_key: self.api_key,
855
+ tls: self.tls.map(|tls| tls.into_client_config()).transpose()?,
856
+ codec: self.codec.map(|codec| codec.into_client_config()),
857
+ grpc_meta: HashMap::new(),
858
+ };
859
+
860
+ if !self.grpc_meta.is_empty() {
861
+ ret.grpc_meta = HashMap::with_capacity(self.grpc_meta.len());
862
+ for (k, v) in self.grpc_meta {
863
+ ret.grpc_meta.insert(normalize_grpc_meta_key(&k), v);
864
+ }
865
+ }
866
+ Ok(ret)
867
+ }
868
+ }
869
+
870
+ #[derive(Debug, Deserialize)]
871
+ #[serde(deny_unknown_fields)]
872
+ struct StrictTomlClientConfigTLS {
873
+ #[serde(default)]
874
+ disabled: bool,
875
+ #[serde(default)]
876
+ client_cert_path: Option<String>,
877
+ #[serde(default)]
878
+ client_cert_data: Option<String>,
879
+ #[serde(default)]
880
+ client_key_path: Option<String>,
881
+ #[serde(default)]
882
+ client_key_data: Option<String>,
883
+ #[serde(default)]
884
+ server_ca_cert_path: Option<String>,
885
+ #[serde(default)]
886
+ server_ca_cert_data: Option<String>,
887
+ #[serde(default)]
888
+ server_name: Option<String>,
889
+ #[serde(default)]
890
+ disable_host_verification: bool,
891
+ }
892
+
893
+ impl StrictTomlClientConfigTLS {
894
+ fn into_client_config(self) -> Result<ClientConfigTLS, ConfigError> {
895
+ if self.client_cert_path.is_some() && self.client_cert_data.is_some() {
896
+ return Err(ConfigError::InvalidConfig(
897
+ "Cannot specify both client_cert_path and client_cert_data".to_string(),
898
+ ));
899
+ }
900
+ if self.client_key_path.is_some() && self.client_key_data.is_some() {
901
+ return Err(ConfigError::InvalidConfig(
902
+ "Cannot specify both client_key_path and client_key_data".to_string(),
903
+ ));
904
+ }
905
+ if self.server_ca_cert_path.is_some() && self.server_ca_cert_data.is_some() {
906
+ return Err(ConfigError::InvalidConfig(
907
+ "Cannot specify both server_ca_cert_path and server_ca_cert_data".to_string(),
908
+ ));
909
+ }
910
+
911
+ let string_to_bytes = |s: Option<String>| {
912
+ s.and_then(|val| {
913
+ if val.is_empty() {
914
+ None
915
+ } else {
916
+ Some(val.as_bytes().to_vec())
917
+ }
918
+ })
919
+ };
920
+
921
+ Ok(ClientConfigTLS {
922
+ disabled: self.disabled,
923
+ client_cert: self
924
+ .client_cert_path
925
+ .map(DataSource::Path)
926
+ .or_else(|| string_to_bytes(self.client_cert_data).map(DataSource::Data)),
927
+ client_key: self
928
+ .client_key_path
929
+ .map(DataSource::Path)
930
+ .or_else(|| string_to_bytes(self.client_key_data).map(DataSource::Data)),
931
+ server_ca_cert: self
932
+ .server_ca_cert_path
933
+ .map(DataSource::Path)
934
+ .or_else(|| string_to_bytes(self.server_ca_cert_data).map(DataSource::Data)),
935
+ server_name: self.server_name,
936
+ disable_host_verification: self.disable_host_verification,
937
+ })
938
+ }
939
+ }
940
+
941
+ #[derive(Debug, Deserialize)]
942
+ #[serde(deny_unknown_fields)]
943
+ struct StrictTomlClientConfigCodec {
944
+ #[serde(default)]
945
+ endpoint: Option<String>,
946
+ #[serde(default)]
947
+ auth: Option<String>,
948
+ }
949
+
950
+ impl StrictTomlClientConfigCodec {
951
+ fn into_client_config(self) -> ClientConfigCodec {
952
+ ClientConfigCodec {
953
+ endpoint: self.endpoint,
954
+ auth: self.auth,
955
+ }
956
+ }
957
+ }
958
+ }
959
+
960
+ #[cfg(test)]
961
+ mod tests {
962
+ use super::*;
963
+ use std::io::Write;
964
+ use tempfile::NamedTempFile;
965
+
966
+ #[test]
967
+ fn test_client_config_toml_multiple_profiles() {
968
+ let toml_str = r#"
969
+ [profile.default]
970
+ address = "localhost:7233"
971
+ namespace = "default"
972
+ api_key = "test-key"
973
+
974
+ [profile.default.tls]
975
+ disabled = false
976
+ client_cert_path = "/path/to/cert"
977
+
978
+ [profile.prod]
979
+ address = "prod.temporal.io:7233"
980
+ namespace = "production"
981
+ "#;
982
+
983
+ let config = ClientConfig::from_toml(toml_str.as_bytes(), Default::default()).unwrap();
984
+
985
+ let default_profile = config.profiles.get("default").unwrap();
986
+ assert_eq!(default_profile.address.as_ref().unwrap(), "localhost:7233");
987
+ assert_eq!(default_profile.namespace.as_ref().unwrap(), "default");
988
+ assert_eq!(default_profile.api_key.as_ref().unwrap(), "test-key");
989
+
990
+ let tls = default_profile.tls.as_ref().unwrap();
991
+ assert!(!tls.disabled);
992
+ assert_eq!(
993
+ tls.client_cert,
994
+ Some(DataSource::Path("/path/to/cert".to_string()))
995
+ );
996
+
997
+ let prod_profile = config.profiles.get("prod").unwrap();
998
+ assert_eq!(
999
+ prod_profile.address.as_ref().unwrap(),
1000
+ "prod.temporal.io:7233"
1001
+ );
1002
+ assert_eq!(prod_profile.namespace.as_ref().unwrap(), "production");
1003
+ }
1004
+
1005
+ #[test]
1006
+ fn test_client_config_toml_roundtrip() {
1007
+ let mut prof = ClientConfigProfile {
1008
+ address: Some("addr".to_string()),
1009
+ namespace: Some("ns".to_string()),
1010
+ api_key: Some("key".to_string()),
1011
+ ..Default::default()
1012
+ };
1013
+ prof.grpc_meta.insert("k".to_string(), "v".to_string());
1014
+
1015
+ let tls = ClientConfigTLS {
1016
+ client_cert: Some(DataSource::Data(b"cert".to_vec())),
1017
+ server_ca_cert: Some(DataSource::Data(b"ca".to_vec())),
1018
+ ..Default::default()
1019
+ };
1020
+ prof.tls = Some(tls);
1021
+
1022
+ let mut conf = ClientConfig::default();
1023
+ conf.profiles.insert("default".to_string(), prof);
1024
+
1025
+ let toml_bytes = conf.to_toml().unwrap();
1026
+ let new_conf = ClientConfig::from_toml(&toml_bytes, Default::default()).unwrap();
1027
+ assert_eq!(conf, new_conf);
1028
+ }
1029
+
1030
+ #[test]
1031
+ fn test_load_client_config_profile_from_file() {
1032
+ // Create temporary file with test data
1033
+ let mut temp_file = NamedTempFile::new().unwrap();
1034
+ writeln!(
1035
+ temp_file,
1036
+ r#"
1037
+ [profile.default]
1038
+ address = "my-address"
1039
+ namespace = "my-namespace"
1040
+ "#
1041
+ )
1042
+ .unwrap();
1043
+
1044
+ // Test explicit file path
1045
+ let options = LoadClientConfigProfileOptions {
1046
+ config_source: Some(DataSource::Path(
1047
+ temp_file.path().to_string_lossy().to_string(),
1048
+ )),
1049
+ ..Default::default()
1050
+ };
1051
+
1052
+ let profile = load_client_config_profile(options, None).unwrap();
1053
+ assert_eq!(profile.address.as_ref().unwrap(), "my-address");
1054
+ assert_eq!(profile.namespace.as_ref().unwrap(), "my-namespace");
1055
+ }
1056
+
1057
+ #[test]
1058
+ fn test_load_client_config_profile_from_env_file_path() {
1059
+ // Create temporary file
1060
+ let mut temp_file = NamedTempFile::new().unwrap();
1061
+ writeln!(
1062
+ temp_file,
1063
+ r#"
1064
+ [profile.default]
1065
+ address = "my-address"
1066
+ namespace = "my-namespace"
1067
+ "#
1068
+ )
1069
+ .unwrap();
1070
+
1071
+ // Test loading via TEMPORAL_CONFIG_FILE env var
1072
+ let mut vars = HashMap::new();
1073
+ vars.insert(
1074
+ "TEMPORAL_CONFIG_FILE".to_string(),
1075
+ temp_file.path().to_string_lossy().to_string(),
1076
+ );
1077
+
1078
+ let options = LoadClientConfigProfileOptions {
1079
+ ..Default::default()
1080
+ };
1081
+
1082
+ let profile = load_client_config_profile(options, Some(&vars)).unwrap();
1083
+ assert_eq!(profile.address.as_ref().unwrap(), "my-address");
1084
+ assert_eq!(profile.namespace.as_ref().unwrap(), "my-namespace");
1085
+ }
1086
+
1087
+ #[test]
1088
+ fn test_load_client_config_profile_with_env_overrides() {
1089
+ let toml_str = r#"
1090
+ [profile.default]
1091
+ address = "my-address"
1092
+ namespace = "my-namespace"
1093
+ api_key = "my-api-key"
1094
+
1095
+ [profile.default.tls]
1096
+ disabled = true
1097
+ client_cert_path = "my-client-cert-path"
1098
+ client_key_path = "my-client-key-path"
1099
+ server_name = "my-server-name"
1100
+ disable_host_verification = true
1101
+
1102
+ [profile.default.codec]
1103
+ endpoint = "my-codec-endpoint"
1104
+ auth = "my-codec-auth"
1105
+
1106
+ [profile.default.grpc_meta]
1107
+ some-header = "some-value"
1108
+ some-other-header = "some-value2"
1109
+ "#;
1110
+ let mut vars = HashMap::new();
1111
+ vars.insert("TEMPORAL_ADDRESS".to_string(), "my-address-new".to_string());
1112
+ vars.insert(
1113
+ "TEMPORAL_NAMESPACE".to_string(),
1114
+ "my-namespace-new".to_string(),
1115
+ );
1116
+ vars.insert("TEMPORAL_API_KEY".to_string(), "my-api-key-new".to_string());
1117
+ vars.insert("TEMPORAL_TLS".to_string(), "true".to_string());
1118
+ vars.insert(
1119
+ "TEMPORAL_TLS_CLIENT_CERT_PATH".to_string(),
1120
+ "my-client-cert-path-new".to_string(),
1121
+ );
1122
+ vars.insert(
1123
+ "TEMPORAL_TLS_CLIENT_KEY_PATH".to_string(),
1124
+ "my-client-key-path-new".to_string(),
1125
+ );
1126
+ vars.insert(
1127
+ "TEMPORAL_TLS_SERVER_NAME".to_string(),
1128
+ "my-server-name-new".to_string(),
1129
+ );
1130
+ vars.insert(
1131
+ "TEMPORAL_TLS_DISABLE_HOST_VERIFICATION".to_string(),
1132
+ "false".to_string(),
1133
+ );
1134
+ vars.insert(
1135
+ "TEMPORAL_CODEC_ENDPOINT".to_string(),
1136
+ "my-codec-endpoint-new".to_string(),
1137
+ );
1138
+ vars.insert(
1139
+ "TEMPORAL_CODEC_AUTH".to_string(),
1140
+ "my-codec-auth-new".to_string(),
1141
+ );
1142
+ vars.insert(
1143
+ "TEMPORAL_GRPC_META_SOME_HEADER".to_string(),
1144
+ "some-value-new".to_string(),
1145
+ );
1146
+ vars.insert(
1147
+ "TEMPORAL_GRPC_META_SOME_THIRD_HEADER".to_string(),
1148
+ "some-value3-new".to_string(),
1149
+ );
1150
+ vars.insert(
1151
+ "TEMPORAL_GRPC_META_some_value4".to_string(),
1152
+ "some-value4-new".to_string(),
1153
+ );
1154
+
1155
+ let options = LoadClientConfigProfileOptions {
1156
+ config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
1157
+ config_file_profile: Some("default".to_string()),
1158
+ ..Default::default()
1159
+ };
1160
+
1161
+ let profile = load_client_config_profile(options, Some(&vars)).unwrap();
1162
+ assert_eq!(profile.address.as_ref().unwrap(), "my-address-new");
1163
+ assert_eq!(profile.namespace.as_ref().unwrap(), "my-namespace-new");
1164
+ assert_eq!(profile.api_key.as_ref().unwrap(), "my-api-key-new");
1165
+
1166
+ let tls = profile.tls.as_ref().unwrap();
1167
+ assert!(!tls.disabled); // TLS enabled via env var
1168
+ assert_eq!(
1169
+ tls.client_cert,
1170
+ Some(DataSource::Path("my-client-cert-path-new".to_string()))
1171
+ );
1172
+ assert_eq!(
1173
+ tls.client_key,
1174
+ Some(DataSource::Path("my-client-key-path-new".to_string()))
1175
+ );
1176
+ assert_eq!(tls.server_name.as_ref().unwrap(), "my-server-name-new");
1177
+ assert!(!tls.disable_host_verification);
1178
+ let codec = profile.codec.as_ref().unwrap();
1179
+ assert_eq!(codec.endpoint.as_ref().unwrap(), "my-codec-endpoint-new");
1180
+ assert_eq!(codec.auth.as_ref().unwrap(), "my-codec-auth-new");
1181
+ assert_eq!(
1182
+ profile.grpc_meta.get("some-header").unwrap(),
1183
+ "some-value-new"
1184
+ );
1185
+ assert_eq!(
1186
+ profile.grpc_meta.get("some-other-header").unwrap(),
1187
+ "some-value2"
1188
+ );
1189
+ assert_eq!(
1190
+ profile.grpc_meta.get("some-third-header").unwrap(),
1191
+ "some-value3-new"
1192
+ );
1193
+ assert_eq!(
1194
+ profile.grpc_meta.get("some-value4").unwrap(),
1195
+ "some-value4-new"
1196
+ );
1197
+ }
1198
+
1199
+ #[test]
1200
+ fn test_client_config_toml_full() {
1201
+ let toml_str = r#"
1202
+ [profile.foo]
1203
+ address = "my-address"
1204
+ namespace = "my-namespace"
1205
+ api_key = "my-api-key"
1206
+ some_future_key = "some future value not handled"
1207
+
1208
+ [profile.foo.tls]
1209
+ disabled = true
1210
+ client_cert_path = "my-client-cert-path"
1211
+ client_key_path = "my-client-key-path"
1212
+ server_ca_cert_path = "my-server-ca-cert-path"
1213
+ server_name = "my-server-name"
1214
+ disable_host_verification = true
1215
+
1216
+ [profile.foo.codec]
1217
+ endpoint = "my-endpoint"
1218
+ auth = "my-auth"
1219
+
1220
+ [profile.foo.grpc_meta]
1221
+ sOme-hEader_key = "some-value"
1222
+ "#;
1223
+
1224
+ let config = ClientConfig::from_toml(toml_str.as_bytes(), Default::default()).unwrap();
1225
+ let profile = config.profiles.get("foo").unwrap();
1226
+
1227
+ assert_eq!(profile.address.as_ref().unwrap(), "my-address");
1228
+ assert_eq!(profile.namespace.as_ref().unwrap(), "my-namespace");
1229
+ assert_eq!(profile.api_key.as_ref().unwrap(), "my-api-key");
1230
+
1231
+ let codec = profile.codec.as_ref().unwrap();
1232
+ assert_eq!(codec.endpoint.as_ref().unwrap(), "my-endpoint");
1233
+ assert_eq!(codec.auth.as_ref().unwrap(), "my-auth");
1234
+
1235
+ let tls = profile.tls.as_ref().unwrap();
1236
+ assert!(tls.disabled);
1237
+ assert_eq!(
1238
+ tls.client_cert,
1239
+ Some(DataSource::Path("my-client-cert-path".to_string()))
1240
+ );
1241
+ assert_eq!(
1242
+ tls.client_key,
1243
+ Some(DataSource::Path("my-client-key-path".to_string()))
1244
+ );
1245
+ assert_eq!(
1246
+ tls.server_ca_cert,
1247
+ Some(DataSource::Path("my-server-ca-cert-path".to_string()))
1248
+ );
1249
+ assert_eq!(tls.server_name.as_ref().unwrap(), "my-server-name");
1250
+ assert!(tls.disable_host_verification);
1251
+
1252
+ // Note: gRPC meta keys get normalized
1253
+ assert_eq!(profile.grpc_meta.len(), 1);
1254
+ assert_eq!(
1255
+ profile.grpc_meta.get("some-header-key").unwrap(),
1256
+ "some-value"
1257
+ );
1258
+
1259
+ // Test round-trip serialization
1260
+ let toml_out = config.to_toml().unwrap();
1261
+ let config2 = ClientConfig::from_toml(&toml_out, Default::default()).unwrap();
1262
+ assert_eq!(config, config2);
1263
+ }
1264
+
1265
+ #[test]
1266
+ fn test_client_config_toml_partial() {
1267
+ let toml_str = r#"
1268
+ [profile.foo]
1269
+ api_key = "my-api-key"
1270
+
1271
+ [profile.foo.tls]
1272
+ "#;
1273
+
1274
+ let config = ClientConfig::from_toml(toml_str.as_bytes(), Default::default()).unwrap();
1275
+ let profile = config.profiles.get("foo").unwrap();
1276
+
1277
+ assert!(profile.address.is_none());
1278
+ assert!(profile.namespace.is_none());
1279
+ assert_eq!(profile.api_key.as_ref().unwrap(), "my-api-key");
1280
+ assert!(profile.codec.is_none());
1281
+ assert!(profile.tls.is_some());
1282
+
1283
+ let tls = profile.tls.as_ref().unwrap();
1284
+ assert!(!tls.disabled); // default value
1285
+ assert!(tls.client_cert.is_none());
1286
+ }
1287
+
1288
+ #[test]
1289
+ fn test_client_config_toml_empty() {
1290
+ let config = ClientConfig::from_toml("".as_bytes(), Default::default()).unwrap();
1291
+ assert!(config.profiles.is_empty());
1292
+
1293
+ // Test round-trip
1294
+ let toml_out = config.to_toml().unwrap();
1295
+ let config2 = ClientConfig::from_toml(&toml_out, Default::default()).unwrap();
1296
+ assert_eq!(config, config2);
1297
+ }
1298
+
1299
+ #[test]
1300
+ fn test_profile_not_found() {
1301
+ let toml_str = r#"
1302
+ [profile.existing]
1303
+ address = "localhost:7233"
1304
+ "#;
1305
+
1306
+ let options = LoadClientConfigProfileOptions {
1307
+ config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
1308
+ config_file_profile: Some("nonexistent".to_string()),
1309
+ ..Default::default()
1310
+ };
1311
+
1312
+ // Should error because we explicitly asked for a profile that doesn't exist
1313
+ let result = load_client_config_profile(options, None);
1314
+ assert!(result.is_err());
1315
+ assert!(matches!(
1316
+ result.unwrap_err(),
1317
+ ConfigError::ProfileNotFound(p) if p == "nonexistent"
1318
+ ));
1319
+ }
1320
+
1321
+ #[test]
1322
+ fn test_client_config_toml_strict_unrecognized_field() {
1323
+ let toml_str = r#"
1324
+ [profile.default]
1325
+ unrecognized_field = "is-bad"
1326
+ "#;
1327
+ let err = ClientConfig::from_toml(
1328
+ toml_str.as_bytes(),
1329
+ ClientConfigFromTOMLOptions { strict: true },
1330
+ )
1331
+ .unwrap_err();
1332
+ let err_str = err.to_string();
1333
+ assert!(err_str.contains("unrecognized_field"));
1334
+ }
1335
+
1336
+ #[test]
1337
+ fn test_client_config_toml_strict_unrecognized_table() {
1338
+ let toml_str = r#"
1339
+ [unrecognized_table]
1340
+ foo = "bar"
1341
+ "#;
1342
+ let err = ClientConfig::from_toml(
1343
+ toml_str.as_bytes(),
1344
+ ClientConfigFromTOMLOptions { strict: true },
1345
+ )
1346
+ .unwrap_err();
1347
+ let err_str = err.to_string();
1348
+ assert!(err_str.contains("unrecognized_table"));
1349
+ }
1350
+
1351
+ #[test]
1352
+ fn test_client_config_both_path_and_data_fails() {
1353
+ // Env vars
1354
+ let mut vars = HashMap::new();
1355
+ vars.insert(
1356
+ "TEMPORAL_TLS_CLIENT_CERT_PATH".to_string(),
1357
+ "some-path".to_string(),
1358
+ );
1359
+ vars.insert(
1360
+ "TEMPORAL_TLS_CLIENT_CERT_DATA".to_string(),
1361
+ "some-data".to_string(),
1362
+ );
1363
+ let options = LoadClientConfigProfileOptions {
1364
+ ..Default::default()
1365
+ };
1366
+ let err = load_client_config_profile(options, Some(&vars)).unwrap_err();
1367
+ assert!(matches!(err, ConfigError::InvalidConfig(_)));
1368
+ assert!(err.to_string().contains("Cannot specify both"));
1369
+
1370
+ // TOML
1371
+ let toml_str = r#"
1372
+ [profile.default]
1373
+ [profile.default.tls]
1374
+ client_cert_path = "some-path"
1375
+ client_cert_data = "some-data"
1376
+ "#;
1377
+ let err = ClientConfig::from_toml(toml_str.as_bytes(), Default::default()).unwrap_err();
1378
+ assert!(matches!(err, ConfigError::InvalidConfig(_)));
1379
+ assert!(err.to_string().contains("Cannot specify both"));
1380
+ }
1381
+
1382
+ #[test]
1383
+ fn test_client_config_path_data_conflict_across_sources() {
1384
+ // Path in TOML, data in env var should fail
1385
+ let toml_str = r#"
1386
+ [profile.default]
1387
+ [profile.default.tls]
1388
+ client_cert_path = "some-path"
1389
+ "#;
1390
+ let mut vars = HashMap::new();
1391
+ vars.insert(
1392
+ "TEMPORAL_TLS_CLIENT_CERT_DATA".to_string(),
1393
+ "some-data".to_string(),
1394
+ );
1395
+ let options = LoadClientConfigProfileOptions {
1396
+ config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
1397
+ ..Default::default()
1398
+ };
1399
+ let err = load_client_config_profile(options, Some(&vars)).unwrap_err();
1400
+ assert!(matches!(err, ConfigError::InvalidConfig(_)));
1401
+ assert!(
1402
+ err.to_string()
1403
+ .contains("when cert path is already specified")
1404
+ );
1405
+
1406
+ // Data in TOML, path in env var should fail
1407
+ let toml_str = r#"
1408
+ [profile.default]
1409
+ [profile.default.tls]
1410
+ client_cert_data = "some-data"
1411
+ "#;
1412
+ let mut vars = HashMap::new();
1413
+ vars.insert(
1414
+ "TEMPORAL_TLS_CLIENT_CERT_PATH".to_string(),
1415
+ "some-path".to_string(),
1416
+ );
1417
+ let options = LoadClientConfigProfileOptions {
1418
+ config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
1419
+ ..Default::default()
1420
+ };
1421
+ let err = load_client_config_profile(options, Some(&vars)).unwrap_err();
1422
+ assert!(matches!(err, ConfigError::InvalidConfig(_)));
1423
+ assert!(
1424
+ err.to_string()
1425
+ .contains("when cert data is already specified")
1426
+ );
1427
+ }
1428
+
1429
+ #[test]
1430
+ fn test_default_profile_not_found_is_ok() {
1431
+ let toml_str = r#"
1432
+ [profile.existing]
1433
+ address = "localhost:7233"
1434
+ "#;
1435
+
1436
+ let options = LoadClientConfigProfileOptions {
1437
+ config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
1438
+ ..Default::default()
1439
+ };
1440
+
1441
+ // Should not error, just returns an empty profile
1442
+ let profile = load_client_config_profile(options, None).unwrap();
1443
+ assert_eq!(profile, ClientConfigProfile::default());
1444
+ }
1445
+
1446
+ #[test]
1447
+ fn test_normalize_grpc_meta_key() {
1448
+ assert_eq!(normalize_grpc_meta_key("SOME_HEADER"), "some-header");
1449
+ assert_eq!(normalize_grpc_meta_key("some_header"), "some-header");
1450
+ assert_eq!(normalize_grpc_meta_key("Some_Header"), "some-header");
1451
+ }
1452
+
1453
+ #[test]
1454
+ fn test_env_var_to_bool() {
1455
+ assert_eq!(env_var_to_bool("true"), Some(true));
1456
+ assert_eq!(env_var_to_bool("TRUE"), Some(true));
1457
+ assert_eq!(env_var_to_bool("1"), Some(true));
1458
+
1459
+ assert_eq!(env_var_to_bool("false"), Some(false));
1460
+ assert_eq!(env_var_to_bool("FALSE"), Some(false));
1461
+ assert_eq!(env_var_to_bool("0"), Some(false));
1462
+
1463
+ assert_eq!(env_var_to_bool("invalid"), None);
1464
+ assert_eq!(env_var_to_bool("yes"), None);
1465
+ assert_eq!(env_var_to_bool("no"), None);
1466
+ }
1467
+
1468
+ #[test]
1469
+ fn test_load_client_config_profile_disables_are_an_error() {
1470
+ let options = LoadClientConfigProfileOptions {
1471
+ disable_file: true,
1472
+ disable_env: true,
1473
+ ..Default::default()
1474
+ };
1475
+
1476
+ let result = load_client_config_profile(options, None);
1477
+ assert!(result.is_err());
1478
+ assert!(
1479
+ result
1480
+ .unwrap_err()
1481
+ .to_string()
1482
+ .contains("Cannot disable both file and environment")
1483
+ );
1484
+ }
1485
+
1486
+ #[test]
1487
+ fn test_load_client_config_profile_from_env_only() {
1488
+ let mut vars = HashMap::new();
1489
+ vars.insert("TEMPORAL_ADDRESS".to_string(), "env-address".to_string());
1490
+ vars.insert(
1491
+ "TEMPORAL_NAMESPACE".to_string(),
1492
+ "env-namespace".to_string(),
1493
+ );
1494
+
1495
+ let options = LoadClientConfigProfileOptions {
1496
+ disable_file: true,
1497
+ ..Default::default()
1498
+ };
1499
+
1500
+ let profile = load_client_config_profile(options, Some(&vars)).unwrap();
1501
+ assert_eq!(profile.address.as_ref().unwrap(), "env-address");
1502
+ assert_eq!(profile.namespace.as_ref().unwrap(), "env-namespace");
1503
+ }
1504
+
1505
+ #[test]
1506
+ fn test_api_key_tls_auto_enable() {
1507
+ // Test 1: When API key is present, TLS should be automatically enabled
1508
+ let toml_str = r#"
1509
+ [profile.default]
1510
+ api_key = "my-api-key"
1511
+ "#;
1512
+
1513
+ let options = LoadClientConfigProfileOptions {
1514
+ config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
1515
+ ..Default::default()
1516
+ };
1517
+
1518
+ let profile = load_client_config_profile(options, None).unwrap();
1519
+
1520
+ // TLS should be enabled due to API key presence
1521
+ assert!(profile.tls.is_some());
1522
+ let tls = profile.tls.as_ref().unwrap();
1523
+ assert!(!tls.disabled);
1524
+ }
1525
+
1526
+ #[test]
1527
+ fn test_no_api_key_no_tls_is_none() {
1528
+ // Test that if no API key is present and no TLS block exists, TLS config is None
1529
+ let toml_str = r#"
1530
+ [profile.default]
1531
+ address = "some-address"
1532
+ "#;
1533
+
1534
+ let options = LoadClientConfigProfileOptions {
1535
+ config_source: Some(DataSource::Data(toml_str.as_bytes().to_vec())),
1536
+ ..Default::default()
1537
+ };
1538
+
1539
+ let profile = load_client_config_profile(options, None).unwrap();
1540
+
1541
+ // TLS should not be enabled
1542
+ assert!(profile.tls.is_none());
1543
+ }
1544
+ }