#![warn(missing_docs)]
use std::time::Duration;
use crate::error::Error;
pub const DAPR_GRPC_ENDPOINT_ENV: &str = "DAPR_GRPC_ENDPOINT";
pub const DAPR_GRPC_PORT_ENV: &str = "DAPR_GRPC_PORT";
pub const DAPR_API_TOKEN_ENV: &str = "DAPR_API_TOKEN";
pub const DAPR_CLIENT_TIMEOUT_SECONDS_ENV: &str = "DAPR_CLIENT_TIMEOUT_SECONDS";
pub const APP_API_TOKEN_ENV: &str = "APP_API_TOKEN";
pub const API_TOKEN_METADATA_KEY: &str = "dapr-api-token";
pub const DEFAULT_DAPR_GRPC_PORT: u16 = 50001;
pub const DEFAULT_CLIENT_TIMEOUT_SECONDS: u64 = 5;
pub fn default_sidecar_address() -> String {
if let Ok(endpoint) = std::env::var(DAPR_GRPC_ENDPOINT_ENV)
&& !endpoint.is_empty()
{
return endpoint;
}
match std::env::var(DAPR_GRPC_PORT_ENV) {
Ok(port) if !port.is_empty() => format!("http://127.0.0.1:{port}"),
_ => format!("http://127.0.0.1:{DEFAULT_DAPR_GRPC_PORT}"),
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ClientOptions {
address: String,
api_token: Option<String>,
timeout: Duration,
}
impl ClientOptions {
pub fn new() -> Self {
Self::default()
}
pub fn from_env() -> Result<Self, Error> {
let address = default_sidecar_address();
let api_token = read_optional_env(DAPR_API_TOKEN_ENV);
let timeout = read_timeout_env()?;
Ok(Self {
address,
api_token,
timeout,
})
}
pub fn with_address(mut self, address: impl Into<String>) -> Self {
self.address = address.into();
self
}
pub fn with_api_token(mut self, token: impl Into<String>) -> Self {
let t = token.into();
self.api_token = if t.is_empty() { None } else { Some(t) };
self
}
pub fn without_api_token(mut self) -> Self {
self.api_token = None;
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn address(&self) -> &str {
&self.address
}
pub fn api_token(&self) -> Option<&str> {
self.api_token.as_deref()
}
pub fn timeout(&self) -> Duration {
self.timeout
}
}
impl Default for ClientOptions {
fn default() -> Self {
Self {
address: default_sidecar_address(),
api_token: read_optional_env(DAPR_API_TOKEN_ENV),
timeout: read_timeout_env()
.unwrap_or_else(|_| Duration::from_secs(DEFAULT_CLIENT_TIMEOUT_SECONDS)),
}
}
}
fn read_optional_env(key: &str) -> Option<String> {
match std::env::var(key) {
Ok(v) if !v.is_empty() => Some(v),
_ => None,
}
}
fn read_timeout_env() -> Result<Duration, Error> {
match std::env::var(DAPR_CLIENT_TIMEOUT_SECONDS_ENV) {
Ok(v) if !v.is_empty() => {
let secs: u64 = v.parse()?;
if secs == 0 {
return Err(Error::ParseIntError);
}
Ok(Duration::from_secs(secs))
}
_ => Ok(Duration::from_secs(DEFAULT_CLIENT_TIMEOUT_SECONDS)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_env<F: FnOnce()>(pairs: &[(&str, Option<&str>)], f: F) {
let _guard = ENV_LOCK.lock().unwrap();
let prev: Vec<(String, Option<String>)> = pairs
.iter()
.map(|(k, _)| (k.to_string(), std::env::var(k).ok()))
.collect();
for (k, v) in pairs {
match v {
Some(val) => unsafe { std::env::set_var(k, val) },
None => unsafe { std::env::remove_var(k) },
}
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
for (k, v) in prev {
match v {
Some(val) => unsafe { std::env::set_var(&k, val) },
None => unsafe { std::env::remove_var(&k) },
}
}
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
#[test]
fn default_address_uses_built_in_default_when_unset() {
with_env(
&[(DAPR_GRPC_ENDPOINT_ENV, None), (DAPR_GRPC_PORT_ENV, None)],
|| {
assert_eq!(default_sidecar_address(), "http://127.0.0.1:50001");
},
);
}
#[test]
fn default_address_uses_port_env() {
with_env(
&[
(DAPR_GRPC_ENDPOINT_ENV, None),
(DAPR_GRPC_PORT_ENV, Some("12345")),
],
|| {
assert_eq!(default_sidecar_address(), "http://127.0.0.1:12345");
},
);
}
#[test]
fn default_address_prefers_endpoint_env() {
with_env(
&[
(DAPR_GRPC_ENDPOINT_ENV, Some("https://sidecar:443?tls=true")),
(DAPR_GRPC_PORT_ENV, Some("12345")),
],
|| {
assert_eq!(default_sidecar_address(), "https://sidecar:443?tls=true");
},
);
}
#[test]
fn options_from_env_reads_token_and_timeout() {
with_env(
&[
(DAPR_GRPC_ENDPOINT_ENV, Some("http://1.2.3.4:50001")),
(DAPR_API_TOKEN_ENV, Some("tok")),
(DAPR_CLIENT_TIMEOUT_SECONDS_ENV, Some("17")),
],
|| {
let opts = ClientOptions::from_env().unwrap();
assert_eq!(opts.address(), "http://1.2.3.4:50001");
assert_eq!(opts.api_token(), Some("tok"));
assert_eq!(opts.timeout(), Duration::from_secs(17));
},
);
}
#[test]
fn options_from_env_rejects_invalid_timeout() {
with_env(
&[(DAPR_CLIENT_TIMEOUT_SECONDS_ENV, Some("not-a-number"))],
|| {
assert!(matches!(
ClientOptions::from_env(),
Err(Error::ParseIntError)
));
},
);
}
#[test]
fn options_from_env_rejects_zero_timeout() {
with_env(&[(DAPR_CLIENT_TIMEOUT_SECONDS_ENV, Some("0"))], || {
assert!(matches!(
ClientOptions::from_env(),
Err(Error::ParseIntError)
));
});
}
#[test]
fn options_default_falls_back_on_invalid_timeout() {
with_env(&[(DAPR_CLIENT_TIMEOUT_SECONDS_ENV, Some("nope"))], || {
let opts = ClientOptions::default();
assert_eq!(
opts.timeout(),
Duration::from_secs(DEFAULT_CLIENT_TIMEOUT_SECONDS)
);
});
}
#[test]
fn builder_overrides_take_precedence() {
let opts = ClientOptions::new()
.with_address("http://override:1234")
.with_api_token("abc")
.with_timeout(Duration::from_secs(42));
assert_eq!(opts.address(), "http://override:1234");
assert_eq!(opts.api_token(), Some("abc"));
assert_eq!(opts.timeout(), Duration::from_secs(42));
}
#[test]
fn empty_api_token_clears() {
let opts = ClientOptions::new()
.with_api_token("abc")
.with_api_token("");
assert_eq!(opts.api_token(), None);
}
}