mod interpolate;
#[cfg(test)]
mod tests;
mod types;
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::time::Duration;
use serde::Deserialize;
use crate::error::ConfigError;
use self::interpolate::interpolate_document;
pub(crate) use self::types::{GatewayUrl, GlobPattern, PromptName, RelativePromptPath, Secret};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Config {
pub(crate) server: ServerConfig,
pub(crate) paths: PathsConfig,
pub(crate) gateway: GatewayConfig,
pub(crate) catalog: CatalogConfig,
pub(crate) prompts: BTreeMap<PromptName, PromptConfig>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawConfig {
server: RawServerConfig,
#[serde(default)]
paths: PathsConfig,
gateway: RawGatewayConfig,
#[serde(default)]
catalog: CatalogConfig,
#[serde(default)]
prompts: BTreeMap<PromptName, PromptConfig>,
}
impl TryFrom<RawConfig> for Config {
type Error = ConfigError;
fn try_from(raw: RawConfig) -> Result<Config, ConfigError> {
let token = raw
.server
.token
.map(|value| Secret::try_from(value).map_err(|_| ConfigError::empty_token()))
.transpose()?;
let key = Secret::try_from(raw.gateway.key)
.map_err(|_| ConfigError::parse("[gateway].key must not be empty"))?;
Ok(Config {
server: ServerConfig {
bind: raw.server.bind,
allowed_hosts: raw.server.allowed_hosts,
token,
max_concurrent_runs: raw.server.max_concurrent_runs,
admission_timeout: raw.server.admission_timeout,
reply_deadline: raw.server.reply_deadline,
retain_completed: raw.server.retain_completed,
watch: raw.server.watch,
watch_debounce: raw.server.watch_debounce,
},
paths: raw.paths,
gateway: GatewayConfig {
url: raw.gateway.url,
key,
},
catalog: raw.catalog,
prompts: raw.prompts,
})
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawServerConfig {
#[serde(default = "default_bind")]
bind: SocketAddr,
#[serde(default)]
allowed_hosts: Vec<String>,
#[serde(default)]
token: Option<String>,
#[serde(default = "default_max_concurrent_runs")]
max_concurrent_runs: NonZeroUsize,
#[serde(default = "default_admission_timeout", with = "humantime_serde")]
admission_timeout: Duration,
#[serde(default = "default_reply_deadline", with = "humantime_serde")]
reply_deadline: Duration,
#[serde(default = "default_retain_completed", with = "humantime_serde")]
retain_completed: Duration,
#[serde(default = "default_watch")]
watch: bool,
#[serde(default = "default_watch_debounce", with = "humantime_serde")]
watch_debounce: Duration,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RawGatewayConfig {
url: GatewayUrl,
key: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub(crate) struct ServerConfig {
pub(crate) bind: SocketAddr,
pub(crate) allowed_hosts: Vec<String>,
pub(crate) token: Option<Secret>,
pub(crate) max_concurrent_runs: NonZeroUsize,
pub(crate) admission_timeout: Duration,
pub(crate) reply_deadline: Duration,
pub(crate) retain_completed: Duration,
pub(crate) watch: bool,
pub(crate) watch_debounce: Duration,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub(crate) struct PathsConfig {
#[serde(default = "default_prompts_dir")]
pub(crate) prompts: PathBuf,
}
impl Default for PathsConfig {
fn default() -> Self {
PathsConfig {
prompts: default_prompts_dir(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub(crate) struct GatewayConfig {
pub(crate) url: GatewayUrl,
pub(crate) key: Secret,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub(crate) struct CatalogConfig {
#[serde(default)]
pub(crate) include: Vec<GlobPattern>,
#[serde(default)]
pub(crate) exclude: Vec<GlobPattern>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub(crate) struct PromptConfig {
#[serde(default = "default_enabled")]
pub(crate) enabled: bool,
#[serde(default)]
pub(crate) file: Option<RelativePromptPath>,
}
const MAX_CONFIG_BYTES: u64 = 4 * 1024 * 1024;
impl Config {
pub fn load(path: &Path) -> Result<Config, ConfigError> {
use std::io::Read as _;
let file = std::fs::File::open(path)
.map_err(|source| ConfigError::read(path.to_path_buf(), source))?;
let mut raw = String::new();
file.take(MAX_CONFIG_BYTES + 1)
.read_to_string(&mut raw)
.map_err(|source| ConfigError::read(path.to_path_buf(), source))?;
if raw.len() as u64 > MAX_CONFIG_BYTES {
return Err(ConfigError::parse(format!(
"config file {} exceeds the {MAX_CONFIG_BYTES}-byte limit",
path.display()
)));
}
Config::from_toml_str(&raw)
}
pub fn from_toml_str(raw: &str) -> Result<Config, ConfigError> {
let mut document: toml::Table = toml::from_str(raw).map_err(ConfigError::parse_toml)?;
interpolate_document(&mut document)?;
let raw_config: RawConfig = toml::Value::Table(document)
.try_into()
.map_err(ConfigError::parse_toml)?;
Config::try_from(raw_config)
}
}
impl std::str::FromStr for Config {
type Err = ConfigError;
fn from_str(s: &str) -> Result<Config, ConfigError> {
Config::from_toml_str(s)
}
}
fn default_bind() -> SocketAddr {
SocketAddr::from(([127, 0, 0, 1], 9310))
}
fn default_max_concurrent_runs() -> NonZeroUsize {
NonZeroUsize::MIN.saturating_add(3)
}
fn default_admission_timeout() -> Duration {
Duration::from_secs(30)
}
fn default_reply_deadline() -> Duration {
Duration::from_secs(240)
}
fn default_retain_completed() -> Duration {
Duration::from_secs(60 * 60)
}
fn default_watch() -> bool {
true
}
fn default_watch_debounce() -> Duration {
Duration::from_millis(500)
}
fn default_prompts_dir() -> PathBuf {
PathBuf::from("prompts")
}
fn default_enabled() -> bool {
true
}