1use std::collections::HashMap;
8use std::path::Path;
9use std::time::Duration;
10
11use anyhow::{Context, Result};
12use figment::Figment;
13use figment::providers::{Env, Format, Toml};
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18#[serde(default)]
19pub struct FolkConfig {
20 pub server: ServerConfig,
21 pub workers: WorkersConfig,
22 pub log: LogConfig,
23 pub dev: DevConfig,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(default)]
28pub struct ServerConfig {
29 pub rpc_socket: String,
31 #[serde(with = "humantime_serde")]
33 pub shutdown_timeout: Duration,
34}
35
36impl Default for ServerConfig {
37 fn default() -> Self {
38 Self {
39 rpc_socket: "/tmp/folk.sock".into(),
40 shutdown_timeout: Duration::from_secs(30),
41 }
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(default)]
47pub struct WorkersConfig {
48 pub script: String,
50 pub php: String,
52 pub count: usize,
54 pub max_jobs: u64,
56 #[serde(with = "humantime_serde")]
58 pub ttl: Duration,
59 #[serde(with = "humantime_serde")]
61 pub exec_timeout: Duration,
62 #[serde(with = "humantime_serde")]
64 pub boot_timeout: Duration,
65 pub warmup: bool,
68}
69
70impl Default for WorkersConfig {
71 fn default() -> Self {
72 Self {
73 script: "vendor/bin/folk-worker".into(),
74 php: "php".into(),
75 count: 4,
76 max_jobs: 1000,
77 ttl: Duration::from_secs(3600),
78 exec_timeout: Duration::from_secs(30),
79 boot_timeout: Duration::from_secs(30),
80 warmup: true,
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(default)]
87pub struct LogConfig {
88 pub filter: String,
90 pub format: LogFormat,
92 #[serde(default)]
96 pub plugins: HashMap<String, String>,
97}
98
99impl Default for LogConfig {
100 fn default() -> Self {
101 Self {
102 filter: "info".into(),
103 format: LogFormat::Text,
104 plugins: HashMap::new(),
105 }
106 }
107}
108
109impl LogConfig {
110 pub fn effective_filter(&self) -> String {
114 if self.plugins.is_empty() {
115 return self.filter.clone();
116 }
117
118 let mut parts = vec![self.filter.clone()];
119 for (plugin, level) in &self.plugins {
120 let target = plugin_name_to_target(plugin);
121 parts.push(format!("{target}={level}"));
122 }
123 parts.join(",")
124 }
125}
126
127fn plugin_name_to_target(name: &str) -> &str {
128 match name {
129 "http" => "folk_plugin_http",
130 "jobs" => "folk_plugin_jobs",
131 "grpc" => "folk_plugin_grpc",
132 "metrics" => "folk_plugin_metrics",
133 "process" => "folk_plugin_process",
134 "core" => "folk_core",
135 "ext" => "folk_ext",
136 _ => name,
137 }
138}
139
140#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
141#[serde(rename_all = "snake_case")]
142pub enum LogFormat {
143 Text,
144 Json,
145 Pretty,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(default)]
153pub struct DevConfig {
154 pub watch: bool,
156 pub watch_paths: Vec<String>,
158 pub watch_extensions: Vec<String>,
160 #[serde(with = "humantime_serde")]
162 pub debounce: Duration,
163}
164
165impl Default for DevConfig {
166 fn default() -> Self {
167 Self {
168 watch: false,
169 watch_paths: vec!["app".into(), "src".into(), "routes".into(), "config".into()],
170 watch_extensions: vec!["php".into()],
171 debounce: Duration::from_millis(300),
172 }
173 }
174}
175
176impl FolkConfig {
177 pub fn load() -> Result<Self> {
180 Self::load_from(Path::new("folk.toml"))
181 }
182
183 pub fn load_from(path: impl AsRef<Path>) -> Result<Self> {
185 let path = path.as_ref();
186 let mut fig = Figment::from(figment::providers::Serialized::defaults(Self::default()));
187 if path.exists() {
188 fig = fig.merge(Toml::file(path));
189 }
190 fig = fig.merge(Env::prefixed("FOLK_").split("_"));
191 fig.extract().context("failed to parse Folk configuration")
192 }
193}