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_concurrent_per_worker: usize,
60 pub max_jobs: u64,
62 #[serde(with = "humantime_serde")]
64 pub ttl: Duration,
65 #[serde(with = "humantime_serde")]
67 pub exec_timeout: Duration,
68 #[serde(with = "humantime_serde")]
70 pub boot_timeout: Duration,
71 pub warmup: bool,
74}
75
76impl Default for WorkersConfig {
77 fn default() -> Self {
78 Self {
79 script: "vendor/bin/folk-worker".into(),
80 php: "php".into(),
81 count: 4,
82 max_concurrent_per_worker: 1,
83 max_jobs: 1000,
84 ttl: Duration::from_secs(3600),
85 exec_timeout: Duration::from_secs(30),
86 boot_timeout: Duration::from_secs(30),
87 warmup: true,
88 }
89 }
90}
91
92impl WorkersConfig {
93 pub fn normalize(&mut self) {
96 if self.max_concurrent_per_worker > 1 {
97 tracing::warn!(
98 requested = self.max_concurrent_per_worker,
99 "max_concurrent_per_worker > 1 is not yet supported; clamping to 1 \
100 (per-worker concurrency requires a future async runtime)"
101 );
102 self.max_concurrent_per_worker = 1;
103 } else if self.max_concurrent_per_worker == 0 {
104 self.max_concurrent_per_worker = 1;
105 }
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(default)]
111pub struct LogConfig {
112 pub filter: String,
114 pub format: LogFormat,
116 #[serde(default)]
120 pub plugins: HashMap<String, String>,
121}
122
123impl Default for LogConfig {
124 fn default() -> Self {
125 Self {
126 filter: "info".into(),
127 format: LogFormat::Text,
128 plugins: HashMap::new(),
129 }
130 }
131}
132
133impl LogConfig {
134 pub fn effective_filter(&self) -> String {
138 if self.plugins.is_empty() {
139 return self.filter.clone();
140 }
141
142 let mut parts = vec![self.filter.clone()];
143 for (plugin, level) in &self.plugins {
144 let target = plugin_name_to_target(plugin);
145 parts.push(format!("{target}={level}"));
146 }
147 parts.join(",")
148 }
149}
150
151fn plugin_name_to_target(name: &str) -> &str {
152 match name {
153 "http" => "folk_plugin_http",
154 "jobs" => "folk_plugin_jobs",
155 "grpc" => "folk_plugin_grpc",
156 "metrics" => "folk_plugin_metrics",
157 "process" => "folk_plugin_process",
158 "core" => "folk_core",
159 "ext" => "folk_ext",
160 _ => name,
161 }
162}
163
164#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
165#[serde(rename_all = "snake_case")]
166pub enum LogFormat {
167 Text,
168 Json,
169 Pretty,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
176#[serde(default)]
177pub struct DevConfig {
178 pub watch: bool,
180 pub watch_paths: Vec<String>,
182 pub watch_extensions: Vec<String>,
184 #[serde(with = "humantime_serde")]
186 pub debounce: Duration,
187}
188
189impl Default for DevConfig {
190 fn default() -> Self {
191 Self {
192 watch: false,
193 watch_paths: vec!["app".into(), "src".into(), "routes".into(), "config".into()],
194 watch_extensions: vec!["php".into()],
195 debounce: Duration::from_millis(300),
196 }
197 }
198}
199
200impl FolkConfig {
201 pub fn load() -> Result<Self> {
204 Self::load_from(Path::new("folk.toml"))
205 }
206
207 pub fn load_from(path: impl AsRef<Path>) -> Result<Self> {
209 let path = path.as_ref();
210 let mut fig = Figment::from(figment::providers::Serialized::defaults(Self::default()));
211 if path.exists() {
212 fig = fig.merge(Toml::file(path));
213 }
214 fig = fig.merge(Env::prefixed("FOLK_").split("_"));
215 fig.extract().context("failed to parse Folk configuration")
216 }
217}