use crate::{
CancellationToken, Error, HeaderMap, RetryPolicy, DEFAULT_BASE_URL, DEFAULT_MODEL,
DEFAULT_TIMEOUT,
};
use std::{fmt, time::Duration};
pub type LogLevel = log::LevelFilter;
#[derive(Clone, Default)]
pub struct RequestOptions {
pub retry: Option<RetryPolicy>,
pub timeout: Option<Duration>,
pub headers: HeaderMap,
pub cancellation_token: Option<CancellationToken>,
}
impl fmt::Debug for RequestOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RequestOptions")
.field("retry", &self.retry)
.field("timeout", &self.timeout)
.field("header_count", &self.headers.len())
.field("cancellable", &self.cancellation_token.is_some())
.finish()
}
}
#[derive(Clone, Default)]
pub(crate) struct ConfigBuilder {
pub api_key: Option<String>,
pub base_url: Option<String>,
pub model: Option<String>,
pub timeout: Option<Duration>,
pub retry: RetryPolicy,
pub headers: HeaderMap,
pub log_level: Option<LogLevel>,
}
impl fmt::Debug for ConfigBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConfigBuilder").finish_non_exhaustive()
}
}
#[derive(Clone)]
pub(crate) struct Config {
pub authorization: reqwest::header::HeaderValue,
pub base_url: String,
pub model: String,
pub timeout: Duration,
pub retry: RetryPolicy,
pub headers: HeaderMap,
pub log_level: LogLevel,
}
impl fmt::Debug for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Config")
.field("base_url", &self.base_url)
.field("model", &self.model)
.finish_non_exhaustive()
}
}
fn env(name: &str) -> Option<String> {
std::env::var(name)
.ok()
.map(|v| v.trim().to_owned())
.filter(|v| !v.is_empty())
}
pub(crate) fn validate_timeout(timeout: Duration) -> Result<(), Error> {
if timeout.is_zero() || std::time::Instant::now().checked_add(timeout).is_none() {
return Err(Error::Configuration(
"timeout must be positive and representable".into(),
));
}
Ok(())
}
impl ConfigBuilder {
pub fn resolve(self) -> Result<Config, Error> {
let key = self
.api_key
.or_else(|| env("TYPESAFE_API_KEY"))
.filter(|v| !v.trim().is_empty())
.ok_or_else(|| {
Error::Configuration("Pass an API key or set TYPESAFE_API_KEY".into())
})?;
let mut authorization = reqwest::header::HeaderValue::from_str(&format!("Bearer {key}"))
.map_err(|_| {
Error::Configuration("API key contains invalid HTTP header characters".into())
})?;
authorization.set_sensitive(true);
let base_url = self
.base_url
.or_else(|| env("TYPESAFE_BASE_URL"))
.unwrap_or_else(|| DEFAULT_BASE_URL.into());
let url = reqwest::Url::parse(&base_url)
.map_err(|_| Error::Configuration("base_url must be an absolute HTTP(S) URL".into()))?;
if !matches!(url.scheme(), "https" | "http")
|| url.host_str().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
{
return Err(Error::Configuration(
"base_url must be an HTTP(S) root without credentials, query, or fragment".into(),
));
}
let timeout = self.timeout.unwrap_or(DEFAULT_TIMEOUT);
validate_timeout(timeout)?;
self.retry.validate()?;
let log_level = match self.log_level {
Some(level) => level,
None => match env("TYPESAFE_LOG_LEVEL").as_deref() {
None => LogLevel::Warn,
Some("debug") => LogLevel::Debug,
Some("info") => LogLevel::Info,
Some("warn" | "warning") => LogLevel::Warn,
Some("error") => LogLevel::Error,
Some("off") => LogLevel::Off,
Some(_) => {
return Err(Error::Configuration(
"TYPESAFE_LOG_LEVEL must be debug, info, warn, warning, error, or off"
.into(),
))
}
},
};
Ok(Config {
authorization,
base_url: url.as_str().trim_end_matches('/').to_owned(),
model: self
.model
.or_else(|| env("TYPESAFE_DEFAULT_MODEL"))
.unwrap_or_else(|| DEFAULT_MODEL.into()),
timeout,
retry: self.retry,
headers: self.headers,
log_level,
})
}
}
macro_rules! config_methods {
() => {
pub fn api_key(mut self, value: impl Into<String>) -> Self {
self.config.api_key = Some(value.into());
self
}
pub fn base_url(mut self, value: impl Into<String>) -> Self {
self.config.base_url = Some(value.into());
self
}
pub fn model(mut self, value: impl Into<String>) -> Self {
self.config.model = Some(value.into());
self
}
pub fn timeout(mut self, value: std::time::Duration) -> Self {
self.config.timeout = Some(value);
self
}
pub fn retry(mut self, value: crate::RetryPolicy) -> Self {
self.config.retry = value;
self
}
pub fn default_headers(mut self, value: crate::HeaderMap) -> Self {
self.config.headers = value;
self
}
pub fn log_level(mut self, value: crate::LogLevel) -> Self {
self.config.log_level = Some(value);
self
}
};
}
pub(crate) use config_methods;