1use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use temporalio_client::{
13 Client, ClientOptions,
14 envconfig::{DataSource, LoadClientConfigProfileOptions},
15 grpc::{CloudService, OperatorService, WorkflowService},
16};
17
18#[derive(Debug, Clone, Default)]
20pub struct ProfileRef {
21 pub name: Option<String>,
23 pub config_file: Option<String>,
25}
26
27#[derive(Debug, thiserror::Error)]
28pub enum ConnectError {
29 #[error("could not load Temporal profile: {0}")]
33 Config(String),
34 #[error("could not connect to Temporal: {0}")]
35 Connect(String),
36}
37
38fn config_source(explicit: Option<&str>) -> Option<DataSource> {
50 if let Some(p) = explicit {
51 return Some(DataSource::Path(p.to_string()));
52 }
53 if std::env::var_os("TEMPORAL_CONFIG_FILE").is_some_and(|v| !v.is_empty()) {
55 return None;
56 }
57 if platform_config_file().is_some_and(|p| p.is_file()) {
59 return None;
60 }
61 let path = xdg_config_file()?;
62 Path::new(&path)
63 .is_file()
64 .then(|| DataSource::Path(path.to_string_lossy().into_owned()))
65}
66
67pub fn xdg_config_file() -> Option<PathBuf> {
69 let base = match std::env::var_os("XDG_CONFIG_HOME") {
70 Some(d) if !d.is_empty() => PathBuf::from(d),
71 _ => PathBuf::from(std::env::var_os("HOME")?).join(".config"),
72 };
73 Some(base.join("temporalio").join("temporal.toml"))
74}
75
76pub fn config_file_in_use(explicit: Option<&str>) -> Option<PathBuf> {
78 if let Some(p) = explicit {
79 return Some(PathBuf::from(p));
80 }
81 if let Some(p) = std::env::var_os("TEMPORAL_CONFIG_FILE").filter(|v| !v.is_empty()) {
82 return Some(PathBuf::from(p));
83 }
84 let platform = platform_config_file();
85 if platform.as_ref().is_some_and(|p| p.is_file()) {
86 return platform;
87 }
88 match xdg_config_file() {
89 Some(p) if p.is_file() => Some(p),
90 _ => platform,
92 }
93}
94
95pub fn platform_config_file() -> Option<PathBuf> {
97 dirs_config_dir().map(|d| d.join("temporalio").join("temporal.toml"))
98}
99
100fn dirs_config_dir() -> Option<PathBuf> {
102 #[cfg(target_os = "macos")]
103 {
104 Some(PathBuf::from(std::env::var_os("HOME")?).join("Library/Application Support"))
105 }
106 #[cfg(not(target_os = "macos"))]
107 {
108 match std::env::var_os("XDG_CONFIG_HOME") {
109 Some(d) if !d.is_empty() => Some(PathBuf::from(d)),
110 _ => Some(PathBuf::from(std::env::var_os("HOME")?).join(".config")),
111 }
112 }
113}
114
115#[derive(Clone)]
118pub struct Conn {
119 client: Client,
120 profile: Arc<str>,
121 namespace: Arc<str>,
122 address: Arc<str>,
123}
124
125impl Conn {
126 pub async fn connect(profile: &ProfileRef) -> Result<Self, ConnectError> {
127 let load = LoadClientConfigProfileOptions::builder()
128 .maybe_config_file_profile(profile.name.clone())
129 .maybe_config_source(config_source(profile.config_file.as_deref()))
130 .build();
131
132 let (conn_opts, client_opts) = ClientOptions::load_from_config(load)
133 .map_err(|e| ConnectError::Config(e.to_string()))?;
134 let namespace: Arc<str> = client_opts.namespace.as_str().into();
135 let address: Arc<str> = conn_opts.target.to_string().into();
138 let client = Client::connect(conn_opts, client_opts)
139 .await
140 .map_err(|e| ConnectError::Connect(e.to_string()))?;
141
142 Ok(Self {
143 client,
144 profile: profile.name.as_deref().unwrap_or("default").into(),
145 namespace,
146 address,
147 })
148 }
149
150 pub fn namespace(&self) -> &str {
151 &self.namespace
152 }
153
154 pub fn profile(&self) -> &str {
155 &self.profile
156 }
157
158 pub fn address(&self) -> &str {
161 &self.address
162 }
163
164 pub fn wf(&self) -> Box<dyn WorkflowService> {
167 self.client.connection().workflow_service()
168 }
169
170 pub fn operator(&self) -> Box<dyn OperatorService> {
171 self.client.connection().operator_service()
172 }
173
174 pub fn cloud(&self) -> Box<dyn CloudService> {
175 self.client.connection().cloud_service()
176 }
177
178 pub fn raw(&self) -> &Client {
181 &self.client
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn an_explicit_path_wins() {
191 assert!(matches!(
193 config_source(Some("/tmp/explicit.toml")),
194 Some(DataSource::Path(p)) if p == "/tmp/explicit.toml"
195 ));
196 assert_eq!(
197 config_file_in_use(Some("/tmp/explicit.toml")),
198 Some(PathBuf::from("/tmp/explicit.toml"))
199 );
200 }
201
202 #[test]
203 fn the_platform_path_is_the_one_the_cli_documents() {
204 let path = platform_config_file().expect("HOME is set in a test run");
207 assert!(path.ends_with("temporalio/temporal.toml"), "{path:?}");
208 #[cfg(target_os = "macos")]
209 assert!(
210 path.to_string_lossy()
211 .contains("Library/Application Support"),
212 "{path:?}"
213 );
214 }
215
216 #[test]
217 fn the_xdg_path_is_only_a_fallback() {
218 let path = xdg_config_file().expect("HOME is set in a test run");
221 assert!(path.ends_with("temporalio/temporal.toml"), "{path:?}");
222 }
223
224 #[test]
225 fn config_file_in_use_always_names_something() {
226 assert!(config_file_in_use(None).is_some());
229 }
230}