use std::path::{Path, PathBuf};
use std::sync::Arc;
use temporalio_client::{
Client, ClientOptions,
envconfig::{DataSource, LoadClientConfigProfileOptions},
grpc::{CloudService, OperatorService, WorkflowService},
};
#[derive(Debug, Clone, Default)]
pub struct ProfileRef {
pub name: Option<String>,
pub config_file: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
#[error("could not load Temporal profile: {0}")]
Config(String),
#[error("could not connect to Temporal: {0}")]
Connect(String),
}
fn config_source(explicit: Option<&str>) -> Option<DataSource> {
if let Some(p) = explicit {
return Some(DataSource::Path(p.to_string()));
}
if std::env::var_os("TEMPORAL_CONFIG_FILE").is_some_and(|v| !v.is_empty()) {
return None;
}
if platform_config_file().is_some_and(|p| p.is_file()) {
return None;
}
let path = xdg_config_file()?;
Path::new(&path)
.is_file()
.then(|| DataSource::Path(path.to_string_lossy().into_owned()))
}
pub fn xdg_config_file() -> Option<PathBuf> {
let base = match std::env::var_os("XDG_CONFIG_HOME") {
Some(d) if !d.is_empty() => PathBuf::from(d),
_ => PathBuf::from(std::env::var_os("HOME")?).join(".config"),
};
Some(base.join("temporalio").join("temporal.toml"))
}
pub fn config_file_in_use(explicit: Option<&str>) -> Option<PathBuf> {
if let Some(p) = explicit {
return Some(PathBuf::from(p));
}
if let Some(p) = std::env::var_os("TEMPORAL_CONFIG_FILE").filter(|v| !v.is_empty()) {
return Some(PathBuf::from(p));
}
let platform = platform_config_file();
if platform.as_ref().is_some_and(|p| p.is_file()) {
return platform;
}
match xdg_config_file() {
Some(p) if p.is_file() => Some(p),
_ => platform,
}
}
pub fn platform_config_file() -> Option<PathBuf> {
dirs_config_dir().map(|d| d.join("temporalio").join("temporal.toml"))
}
fn dirs_config_dir() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
Some(PathBuf::from(std::env::var_os("HOME")?).join("Library/Application Support"))
}
#[cfg(not(target_os = "macos"))]
{
match std::env::var_os("XDG_CONFIG_HOME") {
Some(d) if !d.is_empty() => Some(PathBuf::from(d)),
_ => Some(PathBuf::from(std::env::var_os("HOME")?).join(".config")),
}
}
}
#[derive(Clone)]
pub struct Conn {
client: Client,
profile: Arc<str>,
namespace: Arc<str>,
address: Arc<str>,
}
impl Conn {
pub async fn connect(profile: &ProfileRef) -> Result<Self, ConnectError> {
let load = LoadClientConfigProfileOptions::builder()
.maybe_config_file_profile(profile.name.clone())
.maybe_config_source(config_source(profile.config_file.as_deref()))
.build();
let (conn_opts, client_opts) = ClientOptions::load_from_config(load)
.map_err(|e| ConnectError::Config(e.to_string()))?;
let namespace: Arc<str> = client_opts.namespace.as_str().into();
let address: Arc<str> = conn_opts.target.to_string().into();
let client = Client::connect(conn_opts, client_opts)
.await
.map_err(|e| ConnectError::Connect(e.to_string()))?;
Ok(Self {
client,
profile: profile.name.as_deref().unwrap_or("default").into(),
namespace,
address,
})
}
pub fn namespace(&self) -> &str {
&self.namespace
}
pub fn profile(&self) -> &str {
&self.profile
}
pub fn address(&self) -> &str {
&self.address
}
pub fn wf(&self) -> Box<dyn WorkflowService> {
self.client.connection().workflow_service()
}
pub fn operator(&self) -> Box<dyn OperatorService> {
self.client.connection().operator_service()
}
pub fn cloud(&self) -> Box<dyn CloudService> {
self.client.connection().cloud_service()
}
pub fn raw(&self) -> &Client {
&self.client
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_explicit_path_wins() {
assert!(matches!(
config_source(Some("/tmp/explicit.toml")),
Some(DataSource::Path(p)) if p == "/tmp/explicit.toml"
));
assert_eq!(
config_file_in_use(Some("/tmp/explicit.toml")),
Some(PathBuf::from("/tmp/explicit.toml"))
);
}
#[test]
fn the_platform_path_is_the_one_the_cli_documents() {
let path = platform_config_file().expect("HOME is set in a test run");
assert!(path.ends_with("temporalio/temporal.toml"), "{path:?}");
#[cfg(target_os = "macos")]
assert!(
path.to_string_lossy()
.contains("Library/Application Support"),
"{path:?}"
);
}
#[test]
fn the_xdg_path_is_only_a_fallback() {
let path = xdg_config_file().expect("HOME is set in a test run");
assert!(path.ends_with("temporalio/temporal.toml"), "{path:?}");
}
#[test]
fn config_file_in_use_always_names_something() {
assert!(config_file_in_use(None).is_some());
}
}