use std::{borrow::Cow, env, fmt, time::Duration};
use url::Url;
use crate::error::{MyIdError, MyIdResult};
pub const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 2_000;
pub const DEFAULT_TIMEOUT_MS: u64 = 15_000;
pub(crate) const DEFAULT_USER_AGENT: &str = "myid-client-rust/0.1";
pub(crate) const DEFAULT_PREFIX: &str = "MYID_";
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Config>();
};
#[derive(Clone)]
pub struct Config {
base_url: Url,
client_id: String,
client_secret: String,
connection_timeout_ms: Duration,
timeout_ms: Duration,
user_agent: Cow<'static, str>,
proxy_url: Option<Url>,
}
impl Config {
pub fn new(
base_url: impl AsRef<str>,
client_id: impl Into<String>,
client_secret: impl Into<String>,
) -> MyIdResult<Self> {
let base_url = Self::parse_and_normalize_url(&base_url)?;
let client_id = client_id.into();
let client_secret = client_secret.into();
if client_id.trim().is_empty() {
return Err(MyIdError::validation("client_id bo'sh bo'lmasligi kerak"));
}
if client_secret.trim().is_empty() {
return Err(MyIdError::validation(
"client_secret bo'sh bo'lmasligi kerak",
));
}
Ok(Self {
base_url,
client_id,
client_secret,
connection_timeout_ms: Duration::from_millis(DEFAULT_CONNECT_TIMEOUT_MS),
timeout_ms: Duration::from_millis(DEFAULT_TIMEOUT_MS),
user_agent: Cow::Borrowed(DEFAULT_USER_AGENT),
proxy_url: None,
})
}
pub fn from_env(prefix: Option<&str>) -> MyIdResult<Self> {
#[cfg(feature = "dotenvy")]
{
let _ = dotenvy::dotenv();
}
let p = Self::normalize_prefix(prefix.unwrap_or(DEFAULT_PREFIX));
let base_url =
Self::parse_and_normalize_url(&Self::read_required(&format!("{p}BASE_URL"))?)?;
let client_id = Self::read_required(&format!("{p}CLIENT_ID"))?;
let client_secret = Self::read_required(&format!("{p}CLIENT_SECRET"))?;
let connection_timeout_ms = Self::read_u64_or_default(
&format!("{p}CONNECT_TIMEOUT_MS"),
DEFAULT_CONNECT_TIMEOUT_MS,
)?;
let timeout_ms = Self::read_u64_or_default(&format!("{p}TIMEOUT_MS"), DEFAULT_TIMEOUT_MS)?;
let user_agent: Cow<'static, str> = match Self::read_optional(&format!("{p}USER_AGENT")) {
Some(ua) => Cow::Owned(ua),
None => Cow::Borrowed(DEFAULT_USER_AGENT),
};
let proxy_url = Self::read_optional(&format!("{p}PROXY_URL"))
.map(|raw| Self::parse_url(&raw))
.transpose()?;
Ok(Self {
base_url,
client_id,
client_secret,
connection_timeout_ms: Duration::from_millis(connection_timeout_ms),
timeout_ms: Duration::from_millis(timeout_ms),
user_agent,
proxy_url,
})
}
#[must_use]
#[inline]
pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
self.connection_timeout_ms = timeout;
self
}
#[must_use]
#[inline]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout_ms = timeout;
self
}
#[must_use]
#[inline]
pub fn with_user_agent(mut self, agent: impl Into<String>) -> Self {
self.user_agent = Cow::Owned(agent.into());
self
}
pub fn with_proxy(mut self, url: impl AsRef<str>) -> MyIdResult<Self> {
self.proxy_url = Some(Self::parse_url(&url)?);
Ok(self)
}
#[inline]
pub fn base_url(&self) -> &str {
self.base_url.as_str()
}
#[inline]
pub fn client_id(&self) -> &str {
&self.client_id
}
#[inline]
pub fn client_secret(&self) -> &str {
&self.client_secret
}
#[inline]
pub fn connection_timeout(&self) -> Duration {
self.connection_timeout_ms
}
#[inline]
pub fn timeout(&self) -> Duration {
self.timeout_ms
}
#[inline]
pub fn user_agent(&self) -> &str {
self.user_agent.as_ref()
}
#[inline]
pub fn proxy_url(&self) -> Option<&str> {
self.proxy_url.as_ref().map(Url::as_str)
}
#[inline]
pub(crate) fn base_url_parsed(&self) -> &Url {
&self.base_url
}
fn parse_and_normalize_url(raw: impl AsRef<str>) -> MyIdResult<Url> {
let mut url = Self::parse_url(&raw)?;
if !url.path().ends_with('/') {
url.set_path(&format!("{}/", url.path()));
}
Ok(url)
}
fn parse_url(raw: impl AsRef<str>) -> MyIdResult<Url> {
let url = Url::parse(raw.as_ref())
.map_err(|e| MyIdError::config(format!("invalid URL `{}`: {e}", raw.as_ref())))?;
match url.scheme() {
"http" | "https" => Ok(url),
other => Err(MyIdError::config(format!(
"only http/https are accepted, given: {other}"
))),
}
}
fn normalize_prefix(prefix: &str) -> String {
let s = prefix.trim();
if s.is_empty() {
return DEFAULT_PREFIX.to_string();
}
if s.ends_with('_') {
s.to_string()
} else {
format!("{s}_")
}
}
fn read_required(key: &str) -> MyIdResult<String> {
let value =
env::var(key).map_err(|_| MyIdError::config(format!("missing env var: {key}")))?;
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(MyIdError::config(format!("empty env var: {key}")));
}
Ok(trimmed.to_owned())
}
fn read_optional(key: &str) -> Option<String> {
env::var(key).ok().and_then(|v| {
let trimmed = v.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_owned())
}
})
}
fn read_u64_or_default(key: &str, default: u64) -> MyIdResult<u64> {
match Self::read_optional(key) {
None => Ok(default),
Some(v) => {
let parsed: u64 = v
.parse()
.map_err(|_| MyIdError::config(format!("invalid u64: {key}={v}")))?;
if parsed == 0 {
return Err(MyIdError::config(format!("{key} must be > 0")));
}
Ok(parsed)
}
}
}
}
impl fmt::Debug for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Config")
.field("base_url", &self.base_url.as_str())
.field("client_id", &self.client_id.as_str())
.field("client_secret", &"<redacted>")
.field("connection_timeout_ms", &self.connection_timeout_ms)
.field("timeout_ms", &self.timeout_ms)
.field("user_agent", &self.user_agent.as_ref())
.field("proxy_url", &self.proxy_url.as_ref().map(|p| p.as_str()))
.finish()
}
}