use std::collections::HashMap;
use std::path::Path;
use std::time::Duration;
use anyhow::{Context, Result};
use figment::Figment;
use figment::providers::{Env, Format, Toml};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct FolkConfig {
pub server: ServerConfig,
pub workers: WorkersConfig,
pub log: LogConfig,
pub dev: DevConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerConfig {
pub rpc_socket: String,
#[serde(with = "humantime_serde")]
pub shutdown_timeout: Duration,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
rpc_socket: "/tmp/folk.sock".into(),
shutdown_timeout: Duration::from_secs(30),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct WorkersConfig {
pub script: String,
pub php: String,
pub count: usize,
pub max_jobs: u64,
#[serde(with = "humantime_serde")]
pub ttl: Duration,
#[serde(with = "humantime_serde")]
pub exec_timeout: Duration,
#[serde(with = "humantime_serde")]
pub boot_timeout: Duration,
pub warmup: bool,
}
impl Default for WorkersConfig {
fn default() -> Self {
Self {
script: "vendor/bin/folk-worker".into(),
php: "php".into(),
count: 4,
max_jobs: 1000,
ttl: Duration::from_secs(3600),
exec_timeout: Duration::from_secs(30),
boot_timeout: Duration::from_secs(30),
warmup: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct LogConfig {
pub filter: String,
pub format: LogFormat,
#[serde(default)]
pub plugins: HashMap<String, String>,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
filter: "info".into(),
format: LogFormat::Text,
plugins: HashMap::new(),
}
}
}
impl LogConfig {
pub fn effective_filter(&self) -> String {
if self.plugins.is_empty() {
return self.filter.clone();
}
let mut parts = vec![self.filter.clone()];
for (plugin, level) in &self.plugins {
let target = plugin_name_to_target(plugin);
parts.push(format!("{target}={level}"));
}
parts.join(",")
}
}
fn plugin_name_to_target(name: &str) -> &str {
match name {
"http" => "folk_plugin_http",
"jobs" => "folk_plugin_jobs",
"grpc" => "folk_plugin_grpc",
"metrics" => "folk_plugin_metrics",
"process" => "folk_plugin_process",
"core" => "folk_core",
"ext" => "folk_ext",
_ => name,
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum LogFormat {
Text,
Json,
Pretty,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DevConfig {
pub watch: bool,
pub watch_paths: Vec<String>,
pub watch_extensions: Vec<String>,
#[serde(with = "humantime_serde")]
pub debounce: Duration,
}
impl Default for DevConfig {
fn default() -> Self {
Self {
watch: false,
watch_paths: vec!["app".into(), "src".into(), "routes".into(), "config".into()],
watch_extensions: vec!["php".into()],
debounce: Duration::from_millis(300),
}
}
}
impl FolkConfig {
pub fn load() -> Result<Self> {
Self::load_from(Path::new("folk.toml"))
}
pub fn load_from(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let mut fig = Figment::from(figment::providers::Serialized::defaults(Self::default()));
if path.exists() {
fig = fig.merge(Toml::file(path));
}
fig = fig.merge(Env::prefixed("FOLK_").split("_"));
fig.extract().context("failed to parse Folk configuration")
}
}